diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..fe97d6e6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Generated code is compared byte for byte (snapshots, include_str! runtime +# templates), so text files are LF on every platform. +* text=auto eol=lf +*.bat text eol=crlf +*.ps1 text eol=crlf diff --git a/.github/actions/toolchains/action.yml b/.github/actions/toolchains/action.yml new file mode 100644 index 00000000..efae43f1 --- /dev/null +++ b/.github/actions/toolchains/action.yml @@ -0,0 +1,59 @@ +name: Language toolchains +description: Install the consumer toolchain one WeaveFFI target needs (conformance and fixture checks). +inputs: + lang: + description: Target language (c, cpp, python, ruby, dart, go, swift, dotnet, node, kotlin, wasm) + required: true +runs: + using: composite + steps: + - if: inputs.lang == 'node' || inputs.lang == 'wasm' + uses: actions/setup-node@v4 + with: + node-version: '22' + - if: inputs.lang == 'python' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - if: inputs.lang == 'ruby' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + - if: inputs.lang == 'ruby' + shell: bash + run: gem install ffi + # dart-lang/setup-dart can't run inside a composite action (it registers + # a problem matcher relative to the calling action), so fetch the SDK. + - if: inputs.lang == 'dart' + shell: bash + run: | + os=linux; [ "$RUNNER_OS" = macOS ] && os=macos + arch=x64; case "$(uname -m)" in arm64|aarch64) arch=arm64 ;; esac + curl -fsSL -o "$RUNNER_TEMP/dart.zip" \ + "https://storage.googleapis.com/dart-archive/channels/stable/release/latest/sdk/dartsdk-$os-$arch-release.zip" + unzip -q -o "$RUNNER_TEMP/dart.zip" -d "$RUNNER_TEMP" + echo "$RUNNER_TEMP/dart-sdk/bin" >> "$GITHUB_PATH" + - if: inputs.lang == 'go' + uses: actions/setup-go@v5 + with: + go-version: '1.23' + - if: inputs.lang == 'dotnet' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + - if: inputs.lang == 'swift' && runner.os == 'Linux' + uses: swift-actions/setup-swift@v2 + with: + swift-version: '6.0' + - if: inputs.lang == 'kotlin' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - if: inputs.lang == 'kotlin' + uses: fwilhe2/setup-kotlin@main + with: + version: '2.0.21' + - if: (inputs.lang == 'c' || inputs.lang == 'cpp') && runner.os == 'Linux' + shell: bash + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends clang diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 5cdbd857..1a96aea3 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -1,9 +1,9 @@ name: Bench on: - push: - branches: [ main ] - pull_request: + workflow_dispatch: + schedule: + - cron: '0 9 * * 1' jobs: bench: @@ -31,14 +31,14 @@ jobs: if: github.event_name == 'pull_request' run: | set -euo pipefail - cargo bench -p weaveffi-core --bench codegen_bench -- --test + cargo bench -p weaveffi-gen --bench codegen_bench -- --test cargo bench -p weaveffi-cli --bench generate_bench -- --test # Full sampled run on main; the uploaded artifact is the historical # record of the numbers. - name: Run cargo bench if: github.event_name == 'push' - # The two criterion harnesses (`weaveffi-core` and `weaveffi-cli`) + # The two criterion harnesses (`weaveffi-gen` and `weaveffi-cli`) # accept `--noplot`; the workspace `cargo bench --workspace` flag # also fans out to the default `libtest` harness in every other # crate, which rejects `--noplot`, so we invoke each criterion @@ -48,7 +48,7 @@ jobs: run: | set -euo pipefail { - cargo bench -p weaveffi-core --bench codegen_bench -- --noplot + cargo bench -p weaveffi-gen --bench codegen_bench -- --noplot cargo bench -p weaveffi-cli --bench generate_bench -- --noplot } 2>&1 | tee bench.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a029dad..7e7fa8bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,319 +2,170 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CARGO_TERM_COLOR: always + CARGO_PROFILE_DEV_DEBUG: line-tables-only + jobs: - build-test: - name: Build and test (${{ matrix.os }}) + lint: + name: Format, clippy, and rustdoc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all --check + - run: cargo clippy --workspace --all-targets -- -D warnings + - name: rustdoc + env: + RUSTDOCFLAGS: -D warnings -D rustdoc::all -D rustdoc::missing_crate_level_docs + run: cargo doc --workspace --no-deps + - name: weaveffi-model builds without the IDL formats (as the proc macro uses it) + run: cargo check -p weaveffi-model --no-default-features + + test: + name: Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 with: - cache-targets: true + tool: cargo-insta + # One pass runs every test and fails on any snapshot drift. + - run: cargo insta test --workspace --check + - name: Benchmarks compile + if: runner.os == 'Linux' + run: cargo bench --workspace --no-run - - name: Set up Node.js - uses: actions/setup-node@v4 + generated-drift: + name: Samples, schema, and wasm build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - node-version: '22' - - - name: Check formatting - run: cargo fmt --all --check - - - name: Build workspace (debug) - run: cargo build --workspace --all-targets - - - name: Clippy - run: cargo clippy --workspace --all-targets -- -D warnings - - - name: Run unit tests - run: cargo test --workspace --all-features --no-fail-fast - - - name: Install cargo-insta - run: cargo install --locked cargo-insta - - - name: Verify generator snapshots - run: cargo insta test --workspace --check - - - name: Build CLI (release) - run: cargo build -p weaveffi-cli --release - - - name: Generate bindings from calculator source - shell: bash - run: | - BIN=target/release/weaveffi - [ "${{ runner.os }}" = "Windows" ] && BIN="${BIN}.exe" - "$BIN" generate samples/calculator/src/lib.rs -o generated - - - name: Build sample producer cdylibs (debug) - run: cargo build -p calculator -p contacts -p kvstore -p events -p codec - - # `weaveffi diff --check` is the CI gate we recommend to users, so it has - # to hold for our own samples: a fresh tree diffs clean (exit 0), and a - # tree with a stale file is reported (exit 1) instead of silently passing. - - name: Verify `weaveffi diff --check` on every sample - shell: bash + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - run: cargo build -p weaveffi-cli + # `weaveffi diff --check` is the CI gate we recommend to users, so it + # must hold for our own samples: a fresh tree diffs clean, and a tree + # with a stale file is reported instead of silently passing. + - name: weaveffi diff --check on every sample run: | set -euo pipefail - BIN=target/release/weaveffi - [ "${{ runner.os }}" = "Windows" ] && BIN="${BIN}.exe" - for sample in calculator contacts events kvstore shapes async-demo codec inventory; do - "$BIN" generate "samples/$sample/src/lib.rs" -o "diffcheck/$sample" --force --quiet + BIN=target/debug/weaveffi + for sample in calculator contacts events kvstore async-demo codec; do + "$BIN" generate "samples/$sample/src/lib.rs" -o "diffcheck/$sample" --quiet "$BIN" diff "samples/$sample/src/lib.rs" -o "diffcheck/$sample" --check --quiet done - echo "stale" >> diffcheck/calculator/c/weaveffi.h + echo "stale" >> diffcheck/calculator/c/calculator.h if "$BIN" diff samples/calculator/src/lib.rs -o diffcheck/calculator --check --quiet; then - echo "diff --check did not detect a stale file" >&2 + echo "diff --check did not detect a modified file" >&2 exit 1 fi - - - name: Add wasm target - if: runner.os != 'Windows' - run: rustup target add wasm32-unknown-unknown - - - name: Build calculator wasm (release) - if: runner.os != 'Windows' - run: cargo build -p calculator --target wasm32-unknown-unknown --release - - - name: Verify weaveffi.schema.json is up to date - if: runner.os == 'Linux' + - name: weaveffi.schema.json is up to date run: | - target/release/weaveffi schema --format json-schema > weaveffi.schema.json.regenerated + target/debug/weaveffi schema > weaveffi.schema.json.regenerated diff -u weaveffi.schema.json weaveffi.schema.json.regenerated - - - name: Upload CLI artifact - uses: actions/upload-artifact@v4 - if: runner.os != 'Windows' - with: - name: weaveffi-cli-${{ runner.os }} - path: target/release/weaveffi - if-no-files-found: error - retention-days: 7 - - - name: Upload CLI artifact (Windows) - uses: actions/upload-artifact@v4 - if: runner.os == 'Windows' + - name: Every sample builds for wasm32 + run: cargo build --target wasm32-unknown-unknown -p calculator -p contacts -p events -p kvstore -p async-demo -p codec + + fixtures: + # Snapshot tests pin the generated text; this compiles or type-checks it + # with each language's own toolchain, for every fixture, so invalid code + # in shapes no sample exercises still fails CI. + name: Fixture compile check (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - { target: c, os: ubuntu-latest } + - { target: cpp, os: ubuntu-latest } + - { target: swift, os: macos-latest } + - { target: kotlin, os: ubuntu-latest } + - { target: node, os: ubuntu-latest } + - { target: wasm, os: ubuntu-latest } + - { target: python, os: ubuntu-latest } + - { target: dotnet, os: ubuntu-latest } + - { target: dart, os: ubuntu-latest } + - { target: go, os: ubuntu-latest } + - { target: ruby, os: ubuntu-latest } + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: ./.github/actions/toolchains with: - name: weaveffi-cli-Windows - path: target/release/weaveffi.exe - if-no-files-found: error - retention-days: 7 + lang: ${{ matrix.target }} + - run: bash scripts/check-fixtures.sh ${{ matrix.target }} conformance: - # Correctness gate: every consumer binds through the *generated* wrappers and - # asserts concrete results, so a backend that lowers a type wrong, drifts a - # symbol, or skips a feature fails here. Covers contacts, events, kvstore, - # shapes, async-demo, and codec across C, C++, Python, Ruby, Dart, Go, - # Swift, .NET, Node, Kotlin, and Wasm: the events lanes exercise a callback - # interface plus objects and iterators, the kvstore lanes exercise objects - # in every position (constructors, methods, async methods, iterator methods, - # records and lists carrying objects, clone/destroy), typed error domains, - # builders, and an eviction callback interface, and the codec lanes - # round-trip every value-buffer wire shape; see conformance/run.sh. - name: Conformance harness - runs-on: ubuntu-latest - # A healthy run takes about five minutes. Without a cap, a consumer that - # deadlocks (as the Swift events lane did under Swift 5.10, see below) - # holds the job for the six-hour default before it is marked failed. - timeout-minutes: 45 + # Behavioral gate: every consumer binds through the *generated* bindings + # and asserts concrete results, including leak counters returning to zero. + name: Conformance (${{ matrix.lang }}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + lang: [c, cpp, python, ruby, dart, go, swift, dotnet, node, kotlin, wasm] + os: [ubuntu-latest, macos-latest] steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Add wasm target - run: rustup target add wasm32-unknown-unknown - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - with: - cache-targets: true - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - - - name: Set up .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Set up Dart - uses: dart-lang/setup-dart@v1 - - # Swift 5.10 on Linux miscompiles the events consumer: the top-level - # async code returns to the runtime mid-statement (an iterator - # `first(where:)` call) without ever suspending, so `dispatch_main` - # parks the process forever. Swift 6.0 and 6.1 run the same source - # to completion; the generated package stays on swift-tools 5.7. - - name: Set up Swift - uses: swift-actions/setup-swift@v2 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - swift-version: "6.0" - - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '17' - - # The generated Go bindings surface iter as iter.Seq/iter.Seq2, - # which needs Go 1.23; Ubuntu's packaged golang-go is older and can't - # fetch the newer toolchain on demand. - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.23' - - - name: Install consumer toolchains - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - clang g++ python3 ruby-ffi - sudo snap install kotlin --classic - - - name: Run conformance harness - run: bash conformance/run.sh - - windows-e2e: - name: Windows E2E - runs-on: windows-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - uses: ./.github/actions/toolchains with: - cache-targets: true - - - name: Build CLI (release) - run: cargo build -p weaveffi-cli --release - - - name: Build calculator cdylib (debug) - run: cargo build -p calculator - - - 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 - run: | - $expected = @( - "generated/c/weaveffi.h", - "generated/swift/Package.swift", - "generated/node/types.d.ts", - "generated/kotlin/build.gradle.kts", - "generated/python/calculator/weaveffi.py", - "generated/dotnet/WeaveFFI.cs", - "generated/wasm/README.md" - ) - $missing = @() - foreach ($f in $expected) { - if (!(Test-Path $f)) { $missing += $f } - } - if ($missing.Count -gt 0) { - Write-Error "Missing files: $($missing -join ', ')" - exit 1 - } - Write-Host "All expected generated files present" + lang: ${{ matrix.lang }} + - run: bash conformance/run.sh + env: + ONLY: ${{ matrix.lang }} - android-ndk: - name: Android NDK JNI + android: + # The Kotlin target's JNI shim must compile and link against a real + # producer built for Android. + name: Android (NDK link) runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - cache-targets: true - - - name: Install Android NDK - id: setup-ndk + targets: aarch64-linux-android + - uses: Swatinem/rust-cache@v2 + - id: ndk uses: nttld/setup-ndk@v1 with: ndk-version: r26d - - - name: Add aarch64-linux-android Rust target - run: rustup target add aarch64-linux-android - - - name: Build CLI (release) - run: cargo build -p weaveffi-cli --release - - - name: Generate Kotlin + C bindings for contacts - run: | - 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 kotlin - - - name: Compile JNI shim with NDK clang + - name: Build the kvstore producer for aarch64-linux-android env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - run: | - set -euxo pipefail - CLANG="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang" - if [ ! -x "$CLANG" ]; then - echo "expected NDK clang at $CLANG" >&2 - ls "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" >&2 - exit 1 - fi - "$CLANG" -shared -fPIC \ - -I generated/c \ - -Wl,--unresolved-symbols=ignore-all \ - -o libweaveffi_jni_test.so \ - generated/kotlin/src/main/cpp/weaveffi_jni.c - - - name: Assert exported JNI symbols + CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: ${{ steps.ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang + run: cargo build -p kvstore --target aarch64-linux-android --release + - name: Generate and link the JNI shim against it env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + NDK: ${{ steps.ndk.outputs.ndk-path }} run: | set -euxo pipefail - NM="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm" - file libweaveffi_jni_test.so - ldd libweaveffi_jni_test.so || true - "$NM" -D --defined-only libweaveffi_jni_test.so > nm.out - # The Kotlin generator lowers each interface to its own Kotlin - # class with `native*` externals, so the JNI exports are namespaced - # per type: the ContactBook interface's constructor, methods, and - # destroy hook. Records (like Contact) are value types serialized - # into buffers, so they have no JNI surface of their own; nativeAdd, - # nativeGet, and nativeList cover the buffered record paths. - for sym in \ - Java_com_weaveffi_ContactBook_nativeNew \ - Java_com_weaveffi_ContactBook_nativeAdd \ - Java_com_weaveffi_ContactBook_nativeGet \ - Java_com_weaveffi_ContactBook_nativeList \ - Java_com_weaveffi_ContactBook_nativeCount \ - Java_com_weaveffi_ContactBook_nativeDestroy; do - if ! grep -q " T $sym\$" nm.out; then - echo "missing exported JNI symbol: $sym" >&2 - cat nm.out >&2 - exit 1 - fi - done - echo "All expected JNI symbols are exported." + cargo run -q -p weaveffi-cli -- generate samples/kvstore/src/lib.rs -o generated --target c,kotlin + CLANG="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang" + shim=$(ls generated/kotlin/src/main/cpp/*.c | head -1) + "$CLANG" -shared -fPIC -Werror -I generated/kotlin/src/main/cpp -I generated/c "$shim" \ + -L target/aarch64-linux-android/release -lkvstore -o libkvstore_jni.so + "$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf" -d libkvstore_jni.so | grep 'libkvstore.so' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 66440a9e..9d3017e1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -45,6 +45,7 @@ jobs: cp -r docs/book/html/* site/ mkdir -p site/api/rust cp -r target/doc/* site/api/rust/ + cp weaveffi.schema.json site/ touch site/.nojekyll printf "weaveffi.com" > site/CNAME diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 6876f166..65711754 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -1,10 +1,9 @@ name: Fuzz on: - pull_request: + workflow_dispatch: schedule: - # 08:00 UTC daily. The nightly run can be extended by bumping - # `-max_total_time` below. + # 08:00 UTC daily. Extend a run by bumping `-max_total_time` below. - cron: '0 8 * * *' jobs: @@ -32,8 +31,9 @@ jobs: with: cache-targets: true - - name: Install cargo-fuzz - run: cargo install cargo-fuzz --locked + - uses: taiki-e/install-action@v2 + with: + tool: cargo-fuzz - name: Run ${{ matrix.target }} for 60s # Each target is fuzzed for 60s in PRs (≈5 min total). The cron run diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index eeb0637e..5f53505a 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -2,145 +2,48 @@ name: Quality on: push: - branches: [ main ] + branches: [main] pull_request: jobs: - deny: - name: cargo-deny + supply-chain: + name: cargo-deny, cargo-audit, cargo-machete runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - - name: Install cargo-deny - run: cargo install cargo-deny --locked - - - name: cargo deny check - # `cargo deny check` exits non-zero only when a check produces errors, - # so advisory warnings (yanked, unmaintained) are surfaced but do not - # block the build. - run: cargo deny check - - audit: - name: cargo-audit - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - - name: Install cargo-audit - run: cargo install cargo-audit --locked - - - name: cargo audit - # Ignored advisories: - # RUSTSEC-2023-0057 / RUSTSEC-2023-0058 apply to the crates.io - # `inventory` crate; our `samples/inventory` shares the name but - # is an unrelated, unpublished sample cdylib. - run: | - cargo audit --deny warnings \ - --ignore RUSTSEC-2023-0057 \ - --ignore RUSTSEC-2023-0058 - - machete: - name: cargo-machete - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - - name: Install cargo-machete - run: cargo install cargo-machete --locked - - - name: cargo machete - run: cargo machete --with-metadata - - doc: - name: rustdoc - runs-on: ubuntu-latest - env: - RUSTDOCFLAGS: "-D rustdoc::all -D rustdoc::missing_crate_level_docs" - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - - name: cargo doc - run: cargo doc --workspace --no-deps + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@v2 + with: + tool: cargo-deny,cargo-audit,cargo-machete + - run: cargo deny check + - run: cargo audit --deny warnings + - run: cargo machete --with-metadata coverage: - name: coverage + name: Coverage runs-on: ubuntu-latest - # Threshold is informational while we establish a baseline. continue-on-error: true steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - - name: Install cargo-llvm-cov - run: cargo install cargo-llvm-cov --locked - - - name: Generate lcov coverage - run: cargo llvm-cov --workspace --lcov --output-path lcov.info - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: + tool: cargo-llvm-cov + - run: cargo llvm-cov --workspace --lcov --output-path lcov.info + - uses: codecov/codecov-action@v4 with: files: lcov.info fail_ci_if_error: false docs-linkcheck: - name: docs link check + name: Docs link check runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - - name: Install mdbook + mdbook-linkcheck2 - # Use the maintained `mdbook-linkcheck2` fork rather than the - # original `mdbook-linkcheck` (last release Oct 2022), which fails - # to deserialize mdBook 0.5+ render contexts ("missing field - # `sections`"). - run: | - cargo install mdbook --locked - cargo install mdbook-linkcheck2 --locked - - - name: Validate docs links - run: scripts/check-links.sh + - uses: actions/checkout@v4 + - uses: taiki-e/install-action@v2 + with: + tool: mdbook,mdbook-linkcheck2 + - run: scripts/check-links.sh diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml new file mode 100644 index 00000000..00ae79b1 --- /dev/null +++ b/.github/workflows/release-binaries.yml @@ -0,0 +1,57 @@ +name: Release binaries + +# Builds the `weaveffi` CLI for every supported host and attaches the +# archives to the GitHub release (the same names `cargo binstall weaveffi-cli` +# resolves through the crate's `[package.metadata.binstall]`). +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: Release tag to attach binaries to (for example v0.24.0) + required: true + +permissions: + contents: write + +jobs: + build: + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest } + - { target: aarch64-unknown-linux-gnu, os: ubuntu-24.04-arm } + - { target: x86_64-apple-darwin, os: macos-13 } + - { target: aarch64-apple-darwin, os: macos-latest } + - { target: x86_64-pc-windows-msvc, os: windows-latest } + env: + TAG: ${{ github.event.release.tag_name || inputs.tag }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG }} + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - run: cargo build -p weaveffi-cli --release --locked --target ${{ matrix.target }} + - name: Package and upload + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + name="weaveffi-${{ matrix.target }}" + mkdir "$name" + if [ "${{ runner.os }}" = "Windows" ]; then + cp "target/${{ matrix.target }}/release/weaveffi.exe" README.md LICENSE-MIT LICENSE-APACHE "$name/" + 7z a "$name.zip" "$name" > /dev/null + gh release upload "$TAG" "$name.zip" --clobber + else + cp "target/${{ matrix.target }}/release/weaveffi" README.md LICENSE-MIT LICENSE-APACHE "$name/" + tar czf "$name.tar.gz" "$name" + gh release upload "$TAG" "$name.tar.gz" --clobber + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c9cd130..3392cdcd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,41 +5,53 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: write + pull-requests: write + jobs: + # Opens or updates the release PR whenever releasable commits land. + release-pr: + name: Release PR + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'weavefoundry' }} + concurrency: + group: release-pr + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - uses: release-plz/action@v0.5 + with: + command: release-pr + env: + # A token other than GITHUB_TOKEN, so CI runs on the release PR + # (GitHub doesn't trigger workflows from GITHUB_TOKEN events). + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + # Publishes when the merged release PR bumped the version (a no-op + # otherwise). The release PR passed CI before it could merge, so nothing + # unreviewed or red is ever published. release: - name: Semantic Release + name: Publish runs-on: ubuntu-latest if: ${{ github.repository_owner == 'weavefoundry' }} concurrency: group: release cancel-in-progress: false - permissions: - contents: write - id-token: write - steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Setup Node - uses: actions/setup-node@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: release-plz/action@v0.5 with: - node-version: 22 - cache: npm - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo registry - uses: Swatinem/rust-cache@v2 - - - name: Install npm dependencies - run: npm ci - - - name: Semantic Release - run: npx semantic-release + command: release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # A token other than GITHUB_TOKEN, so the GitHub release it creates + # triggers release-binaries.yml. + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.gitignore b/.gitignore index 38368826..80a4628c 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ examples/dotnet/Contacts/obj/ crates/weaveffi-fuzz/fuzz/artifacts/ crates/weaveffi-fuzz/fuzz/corpus/ crates/weaveffi-fuzz/fuzz/coverage/ + +# Bindings generated inside a sample by `weaveffi generate` +samples/*/bindings/ diff --git a/.releaserc.json b/.releaserc.json deleted file mode 100644 index ef978234..00000000 --- a/.releaserc.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "branches": ["main"], - "tagFormat": "v${version}", - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { "type": "feat", "release": "minor" }, - { "type": "fix", "release": "patch" }, - { "type": "perf", "release": "patch" }, - { "breaking": true, "release": "minor" } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { "type": "feat", "section": "Features" }, - { "type": "fix", "section": "Bug Fixes" }, - { "type": "perf", "section": "Performance" }, - { "type": "revert", "section": "Reverts" }, - { "type": "refactor", "section": "Code Refactoring", "hidden": true }, - { "type": "docs", "section": "Documentation", "hidden": true }, - { "type": "style", "section": "Styles", "hidden": true }, - { "type": "chore", "section": "Miscellaneous", "hidden": true }, - { "type": "ci", "section": "CI", "hidden": true }, - { "type": "build", "section": "Build", "hidden": true }, - { "type": "test", "section": "Tests", "hidden": true } - ] - } - } - ], - "@semantic-release/changelog", - [ - "@semantic-release/exec", - { - "prepareCmd": "bash scripts/update-cargo-versions.sh ${nextRelease.version}" - } - ], - [ - "@semantic-release/git", - { - "assets": ["CHANGELOG.md", "crates/*/Cargo.toml", "Cargo.lock"], - "message": "chore(release): v${nextRelease.version}" - } - ], - "@semantic-release/github", - [ - "@semantic-release/exec", - { - "publishCmd": "bash scripts/publish-crates.sh" - } - ] - ] -} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 556fdee9..924c7e59 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,39 +1,34 @@ # WeaveFFI Architecture -The canonical architecture reference lives in -[`docs/src/architecture.md`](docs/src/architecture.md). +The contributor guide to the architecture lives in +[`docs/src/architecture.md`](docs/src/architecture.md), and the normative C +ABI is [`docs/src/reference/abi.md`](docs/src/reference/abi.md). Read them +before changing a generator, the IDL or IR, validation, the producer macro, +the runtime, or the orchestrator. -Start there if you are: - -- Adding or changing a generator. -- Touching the IDL, IR, validator, or schema version. -- Changing generator configuration, output determinism, or cache behavior. -- Reviewing snapshot-test output. - -At a high level, WeaveFFI follows this pipeline: +The pipeline: ```text -IDL (YAML/JSON/TOML) - → Parse - → IR - → Validate - → Resolve generator config - → Generate target outputs - → Write files and per-generator cache entries +annotated Rust (.rs) or IDL (YAML/JSON/TOML) + → IR (Api) + → validate (every rule, including the C symbol table) + → ResolvedApi + Identity (names from weaveffi.toml / Cargo.toml) + → BindingModel (C symbols and ABI signatures, computed once) + → marshalling plan + → Target::render (pure, per language) + → Orchestrator (capability gate, cache records, changed-file writes, + stale-file removal) ``` -The workspace is split into small crates: - -- `weaveffi-ir` owns the IR and parsers. -- `weaveffi-abi` owns the stable C ABI runtime. -- `weaveffi-core` owns validation, the ABI/binding model, the marshalling - plan (the language-neutral calling contracts every backend renders), the - language-backend framework, generator orchestration, configuration, and - caching. -- `weaveffi-gen-*` crates own target-specific code generation, each implemented - as a `LanguageBackend` over the shared driver. -- `weaveffi-cli` wires the pipeline into the `weaveffi` command. -- `weaveffi-fuzz` contains unpublished fuzz harnesses. - -See the full architecture guide for the dependency graph, data-flow diagram, -cache-key strategy, generator responsibilities, and snapshot-test layout. +The workspace crates: + +- `weaveffi-model`: the IR, IDL parsing, Rust extraction, validation, the + resolved view, package identity, the binding model, ABI lowering, the + marshalling plan, and contract checksums. +- `weaveffi-gen`: the backend framework, the orchestrator and cache, and the + eleven language targets under `targets/`. +- `weaveffi-cli`: the `weaveffi` command. +- `weaveffi-abi`: the C ABI runtime linked into every producer. +- `weaveffi-macros`: `#[weaveffi::module]` and `export_runtime!`. +- `weaveffi`: the producer facade crate. +- `weaveffi-fuzz`: unpublished fuzz harnesses. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e8ec257..c2677c84 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,9 @@ ## Development environment -1. Install the [Rust toolchain](https://rustup.rs/) (stable channel). +1. Install the [Rust toolchain](https://rustup.rs/) (stable channel), + [`just`](https://github.com/casey/just), and `cargo-insta` + (`cargo install just cargo-insta --locked`). 2. Clone the repository: ```bash @@ -19,17 +21,9 @@ cargo build --workspace 4. Run all tests: ```bash -cargo test --workspace +just test # or: cargo insta test --workspace --check ``` -5. (Optional) Preview the documentation locally: - -```bash -cargo install mdbook -mdbook serve docs -p 3000 -n 127.0.0.1 -``` - -Open . ## Claiming an issue @@ -41,97 +35,85 @@ To avoid duplicate work, claim an issue before you start on it: Unsolicited pull requests for issues that are already assigned or already have an open pull request will be closed as duplicates, even if the work is good. -## Documentation - -WeaveFFI has two layers of documentation, and CI checks both: +## Everyday workflow -- **Prose docs** (this book) live under `docs/src/` and build with - [mdBook](https://rust-lang.github.io/mdBook/). New generators must add a - `docs/src/generators/.md` page and link it from - `docs/src/SUMMARY.md`. -- **API docs** are generated from Rust doc comments by `cargo doc`. Every - public item in the library crates carries a doc comment; this is enforced - by `#![deny(missing_docs)]` plus the Clippy doc lints - (`missing_errors_doc`, `missing_panics_doc`, `missing_safety_doc`, and - `doc_markdown`). See the - [doc comment style guide](docs/src/api/doc-style.md) for the conventions, - the standard `# Errors`/`# Panics`/`# Safety` sections, and examples. - -Check both locally before pushing: +The `justfile` wraps the commands CI runs: ```bash -# Lint everything; clippy -D warnings also enforces the doc lints. -just check - -# Build the API docs (rustdoc) and the book. -just doc +just check # cargo fmt --check, clippy -D warnings (with the doc lints), rustdoc -D warnings +just test # cargo insta test --workspace --check: every test, failing on snapshot drift +just snapshots # accept snapshot changes after reviewing them +just fixtures [targets...] # compile-check generated fixtures with each language's toolchain +just conformance # end-to-end conformance harness (ONLY=python just conformance) +just docs # build the mdBook ``` -## Running specific tests +Run `just check` and `just test` before every push. Changes to a generator +should also pass that target's fixture check and conformance lanes. + +### Snapshot tests + +Snapshot tests (`crates/weaveffi-cli/tests/snapshots.rs`) pin the exact +output of every generator for every fixture in +`crates/weaveffi-cli/tests/fixtures/`, using +[`cargo-insta`](https://insta.rs/). When output changes on purpose: ```bash -cargo test -p weaveffi-core -cargo test -p weaveffi-ir +cargo install cargo-insta --locked +cargo insta test --workspace # writes .snap.new files for changed output +cargo insta review # a accepts, r rejects, s skips ``` -## Adding a new generator +Review every diff. Commit accepted `.snap` files in the same commit as the +code that produced them, and never commit `.snap.new` files; CI rejects +pending snapshots. + +### Fixture compile checks + +Snapshots prove the text didn't change, not that it compiles. +`scripts/check-fixtures.sh ` generates every fixture and compiles or +type-checks it with the target language's toolchain (through +`scripts/fixtures/.sh`). Run it for any target whose output you +change; CI runs one job per target. -Each target language is implemented as its own crate (`weaveffi-gen-`) -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: - -1. Create `crates/weaveffi-gen-/` following the layout of an existing - generator (e.g. `weaveffi-gen-c`). Add it to the workspace `members` list - in the root `Cargo.toml` and depend on `weaveffi-core` and `weaveffi-ir`. -2. Implement `weaveffi_core::backend::LanguageBackend`: define the - 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`. 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`. - -## Snapshot tests - -Snapshot tests are the primary defense against generator regressions. They -live in `crates/weaveffi-cli/tests/snapshots.rs` and write -one-file-per-snapshot artifacts under `crates/weaveffi-cli/tests/snapshots/` -using [`cargo-insta`](https://insta.rs/). - -Workflow when output changes intentionally: +### Conformance + +`conformance/run.sh` builds every sample producer with leak counters on, +generates bindings, and runs real consumers in every language against them. +Each consumer must exit 0 with every leak counter at zero. ```bash -cargo install cargo-insta --locked -cargo test -p weaveffi-cli --test snapshots -cargo insta review +ONLY=python bash conformance/run.sh # one language +ONLY=c,go-kvstore bash conformance/run.sh # a language plus one lane +SKIP=wasm SKIP_GEN=1 bash conformance/run.sh # skip lanes; reuse generated bindings ``` -`cargo insta review` opens an interactive TUI showing the `.snap.new` diff -for each pending snapshot. Inspect every diff carefully: +`LANE_TIMEOUT` sets the per-lane limit in seconds (default 300). A missing +toolchain fails its lanes; install it or skip them. -- Press `a` (or run `cargo insta accept`) to promote `.snap.new` files into - their final `.snap` form when the change is correct. -- Press `r` (or run `cargo insta reject`) to delete the `.snap.new` files - when the diff exposes a bug; fix the generator before re-running. -- Press `s` to skip a snapshot and decide later. +## Documentation + +- **Prose docs** live under `docs/src/` and build with + [mdBook](https://rust-lang.github.io/mdBook/). Follow `AGENTS.md` for + style. `scripts/check-links.sh` (needs `mdbook` and `mdbook-linkcheck2`) + fails on broken links, and CI runs it. +- **API docs** come from Rust doc comments. Every public item in the library + crates has one, enforced by `#![deny(missing_docs)]` and the Clippy doc + lints (`missing_errors_doc`, `missing_panics_doc`, `missing_safety_doc`, + `doc_markdown`). See the + [doc comment style guide](docs/src/api/doc-style.md). + +Preview the book with `mdbook serve docs -p 3000 -n 127.0.0.1`. + +## Adding a new generator -After accepting, **commit the resulting `.snap` files in the same commit as -the code change that produced them** so reviewers can see the generator diff -alongside the implementation diff. Never commit `.snap.new` files; CI rejects -them. +Read the [architecture guide](docs/src/architecture.md) first; its "Adding a +generator" section is the checklist. In short: add +`crates/weaveffi-gen/src/targets//` implementing `LanguageBackend`, +register it with one line in the `cli_targets!` registry in +`crates/weaveffi-cli/src/config.rs`, add it to the snapshot tests, add +`scripts/fixtures/.sh` and `conformance//`, and document it under +`docs/src/generators/`. ## Fuzzing @@ -186,9 +168,9 @@ When libFuzzer finds an input that panics or aborts it writes the bytes to crates/weaveffi-fuzz/fuzz/artifacts//crash- ``` -3. Convert the minimized input into a regression test in the affected crate - (`weaveffi-ir` for parser crashes, `weaveffi-core` for validator crashes) - **before** fixing the bug, so the failure is locked in and cannot regress. +3. Convert the minimized input into a regression test in `weaveffi-model` + (which owns the parsers and the validator) **before** fixing the bug, so + the failure is locked in and can't regress. ## Commit conventions @@ -206,22 +188,22 @@ Use the form: Subject rules: -- Imperative mood, no trailing period, ≤ 72 characters -- UTF‑8 allowed; avoid emoji in the subject +- Imperative mood, no trailing period, 72 characters or fewer +- UTF-8 allowed; avoid emoji in the subject Accepted types: -- `build` – build system or external dependencies (e.g., package.json, tooling) -- `chore` – maintenance (no app behavior change) -- `ci` – continuous integration configuration (workflows, pipelines) -- `docs` – documentation only -- `feat` – user-facing feature or capability -- `fix` – bug fix -- `perf` – performance improvements -- `refactor` – code change that neither fixes a bug nor adds a feature -- `revert` – revert of a previous commit -- `style` – formatting/whitespace (no code behavior) -- `test` – add/adjust tests only +- `build`: build system or external dependencies (e.g., package.json, tooling) +- `chore`: maintenance (no app behavior change) +- `ci`: continuous integration configuration (workflows, pipelines) +- `docs`: documentation only +- `feat`: user-facing feature or capability +- `fix`: bug fix +- `perf`: performance improvements +- `refactor`: code change that neither fixes a bug nor adds a feature +- `revert`: revert of a previous commit +- `style`: formatting/whitespace (no code behavior) +- `test`: add/adjust tests only Examples: @@ -250,17 +232,21 @@ BREAKING CHANGE: JS bindings now return Promises instead of using callbacks; upd ## Versioning and releases -- All crates are versioned in lockstep. Versions are tracked in each `crates/*/Cargo.toml` and updated automatically by [semantic-release](https://semantic-release.gitbook.io/) via `scripts/update-cargo-versions.sh`. -- **Automated release pipeline** (on every merge to `main`): - 1. `semantic-release` scans Conventional Commit messages since the last tag. - 2. It determines the next SemVer bump: `feat` → **minor**, `fix`/`perf` → **patch**, `BREAKING CHANGE` → **minor** (while version < 1.0; see note below). - 3. `CHANGELOG.md` is generated, `Cargo.toml` versions are updated, and a tagged release commit (`chore(release): vX.Y.Z`) is pushed. - 4. All publishable crates are published to [crates.io](https://crates.io) in dependency order. - 5. A GitHub Release is created with auto-generated release notes. -- Commit types that trigger a release: `feat` (minor), `fix` and `perf` (patch), `BREAKING CHANGE` (minor while pre-1.0). All other types (`build`, `chore`, `ci`, `docs`, `refactor`, `revert`, `style`, `test`) are recorded in the changelog but do **not** trigger a release on their own. -- **Pre-1.0 breaking changes**: The `{ "breaking": true, "release": "minor" }` rule in `.releaserc.json` caps breaking changes to a minor bump. When the project is ready for 1.0.0, remove that rule so breaking changes bump major as normal. -- Tag format: `v`-prefixed (e.g., `v0.1.0`). -- Manual version bumps are no longer needed; just merge PRs with valid Conventional Commit titles. For ad-hoc runs, use the workflow's **Run workflow** button (`workflow_dispatch`). +- Every published crate shares one version, set once in the root + `Cargo.toml` (`[workspace.package]`). +- Releases are automated by [release-plz](https://release-plz.dev/) + (`release-plz.toml`, `.github/workflows/release.yml`). On every push to + `main` it opens or updates a release PR that bumps the version from the + Conventional Commits since the last release and updates `CHANGELOG.md`. + CI runs on that PR like any other. +- Merging the release PR publishes every crate to crates.io, tags + `v{version}`, and creates the GitHub release, which triggers + `.github/workflows/release-binaries.yml` to attach prebuilt `weaveffi` + binaries (the archives `cargo binstall weaveffi-cli` resolves). +- The bump follows release-plz's Conventional Commits rules for `0.x` + versions: breaking changes bump the minor version. Only `feat`, `fix`, + `perf`, and `revert` commits appear in the changelog. +- Don't bump versions or edit `CHANGELOG.md` by hand. ### Branching rules @@ -296,13 +282,21 @@ fix/android-jni-crash ## CI -- **CI** (`ci.yml`): runs `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test`, snapshot verification (`cargo insta test`), sample builds, and JSON Schema drift checks on macOS, Linux, and Windows for every push and PR. A second job runs the full conformance harness (`conformance/run.sh`): it installs all eleven consumer toolchains, builds every sample producer, generates bindings, and compiles and runs the consumer for every language lane, including the `async-demo` lanes. -- **Quality** (`quality.yml`): `cargo deny` (licenses/bans/advisories), `cargo audit`, `cargo machete` (unused dependencies), rustdoc with warnings denied, and test coverage via `cargo llvm-cov`. -- **Docs** (`docs.yml`): builds the mdBook and the rustdoc API docs and deploys them. -- **Bench** (`bench.yml`): smoke-runs the criterion benchmarks on PRs and tracks results on pushes to `main`. -- **Fuzz** (`fuzz.yml`): short fuzzing runs of the parser and validator harnesses on PRs and on a schedule. -- **PR Lint** (`pr-lint.yml`): validates the PR title against Conventional Commits format (protects squash merges) and checks individual commit messages via commitlint (protects rebase merges). -- **Release** (`release.yml`): runs on merge to `main`; computes version, generates changelog, tags, creates GitHub Release, and publishes all workspace crates to crates.io. +- **CI** (`ci.yml`): formatting, clippy, rustdoc, and a build of + `weaveffi-model` without its IDL features; the test suite with snapshot + checks on Linux, macOS, and Windows; `weaveffi diff --check` on every + sample, the JSON Schema drift check, and a `wasm32` build of every sample; + the fixture compile check per target; the conformance harness per language + on Linux and macOS; and an Android NDK link of the Kotlin JNI shim. +- **Quality** (`quality.yml`): `cargo deny`, `cargo audit`, `cargo machete`, + coverage with `cargo llvm-cov`, and the docs link check. +- **Docs** (`docs.yml`): builds and deploys the mdBook and rustdoc. +- **Bench** (`bench.yml`), **Fuzz** (`fuzz.yml`): benchmark and fuzzing + runs. +- **PR Lint** (`pr-lint.yml`): checks the PR title and commit messages + against Conventional Commits. +- **Release** (`release.yml`) and **Release binaries** + (`release-binaries.yml`): see above. ## Security diff --git a/Cargo.lock b/Cargo.lock index cc78f904..beeaed9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,9 @@ name = "camino" version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] [[package]] name = "cast" @@ -743,13 +746,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "inventory" -version = "0.1.0" -dependencies = [ - "weaveffi", -] - [[package]] name = "is-terminal" version = "0.4.17" @@ -816,12 +812,6 @@ dependencies = [ "weaveffi", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.189" @@ -850,21 +840,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "memchr" version = "2.8.3" @@ -916,15 +891,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1366,22 +1332,6 @@ dependencies = [ "digest", ] -[[package]] -name = "shapes" -version = "0.1.0" -dependencies = [ - "weaveffi", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "2.0.1" @@ -1546,15 +1496,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - [[package]] name = "tinystr" version = "0.8.4" @@ -1656,55 +1597,6 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - [[package]] name = "trybuild" version = "1.0.121" @@ -1768,12 +1660,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "version_check" version = "0.9.5" @@ -1858,7 +1744,7 @@ name = "weaveffi" version = "0.23.0" dependencies = [ "weaveffi-abi", - "weaveffi-core", + "weaveffi-gen", "weaveffi-macros", ] @@ -1869,23 +1755,15 @@ dependencies = [ "proptest", ] -[[package]] -name = "weaveffi-bridge" -version = "0.23.0" -dependencies = [ - "syn 2.0.119", - "weaveffi-ir", -] - [[package]] name = "weaveffi-cli" version = "0.23.0" dependencies = [ + "anyhow", "assert_cmd", "camino", "clap", "clap_complete", - "criterion", "insta", "miette", "predicates", @@ -1896,40 +1774,8 @@ dependencies = [ "similar", "tempfile", "toml 0.8.23", - "tracing-subscriber", - "weaveffi-bridge", - "weaveffi-core", - "weaveffi-gen-c", - "weaveffi-gen-cpp", - "weaveffi-gen-dart", - "weaveffi-gen-dotnet", - "weaveffi-gen-go", - "weaveffi-gen-kotlin", - "weaveffi-gen-node", - "weaveffi-gen-python", - "weaveffi-gen-ruby", - "weaveffi-gen-swift", - "weaveffi-gen-wasm", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-core" -version = "0.23.0" -dependencies = [ - "anyhow", - "camino", - "criterion", - "heck", - "miette", - "rayon", - "serde", - "serde_json", - "sha2", - "tempfile", - "thiserror", - "toml 0.8.23", - "weaveffi-ir", + "weaveffi-gen", + "weaveffi-model", ] [[package]] @@ -1937,156 +1783,53 @@ name = "weaveffi-fuzz" version = "0.23.0" dependencies = [ "libfuzzer-sys", - "weaveffi-core", - "weaveffi-ir", + "weaveffi-model", ] [[package]] -name = "weaveffi-gen-c" -version = "0.23.0" -dependencies = [ - "camino", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-gen-cpp" -version = "0.23.0" -dependencies = [ - "camino", - "heck", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-gen-dart" -version = "0.23.0" -dependencies = [ - "camino", - "heck", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-gen-dotnet" -version = "0.23.0" -dependencies = [ - "camino", - "heck", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-gen-go" -version = "0.23.0" -dependencies = [ - "camino", - "heck", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-gen-kotlin" -version = "0.23.0" -dependencies = [ - "camino", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-gen-node" -version = "0.23.0" -dependencies = [ - "camino", - "heck", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-gen-python" -version = "0.23.0" -dependencies = [ - "camino", - "heck", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-gen-ruby" +name = "weaveffi-gen" version = "0.23.0" dependencies = [ + "anyhow", "camino", + "criterion", "heck", + "rayon", "serde", - "weaveffi-core", - "weaveffi-ir", + "serde_json", + "sha2", + "tempfile", + "thiserror", + "weaveffi-model", ] [[package]] -name = "weaveffi-gen-swift" +name = "weaveffi-macros" version = "0.23.0" dependencies = [ - "camino", - "heck", - "serde", - "weaveffi-core", - "weaveffi-ir", + "proc-macro2", + "quote", + "syn 2.0.119", + "trybuild", + "weaveffi", + "weaveffi-model", ] [[package]] -name = "weaveffi-gen-wasm" +name = "weaveffi-model" version = "0.23.0" dependencies = [ - "camino", "heck", - "serde", - "weaveffi-core", - "weaveffi-ir", -] - -[[package]] -name = "weaveffi-ir" -version = "0.23.0" -dependencies = [ "miette", "schemars", "serde", "serde_json", "serde_yaml", + "syn 2.0.119", "thiserror", "toml 0.8.23", ] -[[package]] -name = "weaveffi-macros" -version = "0.23.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "trybuild", - "weaveffi", - "weaveffi-bridge", - "weaveffi-core", - "weaveffi-ir", -] - [[package]] name = "web-sys" version = "0.3.105" @@ -2161,9 +1904,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ef321eed..1c6387d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,30 +1,16 @@ [workspace] members = [ - "crates/weaveffi-ir", + "crates/weaveffi-model", "crates/weaveffi-abi", - "crates/weaveffi-bridge", "crates/weaveffi-macros", "crates/weaveffi", - "crates/weaveffi-core", - "crates/weaveffi-gen-c", - "crates/weaveffi-gen-swift", - "crates/weaveffi-gen-kotlin", - "crates/weaveffi-gen-node", - "crates/weaveffi-gen-wasm", - "crates/weaveffi-gen-python", - "crates/weaveffi-gen-dotnet", - "crates/weaveffi-gen-cpp", - "crates/weaveffi-gen-dart", - "crates/weaveffi-gen-go", - "crates/weaveffi-gen-ruby", + "crates/weaveffi-gen", "crates/weaveffi-cli", "samples/calculator", "samples/contacts", - "samples/inventory", "samples/async-demo", "samples/events", "samples/kvstore", - "samples/shapes", "samples/codec", "crates/weaveffi-fuzz", ] @@ -35,6 +21,8 @@ default-members = [ ] [workspace.package] +version = "0.23.0" +rust-version = "1.85" edition = "2021" license = "MIT OR Apache-2.0" authors = ["WeaveFFI Contributors"] @@ -42,6 +30,11 @@ repository = "https://github.com/weavefoundry/weaveffi" description = "WeaveFFI: Rust core with stable C ABI and multi-language bindings" [workspace.dependencies] +weaveffi = { path = "crates/weaveffi", version = "0.23.0" } +weaveffi-abi = { path = "crates/weaveffi-abi", version = "0.23.0" } +weaveffi-macros = { path = "crates/weaveffi-macros", version = "0.23.0" } +weaveffi-model = { path = "crates/weaveffi-model", version = "0.23.0", default-features = false } +weaveffi-gen = { path = "crates/weaveffi-gen", version = "0.23.0" } serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.9" serde_json = "1.0" @@ -50,10 +43,9 @@ thiserror = "1.0" anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } clap_complete = "4.5" -camino = "1.1" +camino = { version = "1.1", features = ["serde1"] } sha2 = "0.10" heck = "0.5" -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } syn = { version = "2.0", features = ["full", "parsing", "visit"] } quote = "1.0" similar = "2.0" @@ -61,10 +53,8 @@ tempfile = "3" criterion = { version = "0.5", features = ["html_reports"] } miette = { version = "7", features = ["fancy"] } insta = { version = "1", features = ["yaml", "json"] } -notify = "6" schemars = { version = "0.8", features = ["preserve_order"] } rayon = "1.10" -clap_mangen = "0.2" [workspace.lints.rust] unsafe_code = "deny" diff --git a/README.md b/README.md index eea6e295..c4de2e91 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,44 @@ # 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.9.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 -else: no hand-written JNI, no duplicate implementations, no unsafe boilerplate. -Define your API once as an IDL in YAML, JSON, or TOML and ship idiomatic -packages for C, C++, Swift, Kotlin, Node.js, WebAssembly, Python, .NET, Dart, -Go, and Ruby that all talk to the same stable C ABI. Interfaces become real, -reference-counted objects that consumers can share, store in records and -lists, and release deterministically; callback interfaces let the native -library call back into code the consumer wrote; and error domains become typed -errors consumers can catch and match on, not flat functions and raw integer -codes. Writing your producer in Rust? Annotate a normal module with -`#[weaveffi::module]` and the macro generates both the C ABI and the IDL for -you. Every path shares one engine, so the library you build and the bindings -you ship cannot drift. +[![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.10.0-orange)](./weaveffi.schema.json) [![C ABI](https://img.shields.io/badge/C%20ABI-3-orange)](docs/src/reference/abi.md) + +WeaveFFI generates idiomatic, type-safe bindings for 11 languages (C, C++, +Swift, Kotlin, Node.js, WebAssembly, Python, .NET, Dart, Go, and Ruby) for any +native library that exposes a C ABI. Write the library in Rust and annotate a +module with `#[weaveffi::module]`, or describe it in a YAML, JSON, or TOML IDL +and implement the generated C header in C, C++, Zig, or anything else. Either +way, every language talks to the same stable C ABI, and every package is named +after your library, not after WeaveFFI. + +What you get, in every language: + +- **Real objects.** Interfaces become classes backed by a reference-counted + native object, released deterministically (`close()`, `Dispose()`, + `deinit`, RAII) with a garbage-collector backstop, and safe to close while + another thread is mid-call. +- **Typed errors.** An error domain becomes an exception hierarchy, a Swift + error enum, or a Go error type the consumer can match on. +- **Async with cancellation.** `async` functions become `Promise`s, coroutines, + `Task`s, `async throws`, or awaitables, and cancelling them (an + `AbortSignal`, a `CancellationToken`, a `context.Context`, task + cancellation) cancels the native work. +- **Callbacks.** A callback interface becomes a protocol, interface, or + abstract class the consumer implements and the library calls from any + thread. +- **Load-time safety.** Each generated package checks the library's ABI + revision and a per-module contract checksum before the first call, so a + stale binding fails with a clear error instead of corrupting memory. ## Quickstart -**1. Install the CLI:** +**1. Install the CLI.** ```bash cargo install weaveffi-cli ``` -**2. Define your API as an IDL** in `kvstore.yml`. Any native library that -exposes a C ABI (written in C, C++, Zig, Rust, ...) implements the symbols it -declares. An interface is a reference-counted object with constructors and -methods; an error domain plus `throws: true` gives its fallible members typed -errors: - -```yaml -version: "0.9.0" -modules: - - name: kv - errors: - name: KvError - codes: - - { name: KeyNotFound, code: 1001, message: "key not found" } - - { name: StoreFull, code: 1003, message: "store has reached capacity" } - interfaces: - - name: 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 - params: [] - return: i64 -``` - -**Producing in Rust?** Skip the hand-written IDL: annotate a normal module with -`#[weaveffi::module]` (after `cargo add weaveffi`) and the macro emits the C ABI -and derives the IDL for you, so you write no `unsafe` glue. An interface type -must be `Send + Sync` (the object is shared across the boundary and reference -counted as an `Arc`); its `pub` methods take `&self` or `self: Arc`. -See [The Rust Producer Macro](docs/src/guides/producer-macro.md) for the full -walkthrough. +**2. Write the library.** In a Rust crate with `crate-type = ["cdylib"]` and +`cargo add weaveffi`: ```rust #[weaveffi::module] @@ -82,92 +55,116 @@ pub mod kv { StoreFull = 1003, } - // Reference counted by the producer; every consumer wrapper holds one - // strong reference and releases it when it's closed or collected. + impl std::fmt::Display for KvError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + KvError::KeyNotFound => "key not found", + KvError::StoreFull => "store has reached capacity", + }) + } + } + + /// An in-memory key-value store. #[weaveffi::interface] pub struct Store { entries: Mutex>>, } impl Store { + /// Open a store. pub fn open(path: String) -> Result { - let _ = path; // in-memory demo + let _ = path; Ok(Store { entries: Mutex::new(BTreeMap::new()) }) } - pub fn put(&self, key: String, value: Vec) -> Result { + /// Insert `value` under `key`; true if the key is new. + pub fn put(&self, key: &str, value: Vec) -> Result { let mut entries = self.entries.lock().unwrap(); - if entries.len() >= 1024 && !entries.contains_key(&key) { + if entries.len() >= 1024 && !entries.contains_key(key) { return Err(KvError::StoreFull); } - Ok(entries.insert(key, value).is_none()) + Ok(entries.insert(key.to_string(), value).is_none()) } + /// The number of entries. pub fn count(&self) -> i64 { self.entries.lock().unwrap().len() as i64 } } } -// Emit the fixed C ABI runtime surface once per cdylib. +// The C ABI runtime surface, once per cdylib. weaveffi::export_runtime!(); ``` -**3. Generate bindings** from the IDL (or, for a Rust producer, straight from -the annotated source): +The macro emits the `extern "C"` functions; you write no `unsafe` code. An +interface type must be `Send + Sync`, because the object is shared across the +boundary as an `Arc`. + +**3. Generate bindings.** ```bash -weaveffi generate kvstore.yml -o generated --target c,swift,python -# Rust producer: point generate at the annotated source instead -weaveffi generate src/lib.rs -o generated --target c,swift,python +weaveffi init # writes weaveffi.toml: [project] input = "src/lib.rs" +weaveffi generate # every target, into ./bindings +cargo build # builds libkvstore ``` -**4. Use the generated code from any of the eleven supported languages.** -Every target gets a real `Store` class whose objects hold one strong -reference to the native store and release it deterministically (`deinit`, -`close()`, `Dispose()`, RAII) with a garbage-collector backstop where the -language has one, plus a typed `KvError` consumers can catch and match on. -Click each block below to see what WeaveFFI emits. +**4. Use them.** From Python, for example: + +```python +import kvstore + +with kvstore.Store.open("data.kv") as store: + store.put("greeting", b"hello") + print(store.count()) # 1 + +try: + ... +except kvstore.StoreFull as e: + print(e.code, e.message) # 1003 store has reached capacity +```
-C: generated/c/weaveffi.h +The C header every target binds to (bindings/c/kvstore.h) ```c -typedef enum { weaveffi_kv_KvError_KeyNotFound = 1001, weaveffi_kv_KvError_StoreFull = 1003 } weaveffi_kv_KvError; -typedef struct weaveffi_kv_Store weaveffi_kv_Store; - -// Module: kv -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_open(const char* path, weaveffi_error* out_err); -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_error* out_err); -WEAVEFFI_API int64_t weaveffi_kv_Store_count(const weaveffi_kv_Store* self, weaveffi_error* out_err); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_clone(const weaveffi_kv_Store* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self); +#define KVSTORE_ABI_VERSION 3u +KVSTORE_API uint32_t kvstore_abi_version(void); +#define KVSTORE_KV_CHECKSUM 0xf6b8170a59ae07a7ull +KVSTORE_API uint64_t kvstore_kv_checksum(void); + +typedef enum { + kvstore_kv_KvError_KeyNotFound = 1001, + kvstore_kv_KvError_StoreFull = 1003 +} kvstore_kv_KvError; +typedef struct kvstore_kv_Store kvstore_kv_Store; + +KVSTORE_API kvstore_kv_Store* kvstore_kv_Store_open(const uint8_t* path_ptr, size_t path_len, kvstore_error* out_err); +KVSTORE_API bool kvstore_kv_Store_put(const kvstore_kv_Store* self, const uint8_t* key_ptr, size_t key_len, const uint8_t* value_ptr, size_t value_len, kvstore_error* out_err); +KVSTORE_API int64_t kvstore_kv_Store_count(const kvstore_kv_Store* self, kvstore_error* out_err); +KVSTORE_API kvstore_kv_Store* kvstore_kv_Store_clone(const kvstore_kv_Store* self); +KVSTORE_API void kvstore_kv_Store_destroy(kvstore_kv_Store* self); ``` +Every symbol and type carries the library's own prefix (`kvstore_`), so any +number of WeaveFFI-built libraries can link into one process. +
-Swift: generated/swift/Sources/WeaveFFI/WeaveFFI.swift +Swift (bindings/swift/Sources/Kvstore/Kvstore.swift) ```swift -public enum KvError: Error, LocalizedError { +public enum KvError: Error, LocalizedError, Sendable { case keyNotFound(message: String) case storeFull(message: String) } -public final class Store { - let ptr: OpaquePointer - - deinit { - weaveffi_kv_Store_destroy(ptr) - } +public final class Store: @unchecked Sendable { + deinit { kvstore_kv_Store_destroy(ptr) } public static func open(path: String) throws -> Store { /* ... */ } - public func put(key: String, value: Data) throws -> Bool { /* ... */ } - public func count() -> Int64 { /* ... */ } } ``` @@ -175,23 +172,17 @@ public final class Store {
-Python: generated/python/kvstore/weaveffi.pyi +Python type stubs (bindings/python/kvstore/kvstore.pyi) ```python -class KvError(WeaveFFIError): - KeyNotFound: Type["KeyNotFound"] - StoreFull: Type["StoreFull"] - -class KeyNotFound(KvError): - CODE: int # 1001 - -class StoreFull(KvError): - CODE: int # 1003 +class KvError(Error): ... +class KeyNotFound(KvError): ... +class StoreFull(KvError): ... class Store: def close(self) -> None: ... def __enter__(self) -> "Store": ... - def __exit__(self, *exc: object) -> bool: ... + def __exit__(self, *exc: object) -> None: ... @classmethod def open(cls, path: str) -> "Store": ... def put(self, key: str, value: bytes) -> bool: ... @@ -200,197 +191,103 @@ class Store:
-The remaining targets follow the same pattern with their own idioms: a class -(or the closest analogue) that owns one reference to the native object and -releases it through the language's natural disposal hook, and the module's -error domain as a typed error or exception. Add a `callback_interfaces:` block -to the IDL and each target also gets an interface, protocol, or abstract class -the consumer implements and the native library calls. - -## Why WeaveFFI? - -- **One definition, eleven languages.** Write the API once (safe Rust or an - IDL) and ship packages to npm, SwiftPM, Maven, PyPI, NuGet, pub.dev, - RubyGems, and Go modules. Each package is standalone: consumers don't need - WeaveFFI installed. -- **Stable C ABI underneath.** Every target speaks to the same `extern "C"` - contract, so adding a new platform later is a code-gen change, not a - rewrite. Rust producers get that C ABI for free from the - `#[weaveffi::module]` macro; any other backend that can expose a C ABI (C, - C++, Zig) implements the generated header directly. -- **Idiomatic per-target output.** No lowest-common-denominator surface area. - Interfaces become real classes with methods and deterministic disposal, and - error domains become typed errors (a Swift error enum, Python exception - classes, and each remaining target's own exception idiom). Swift gets - `async/await` and `throws`, Kotlin gets `suspend` and JNI glue, Python - gets typed `.pyi` stubs, TypeScript gets `Promise`s and `BigInt`, and Dart - gets `dart:ffi` with `NativeFinalizer`, all from the same definition. -- **Objects that compose.** Interface objects are reference counted by the - producer (`Arc` in Rust) and expose `_clone`/`_destroy` at the C ABI, so - they can be returned, passed, shared between wrappers, and nested inside - records, optionals, lists, maps, iterators, and async results. Every wrapper - releases its reference when it's closed, disposed, or dropped. -- **Callback interfaces.** Declare a `callback_interfaces:` block (or a - `#[weaveffi::callback_interface] trait` in Rust) and the consumer implements - it as a protocol, interface, or abstract class. The producer receives an - `Arc` it can retain and call from any thread; a consumer-side - failure surfaces to the original caller as a foreign error rather than a - crash. -- **The whole IDL surface, on every target.** Interfaces, typed error - domains, async functions (with a pluggable spawner, so Tokio users can plug - in their runtime), iterators, and callback interfaces work across all - eleven languages. Generators declare their capabilities and - `weaveffi generate` fails with a clear error (never a silent skip) in the - rare mode that can't deliver a feature you use (Wasm's Emscripten compat - mode excludes async functions and callback interfaces). See the - [feature matrix](docs/src/generators/README.md#feature-support-matrix). - -## How does it compare? - -See [Comparison](docs/src/comparison.md) for a side-by-side feature matrix -versus UniFFI, Diplomat, cbindgen, swift-bridge, napi-rs, wasm-bindgen, SWIG, -and autocxx, plus an honest "when to choose WeaveFFI" guide. - -## Supported targets - -Every target consumes the same C ABI (revision 2). The table lists what each -generator emits, how its wrapper owns an interface object (each wrapper holds -one strong reference and releases it through the listed hook), and how a -consumer implements a callback interface. - -| Target | Output directory | What you get | Objects | Callback interfaces | -|--------|------------------|--------------|---------|---------------------| -| **C** | `c/` | `weaveffi.h` header with opaque interface typedefs, `_clone`/`_destroy` pairs, vtable typedefs, function prototypes, and the shared `weaveffi_error` type | Raw `{tag}*`; call `_clone` to share and `_destroy` to release | Fill a `{tag}_vtable` struct (`ctx` + function pointers + `free`) by hand | -| **C++** | `cpp/` | RAII header (`weaveffi.hpp`) with `std::optional`/`std::vector`/`std::unordered_map` wrappers, exception-based errors, and a `CMakeLists.txt` | Copyable RAII class (copy calls `_clone`, destructor calls `_destroy`) | Subclass the abstract class and pass a `std::shared_ptr` | -| **Swift** | `swift/` | SwiftPM package wrapping the C ABI with `throws`, `async/await`, and `Sequence` iterators | `final class` releasing in `deinit` | Conform to the generated `protocol` | -| **Kotlin** | `kotlin/` | Kotlin wrapper, JNI C shim, and a Gradle (`build.gradle.kts`) project for Android or the desktop JVM | `AutoCloseable` class with a `java.lang.ref.Cleaner` backstop | Implement the generated `interface` | -| **Node.js** | `node/` | N-API addon loader + TypeScript declarations (`BigInt` for 64-bit integers) and a `package.json` | Class with `close()` / `[Symbol.dispose]` and a `FinalizationRegistry` backstop | Pass any object matching the TypeScript `interface` | -| **Wasm** | `wasm/` | JavaScript loader + TypeScript declarations for `wasm32-unknown-unknown` builds, packaged as npm | Class with `close()` / `[Symbol.dispose]` and a `FinalizationRegistry` backstop | Pass any object matching the TypeScript `interface`; delivery is synchronous | -| **Python** | `python/` | `ctypes` bindings + `.pyi` type stubs + `pyproject.toml` | Class with `close()`, context-manager support, and a `__del__` backstop | Subclass the generated `ABC` | -| **.NET** | `dotnet/` | C# P/Invoke bindings + `.csproj` + `.nuspec` for NuGet | `IDisposable` class with a finalizer backstop | Implement the generated `I*` interface | -| **Dart** | `dart/` | `dart:ffi` bindings + `pubspec.yaml` for Flutter and Dart projects | `Finalizable` class with `dispose()` and a `NativeFinalizer` backstop | Extend the generated `abstract class` | -| **Go** | `go/` | CGo bindings + `go.mod` for Go modules | Struct with `Close()` and a `runtime.SetFinalizer` backstop | Implement the generated `interface` | -| **Ruby** | `ruby/` | FFI gem bindings + `.gemspec` for RubyGems | Class with `close` backed by an `FFI::AutoPointer` | Include the generated module and implement its methods | +Not writing the library in Rust? Describe the same API in an IDL, run +`weaveffi init` outside a Cargo crate for a starter `api.yml`, generate +`--target c`, and implement the header. See [Getting +Started](docs/src/getting-started.md). + +## Targets + +| Target | Package | Objects | Async | Cancellation | Callbacks | +|---|---|---|---|---|---| +| C | `{library}.h` (+ `{library}_buffer.h` codecs) | `_clone` / `_destroy` | completion callback | cancel token | vtable struct | +| C++ | `{library}.hpp`, CMake target | RAII class | `std::future` | `CancelToken` | abstract class | +| Swift | SwiftPM package | `deinit` | `async throws` | task cancellation | `protocol` | +| Kotlin | Gradle (Android or JVM) | `AutoCloseable` + cleaner | `suspend` | coroutine cancellation | `interface` | +| Node.js | npm package (N-API addon) | `close()`, `Symbol.dispose` | `Promise` | `AbortSignal` | object | +| WebAssembly | npm package (ESM) | `close()`, `Symbol.dispose` | `Promise` | `AbortSignal` | object | +| Python | `pyproject.toml` package + `.pyi` | `close()`, `with` | awaitable | task cancellation | ABC | +| .NET | `.csproj` (NuGet) | `IDisposable` over `SafeHandle` | `Task` | `CancellationToken` | interface | +| Dart | pub package | `dispose()` + `NativeFinalizer` | `Future` | `CancelToken` | abstract class | +| Go | Go module (cgo) | `Close() error` | blocking call | `context.Context` | interface | +| Ruby | gem | `close` | blocking call | `cancel:` token | module | + +The [capability matrix](docs/src/generators/README.md) and the per-language +pages have the details: type mapping, naming, loading, and known limits. ## Install -**From crates.io** (requires the [Rust toolchain](https://rustup.rs/)): - ```bash -cargo install weaveffi-cli +cargo install weaveffi-cli # from source +cargo binstall weaveffi-cli # prebuilt ``` -**Pre-built binaries** for macOS, Linux, and Windows are attached to every -[GitHub release](https://github.com/weavefoundry/weaveffi/releases). Download -the archive for your platform, extract the `weaveffi` binary, and put it on -your `PATH`. +Prebuilt archives for Linux (x64, arm64), macOS (x64, arm64), and Windows +(x64) are attached to every [GitHub +release](https://github.com/weavefoundry/weaveffi/releases). -Verify the install: +## CLI -```bash -weaveffi --version -weaveffi schema-version # prints 0.9.0 -``` +| Command | What it does | +|---|---| +| `weaveffi init [dir]` | Write `weaveffi.toml` (and a starter IDL outside a Rust crate) | +| `weaveffi generate [input]` | Generate bindings; `--target c,swift` to subset, `--dry-run` to list files. Only changed files are rewritten, and files a previous run produced that are no longer generated are removed | +| `weaveffi validate [input]` | Validate without generating; `--warn` for lints, `--format json` | +| `weaveffi diff [input]` | Show what regenerating would change; `--check` exits non-zero for CI | +| `weaveffi package [input]` | Assemble publishable packages that bundle prebuilt native libraries | +| `weaveffi extract ` | Print the IDL derived from annotated Rust | +| `weaveffi schema` | Print the JSON Schema of the IDL | -## CLI reference - -| Command | Description | -|---------|-------------| -| `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 (Kotlin bundles `jniLibs//`, Wasm bundles the `.wasm` into an npm package); `--binaries ` for prebuilt libs or `--build ` to cross-compile a Rust producer | -| `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 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 IDL schema version (`0.9.0`) | -| `weaveffi completions ` | Print shell completion scripts (`bash`, `zsh`, `fish`, `powershell`, `elvish`) | - -### 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): +With a `[project]` table in `weaveffi.toml`, every command works without +arguments from anywhere in the project: ```toml +[project] +input = "src/lib.rs" # or an IDL such as "kvstore.yml" +out = "bindings" +targets = ["c", "swift", "kotlin", "python"] + [package] -name = "kvstore" # stamped into every generated manifest -version = "0.1.0" -description = "An embedded key-value store" +version = "1.0.0" 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.9.0" -modules: ... ``` -Regenerate the schema with `weaveffi schema --format json-schema > weaveffi.schema.json`. +A Rust producer's package name, C prefix, and library name come from its +`Cargo.toml`; an IDL's come from `[package]`. See +[Configuration](docs/src/guides/config.md). ## Documentation -Full documentation lives at (sources under -[`docs/`](./docs/)). Key pages: - -- [Introduction](docs/src/intro.md): what WeaveFFI is and why it exists -- [Getting Started](docs/src/getting-started.md): install, define an IDL, generate, and call from C -- [C ABI Contract](docs/src/reference/abi.md): the normative description of ABI revision 2 (objects, callback interfaces, value buffers, async, iterators) -- [Comparison](docs/src/comparison.md): feature matrix vs UniFFI, Diplomat, cbindgen, swift-bridge, napi-rs, wasm-bindgen, SWIG, autocxx -- [FAQ](docs/src/faq.md): top questions about scope, object ownership, callbacks, runtime cost, and platform support -- [Samples](docs/src/samples.md): the eight sample producers, from `calculator` to the `codec` round-trip oracle -- [Generators](docs/src/generators/): per-target reference for each of the eleven languages -- [Guides](docs/src/guides/): the producer macro, memory ownership, error handling, async, configuration, packaging -- [Stability and Versioning](docs/src/stability.md): what changed in schema 0.9.0 / ABI 2 and how to migrate +The book lives at (sources in [`docs/`](docs/)): +[Getting Started](docs/src/getting-started.md), [the producer +macro](docs/src/guides/producer-macro.md), [errors and +memory](docs/src/guides/errors-and-memory.md), [async and +cancellation](docs/src/guides/async.md), the [IDL +reference](docs/src/reference/idl.md), the normative [C ABI +contract](docs/src/reference/abi.md), and a +[comparison](docs/src/comparison.md) with UniFFI, Diplomat, cbindgen, and +others. ## Status -WeaveFFI is in active `0.x` development. The current IDL schema is `0.9.0` -and the C ABI is at revision 2 (reference-counted objects, callback -interfaces, and a pluggable async spawner; `handle`, borrowed `&str`/`&[u8]` -spellings, module-level callbacks and listeners, and the arena runtime were -removed). Following [Semantic Versioning](https://semver.org/), the public -surface (the CLI, the IDL schema, the generated code, and the `weaveffi-abi` -runtime symbols) may change between minor releases while the project is -pre-1.0, and only the current IDL schema version is accepted. See [Stability -and Versioning](docs/src/stability.md) for the migration guide and the -recommended `weaveffi diff --check` CI workflow, and [Roadmap](docs/src/roadmap.md) -for what's planned next. - -The full quality gate (`cargo fmt`, `cargo clippy -D warnings`, `cargo -test`, `cargo doc -D warnings`, `cargo deny`, `cargo audit`, `cargo -machete`, `cargo insta test --check`, `cargo bench --no-run`, and -`weaveffi diff --check` on every sample) runs in CI on every PR. The test -suite includes a property-based test of the value-buffer codec, `trybuild` -UI tests pinning the producer macro's compile-time diagnostics, and a -snapshot corpus of five IDL fixtures across all eleven generators. A -conformance harness builds real producers and runs generated consumers end -to end in all eleven languages, with dedicated async lanes and a `codec` -lane that round-trips every value-buffer wire shape (including object -tokens) through every language. - -Releases are fully automated by [semantic-release](https://semantic-release.gitbook.io/) -on merge to `main`. +WeaveFFI is pre-1.0: the IDL schema is `0.10.0` and the C ABI is revision 3, +and either may change in a minor release. See [Stability and +Versioning](docs/src/stability.md) for the policy and the migration notes, and +the [Roadmap](docs/src/roadmap.md) for what's next. + +Every pull request runs formatting, clippy, rustdoc, the test suite and +snapshot corpus on Linux, macOS, and Windows, a compile check of every +snapshot fixture with each language's own toolchain, and an end-to-end +conformance harness that builds real producers and runs generated consumers in +all 11 languages on Linux and macOS, asserting that every native object, +callback, iterator, token, and allocation is released by the end of each run. ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow, snapshot -test conventions, fuzzing setup, and Conventional Commit rules. +See [CONTRIBUTING.md](CONTRIBUTING.md). ## License diff --git a/conformance/c/async_demo.c b/conformance/c/async_demo.c index d77b4058..66883e34 100644 --- a/conformance/c/async_demo.c +++ b/conformance/c/async_demo.c @@ -1,161 +1,245 @@ -// Conformance consumer: async-demo sample, C target. +// Conformance consumer: async-demo sample, C target (ABI revision 3). // -// Includes the *generated* C header and links the async_demo cdylib, -// exercising the raw async launcher convention: each `*_async` symbol takes a -// completion callback plus a context pointer and fires it once from the -// producer's worker thread, with buffered results (the TaskResult record and -// the list-of-records batch) borrowed for the duration of the callback and -// direct scalars passed by value. Also covers the typed error code delivered -// through the callback's error slot (InvalidName == 1), the plain sync -// functions beside the async ones, and active_callbacks settling to zero. -// Completion arrives on the producer's worker thread, so each wait polls an -// atomic flag. Exits 0 on success; aborts (non-zero) on any failed assertion. - -#include +// Exercises the raw async launcher convention: each async function's symbol +// takes its inputs, a completion callback, and a context pointer, returns at +// once, and fires the callback exactly once from a producer thread. Buffered +// results (the TaskResult record, the list-of-records batch) arrive as +// consumer-owned (ptr, len) runs released with async_demo_free_bytes; a +// non-null error is heap-boxed and released with async_demo_error_free. +// The cancellable `wait` takes a cancel token: cancelling it completes the +// call with code -5 long before its timeout, whether the token fires before +// or after launch, and the consumer may destroy its token reference at any +// time. Also covers many concurrent launches, the plain sync function beside +// the async ones, and the producer's leak counters settling to zero. + +#include "harness.h" + #include -#include -#include -#include -#include "weaveffi.h" -#include "wvbuf.h" +#include "async_demo_buffer.h" + +#define CANCELLED (-5) -// A decoded TaskResult record: id, value, success. +// One in-flight call's completion slot; the callback runs on a producer +// thread, so the main thread polls `done`. typedef struct { - int64_t id; - char* value; - int success; -} task_result_t; - -static void read_task_result(wv_reader* r, task_result_t* t) { - t->id = wv_get_i64(r); - t->value = wv_get_str(r); - t->success = wv_get_bool(r); + atomic_int done; + atomic_int fired; // completions observed (must end at exactly 1) + int32_t code; + char message[96]; + int64_t i64; + int32_t i32; + uint8_t* buf; // owned result run + size_t buf_len; +} call; + +static void call_reset(call* c) { + memset(c, 0, sizeof *c); + atomic_init(&c->done, 0); + atomic_init(&c->fired, 0); } -// Poll the completion flag; the callback fires on the producer's worker -// thread, so the main thread spins until it lands. -static void wait_done(atomic_int* done) { - while (!atomic_load(done)) { - } +static void record_err(call* c, async_demo_error* err) { + c->code = err ? err->code : 0; + if (err && err->message) snprintf(c->message, sizeof c->message, "%s", err->message); + async_demo_error_free(err); } -// --- run_task("alpha"): buffered record result, decoded in the callback. --- -static atomic_int g_task_done = 0; -static int32_t g_task_err = -1; -static task_result_t g_task_result; - -static void on_task_done(void* context, weaveffi_error* err, - const uint8_t* result_ptr, size_t result_len) { - (void)context; - g_task_err = err ? err->code : 0; - if (g_task_err == 0) { - wv_reader r; - wv_r_init(&r, result_ptr, result_len); - read_task_result(&r, &g_task_result); - wv_r_expect_end(&r); - } - atomic_store(&g_task_done, 1); +static void finish(call* c) { + atomic_fetch_add(&c->fired, 1); + atomic_store(&c->done, 1); } -// --- run_task(""): the typed error code lands in the callback's err slot. --- -static atomic_int g_err_done = 0; -static int32_t g_err_code = -1; - -static void on_task_err(void* context, weaveffi_error* err, - const uint8_t* result_ptr, size_t result_len) { - (void)context; - (void)result_ptr; - (void)result_len; - g_err_code = err ? err->code : 0; - atomic_store(&g_err_done, 1); +static void on_buffer(void* context, async_demo_error* err, const uint8_t* result_ptr, + size_t result_len) { + call* c = (call*)context; + record_err(c, err); + // The result run is ours: keep it and release it after decoding. + c->buf = (uint8_t*)result_ptr; + c->buf_len = result_len; + finish(c); } -// --- run_batch: buffered list-of-records result. --- -static atomic_int g_batch_done = 0; -static int32_t g_batch_err = -1; -static size_t g_batch_count = 0; -static task_result_t g_batch[3]; - -static void on_batch_done(void* context, weaveffi_error* err, - const uint8_t* result_ptr, size_t result_len) { - (void)context; - g_batch_err = err ? err->code : 0; - if (g_batch_err == 0) { - wv_reader r; - wv_r_init(&r, result_ptr, result_len); - g_batch_count = wv_get_u32(&r); - for (size_t i = 0; i < g_batch_count && i < 3; i++) { - read_task_result(&r, &g_batch[i]); - } - wv_r_expect_end(&r); - } - atomic_store(&g_batch_done, 1); +static void on_i64(void* context, async_demo_error* err, int64_t result) { + call* c = (call*)context; + record_err(c, err); + c->i64 = result; + finish(c); } -// --- run_n_tasks: direct scalar result. --- -static atomic_int g_n_done = 0; -static int32_t g_n_err = -1; -static int32_t g_n_result = -1; +static void on_i32(void* context, async_demo_error* err, int32_t result) { + call* c = (call*)context; + record_err(c, err); + c->i32 = result; + finish(c); +} -static void on_n_done(void* context, weaveffi_error* err, int32_t result) { - (void)context; - g_n_err = err ? err->code : 0; - g_n_result = result; - atomic_store(&g_n_done, 1); +// Wait for a completion, failing after `limit_ms`. Returns the elapsed time. +static long await_call(call* c, long limit_ms) { + long waited = 0; + while (!atomic_load(&c->done)) { + assert(waited < limit_ms && "async call did not complete in time"); + sleep_ms(1); + waited++; + } + assert(atomic_load(&c->fired) == 1); + return waited; } -int main(void) { - weaveffi_error err = {0}; - - // Async record return: the callback borrows the encoded TaskResult. - weaveffi_tasks_run_task_async("alpha", on_task_done, NULL); - wait_done(&g_task_done); - assert(g_task_err == 0); - assert(g_task_result.id > 0); - assert(strcmp(g_task_result.value, "completed: alpha") == 0); - assert(g_task_result.success == 1); - free(g_task_result.value); - - // Typed async error: the empty name reports InvalidName (code 1). - weaveffi_tasks_run_task_async("", on_task_err, NULL); - wait_done(&g_err_done); - assert(g_err_code == 1); - - // Buffered list-of-records both ways: encode ["a", "b", "c"], decode - // three results. - wv_writer names; - wv_w_init(&names); - wv_put_u32(&names, 3); - wv_put_str(&names, "a"); - wv_put_str(&names, "b"); - wv_put_str(&names, "c"); - weaveffi_tasks_run_batch_async(names.buf, names.len, on_batch_done, NULL); - wait_done(&g_batch_done); - wv_w_free(&names); - assert(g_batch_err == 0); - assert(g_batch_count == 3); +static void run_task(void) { + call c; + call_reset(&c); + async_demo_tasks_run_task(STR("alpha"), on_buffer, &c); + await_call(&c, 5000); + assert(c.code == 0 && c.buf != NULL); + async_demo_tasks_TaskResult r; + assert(async_demo_tasks_TaskResult_decode(c.buf, c.buf_len, &r)); + async_demo_free_bytes(c.buf, c.buf_len); + assert(r.id > 0); + assert(strcmp(r.value.ptr, "completed: alpha") == 0); + assert(r.success); + async_demo_tasks_TaskResult_free(&r); + + // Typed async error: the empty name reports InvalidName with its message + // and no result run. + call_reset(&c); + async_demo_tasks_run_task(NULL, 0, on_buffer, &c); + await_call(&c, 5000); + assert(c.code == async_demo_tasks_TaskError_InvalidName); + assert(strcmp(c.message, "task name must not be empty") == 0); + assert(c.buf == NULL && c.buf_len == 0); +} + +static void run_batch(void) { + async_demo_str names[3] = {async_demo_str_of("a"), async_demo_str_of("b"), + async_demo_str_of("c")}; + async_demo_list_string in; + in.items = names; + in.len = 3; + async_demo_writer w; + memset(&w, 0, sizeof w); + async_demo_list_string_write(&w, &in); + call c; + call_reset(&c); + async_demo_tasks_run_batch(w.ptr, w.len, on_buffer, &c); + // Inputs are lifted before the launcher returns, so the buffer can go now. + async_demo_writer_free(&w); + await_call(&c, 5000); + assert(c.code == 0); + async_demo_list_tasks_TaskResult out; + assert(async_demo_list_tasks_TaskResult_decode(c.buf, c.buf_len, &out)); + async_demo_free_bytes(c.buf, c.buf_len); + assert(out.len == 3); const char* expected[3] = {"completed: a", "completed: b", "completed: c"}; for (size_t i = 0; i < 3; i++) { - assert(strcmp(g_batch[i].value, expected[i]) == 0); - assert(g_batch[i].success == 1); - free(g_batch[i].value); + assert(strcmp(out.items[i].value.ptr, expected[i]) == 0); + assert(out.items[i].success); + } + assert(out.items[0].id < out.items[1].id && out.items[1].id < out.items[2].id); + async_demo_list_tasks_TaskResult_free(&out); +} + +static void run_n_tasks(void) { + // Many concurrent launches, each with its own context: every completion + // fires exactly once with its own value. + enum { N = 64 }; + static call calls[N]; + for (int i = 0; i < N; i++) { + call_reset(&calls[i]); + async_demo_tasks_run_n_tasks(i, on_i32, &calls[i]); + } + for (int i = 0; i < N; i++) { + await_call(&calls[i], 5000); + assert(calls[i].code == 0 && calls[i].i32 == i); } +} - // Direct scalar through the async callback. - weaveffi_tasks_run_n_tasks_async(7, on_n_done, NULL); - wait_done(&g_n_done); - assert(g_n_err == 0); - assert(g_n_result == 7); +static void wait_and_cancel(void) { + call c; + + // No token: wait runs to its (short) timeout. + call_reset(&c); + async_demo_tasks_wait(5, NULL, on_i64, &c); + await_call(&c, 5000); + assert(c.code == 0 && c.i64 == 5); + + // A token that is never cancelled changes nothing. + async_demo_cancel_token* token = async_demo_cancel_token_create(); + assert(token != NULL && !async_demo_cancel_token_is_cancelled(token)); + call_reset(&c); + async_demo_tasks_wait(10, token, on_i64, &c); + await_call(&c, 5000); + assert(c.code == 0 && c.i64 == 10); + async_demo_cancel_token_destroy(token); + + // Cancel mid-flight: a one-minute wait completes with -5 almost at once. + // The consumer drops its token reference right after cancelling; the + // producer holds its own until the call completes. + token = async_demo_cancel_token_create(); + call_reset(&c); + async_demo_tasks_wait(60000, token, on_i64, &c); + sleep_ms(20); + assert(!atomic_load(&c.done) && "the wait is still pending"); + async_demo_cancel_token_cancel(token); + async_demo_cancel_token_cancel(token); // idempotent + assert(async_demo_cancel_token_is_cancelled(token)); + async_demo_cancel_token_destroy(token); + long waited = await_call(&c, 5000); + assert(waited < 5000); + assert(c.code == CANCELLED); + assert(c.message[0] != '\0'); + + // Cancelled before launch: completes with -5 without waiting. + token = async_demo_cancel_token_create(); + async_demo_cancel_token_cancel(token); + call_reset(&c); + async_demo_tasks_wait(60000, token, on_i64, &c); + await_call(&c, 5000); + assert(c.code == CANCELLED); + async_demo_cancel_token_destroy(token); + + // One token cancels several in-flight calls at once. + token = async_demo_cancel_token_create(); + call many[3]; + for (int i = 0; i < 3; i++) { + call_reset(&many[i]); + async_demo_tasks_wait(60000, token, on_i64, &many[i]); + } + async_demo_cancel_token_cancel(token); + for (int i = 0; i < 3; i++) { + await_call(&many[i], 5000); + assert(many[i].code == CANCELLED); + } + async_demo_cancel_token_destroy(token); + + // Destroying the consumer's reference is not cancellation: the call + // keeps its own reference and runs to its timeout. + token = async_demo_cancel_token_create(); + call_reset(&c); + async_demo_tasks_wait(100, token, on_i64, &c); + async_demo_cancel_token_destroy(token); + await_call(&c, 5000); + assert(c.code == 0 && c.i64 == 100); +} - // Sync functions beside the async ones. - assert(weaveffi_tasks_cancel_task(1, &err) == false); - assert(err.code == 0); +int main(void) { + async_demo_error err = {0}; + + assert(ASYNC_DEMO_ABI_VERSION == 3u); + assert(async_demo_abi_version() == ASYNC_DEMO_ABI_VERSION); + assert(async_demo_tasks_checksum() == ASYNC_DEMO_TASKS_CHECKSUM); + + run_task(); + run_batch(); + run_n_tasks(); + wait_and_cancel(); - // Every spawned task body has completed by the time its callback fires. - assert(weaveffi_tasks_active_callbacks(&err) == 0); + // Every task body, cancelled ones included, has been dropped. + for (int i = 0; i < 2000 && async_demo_tasks_active_callbacks(&err) != 0; i++) sleep_ms(1); + assert(async_demo_tasks_active_callbacks(&err) == 0); assert(err.code == 0); - printf("c async-demo conformance: OK\n"); + ASSERT_NO_LEAKS(async_demo_debug_live); + printf("c/async-demo: OK\n"); return 0; } diff --git a/conformance/c/codec.c b/conformance/c/codec.c index 4ef67365..7b18b6ae 100644 --- a/conformance/c/codec.c +++ b/conformance/c/codec.c @@ -1,838 +1,461 @@ -// Conformance consumer: codec sample, C target (ABI revision 2). +// Conformance consumer: codec sample, C target (ABI revision 3). // -// A round-trip check of the value-buffer protocol against the producer's own -// codec: every fixed-width scalar, strings (including non-ASCII), bytes, -// present and absent optionals, lists (nested and empty), string- and -// integer-keyed maps with record values, nested records, every rich-enum -// variant, lists of optionals and enums, and object tokens in a record -// field, an optional, and a list. Fixtures fetched from the producer are -// checked field by field (producer encodes, consumer decodes), handed back -// to `verify_*` (consumer encodes, producer decodes), and re-encoded through -// `roundtrip_*` and compared again; hand-built edge values (empty -// containers, 64-bit extremes, NaN, infinities, negative zero) go through -// the same round trip. Exits 0 on success; aborts on any mismatch. - -#include +// A round-trip check of the value-buffer protocol between the producer's own +// codec and the generated `codec_buffer.h`: every fixed-width scalar, +// strings (non-ASCII and with interior NUL bytes), bytes, present and absent +// optionals, lists (nested and empty), string- and integer-keyed maps with +// record values, nested records, every rich-enum variant, lists of optionals +// and enums, and objects in a record field, an optional, and a list. +// Fixtures fetched from the producer are checked field by field (producer +// encodes, consumer decodes), handed back to `verify_*` (consumer encodes, +// producer decodes), and re-encoded through `roundtrip_*`, which must return +// the very same bytes; hand-built edge values (empty containers, 64-bit +// extremes, NaN, infinities, negative zero) take the same path. Malformed +// input is rejected on both sides. Ends by asserting the producer's leak +// counters are zero. + +#include "harness.h" + #include -#include -#include -#include - -#include "weaveffi.h" -#include "wvbuf.h" - -// ── Scalars ──────────────────────────────────────────────────────────────── - -typedef struct { - int8_t i8v; - uint8_t u8v; - int16_t i16v; - uint16_t u16v; - int32_t i32v; - uint32_t u32v; - int64_t i64v; - uint64_t u64v; - float f32v; - double f64v; - int flag; - int32_t color; -} scalars_t; - -static void read_scalars(wv_reader* r, scalars_t* s) { - s->i8v = wv_get_i8(r); - s->u8v = wv_get_u8(r); - s->i16v = wv_get_i16(r); - s->u16v = wv_get_u16(r); - s->i32v = wv_get_i32(r); - s->u32v = wv_get_u32(r); - s->i64v = wv_get_i64(r); - s->u64v = wv_get_u64(r); - s->f32v = wv_get_f32(r); - s->f64v = wv_get_f64(r); - s->flag = wv_get_bool(r); - s->color = wv_get_i32(r); -} -static void write_scalars(wv_writer* w, const scalars_t* s) { - wv_put_i8(w, s->i8v); - wv_put_u8(w, s->u8v); - wv_put_i16(w, s->i16v); - wv_put_u16(w, s->u16v); - wv_put_i32(w, s->i32v); - wv_put_u32(w, s->u32v); - wv_put_i64(w, s->i64v); - wv_put_u64(w, s->u64v); - wv_put_f32(w, s->f32v); - wv_put_f64(w, s->f64v); - wv_put_bool(w, s->flag); - wv_put_i32(w, s->color); -} +#include "codec_buffer.h" + +// ── comparisons ──────────────────────────────────────────────────────────── // Bitwise float equality, so NaN payloads and the sign of zero count. static int f32_bits_eq(float a, float b) { return memcmp(&a, &b, 4) == 0; } static int f64_bits_eq(double a, double b) { return memcmp(&a, &b, 8) == 0; } -static int scalars_eq(const scalars_t* a, const scalars_t* b) { - return a->i8v == b->i8v && a->u8v == b->u8v && a->i16v == b->i16v && - a->u16v == b->u16v && a->i32v == b->i32v && a->u32v == b->u32v && - a->i64v == b->i64v && a->u64v == b->u64v && - f32_bits_eq(a->f32v, b->f32v) && f64_bits_eq(a->f64v, b->f64v) && - a->flag == b->flag && a->color == b->color; +static int str_eq(codec_str a, codec_str b) { + return a.len == b.len && (a.len == 0 || memcmp(a.ptr, b.ptr, a.len) == 0); } -// The canonical fixture the producer hands out and expects back. -static const scalars_t CANONICAL_SCALARS = { - -8, 200, -16000, 60000, -2000000000, 4000000000u, - -9007199254740993LL, 18446744073709551615ULL, 1.5f, -2.25e100, 1, - weaveffi_codec_Color_Blue, -}; - -// ── Shape (rich enum) ────────────────────────────────────────────────────── - -typedef struct { - int32_t tag; - double radius; // Circle - float width, height; // Rect - char* label; // Labeled - int32_t count; // Labeled - scalars_t inner; // Nested - int has_note; // Nested - char* note; // Nested (when has_note) -} shape_t; - -static void read_shape(wv_reader* r, shape_t* s) { - memset(s, 0, sizeof *s); - s->tag = wv_get_i32(r); - switch (s->tag) { - case weaveffi_codec_Shape_Empty: - break; - case weaveffi_codec_Shape_Circle: - s->radius = wv_get_f64(r); - break; - case weaveffi_codec_Shape_Rect: - s->width = wv_get_f32(r); - s->height = wv_get_f32(r); - break; - case weaveffi_codec_Shape_Labeled: - s->label = wv_get_str(r); - s->count = wv_get_i32(r); - break; - case weaveffi_codec_Shape_Nested: - read_scalars(r, &s->inner); - s->has_note = wv_get_bool(r); - if (s->has_note) s->note = wv_get_str(r); - break; - default: - assert(0 && "unknown Shape tag"); - } +static int bytes_view_eq(codec_bytes a, codec_bytes b) { + return a.len == b.len && (a.len == 0 || memcmp(a.ptr, b.ptr, a.len) == 0); } -static void write_shape(wv_writer* w, const shape_t* s) { - wv_put_i32(w, s->tag); - switch (s->tag) { - case weaveffi_codec_Shape_Empty: - break; - case weaveffi_codec_Shape_Circle: - wv_put_f64(w, s->radius); - break; - case weaveffi_codec_Shape_Rect: - wv_put_f32(w, s->width); - wv_put_f32(w, s->height); - break; - case weaveffi_codec_Shape_Labeled: - wv_put_str(w, s->label); - wv_put_i32(w, s->count); - break; - case weaveffi_codec_Shape_Nested: - write_scalars(w, &s->inner); - wv_put_bool(w, s->has_note); - if (s->has_note) wv_put_str(w, s->note); - break; - default: - assert(0 && "unknown Shape tag"); - } +static int opt_str_eq(const codec_str* a, const codec_str* b) { + return (a == NULL) == (b == NULL) && (a == NULL || str_eq(*a, *b)); } -static int str_eq(const char* a, const char* b) { - if (a == NULL || b == NULL) return a == b; - return strcmp(a, b) == 0; +static int scalars_eq(const codec_codec_Scalars* a, const codec_codec_Scalars* b) { + return a->i8_value == b->i8_value && a->u8_value == b->u8_value && + a->i16_value == b->i16_value && a->u16_value == b->u16_value && + a->i32_value == b->i32_value && a->u32_value == b->u32_value && + a->i64_value == b->i64_value && a->u64_value == b->u64_value && + f32_bits_eq(a->f32_value, b->f32_value) && f64_bits_eq(a->f64_value, b->f64_value) && + a->flag == b->flag && a->color == b->color; } -static int shape_eq(const shape_t* a, const shape_t* b) { +static int shape_eq(const codec_codec_Shape* a, const codec_codec_Shape* b) { if (a->tag != b->tag) return 0; switch (a->tag) { - case weaveffi_codec_Shape_Empty: - return 1; - case weaveffi_codec_Shape_Circle: - return f64_bits_eq(a->radius, b->radius); - case weaveffi_codec_Shape_Rect: - return f32_bits_eq(a->width, b->width) && f32_bits_eq(a->height, b->height); - case weaveffi_codec_Shape_Labeled: - return str_eq(a->label, b->label) && a->count == b->count; - case weaveffi_codec_Shape_Nested: - return scalars_eq(&a->inner, &b->inner) && a->has_note == b->has_note && - (!a->has_note || str_eq(a->note, b->note)); - default: - return 0; + case codec_codec_Shape_Empty: + return 1; + case codec_codec_Shape_Circle: + return f64_bits_eq(a->as.Circle.radius, b->as.Circle.radius); + case codec_codec_Shape_Rect: + return f32_bits_eq(a->as.Rect.width, b->as.Rect.width) && + f32_bits_eq(a->as.Rect.height, b->as.Rect.height); + case codec_codec_Shape_Labeled: + return str_eq(a->as.Labeled.label, b->as.Labeled.label) && + a->as.Labeled.count == b->as.Labeled.count; + case codec_codec_Shape_Nested: + return scalars_eq(&a->as.Nested.inner, &b->as.Nested.inner) && + opt_str_eq(a->as.Nested.note, b->as.Nested.note); + default: + return 0; } } -static void shape_free(shape_t* s) { - free(s->label); - free(s->note); - s->label = s->note = NULL; -} - -static shape_t shape_empty(void) { - shape_t s; - memset(&s, 0, sizeof s); - s.tag = weaveffi_codec_Shape_Empty; - return s; -} - -static shape_t shape_circle(double radius) { - shape_t s = shape_empty(); - s.tag = weaveffi_codec_Shape_Circle; - s.radius = radius; - return s; -} - -static shape_t shape_rect(float w, float h) { - shape_t s = shape_empty(); - s.tag = weaveffi_codec_Shape_Rect; - s.width = w; - s.height = h; - return s; -} - -// `label` is copied so every shape_t owns its strings uniformly. -static shape_t shape_labeled(const char* label, int32_t count) { - shape_t s = shape_empty(); - s.tag = weaveffi_codec_Shape_Labeled; - s.label = strdup(label); - s.count = count; - return s; -} - -static shape_t shape_nested(const scalars_t* inner, const char* note) { - shape_t s = shape_empty(); - s.tag = weaveffi_codec_Shape_Nested; - s.inner = *inner; - s.has_note = note != NULL; - s.note = note ? strdup(note) : NULL; - return s; -} - -// ── Composite ────────────────────────────────────────────────────────────── - -typedef struct { - int32_t* v; - uint32_t n; -} i32_list_t; - -typedef struct { - char* key; - int64_t value; -} by_name_t; - -typedef struct { - int32_t key; - scalars_t value; -} by_id_t; - -typedef struct { - int has; - int value; -} opt_bool_t; - -typedef struct { - char* name; - uint8_t* blob; - uint32_t blob_len; - int has_some_i64; - int64_t some_i64; - int has_none_i64; - int64_t none_i64; - int has_some_text; - char* some_text; - char** names; - uint32_t names_n; - i32_list_t* matrix; - uint32_t matrix_n; - double* empty; - uint32_t empty_n; - by_name_t* by_name; - uint32_t by_name_n; - by_id_t* by_id; - uint32_t by_id_n; - scalars_t scalars; - shape_t shape; - shape_t* shapes; - uint32_t shapes_n; - int has_maybe_shape; - shape_t maybe_shape; - int has_maybe_list; - uint8_t* maybe_list; - uint32_t maybe_list_n; - opt_bool_t* sparse; - uint32_t sparse_n; - int32_t* colors; - uint32_t colors_n; -} composite_t; - -static void* xcalloc(size_t n, size_t size) { - void* p = calloc(n ? n : 1, size); - assert(p != NULL); - return p; -} - -static void read_composite(wv_reader* r, composite_t* c) { - memset(c, 0, sizeof *c); - c->name = wv_get_str(r); - c->blob_len = wv_get_u32(r); - c->blob = (uint8_t*)xcalloc(c->blob_len, 1); - memcpy(c->blob, wv_take(r, c->blob_len), c->blob_len); - c->has_some_i64 = wv_get_bool(r); - if (c->has_some_i64) c->some_i64 = wv_get_i64(r); - c->has_none_i64 = wv_get_bool(r); - if (c->has_none_i64) c->none_i64 = wv_get_i64(r); - c->has_some_text = wv_get_bool(r); - if (c->has_some_text) c->some_text = wv_get_str(r); - c->names_n = wv_get_u32(r); - c->names = (char**)xcalloc(c->names_n, sizeof(char*)); - for (uint32_t i = 0; i < c->names_n; i++) c->names[i] = wv_get_str(r); - c->matrix_n = wv_get_u32(r); - c->matrix = (i32_list_t*)xcalloc(c->matrix_n, sizeof(i32_list_t)); - for (uint32_t i = 0; i < c->matrix_n; i++) { - c->matrix[i].n = wv_get_u32(r); - c->matrix[i].v = (int32_t*)xcalloc(c->matrix[i].n, sizeof(int32_t)); - for (uint32_t j = 0; j < c->matrix[i].n; j++) c->matrix[i].v[j] = wv_get_i32(r); - } - c->empty_n = wv_get_u32(r); - c->empty = (double*)xcalloc(c->empty_n, sizeof(double)); - for (uint32_t i = 0; i < c->empty_n; i++) c->empty[i] = wv_get_f64(r); - c->by_name_n = wv_get_u32(r); - c->by_name = (by_name_t*)xcalloc(c->by_name_n, sizeof(by_name_t)); - for (uint32_t i = 0; i < c->by_name_n; i++) { - c->by_name[i].key = wv_get_str(r); - c->by_name[i].value = wv_get_i64(r); - } - c->by_id_n = wv_get_u32(r); - c->by_id = (by_id_t*)xcalloc(c->by_id_n, sizeof(by_id_t)); - for (uint32_t i = 0; i < c->by_id_n; i++) { - c->by_id[i].key = wv_get_i32(r); - read_scalars(r, &c->by_id[i].value); - } - read_scalars(r, &c->scalars); - read_shape(r, &c->shape); - c->shapes_n = wv_get_u32(r); - c->shapes = (shape_t*)xcalloc(c->shapes_n, sizeof(shape_t)); - for (uint32_t i = 0; i < c->shapes_n; i++) read_shape(r, &c->shapes[i]); - c->has_maybe_shape = wv_get_bool(r); - if (c->has_maybe_shape) read_shape(r, &c->maybe_shape); - c->has_maybe_list = wv_get_bool(r); - if (c->has_maybe_list) { - c->maybe_list_n = wv_get_u32(r); - c->maybe_list = (uint8_t*)xcalloc(c->maybe_list_n, 1); - for (uint32_t i = 0; i < c->maybe_list_n; i++) c->maybe_list[i] = wv_get_u8(r); +static int composite_eq(const codec_codec_Composite* a, const codec_codec_Composite* b) { + if (!str_eq(a->name, b->name) || !bytes_view_eq(a->blob, b->blob)) return 0; + if ((a->some_i64 == NULL) != (b->some_i64 == NULL)) return 0; + if (a->some_i64 && *a->some_i64 != *b->some_i64) return 0; + if ((a->none_i64 == NULL) != (b->none_i64 == NULL)) return 0; + if (a->none_i64 && *a->none_i64 != *b->none_i64) return 0; + if (!opt_str_eq(a->some_text, b->some_text)) return 0; + if (a->names.len != b->names.len) return 0; + for (size_t i = 0; i < a->names.len; i++) { + if (!str_eq(a->names.items[i], b->names.items[i])) return 0; } - c->sparse_n = wv_get_u32(r); - c->sparse = (opt_bool_t*)xcalloc(c->sparse_n, sizeof(opt_bool_t)); - for (uint32_t i = 0; i < c->sparse_n; i++) { - c->sparse[i].has = wv_get_bool(r); - if (c->sparse[i].has) c->sparse[i].value = wv_get_bool(r); + if (a->matrix.len != b->matrix.len) return 0; + for (size_t i = 0; i < a->matrix.len; i++) { + const codec_list_i32* x = &a->matrix.items[i]; + const codec_list_i32* y = &b->matrix.items[i]; + if (x->len != y->len) return 0; + for (size_t j = 0; j < x->len; j++) { + if (x->items[j] != y->items[j]) return 0; + } } - c->colors_n = wv_get_u32(r); - c->colors = (int32_t*)xcalloc(c->colors_n, sizeof(int32_t)); - for (uint32_t i = 0; i < c->colors_n; i++) c->colors[i] = wv_get_i32(r); -} - -static void write_composite(wv_writer* w, const composite_t* c) { - wv_put_str(w, c->name); - wv_put_bytes(w, c->blob, c->blob_len); - wv_put_bool(w, c->has_some_i64); - if (c->has_some_i64) wv_put_i64(w, c->some_i64); - wv_put_bool(w, c->has_none_i64); - if (c->has_none_i64) wv_put_i64(w, c->none_i64); - wv_put_bool(w, c->has_some_text); - if (c->has_some_text) wv_put_str(w, c->some_text); - wv_put_u32(w, c->names_n); - for (uint32_t i = 0; i < c->names_n; i++) wv_put_str(w, c->names[i]); - wv_put_u32(w, c->matrix_n); - for (uint32_t i = 0; i < c->matrix_n; i++) { - wv_put_u32(w, c->matrix[i].n); - for (uint32_t j = 0; j < c->matrix[i].n; j++) wv_put_i32(w, c->matrix[i].v[j]); + if (a->empty.len != b->empty.len) return 0; + for (size_t i = 0; i < a->empty.len; i++) { + if (!f64_bits_eq(a->empty.items[i], b->empty.items[i])) return 0; } - wv_put_u32(w, c->empty_n); - for (uint32_t i = 0; i < c->empty_n; i++) wv_put_f64(w, c->empty[i]); - wv_put_u32(w, c->by_name_n); - for (uint32_t i = 0; i < c->by_name_n; i++) { - wv_put_str(w, c->by_name[i].key); - wv_put_i64(w, c->by_name[i].value); + if (a->by_name.len != b->by_name.len) return 0; + for (size_t i = 0; i < a->by_name.len; i++) { + if (!str_eq(a->by_name.keys[i], b->by_name.keys[i]) || + a->by_name.values[i] != b->by_name.values[i]) { + return 0; + } } - wv_put_u32(w, c->by_id_n); - for (uint32_t i = 0; i < c->by_id_n; i++) { - wv_put_i32(w, c->by_id[i].key); - write_scalars(w, &c->by_id[i].value); + if (a->by_id.len != b->by_id.len) return 0; + for (size_t i = 0; i < a->by_id.len; i++) { + if (a->by_id.keys[i] != b->by_id.keys[i] || + !scalars_eq(&a->by_id.values[i], &b->by_id.values[i])) { + return 0; + } } - write_scalars(w, &c->scalars); - write_shape(w, &c->shape); - wv_put_u32(w, c->shapes_n); - for (uint32_t i = 0; i < c->shapes_n; i++) write_shape(w, &c->shapes[i]); - wv_put_bool(w, c->has_maybe_shape); - if (c->has_maybe_shape) write_shape(w, &c->maybe_shape); - wv_put_bool(w, c->has_maybe_list); - if (c->has_maybe_list) { - wv_put_u32(w, c->maybe_list_n); - for (uint32_t i = 0; i < c->maybe_list_n; i++) wv_put_u8(w, c->maybe_list[i]); + if (!scalars_eq(&a->scalars, &b->scalars) || !shape_eq(&a->shape, &b->shape)) return 0; + if (a->shapes.len != b->shapes.len) return 0; + for (size_t i = 0; i < a->shapes.len; i++) { + if (!shape_eq(&a->shapes.items[i], &b->shapes.items[i])) return 0; } - wv_put_u32(w, c->sparse_n); - for (uint32_t i = 0; i < c->sparse_n; i++) { - wv_put_bool(w, c->sparse[i].has); - if (c->sparse[i].has) wv_put_bool(w, c->sparse[i].value); + if ((a->maybe_shape == NULL) != (b->maybe_shape == NULL)) return 0; + if (a->maybe_shape && !shape_eq(a->maybe_shape, b->maybe_shape)) return 0; + if ((a->maybe_list == NULL) != (b->maybe_list == NULL)) return 0; + if (a->maybe_list && !bytes_view_eq(*a->maybe_list, *b->maybe_list)) return 0; + if (a->sparse.len != b->sparse.len) return 0; + for (size_t i = 0; i < a->sparse.len; i++) { + const bool* x = a->sparse.items[i]; + const bool* y = b->sparse.items[i]; + if ((x == NULL) != (y == NULL) || (x && *x != *y)) return 0; } - wv_put_u32(w, c->colors_n); - for (uint32_t i = 0; i < c->colors_n; i++) wv_put_i32(w, c->colors[i]); -} - -static int composite_eq(const composite_t* a, const composite_t* b) { - if (!str_eq(a->name, b->name)) return 0; - if (a->blob_len != b->blob_len || memcmp(a->blob, b->blob, a->blob_len) != 0) return 0; - if (a->has_some_i64 != b->has_some_i64 || (a->has_some_i64 && a->some_i64 != b->some_i64)) - return 0; - if (a->has_none_i64 != b->has_none_i64 || (a->has_none_i64 && a->none_i64 != b->none_i64)) - return 0; - if (a->has_some_text != b->has_some_text || - (a->has_some_text && !str_eq(a->some_text, b->some_text))) - return 0; - if (a->names_n != b->names_n) return 0; - for (uint32_t i = 0; i < a->names_n; i++) - if (!str_eq(a->names[i], b->names[i])) return 0; - if (a->matrix_n != b->matrix_n) return 0; - for (uint32_t i = 0; i < a->matrix_n; i++) { - if (a->matrix[i].n != b->matrix[i].n) return 0; - for (uint32_t j = 0; j < a->matrix[i].n; j++) - if (a->matrix[i].v[j] != b->matrix[i].v[j]) return 0; + if (a->colors.len != b->colors.len) return 0; + for (size_t i = 0; i < a->colors.len; i++) { + if (a->colors.items[i] != b->colors.items[i]) return 0; } - if (a->empty_n != b->empty_n) return 0; - for (uint32_t i = 0; i < a->empty_n; i++) - if (!f64_bits_eq(a->empty[i], b->empty[i])) return 0; - if (a->by_name_n != b->by_name_n) return 0; - for (uint32_t i = 0; i < a->by_name_n; i++) - if (!str_eq(a->by_name[i].key, b->by_name[i].key) || - a->by_name[i].value != b->by_name[i].value) - return 0; - if (a->by_id_n != b->by_id_n) return 0; - for (uint32_t i = 0; i < a->by_id_n; i++) - if (a->by_id[i].key != b->by_id[i].key || - !scalars_eq(&a->by_id[i].value, &b->by_id[i].value)) - return 0; - if (!scalars_eq(&a->scalars, &b->scalars)) return 0; - if (!shape_eq(&a->shape, &b->shape)) return 0; - if (a->shapes_n != b->shapes_n) return 0; - for (uint32_t i = 0; i < a->shapes_n; i++) - if (!shape_eq(&a->shapes[i], &b->shapes[i])) return 0; - if (a->has_maybe_shape != b->has_maybe_shape || - (a->has_maybe_shape && !shape_eq(&a->maybe_shape, &b->maybe_shape))) - return 0; - if (a->has_maybe_list != b->has_maybe_list) return 0; - if (a->has_maybe_list && - (a->maybe_list_n != b->maybe_list_n || - memcmp(a->maybe_list, b->maybe_list, a->maybe_list_n) != 0)) - return 0; - if (a->sparse_n != b->sparse_n) return 0; - for (uint32_t i = 0; i < a->sparse_n; i++) - if (a->sparse[i].has != b->sparse[i].has || - (a->sparse[i].has && a->sparse[i].value != b->sparse[i].value)) - return 0; - if (a->colors_n != b->colors_n) return 0; - for (uint32_t i = 0; i < a->colors_n; i++) - if (a->colors[i] != b->colors[i]) return 0; return 1; } -static void composite_free(composite_t* c) { - free(c->name); - free(c->blob); - free(c->some_text); - for (uint32_t i = 0; i < c->names_n; i++) free(c->names[i]); - free(c->names); - for (uint32_t i = 0; i < c->matrix_n; i++) free(c->matrix[i].v); - free(c->matrix); - free(c->empty); - for (uint32_t i = 0; i < c->by_name_n; i++) free(c->by_name[i].key); - free(c->by_name); - free(c->by_id); - shape_free(&c->shape); - for (uint32_t i = 0; i < c->shapes_n; i++) shape_free(&c->shapes[i]); - free(c->shapes); - if (c->has_maybe_shape) shape_free(&c->maybe_shape); - free(c->maybe_list); - free(c->sparse); - free(c->colors); - memset(c, 0, sizeof *c); -} +// The canonical fixture the producer hands out and expects back. +static const codec_codec_Scalars CANONICAL_SCALARS = { + -8, 200, -16000, 60000, -2000000000, 4000000000u, -9007199254740993LL, UINT64_MAX, + 1.5f, -2.25e100, true, codec_codec_Color_Blue, +}; // ── ABI helpers ──────────────────────────────────────────────────────────── -// Decode a buffered return, checking it was consumed exactly, and free it. -#define TAKE_BUFFER(ptr, len, reader_body) \ +// A function taking one buffered value and returning one buffered value. +typedef const uint8_t* (*buffer_fn)(const uint8_t*, size_t, size_t*, codec_error*); + +// Call `fn` with the bytes in `w` and return its owned result, asserting it +// succeeded and (when `exact`) echoed the input byte for byte. The caller +// decodes `*out_len` bytes and releases them with codec_free_bytes. +static const uint8_t* call_buffer(buffer_fn fn, codec_writer* w, size_t* out_len, int exact) { + assert(!w->failed); + codec_error err = {0}; + const uint8_t* p = fn(w->ptr, w->len, out_len, &err); + assert(err.code == 0 && p != NULL); + if (exact) assert(*out_len == w->len && memcmp(p, w->ptr, w->len) == 0); + codec_writer_free(w); + return p; +} + +#define ENCODE(write, value, w) \ + codec_writer w; \ + memset(&w, 0, sizeof w); \ + write(&w, value) + +// Decode an owned buffer with `decode`, then release it. +#define TAKE(decode, p, len, out) \ do { \ - assert((ptr) != NULL); \ - wv_reader r_; \ - wv_r_init(&r_, (ptr), (len)); \ - reader_body; \ - wv_r_expect_end(&r_); \ - weaveffi_free_bytes((uint8_t*)(ptr), (len)); \ + assert(decode(p, len, out)); \ + codec_free_bytes((uint8_t*)(p), len); \ } while (0) -static void fetch_sample_scalars(scalars_t* out) { - weaveffi_error err = {0}; +static void fetch_scalars(codec_codec_Scalars* out) { + codec_error err = {0}; size_t len = 0; - const uint8_t* p = weaveffi_codec_sample_scalars(&len, &err); + const uint8_t* p = codec_codec_sample_scalars(&len, &err); assert(err.code == 0); - TAKE_BUFFER(p, len, read_scalars(&r_, out)); + TAKE(codec_codec_Scalars_decode, p, len, out); } -static void fetch_sample_composite(composite_t* out) { - weaveffi_error err = {0}; +static void fetch_composite(codec_codec_Composite* out) { + codec_error err = {0}; size_t len = 0; - const uint8_t* p = weaveffi_codec_sample_composite(&len, &err); + const uint8_t* p = codec_codec_sample_composite(&len, &err); assert(err.code == 0); - TAKE_BUFFER(p, len, read_composite(&r_, out)); + TAKE(codec_codec_Composite_decode, p, len, out); } -static void roundtrip_scalars(const scalars_t* in, scalars_t* out) { - weaveffi_error err = {0}; - wv_writer w; - wv_w_init(&w); - write_scalars(&w, in); +static void roundtrip_scalars(const codec_codec_Scalars* in, codec_codec_Scalars* out) { + ENCODE(codec_codec_Scalars_write, in, w); size_t len = 0; - const uint8_t* p = weaveffi_codec_roundtrip_scalars(w.buf, w.len, &len, &err); - assert(err.code == 0); - assert(len == w.len && memcmp(p, w.buf, len) == 0 && - "our encoding is byte-identical to the producer's"); - wv_w_free(&w); - TAKE_BUFFER(p, len, read_scalars(&r_, out)); + const uint8_t* p = call_buffer(codec_codec_roundtrip_scalars, &w, &len, 1); + TAKE(codec_codec_Scalars_decode, p, len, out); } -static void roundtrip_composite(const composite_t* in, composite_t* out) { - weaveffi_error err = {0}; - wv_writer w; - wv_w_init(&w); - write_composite(&w, in); +static void roundtrip_composite(const codec_codec_Composite* in, codec_codec_Composite* out) { + ENCODE(codec_codec_Composite_write, in, w); size_t len = 0; - const uint8_t* p = weaveffi_codec_roundtrip_composite(w.buf, w.len, &len, &err); - assert(err.code == 0); - assert(len == w.len && memcmp(p, w.buf, len) == 0 && - "our encoding is byte-identical to the producer's"); - wv_w_free(&w); - TAKE_BUFFER(p, len, read_composite(&r_, out)); + const uint8_t* p = call_buffer(codec_codec_roundtrip_composite, &w, &len, 1); + TAKE(codec_codec_Composite_decode, p, len, out); } -static void roundtrip_shape(const shape_t* in, shape_t* out) { - weaveffi_error err = {0}; - wv_writer w; - wv_w_init(&w); - write_shape(&w, in); +static void roundtrip_shape(const codec_codec_Shape* in, codec_codec_Shape* out) { + ENCODE(codec_codec_Shape_write, in, w); size_t len = 0; - const uint8_t* p = weaveffi_codec_roundtrip_shape(w.buf, w.len, &len, &err); - assert(err.code == 0); - assert(len == w.len && memcmp(p, w.buf, len) == 0); - wv_w_free(&w); - TAKE_BUFFER(p, len, read_shape(&r_, out)); + const uint8_t* p = call_buffer(codec_codec_roundtrip_shape, &w, &len, 1); + TAKE(codec_codec_Shape_decode, p, len, out); } -static int verify_scalars(const scalars_t* s, weaveffi_error* err) { - wv_writer w; - wv_w_init(&w); - write_scalars(&w, s); - int ok = weaveffi_codec_verify_scalars(w.buf, w.len, err); - wv_w_free(&w); +static int verify_scalars(const codec_codec_Scalars* s, codec_error* err) { + ENCODE(codec_codec_Scalars_write, s, w); + int ok = codec_codec_verify_scalars(w.ptr, w.len, err); + codec_writer_free(&w); return ok; } -static int verify_composite(const composite_t* c, weaveffi_error* err) { - wv_writer w; - wv_w_init(&w); - write_composite(&w, c); - int ok = weaveffi_codec_verify_composite(w.buf, w.len, err); - wv_w_free(&w); +static int verify_composite(const codec_codec_Composite* c, codec_error* err) { + ENCODE(codec_codec_Composite_write, c, w); + int ok = codec_codec_verify_composite(w.ptr, w.len, err); + codec_writer_free(&w); return ok; } -static char* describe_shape(const shape_t* s) { - weaveffi_error err = {0}; - wv_writer w; - wv_w_init(&w); - write_shape(&w, s); - const char* text = weaveffi_codec_describe_shape(w.buf, w.len, &err); - assert(err.code == 0 && text != NULL); - wv_w_free(&w); - char* copy = strdup(text); - weaveffi_free_string(text); - return copy; -} - -// ── Holder (objects inside buffers) ──────────────────────────────────────── - -typedef struct { - weaveffi_codec_Token* primary; - weaveffi_codec_Token* spare; // NULL when absent - weaveffi_codec_Token* many[8]; - uint32_t many_n; -} holder_t; - -// Decoding adopts one strong reference per token. -static void read_holder(wv_reader* r, holder_t* h) { - memset(h, 0, sizeof *h); - h->primary = (weaveffi_codec_Token*)wv_get_obj(r); - h->spare = wv_get_bool(r) ? (weaveffi_codec_Token*)wv_get_obj(r) : NULL; - h->many_n = wv_get_u32(r); - assert(h->many_n <= 8); - for (uint32_t i = 0; i < h->many_n; i++) h->many[i] = (weaveffi_codec_Token*)wv_get_obj(r); -} - -// Encoding mints a fresh reference per token with `_clone`; the holder's -// own references are untouched. -static void write_holder(wv_writer* w, const holder_t* h) { - wv_put_obj(w, weaveffi_codec_Token_clone(h->primary)); - wv_put_bool(w, h->spare != NULL); - if (h->spare) wv_put_obj(w, weaveffi_codec_Token_clone(h->spare)); - wv_put_u32(w, h->many_n); - for (uint32_t i = 0; i < h->many_n; i++) wv_put_obj(w, weaveffi_codec_Token_clone(h->many[i])); -} - -static void holder_release(holder_t* h) { - weaveffi_codec_Token_destroy(h->primary); - weaveffi_codec_Token_destroy(h->spare); - for (uint32_t i = 0; i < h->many_n; i++) weaveffi_codec_Token_destroy(h->many[i]); - memset(h, 0, sizeof *h); -} - -static int64_t token_value(const weaveffi_codec_Token* t) { - weaveffi_error err = {0}; - int64_t v = weaveffi_codec_Token_value(t, &err); - assert(err.code == 0); - return v; +// describe_* return an owned (ptr, len) string; copy it out NUL-terminated. +static char* take_string(const uint8_t* p, size_t len) { + char* s = (char*)calloc(len + 1, 1); + assert(s != NULL); + if (len) memcpy(s, p, len); + codec_free_bytes((uint8_t*)p, len); + return s; } -static void make_holder(int64_t base, int with_spare, holder_t* out) { - weaveffi_error err = {0}; +static char* describe_shape(const codec_codec_Shape* s) { + ENCODE(codec_codec_Shape_write, s, w); + codec_error err = {0}; size_t len = 0; - const uint8_t* p = weaveffi_codec_make_holder(base, with_spare, &len, &err); + const uint8_t* p = codec_codec_describe_shape(w.ptr, w.len, &len, &err); + codec_writer_free(&w); assert(err.code == 0); - TAKE_BUFFER(p, len, read_holder(&r_, out)); + return take_string(p, len); } -// ── main ─────────────────────────────────────────────────────────────────── +static codec_codec_Shape shape_of(codec_codec_Shape_Tag tag) { + codec_codec_Shape s; + memset(&s, 0, sizeof s); + s.tag = tag; + return s; +} -int main(void) { - weaveffi_error err = {0}; - - assert(weaveffi_abi_version() == 2u && WEAVEFFI_ABI_VERSION == 2u); - - // ── Scalars: producer encodes, consumer decodes ────────────────────── - scalars_t s; - fetch_sample_scalars(&s); - assert(s.i8v == -8); - assert(s.u8v == 200); - assert(s.i16v == -16000); - assert(s.u16v == 60000); - assert(s.i32v == -2000000000); - assert(s.u32v == 4000000000u); - assert(s.i64v == -9007199254740993LL); - assert(s.u64v == UINT64_MAX); - assert(s.f32v == 1.5f); - assert(s.f64v == -2.25e100); - assert(s.flag == 1); - assert(s.color == weaveffi_codec_Color_Blue && s.color == 7); +// ── sections ─────────────────────────────────────────────────────────────── + +static void scalars(void) { + codec_error err = {0}; + + // Producer encodes, consumer decodes. + codec_codec_Scalars s; + fetch_scalars(&s); + assert(s.i8_value == -8); + assert(s.u8_value == 200); + assert(s.i16_value == -16000); + assert(s.u16_value == 60000); + assert(s.i32_value == -2000000000); + assert(s.u32_value == 4000000000u); + assert(s.i64_value == -9007199254740993LL); + assert(s.u64_value == UINT64_MAX); + assert(s.f32_value == 1.5f); + assert(s.f64_value == -2.25e100); + assert(s.flag); + assert(s.color == codec_codec_Color_Blue && s.color == 7); assert(scalars_eq(&s, &CANONICAL_SCALARS)); // Consumer encodes, producer decodes and compares to its canonical value. assert(verify_scalars(&s, &err) && err.code == 0); - // Consumer encodes, producer re-encodes: field-by-field equality. - scalars_t s2; + // Consumer encodes, producer re-encodes the same bytes. + codec_codec_Scalars s2; roundtrip_scalars(&s, &s2); assert(scalars_eq(&s, &s2)); - // A one-field change is a Mismatch (code 1) with the doc-comment message. - s2.flag = 0; + // A one-field change is a Mismatch (code 1) with the Display message. + s2.flag = false; assert(!verify_scalars(&s2, &err)); - assert(err.code == weaveffi_codec_CodecError_Mismatch && err.code == 1); + assert(err.code == codec_codec_CodecError_Mismatch && err.code == 1); assert(err.message != NULL && strcmp(err.message, "value does not match the canonical fixture") == 0); assert(err.payload_ptr == NULL && err.payload_len == 0); - weaveffi_error_clear(&err); + codec_error_clear(&err); // Hand-built edge scalars: extremes, NaN, infinities, negative zero. - scalars_t edge = { + codec_codec_Scalars edge = { INT8_MIN, UINT8_MAX, INT16_MIN, UINT16_MAX, INT32_MIN, UINT32_MAX, - INT64_MIN, UINT64_MAX, NAN, -0.0, 0, weaveffi_codec_Color_Red, + INT64_MIN, UINT64_MAX, NAN, -0.0, false, codec_codec_Color_Red, }; - scalars_t edge2; - roundtrip_scalars(&edge, &edge2); - assert(scalars_eq(&edge, &edge2)); - assert(isnan(edge2.f32v) && signbit(edge2.f64v) && edge2.f64v == 0.0); - edge.i8v = INT8_MAX; - edge.i16v = INT16_MAX; - edge.i32v = INT32_MAX; - edge.i64v = INT64_MAX; - edge.u8v = 0; - edge.u16v = 0; - edge.u32v = 0; - edge.u64v = 0; - edge.f32v = -INFINITY; - edge.f64v = INFINITY; - edge.color = weaveffi_codec_Color_Green; - roundtrip_scalars(&edge, &edge2); - assert(scalars_eq(&edge, &edge2)); - assert(isinf(edge2.f32v) && edge2.f32v < 0 && isinf(edge2.f64v) && edge2.f64v > 0); - edge.f64v = NAN; - edge.f32v = 0.0f; - roundtrip_scalars(&edge, &edge2); - assert(isnan(edge2.f64v) && scalars_eq(&edge, &edge2)); - - // ── Composite: producer encodes, consumer decodes ──────────────────── - composite_t c; - fetch_sample_composite(&c); - assert(strcmp(c.name, "h\xC3\xA9llo w\xC3\xB6rld \xE2\x9C\x93") == 0); + roundtrip_scalars(&edge, &s2); + assert(scalars_eq(&edge, &s2)); + assert(isnan(s2.f32_value) && signbit(s2.f64_value) && s2.f64_value == 0.0); + edge.i8_value = INT8_MAX; + edge.i16_value = INT16_MAX; + edge.i32_value = INT32_MAX; + edge.i64_value = INT64_MAX; + edge.u8_value = 0; + edge.u16_value = 0; + edge.u32_value = 0; + edge.u64_value = 0; + edge.f32_value = -INFINITY; + edge.f64_value = INFINITY; + edge.color = codec_codec_Color_Green; + roundtrip_scalars(&edge, &s2); + assert(scalars_eq(&edge, &s2)); + assert(isinf(s2.f32_value) && s2.f32_value < 0 && isinf(s2.f64_value) && s2.f64_value > 0); +} + +static void composite(void) { + codec_error err = {0}; + + // Producer encodes, consumer decodes. + codec_codec_Composite c; + fetch_composite(&c); + assert(strcmp(c.name.ptr, "h\xC3\xA9llo w\xC3\xB6rld \xE2\x9C\x93") == 0); const uint8_t blob[6] = {0, 1, 2, 253, 254, 255}; - assert(c.blob_len == 6 && memcmp(c.blob, blob, 6) == 0); - assert(c.has_some_i64 && c.some_i64 == INT64_MIN); - assert(!c.has_none_i64); - assert(c.has_some_text && strcmp(c.some_text, "") == 0); - assert(c.names_n == 3 && strcmp(c.names[0], "a") == 0 && strcmp(c.names[1], "") == 0 && - strcmp(c.names[2], "ccc") == 0); - assert(c.matrix_n == 3); - assert(c.matrix[0].n == 3 && c.matrix[0].v[0] == 1 && c.matrix[0].v[1] == 2 && - c.matrix[0].v[2] == 3); - assert(c.matrix[1].n == 0); - assert(c.matrix[2].n == 1 && c.matrix[2].v[0] == -4); - assert(c.empty_n == 0); + assert(c.blob.len == 6 && memcmp(c.blob.ptr, blob, 6) == 0); + assert(c.some_i64 != NULL && *c.some_i64 == INT64_MIN); + assert(c.none_i64 == NULL); + assert(c.some_text != NULL && c.some_text->len == 0 && c.some_text->ptr[0] == '\0'); + assert(c.names.len == 3 && strcmp(c.names.items[0].ptr, "a") == 0 && + c.names.items[1].len == 0 && strcmp(c.names.items[2].ptr, "ccc") == 0); + assert(c.matrix.len == 3); + assert(c.matrix.items[0].len == 3 && c.matrix.items[0].items[0] == 1 && + c.matrix.items[0].items[1] == 2 && c.matrix.items[0].items[2] == 3); + assert(c.matrix.items[1].len == 0 && c.matrix.items[1].items == NULL); + assert(c.matrix.items[2].len == 1 && c.matrix.items[2].items[0] == -4); + assert(c.empty.len == 0); // BTreeMap keys arrive sorted. - assert(c.by_name_n == 3); - assert(strcmp(c.by_name[0].key, "neg") == 0 && c.by_name[0].value == -3); - assert(strcmp(c.by_name[1].key, "one") == 0 && c.by_name[1].value == 1); - assert(strcmp(c.by_name[2].key, "two") == 0 && c.by_name[2].value == 2); - assert(c.by_id_n == 2); - assert(c.by_id[0].key == -1 && scalars_eq(&c.by_id[0].value, &CANONICAL_SCALARS)); - assert(c.by_id[1].key == 42 && c.by_id[1].value.flag == 0 && - c.by_id[1].value.u64v == UINT64_MAX); + assert(c.by_name.len == 3); + assert(strcmp(c.by_name.keys[0].ptr, "neg") == 0 && c.by_name.values[0] == -3); + assert(strcmp(c.by_name.keys[1].ptr, "one") == 0 && c.by_name.values[1] == 1); + assert(strcmp(c.by_name.keys[2].ptr, "two") == 0 && c.by_name.values[2] == 2); + assert(c.by_id.len == 2); + assert(c.by_id.keys[0] == -1 && scalars_eq(&c.by_id.values[0], &CANONICAL_SCALARS)); + assert(c.by_id.keys[1] == 42 && !c.by_id.values[1].flag && + c.by_id.values[1].u64_value == UINT64_MAX); assert(scalars_eq(&c.scalars, &CANONICAL_SCALARS)); - assert(c.shape.tag == weaveffi_codec_Shape_Labeled && strcmp(c.shape.label, "tag") == 0 && - c.shape.count == 3); - assert(c.shapes_n == 5); - assert(c.shapes[0].tag == weaveffi_codec_Shape_Empty); - assert(c.shapes[1].tag == weaveffi_codec_Shape_Circle && c.shapes[1].radius == 2.5); - assert(c.shapes[2].tag == weaveffi_codec_Shape_Rect && c.shapes[2].width == 1.0f && - c.shapes[2].height == 0.5f); - assert(c.shapes[3].tag == weaveffi_codec_Shape_Labeled && strcmp(c.shapes[3].label, "") == 0 && - c.shapes[3].count == -1); - assert(c.shapes[4].tag == weaveffi_codec_Shape_Nested && - scalars_eq(&c.shapes[4].inner, &CANONICAL_SCALARS) && c.shapes[4].has_note && - strcmp(c.shapes[4].note, "n") == 0); - assert(c.has_maybe_shape && c.maybe_shape.tag == weaveffi_codec_Shape_Nested && - !c.maybe_shape.has_note && scalars_eq(&c.maybe_shape.inner, &CANONICAL_SCALARS)); - assert(c.has_maybe_list && c.maybe_list_n == 2 && c.maybe_list[0] == 9 && c.maybe_list[1] == 8); - assert(c.sparse_n == 3); - assert(c.sparse[0].has && c.sparse[0].value == 1); - assert(!c.sparse[1].has); - assert(c.sparse[2].has && c.sparse[2].value == 0); - assert(c.colors_n == 3 && c.colors[0] == weaveffi_codec_Color_Red && - c.colors[1] == weaveffi_codec_Color_Green && c.colors[2] == weaveffi_codec_Color_Blue); + assert(c.shape.tag == codec_codec_Shape_Labeled && + strcmp(c.shape.as.Labeled.label.ptr, "tag") == 0 && c.shape.as.Labeled.count == 3); + assert(c.shapes.len == 5); + const codec_codec_Shape* sh = c.shapes.items; + assert(sh[0].tag == codec_codec_Shape_Empty); + assert(sh[1].tag == codec_codec_Shape_Circle && sh[1].as.Circle.radius == 2.5); + assert(sh[2].tag == codec_codec_Shape_Rect && sh[2].as.Rect.width == 1.0f && + sh[2].as.Rect.height == 0.5f); + assert(sh[3].tag == codec_codec_Shape_Labeled && sh[3].as.Labeled.label.len == 0 && + sh[3].as.Labeled.count == -1); + assert(sh[4].tag == codec_codec_Shape_Nested && + scalars_eq(&sh[4].as.Nested.inner, &CANONICAL_SCALARS) && + sh[4].as.Nested.note != NULL && strcmp(sh[4].as.Nested.note->ptr, "n") == 0); + assert(c.maybe_shape != NULL && c.maybe_shape->tag == codec_codec_Shape_Nested && + c.maybe_shape->as.Nested.note == NULL && + scalars_eq(&c.maybe_shape->as.Nested.inner, &CANONICAL_SCALARS)); + assert(c.maybe_list != NULL && c.maybe_list->len == 2 && c.maybe_list->ptr[0] == 9 && + c.maybe_list->ptr[1] == 8); + assert(c.sparse.len == 3); + assert(c.sparse.items[0] != NULL && *c.sparse.items[0] == true); + assert(c.sparse.items[1] == NULL); + assert(c.sparse.items[2] != NULL && *c.sparse.items[2] == false); + assert(c.colors.len == 3 && c.colors.items[0] == codec_codec_Color_Red && + c.colors.items[1] == codec_codec_Color_Green && + c.colors.items[2] == codec_codec_Color_Blue); // Consumer encodes, producer decodes. assert(verify_composite(&c, &err) && err.code == 0); - // Consumer encodes, producer re-encodes, consumer decodes again. - composite_t c2; + // Consumer encodes, producer re-encodes the same bytes, consumer decodes. + codec_codec_Composite c2; roundtrip_composite(&c, &c2); assert(composite_eq(&c, &c2)); // Perturb one deeply nested field: Mismatch. - c2.sparse[1].has = 1; - c2.sparse[1].value = 1; + *c2.sparse.items[2] = true; assert(!verify_composite(&c2, &err)); - assert(err.code == weaveffi_codec_CodecError_Mismatch); - weaveffi_error_clear(&err); - composite_free(&c2); + assert(err.code == codec_codec_CodecError_Mismatch); + codec_error_clear(&err); + codec_codec_Composite_free(&c2); // describe_composite renders what the producer saw. { - wv_writer w; - wv_w_init(&w); - write_composite(&w, &c); - const char* text = weaveffi_codec_describe_composite(w.buf, w.len, &err); - assert(err.code == 0 && text != NULL); + ENCODE(codec_codec_Composite_write, &c, w); + size_t len = 0; + const uint8_t* p = codec_codec_describe_composite(w.ptr, w.len, &len, &err); + codec_writer_free(&w); + assert(err.code == 0); + char* text = take_string(p, len); assert(strstr(text, "name: \"h\xC3\xA9llo w\xC3\xB6rld \xE2\x9C\x93\"") != NULL); assert(strstr(text, "some_i64: Some(-9223372036854775808)") != NULL); assert(strstr(text, "shape: Labeled { label: \"tag\", count: 3 }") != NULL); - weaveffi_free_string(text); - wv_w_free(&w); + free(text); } - composite_free(&c); - - // Hand-built edge composite: everything empty or absent, unicode name, - // maps with extreme keys, a list of absent optionals. - composite_t e; + codec_codec_Composite_free(&c); + assert(c.names.items == NULL && c.some_i64 == NULL && "free zeroes the value"); +} + +static void edge_composite(void) { + codec_error err = {0}; + + // Hand-built edge composite borrowing caller memory: everything empty or + // absent, a name with interior NUL bytes, maps with extreme keys, a list + // of absent optionals, NaN and negative zero. + static const char name[] = "\xE6\x97\xA5\xE6\x9C\xAC\0nul\0 \xF0\x9F\x8E\x89"; + int64_t some = INT64_MAX; + int64_t none = -1; + int32_t row[2] = {INT32_MIN, INT32_MAX}; + codec_list_i32 rows[1] = {{row, 2}}; + double floats[2] = {-0.0, NAN}; + codec_str keys[1] = {{"", 0}}; + int64_t values[1] = {INT64_MIN}; + codec_bytes empty_bytes = {NULL, 0}; + bool* sparse[2] = {NULL, NULL}; + + codec_codec_Composite e; memset(&e, 0, sizeof e); - e.name = strdup("\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E \xF0\x9F\x8E\x89"); // 日本語 🎉 - e.blob = (uint8_t*)xcalloc(0, 1); - e.blob_len = 0; - e.has_some_i64 = 1; - e.some_i64 = INT64_MAX; - e.has_none_i64 = 1; - e.none_i64 = -1; - e.has_some_text = 0; - e.names_n = 0; - e.names = (char**)xcalloc(0, sizeof(char*)); - e.matrix_n = 1; - e.matrix = (i32_list_t*)xcalloc(1, sizeof(i32_list_t)); - e.matrix[0].n = 2; - e.matrix[0].v = (int32_t*)xcalloc(2, sizeof(int32_t)); - e.matrix[0].v[0] = INT32_MIN; - e.matrix[0].v[1] = INT32_MAX; - e.empty_n = 2; - e.empty = (double*)xcalloc(2, sizeof(double)); - e.empty[0] = -0.0; - e.empty[1] = NAN; - e.by_name_n = 1; - e.by_name = (by_name_t*)xcalloc(1, sizeof(by_name_t)); - e.by_name[0].key = strdup(""); - e.by_name[0].value = INT64_MIN; - e.by_id_n = 0; - e.by_id = (by_id_t*)xcalloc(0, sizeof(by_id_t)); - e.scalars = edge; - e.shape = shape_empty(); - e.shapes_n = 0; - e.shapes = (shape_t*)xcalloc(0, sizeof(shape_t)); - e.has_maybe_shape = 0; - e.has_maybe_list = 1; - e.maybe_list_n = 0; - e.maybe_list = (uint8_t*)xcalloc(0, 1); - e.sparse_n = 2; - e.sparse = (opt_bool_t*)xcalloc(2, sizeof(opt_bool_t)); - e.colors_n = 0; - e.colors = (int32_t*)xcalloc(0, sizeof(int32_t)); - composite_t e2; + e.name.ptr = name; + e.name.len = sizeof name - 1; + e.some_i64 = &some; + e.none_i64 = &none; + e.matrix.items = rows; + e.matrix.len = 1; + e.empty.items = floats; + e.empty.len = 2; + e.by_name.keys = keys; + e.by_name.values = values; + e.by_name.len = 1; + e.scalars = CANONICAL_SCALARS; + e.shape = shape_of(codec_codec_Shape_Empty); + e.maybe_list = &empty_bytes; + e.sparse.items = sparse; + e.sparse.len = 2; + + codec_codec_Composite e2; roundtrip_composite(&e, &e2); assert(composite_eq(&e, &e2)); - assert(strcmp(e2.name, e.name) == 0 && e2.blob_len == 0 && e2.has_maybe_list && - e2.maybe_list_n == 0 && isnan(e2.empty[1]) && signbit(e2.empty[0])); + assert(e2.name.len == sizeof name - 1 && memcmp(e2.name.ptr, name, sizeof name - 1) == 0); + assert(e2.blob.len == 0 && e2.maybe_list != NULL && e2.maybe_list->len == 0); + assert(isnan(e2.empty.items[1]) && signbit(e2.empty.items[0])); + assert(e2.sparse.len == 2 && e2.sparse.items[0] == NULL && e2.sparse.items[1] == NULL); assert(!verify_composite(&e, &err) && err.code == 1); - weaveffi_error_clear(&err); - composite_free(&e2); - composite_free(&e); - - // ── Shape variants through roundtrip_shape and describe_shape ─────── - shape_t variants[5]; - variants[0] = shape_empty(); - variants[1] = shape_circle(2.5); - variants[2] = shape_rect(1.0f, 0.5f); - variants[3] = shape_labeled("tag", 3); - variants[4] = shape_nested(&CANONICAL_SCALARS, "n"); + codec_error_clear(&err); + codec_codec_Composite_free(&e2); +} + +static void shapes(void) { + codec_error err = {0}; + codec_str n = codec_str_of("n"); + codec_codec_Shape v[5]; + v[0] = shape_of(codec_codec_Shape_Empty); + v[1] = shape_of(codec_codec_Shape_Circle); + v[1].as.Circle.radius = 2.5; + v[2] = shape_of(codec_codec_Shape_Rect); + v[2].as.Rect.width = 1.0f; + v[2].as.Rect.height = 0.5f; + v[3] = shape_of(codec_codec_Shape_Labeled); + v[3].as.Labeled.label = codec_str_of("tag"); + v[3].as.Labeled.count = 3; + v[4] = shape_of(codec_codec_Shape_Nested); + v[4].as.Nested.inner = CANONICAL_SCALARS; + v[4].as.Nested.note = &n; const char* descriptions[5] = { "Empty", "Circle { radius: 2.5 }", @@ -841,9 +464,9 @@ int main(void) { NULL, }; for (int i = 0; i < 5; i++) { - shape_t back; - roundtrip_shape(&variants[i], &back); - assert(shape_eq(&variants[i], &back)); + codec_codec_Shape back; + roundtrip_shape(&v[i], &back); + assert(shape_eq(&v[i], &back)); char* text = describe_shape(&back); if (descriptions[i]) { assert(strcmp(text, descriptions[i]) == 0); @@ -852,241 +475,265 @@ int main(void) { strstr(text, "note: Some(\"n\") }") != NULL); } free(text); - shape_free(&back); - } - { - shape_t no_note = shape_nested(&edge, NULL); - shape_t back; - roundtrip_shape(&no_note, &back); - assert(shape_eq(&no_note, &back) && !back.has_note); - shape_free(&back); - shape_free(&no_note); - - shape_t huge = shape_labeled("", INT32_MIN); - roundtrip_shape(&huge, &back); - assert(shape_eq(&huge, &back)); - shape_free(&back); - shape_free(&huge); + codec_codec_Shape_free(&back); } - // roundtrip_shapes: a list of every variant. - { - wv_writer w; - wv_w_init(&w); - wv_put_u32(&w, 5); - for (int i = 0; i < 5; i++) write_shape(&w, &variants[i]); - size_t len = 0; - const uint8_t* p = weaveffi_codec_roundtrip_shapes(w.buf, w.len, &len, &err); - assert(err.code == 0); - assert(len == w.len && memcmp(p, w.buf, len) == 0); - wv_w_free(&w); - TAKE_BUFFER(p, len, { - assert(wv_get_u32(&r_) == 5); - for (int i = 0; i < 5; i++) { - shape_t back; - read_shape(&r_, &back); - assert(shape_eq(&variants[i], &back)); - shape_free(&back); - } - }); - wv_w_init(&w); - wv_put_u32(&w, 0); - p = weaveffi_codec_roundtrip_shapes(w.buf, w.len, &len, &err); - assert(err.code == 0); - TAKE_BUFFER(p, len, assert(wv_get_u32(&r_) == 0)); - wv_w_free(&w); - } - for (int i = 0; i < 5; i++) shape_free(&variants[i]); + codec_codec_Shape no_note = shape_of(codec_codec_Shape_Nested); + no_note.as.Nested.inner = CANONICAL_SCALARS; + codec_codec_Shape back; + roundtrip_shape(&no_note, &back); + assert(shape_eq(&no_note, &back) && back.as.Nested.note == NULL); + codec_codec_Shape_free(&back); + + codec_codec_Shape low = shape_of(codec_codec_Shape_Labeled); + low.as.Labeled.count = INT32_MIN; + roundtrip_shape(&low, &back); + assert(shape_eq(&low, &back)); + codec_codec_Shape_free(&back); + + // roundtrip_shapes: a list of every variant, then an empty list. + codec_list_codec_Shape list; + list.items = v; + list.len = 5; + ENCODE(codec_list_codec_Shape_write, &list, w); + size_t len = 0; + const uint8_t* p = call_buffer(codec_codec_roundtrip_shapes, &w, &len, 1); + codec_list_codec_Shape out; + TAKE(codec_list_codec_Shape_decode, p, len, &out); + assert(out.len == 5); + for (int i = 0; i < 5; i++) assert(shape_eq(&v[i], &out.items[i])); + codec_list_codec_Shape_free(&out); + list.len = 0; + ENCODE(codec_list_codec_Shape_write, &list, w2); + p = call_buffer(codec_codec_roundtrip_shapes, &w2, &len, 1); + TAKE(codec_list_codec_Shape_decode, p, len, &out); + assert(out.len == 0 && out.items == NULL); + + // The producer rejects malformed buffers as marshalling failures (-3): + // truncated mid-value, an unknown tag, and trailing bytes. + const uint8_t truncated[3] = {1, 0, 0}; + assert(codec_codec_roundtrip_shape(BYTES(truncated), &len, &err) == NULL); + assert(err.code == -3); + codec_error_clear(&err); + const uint8_t bad_tag[4] = {99, 0, 0, 0}; + assert(codec_codec_roundtrip_shape(BYTES(bad_tag), &len, &err) == NULL); + assert(err.code == -3); + codec_error_clear(&err); + const uint8_t trailing[5] = {0, 0, 0, 0, 0}; + assert(codec_codec_roundtrip_shape(BYTES(trailing), &len, &err) == NULL); + assert(err.code == -3); + codec_error_clear(&err); + + // The consumer's decoder rejects them too, leaving nothing allocated. + assert(!codec_codec_Shape_decode(BYTES(truncated), &back)); + assert(!codec_codec_Shape_decode(BYTES(bad_tag), &back)); + assert(!codec_codec_Shape_decode(BYTES(trailing), &back)); + const uint8_t bad_bool[2] = {1, 2}; + bool* flag = NULL; + assert(!codec_opt_bool_decode(BYTES(bad_bool), &flag) && flag == NULL); +} + +static void standalone(void) { + codec_error err = {0}; + size_t len = 0; - // A malformed buffer (truncated mid-value) is a marshalling failure (-3). - { - uint8_t truncated[3] = {1, 0, 0}; - size_t len = 0; - assert(weaveffi_codec_roundtrip_shape(truncated, sizeof truncated, &len, &err) == NULL); - assert(err.code == -3); - weaveffi_error_clear(&err); - } + // Standalone optional: present, then absent. + int64_t minus_one = -1; + ENCODE(codec_opt_i64_write, &minus_one, w); + const uint8_t* p = call_buffer(codec_codec_roundtrip_opt_i64, &w, &len, 1); + int64_t* opt = NULL; + TAKE(codec_opt_i64_decode, p, len, &opt); + assert(opt != NULL && *opt == -1); + codec_opt_i64_free(&opt); + ENCODE(codec_opt_i64_write, NULL, w2); + p = call_buffer(codec_codec_roundtrip_opt_i64, &w2, &len, 1); + TAKE(codec_opt_i64_decode, p, len, &opt); + assert(opt == NULL); + + // Map: encoded out of key order, comes back sorted by the BTreeMap. + codec_str keys[3] = {codec_str_of("b"), codec_str_of("a"), codec_str_of("")}; + int64_t values[3] = {-2, 1, INT64_MAX}; + codec_map_string_i64 map; + map.keys = keys; + map.values = values; + map.len = 3; + ENCODE(codec_map_string_i64_write, &map, w3); + p = call_buffer(codec_codec_roundtrip_map, &w3, &len, 0); + codec_map_string_i64 sorted; + TAKE(codec_map_string_i64_decode, p, len, &sorted); + assert(sorted.len == 3); + assert(sorted.keys[0].len == 0 && sorted.values[0] == INT64_MAX); + assert(strcmp(sorted.keys[1].ptr, "a") == 0 && sorted.values[1] == 1); + assert(strcmp(sorted.keys[2].ptr, "b") == 0 && sorted.values[2] == -2); + codec_map_string_i64_free(&sorted); + map.len = 0; + ENCODE(codec_map_string_i64_write, &map, w4); + p = call_buffer(codec_codec_roundtrip_map, &w4, &len, 1); + TAKE(codec_map_string_i64_decode, p, len, &sorted); + assert(sorted.len == 0); + + // Strings cross as (ptr, len) in both directions, interior NULs included. + static const char text[] = "h\xC3\xA9llo \0w\xC3\xB6rld\0"; + p = codec_codec_roundtrip_string((const uint8_t*)text, sizeof text - 1, &len, &err); + assert(err.code == 0 && len == sizeof text - 1 && memcmp(p, text, len) == 0); + codec_free_bytes((uint8_t*)p, len); + p = codec_codec_roundtrip_string(NULL, 0, &len, &err); + assert(err.code == 0 && len == 0); + codec_free_bytes((uint8_t*)p, len); + // A NULL pointer with a length, and invalid UTF-8, are marshalling + // failures. + assert(codec_codec_roundtrip_string(NULL, 2, &len, &err) == NULL && err.code == -3); + codec_error_clear(&err); + const uint8_t bad_utf8[3] = {'a', 0xFF, 'b'}; + assert(codec_codec_roundtrip_string(BYTES(bad_utf8), &len, &err) == NULL && err.code == -3); + codec_error_clear(&err); + + // Bytes, including the empty run. + const uint8_t raw[4] = {0, 255, 0, 128}; + p = codec_codec_roundtrip_bytes(BYTES(raw), &len, &err); + assert(err.code == 0 && len == 4 && memcmp(p, raw, 4) == 0); + codec_free_bytes((uint8_t*)p, len); + p = codec_codec_roundtrip_bytes(raw, 0, &len, &err); + assert(err.code == 0 && len == 0); + codec_free_bytes((uint8_t*)p, len); + + // Direct family. + assert(codec_codec_roundtrip_i64(INT64_MIN, &err) == INT64_MIN); + assert(codec_codec_roundtrip_i64(INT64_MAX, &err) == INT64_MAX); + assert(codec_codec_roundtrip_u64(UINT64_MAX, &err) == UINT64_MAX); + assert(codec_codec_roundtrip_u64(1ULL << 63, &err) == (1ULL << 63)); + assert(isnan(codec_codec_roundtrip_f64(NAN, &err))); + assert(signbit(codec_codec_roundtrip_f64(-0.0, &err))); + assert(codec_codec_roundtrip_f64(INFINITY, &err) == INFINITY); + assert(codec_codec_roundtrip_f64(-2.25e100, &err) == -2.25e100); + assert(codec_codec_roundtrip_bool(true, &err) == true); + assert(codec_codec_roundtrip_bool(false, &err) == false); + assert(codec_codec_roundtrip_color(codec_codec_Color_Blue, &err) == codec_codec_Color_Blue); + assert(codec_codec_roundtrip_color(codec_codec_Color_Red, &err) == 0); + assert(err.code == 0); + codec_codec_roundtrip_color((codec_codec_Color)3, &err); + assert(err.code == -3 && "an undeclared discriminant is a marshalling failure"); + codec_error_clear(&err); +} - // ── Standalone optional, map, string, bytes, and direct scalars ────── - { - wv_writer w; - wv_w_init(&w); - wv_put_bool(&w, 1); - wv_put_i64(&w, -1); - size_t len = 0; - const uint8_t* p = weaveffi_codec_roundtrip_opt_i64(w.buf, w.len, &len, &err); - assert(err.code == 0); - TAKE_BUFFER(p, len, { assert(wv_get_bool(&r_) == 1 && wv_get_i64(&r_) == -1); }); - wv_w_free(&w); - const uint8_t none[1] = {0}; - p = weaveffi_codec_roundtrip_opt_i64(none, 1, &len, &err); - assert(err.code == 0); - TAKE_BUFFER(p, len, assert(wv_get_bool(&r_) == 0)); - - // Map: encoded out of key order, comes back sorted by the BTreeMap. - wv_w_init(&w); - wv_put_u32(&w, 3); - wv_put_str(&w, "b"); - wv_put_i64(&w, -2); - wv_put_str(&w, "a"); - wv_put_i64(&w, 1); - wv_put_str(&w, ""); - wv_put_i64(&w, INT64_MAX); - p = weaveffi_codec_roundtrip_map(w.buf, w.len, &len, &err); - assert(err.code == 0); - TAKE_BUFFER(p, len, { - assert(wv_get_u32(&r_) == 3); - char* k = wv_get_str(&r_); - assert(strcmp(k, "") == 0 && wv_get_i64(&r_) == INT64_MAX); - free(k); - k = wv_get_str(&r_); - assert(strcmp(k, "a") == 0 && wv_get_i64(&r_) == 1); - free(k); - k = wv_get_str(&r_); - assert(strcmp(k, "b") == 0 && wv_get_i64(&r_) == -2); - free(k); - }); - wv_w_free(&w); - wv_w_init(&w); - wv_put_u32(&w, 0); - p = weaveffi_codec_roundtrip_map(w.buf, w.len, &len, &err); - assert(err.code == 0); - TAKE_BUFFER(p, len, assert(wv_get_u32(&r_) == 0)); - wv_w_free(&w); - - // Strings and bytes keep their direct ABI. - const char* text = weaveffi_codec_roundtrip_string("h\xC3\xA9llo \xE2\x9C\x93", &err); - assert(err.code == 0 && strcmp(text, "h\xC3\xA9llo \xE2\x9C\x93") == 0); - weaveffi_free_string(text); - text = weaveffi_codec_roundtrip_string("", &err); - assert(err.code == 0 && strcmp(text, "") == 0); - weaveffi_free_string(text); - assert(weaveffi_codec_roundtrip_string(NULL, &err) == NULL && err.code == -3); - weaveffi_error_clear(&err); - const uint8_t raw[4] = {0, 255, 0, 128}; - p = weaveffi_codec_roundtrip_bytes(raw, 4, &len, &err); - assert(err.code == 0 && len == 4 && memcmp(p, raw, 4) == 0); - weaveffi_free_bytes((uint8_t*)p, len); - p = weaveffi_codec_roundtrip_bytes(raw, 0, &len, &err); - assert(err.code == 0 && len == 0); - weaveffi_free_bytes((uint8_t*)p, len); - - // Direct family. - assert(weaveffi_codec_roundtrip_i64(INT64_MIN, &err) == INT64_MIN); - assert(weaveffi_codec_roundtrip_i64(INT64_MAX, &err) == INT64_MAX); - assert(weaveffi_codec_roundtrip_u64(UINT64_MAX, &err) == UINT64_MAX); - assert(weaveffi_codec_roundtrip_u64(1ULL << 63, &err) == (1ULL << 63)); - assert(isnan(weaveffi_codec_roundtrip_f64(NAN, &err))); - assert(signbit(weaveffi_codec_roundtrip_f64(-0.0, &err))); - assert(weaveffi_codec_roundtrip_f64(INFINITY, &err) == INFINITY); - assert(weaveffi_codec_roundtrip_f64(-2.25e100, &err) == -2.25e100); - assert(weaveffi_codec_roundtrip_bool(true, &err) == true); - assert(weaveffi_codec_roundtrip_bool(false, &err) == false); - assert(weaveffi_codec_roundtrip_color(weaveffi_codec_Color_Blue, &err) == - weaveffi_codec_Color_Blue); - assert(weaveffi_codec_roundtrip_color(weaveffi_codec_Color_Red, &err) == 0); - assert(err.code == 0); - weaveffi_codec_roundtrip_color((weaveffi_codec_Color)3, &err); - assert(err.code == -3 && "an undeclared discriminant is a marshalling failure"); - weaveffi_error_clear(&err); - } +static int64_t token_value(const codec_codec_Token* t) { + codec_error err = {0}; + int64_t v = codec_codec_Token_value(t, &err); + assert(err.code == 0); + return v; +} + +static codec_codec_Holder make_holder(int64_t base, bool with_spare) { + codec_error err = {0}; + size_t len = 0; + const uint8_t* p = codec_codec_make_holder(base, with_spare, &len, &err); + assert(err.code == 0); + codec_codec_Holder h; + TAKE(codec_codec_Holder_decode, p, len, &h); // adopts every token + return h; +} + +static void objects(void) { + codec_error err = {0}; - // ── Token objects and Holder (objects inside buffers) ──────────────── - weaveffi_codec_Token* t = weaveffi_codec_Token_new(5, &err); + codec_codec_Token* t = codec_codec_Token_new(5, &err); assert(err.code == 0 && t != NULL); assert(token_value(t) == 5); - weaveffi_codec_Token* t2 = weaveffi_codec_Token_clone(t); + codec_codec_Token* t2 = codec_codec_Token_clone(t); assert(t2 == t); - weaveffi_codec_Token_destroy(t); + codec_codec_Token_destroy(t); assert(token_value(t2) == 5 && "clone survives destroying the original"); - weaveffi_codec_Token_destroy(t2); - weaveffi_codec_Token_destroy(NULL); - assert(weaveffi_codec_Token_clone(NULL) == NULL); + codec_codec_Token_destroy(t2); + codec_codec_Token_destroy(NULL); + assert(codec_codec_Token_clone(NULL) == NULL); - holder_t h; - make_holder(10, 1, &h); + codec_codec_Holder h = make_holder(10, true); assert(token_value(h.primary) == 10); assert(h.spare != NULL && token_value(h.spare) == 11); - assert(h.many_n == 3); - assert(token_value(h.many[0]) == 12 && token_value(h.many[1]) == 13 && - token_value(h.many[2]) == 14); - assert(h.primary != h.spare && h.primary != h.many[0]); + assert(h.many.len == 3); + assert(token_value(h.many.items[0]) == 12 && token_value(h.many.items[1]) == 13 && + token_value(h.many.items[2]) == 14); + assert(h.primary != h.spare && h.primary != h.many.items[0]); + + // sum_holder consumes one encoding; the writer stored a fresh reference + // per token, so ours stay intact. + ENCODE(codec_codec_Holder_write, &h, w); + assert(codec_codec_sum_holder(w.ptr, w.len, &err) == 10 + 11 + 12 + 13 + 14); + assert(err.code == 0); + codec_writer_free(&w); + assert(token_value(h.primary) == 10 && "our references are intact"); + + // primary_of returns the very same object as holder.primary, as an owned + // reference we release. + ENCODE(codec_codec_Holder_write, &h, w2); + codec_codec_Token* primary = codec_codec_primary_of(w2.ptr, w2.len, &err); + codec_writer_free(&w2); + assert(err.code == 0 && primary == h.primary); + codec_codec_Token_destroy(primary); + assert(token_value(h.primary) == 10); - // sum_holder consumes one encoding (one reference per token). - { - wv_writer w; - wv_w_init(&w); - write_holder(&w, &h); - assert(weaveffi_codec_sum_holder(w.buf, w.len, &err) == 10 + 11 + 12 + 13 + 14); - assert(err.code == 0); - wv_w_free(&w); - assert(token_value(h.primary) == 10 && "our references are intact"); - } + // same_primary: two encodings of one holder share a primary; a different + // holder does not. + codec_codec_Holder h2 = make_holder(100, false); + assert(h2.spare == NULL && h2.many.len == 3 && token_value(h2.primary) == 100); + ENCODE(codec_codec_Holder_write, &h, a); + ENCODE(codec_codec_Holder_write, &h, b); + assert(codec_codec_same_primary(a.ptr, a.len, b.ptr, b.len, &err)); + assert(err.code == 0); + codec_writer_free(&a); + codec_writer_free(&b); + ENCODE(codec_codec_Holder_write, &h, a2); + ENCODE(codec_codec_Holder_write, &h2, b2); + assert(!codec_codec_same_primary(a2.ptr, a2.len, b2.ptr, b2.len, &err)); + assert(err.code == 0); + codec_writer_free(&a2); + codec_writer_free(&b2); + + // A holder mixing our own tokens with the producer's, built from + // borrowed pointers. + codec_codec_Token* mine = codec_codec_Token_new(1000, &err); + codec_codec_Token* neg = codec_codec_Token_new(-1, &err); + codec_codec_Token* many[2] = {h2.primary, neg}; + codec_codec_Holder mixed; + mixed.primary = mine; + mixed.spare = h.primary; + mixed.many.items = many; + mixed.many.len = 2; + ENCODE(codec_codec_Holder_write, &mixed, m1); + assert(codec_codec_sum_holder(m1.ptr, m1.len, &err) == 1000 + 10 + 100 - 1); + codec_writer_free(&m1); + ENCODE(codec_codec_Holder_write, &mixed, m2); + codec_codec_Token* p = codec_codec_primary_of(m2.ptr, m2.len, &err); + codec_writer_free(&m2); + assert(p == mine && token_value(p) == 1000); + codec_codec_Token_destroy(p); + codec_codec_Token_destroy(mine); + codec_codec_Token_destroy(neg); + assert(token_value(h.primary) == 10 && token_value(h2.primary) == 100 && + "the references written into `mixed` never touched ours"); + + // A zero token in a required position is a marshalling failure. + const uint8_t zero_token[13] = {0}; + assert(codec_codec_sum_holder(BYTES(zero_token), &err) == 0 && err.code == -3); + codec_error_clear(&err); + + codec_codec_Holder_free(&h2); + codec_codec_Holder_free(&h); + assert(h.primary == NULL && h.many.items == NULL); +} - // primary_of returns the very same object as holder.primary, as an - // owned reference we release; the holder's reference stays valid. - { - wv_writer w; - wv_w_init(&w); - write_holder(&w, &h); - weaveffi_codec_Token* primary = weaveffi_codec_primary_of(w.buf, w.len, &err); - assert(err.code == 0); - assert(primary == h.primary); - wv_w_free(&w); - assert(token_value(primary) == 10); - weaveffi_codec_Token_destroy(primary); - assert(token_value(h.primary) == 10); - } +int main(void) { + assert(CODEC_ABI_VERSION == 3u && codec_abi_version() == CODEC_ABI_VERSION); + assert(codec_codec_checksum() == CODEC_CODEC_CHECKSUM); - // same_primary: two encodings of the same holder share a primary; a - // different holder does not. - holder_t h2; - make_holder(100, 0, &h2); - assert(h2.spare == NULL && h2.many_n == 3 && token_value(h2.primary) == 100); - { - wv_writer a, b; - wv_w_init(&a); - wv_w_init(&b); - write_holder(&a, &h); - write_holder(&b, &h); - assert(weaveffi_codec_same_primary(a.buf, a.len, b.buf, b.len, &err)); - assert(err.code == 0); - wv_w_free(&a); - wv_w_free(&b); - wv_w_init(&a); - wv_w_init(&b); - write_holder(&a, &h); - write_holder(&b, &h2); - assert(!weaveffi_codec_same_primary(a.buf, a.len, b.buf, b.len, &err)); - assert(err.code == 0); - wv_w_free(&a); - wv_w_free(&b); - - // A holder mixing our own tokens with the producer's. - holder_t mixed; - memset(&mixed, 0, sizeof mixed); - mixed.primary = weaveffi_codec_Token_new(1000, &err); - mixed.spare = weaveffi_codec_Token_clone(h.primary); - mixed.many_n = 2; - mixed.many[0] = weaveffi_codec_Token_clone(h2.primary); - mixed.many[1] = weaveffi_codec_Token_new(-1, &err); - wv_w_init(&a); - write_holder(&a, &mixed); - assert(weaveffi_codec_sum_holder(a.buf, a.len, &err) == 1000 + 10 + 100 - 1); - wv_w_free(&a); - wv_w_init(&a); - write_holder(&a, &mixed); - weaveffi_codec_Token* p = weaveffi_codec_primary_of(a.buf, a.len, &err); - assert(p == mixed.primary && token_value(p) == 1000); - weaveffi_codec_Token_destroy(p); - wv_w_free(&a); - holder_release(&mixed); - assert(token_value(h.primary) == 10 && token_value(h2.primary) == 100 && - "the clones written into `mixed` never touched our references"); - } - holder_release(&h2); - holder_release(&h); + scalars(); + composite(); + edge_composite(); + shapes(); + standalone(); + objects(); + ASSERT_NO_LEAKS(codec_debug_live); printf("c/codec: OK\n"); return 0; } diff --git a/conformance/c/contacts.c b/conformance/c/contacts.c index c668f206..69d620d4 100644 --- a/conformance/c/contacts.c +++ b/conformance/c/contacts.c @@ -1,145 +1,247 @@ -// Conformance consumer: contacts sample, C target. +// Conformance consumer: contacts sample, C target (ABI revision 3). // -// Includes the *generated* C header and links the contacts cdylib, exercising -// the ContactBook interface (constructor, methods, destroy), the Contact -// record crossing the ABI as a serialized value buffer, enums, buffered -// optional strings, buffered lists, and the typed error-domain codes surfaced -// through the error-out convention. Exits 0 on success; aborts (non-zero) on -// any failed assertion. - -#include -#include -#include - -#include "weaveffi.h" -#include "wvbuf.h" - -// A decoded Contact record: id, first_name, last_name, email?, contact_type. -typedef struct { - int64_t id; - char* first_name; - char* last_name; - char* email; // NULL when absent - int32_t contact_type; -} contact_t; - -static void read_contact(wv_reader* r, contact_t* c) { - c->id = wv_get_i64(r); - c->first_name = wv_get_str(r); - c->last_name = wv_get_str(r); - c->email = wv_get_bool(r) ? wv_get_str(r) : NULL; - c->contact_type = wv_get_i32(r); +// Includes the generated `contacts.h` and `contacts_buffer.h` and links the +// contacts cdylib. Covers the load-time contract check (ABI revision and +// both roots' checksums), the ContactBook interface (constructor, methods, +// destroy), (ptr, len) string parameters, the Contact record and its +// optional email crossing as value buffers, typed error codes and messages, +// the nested `contacts.groups` module (which uses the parent's C-style enum, +// interface, and error domain), and the sibling `directory` root sharing the +// Contact record. Ends by asserting the producer's leak counters are zero. + +#include "harness.h" + +#include "contacts_buffer.h" + +static void check_contract(void) { + assert(CONTACTS_ABI_VERSION == 3u); + assert(contacts_abi_version() == CONTACTS_ABI_VERSION); + assert(contacts_contacts_checksum() == CONTACTS_CONTACTS_CHECKSUM); + assert(contacts_directory_checksum() == CONTACTS_DIRECTORY_CHECKSUM); + assert(CONTACTS_CONTACTS_CHECKSUM != CONTACTS_DIRECTORY_CHECKSUM); } -static void contact_free(contact_t* c) { - free(c->first_name); - free(c->last_name); - free(c->email); -} - -// Decode one Contact from an owned return buffer, then release the buffer. -static void take_contact(const uint8_t* ptr, size_t len, contact_t* c) { +// Decode an owned Contact return, release the buffer, and return the value +// (which the caller frees with contacts_contacts_Contact_free). +static contacts_contacts_Contact take_contact(const uint8_t* ptr, size_t len) { assert(ptr != NULL); - wv_reader r; - wv_r_init(&r, ptr, len); - read_contact(&r, c); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)ptr, len); + contacts_contacts_Contact c; + assert(contacts_contacts_Contact_decode(ptr, len, &c)); + contacts_free_bytes((uint8_t*)ptr, len); + return c; } -int main(void) { - weaveffi_error err = {0}; +// Add a contact (with an optional email), returning the stored record. +static contacts_contacts_Contact add(contacts_contacts_ContactBook* book, const char* first, + const char* last, const char* email, + contacts_contacts_ContactType type) { + contacts_error err = {0}; + contacts_str email_view = contacts_str_of(email); + contacts_writer w; + memset(&w, 0, sizeof w); + contacts_opt_string_write(&w, email ? &email_view : NULL); + size_t len = 0; + const uint8_t* buf = contacts_contacts_ContactBook_add(book, STR(first), STR(last), w.ptr, + w.len, type, &len, &err); + assert(err.code == 0); + contacts_writer_free(&w); + return take_contact(buf, len); +} - // Optional-string parameter encodings: present and absent. - wv_writer some_email; - wv_w_init(&some_email); - wv_put_bool(&some_email, 1); - wv_put_str(&some_email, "alice@example.com"); - const uint8_t no_email[1] = {0}; +static void book_surface(void) { + contacts_error err = {0}; + contacts_contacts_ContactBook* book = contacts_contacts_ContactBook_new(&err); + assert(err.code == 0 && book != NULL); - // Interface constructor. - weaveffi_contacts_ContactBook* book = weaveffi_contacts_ContactBook_new(&err); + contacts_contacts_Contact alice = + add(book, "Alice", "Smith", "alice@example.com", contacts_contacts_ContactType_Work); + assert(alice.id > 0); + assert(strcmp(alice.first_name.ptr, "Alice") == 0); + assert(strcmp(alice.last_name.ptr, "Smith") == 0); + assert(alice.email != NULL && strcmp(alice.email->ptr, "alice@example.com") == 0); + assert(alice.contact_type == contacts_contacts_ContactType_Work); + + // get -> a fresh snapshot of the same record. + size_t len = 0; + const uint8_t* buf = contacts_contacts_ContactBook_get(book, alice.id, &len, &err); assert(err.code == 0); - assert(book != NULL); + contacts_contacts_Contact snap = take_contact(buf, len); + assert(snap.id == alice.id && strcmp(snap.last_name.ptr, "Smith") == 0); + contacts_contacts_Contact_free(&snap); - // add -> Contact value buffer; methods take the receiver as the leading arg. - size_t out_len = 0; - const uint8_t* buf = weaveffi_contacts_ContactBook_add( - book, "Alice", "Smith", some_email.buf, some_email.len, - weaveffi_contacts_ContactType_Work, &out_len, &err); - assert(err.code == 0); - contact_t alice; - take_contact(buf, out_len, &alice); - assert(alice.id > 0); - assert(strcmp(alice.first_name, "Alice") == 0); - assert(alice.email != NULL && strcmp(alice.email, "alice@example.com") == 0); - assert(alice.contact_type == weaveffi_contacts_ContactType_Work); - int64_t alice_id = alice.id; - contact_free(&alice); - - // get -> fresh Contact snapshot decoded from a new buffer. - buf = weaveffi_contacts_ContactBook_get(book, alice_id, &out_len, &err); + // An absent email round-trips as an absent optional. + contacts_contacts_Contact bob = + add(book, "Bob", "Jones", NULL, contacts_contacts_ContactType_Personal); + assert(bob.email == NULL); + + // A non-ASCII name crosses intact. + contacts_contacts_Contact zoe = + add(book, "Zo\xC3\xAB", "Quinn", NULL, contacts_contacts_ContactType_Other); + assert(zoe.first_name.len == 4 && strcmp(zoe.first_name.ptr, "Zo\xC3\xAB") == 0); + + // count + list: the list return is one buffer holding every record. + assert(contacts_contacts_ContactBook_count(book, &err) == 3); + buf = contacts_contacts_ContactBook_list(book, &len, &err); + assert(err.code == 0 && buf != NULL); + contacts_list_contacts_Contact all; + assert(contacts_list_contacts_Contact_decode(buf, len, &all)); + contacts_free_bytes((uint8_t*)buf, len); + assert(all.len == 3); + assert(strcmp(all.items[0].first_name.ptr, "Alice") == 0); + assert(strcmp(all.items[1].first_name.ptr, "Bob") == 0); + assert(all.items[2].id == zoe.id); + contacts_list_contacts_Contact_free(&all); + + // remove, then the typed NotFound error for the missing id. + assert(contacts_contacts_ContactBook_remove(book, alice.id, &err)); + assert(!contacts_contacts_ContactBook_remove(book, alice.id, &err)); + assert(contacts_contacts_ContactBook_count(book, &err) == 2); + assert(contacts_contacts_ContactBook_get(book, alice.id, &len, &err) == NULL); + assert(err.code == contacts_contacts_ContactsError_NotFound); + assert(strcmp(err.message, "contact not found") == 0); + contacts_error_clear(&err); + assert(err.code == 0 && err.message == NULL); + + // An empty first name reports InvalidName. A NULL pointer with length 0 + // is the empty string. + const uint8_t absent[1] = {0}; + buf = contacts_contacts_ContactBook_add(book, NULL, 0, STR("Nameless"), BYTES(absent), + contacts_contacts_ContactType_Other, &len, &err); + assert(buf == NULL); + assert(err.code == contacts_contacts_ContactsError_InvalidName); + assert(strcmp(err.message, "name must not be empty") == 0); + contacts_error_clear(&err); + + // A NULL pointer with a nonzero length is a marshalling failure (-3). + buf = contacts_contacts_ContactBook_add(book, NULL, 3, STR("X"), BYTES(absent), + contacts_contacts_ContactType_Other, &len, &err); + assert(buf == NULL && err.code == -3); + contacts_error_clear(&err); + + contacts_contacts_Contact_free(&alice); + contacts_contacts_Contact_free(&bob); + contacts_contacts_Contact_free(&zoe); + contacts_contacts_ContactBook_destroy(book); +} + +// contacts.groups: nested under the contacts root, so its functions take +// the parent's ContactBook and ContactType and report the parent's errors. +static void nested_module(void) { + contacts_error err = {0}; + contacts_contacts_ContactBook* book = contacts_contacts_ContactBook_new(&err); + contacts_contacts_Contact c[3] = { + add(book, "Ann", "Lee", NULL, contacts_contacts_ContactType_Work), + add(book, "Bo", "Kim", "bo@example.com", contacts_contacts_ContactType_Work), + add(book, "Cy", "Ng", NULL, contacts_contacts_ContactType_Personal), + }; + + assert(contacts_contacts_groups_count_of_type(book, contacts_contacts_ContactType_Work, + &err) == 2); + assert(contacts_contacts_groups_count_of_type(book, contacts_contacts_ContactType_Other, + &err) == 0); assert(err.code == 0); - contact_t snap; - take_contact(buf, out_len, &snap); - assert(strcmp(snap.first_name, "Alice") == 0); - assert(strcmp(snap.last_name, "Smith") == 0); - contact_free(&snap); - - // Optional string: an absent email round-trips as the 0 flag byte. - buf = weaveffi_contacts_ContactBook_add( - book, "Bob", "Jones", no_email, sizeof no_email, - weaveffi_contacts_ContactType_Personal, &out_len, &err); + assert(contacts_contacts_groups_dominant_type(book, &err) == + contacts_contacts_ContactType_Work); + + contacts_contacts_ContactBook* work = + contacts_contacts_groups_split_by_type(book, contacts_contacts_ContactType_Work, &err); + assert(err.code == 0 && work != NULL && work != book); + assert(contacts_contacts_ContactBook_count(work, &err) == 2); + assert(contacts_contacts_groups_dominant_type(work, &err) == + contacts_contacts_ContactType_Work); + contacts_contacts_ContactBook_destroy(work); + + size_t len = 0; + const uint8_t* buf = contacts_contacts_groups_first_of_type( + book, contacts_contacts_ContactType_Work, &len, &err); assert(err.code == 0); - contact_t bob; - take_contact(buf, out_len, &bob); - assert(bob.email == NULL); - int64_t bob_id = bob.id; - contact_free(&bob); + contacts_contacts_Contact first = take_contact(buf, len); + assert(strcmp(first.first_name.ptr, "Ann") == 0); + contacts_contacts_Contact_free(&first); + + // The parent domain's NotFound code, reported from the nested module. + buf = contacts_contacts_groups_first_of_type(book, contacts_contacts_ContactType_Other, + &len, &err); + assert(buf == NULL); + assert(err.code == contacts_contacts_ContactsError_NotFound); + contacts_error_clear(&err); + + // An out-of-range enum discriminant is a marshalling failure (-3). + assert(contacts_contacts_groups_count_of_type(book, (contacts_contacts_ContactType)9, + &err) == 0); + assert(err.code == -3); + contacts_error_clear(&err); + + for (int i = 0; i < 3; i++) contacts_contacts_Contact_free(&c[i]); + contacts_contacts_ContactBook_destroy(book); +} - // count + list: the list return is one buffer holding u32 count + records. - assert(weaveffi_contacts_ContactBook_count(book, &err) == 2); - buf = weaveffi_contacts_ContactBook_list(book, &out_len, &err); +// directory: a sibling root that shares only the Contact record. +static void sibling_root(void) { + contacts_error err = {0}; + contacts_contacts_ContactBook* book = contacts_contacts_ContactBook_new(&err); + contacts_contacts_Contact zoe = + add(book, "Zoe", "Quinn", NULL, contacts_contacts_ContactType_Personal); + contacts_contacts_Contact ada = + add(book, "Ada", "Byron", "ada@example.com", contacts_contacts_ContactType_Work); + contacts_contacts_ContactBook_destroy(book); + + // card(Contact) -> Card: a record built in one root, read by the other. + contacts_writer w; + memset(&w, 0, sizeof w); + contacts_contacts_Contact_write(&w, &ada); + size_t len = 0; + const uint8_t* buf = contacts_directory_card(w.ptr, w.len, &len, &err); + contacts_writer_free(&w); assert(err.code == 0 && buf != NULL); - wv_reader lr; - wv_r_init(&lr, buf, out_len); - uint32_t n = wv_get_u32(&lr); - assert(n == 2); - int seen_alice = 0, seen_bob = 0; - for (uint32_t i = 0; i < n; i++) { - contact_t c; - read_contact(&lr, &c); - if (strcmp(c.first_name, "Alice") == 0) seen_alice = 1; - if (strcmp(c.first_name, "Bob") == 0) seen_bob = 1; - contact_free(&c); - } - wv_r_expect_end(&lr); - weaveffi_free_bytes((uint8_t*)buf, out_len); - assert(seen_alice && seen_bob); - - // remove + typed error path: the domain code for a missing id is - // ContactsError.NotFound (2), surfaced through the error-out slot. - assert(weaveffi_contacts_ContactBook_remove(book, alice_id, &err)); - assert(weaveffi_contacts_ContactBook_count(book, &err) == 1); - - const uint8_t* missing = - weaveffi_contacts_ContactBook_get(book, 9999, &out_len, &err); - assert(missing == NULL); - assert(err.code == weaveffi_contacts_ContactsError_NotFound); - weaveffi_error_clear(&err); - - // Typed error path: empty first name reports ContactsError.InvalidName (1). - const uint8_t* bad = weaveffi_contacts_ContactBook_add( - book, "", "Nameless", no_email, sizeof no_email, - weaveffi_contacts_ContactType_Other, &out_len, &err); - assert(bad == NULL); - assert(err.code == weaveffi_contacts_ContactsError_InvalidName); - weaveffi_error_clear(&err); - - assert(weaveffi_contacts_ContactBook_remove(book, bob_id, &err)); - weaveffi_contacts_ContactBook_destroy(book); - wv_w_free(&some_email); + contacts_directory_Card card; + assert(contacts_directory_Card_decode(buf, len, &card)); + contacts_free_bytes((uint8_t*)buf, len); + assert(strcmp(card.display_name.ptr, "Byron, Ada") == 0); + assert(strcmp(card.initials.ptr, "AB") == 0); + assert(card.has_email); + contacts_directory_Card_free(&card); + + // sorted([Contact]) -> [Contact], ordered by last name. + contacts_contacts_Contact pair[2] = {zoe, ada}; + contacts_list_contacts_Contact in; + in.items = pair; + in.len = 2; + memset(&w, 0, sizeof w); + contacts_list_contacts_Contact_write(&w, &in); + buf = contacts_directory_sorted(w.ptr, w.len, &len, &err); + contacts_writer_free(&w); + assert(err.code == 0); + contacts_list_contacts_Contact sorted; + assert(contacts_list_contacts_Contact_decode(buf, len, &sorted)); + contacts_free_bytes((uint8_t*)buf, len); + assert(sorted.len == 2); + assert(strcmp(sorted.items[0].last_name.ptr, "Byron") == 0); + assert(sorted.items[0].email != NULL); + assert(strcmp(sorted.items[1].last_name.ptr, "Quinn") == 0); + assert(sorted.items[1].email == NULL); + contacts_list_contacts_Contact_free(&sorted); + + // An empty list reports the directory's own Empty code. + in.len = 0; + memset(&w, 0, sizeof w); + contacts_list_contacts_Contact_write(&w, &in); + assert(contacts_directory_sorted(w.ptr, w.len, &len, &err) == NULL); + contacts_writer_free(&w); + assert(err.code == contacts_directory_DirectoryError_Empty); + assert(strcmp(err.message, "no contacts to list") == 0); + contacts_error_clear(&err); + + contacts_contacts_Contact_free(&zoe); + contacts_contacts_Contact_free(&ada); +} +int main(void) { + check_contract(); + book_surface(); + nested_module(); + sibling_root(); + ASSERT_NO_LEAKS(contacts_debug_live); printf("c/contacts: OK\n"); return 0; } diff --git a/conformance/c/events.c b/conformance/c/events.c index 561eb794..03b9bc31 100644 --- a/conformance/c/events.c +++ b/conformance/c/events.c @@ -1,25 +1,24 @@ -// Conformance consumer: events sample, C target (ABI revision 2). +// Conformance consumer: events sample, C target (ABI revision 3). // // Exercises the raw callback-interface ABI: a hand-written `Subscriber` // vtable (one function per method taking `void* ctx` first and a trailing -// `weaveffi_error* out_err`, plus the `free` entry the producer calls when -// it drops its last reference), the reference-counted `EventBus` object +// `events_error* out_err`, plus the `free` entry the producer calls when it +// drops its last reference), the reference-counted `EventBus` object // (`_clone`/`_destroy`, an object handed *to* the consumer through // `on_attached`), `Delivery` return values steering `publish`'s accepted -// count, a foreign error raised from a callback surfacing to the caller as -// code -4, the `Message` record decoded from a borrowed value buffer, the -// `messages()` iterator, the `last_message()` optional, and the async -// `publish_later` launcher. Exits 0 on success; aborts on any mismatch. +// count, a consumer failure reported through `out_err` from `route` (the +// method whose Rust signature returns `Result<_, ForeignError>`) surfacing +// to the caller as code -4, the `Message` record decoded from a borrowed +// value buffer, the `messages()` iterator yielding owned strings, the +// `last_message()` optional, and the async `publish_later` launcher, whose +// subscriber callbacks run on a producer thread. Ends by asserting the +// producer's leak counters are zero. + +#include "harness.h" -#include #include -#include -#include -#include -#include -#include "weaveffi.h" -#include "wvbuf.h" +#include "events_buffer.h" // ── consumer-side subscriber ─────────────────────────────────────────────── @@ -27,97 +26,93 @@ typedef struct { const char* skip_topic; // route() answers Skip for this topic const char* stop_topic; // route() answers AcceptAndStop for this topic - const char* fail_topic; // route() reports a foreign error for this topic + const char* fail_topic; // route() reports a consumer failure int keep_bus; // on_attached keeps the bus reference in `bus` int64_t received; // running count returned from on_message int attached; // on_attached invocations - weaveffi_events_EventBus* bus; // kept reference (when keep_bus) + int routed; // route invocations + events_events_EventBus* bus; // kept reference (when keep_bus) int64_t attached_count; // subscriber_count observed inside on_attached // Fields of the last decoded Message. int64_t last_seq; char last_topic[64]; char last_text[64]; - uint32_t last_tag_count; + size_t last_tag_count; char last_tag0[64]; } sub_ctx; -static int g_freed = 0; +static atomic_int g_freed = 0; + +static int topic_is(const uint8_t* ptr, size_t len, const char* topic) { + return topic != NULL && bytes_eq(ptr, len, topic); +} -static weaveffi_events_Delivery sub_route(void* ctx, const char* topic, - weaveffi_error* out_err) { +static events_events_Delivery sub_route(void* ctx, const uint8_t* topic_ptr, size_t topic_len, + events_error* out_err) { sub_ctx* s = (sub_ctx*)ctx; - if (s->fail_topic && strcmp(topic, s->fail_topic) == 0) { - weaveffi_error_set(out_err, -4, "subscriber rejected topic"); - return weaveffi_events_Delivery_Skip; + s->routed++; + if (topic_is(topic_ptr, topic_len, s->fail_topic)) { + events_error_set(out_err, -4, "subscriber rejected topic"); + return events_events_Delivery_Skip; } - if (s->skip_topic && strcmp(topic, s->skip_topic) == 0) { - return weaveffi_events_Delivery_Skip; + if (topic_is(topic_ptr, topic_len, s->skip_topic)) { + return events_events_Delivery_Skip; } - if (s->stop_topic && strcmp(topic, s->stop_topic) == 0) { - return weaveffi_events_Delivery_AcceptAndStop; + if (topic_is(topic_ptr, topic_len, s->stop_topic)) { + return events_events_Delivery_AcceptAndStop; } - return weaveffi_events_Delivery_Accept; + return events_events_Delivery_Accept; } -// Message record: seq (i64), topic (string), text (string), tags ([string]). -static int64_t sub_on_message(void* ctx, const uint8_t* message_ptr, - size_t message_len, weaveffi_error* out_err) { +// The Message buffer is borrowed for the duration of the call. +static int64_t sub_on_message(void* ctx, const uint8_t* message_ptr, size_t message_len, + events_error* out_err) { (void)out_err; sub_ctx* s = (sub_ctx*)ctx; - wv_reader r; - wv_r_init(&r, message_ptr, message_len); - s->last_seq = wv_get_i64(&r); - char* topic = wv_get_str(&r); - snprintf(s->last_topic, sizeof s->last_topic, "%s", topic); - free(topic); - char* text = wv_get_str(&r); - snprintf(s->last_text, sizeof s->last_text, "%s", text); - free(text); - s->last_tag_count = wv_get_u32(&r); + events_events_Message m; + assert(events_events_Message_decode(message_ptr, message_len, &m)); + s->last_seq = m.seq; + snprintf(s->last_topic, sizeof s->last_topic, "%s", m.topic.ptr); + snprintf(s->last_text, sizeof s->last_text, "%s", m.text.ptr); + s->last_tag_count = m.tags.len; s->last_tag0[0] = '\0'; - for (uint32_t i = 0; i < s->last_tag_count; i++) { - char* tag = wv_get_str(&r); - if (i == 0) snprintf(s->last_tag0, sizeof s->last_tag0, "%s", tag); - free(tag); - } - wv_r_expect_end(&r); + if (m.tags.len > 0) snprintf(s->last_tag0, sizeof s->last_tag0, "%s", m.tags.items[0].ptr); + events_events_Message_free(&m); s->received++; return s->received; } // The bus arrives as one strong reference the consumer adopts: it is usable // right here, and it is ours to keep or release. -static void sub_on_attached(void* ctx, weaveffi_events_EventBus* bus, - weaveffi_error* out_err) { +static void sub_on_attached(void* ctx, events_events_EventBus* bus, events_error* out_err) { (void)out_err; sub_ctx* s = (sub_ctx*)ctx; s->attached++; - weaveffi_error err = {0}; - s->attached_count = weaveffi_events_EventBus_subscriber_count(bus, &err); + events_error err = {0}; + s->attached_count = events_events_EventBus_subscriber_count(bus, &err); assert(err.code == 0); if (s->keep_bus) { s->bus = bus; } else { - weaveffi_events_EventBus_destroy(bus); + events_events_EventBus_destroy(bus); } } static void sub_free(void* ctx) { sub_ctx* s = (sub_ctx*)ctx; - weaveffi_events_EventBus_destroy(s->bus); // null is a no-op + events_events_EventBus_destroy(s->bus); // null is a no-op free(s); - g_freed++; + atomic_fetch_add(&g_freed, 1); } -static const weaveffi_events_Subscriber_vtable SUB_VTABLE = { +static const events_events_Subscriber_vtable SUB_VTABLE = { sub_route, sub_on_message, sub_on_attached, sub_free, }; -static sub_ctx* new_sub(const char* skip, const char* stop, const char* fail, - int keep_bus) { +static sub_ctx* new_sub(const char* skip, const char* stop, const char* fail, int keep_bus) { sub_ctx* s = (sub_ctx*)calloc(1, sizeof *s); assert(s != NULL); s->skip_topic = skip; @@ -130,72 +125,83 @@ static sub_ctx* new_sub(const char* skip, const char* stop, const char* fail, // ── helpers ──────────────────────────────────────────────────────────────── // publish(topic, text, tags): `tags` is a buffered `[string]`. -static int64_t publish(weaveffi_events_EventBus* bus, const char* topic, - const char* text, const char** tags, uint32_t ntags, - weaveffi_error* err) { - wv_writer w; - wv_w_init(&w); - wv_put_u32(&w, ntags); - for (uint32_t i = 0; i < ntags; i++) wv_put_str(&w, tags[i]); - int64_t n = weaveffi_events_EventBus_publish(bus, topic, text, w.buf, w.len, err); - wv_w_free(&w); +static int64_t publish(events_events_EventBus* bus, const char* topic, const char* text, + const char** tags, size_t ntags, events_error* err) { + events_str views[4]; + assert(ntags <= 4); + for (size_t i = 0; i < ntags; i++) views[i] = events_str_of(tags[i]); + events_list_string list; + list.items = views; + list.len = ntags; + events_writer w; + memset(&w, 0, sizeof w); + events_list_string_write(&w, &list); + int64_t n = events_events_EventBus_publish(bus, STR(topic), STR(text), w.ptr, w.len, err); + events_writer_free(&w); return n; } -// Collect messages() into `out`, returning how many were yielded. -static int collect_messages(weaveffi_events_EventBus* bus, char** out, int cap) { - weaveffi_error err = {0}; - weaveffi_events_EventBus_MessagesIterator* it = - weaveffi_events_EventBus_messages(bus, &err); +// Collect messages() into `out`, returning how many were yielded. Each item +// is an owned (ptr, len) string released with events_free_bytes. +static int collect_messages(events_events_EventBus* bus, char** out, int cap) { + events_error err = {0}; + events_events_EventBus_MessagesIterator* it = events_events_EventBus_messages(bus, &err); assert(err.code == 0 && it != NULL); int n = 0; - const char* item = NULL; - while (weaveffi_events_EventBus_MessagesIterator_next(it, &item, &err) != 0) { - assert(err.code == 0 && item != NULL); - assert(n < cap); - out[n++] = strdup(item); - weaveffi_free_string(item); + const uint8_t* item = NULL; + size_t len = 0; + while (events_events_EventBus_MessagesIterator_next(it, &item, &len, &err) == 1) { + assert(err.code == 0 && n < cap); + out[n] = (char*)calloc(len + 1, 1); + memcpy(out[n++], item, len); + events_free_bytes((uint8_t*)item, len); } assert(err.code == 0); - weaveffi_events_EventBus_MessagesIterator_destroy(it); + events_events_EventBus_MessagesIterator_destroy(it); return n; } +static void free_texts(char** texts, int n) { + for (int i = 0; i < n; i++) free(texts[i]); +} + +// Decode last_message()'s buffered `Message?`. +static events_events_Message* last_message(events_events_EventBus* bus) { + events_error err = {0}; + size_t len = 0; + const uint8_t* buf = events_events_EventBus_last_message(bus, &len, &err); + assert(err.code == 0 && buf != NULL); + events_events_Message* m = NULL; + assert(events_opt_events_Message_decode(buf, len, &m)); + events_free_bytes((uint8_t*)buf, len); + return m; +} + // ── async completion state ───────────────────────────────────────────────── static atomic_int g_later_done = 0; static int32_t g_later_err = -1; static int64_t g_later_result = -1; -static void on_publish_later(void* context, weaveffi_error* err, int64_t result) { +static void on_publish_later(void* context, events_error* err, int64_t result) { assert(context == (void*)0x1234); g_later_err = err ? err->code : 0; - weaveffi_error_free(err); + events_error_free(err); g_later_result = result; atomic_store(&g_later_done, 1); } int main(void) { - weaveffi_error err = {0}; + events_error err = {0}; - // ABI revision handshake. - assert(WEAVEFFI_ABI_VERSION == 2u); - assert(weaveffi_abi_version() == 2u); + assert(EVENTS_ABI_VERSION == 3u && events_abi_version() == EVENTS_ABI_VERSION); + assert(events_events_checksum() == EVENTS_EVENTS_CHECKSUM); - weaveffi_events_EventBus* bus = weaveffi_events_EventBus_new(&err); + events_events_EventBus* bus = events_events_EventBus_new(&err); assert(err.code == 0 && bus != NULL); - assert(weaveffi_events_EventBus_subscriber_count(bus, &err) == 0); - - // last_message() on an empty bus: a buffered `Message?` with flag 0. - size_t out_len = 0; - const uint8_t* last = weaveffi_events_EventBus_last_message(bus, &out_len, &err); - assert(err.code == 0 && last != NULL); - { - wv_reader r; - wv_r_init(&r, last, out_len); - assert(wv_get_bool(&r) == 0 && "no message yet"); - wv_r_expect_end(&r); - } - weaveffi_free_bytes((uint8_t*)last, out_len); + assert(events_events_EventBus_subscriber_count(bus, &err) == 0); + + // last_message() on an empty bus: an absent optional. + assert(last_message(bus) == NULL); // messages() on an empty bus yields nothing. char* texts[8]; @@ -208,19 +214,19 @@ int main(void) { sub_ctx* b = new_sub(NULL, NULL, NULL, 0); sub_ctx* c = new_sub(NULL, NULL, "boom", 0); - assert(weaveffi_events_EventBus_subscribe(bus, a, &SUB_VTABLE, &err) == 1); + assert(events_events_EventBus_subscribe(bus, a, &SUB_VTABLE, &err) == 1); assert(err.code == 0); assert(a->attached == 1); assert(a->attached_count == 0 && "on_attached runs before the bus retains us"); assert(a->bus == bus && "the object handed to the callback is the same bus"); - assert(weaveffi_events_EventBus_subscribe(bus, b, &SUB_VTABLE, &err) == 2); + assert(events_events_EventBus_subscribe(bus, b, &SUB_VTABLE, &err) == 2); assert(b->attached == 1 && b->attached_count == 1); - assert(weaveffi_events_EventBus_subscribe(bus, c, &SUB_VTABLE, &err) == 3); + assert(events_events_EventBus_subscribe(bus, c, &SUB_VTABLE, &err) == 3); assert(c->attached == 1 && c->attached_count == 2); - assert(weaveffi_events_EventBus_subscriber_count(bus, &err) == 3); + assert(events_events_EventBus_subscriber_count(bus, &err) == 3); // The kept reference is usable independently of the original pointer. - assert(weaveffi_events_EventBus_subscriber_count(a->bus, &err) == 3); + assert(events_events_EventBus_subscriber_count(a->bus, &err) == 3); // Everyone accepts "news": 3 deliveries, and each saw the same Message. const char* tags[] = {"x", "y"}; @@ -239,118 +245,115 @@ int main(void) { assert(b->last_seq == 2 && b->last_tag_count == 0); // `a` answers AcceptAndStop for "stop": exactly 1 delivery, later - // subscribers never see it. + // subscribers are never asked. + int b_routed = b->routed; assert(publish(bus, "stop", "last", NULL, 0, &err) == 1); assert(a->received == 2 && b->received == 2 && c->received == 2); assert(a->last_seq == 3 && strcmp(a->last_text, "last") == 0); + assert(b->routed == b_routed); - // `c` raises through out_err on "boom": the whole publish aborts with - // FOREIGN_ERROR_CODE and the foreign message. + // `route` is the Result-returning callback method: `c` reports a failure + // through out_err on "boom", the producer receives it as an Err and + // chooses to fail the whole publish with code -4 and the message. publish(bus, "boom", "x", NULL, 0, &err); assert(err.code == -4); assert(err.message != NULL && strstr(err.message, "rejected topic") != NULL); - weaveffi_error_clear(&err); + events_error_clear(&err); assert(err.code == 0 && err.message == NULL); + assert(a->received == 3 && b->received == 3 && c->received == 2 && + "subscribers before the failure were delivered to"); // The bus (and its subscribers) stay usable afterward. assert(publish(bus, "ok", "y", NULL, 0, &err) == 3); assert(err.code == 0); - // Async publish: the completion callback fires on a producer thread with - // the accepted count. - weaveffi_events_EventBus_publish_later_async(bus, "later", "z", on_publish_later, - (void*)0x1234); - for (int i = 0; i < 5000 && !atomic_load(&g_later_done); i++) usleep(1000); + // Async publish: the launcher returns at once; the subscribers' callbacks + // and then the completion run on a producer thread. + events_events_EventBus_publish_later(bus, STR("later"), STR("z"), on_publish_later, + (void*)0x1234); + for (int i = 0; i < 5000 && !atomic_load(&g_later_done); i++) sleep_ms(1); assert(atomic_load(&g_later_done)); assert(g_later_err == 0); assert(g_later_result == 3); + assert(strcmp(b->last_text, "z") == 0); // messages(): every published text in order, including the aborted one // (the bus logs before it dispatches). int n = collect_messages(bus, texts, 8); const char* expected[] = {"hello", "psst", "last", "x", "y", "z"}; assert(n == 6); - for (int i = 0; i < n; i++) { - assert(strcmp(texts[i], expected[i]) == 0); - free(texts[i]); - } + for (int i = 0; i < n; i++) assert(strcmp(texts[i], expected[i]) == 0); + free_texts(texts, n); // last_message(): present, with the async publish's fields. - last = weaveffi_events_EventBus_last_message(bus, &out_len, &err); - assert(err.code == 0 && last != NULL); - { - wv_reader r; - wv_r_init(&r, last, out_len); - assert(wv_get_bool(&r) == 1); - assert(wv_get_i64(&r) == 6); - char* topic = wv_get_str(&r); - assert(strcmp(topic, "later") == 0); - free(topic); - char* text = wv_get_str(&r); - assert(strcmp(text, "z") == 0); - free(text); - assert(wv_get_u32(&r) == 0 && "publish_later attaches no tags"); - wv_r_expect_end(&r); - } - weaveffi_free_bytes((uint8_t*)last, out_len); + events_events_Message* last = last_message(bus); + assert(last != NULL); + assert(last->seq == 6); + assert(strcmp(last->topic.ptr, "later") == 0); + assert(strcmp(last->text.ptr, "z") == 0); + assert(last->tags.len == 0 && "publish_later attaches no tags"); + events_opt_events_Message_free(&last); + assert(last == NULL); // route_once: a free function taking the callback interface. The // producer does not retain it, so `free` runs before the call returns. + assert(atomic_load(&g_freed) == 0); sub_ctx* d = new_sub("quiet", NULL, NULL, 0); - assert(g_freed == 0); - assert(weaveffi_events_route_once(d, &SUB_VTABLE, "quiet", &err) == - weaveffi_events_Delivery_Skip); + assert(events_events_route_once(d, &SUB_VTABLE, STR("quiet"), &err) == + events_events_Delivery_Skip); assert(err.code == 0); - assert(g_freed == 1 && "route_once released its subscriber"); + assert(atomic_load(&g_freed) == 1 && "route_once released its subscriber"); sub_ctx* e = new_sub(NULL, "stop", NULL, 0); - assert(weaveffi_events_route_once(e, &SUB_VTABLE, "stop", &err) == - weaveffi_events_Delivery_AcceptAndStop); - assert(weaveffi_events_route_once(new_sub(NULL, NULL, NULL, 0), &SUB_VTABLE, - "anything", &err) == - weaveffi_events_Delivery_Accept); - assert(g_freed == 3); - - // A foreign error from route_once surfaces the same way. + assert(events_events_route_once(e, &SUB_VTABLE, STR("stop"), &err) == + events_events_Delivery_AcceptAndStop); + assert(events_events_route_once(new_sub(NULL, NULL, NULL, 0), &SUB_VTABLE, + STR("anything"), &err) == events_events_Delivery_Accept); + assert(atomic_load(&g_freed) == 3); + + // A failure from the Result-returning route surfaces the same way, and + // the subscriber is still released exactly once. sub_ctx* f = new_sub(NULL, NULL, "boom", 0); - weaveffi_events_route_once(f, &SUB_VTABLE, "boom", &err); + events_events_route_once(f, &SUB_VTABLE, STR("boom"), &err); assert(err.code == -4); - weaveffi_error_clear(&err); - assert(g_freed == 4); + assert(strstr(err.message, "rejected topic") != NULL); + events_error_clear(&err); + assert(atomic_load(&g_freed) == 4); // Release the bus reference `a` kept, then drop every subscriber: each // `free` entry runs exactly once. - weaveffi_events_EventBus_destroy(a->bus); + events_events_EventBus_destroy(a->bus); a->bus = NULL; - assert(weaveffi_events_EventBus_subscriber_count(bus, &err) == 3); - weaveffi_events_EventBus_clear_subscribers(bus, &err); + assert(events_events_EventBus_subscriber_count(bus, &err) == 3); + events_events_EventBus_clear_subscribers(bus, &err); assert(err.code == 0); - assert(g_freed == 7 && "clear_subscribers freed a, b, and c"); - assert(weaveffi_events_EventBus_subscriber_count(bus, &err) == 0); + assert(atomic_load(&g_freed) == 7 && "clear_subscribers freed a, b, and c"); + assert(events_events_EventBus_subscriber_count(bus, &err) == 0); assert(publish(bus, "empty", "nobody", NULL, 0, &err) == 0); // Reference counting: clone yields the same pointer; destroying the // original leaves the clone usable. - weaveffi_events_EventBus* again = weaveffi_events_EventBus_clone(bus); + events_events_EventBus* again = events_events_EventBus_clone(bus); assert(again == bus); - weaveffi_events_EventBus_destroy(bus); - assert(weaveffi_events_EventBus_subscriber_count(again, &err) == 0); + events_events_EventBus_destroy(bus); + assert(events_events_EventBus_subscriber_count(again, &err) == 0); assert(err.code == 0); n = collect_messages(again, texts, 8); assert(n == 7); - for (int i = 0; i < n; i++) free(texts[i]); - weaveffi_events_EventBus_destroy(again); - weaveffi_events_EventBus_destroy(NULL); - assert(weaveffi_events_EventBus_clone(NULL) == NULL); + free_texts(texts, n); + events_events_EventBus_destroy(again); + events_events_EventBus_destroy(NULL); + assert(events_events_EventBus_clone(NULL) == NULL); // Destroying a bus releases its subscribers too. - weaveffi_events_EventBus* bus2 = weaveffi_events_EventBus_new(&err); + events_events_EventBus* bus2 = events_events_EventBus_new(&err); sub_ctx* g = new_sub(NULL, NULL, NULL, 0); - assert(weaveffi_events_EventBus_subscribe(bus2, g, &SUB_VTABLE, &err) == 1); + assert(events_events_EventBus_subscribe(bus2, g, &SUB_VTABLE, &err) == 1); assert(publish(bus2, "t", "u", NULL, 0, &err) == 1); assert(g->received == 1); - weaveffi_events_EventBus_destroy(bus2); - assert(g_freed == 8 && "destroying the bus frees its subscriber"); + events_events_EventBus_destroy(bus2); + assert(atomic_load(&g_freed) == 8 && "destroying the bus frees its subscriber"); + ASSERT_NO_LEAKS(events_debug_live); printf("c/events: OK\n"); return 0; } diff --git a/conformance/c/harness.h b/conformance/c/harness.h new file mode 100644 index 00000000..10441087 --- /dev/null +++ b/conformance/c/harness.h @@ -0,0 +1,62 @@ +// Shared helpers for the C conformance consumers: always-on assertions, +// string-slot shorthands for the ABI's (ptr, len) strings, and the leak +// check every consumer runs before exiting. +#ifndef CONFORMANCE_HARNESS_H +#define CONFORMANCE_HARNESS_H + +#undef NDEBUG +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#include +#include +#include +#include +#include +#include + +// Expand a NUL-terminated C string into the two slots of a string +// parameter: `const uint8_t* {name}_ptr, size_t {name}_len`. +#define STR(s) (const uint8_t*)(s), strlen(s) + +// Expand a byte array into a bytes parameter's (ptr, len) slots. +#define BYTES(a) (const uint8_t*)(a), sizeof(a) + +static inline void sleep_ms(long ms) { + struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L}; + nanosleep(&ts, NULL); +} + +// `true` when the `len` bytes at `ptr` spell the C string `s` exactly. +static inline int bytes_eq(const uint8_t* ptr, size_t len, const char* s) { + size_t n = strlen(s); + return len == n && (n == 0 || memcmp(ptr, s, n) == 0); +} + +// Assert that every leak counter of a producer built with `leak-check` is +// zero: 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned +// allocations. A producer worker thread may still be unwinding a finished +// async call (dropping its future and token reference just after the +// completion fired), so the counters get up to two seconds to settle. +#define ASSERT_NO_LEAKS(debug_live) \ + do { \ + static const char* const kinds[5] = {"objects", "callbacks", \ + "iterators", "cancel tokens", \ + "allocations"}; \ + for (int wait = 0; wait < 2000; wait++) { \ + uint64_t total = 0; \ + for (int32_t kind = 0; kind <= 4; kind++) total += debug_live(kind); \ + if (total == 0) break; \ + sleep_ms(1); \ + } \ + for (int32_t kind = 0; kind <= 4; kind++) { \ + uint64_t live = debug_live(kind); \ + if (live != 0) { \ + fprintf(stderr, "leak: %llu live %s\n", \ + (unsigned long long)live, kinds[kind]); \ + abort(); \ + } \ + } \ + } while (0) + +#endif // CONFORMANCE_HARNESS_H diff --git a/conformance/c/inventory.c b/conformance/c/inventory.c deleted file mode 100644 index efd2cb7b..00000000 --- a/conformance/c/inventory.c +++ /dev/null @@ -1,387 +0,0 @@ -// Conformance consumer: inventory sample, C target (ABI revision 2). -// -// The only multi-module sample: the `products` module (the `Catalog` -// interface, the `Product` record with an optional and a list field, the -// `Category` enum, the `ProductsError` domain) and the `orders` module (free -// functions over `OrderItem`/`Order` records, the `OrdersError` domain, and -// `add_product_to_order`, which takes a `products::Product` across the -// module boundary). Both error domains reuse the small codes 1 and 2, so the -// consumer checks the doc-comment messages as well as the codes to prove -// each function reports its own domain. Also covers `Catalog` reference -// counting (`_clone`/`_destroy`). Exits 0 on success; aborts on any mismatch. - -#include -#include -#include -#include -#include - -#include "weaveffi.h" -#include "wvbuf.h" - -// ── products::Product ────────────────────────────────────────────────────── -// id, name, description?, price, category, tags. - -typedef struct { - int64_t id; - char* name; - char* description; // NULL when absent - double price; - int32_t category; - char** tags; - uint32_t tags_n; -} product_t; - -static void read_product(wv_reader* r, product_t* p) { - memset(p, 0, sizeof *p); - p->id = wv_get_i64(r); - p->name = wv_get_str(r); - p->description = wv_get_bool(r) ? wv_get_str(r) : NULL; - p->price = wv_get_f64(r); - p->category = wv_get_i32(r); - p->tags_n = wv_get_u32(r); - p->tags = (char**)calloc(p->tags_n ? p->tags_n : 1, sizeof(char*)); - assert(p->tags != NULL); - for (uint32_t i = 0; i < p->tags_n; i++) p->tags[i] = wv_get_str(r); -} - -static void write_product(wv_writer* w, const product_t* p) { - wv_put_i64(w, p->id); - wv_put_str(w, p->name); - wv_put_bool(w, p->description != NULL); - if (p->description) wv_put_str(w, p->description); - wv_put_f64(w, p->price); - wv_put_i32(w, p->category); - wv_put_u32(w, p->tags_n); - for (uint32_t i = 0; i < p->tags_n; i++) wv_put_str(w, p->tags[i]); -} - -static void product_free(product_t* p) { - free(p->name); - free(p->description); - for (uint32_t i = 0; i < p->tags_n; i++) free(p->tags[i]); - free(p->tags); - memset(p, 0, sizeof *p); -} - -// Decode one Product from an owned return buffer, then release the buffer. -static void take_product(const uint8_t* ptr, size_t len, product_t* p) { - assert(ptr != NULL); - wv_reader r; - wv_r_init(&r, ptr, len); - read_product(&r, p); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)ptr, len); -} - -// ── orders::OrderItem / orders::Order ────────────────────────────────────── -// OrderItem: product_id, quantity (i32), unit_price. Order: id, items, total, -// status. - -typedef struct { - int64_t product_id; - int32_t quantity; - double unit_price; -} order_item_t; - -typedef struct { - int64_t id; - order_item_t* items; - uint32_t items_n; - double total; - char* status; -} order_t; - -static void write_items(wv_writer* w, const order_item_t* items, uint32_t n) { - wv_put_u32(w, n); - for (uint32_t i = 0; i < n; i++) { - wv_put_i64(w, items[i].product_id); - wv_put_i32(w, items[i].quantity); - wv_put_f64(w, items[i].unit_price); - } -} - -static void take_order(const uint8_t* ptr, size_t len, order_t* o) { - assert(ptr != NULL); - wv_reader r; - wv_r_init(&r, ptr, len); - o->id = wv_get_i64(&r); - o->items_n = wv_get_u32(&r); - o->items = (order_item_t*)calloc(o->items_n ? o->items_n : 1, sizeof(order_item_t)); - assert(o->items != NULL); - for (uint32_t i = 0; i < o->items_n; i++) { - o->items[i].product_id = wv_get_i64(&r); - o->items[i].quantity = wv_get_i32(&r); - o->items[i].unit_price = wv_get_f64(&r); - } - o->total = wv_get_f64(&r); - o->status = wv_get_str(&r); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)ptr, len); -} - -static void order_free(order_t* o) { - free(o->items); - free(o->status); - memset(o, 0, sizeof *o); -} - -static int64_t create_order(const order_item_t* items, uint32_t n, weaveffi_error* err) { - wv_writer w; - wv_w_init(&w); - write_items(&w, items, n); - int64_t id = weaveffi_orders_create_order(w.buf, w.len, err); - wv_w_free(&w); - return id; -} - -static void get_order(int64_t id, order_t* o) { - weaveffi_error err = {0}; - size_t len = 0; - const uint8_t* p = weaveffi_orders_get_order(id, &len, &err); - assert(err.code == 0); - take_order(p, len, o); -} - -static int approx(double a, double b) { return fabs(a - b) < 1e-9; } - -int main(void) { - weaveffi_error err = {0}; - size_t out_len = 0; - - assert(weaveffi_abi_version() == 2u && WEAVEFFI_ABI_VERSION == 2u); - - // ── products ───────────────────────────────────────────────────────── - weaveffi_products_Catalog* catalog = weaveffi_products_Catalog_new(&err); - assert(err.code == 0 && catalog != NULL); - - // add_product -> Product buffer with a fresh id, absent description, and - // no tags. - const uint8_t* buf = weaveffi_products_Catalog_add_product( - catalog, "Widget", 9.99, weaveffi_products_Category_Electronics, &out_len, &err); - assert(err.code == 0); - product_t widget; - take_product(buf, out_len, &widget); - assert(widget.id == 1 && "each catalog hands out ids from 1"); - assert(strcmp(widget.name, "Widget") == 0); - assert(widget.description == NULL); - assert(widget.price == 9.99); - assert(widget.category == weaveffi_products_Category_Electronics); - assert(widget.tags_n == 0); - - // ProductsError.InvalidPrice (1) with its doc-comment message. - assert(weaveffi_products_Catalog_add_product(catalog, "Free", 0.0, - weaveffi_products_Category_Food, &out_len, - &err) == NULL); - assert(err.code == weaveffi_products_ProductsError_InvalidPrice); - assert(err.message != NULL && strcmp(err.message, "price must be positive") == 0); - assert(err.payload_ptr == NULL && err.payload_len == 0); - weaveffi_error_clear(&err); - assert(err.code == 0 && err.message == NULL); - - // ProductsError.ProductNotFound (2). - assert(weaveffi_products_Catalog_get_product(catalog, 999, &out_len, &err) == NULL); - assert(err.code == weaveffi_products_ProductsError_ProductNotFound); - assert(strcmp(err.message, "product not found") == 0); - weaveffi_error_clear(&err); - - // An undeclared Category discriminant is a marshalling failure (-3). - assert(weaveffi_products_Catalog_add_product(catalog, "Odd", 1.0, - (weaveffi_products_Category)42, &out_len, - &err) == NULL); - assert(err.code == -3); - weaveffi_error_clear(&err); - - // Two more products, then search by shelf: a buffered list of records. - buf = weaveffi_products_Catalog_add_product( - catalog, "Shirt", 29.99, weaveffi_products_Category_Clothing, &out_len, &err); - assert(err.code == 0); - product_t shirt; - take_product(buf, out_len, &shirt); - assert(shirt.id == 2 && shirt.category == weaveffi_products_Category_Clothing); - buf = weaveffi_products_Catalog_add_product( - catalog, "Phone", 499.99, weaveffi_products_Category_Electronics, &out_len, &err); - assert(err.code == 0); - product_t phone; - take_product(buf, out_len, &phone); - assert(phone.id == 3); - - buf = weaveffi_products_Catalog_search(catalog, weaveffi_products_Category_Electronics, - &out_len, &err); - assert(err.code == 0 && buf != NULL); - { - wv_reader r; - wv_r_init(&r, buf, out_len); - assert(wv_get_u32(&r) == 2); - product_t a, b; - read_product(&r, &a); - read_product(&r, &b); - wv_r_expect_end(&r); - assert(a.id == 1 && strcmp(a.name, "Widget") == 0); - assert(b.id == 3 && strcmp(b.name, "Phone") == 0 && b.price == 499.99); - product_free(&a); - product_free(&b); - } - weaveffi_free_bytes((uint8_t*)buf, out_len); - buf = weaveffi_products_Catalog_search(catalog, weaveffi_products_Category_Books, &out_len, - &err); - assert(err.code == 0 && buf != NULL); - { - wv_reader r; - wv_r_init(&r, buf, out_len); - assert(wv_get_u32(&r) == 0 && "empty list is just a zero count"); - wv_r_expect_end(&r); - } - weaveffi_free_bytes((uint8_t*)buf, out_len); - - // update_price: bool return, typed error path. - assert(weaveffi_products_Catalog_update_price(catalog, widget.id, 20.0, &err)); - assert(err.code == 0); - buf = weaveffi_products_Catalog_get_product(catalog, widget.id, &out_len, &err); - assert(err.code == 0); - product_t fetched; - take_product(buf, out_len, &fetched); - assert(fetched.price == 20.0 && strcmp(fetched.name, "Widget") == 0); - product_free(&fetched); - assert(!weaveffi_products_Catalog_update_price(catalog, 999, 1.0, &err)); - assert(err.code == 0); - assert(!weaveffi_products_Catalog_update_price(catalog, widget.id, -5.0, &err)); - assert(err.code == weaveffi_products_ProductsError_InvalidPrice); - weaveffi_error_clear(&err); - - // remove: true once, then false. - assert(weaveffi_products_Catalog_remove(catalog, shirt.id, &err) && err.code == 0); - assert(!weaveffi_products_Catalog_remove(catalog, shirt.id, &err) && err.code == 0); - assert(weaveffi_products_Catalog_get_product(catalog, shirt.id, &out_len, &err) == NULL); - assert(err.code == weaveffi_products_ProductsError_ProductNotFound); - weaveffi_error_clear(&err); - - // Reference counting: a clone is the same pointer and outlives the - // original reference. - weaveffi_products_Catalog* again = weaveffi_products_Catalog_clone(catalog); - assert(again == catalog); - weaveffi_products_Catalog_destroy(catalog); - buf = weaveffi_products_Catalog_get_product(again, phone.id, &out_len, &err); - assert(err.code == 0); - take_product(buf, out_len, &fetched); - assert(strcmp(fetched.name, "Phone") == 0); - product_free(&fetched); - catalog = again; - - // A second catalog is independent (its own ids and products). - weaveffi_products_Catalog* other = weaveffi_products_Catalog_new(&err); - buf = weaveffi_products_Catalog_add_product(other, "Novel", 12.5, - weaveffi_products_Category_Books, &out_len, - &err); - assert(err.code == 0); - take_product(buf, out_len, &fetched); - assert(fetched.id == 1 && fetched.category == weaveffi_products_Category_Books); - product_free(&fetched); - assert(weaveffi_products_Catalog_get_product(other, phone.id, &out_len, &err) == NULL); - assert(err.code == weaveffi_products_ProductsError_ProductNotFound); - weaveffi_error_clear(&err); - weaveffi_products_Catalog_destroy(other); - - // ── orders ─────────────────────────────────────────────────────────── - // create_order takes a buffered list of records and returns an id. - order_item_t items[2] = {{1, 2, 10.0}, {2, 1, 25.0}}; - int64_t order_id = create_order(items, 2, &err); - assert(err.code == 0 && order_id >= 1); - - order_t order; - get_order(order_id, &order); - assert(order.id == order_id); - assert(order.items_n == 2); - assert(order.items[0].product_id == 1 && order.items[0].quantity == 2 && - order.items[0].unit_price == 10.0); - assert(order.items[1].product_id == 2 && order.items[1].quantity == 1 && - order.items[1].unit_price == 25.0); - assert(order.total == 45.0); - assert(strcmp(order.status, "pending") == 0); - order_free(&order); - - // OrdersError.EmptyOrder (2): same numeric code as ProductNotFound but a - // different domain, distinguishable by message. - assert(create_order(NULL, 0, &err) == 0); - assert(err.code == weaveffi_orders_OrdersError_EmptyOrder); - assert(strcmp(err.message, "order must contain at least one item") == 0); - weaveffi_error_clear(&err); - - // OrdersError.OrderNotFound (1). - assert(weaveffi_orders_get_order(999, &out_len, &err) == NULL); - assert(err.code == weaveffi_orders_OrdersError_OrderNotFound); - assert(strcmp(err.message, "order not found") == 0); - weaveffi_error_clear(&err); - - // cancel_order: true on the transition, false afterward and for unknown - // ids; status is visible through get_order. - assert(weaveffi_orders_cancel_order(order_id, &err) && err.code == 0); - assert(!weaveffi_orders_cancel_order(order_id, &err) && err.code == 0); - assert(!weaveffi_orders_cancel_order(999, &err) && err.code == 0); - get_order(order_id, &order); - assert(strcmp(order.status, "cancelled") == 0); - order_free(&order); - - // ── cross-module: a products::Product parameter in the orders module ─ - // Hand-build a Product with the optional description present and tags. - order_item_t seed[1] = {{0, 1, 1.0}}; - int64_t order2 = create_order(seed, 1, &err); - assert(err.code == 0 && order2 == order_id + 1); - - product_t gadget; - memset(&gadget, 0, sizeof gadget); - gadget.id = 77; - gadget.name = strdup("Gadget"); - gadget.description = strdup("shiny \xE2\x9C\x93"); - gadget.price = 49.99; - gadget.category = weaveffi_products_Category_Electronics; - gadget.tags_n = 2; - gadget.tags = (char**)calloc(2, sizeof(char*)); - gadget.tags[0] = strdup("new"); - gadget.tags[1] = strdup(""); - wv_writer pw; - wv_w_init(&pw); - write_product(&pw, &gadget); - assert(weaveffi_orders_add_product_to_order(order2, pw.buf, pw.len, &err)); - assert(err.code == 0); - assert(!weaveffi_orders_add_product_to_order(999, pw.buf, pw.len, &err)); - assert(err.code == 0); - wv_w_free(&pw); - - get_order(order2, &order); - assert(order.items_n == 2); - assert(order.items[1].product_id == 77 && order.items[1].quantity == 1 && - order.items[1].unit_price == 49.99); - assert(approx(order.total, 50.99)); - assert(strcmp(order.status, "pending") == 0); - order_free(&order); - - // The producer-made Product round-trips into an order too. - wv_w_init(&pw); - write_product(&pw, &phone); - assert(weaveffi_orders_add_product_to_order(order2, pw.buf, pw.len, &err)); - wv_w_free(&pw); - get_order(order2, &order); - assert(order.items_n == 3 && order.items[2].product_id == 3 && - order.items[2].unit_price == 499.99); - assert(approx(order.total, 550.98)); - order_free(&order); - - // A truncated Product buffer is a marshalling failure (-3). - { - uint8_t truncated[2] = {1, 0}; - assert(!weaveffi_orders_add_product_to_order(order2, truncated, 2, &err)); - assert(err.code == -3); - weaveffi_error_clear(&err); - } - - product_free(&gadget); - product_free(&widget); - product_free(&shirt); - product_free(&phone); - weaveffi_products_Catalog_destroy(catalog); - weaveffi_products_Catalog_destroy(NULL); - - printf("c/inventory: OK\n"); - return 0; -} diff --git a/conformance/c/kvstore.c b/conformance/c/kvstore.c index 738fc2c3..b5941f71 100644 --- a/conformance/c/kvstore.c +++ b/conformance/c/kvstore.c @@ -1,31 +1,26 @@ -// Conformance consumer: kvstore sample, C target (ABI revision 2). +// Conformance consumer: kvstore sample, C target (ABI revision 3). // // Exercises the Store interface ABI end to end: the fallible constructor // (`Store_open`), instance methods taking the receiver as the leading -// argument, the static method (`Store_default_capacity`), the typed -// error-domain codes surfaced through the error-out slot, the iterator -// out-param `next` convention, the `Entry` record decoded from a value -// buffer (list and map fields included), the `kv.stats` submodule, the -// hand-written `EvictionListener` vtable (set -> fire synchronously on -// delete and on expiry-on-read -> replace/clear/detach -> `free`), a -// foreign error raised from the listener surfacing as code -4, the -// reference-counted object graph (`share`, `fork`, `clone`/`destroy`, -// `larger` with `Store?` both ways, `describe` returning a record that -// carries object tokens, `open_many` returning a list of objects, -// `total_count` taking objects inside a list and a record), and the raw -// `_async` launcher with a cancel token. Completion arrives on the -// producer's worker thread (synchronized here with C11 atomics). Exits 0 on -// success; aborts otherwise. - -#include +// argument, the static method (`Store_default_capacity`), (ptr, len) string +// and bytes parameters, the typed error-domain codes surfaced through the +// error-out slot, the iterator's owned (ptr, len) items, the `Entry` record +// (list and map fields included) decoded through `kvstore_buffer.h`, the +// `kv.stats` submodule, the hand-written `EvictionListener` vtable (set, +// fire synchronously on delete and on expiry-on-read, replace, clear, +// detach, `free`), a consumer failure raised from the listener surfacing as +// code -4, the reference-counted object graph (`share`, `fork`, +// `clone`/`destroy`, `larger` with `Store?` both ways, `describe` returning +// a record that carries objects, `open_many` returning a list of objects, +// `total_count` taking objects inside a list and an optional record), and +// the cancellable async `compact`, whose cancelled call completes with -5. +// Ends by asserting the producer's leak counters are zero. + +#include "harness.h" + #include -#include -#include -#include -#include -#include "weaveffi.h" -#include "wvbuf.h" +#include "kvstore_buffer.h" // ── eviction listener (consumer-implemented callback interface) ──────────── @@ -35,39 +30,25 @@ typedef struct { int32_t last_reason; size_t last_value_len; int detach_after; // return false (detach) once this many evictions ran - int fail; // report a foreign error instead of observing + int fail; // report a consumer failure instead of observing } listener_ctx; static int g_listener_freed = 0; -// Entry record: id, key, value (bytes), created_at, expires_at?, tags, -// metadata. The buffer is borrowed for the duration of the dispatch. +// The Entry buffer is borrowed for the duration of the dispatch. static bool on_evict(void* ctx, const uint8_t* entry_ptr, size_t entry_len, - weaveffi_kv_EvictionReason reason, weaveffi_error* out_err) { + kvstore_kv_EvictionReason reason, kvstore_error* out_err) { listener_ctx* l = (listener_ctx*)ctx; if (l->fail) { - weaveffi_error_set(out_err, -4, "listener exploded"); + kvstore_error_set(out_err, -4, "listener exploded"); return true; } - wv_reader r; - wv_r_init(&r, entry_ptr, entry_len); - int64_t id = wv_get_i64(&r); - assert(id > 0); - char* key = wv_get_str(&r); - snprintf(l->last_key, sizeof l->last_key, "%s", key); - free(key); - l->last_value_len = wv_get_u32(&r); - wv_take(&r, l->last_value_len); - assert(wv_get_i64(&r) > 0 && "created_at"); - if (wv_get_bool(&r)) wv_get_i64(&r); // expires_at? - uint32_t tags = wv_get_u32(&r); - for (uint32_t i = 0; i < tags; i++) free(wv_get_str(&r)); - uint32_t meta = wv_get_u32(&r); - for (uint32_t i = 0; i < meta; i++) { - free(wv_get_str(&r)); - free(wv_get_str(&r)); - } - wv_r_expect_end(&r); + kvstore_kv_Entry e; + assert(kvstore_kv_Entry_decode(entry_ptr, entry_len, &e)); + assert(e.id > 0 && e.created_at > 0); + snprintf(l->last_key, sizeof l->last_key, "%s", e.key.ptr); + l->last_value_len = e.value.len; + kvstore_kv_Entry_free(&e); l->last_reason = (int32_t)reason; l->evictions++; return l->detach_after == 0 || l->evictions < l->detach_after; @@ -78,7 +59,7 @@ static void listener_free(void* ctx) { g_listener_freed++; } -static const weaveffi_kv_EvictionListener_vtable LISTENER_VTABLE = { +static const kvstore_kv_EvictionListener_vtable LISTENER_VTABLE = { on_evict, listener_free, }; @@ -96,454 +77,447 @@ static atomic_int g_compact_done = 0; static int64_t g_compact_result = -1; static int32_t g_compact_err = -1; -static void on_compact_done(void* context, weaveffi_error* err, int64_t result) { - (void)context; +static void on_compact_done(void* context, kvstore_error* err, int64_t result) { + assert(context == (void*)0x5150); g_compact_err = err ? err->code : 0; - weaveffi_error_free(err); + kvstore_error_free(err); g_compact_result = result; atomic_store(&g_compact_done, 1); } static void wait_compact(void) { - for (int i = 0; i < 5000 && !atomic_load(&g_compact_done); i++) usleep(1000); + for (int i = 0; i < 5000 && !atomic_load(&g_compact_done); i++) sleep_ms(1); assert(atomic_load(&g_compact_done)); atomic_store(&g_compact_done, 0); } // ── helpers ──────────────────────────────────────────────────────────────── -// Optional-i64 parameter encodings for `ttl_seconds`. -static const uint8_t TTL_NONE[1] = {0}; +// The `ttl_seconds: i64?` parameter, encoded present or absent. +typedef struct { + kvstore_writer w; +} ttl_buf; + +static ttl_buf ttl(const int64_t* seconds) { + ttl_buf t; + memset(&t, 0, sizeof t); + kvstore_opt_i64_write(&t.w, seconds); + return t; +} -static weaveffi_kv_Store* open_store(const char* path) { - weaveffi_error err = {0}; - weaveffi_kv_Store* s = weaveffi_kv_Store_open(path, &err); +static kvstore_kv_Store* open_store(const char* path) { + kvstore_error err = {0}; + kvstore_kv_Store* s = kvstore_kv_Store_open(STR(path), &err); assert(err.code == 0 && s != NULL); return s; } -static void put(weaveffi_kv_Store* s, const char* key, const uint8_t* v, size_t n) { - weaveffi_error err = {0}; - assert(weaveffi_kv_Store_put(s, key, v, n, weaveffi_kv_EntryKind_Persistent, - TTL_NONE, sizeof TTL_NONE, &err)); +static void put_ttl(kvstore_kv_Store* s, const char* key, const uint8_t* v, size_t n, + const int64_t* seconds) { + kvstore_error err = {0}; + ttl_buf t = ttl(seconds); + assert(kvstore_kv_Store_put(s, STR(key), v, n, kvstore_kv_EntryKind_Persistent, t.w.ptr, + t.w.len, &err)); assert(err.code == 0); + kvstore_writer_free(&t.w); } -// put with `Some(ttl)`: flag byte + little-endian i64. -static void put_ttl(weaveffi_kv_Store* s, const char* key, const uint8_t* v, - size_t n, int64_t ttl) { - weaveffi_error err = {0}; - wv_writer w; - wv_w_init(&w); - wv_put_bool(&w, 1); - wv_put_i64(&w, ttl); - assert(weaveffi_kv_Store_put(s, key, v, n, weaveffi_kv_EntryKind_Volatile, - w.buf, w.len, &err)); - assert(err.code == 0); - wv_w_free(&w); +static void put(kvstore_kv_Store* s, const char* key, const uint8_t* v, size_t n) { + put_ttl(s, key, v, n, NULL); } -static int64_t count(weaveffi_kv_Store* s) { - weaveffi_error err = {0}; - int64_t n = weaveffi_kv_Store_count(s, &err); +static int64_t count(const kvstore_kv_Store* s) { + kvstore_error err = {0}; + int64_t n = kvstore_kv_Store_count(s, &err); assert(err.code == 0); return n; } -// A decoded StoreInfo record: label, store (object token), mirror (object -// token?), count. The adopted references are the caller's to destroy. -typedef struct { - char* label; - weaveffi_kv_Store* store; - weaveffi_kv_Store* mirror; // NULL when absent - int64_t count; -} store_info_t; - -static void read_store_info(wv_reader* r, store_info_t* info) { - info->label = wv_get_str(r); - info->store = (weaveffi_kv_Store*)wv_get_obj(r); - info->mirror = wv_get_bool(r) ? (weaveffi_kv_Store*)wv_get_obj(r) : NULL; - info->count = wv_get_i64(r); +// Collect list_keys(prefix?) into `out`; each key is an owned string. +static int list_keys(kvstore_kv_Store* s, const char* prefix, char** out, int cap) { + kvstore_error err = {0}; + kvstore_str view = kvstore_str_of(prefix); + kvstore_writer w; + memset(&w, 0, sizeof w); + kvstore_opt_string_write(&w, prefix ? &view : NULL); + kvstore_kv_Store_ListKeysIterator* it = kvstore_kv_Store_list_keys(s, w.ptr, w.len, &err); + kvstore_writer_free(&w); + assert(err.code == 0 && it != NULL); + int n = 0; + const uint8_t* item = NULL; + size_t len = 0; + while (kvstore_kv_Store_ListKeysIterator_next(it, &item, &len, &err) == 1) { + assert(n < cap); + out[n] = (char*)calloc(len + 1, 1); + memcpy(out[n++], item, len); + kvstore_free_bytes((uint8_t*)item, len); + } + assert(err.code == 0); + kvstore_kv_Store_ListKeysIterator_destroy(it); + return n; } -// Encode a StoreInfo. Object fields are written as fresh strong references -// (`_clone`), since the buffer's reader adopts them. -static void write_store_info(wv_writer* w, const char* label, - weaveffi_kv_Store* store, weaveffi_kv_Store* mirror, - int64_t n) { - wv_put_str(w, label); - wv_put_obj(w, weaveffi_kv_Store_clone(store)); - wv_put_bool(w, mirror != NULL); - if (mirror) wv_put_obj(w, weaveffi_kv_Store_clone(mirror)); - wv_put_i64(w, n); +static kvstore_kv_StoreInfo describe(kvstore_kv_Store* s, const char* label, + const kvstore_kv_Store* mirror) { + kvstore_error err = {0}; + size_t len = 0; + const uint8_t* buf = kvstore_kv_Store_describe(s, STR(label), mirror, &len, &err); + assert(err.code == 0 && buf != NULL); + kvstore_kv_StoreInfo info; + assert(kvstore_kv_StoreInfo_decode(buf, len, &info)); + kvstore_free_bytes((uint8_t*)buf, len); + return info; } -int main(void) { - weaveffi_error err = {0}; +static void basics(kvstore_kv_Store* store) { + kvstore_error err = {0}; const uint8_t payload[3] = {1, 2, 3}; - assert(weaveffi_abi_version() == 2u && WEAVEFFI_ABI_VERSION == 2u); - - // Static method: no receiver, plain error-out slot. - assert(weaveffi_kv_Store_default_capacity(&err) == 1000000); - assert(err.code == 0); - - // Fallible constructor, typed error path: an empty path reports the - // KvError.IoError domain code (1004) with its doc-comment message. - weaveffi_kv_Store* bad = weaveffi_kv_Store_open("", &err); - assert(bad == NULL); - assert(err.code == weaveffi_kv_KvError_IoError); - assert(err.message != NULL && strcmp(err.message, "I/O failure") == 0); - weaveffi_error_clear(&err); - - // A null `string` argument is a marshalling failure (-3). - assert(weaveffi_kv_Store_open(NULL, &err) == NULL); - assert(err.code == -3); - weaveffi_error_clear(&err); - - weaveffi_kv_Store* store = open_store("/tmp/conformance-kvstore-c"); - // Populate two keys so count/iterator/stats have something to report. - put(store, "alpha", payload, sizeof payload); - assert(weaveffi_kv_Store_put(store, "beta", payload, sizeof payload, - weaveffi_kv_EntryKind_Volatile, - TTL_NONE, sizeof TTL_NONE, &err)); + put(store, "alpha", BYTES(payload)); + ttl_buf none = ttl(NULL); + assert(kvstore_kv_Store_put(store, STR("beta"), BYTES(payload), + kvstore_kv_EntryKind_Volatile, none.w.ptr, none.w.len, &err)); assert(err.code == 0); assert(count(store) == 2); // An out-of-range enum discriminant is a marshalling failure (-3). - assert(!weaveffi_kv_Store_put(store, "bad", payload, sizeof payload, - (weaveffi_kv_EntryKind)999, TTL_NONE, - sizeof TTL_NONE, &err)); + assert(!kvstore_kv_Store_put(store, STR("bad"), BYTES(payload), (kvstore_kv_EntryKind)999, + none.w.ptr, none.w.len, &err)); assert(err.code == -3); - weaveffi_error_clear(&err); + kvstore_error_clear(&err); + // So is invalid UTF-8 in a string parameter. + const uint8_t bad_utf8[2] = {0xC3, 0x28}; + assert(!kvstore_kv_Store_put(store, BYTES(bad_utf8), BYTES(payload), + kvstore_kv_EntryKind_Volatile, none.w.ptr, none.w.len, &err)); + assert(err.code == -3); + kvstore_error_clear(&err); + kvstore_writer_free(&none.w); assert(count(store) == 2); - // Typed error path on a method: a missing key reports KeyNotFound (1001). - size_t out_len = 0; - const uint8_t* nope = weaveffi_kv_Store_get(store, "missing", &out_len, &err); - assert(nope == NULL); - assert(err.code == weaveffi_kv_KvError_KeyNotFound); + // Typed error path on a method: a missing key reports KeyNotFound. + size_t len = 0; + assert(kvstore_kv_Store_get(store, STR("missing"), &len, &err) == NULL); + assert(err.code == kvstore_kv_KvError_KeyNotFound); assert(strcmp(err.message, "key not found") == 0); - weaveffi_error_clear(&err); + kvstore_error_clear(&err); - // Deprecated method still works at the ABI level. The generated header - // marks it deprecated (that attribute is part of what this consumer - // verifies), so silence the warning for this one deliberate call. + // The deprecated method still works; the header marks it deprecated, so + // silence the warning for this one deliberate call. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - assert(weaveffi_kv_Store_legacy_put(store, "old", payload, sizeof payload, &err)); + assert(kvstore_kv_Store_legacy_put(store, STR("old"), BYTES(payload), &err)); #pragma clang diagnostic pop assert(err.code == 0); - assert(weaveffi_kv_Store_delete(store, "old", &err) && err.code == 0); - assert(!weaveffi_kv_Store_delete(store, "old", &err) && err.code == 0); - - // Iterator: `next` writes one element per call and returns 1/0. Keys - // come back sorted. The optional prefix parameter is a buffered - // `string?`; absent first, then present. - weaveffi_kv_Store_ListKeysIterator* it = - weaveffi_kv_Store_list_keys(store, TTL_NONE, sizeof TTL_NONE, &err); - assert(err.code == 0 && it != NULL); - const char* expected_keys[] = {"alpha", "beta"}; - int n = 0; - const char* item = NULL; - weaveffi_error iter_err = {0}; - while (weaveffi_kv_Store_ListKeysIterator_next(it, &item, &iter_err) != 0) { - assert(n < 2 && strcmp(item, expected_keys[n]) == 0); - weaveffi_free_string(item); - n++; - } - assert(iter_err.code == 0); - weaveffi_kv_Store_ListKeysIterator_destroy(it); - assert(n == 2); - - wv_writer prefix; - wv_w_init(&prefix); - wv_put_bool(&prefix, 1); - wv_put_str(&prefix, "be"); - it = weaveffi_kv_Store_list_keys(store, prefix.buf, prefix.len, &err); - assert(err.code == 0 && it != NULL); - assert(weaveffi_kv_Store_ListKeysIterator_next(it, &item, &iter_err) == 1); - assert(strcmp(item, "beta") == 0); - weaveffi_free_string(item); - assert(weaveffi_kv_Store_ListKeysIterator_next(it, &item, &iter_err) == 0); - weaveffi_kv_Store_ListKeysIterator_destroy(it); - wv_w_free(&prefix); - - // get -> buffered `Entry?`: flag byte, then the record's fields in - // declaration order (id, key, value, created_at, expires_at?, tags, - // metadata). The list and map fields nest inside the same buffer. - const uint8_t* ebuf = weaveffi_kv_Store_get(store, "alpha", &out_len, &err); - assert(err.code == 0 && ebuf != NULL); - wv_reader r; - wv_r_init(&r, ebuf, out_len); - assert(wv_get_bool(&r) == 1 && "entry present"); - int64_t id = wv_get_i64(&r); - assert(id == 1 && "first id handed out by this store"); - char* key = wv_get_str(&r); - assert(strcmp(key, "alpha") == 0); - free(key); - uint32_t vlen = wv_get_u32(&r); - assert(vlen == 3); - const uint8_t* vbytes = wv_take(&r, vlen); - assert(memcmp(vbytes, payload, 3) == 0); - int64_t created_at = wv_get_i64(&r); - assert(created_at > 0); - assert(wv_get_bool(&r) == 0 && "no TTL: expires_at is absent"); - assert(wv_get_u32(&r) == 0 && "no tags"); - assert(wv_get_u32(&r) == 0 && "no metadata"); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)ebuf, out_len); - - // kv.stats submodule: the Stats record is three i64 fields in one buffer. - const uint8_t* sbuf = weaveffi_kv_stats_get_stats(store, &out_len, &err); - assert(err.code == 0 && sbuf != NULL); - wv_r_init(&r, sbuf, out_len); - assert(wv_get_i64(&r) == 2 && "total_entries"); - assert(wv_get_i64(&r) == 6 && "total_bytes: two 3-byte values"); - assert(wv_get_i64(&r) == 0 && "expired_entries"); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)sbuf, out_len); - - // ── eviction listener ──────────────────────────────────────────────── - // delete fires on_evict synchronously on the calling thread with the - // decoded Entry and reason Deleted. + assert(kvstore_kv_Store_delete(store, STR("old"), &err) && err.code == 0); + assert(!kvstore_kv_Store_delete(store, STR("old"), &err) && err.code == 0); + + // Iterator: keys come back sorted, filtered by the optional prefix. + char* keys[4]; + int n = list_keys(store, NULL, keys, 4); + assert(n == 2 && strcmp(keys[0], "alpha") == 0 && strcmp(keys[1], "beta") == 0); + for (int i = 0; i < n; i++) free(keys[i]); + n = list_keys(store, "be", keys, 4); + assert(n == 1 && strcmp(keys[0], "beta") == 0); + free(keys[0]); + assert(list_keys(store, "zzz", keys, 4) == 0); + + // get -> buffered `Entry?` with nested list and map fields. + const uint8_t* buf = kvstore_kv_Store_get(store, STR("alpha"), &len, &err); + assert(err.code == 0 && buf != NULL); + kvstore_kv_Entry* e = NULL; + assert(kvstore_opt_kv_Entry_decode(buf, len, &e)); + kvstore_free_bytes((uint8_t*)buf, len); + assert(e != NULL); + assert(e->id == 1 && "first id handed out by this store"); + assert(strcmp(e->key.ptr, "alpha") == 0); + assert(e->value.len == 3 && memcmp(e->value.ptr, payload, 3) == 0); + assert(e->created_at > 0); + assert(e->expires_at == NULL && "no TTL"); + assert(e->tags.len == 0 && e->metadata.len == 0); + kvstore_opt_kv_Entry_free(&e); + + // kv.stats submodule: takes the parent module's Store. + buf = kvstore_kv_stats_get_stats(store, &len, &err); + assert(err.code == 0 && buf != NULL); + kvstore_kv_stats_Stats stats; + assert(kvstore_kv_stats_Stats_decode(buf, len, &stats)); + kvstore_free_bytes((uint8_t*)buf, len); + assert(stats.total_entries == 2); + assert(stats.total_bytes == 6 && "two 3-byte values"); + assert(stats.expired_entries == 0); +} + +static void listeners(kvstore_kv_Store* store) { + kvstore_error err = {0}; + const uint8_t payload[3] = {1, 2, 3}; + size_t len = 0; + + // delete fires on_evict synchronously with the Entry and reason Deleted. listener_ctx* l1 = new_listener(0, 0); - weaveffi_kv_Store_set_eviction_listener(store, l1, &LISTENER_VTABLE, &err); + kvstore_kv_Store_set_eviction_listener(store, l1, &LISTENER_VTABLE, &err); assert(err.code == 0); - assert(weaveffi_kv_Store_delete(store, "beta", &err) && err.code == 0); + assert(kvstore_kv_Store_delete(store, STR("beta"), &err) && err.code == 0); assert(l1->evictions == 1); assert(strcmp(l1->last_key, "beta") == 0); - assert(l1->last_reason == weaveffi_kv_EvictionReason_Deleted); + assert(l1->last_reason == kvstore_kv_EvictionReason_Deleted); assert(l1->last_value_len == 3); - // An expired entry is evicted on read: get reports Expired (1002) and - // the listener sees reason Expired. - put_ttl(store, "stale", payload, 2, -1); - assert(weaveffi_kv_Store_get(store, "stale", &out_len, &err) == NULL); - assert(err.code == weaveffi_kv_KvError_Expired); - weaveffi_error_clear(&err); + // An expired entry is evicted on read: get reports Expired and the + // listener sees reason Expired. + const int64_t past = -1; + put_ttl(store, "stale", payload, 2, &past); + assert(kvstore_kv_Store_get(store, STR("stale"), &len, &err) == NULL); + assert(err.code == kvstore_kv_KvError_Expired); + kvstore_error_clear(&err); assert(l1->evictions == 2); assert(strcmp(l1->last_key, "stale") == 0); - assert(l1->last_reason == weaveffi_kv_EvictionReason_Expired); + assert(l1->last_reason == kvstore_kv_EvictionReason_Expired); assert(l1->last_value_len == 2); // Replacing the listener frees the previous one; clearing frees the // current one. assert(g_listener_freed == 0); - listener_ctx* l2 = new_listener(0, 0); - weaveffi_kv_Store_set_eviction_listener(store, l2, &LISTENER_VTABLE, &err); + kvstore_kv_Store_set_eviction_listener(store, new_listener(0, 0), &LISTENER_VTABLE, &err); assert(g_listener_freed == 1 && "replaced listener freed"); - weaveffi_kv_Store_clear_eviction_listener(store, &err); - assert(err.code == 0); - assert(g_listener_freed == 2 && "cleared listener freed"); - weaveffi_kv_Store_clear_eviction_listener(store, &err); + kvstore_kv_Store_clear_eviction_listener(store, &err); + assert(err.code == 0 && g_listener_freed == 2); + kvstore_kv_Store_clear_eviction_listener(store, &err); assert(err.code == 0 && g_listener_freed == 2); // Nothing is attached now: a delete is not observed anywhere. put(store, "gamma", payload, 1); - assert(weaveffi_kv_Store_delete(store, "gamma", &err)); + assert(kvstore_kv_Store_delete(store, STR("gamma"), &err)); // A listener returning false detaches itself (and is freed). - listener_ctx* l3 = new_listener(1, 0); - weaveffi_kv_Store_set_eviction_listener(store, l3, &LISTENER_VTABLE, &err); + kvstore_kv_Store_set_eviction_listener(store, new_listener(1, 0), &LISTENER_VTABLE, &err); put(store, "d1", payload, 1); put(store, "d2", payload, 1); - assert(weaveffi_kv_Store_delete(store, "d1", &err)); + assert(kvstore_kv_Store_delete(store, STR("d1"), &err)); assert(g_listener_freed == 3 && "listener that answered false is freed"); - assert(weaveffi_kv_Store_delete(store, "d2", &err)); + assert(kvstore_kv_Store_delete(store, STR("d2"), &err)); assert(g_listener_freed == 3); - // A listener that reports a foreign error aborts the delete with -4; the - // store stays usable and still holds the listener until it is cleared. - listener_ctx* l4 = new_listener(0, 1); - weaveffi_kv_Store_set_eviction_listener(store, l4, &LISTENER_VTABLE, &err); + // A listener that reports a failure aborts the delete with -4; the store + // stays usable and still holds the listener until it is cleared. + kvstore_kv_Store_set_eviction_listener(store, new_listener(0, 1), &LISTENER_VTABLE, &err); put(store, "boom", payload, 1); - weaveffi_kv_Store_delete(store, "boom", &err); + kvstore_kv_Store_delete(store, STR("boom"), &err); assert(err.code == -4); assert(err.message != NULL && strstr(err.message, "listener exploded") != NULL); - weaveffi_error_clear(&err); + kvstore_error_clear(&err); assert(count(store) == 1 && "the entry was removed before the listener ran"); assert(g_listener_freed == 3); - weaveffi_kv_Store_clear_eviction_listener(store, &err); + kvstore_kv_Store_clear_eviction_listener(store, &err); assert(g_listener_freed == 4); +} + +static void compaction(kvstore_kv_Store* store) { + const uint8_t payload[3] = {1, 2, 3}; - // ── async compaction ───────────────────────────────────────────────── - // An immediately-expired entry gives compact 3 bytes to reclaim. The raw - // `_async` launcher returns immediately; completion arrives on the - // producer's worker thread. - put_ttl(store, "doomed", payload, sizeof payload, 0); - weaveffi_kv_Store_compact_async(store, NULL, on_compact_done, NULL); + // An immediately-expired entry gives compact 3 bytes to reclaim. The + // launcher returns at once; completion arrives on a producer thread. + const int64_t now = 0; + put_ttl(store, "doomed", BYTES(payload), &now); + kvstore_kv_Store_compact(store, NULL, on_compact_done, (void*)0x5150); wait_compact(); assert(g_compact_err == 0); assert(g_compact_result == 3); assert(count(store) == 1); - // A cancelled token makes compact fail with IoError through the - // callback's heap-boxed error. - weaveffi_cancel_token* token = weaveffi_cancel_token_create(); - assert(!weaveffi_cancel_token_is_cancelled(token)); - weaveffi_cancel_token_cancel(token); - assert(weaveffi_cancel_token_is_cancelled(token)); - weaveffi_kv_Store_compact_async(store, token, on_compact_done, NULL); + // With a live token, compaction runs normally. + kvstore_cancel_token* token = kvstore_cancel_token_create(); + kvstore_kv_Store_compact(store, token, on_compact_done, (void*)0x5150); wait_compact(); - assert(g_compact_err == weaveffi_kv_KvError_IoError); - weaveffi_cancel_token_destroy(token); + assert(g_compact_err == 0 && g_compact_result == 0); + kvstore_cancel_token_destroy(token); + + // A cancelled token completes the call with the cancelled code (-5) + // through the heap-boxed error. + token = kvstore_cancel_token_create(); + assert(!kvstore_cancel_token_is_cancelled(token)); + kvstore_cancel_token_cancel(token); + assert(kvstore_cancel_token_is_cancelled(token)); + kvstore_kv_Store_compact(store, token, on_compact_done, (void*)0x5150); + kvstore_cancel_token_destroy(token); + wait_compact(); + assert(g_compact_err == -5); + assert(count(store) == 1); +} - // ── reference counting and the object graph ────────────────────────── - // share() returns the very same object: identical pointer, and after the - // original reference is released the shared one still sees the data. - weaveffi_kv_Store* shared = weaveffi_kv_Store_share(store, &err); - assert(err.code == 0); - assert(shared == store && "share returns the same object"); - weaveffi_kv_Store_destroy(store); +static void object_graph(kvstore_kv_Store* store) { + kvstore_error err = {0}; + const uint8_t payload[3] = {1, 2, 3}; + size_t len = 0; + + // share() returns the very same object; after the original reference is + // released the shared one still sees the data. + kvstore_kv_Store* shared = kvstore_kv_Store_share(store, &err); + assert(err.code == 0 && shared == store); + kvstore_kv_Store_destroy(store); assert(count(shared) == 1 && "still alive through the shared reference"); put(shared, "via-shared", payload, 1); assert(count(shared) == 2); store = shared; // clone: same pointer; destroy the original, the clone still works. - weaveffi_kv_Store* cloned = weaveffi_kv_Store_clone(store); + kvstore_kv_Store* cloned = kvstore_kv_Store_clone(store); assert(cloned == store); - weaveffi_kv_Store_destroy(store); + kvstore_kv_Store_destroy(store); assert(count(cloned) == 2 && "clone outlives the destroyed original"); store = cloned; - assert(weaveffi_kv_Store_clone(NULL) == NULL); - weaveffi_kv_Store_destroy(NULL); + assert(kvstore_kv_Store_clone(NULL) == NULL); + kvstore_kv_Store_destroy(NULL); // fork() is a distinct object with a copy of the entries. - weaveffi_kv_Store* forked = weaveffi_kv_Store_fork(store, &err); + kvstore_kv_Store* forked = kvstore_kv_Store_fork(store, &err); assert(err.code == 0 && forked != NULL && forked != store); assert(count(forked) == 2); put(forked, "only-in-fork", payload, 1); assert(count(forked) == 3 && count(store) == 2); - // larger(): `Store?` in and out. null means "none" both ways; a returned + // larger(): `Store?` in and out; NULL is "none" both ways, and a returned // object is an owned reference. - weaveffi_kv_Store* empty = open_store("/tmp/conformance-kvstore-c-empty"); - assert(weaveffi_kv_Store_larger(empty, NULL, &err) == NULL); + kvstore_kv_Store* empty = open_store("/tmp/conformance-kvstore-c-empty"); + assert(kvstore_kv_Store_larger(empty, NULL, &err) == NULL); assert(err.code == 0); - weaveffi_kv_Store* bigger = weaveffi_kv_Store_larger(empty, forked, &err); + kvstore_kv_Store* bigger = kvstore_kv_Store_larger(empty, forked, &err); assert(bigger == forked); - weaveffi_kv_Store_destroy(bigger); - bigger = weaveffi_kv_Store_larger(forked, store, &err); + kvstore_kv_Store_destroy(bigger); + bigger = kvstore_kv_Store_larger(forked, store, &err); assert(bigger == forked && "self wins when it holds more"); - weaveffi_kv_Store_destroy(bigger); - bigger = weaveffi_kv_Store_larger(store, NULL, &err); + kvstore_kv_Store_destroy(bigger); + bigger = kvstore_kv_Store_larger(store, NULL, &err); assert(bigger == store && "a non-empty self is returned when other is absent"); - weaveffi_kv_Store_destroy(bigger); - assert(count(forked) == 3 && "still alive after releasing the returned refs"); - - // describe(): a record carrying the object itself (a token that is one - // strong reference) and an optional object. - const uint8_t* ibuf = weaveffi_kv_Store_describe(store, "primary", NULL, &out_len, &err); - assert(err.code == 0 && ibuf != NULL); - store_info_t info; - wv_r_init(&r, ibuf, out_len); - read_store_info(&r, &info); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)ibuf, out_len); - assert(strcmp(info.label, "primary") == 0); + kvstore_kv_Store_destroy(bigger); + + // describe(): a record carrying the object itself and an optional object. + kvstore_kv_StoreInfo info = describe(store, "primary", NULL); + assert(strcmp(info.label.ptr, "primary") == 0); assert(info.store == store && "describe().store is the receiver"); - assert(info.mirror == NULL); - assert(info.count == 2); - free(info.label); - weaveffi_kv_Store_destroy(info.store); // release the adopted token - - ibuf = weaveffi_kv_Store_describe(forked, "with-mirror", store, &out_len, &err); - assert(err.code == 0 && ibuf != NULL); - wv_r_init(&r, ibuf, out_len); - read_store_info(&r, &info); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)ibuf, out_len); - assert(strcmp(info.label, "with-mirror") == 0); + assert(info.mirror == NULL && info.count == 2); + kvstore_kv_StoreInfo_free(&info); // releases the adopted reference + + info = describe(forked, "with-mirror", store); assert(info.store == forked && info.mirror == store && info.count == 3); assert(count(info.mirror) == 2 && "the mirror token is a live reference"); - free(info.label); - weaveffi_kv_Store_destroy(info.store); - weaveffi_kv_Store_destroy(info.mirror); - - // open_many(): a list of objects as a return (u32 count + tokens). - wv_writer paths; - wv_w_init(&paths); - wv_put_u32(&paths, 2); - wv_put_str(&paths, "/a"); - wv_put_str(&paths, "/b"); - const uint8_t* mbuf = weaveffi_kv_Store_open_many(paths.buf, paths.len, &out_len, &err); - assert(err.code == 0 && mbuf != NULL); - wv_r_init(&r, mbuf, out_len); - assert(wv_get_u32(&r) == 2); - weaveffi_kv_Store* m0 = (weaveffi_kv_Store*)wv_get_obj(&r); - weaveffi_kv_Store* m1 = (weaveffi_kv_Store*)wv_get_obj(&r); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)mbuf, out_len); - wv_w_free(&paths); - assert(m0 != NULL && m1 != NULL && m0 != m1); - put(m0, "m", payload, 1); - assert(count(m0) == 1 && count(m1) == 0); + kvstore_kv_StoreInfo_free(&info); + + // open_many(): a list of objects as a return. + kvstore_str paths[2] = {kvstore_str_of("/a"), kvstore_str_of("/b")}; + kvstore_list_string path_list; + path_list.items = paths; + path_list.len = 2; + kvstore_writer w; + memset(&w, 0, sizeof w); + kvstore_list_string_write(&w, &path_list); + const uint8_t* buf = kvstore_kv_Store_open_many(w.ptr, w.len, &len, &err); + kvstore_writer_free(&w); + assert(err.code == 0 && buf != NULL); + kvstore_list_kv_Store many; + assert(kvstore_list_kv_Store_decode(buf, len, &many)); + kvstore_free_bytes((uint8_t*)buf, len); + assert(many.len == 2 && many.items[0] != many.items[1]); + put(many.items[0], "m", payload, 1); + assert(count(many.items[0]) == 1 && count(many.items[1]) == 0); // A failing path fails the whole call with the typed code and no list. - wv_w_init(&paths); - wv_put_u32(&paths, 2); - wv_put_str(&paths, "/ok"); - wv_put_str(&paths, ""); - assert(weaveffi_kv_Store_open_many(paths.buf, paths.len, &out_len, &err) == NULL); - assert(err.code == weaveffi_kv_KvError_IoError); - weaveffi_error_clear(&err); - wv_w_free(&paths); - - // total_count(): objects inside a parameter list and inside an optional - // record. Every token written is a fresh `_clone`, which the producer - // adopts and drops; our own references stay valid afterward. - wv_writer stores; - wv_w_init(&stores); - wv_put_u32(&stores, 3); - wv_put_obj(&stores, weaveffi_kv_Store_clone(m0)); - wv_put_obj(&stores, weaveffi_kv_Store_clone(m1)); - wv_put_obj(&stores, weaveffi_kv_Store_clone(forked)); - wv_writer extra; - wv_w_init(&extra); - wv_put_bool(&extra, 1); - write_store_info(&extra, "extra", store, forked, count(store)); - int64_t total = weaveffi_kv_Store_total_count(stores.buf, stores.len, - extra.buf, extra.len, &err); + paths[1] = kvstore_str_of(""); + memset(&w, 0, sizeof w); + kvstore_list_string_write(&w, &path_list); + assert(kvstore_kv_Store_open_many(w.ptr, w.len, &len, &err) == NULL); + kvstore_writer_free(&w); + assert(err.code == kvstore_kv_KvError_IoError); + assert(strcmp(err.message, "I/O failure") == 0); + kvstore_error_clear(&err); + + // total_count(): objects inside a list and inside an optional record. + // The writer stores a fresh `_clone` per object, which the producer + // adopts and drops; our own references stay valid. + kvstore_kv_Store* three[3] = {many.items[0], many.items[1], forked}; + kvstore_list_kv_Store stores; + stores.items = three; + stores.len = 3; + kvstore_kv_StoreInfo extra; + extra.label = kvstore_str_of("extra"); + extra.store = store; + extra.mirror = forked; + extra.count = count(store); + kvstore_writer ws, we; + memset(&ws, 0, sizeof ws); + memset(&we, 0, sizeof we); + kvstore_list_kv_Store_write(&ws, &stores); + kvstore_opt_kv_StoreInfo_write(&we, &extra); + assert(kvstore_kv_Store_total_count(ws.ptr, ws.len, we.ptr, we.len, &err) == + 1 + 0 + 3 + 2); assert(err.code == 0); - assert(total == 1 + 0 + 3 + 2); - wv_w_free(&stores); - wv_w_free(&extra); + kvstore_writer_free(&ws); + kvstore_writer_free(&we); // ... and with the optional record absent, plus an empty list. - wv_w_init(&stores); - wv_put_u32(&stores, 1); - wv_put_obj(&stores, weaveffi_kv_Store_clone(m0)); - const uint8_t none[1] = {0}; - assert(weaveffi_kv_Store_total_count(stores.buf, stores.len, none, 1, &err) == 1); - wv_w_free(&stores); - wv_w_init(&stores); - wv_put_u32(&stores, 0); - assert(weaveffi_kv_Store_total_count(stores.buf, stores.len, none, 1, &err) == 0); - wv_w_free(&stores); + stores.len = 1; + memset(&ws, 0, sizeof ws); + memset(&we, 0, sizeof we); + kvstore_list_kv_Store_write(&ws, &stores); + kvstore_opt_kv_StoreInfo_write(&we, NULL); + assert(kvstore_kv_Store_total_count(ws.ptr, ws.len, we.ptr, we.len, &err) == 1); + kvstore_writer_free(&ws); + stores.len = 0; + memset(&ws, 0, sizeof ws); + kvstore_list_kv_Store_write(&ws, &stores); + assert(kvstore_kv_Store_total_count(ws.ptr, ws.len, we.ptr, we.len, &err) == 0); + kvstore_writer_free(&ws); + kvstore_writer_free(&we); // Everything we still hold is intact. - assert(count(m0) == 1 && count(m1) == 0 && count(forked) == 3 && count(store) == 2); + assert(count(many.items[0]) == 1 && count(forked) == 3 && count(store) == 2); // clear() and release every reference exactly once. - weaveffi_kv_Store_clear(forked, &err); + kvstore_kv_Store_clear(forked, &err); assert(err.code == 0 && count(forked) == 0); - weaveffi_kv_Store_destroy(m0); - weaveffi_kv_Store_destroy(m1); - weaveffi_kv_Store_destroy(empty); - weaveffi_kv_Store_destroy(forked); - weaveffi_kv_Store_destroy(store); + kvstore_list_kv_Store_free(&many); // destroys both opened stores + kvstore_kv_Store_destroy(empty); + kvstore_kv_Store_destroy(forked); + kvstore_kv_Store_destroy(store); +} + +int main(void) { + kvstore_error err = {0}; + + assert(KVSTORE_ABI_VERSION == 3u && kvstore_abi_version() == KVSTORE_ABI_VERSION); + assert(kvstore_kv_checksum() == KVSTORE_KV_CHECKSUM); + + // Static method: no receiver, plain error-out slot. + assert(kvstore_kv_Store_default_capacity(&err) == 1000000); + assert(err.code == 0); + + // Fallible constructor, typed error path: an empty path reports IoError + // with the error's Display message. + assert(kvstore_kv_Store_open(STR(""), &err) == NULL); + assert(err.code == kvstore_kv_KvError_IoError); + assert(err.message != NULL && strcmp(err.message, "I/O failure") == 0); + kvstore_error_clear(&err); + + // A NULL string pointer with a nonzero length is a marshalling failure. + assert(kvstore_kv_Store_open(NULL, 4, &err) == NULL); + assert(err.code == -3); + kvstore_error_clear(&err); + + kvstore_kv_Store* store = open_store("/tmp/conformance-kvstore-c"); + basics(store); + listeners(store); + compaction(store); + object_graph(store); // consumes `store` // A store dropped with a listener attached frees the listener. - weaveffi_kv_Store* with_listener = open_store("/tmp/conformance-kvstore-c-l"); - weaveffi_kv_Store_set_eviction_listener(with_listener, new_listener(0, 0), - &LISTENER_VTABLE, &err); + kvstore_kv_Store* with_listener = open_store("/tmp/conformance-kvstore-c-l"); + kvstore_kv_Store_set_eviction_listener(with_listener, new_listener(0, 0), &LISTENER_VTABLE, + &err); assert(g_listener_freed == 4); - weaveffi_kv_Store_destroy(with_listener); + kvstore_kv_Store_destroy(with_listener); assert(g_listener_freed == 5 && "dropping the store frees its listener"); + ASSERT_NO_LEAKS(kvstore_debug_live); printf("c/kvstore: OK\n"); return 0; } diff --git a/conformance/c/producer.c b/conformance/c/producer.c index 89aeadf2..6721b7f7 100644 --- a/conformance/c/producer.c +++ b/conformance/c/producer.c @@ -1,73 +1,149 @@ -// Producer-side conformance: a C backend that *implements* the generated C ABI -// (rather than consuming a prebuilt library) must still export its symbols when -// the shared library is built with hidden default visibility -// (-fvisibility=hidden, the release-build norm and the MSVC default). The -// generated header tags every prototype with the WEAVEFFI_API visibility macro, -// so an implementing definition keeps default visibility and the symbol stays -// exported. Without the macro the symbols would be hidden and unusable, exactly -// the failure reported in https://github.com/weavefoundry/weaveffi/issues/23. +// Producer-side conformance: a C library that *implements* the generated +// calculator header instead of consuming a prebuilt one. // -// This file implements every symbol the calculator header declares (the user -// functions plus the runtime helpers a non-Rust producer also has to supply) -// with trivial bodies, so it links as a self-contained shared library. The -// harness then checks with `nm` that the symbols are exported. -#include "weaveffi.h" +// A hand-written producer exports three groups of symbols, all prefixed with +// the header's prefix (`calculator`): +// +// * the API's own functions (`calculator_calculator_add`, ...); +// * the runtime surface every consumer relies on: the ABI revision, the +// error helpers, `free_bytes` for returned strings and buffers, cancel +// tokens, and the debug leak counter; +// * one checksum function per top-level module, returning the constant the +// header was generated with, so a consumer generated from a different +// contract refuses to load. +// +// The harness builds this file with -fvisibility=hidden and checks with `nm` +// that every symbol is still exported (the header's CALCULATOR_API macro +// marks each prototype), then runs producer_check.c against it. +#include +#include +#include -int32_t weaveffi_calculator_add(int32_t a, int32_t b, weaveffi_error* out_err) { - (void)out_err; - return a + b; -} +#include "calculator.h" -int32_t weaveffi_calculator_mul(int32_t a, int32_t b, weaveffi_error* out_err) { - (void)out_err; - return a * b; +// Live returned allocations and cancel tokens, for calculator_debug_live. +static atomic_uint_fast64_t live_allocations; +static atomic_uint_fast64_t live_tokens; + +// ── runtime ──────────────────────────────────────────────────────────────── + +uint32_t calculator_abi_version(void) { return CALCULATOR_ABI_VERSION; } + +uint64_t calculator_calculator_checksum(void) { return CALCULATOR_CALCULATOR_CHECKSUM; } + +void calculator_error_set(calculator_error* err, int32_t code, const char* message) { + if (err == NULL) return; + calculator_error_clear(err); + err->code = code; + if (message != NULL) { + size_t n = strlen(message) + 1; + char* copy = (char*)malloc(n); + if (copy != NULL) memcpy(copy, message, n); + err->message = copy; + } } -int32_t weaveffi_calculator_div(int32_t a, int32_t b, weaveffi_error* out_err) { - (void)out_err; - return b != 0 ? a / b : 0; +void calculator_error_clear(calculator_error* err) { + if (err == NULL) return; + free((void*)err->message); + free((void*)err->payload_ptr); + err->code = 0; + err->message = NULL; + err->payload_ptr = NULL; + err->payload_len = 0; } -const char* weaveffi_calculator_echo(const char* s, weaveffi_error* out_err) { - (void)out_err; - return s; +void calculator_error_free(calculator_error* err) { + calculator_error_clear(err); + free(err); } -void weaveffi_error_clear(weaveffi_error* err) { - if (err) { - err->code = 0; - err->message = 0; - err->payload_ptr = 0; - err->payload_len = 0; +void calculator_free_bytes(uint8_t* ptr, size_t len) { + (void)len; + if (ptr != NULL) { + atomic_fetch_sub(&live_allocations, 1); + free(ptr); } } -void weaveffi_error_free(weaveffi_error* err) { - (void)err; +struct calculator_cancel_token { + atomic_int refs; + atomic_bool cancelled; +}; + +calculator_cancel_token* calculator_cancel_token_create(void) { + calculator_cancel_token* t = (calculator_cancel_token*)malloc(sizeof *t); + if (t == NULL) return NULL; + atomic_init(&t->refs, 1); + atomic_init(&t->cancelled, false); + atomic_fetch_add(&live_tokens, 1); + return t; } -void weaveffi_free_string(const char* ptr) { - (void)ptr; +void calculator_cancel_token_cancel(calculator_cancel_token* token) { + if (token != NULL) atomic_store(&token->cancelled, true); } -void weaveffi_free_bytes(uint8_t* ptr, size_t len) { - (void)ptr; - (void)len; +bool calculator_cancel_token_is_cancelled(const calculator_cancel_token* token) { + return token != NULL && atomic_load(&((calculator_cancel_token*)token)->cancelled); } -weaveffi_cancel_token* weaveffi_cancel_token_create(void) { - return 0; +void calculator_cancel_token_destroy(calculator_cancel_token* token) { + if (token != NULL && atomic_fetch_sub(&token->refs, 1) == 1) { + atomic_fetch_sub(&live_tokens, 1); + free(token); + } } -void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token) { - (void)token; +uint64_t calculator_debug_live(int32_t kind) { + switch (kind) { + case 3: + return atomic_load(&live_tokens); + case 4: + return atomic_load(&live_allocations); + default: + return 0; + } } -bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token) { - (void)token; - return false; +// ── API ──────────────────────────────────────────────────────────────────── + +int32_t calculator_calculator_add(int32_t a, int32_t b, calculator_error* out_err) { + (void)out_err; + return a + b; } -void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token) { - (void)token; +int32_t calculator_calculator_mul(int32_t a, int32_t b, calculator_error* out_err) { + (void)out_err; + return a * b; +} + +int32_t calculator_calculator_div(int32_t a, int32_t b, calculator_error* out_err) { + if (b == 0) { + calculator_error_set(out_err, calculator_calculator_CalcError_DivisionByZero, + "division by zero"); + return 0; + } + return a / b; +} + +// Strings arrive as borrowed (ptr, len) and return as an owned run the +// consumer releases with calculator_free_bytes; NULL + 0 is the empty string. +const uint8_t* calculator_calculator_echo(const uint8_t* s_ptr, size_t s_len, size_t* out_len, + calculator_error* out_err) { + *out_len = 0; + if (s_ptr == NULL && s_len != 0) { + calculator_error_set(out_err, -3, "null string with a nonzero length"); + return NULL; + } + if (s_len == 0) return NULL; + uint8_t* copy = (uint8_t*)malloc(s_len); + if (copy == NULL) { + calculator_error_set(out_err, -1, "out of memory"); + return NULL; + } + memcpy(copy, s_ptr, s_len); + atomic_fetch_add(&live_allocations, 1); + *out_len = s_len; + return copy; } diff --git a/conformance/c/producer_check.c b/conformance/c/producer_check.c new file mode 100644 index 00000000..e82238c5 --- /dev/null +++ b/conformance/c/producer_check.c @@ -0,0 +1,41 @@ +// Drives the hand-written C producer (producer.c) through the generated +// calculator header exactly as a consumer of the Rust producer would: the +// load-time contract check, a call, a typed error, a (ptr, len) string round +// trip with an interior NUL, a cancel token, and the leak counters. + +#include "harness.h" + +#include "calculator.h" + +int main(void) { + calculator_error err = {0}; + + assert(calculator_abi_version() == CALCULATOR_ABI_VERSION); + assert(calculator_calculator_checksum() == CALCULATOR_CALCULATOR_CHECKSUM); + + assert(calculator_calculator_add(2, 3, &err) == 5 && err.code == 0); + assert(calculator_calculator_mul(4, -5, &err) == -20); + assert(calculator_calculator_div(9, 0, &err) == 0); + assert(err.code == calculator_calculator_CalcError_DivisionByZero); + assert(strcmp(err.message, "division by zero") == 0); + calculator_error_clear(&err); + assert(err.code == 0 && err.message == NULL); + + static const char text[] = "a\0b"; + size_t len = 0; + const uint8_t* echoed = + calculator_calculator_echo((const uint8_t*)text, sizeof text - 1, &len, &err); + assert(err.code == 0 && len == 3 && memcmp(echoed, text, 3) == 0); + assert(calculator_debug_live(4) == 1); + calculator_free_bytes((uint8_t*)echoed, len); + + calculator_cancel_token* token = calculator_cancel_token_create(); + assert(!calculator_cancel_token_is_cancelled(token)); + calculator_cancel_token_cancel(token); + assert(calculator_cancel_token_is_cancelled(token)); + calculator_cancel_token_destroy(token); + + ASSERT_NO_LEAKS(calculator_debug_live); + printf("c/producer: OK\n"); + return 0; +} diff --git a/conformance/c/run.sh b/conformance/c/run.sh new file mode 100755 index 00000000..4792aae1 --- /dev/null +++ b/conformance/c/run.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Conformance lanes for the c target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane compiles one +# consumer against a sample's generated `c/` directory as-is (the ABI header +# plus the value-buffer helper header) and must exit 0. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# c_consumer : compile conformance/c/ against the +# sample's generated headers with warnings as errors and the address and +# undefined-behavior sanitizers, link the sample's cdylib, and run it. +c_consumer() { + local sample=$1 src=$2 + local lib=${sample//-/_} + local exe="$OUT/c_${lib}" + clang -std=c11 -Wall -Wextra -Werror -g -fsanitize=address,undefined \ + -I "$GENROOT/$sample/c" -I "$ROOT/conformance/c" \ + "$ROOT/conformance/c/$src" -L "$LIBDIR" -l"$lib" -lpthread -lm -o "$exe" \ + && "$exe" +} + +c_contacts() { c_consumer contacts contacts.c; } +c_events() { c_consumer events events.c; } +c_kvstore() { c_consumer kvstore kvstore.c; } +c_async_demo() { c_consumer async-demo async_demo.c; } +c_codec() { c_consumer codec codec.c; } + +# Producer lane: unlike every other lane (which *consumes* a prebuilt cdylib), +# this compiles a C library that *implements* the generated calculator header, +# including every runtime and checksum symbol a hand-written producer must +# export, under hidden default visibility (-fvisibility=hidden, the release +# norm and the MSVC default). The header tags each prototype with +# CALCULATOR_API, so the definitions stay exported; the lane checks each one +# with `nm`. Regression oracle for +# https://github.com/weavefoundry/weaveffi/issues/23. +c_producer_exports() { + local incdir="$GENROOT/calculator/c" + local lib="$OUT/libcalc_producer.$EXT" + clang -std=c11 -Wall -Wextra -Werror -shared -fPIC -fvisibility=hidden \ + -I "$incdir" "$ROOT/conformance/c/producer.c" -o "$lib" \ + || { echo "producer compile failed" >&2; return 1; } + local syms + syms=$(nm -g --defined-only "$lib" 2>/dev/null) || syms=$(nm -gU "$lib" 2>/dev/null) + local sym + for sym in calculator_calculator_add calculator_calculator_mul \ + calculator_calculator_div calculator_calculator_echo \ + calculator_abi_version calculator_calculator_checksum \ + calculator_error_set calculator_error_clear calculator_error_free \ + calculator_free_bytes calculator_cancel_token_create \ + calculator_cancel_token_cancel calculator_cancel_token_is_cancelled \ + calculator_cancel_token_destroy calculator_debug_live; do + if ! printf '%s\n' "$syms" | grep -Eq "(^| )_?${sym}\$"; then + echo "symbol '$sym' not exported under hidden visibility from $lib" >&2 + printf '%s\n' "$syms" | head -40 >&2 + return 1 + fi + done + if printf '%s\n' "$syms" | grep -q 'weaveffi'; then + echo "a hand-written producer must not need any weaveffi_* symbol" >&2 + return 1 + fi + # Drive the hand-written producer through the same header. + clang -std=c11 -Wall -Wextra -Werror -I "$incdir" -I "$ROOT/conformance/c" \ + "$ROOT/conformance/c/producer_check.c" -L "$OUT" -lcalc_producer \ + -o "$OUT/c_producer_check" \ + && DYLD_LIBRARY_PATH="$OUT:${DYLD_LIBRARY_PATH:-}" \ + LD_LIBRARY_PATH="$OUT:${LD_LIBRARY_PATH:-}" "$OUT/c_producer_check" +} + +lane c-contacts c_contacts +lane c-events c_events +lane c-kvstore c_kvstore +lane c-async-demo c_async_demo +lane c-codec c_codec +lane c-producer-exports c_producer_exports + +finish_lanes diff --git a/conformance/c/shapes.c b/conformance/c/shapes.c deleted file mode 100644 index 8f5d07ba..00000000 --- a/conformance/c/shapes.c +++ /dev/null @@ -1,92 +0,0 @@ -// Conformance consumer: shapes sample, C target. -// -// Includes the *generated* C header and links the shapes cdylib, exercising -// rich (algebraic) enums crossing the ABI as value buffers (i32 tag followed -// by the active variant's fields) plus the expanded numeric set (f32 fields, -// u8 field, u64 return). Exits 0 on success; aborts on any failed assertion. - -#include -#include -#include -#include - -#include "weaveffi.h" -#include "wvbuf.h" - -// Shape variant tags, in declaration order from the IDL. -enum { SHAPE_EMPTY = 0, SHAPE_CIRCLE = 1, SHAPE_RECTANGLE = 2, SHAPE_LABELED = 3 }; - -int main(void) { - weaveffi_error err = {0}; - - // Encode Circle { radius: 2.5 }: i32 tag + f64 radius. - wv_writer circle; - wv_w_init(&circle); - wv_put_i32(&circle, SHAPE_CIRCLE); - wv_put_f64(&circle, 2.5); - - // describe: buffered rich-enum parameter, string return. - const char* desc = weaveffi_shapes_describe(circle.buf, circle.len, &err); - assert(err.code == 0); - assert(strcmp(desc, "circle(r=2.5)") == 0); - weaveffi_free_string(desc); - - // Rectangle { width: 3.0f32, height: 4.0f32 } dispatches on its tag too. - wv_writer rect; - wv_w_init(&rect); - wv_put_i32(&rect, SHAPE_RECTANGLE); - wv_put_f32(&rect, 3.0f); - wv_put_f32(&rect, 4.0f); - desc = weaveffi_shapes_describe(rect.buf, rect.len, &err); - assert(err.code == 0); - assert(strstr(desc, "rect") != NULL); - weaveffi_free_string(desc); - wv_w_free(&rect); - - // Labeled { label: "hex", count: 6 }: string + u8 payload. - wv_writer labeled; - wv_w_init(&labeled); - wv_put_i32(&labeled, SHAPE_LABELED); - wv_put_str(&labeled, "hex"); - wv_put_u8(&labeled, 6); - desc = weaveffi_shapes_describe(labeled.buf, labeled.len, &err); - assert(err.code == 0); - assert(strstr(desc, "hex") != NULL); - weaveffi_free_string(desc); - wv_w_free(&labeled); - - // Empty: tag only. - wv_writer empty; - wv_w_init(&empty); - wv_put_i32(&empty, SHAPE_EMPTY); - desc = weaveffi_shapes_describe(empty.buf, empty.len, &err); - assert(err.code == 0); - weaveffi_free_string(desc); - wv_w_free(&empty); - - // scale: rich enum in and out. The returned buffer holds the scaled shape. - size_t out_len = 0; - const uint8_t* big = - weaveffi_shapes_scale(circle.buf, circle.len, 4.0, &out_len, &err); - assert(err.code == 0 && big != NULL); - wv_reader r; - wv_r_init(&r, big, out_len); - assert(wv_get_i32(&r) == SHAPE_CIRCLE); - assert(fabs(wv_get_f64(&r) - 10.0) < 1e-9); - wv_r_expect_end(&r); - weaveffi_free_bytes((uint8_t*)big, out_len); - wv_w_free(&circle); - - // A C-style enum keeps its plain int32 constants. - assert(weaveffi_shapes_Channel_Green == 1); - - // numerics: `[u8]` canonicalizes to `bytes`, so it keeps the raw - // pointer-plus-length ABI (no buffer framing). - uint8_t raw[4] = {250, 250, 250, 250}; - uint64_t total = weaveffi_shapes_sum_bytes(raw, sizeof raw, &err); - assert(err.code == 0); - assert(total == 1000); - - printf("c/shapes: OK\n"); - return 0; -} diff --git a/conformance/c/wvbuf.h b/conformance/c/wvbuf.h deleted file mode 100644 index 3a44b70b..00000000 --- a/conformance/c/wvbuf.h +++ /dev/null @@ -1,202 +0,0 @@ -// Minimal WeaveFFI value-buffer writer/reader for the C conformance -// consumers. Implements the wire format specified in -// docs/src/reference/value-buffers.md: little-endian, packed, u32 length -// prefixes. Consumers assert on malformed input, which is exactly the -// contract-violation behavior the spec prescribes for generated bindings. -#ifndef WVBUF_H -#define WVBUF_H - -#include -#include -#include -#include - -// ── writer ────────────────────────────────────────────────────────────────── - -typedef struct { - uint8_t* buf; - size_t len; - size_t cap; -} wv_writer; - -static inline void wv_w_init(wv_writer* w) { - w->buf = NULL; - w->len = 0; - w->cap = 0; -} - -static inline void wv_w_free(wv_writer* w) { - free(w->buf); - w->buf = NULL; - w->len = w->cap = 0; -} - -static inline void wv_put_raw(wv_writer* w, const void* p, size_t n) { - if (w->len + n > w->cap) { - size_t cap = w->cap ? w->cap * 2 : 64; - while (cap < w->len + n) cap *= 2; - w->buf = (uint8_t*)realloc(w->buf, cap); - assert(w->buf != NULL); - w->cap = cap; - } - memcpy(w->buf + w->len, p, n); - w->len += n; -} - -static inline void wv_put_u8(wv_writer* w, uint8_t v) { wv_put_raw(w, &v, 1); } -static inline void wv_put_i8(wv_writer* w, int8_t v) { wv_put_u8(w, (uint8_t)v); } -static inline void wv_put_bool(wv_writer* w, int v) { wv_put_u8(w, v ? 1 : 0); } - -static inline void wv_put_u16(wv_writer* w, uint16_t v) { - uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; - wv_put_raw(w, b, 2); -} - -static inline void wv_put_i16(wv_writer* w, int16_t v) { - wv_put_u16(w, (uint16_t)v); -} - -static inline void wv_put_u32(wv_writer* w, uint32_t v) { - uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), - (uint8_t)(v >> 24)}; - wv_put_raw(w, b, 4); -} - -static inline void wv_put_i32(wv_writer* w, int32_t v) { - wv_put_u32(w, (uint32_t)v); -} - -static inline void wv_put_u64(wv_writer* w, uint64_t v) { - uint8_t b[8]; - for (int i = 0; i < 8; i++) b[i] = (uint8_t)(v >> (8 * i)); - wv_put_raw(w, b, 8); -} - -static inline void wv_put_i64(wv_writer* w, int64_t v) { - wv_put_u64(w, (uint64_t)v); -} - -static inline void wv_put_f32(wv_writer* w, float v) { - uint32_t bits; - memcpy(&bits, &v, 4); - wv_put_u32(w, bits); -} - -static inline void wv_put_f64(wv_writer* w, double v) { - uint64_t bits; - memcpy(&bits, &v, 8); - wv_put_u64(w, bits); -} - -static inline void wv_put_str(wv_writer* w, const char* s) { - size_t n = strlen(s); - wv_put_u32(w, (uint32_t)n); - wv_put_raw(w, s, n); -} - -static inline void wv_put_bytes(wv_writer* w, const uint8_t* p, size_t n) { - wv_put_u32(w, (uint32_t)n); - wv_put_raw(w, p, n); -} - -// Object token: the object pointer widened to u64. The token carries one -// strong reference, so the caller passes a pointer it owns and gives up (in -// practice the result of `{tag}_clone`), never a pointer it still holds. -static inline void wv_put_obj(wv_writer* w, const void* owned_ref) { - assert(owned_ref != NULL && "zero object token in a required position"); - wv_put_u64(w, (uint64_t)(uintptr_t)owned_ref); -} - -// ── reader ────────────────────────────────────────────────────────────────── - -typedef struct { - const uint8_t* p; - size_t len; - size_t off; -} wv_reader; - -static inline void wv_r_init(wv_reader* r, const uint8_t* p, size_t len) { - r->p = p; - r->len = len; - r->off = 0; -} - -static inline const uint8_t* wv_take(wv_reader* r, size_t n) { - assert(r->off + n <= r->len && "value buffer exhausted"); - const uint8_t* at = r->p + r->off; - r->off += n; - return at; -} - -static inline uint8_t wv_get_u8(wv_reader* r) { return *wv_take(r, 1); } -static inline int8_t wv_get_i8(wv_reader* r) { return (int8_t)wv_get_u8(r); } - -static inline int wv_get_bool(wv_reader* r) { - uint8_t v = wv_get_u8(r); - assert(v <= 1 && "invalid bool byte"); - return v; -} - -static inline uint16_t wv_get_u16(wv_reader* r) { - const uint8_t* b = wv_take(r, 2); - return (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)); -} - -static inline int16_t wv_get_i16(wv_reader* r) { return (int16_t)wv_get_u16(r); } - -static inline uint32_t wv_get_u32(wv_reader* r) { - const uint8_t* b = wv_take(r, 4); - return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | - ((uint32_t)b[3] << 24); -} - -static inline int32_t wv_get_i32(wv_reader* r) { return (int32_t)wv_get_u32(r); } - -static inline uint64_t wv_get_u64(wv_reader* r) { - const uint8_t* b = wv_take(r, 8); - uint64_t v = 0; - for (int i = 0; i < 8; i++) v |= (uint64_t)b[i] << (8 * i); - return v; -} - -static inline int64_t wv_get_i64(wv_reader* r) { return (int64_t)wv_get_u64(r); } - -static inline float wv_get_f32(wv_reader* r) { - uint32_t bits = wv_get_u32(r); - float v; - memcpy(&v, &bits, 4); - return v; -} - -static inline double wv_get_f64(wv_reader* r) { - uint64_t bits = wv_get_u64(r); - double v; - memcpy(&v, &bits, 8); - return v; -} - -// Returns a heap-allocated NUL-terminated copy; caller frees with free(). -static inline char* wv_get_str(wv_reader* r) { - uint32_t n = wv_get_u32(r); - const uint8_t* at = wv_take(r, n); - char* s = (char*)malloc((size_t)n + 1); - assert(s != NULL); - memcpy(s, at, n); - s[n] = '\0'; - return s; -} - -// Object token: the reader adopts the strong reference it carries, so the -// returned pointer must eventually be released with `{tag}_destroy` (or kept -// as the consumer's own reference to the object). -static inline void* wv_get_obj(wv_reader* r) { - uint64_t token = wv_get_u64(r); - assert(token != 0 && "zero object token in a required position"); - return (void*)(uintptr_t)token; -} - -static inline void wv_r_expect_end(const wv_reader* r) { - assert(r->off == r->len && "trailing bytes after value"); -} - -#endif // WVBUF_H diff --git a/conformance/cpp/async_demo.cpp b/conformance/cpp/async_demo.cpp index 10657817..6298b19c 100644 --- a/conformance/cpp/async_demo.cpp +++ b/conformance/cpp/async_demo.cpp @@ -1,63 +1,143 @@ // Conformance consumer: async-demo sample, C++ target. // -// Drives the std::future-backed async surface of the generated header-only -// wrapper end to end: `weaveffi::tasks::run_task` settled from the producer's -// worker thread with a TaskResult struct decoded from a value buffer, the -// typed InvalidNameError (extending TaskError extending WeaveFFIError) -// rethrown by future::get for an empty name, the buffered list-of-records -// round trip through `run_batch`, the direct-scalar `run_n_tasks`, the sync -// `cancel_task`, and `active_callbacks` settling to zero once every task body -// has completed. Exits 0 on success; aborts (non-zero) on any failed -// assertion. - -#include +// Drives the std::future-backed async surface end to end: `run_task` +// settled from a producer thread with a decoded TaskResult record, the typed +// InvalidNameError (extending TaskError extending Error) rethrown by +// future::get, the buffered list-of-records round trip through `run_batch`, +// the direct-scalar `run_n_tasks`, and cancellation of the cancellable +// `wait` through an RAII CancelToken: cancelling an in-flight call settles +// its future with Cancelled (code -5) long before its timeout, a token +// destroyed mid-flight leaves the call running, and omitting the token runs +// to completion. Ends by asserting no task body is still active and the +// producer's leak counters are all zero. + +#include #include -#include +#include #include +#include #include -#include "weaveffi.hpp" +#include "async_demo.hpp" +#include "check.hpp" + +namespace ad = ::async_demo; + +static void run() { + ad::check_library(); + + ad::TaskResult result = ad::tasks::run_task("alpha").get(); + CHECK(result.id > 0); + CHECK(result.value == "completed: alpha"); + CHECK(result.success); -int main() { - // Async record return: future::get blocks until the worker-thread - // callback delivers the decoded TaskResult. - weaveffi::TaskResult result = weaveffi::tasks::run_task("alpha").get(); - assert(result.id > 0); - assert(result.value == "completed: alpha"); - assert(result.success); - - // Typed async error: the empty name settles the future with the typed - // exception, rethrown on get(). bool threw = false; try { - weaveffi::tasks::run_task("").get(); - } catch (const weaveffi::InvalidNameError& e) { - threw = true; - assert(e.code() == 1); - assert(dynamic_cast(&e) != nullptr); - assert(dynamic_cast(&e) != nullptr); - } - assert(threw && "expected InvalidNameError for empty name"); - - // Buffered list-of-records both ways. - std::vector batch = - weaveffi::tasks::run_batch({"a", "b", "c"}).get(); - assert(batch.size() == 3); + ad::tasks::run_task("").get(); + } catch (const ad::InvalidNameError& e) { + threw = e.code() == 1; + CHECK(dynamic_cast(&e) != nullptr); + CHECK(dynamic_cast(&e) != nullptr); + } + CHECK(threw); + + std::vector batch = ad::tasks::run_batch({"a", "b", "c"}).get(); + CHECK(batch.size() == 3); const char* expected[3] = {"completed: a", "completed: b", "completed: c"}; for (size_t i = 0; i < 3; i++) { - assert(batch[i].value == expected[i]); - assert(batch[i].success); + CHECK(batch[i].value == expected[i]); + CHECK(batch[i].success); + } + CHECK(ad::tasks::run_batch({}).get().empty()); + + CHECK(ad::tasks::run_n_tasks(7).get() == 7); + + // Many concurrent calls, each settled exactly once. + { + std::vector> pending; + for (int32_t i = 0; i < 64; i++) pending.push_back(ad::tasks::run_n_tasks(i)); + for (int32_t i = 0; i < 64; i++) CHECK(pending[i].get() == i); } - // Direct scalar through the async callback. - assert(weaveffi::tasks::run_n_tasks(7).get() == 7); + // No token: the call runs to its timeout. + CHECK(ad::tasks::wait(20).get() == 20); - // Sync functions beside the async ones. - assert(!weaveffi::tasks::cancel_task(1)); + // Cancelling an in-flight call settles it with Cancelled right away. + { + ad::CancelToken token; + auto start = std::chrono::steady_clock::now(); + std::future pending = ad::tasks::wait(30000, token); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + CHECK(pending.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + token.cancel(); + CHECK(token.is_cancelled()); + bool cancelled = false; + try { + pending.get(); + } catch (const ad::Cancelled& e) { + cancelled = e.code() == -5; + } + CHECK(cancelled); + CHECK(std::chrono::steady_clock::now() - start < std::chrono::seconds(10)); + } + + // A token cancelled before the call starts cancels it too, and one token + // can cancel several calls. + { + ad::CancelToken token; + std::future first = ad::tasks::wait(30000, token); + std::future second = ad::tasks::wait(30000, token); + token.cancel(); + int cancelled = 0; + for (auto* f : {&first, &second}) { + try { + f->get(); + } catch (const ad::Cancelled&) { + cancelled++; + } + } + CHECK(cancelled == 2); + std::future late = ad::tasks::wait(30000, token); + bool caught = false; + try { + late.get(); + } catch (const ad::Cancelled&) { + caught = true; + } + CHECK(caught); + } + + // Destroying the consumer's token mid-flight releases only its own + // reference: the call keeps running and completes normally. + { + std::future pending; + { + ad::CancelToken token; + pending = ad::tasks::wait(50, token); + } + CHECK(pending.get() == 50); + } - // Every spawned task body has completed by the time its callback fires. - assert(weaveffi::tasks::active_callbacks() == 0); + // A moved-from token is empty and cancelling it is a no-op. + { + ad::CancelToken token; + ad::CancelToken moved = std::move(token); + token.cancel(); + CHECK(!moved.is_cancelled()); + CHECK(ad::tasks::wait(1, moved).get() == 1); + } - std::printf("cpp async-demo conformance: OK\n"); + // Every spawned task body has finished, including the cancelled ones, + // whose futures the runtime dropped. + for (int i = 0; i < 200 && ad::tasks::active_callbacks() != 0; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + CHECK(ad::tasks::active_callbacks() == 0); +} + +int main() { + run(); + check_no_leaks(async_demo_debug_live, "async-demo"); + std::printf("cpp/async-demo: OK\n"); return 0; } diff --git a/conformance/cpp/calculator.cpp b/conformance/cpp/calculator.cpp new file mode 100644 index 00000000..b1aeb4bb --- /dev/null +++ b/conformance/cpp/calculator.cpp @@ -0,0 +1,49 @@ +// Conformance consumer: calculator sample, C++ target. +// +// Covers the smallest surface: direct scalars, a throwing function whose +// typed DivisionByZeroError derives from CalcError and Error, a string round +// trip through `std::string_view` (including an interior NUL, which the +// pointer-and-length ABI preserves), and the explicit library check. +// Ends by asserting the producer's leak counters are all zero. + +#include +#include + +#include "calculator.hpp" +#include "check.hpp" + +using namespace calculator; +namespace calc = ::calculator::calculator; + +static void run() { + check_library(); + + CHECK(calc::add(2, 3) == 5); + CHECK(calc::mul(-4, 6) == -24); + CHECK(calc::div(17, 5) == 3); + + bool caught = false; + try { + calc::div(1, 0); + } catch (const DivisionByZeroError& e) { + caught = e.code() == 1 && std::string(e.what()) == "division by zero"; + CHECK(dynamic_cast(&e) != nullptr); + CHECK(dynamic_cast(&e) != nullptr); + } + CHECK(caught); + + CHECK(calc::echo("hello") == "hello"); + CHECK(calc::echo("").empty()); + CHECK(calc::echo("héllo ✓") == "héllo ✓"); + const std::string with_nul("a\0b\0", 4); + const std::string echoed = calc::echo(with_nul); + CHECK(echoed.size() == 4); + CHECK(echoed == with_nul); +} + +int main() { + run(); + check_no_leaks(calculator_debug_live, "calculator"); + std::printf("cpp/calculator: OK\n"); + return 0; +} diff --git a/conformance/cpp/check.hpp b/conformance/cpp/check.hpp new file mode 100644 index 00000000..61c8e926 --- /dev/null +++ b/conformance/cpp/check.hpp @@ -0,0 +1,43 @@ +// Shared helpers for the C++ conformance consumers. +// +// CHECK aborts the consumer with a message on a failed assertion (unlike +// assert, it isn't compiled out under NDEBUG). check_no_leaks polls a +// producer's `{prefix}_debug_live` counters until every kind reads zero: +// async completions release their last producer-side references (the task, +// the cancel token reference) just after the consumer's future is ready, so +// the counters settle shortly after the last call returns. + +#pragma once + +#include +#include +#include +#include +#include + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, __LINE__, #cond); \ + std::exit(1); \ + } \ + } while (0) + +// Assert that `debug_live(kind)` settles to zero for every kind: 0 objects, +// 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. +inline void check_no_leaks(uint64_t (*debug_live)(int32_t), const char* sample) { + static const char* const kinds[] = {"objects", "callbacks", "iterators", "cancel tokens", + "allocations"}; + for (int32_t kind = 0; kind <= 4; ++kind) { + uint64_t live = debug_live(kind); + for (int i = 0; live != 0 && i < 200; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + live = debug_live(kind); + } + if (live != 0) { + std::fprintf(stderr, "cpp/%s: %llu live %s at exit\n", sample, + static_cast(live), kinds[kind]); + std::exit(1); + } + } +} diff --git a/conformance/cpp/codec.cpp b/conformance/cpp/codec.cpp index 6981c3ae..8d98c94c 100644 --- a/conformance/cpp/codec.cpp +++ b/conformance/cpp/codec.cpp @@ -1,4 +1,4 @@ -// Conformance consumer: codec sample, C++ target (ABI revision 2). +// Conformance consumer: codec sample, C++ target. // // Round-trips every value-buffer wire shape through the producer oracle with // the generated header-only wrapper: @@ -8,12 +8,14 @@ // unless the bytes decode to exactly the canonical value; // - `roundtrip_*` covers the direct, string, and bytes families plus // consumer-built values with edge cases (empty strings, lists, and maps, -// non-ASCII text, i64/u64 extremes, NaN, infinities, negative zero); +// non-ASCII text, interior NULs, i64/u64 extremes, NaN, infinities, +// negative zero); // - `Shape` exercises every rich-enum variant as a std::variant; // - `Holder` puts `Token` objects inside a record field, an optional, and a // list: decoding adopts one reference per token, encoding clones one per // token, `primary_of` returns the same object as `holder.primary`, and // `same_primary` compares identity through two encodings. +// Ends by asserting the producer's leak counters are all zero. // Exits non-zero on the first failed check. #include @@ -28,7 +30,8 @@ #include #include -#include "weaveffi.hpp" +#include "check.hpp" +#include "codec.hpp" // The root namespace is `codec` and the module namespace is `codec::codec`, // so types live at `codec::Scalars` and functions at `codec::codec::f`. @@ -134,8 +137,8 @@ static void check_canonical_scalars(const Scalars& s, const char* who) { check(s.color == Color::Blue, (prefix + "color").c_str()); } -int main() { - codec::check_abi_version(); +static void run() { + codec::check_library(); // Scalars: producer encodes, consumer decodes, and back again. Scalars sample = oracle::sample_scalars(); @@ -371,6 +374,14 @@ int main() { check(oracle::roundtrip_string("h\xC3\xA9llo \xE2\x9C\x93 \xF0\x9F\x8E\x89") == "h\xC3\xA9llo \xE2\x9C\x93 \xF0\x9F\x8E\x89", "roundtrip_string non-ASCII"); + // Strings cross as pointer and length, so interior NULs survive both + // as a direct argument and inside a value buffer. + const std::string with_nul("a\0b\0\0c", 6); + check(oracle::roundtrip_string(with_nul) == with_nul, "roundtrip_string interior NUL"); + check(oracle::roundtrip_string(std::string(1, '\0')).size() == 1, + "roundtrip_string lone NUL"); + std::unordered_map nul_keys{{with_nul, 1}, {std::string("\0", 1), 2}}; + check(oracle::roundtrip_map(nul_keys) == nul_keys, "roundtrip_map with NUL-bearing keys"); check(oracle::roundtrip_bytes({}).empty(), "roundtrip_bytes empty"); std::vector all_bytes; @@ -470,6 +481,11 @@ int main() { "long-lived tokens are still alive after the loop"); } +} + +int main() { + run(); + check_no_leaks(codec_debug_live, "codec"); std::printf("cpp/codec: OK\n"); return 0; } diff --git a/conformance/cpp/contacts.cpp b/conformance/cpp/contacts.cpp index 27b1bebf..ee0dd270 100644 --- a/conformance/cpp/contacts.cpp +++ b/conformance/cpp/contacts.cpp @@ -1,103 +1,123 @@ // Conformance consumer: contacts sample, C++ target. // -// Drives the generated header-only wrappers over the original struct/enum/ -// optional surface: `ContactBook` is a move-only RAII interface class whose -// canonical `new` constructor maps to the default constructor (destroy runs -// in the destructor), `Contact` is a plain value struct decoded from a value -// buffer (the optional email rides inside the same buffer), enums cross as -// `enum class` values, and throwing wrappers surface the `ContactsError` -// domain hierarchy (`InvalidNameError`, `NotFoundError`). Aborts (non-zero) -// on any failed assertion. +// Drives the generated header-only wrappers over the struct/enum/optional +// surface and the module graph: +// - `ContactBook` is an RAII interface class whose canonical `new` +// constructor maps to the default constructor; `Contact` is a value struct +// decoded from a value buffer; enums cross as `enum class` values; and +// throwing wrappers surface the `ContactsError` hierarchy. +// - `contacts::groups` is nested under the `contacts` root: its functions +// take and return the parent's `ContactBook` interface and `ContactType` +// enum, and report the parent's `ContactsError` domain. +// - `directory` is a sibling root sharing the `Contact` record and declaring +// its own `DirectoryError` domain. +// Ends by asserting the producer's leak counters are all zero. -#include #include #include #include #include -#include "weaveffi.hpp" +#include "check.hpp" +#include "contacts.hpp" -using namespace weaveffi; +using namespace contacts; +namespace grp = ::contacts::contacts::groups; +namespace dir = ::contacts::directory; -int main() { +static void run() { // Canonical `new` constructor maps to the default constructor. ContactBook book; - assert(book.count() == 0); + CHECK(book.count() == 0); - // add -> Contact value struct; the present email crosses as a buffered - // `string?` built from std::optional. - Contact alice = book.add("Alice", "Smith", std::string("alice@example.com"), - ContactType::Work); - assert(alice.id > 0); - assert(alice.first_name == "Alice"); - assert(alice.last_name == "Smith"); - assert(alice.email.has_value() && *alice.email == "alice@example.com"); - assert(alice.contact_type == ContactType::Work); + Contact alice = book.add("Alice", "Smith", std::string("alice@example.com"), ContactType::Work); + CHECK(alice.id > 0); + CHECK(alice.first_name == "Alice"); + CHECK(alice.last_name == "Smith"); + CHECK(alice.email.has_value() && *alice.email == "alice@example.com"); + CHECK(alice.contact_type == ContactType::Work); - // get -> a fresh Contact snapshot decoded from a new buffer. Contact snap = book.get(alice.id); - assert(snap.first_name == "Alice"); - assert(snap.last_name == "Smith"); - assert(snap.email.has_value() && *snap.email == "alice@example.com"); + CHECK(snap.first_name == "Alice" && snap.last_name == "Smith"); + CHECK(snap.email.has_value() && *snap.email == "alice@example.com"); - // Optional string: an absent email round-trips as a disengaged optional. Contact bob = book.add("Bob", "Jones", std::nullopt, ContactType::Personal); - assert(!bob.email.has_value()); - assert(bob.contact_type == ContactType::Personal); + CHECK(!bob.email.has_value()); + CHECK(bob.contact_type == ContactType::Personal); - // count + list: the list return is one buffer decoded into a vector. - assert(book.count() == 2); + Contact carol = book.add("Carol", "Adams", std::nullopt, ContactType::Work); + CHECK(book.count() == 3); std::vector everyone = book.list(); - assert(everyone.size() == 2); - bool saw_alice = false, saw_bob = false; - for (const auto& c : everyone) { - if (c.first_name == "Alice") saw_alice = true; - if (c.first_name == "Bob") saw_bob = true; - } - assert(saw_alice && saw_bob); + CHECK(everyone.size() == 3); - // remove returns whether the id existed; a second remove reports false. - assert(book.remove(alice.id)); - assert(!book.remove(alice.id)); - assert(book.count() == 1); - - // A missing id throws the per-code subclass; catching the domain base - // proves the hierarchy (NotFoundError -> ContactsError -> WeaveFFIError). - bool caught_missing = false; + // Typed errors: the per-code subclass derives from the domain and Error. + bool caught = false; try { book.get(9999); - } catch (const ContactsError& e) { - caught_missing = (e.code() == 2); + } catch (const NotFoundError& e) { + caught = e.code() == 2 && std::string(e.what()) == "contact not found"; + CHECK(dynamic_cast(&e) != nullptr); + CHECK(dynamic_cast(&e) != nullptr); + } + CHECK(caught); + caught = false; + try { + book.add("", "Nameless", std::nullopt, ContactType::Other); + } catch (const InvalidNameError& e) { + caught = e.code() == 1; } - assert(caught_missing); + CHECK(caught); + CHECK(book.count() == 3); - // The most-derived type is the per-code class. - bool caught_typed = false; + // The nested module uses the parent's interface and enum. + CHECK(grp::count_of_type(book, ContactType::Work) == 2); + CHECK(grp::count_of_type(book, ContactType::Other) == 0); + CHECK(grp::dominant_type(book) == ContactType::Work); + ContactBook work = grp::split_by_type(book, ContactType::Work); + CHECK(work.count() == 2); + CHECK(book.count() == 3); + Contact first_personal = grp::first_of_type(book, ContactType::Personal); + CHECK(first_personal.first_name == "Bob"); + // ...and reports the parent's error domain. + caught = false; try { - book.get(9999); - } catch (const NotFoundError&) { - caught_typed = true; + grp::first_of_type(work, ContactType::Personal); + } catch (const NotFoundError& e) { + caught = e.code() == 2; } - assert(caught_typed); + CHECK(caught); - // Typed error path: an empty first name reports InvalidNameError (1) and - // leaves the book unchanged. - bool caught_invalid = false; + // The sibling root shares the Contact record. + Card card = dir::card(alice); + CHECK(card.display_name == "Smith, Alice"); + CHECK(card.initials == "AS"); + CHECK(card.has_email); + CHECK(!dir::card(bob).has_email); + std::vector sorted = dir::sorted({alice, bob, carol}); + CHECK(sorted.size() == 3); + CHECK(sorted[0].last_name == "Adams" && sorted[1].last_name == "Jones" && + sorted[2].last_name == "Smith"); + caught = false; try { - book.add("", "Nameless", std::nullopt, ContactType::Other); - } catch (const InvalidNameError& e) { - caught_invalid = (e.code() == 1); + dir::sorted({}); + } catch (const EmptyError& e) { + caught = e.code() == 1; + CHECK(dynamic_cast(&e) != nullptr); } - assert(caught_invalid); - assert(book.count() == 1); + CHECK(caught); + + CHECK(book.remove(alice.id)); + CHECK(!book.remove(alice.id)); + CHECK(book.count() == 2); // Each ContactBook owns its own state. ContactBook other; - assert(other.count() == 0); - assert(book.count() == 1); + CHECK(other.count() == 0); +} - // RAII: the destructors call weaveffi_contacts_ContactBook_destroy when - // `book` and `other` leave scope; no explicit close. +int main() { + run(); + check_no_leaks(contacts_debug_live, "contacts"); std::printf("cpp/contacts: OK\n"); return 0; } diff --git a/conformance/cpp/events.cpp b/conformance/cpp/events.cpp index a8af3a6c..e36e8919 100644 --- a/conformance/cpp/events.cpp +++ b/conformance/cpp/events.cpp @@ -1,4 +1,4 @@ -// Conformance consumer: events sample, C++ target (ABI revision 2). +// Conformance consumer: events sample, C++ target. // // Drives the generated header-only wrapper over the callback-interface and // reference-counted-object surface: @@ -12,11 +12,15 @@ // - the `bus` handed to `on_attached` is an owned wrapper the consumer may // keep and use. // - `Delivery` return values steer `publish`'s accepted count. -// - a subscriber that throws surfaces to the caller as `WeaveFFIError` -// with code -4 and leaves the bus usable. +// - a subscriber that throws surfaces to the caller as `Error` with code +// -4 and leaves the bus usable. `route` is a `Result`-returning method on +// the producer side, so its failure arrives there as an `Err` value the +// bus propagates; `on_message` returns a plain value, so its failure +// aborts the producer's call directly. Both reach C++ the same way. // - `messages()` is a lazy single-pass range; `last_message()` is an // optional record; `publish_later` is a std::future settled from a // producer thread. +// Ends by asserting the producer's leak counters are all zero. // Exits non-zero on the first failed check. #include @@ -29,9 +33,11 @@ #include #include -#include "weaveffi.hpp" +#include "check.hpp" +#include "events.hpp" -using namespace weaveffi; +using namespace events; +namespace ev = ::events::events; static void check(bool ok, const char* what) { if (!ok) { @@ -47,7 +53,7 @@ struct SubState { std::vector received; int attached = 0; int64_t attached_count_seen = -1; - const weaveffi_events_EventBus* attached_handle = nullptr; + const events_events_EventBus* attached_handle = nullptr; std::optional kept_bus; int freed = 0; }; @@ -68,8 +74,8 @@ class RecordingSubscriber : public Subscriber { ~RecordingSubscriber() override { state_->freed++; } - Delivery route(const std::string& topic) override { - state_->routed.push_back(topic); + Delivery route(std::string_view topic) override { + state_->routed.emplace_back(topic); if (topic == skip_topic_) return Delivery::Skip; if (topic == stop_topic_) return Delivery::AcceptAndStop; return Delivery::Accept; @@ -101,8 +107,8 @@ class ThrowingSubscriber : public Subscriber { ThrowingSubscriber(std::string route_bomb, std::string message_bomb) : route_bomb_(std::move(route_bomb)), message_bomb_(std::move(message_bomb)) {} - Delivery route(const std::string& topic) override { - if (topic == route_bomb_) throw std::runtime_error("route rejected " + topic); + Delivery route(std::string_view topic) override { + if (topic == route_bomb_) throw std::runtime_error("route rejected " + std::string(topic)); return Delivery::Accept; } @@ -114,8 +120,8 @@ class ThrowingSubscriber : public Subscriber { void on_attached(EventBus) override {} }; -int main() { - check_abi_version(); +static void run() { + check_library(); EventBus bus; check(bus.handle() != nullptr, "constructor yields a live handle"); @@ -250,14 +256,14 @@ int main() { auto tmp_state = std::make_shared(); auto tmp = std::make_shared(tmp_state, "quiet", "stop", false); std::weak_ptr weak = tmp; - check(events::route_once(std::move(tmp), "quiet") == Delivery::Skip, "route_once Skip"); + check(ev::route_once(std::move(tmp), "quiet") == Delivery::Skip, "route_once Skip"); check(weak.expired() && tmp_state->freed == 1, "route_once released the subscriber when the call returned"); tmp_state = std::make_shared(); - check(events::route_once(std::make_shared(tmp_state, "", "stop", false), + check(ev::route_once(std::make_shared(tmp_state, "", "stop", false), "stop") == Delivery::AcceptAndStop, "route_once AcceptAndStop"); - check(events::route_once(std::make_shared(tmp_state, "", "", false), + check(ev::route_once(std::make_shared(tmp_state, "", "", false), "other") == Delivery::Accept, "route_once Accept"); check(tmp_state->attached == 0, "route_once never calls on_attached"); @@ -287,7 +293,7 @@ int main() { bool caught = false; try { bus2.publish("boom", "x", {}); - } catch (const WeaveFFIError& e) { + } catch (const Error& e) { caught = true; check(e.code() == -4, "route exception maps to FOREIGN_ERROR_CODE (-4)"); check(std::string(e.what()).find("route rejected boom") != std::string::npos, @@ -299,7 +305,7 @@ int main() { caught = false; try { bus2.publish("fine", "explode", {}); - } catch (const WeaveFFIError& e) { + } catch (const Error& e) { caught = true; check(e.code() == -4, "on_message exception maps to -4"); check(std::string(e.what()).find("on_message exploded") != std::string::npos, @@ -316,7 +322,7 @@ int main() { caught = false; try { bus2.publish_later("boom", "z").get(); - } catch (const WeaveFFIError& e) { + } catch (const Error& e) { caught = true; check(e.code() == -4, "async foreign error maps to -4"); } @@ -325,8 +331,8 @@ int main() { // route_once with a throwing subscriber. caught = false; try { - events::route_once(std::make_shared("boom", ""), "boom"); - } catch (const WeaveFFIError& e) { + ev::route_once(std::make_shared("boom", ""), "boom"); + } catch (const Error& e) { caught = (e.code() == -4); } check(caught, "route_once surfaces the foreign error"); @@ -350,7 +356,12 @@ int main() { check(bus.publish("after", "clear", {}) == 0, "publishing with no subscribers accepts none"); check(bus.last_message()->seq == 5, "log kept counting"); - // `bus` releases the last reference when main returns. + // `bus` releases the last reference when run returns. +} + +int main() { + run(); + check_no_leaks(events_debug_live, "events"); std::printf("cpp/events: OK\n"); return 0; } diff --git a/conformance/cpp/kvstore.cpp b/conformance/cpp/kvstore.cpp index e09a17ea..129a8c16 100644 --- a/conformance/cpp/kvstore.cpp +++ b/conformance/cpp/kvstore.cpp @@ -1,4 +1,4 @@ -// Conformance consumer: kvstore sample, C++ target (ABI revision 2). +// Conformance consumer: kvstore sample, C++ target. // // Drives the generated header-only wrapper over the object-graph surface: // - `Store` is a copyable RAII interface wrapper (copies `_clone`, moves @@ -12,11 +12,13 @@ // - `EvictionListener` is an abstract class the consumer subclasses; a // returned `false` detaches (and frees) it, replacing or clearing frees // the previous one, and an implementation that throws surfaces to the -// caller as `WeaveFFIError` code -4. +// caller as `Error` code -4. // - the pre-existing surface still works: value records with optional, // list, map, and bytes fields, the `KvError` hierarchy, the lazy // `list_keys` range, the `kv::stats` nested module, and the -// std::future-backed cancellable `compact`. +// std::future-backed cancellable `compact`, cancelled through an RAII +// `CancelToken` into `Cancelled`. +// Ends by asserting the producer's leak counters are all zero. // Exits non-zero on the first failed check. #include @@ -29,7 +31,8 @@ #include #include -#include "weaveffi.hpp" +#include "check.hpp" +#include "kvstore.hpp" using namespace kvstore; @@ -70,8 +73,8 @@ class RecordingListener : public EvictionListener { } }; -int main() { - check_abi_version(); +static void run() { + check_library(); check(Store::default_capacity() == 1000000, "static default_capacity"); @@ -86,7 +89,7 @@ int main() { } catch (const IoError& e) { caught = (e.code() == 1004); check(dynamic_cast(&e) != nullptr, "IoError is a KvError"); - check(dynamic_cast(&e) != nullptr, "IoError is a WeaveFFIError"); + check(dynamic_cast(&e) != nullptr, "IoError is an Error"); } check(caught, "open(\"\") throws IoError 1004"); } @@ -343,7 +346,7 @@ int main() { } // A listener that throws surfaces to the caller as a foreign error (-4) - // on a `throws` method: the generic WeaveFFIError, not a KvError. + // on a `throws` method: the generic Error, not a KvError. { auto state = std::make_shared(); store.set_eviction_listener(std::make_shared(state, 1000, "bomb")); @@ -353,7 +356,7 @@ int main() { store.delete_("bomb"); } catch (const KvError&) { check(false, "a foreign error must not be mapped to a domain exception"); - } catch (const WeaveFFIError& e) { + } catch (const Error& e) { caught = true; check(e.code() == -4, "listener exception maps to FOREIGN_ERROR_CODE (-4)"); check(std::string(e.what()).find("listener refused bomb") != std::string::npos, @@ -378,17 +381,18 @@ int main() { check(pending.get() == 3, "compact reclaimed the expired bytes"); check(store.count() == 2, "compact left the live entries"); - weaveffi_cancel_token* token = weaveffi_cancel_token_create(); - weaveffi_cancel_token_cancel(token); + CancelToken token; + check(!token.is_cancelled(), "a new token is not cancelled"); + token.cancel(); + check(token.is_cancelled(), "cancel() marks the token"); std::future cancelled = store.compact(token); bool caught = false; try { cancelled.get(); - } catch (const IoError& e) { - caught = (e.code() == 1004); + } catch (const Cancelled& e) { + caught = (e.code() == -5); } - check(caught, "a pre-cancelled compact settles the future with IoError"); - weaveffi_cancel_token_destroy(token); + check(caught, "a pre-cancelled compact settles the future with Cancelled"); } // Non-throwing method still works, then release everything via RAII. @@ -403,6 +407,11 @@ int main() { check(last.count() == 0, "moved-to store is usable"); } +} + +int main() { + run(); + check_no_leaks(kvstore_debug_live, "kvstore"); std::printf("cpp/kvstore: OK\n"); return 0; } diff --git a/conformance/cpp/run.sh b/conformance/cpp/run.sh new file mode 100755 index 00000000..4117cd06 --- /dev/null +++ b/conformance/cpp/run.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Conformance lanes for the cpp target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane compiles and runs one +# consumer against one sample and must exit 0. +# +# Each lane builds a throwaway CMake project that adds the generated `cpp/` +# directory as-is with add_subdirectory and links its `{library}::cpp` +# INTERFACE target. The only input besides the generated tree is the +# `{PREFIX}_LIBRARY` environment variable naming the sample's cdylib. The +# consumer compiles with warnings as errors under AddressSanitizer and +# UndefinedBehaviorSanitizer. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# cpp_lane +cpp_lane() { + local sample=$1 stem=$2 + local lib=${sample//-/_} + local proj="$OUT/cpp/$sample" + rm -rf "$proj" + mkdir -p "$proj" + cat > "$proj/CMakeLists.txt" </dev/null \ + && cmake --build "$proj/build" >/dev/null \ + && "$proj/build/consumer" +} + +cpp_calculator() { cpp_lane calculator calculator; } +cpp_contacts() { cpp_lane contacts contacts; } +cpp_events() { cpp_lane events events; } +cpp_kvstore() { cpp_lane kvstore kvstore; } +cpp_async_demo() { cpp_lane async-demo async_demo; } +cpp_codec() { cpp_lane codec codec; } + +lane cpp-calculator cpp_calculator +lane cpp-contacts cpp_contacts +lane cpp-events cpp_events +lane cpp-kvstore cpp_kvstore +lane cpp-async-demo cpp_async_demo +lane cpp-codec cpp_codec + +finish_lanes diff --git a/conformance/cpp/shapes.cpp b/conformance/cpp/shapes.cpp deleted file mode 100644 index 86437149..00000000 --- a/conformance/cpp/shapes.cpp +++ /dev/null @@ -1,60 +0,0 @@ -// Conformance consumer: shapes sample, C++ target. -// -// Drives the generated header-only wrappers for rich (algebraic) enums as -// value types: `Shape` is a plain struct holding a std::variant of per-variant -// payload structs plus a `tag()` reader, and the free functions take and -// return it by value (the wrapper packs and unpacks the value buffer -// underneath). Functions live at bare snake_case names in the per-module -// namespace, so with the configured root namespace `shapes` they sit at -// `shapes::shapes::describe`. Also covers the expanded numerics (f32 fields, -// u8 field, u64 return). Aborts (non-zero) on any failed assertion. - -#include -#include -#include -#include -#include -#include - -#include "weaveffi.hpp" - -using shapes::Shape; -using namespace shapes::shapes; - -int main() { - // Unit variant. - Shape empty{Shape::Empty{}}; - assert(empty.tag() == Shape::Tag::Empty); - assert(std::holds_alternative(empty.value)); - - // f64 payload. - Shape circle{Shape::Circle{2.5}}; - assert(circle.tag() == Shape::Tag::Circle); - assert(std::fabs(std::get(circle.value).radius - 2.5) < 1e-9); - - // Two f32 payloads. - Shape rect{Shape::Rectangle{3.0f, 4.0f}}; - assert(rect.tag() == Shape::Tag::Rectangle); - assert(std::fabs(std::get(rect.value).width - 3.0f) < 1e-6f); - assert(std::fabs(std::get(rect.value).height - 4.0f) < 1e-6f); - - // string + u8 payload. - Shape labeled{Shape::Labeled{"hex", 6}}; - assert(labeled.tag() == Shape::Tag::Labeled); - assert(std::get(labeled.value).label == "hex"); - assert(std::get(labeled.value).count == 6); - - // Free functions: Shape in, string/Shape out. - assert(describe(circle) == "circle(r=2.5)"); - - Shape big = scale(circle, 4.0); - assert(big.tag() == Shape::Tag::Circle); - assert(std::fabs(std::get(big.value).radius - 10.0) < 1e-9); - - // Numerics: list in, u64 out. - std::vector bytes{250, 250, 250, 250}; - assert(sum_bytes(bytes) == 1000); - - std::printf("cpp/shapes: OK\n"); - return 0; -} diff --git a/conformance/dart/async_demo.dart b/conformance/dart/async_demo.dart index 1d741e29..ab6a7b19 100644 --- a/conformance/dart/async_demo.dart +++ b/conformance/dart/async_demo.dart @@ -1,54 +1,77 @@ // Conformance consumer: async-demo sample, Dart target. // -// Drives the Future-backed async surface end to end: `runTask` settled -// through a NativeCallable.listener from the producer's worker thread and -// decoded from a value buffer into the plain TaskResult class, the typed -// InvalidNameException (extending TaskException extending WeaveFFIException) -// for an empty name, the buffered list-of-records round trip through -// `runBatch`, the direct-scalar `runNTasks`, the sync `cancelTask`, and -// `activeCallbacks` settling to zero once every task body has completed. -// Throws (non-zero exit) on any mismatch. - -import 'package:__PKG__/__LIB__.dart' as wv; - -void expect(bool cond, String msg) { - if (!cond) throw StateError('assertion failed: $msg'); +// Futures completed from the producer's worker threads: a record result, the +// typed InvalidNameException, a buffered list both ways, a direct scalar, +// many concurrent calls, and cancellation through CancelToken (cancelling a +// pending call, a token cancelled before launch, one token shared by two +// calls, and a cancel after completion), each cancelled call failing with +// CancelledException (code -5) long before its timeout. + +import 'package:async_demo/async_demo.dart' as tasks; + +import 'support.dart'; + +Future run() async { + final result = await tasks.runTask('alpha'); + expect(result.id > 0, 'runTask id'); + expect(result.value == 'completed: alpha', 'runTask value (${result.value})'); + expect(result.success, 'runTask success'); + + final invalid = await expectThrowsAsync( + () => tasks.runTask(''), 'empty name'); + expect(invalid.code == 1 && invalid is tasks.TaskException, 'InvalidName'); + + final batch = await tasks.runBatch(['a', 'b', 'c']); + expect(batch.map((r) => r.value).join('|') == + 'completed: a|completed: b|completed: c', + 'runBatch values'); + expect((await tasks.runBatch([])).isEmpty, 'runBatch empty'); + + expect(await tasks.runNTasks(7) == 7, 'runNTasks'); + final many = await Future.wait( + >[for (var i = 0; i < 64; i++) tasks.runNTasks(i)]); + expect(many.length == 64 && many[63] == 63, 'concurrent runNTasks'); + + // A timeout that elapses completes normally, with or without a token. + expect(await tasks.wait(20) == 20, 'wait elapses'); + final idle = tasks.CancelToken(); + expect(await tasks.wait(10, cancelToken: idle) == 10, 'uncancelled token'); + idle.cancel(); + expect(idle.isCancelled, 'cancel after completion is harmless'); + + // Cancelling a pending call completes it with CancelledException. + final watch = Stopwatch()..start(); + final token = tasks.CancelToken(); + final pending = outcome(tasks.wait(60000, cancelToken: token)); + await Future.delayed(const Duration(milliseconds: 20)); + token.cancel(); + final cancelled = await pending; + expect(cancelled is tasks.CancelledException, 'cancelled wait (got $cancelled)'); + final code = (cancelled! as tasks.NativeException).code; + expect(code == -5 && code == tasks.NativeException.cancelledCode, + 'cancelled code (got $code)'); + expect(watch.elapsed < const Duration(seconds: 5), 'cancel is prompt'); + + // A token cancelled before launch, and one token shared by two calls. + await expectThrowsAsync( + () => tasks.wait(60000, cancelToken: token), 'pre-cancelled token'); + final shared = tasks.CancelToken(); + final both = Future.wait(>[ + outcome(tasks.wait(60000, cancelToken: shared)), + outcome(tasks.wait(60000, cancelToken: shared)), + ]); + shared.cancel(); + shared.cancel(); + final outcomes = await both; + expect(outcomes.every((o) => o is tasks.CancelledException), + 'one token cancels both calls (got $outcomes)'); + expect(watch.elapsed < const Duration(seconds: 10), 'all cancels prompt'); + + await settle(() => tasks.activeCallbacks() == 0, 'task bodies finish'); } Future main() async { - // Async record return: the Future resolves with a plain TaskResult. - final result = await wv.runTask('alpha'); - expect(result.id > 0, 'runTask assigns an id'); - expect(result.value == 'completed: alpha', 'runTask value (got ${result.value})'); - expect(result.success, 'runTask success flag'); - - // Typed async error: the empty name settles with InvalidNameException. - try { - await wv.runTask(''); - expect(false, 'expected InvalidNameException for empty name'); - } on wv.InvalidNameException catch (e) { - expect(e.code == 1, 'InvalidName carries code 1 (got ${e.code})'); - expect(e is wv.TaskException, 'subclass of TaskException'); - expect(e is wv.WeaveFFIException, 'subclass of the brand exception'); - } - - // Buffered list-of-records both ways. - final batch = await wv.runBatch(['a', 'b', 'c']); - expect( - batch.map((r) => r.value).join('|') == - 'completed: a|completed: b|completed: c', - 'runBatch values', - ); - expect(batch.every((r) => r.success), 'runBatch success flags'); - - // Direct scalar through the async callback. - expect(await wv.runNTasks(7) == 7, 'runNTasks echoes n'); - - // Sync functions beside the async ones. - expect(!wv.cancelTask(1), 'cancelTask reports not cancelled'); - - // Every spawned task body has completed by the time its callback fires. - expect(wv.activeCallbacks() == 0, 'activeCallbacks settles to zero'); - - print('dart async-demo conformance: OK'); + await run(); + await expectNoLeaks('async_demo'); + print('dart/async-demo: OK'); } diff --git a/conformance/dart/calculator.dart b/conformance/dart/calculator.dart new file mode 100644 index 00000000..b30bd874 --- /dev/null +++ b/conformance/dart/calculator.dart @@ -0,0 +1,34 @@ +// Conformance consumer: calculator sample, Dart target. +// +// Direct scalars, the typed CalcException domain, and strings crossing as +// (ptr, len) UTF-8 runs, including interior NUL and non-ASCII text. + +import 'package:calculator/calculator.dart' as calc; + +import 'support.dart'; + +void run() { + expect(calc.add(2, 3) == 5, 'add'); + expect(calc.mul(-4, 6) == -24, 'mul'); + expect(calc.div(17, 5) == 3, 'div'); + + final e = expectThrows( + () => calc.div(1, 0), 'div by zero'); + expect(e.code == 1, 'DivisionByZero code (got ${e.code})'); + expect(e.message == 'division by zero', 'message (got ${e.message})'); + expect(e is calc.CalcException && e is calc.NativeException, + 'domain hierarchy'); + + for (final s in ['', 'hello', 'héllo wörld ✓ \u{1F600}', 'a\u0000b']) { + final back = calc.echo(s); + expect(back == s, 'echo round trip (got ${back.codeUnits})'); + } + final nul = calc.echo('nul\u0000inside'); + expect(nul.length == 10 && nul.codeUnitAt(3) == 0, 'interior NUL survives'); +} + +Future main() async { + run(); + await expectNoLeaks('calculator'); + print('dart/calculator: OK'); +} diff --git a/conformance/dart/codec.dart b/conformance/dart/codec.dart index 4a71c16b..eb5a3c99 100644 --- a/conformance/dart/codec.dart +++ b/conformance/dart/codec.dart @@ -17,11 +17,9 @@ import 'dart:typed_data'; -import 'package:__PKG__/__LIB__.dart' as wv; +import 'package:codec/codec.dart' as wv; -void expect(bool cond, String msg) { - if (!cond) throw StateError('assertion failed: $msg'); -} +import 'support.dart'; const int i64Min = -9223372036854775808; const int i64Max = 9223372036854775807; @@ -203,7 +201,7 @@ void checkScalars() { expect(e.code == 1, 'Mismatch code == 1 (got ${e.code})'); expect(e.message == 'value does not match the canonical fixture', 'Mismatch message (got ${e.message})'); - expect(e is wv.CodecException && e is wv.WeaveFFIException, + expect(e is wv.CodecException && e is wv.NativeException, 'Mismatch extends the domain and brand exceptions'); } @@ -460,6 +458,9 @@ void checkDirect() { expect(m.length == 3 && m[''] == i64Min && m['k'] == -1 && m['ü'] == i64Max, 'map values (got $m)'); expect(wv.roundtripString('') == '', 'empty string'); + final nul = wv.roundtripString('a\u0000b\u0000'); + expect(nul.length == 4 && nul.codeUnitAt(1) == 0 && nul.codeUnitAt(3) == 0, + 'interior and trailing NUL survive a (ptr, len) string'); expect(wv.roundtripString('héllo wörld ✓ \u{1F600}') == 'héllo wörld ✓ \u{1F600}', 'unicode string'); @@ -587,11 +588,12 @@ void checkHolder() { } } -void main() { +Future main() async { checkScalars(); checkComposite(); checkShapes(); checkDirect(); checkHolder(); + await expectNoLeaks('codec'); print('dart/codec: OK'); } diff --git a/conformance/dart/contacts.dart b/conformance/dart/contacts.dart index b4ac2ec9..89397eb8 100644 --- a/conformance/dart/contacts.dart +++ b/conformance/dart/contacts.dart @@ -1,78 +1,88 @@ // Conformance consumer: contacts sample, Dart target. // -// Binds through the generated `package:weaveffi` wrapper and drives the -// ContactBook interface surface: the unnamed factory constructor, instance -// methods passing the object pointer (add/get/list/remove/count), enum -// marshalling, the Contact record decoded from a value buffer into a plain -// Dart value class, optional strings (null email), list-of-record returns, -// boolean returns, and the typed ContactsException hierarchy -// (InvalidNameException = 1, NotFoundException = 2) raised by throwing -// methods. +// The ContactBook interface (unnamed factory, instance methods, dispose and +// use after dispose), the Contact record with an optional string and a +// C-style enum field, the typed ContactsException domain, the nested +// `contacts.groups` module taking and returning the parent's ContactType +// enum and ContactBook interface and reporting the parent's domain, and the +// sibling `directory` root sharing the Contact record and declaring its own +// DirectoryException domain. -import 'package:__PKG__/__LIB__.dart' as wv; +import 'package:contacts/contacts.dart' as ct; -void expect(bool cond, String msg) { - if (!cond) throw StateError('assertion failed: $msg'); -} +import 'support.dart'; -void main() { - final book = wv.ContactBook(); +void run() { + final book = ct.ContactBook(); final alice = - book.add('Alice', 'Smith', 'alice@example.com', wv.ContactType.work); + book.add('Alice', 'Smith', 'alice@example.com', ct.ContactType.work); expect(alice.id > 0, 'alice id positive'); - expect(alice.firstName == 'Alice', 'firstName'); - expect(alice.lastName == 'Smith', 'lastName'); + expect(alice.firstName == 'Alice' && alice.lastName == 'Smith', 'names'); expect(alice.email == 'alice@example.com', 'email'); - expect(alice.contactType == wv.ContactType.work, 'contactType'); - - final fetched = book.get(alice.id); - expect(fetched.firstName == 'Alice', 'get returns the stored record'); + expect(alice.contactType == ct.ContactType.work, 'contactType'); + expect(book.get(alice.id).firstName == 'Alice', 'get'); - // Optional string: a missing email round-trips as null. - final bob = book.add('Bob', 'Jones', null, wv.ContactType.personal); - expect(bob.email == null, 'bob email null'); - expect(bob.contactType == wv.ContactType.personal, 'bob contactType'); + final bob = book.add('Bob', 'Jones', null, ct.ContactType.personal); + expect(bob.email == null, 'absent email'); + book.add('Carol', 'Adams', 'carol@example.com', ct.ContactType.work); + book.add('Dan', 'Brown', null, ct.ContactType.other); + expect(book.count() == 4, 'count'); + final names = book.list().map((c) => c.firstName).toList()..sort(); + expect(names.join(',') == 'Alice,Bob,Carol,Dan', 'list (got $names)'); - expect(book.count() == 2, 'count == 2'); - final everyone = book.list(); - expect(everyone.length == 2, 'list length == 2'); - final names = everyone.map((p) => p.firstName).toList()..sort(); - expect(names.join(',') == 'Alice,Bob', 'list names'); + final invalid = expectThrows( + () => book.add('', 'Smith', null, ct.ContactType.personal), + 'empty name'); + expect(invalid is ct.InvalidNameException && invalid.code == 1, + 'InvalidName (got $invalid)'); + final missing = + expectThrows(() => book.get(9999), 'missing id'); + expect(missing.code == 2 && missing is ct.NativeException, 'NotFound'); - // Typed error: an empty name raises the InvalidNameException class of the - // ContactsException domain, carrying its stable code. - try { - book.add('', 'Smith', null, wv.ContactType.personal); - throw StateError('expected InvalidNameException for empty name'); - } on wv.ContactsException catch (e) { - expect(e is wv.InvalidNameException, 'InvalidName subclass (got $e)'); - expect(e.code == 1, 'InvalidName code == 1 (got ${e.code})'); - } + // Nested module: the parent's enum and interface cross the module + // boundary, and the parent's error domain applies. + expect(ct.countOfType(book, ct.ContactType.work) == 2, 'countOfType work'); + expect(ct.countOfType(book, ct.ContactType.other) == 1, 'countOfType other'); + expect(ct.dominantType(book) == ct.ContactType.work, 'dominantType'); + final work = ct.splitByType(book, ct.ContactType.work); + expect(work.count() == 2, 'splitByType returns a new book'); + expect(work.list().every((c) => c.contactType == ct.ContactType.work), + 'split book holds only work contacts'); + expect(ct.firstOfType(book, ct.ContactType.personal).firstName == 'Bob', + 'firstOfType'); + work.dispose(); + final empty = ct.ContactBook(); + expectThrows( + () => ct.firstOfType(empty, ct.ContactType.work), + 'nested module raises the parent domain'); + expect(ct.dominantType(empty) == ct.ContactType.personal, 'empty book'); + empty.dispose(); - expect(book.remove(alice.id) == true, 'remove returns true'); - expect(book.count() == 1, 'count == 1 after remove'); - expect(book.remove(alice.id) == false, 'second remove returns false'); + // Sibling root: the shared record crosses into another module tree. + final card = ct.card(alice); + expect(card.displayName == 'Smith, Alice', 'card name'); + expect(card.initials == 'AS' && card.hasEmail, 'card initials and email'); + expect(!ct.card(bob).hasEmail, 'card without email'); + final sorted = ct.sorted(book.list()); + expect(sorted.map((c) => c.lastName).join(',') == 'Adams,Brown,Jones,Smith', + 'sorted (got ${sorted.map((c) => c.lastName)})'); + final none = expectThrows( + () => ct.sorted([]), 'sorted empty'); + expect(none is ct.EmptyException && none.code == 1, 'EmptyException'); - // Typed error: a missing id raises NotFoundException, which is also - // catchable as the domain and the generic brand exception. - try { - book.get(9999); - throw StateError('expected NotFoundException for missing contact'); - } on wv.NotFoundException catch (e) { - expect(e.code == 2, 'NotFound code == 2 (got ${e.code})'); - expect(e is wv.ContactsException, 'NotFound extends ContactsException'); - expect(e is wv.WeaveFFIException, 'NotFound extends the generic brand'); - } + expect(book.remove(alice.id), 'remove'); + expect(!book.remove(alice.id), 'second remove'); + expect(book.count() == 3, 'count after remove'); - // Releasing the object twice is safe; using it afterward is a StateError. book.dispose(); book.dispose(); - try { - book.count(); - throw StateError('expected StateError after dispose'); - } on StateError catch (e) { - expect(e.message.contains('dispose'), 'use after dispose message'); - } + final gone = expectThrows(() => book.count(), 'use after dispose'); + expect(gone.message.contains('dispose'), 'use after dispose message'); +} + +Future main() async { + run(); + await expectNoLeaks('contacts'); print('dart/contacts: OK'); } diff --git a/conformance/dart/events.dart b/conformance/dart/events.dart index 1c2aa1bb..7fa78ff6 100644 --- a/conformance/dart/events.dart +++ b/conformance/dart/events.dart @@ -1,30 +1,27 @@ // Conformance consumer: events sample, Dart target. // -// Drives the ABI 2 surface end to end: a consumer-implemented `Subscriber` -// callback interface (a Dart class extending the generated abstract class, -// bound through isolate-local NativeCallable vtable trampolines), the -// reference-counted `EventBus` object (constructor, methods, dispose, double -// dispose, use after dispose), an object handed *to* the consumer through a -// callback method (`onAttached` adopts a second `EventBus` wrapper to the -// same native object and keeps it alive past the original's dispose), the -// `Delivery` enum steering `publish`'s accepted count, the buffered -// `Message` record decoded inside a callback, the lazy `Iterable` iterator -// behind `messages()`, the optional `lastMessage()`, the Future-backed -// `publishLater`, and a Dart exception thrown inside a callback surfacing to -// the original caller as `WeaveFFIException` with the foreign code (-4) -// without unwinding through the native frame. Throws (non-zero exit) on any -// mismatch. - -import 'package:__PKG__/__LIB__.dart' as wv; - -void expect(bool cond, String msg) { - if (!cond) throw StateError('assertion failed: $msg'); -} - -/// A subscriber that records every callback it receives. `skipTopic` is -/// routed as [wv.Delivery.skip], `stop` as [wv.Delivery.acceptAndStop], and -/// `failTopic` throws from `route`; `failOnMessage` throws from `onMessage`. -class RecordingSubscriber extends wv.Subscriber { +// A consumer-implemented `Subscriber`: `route` (a `Result`-returning method +// on the producer side) and `onMessage` return values synchronously through +// isolate-local trampolines, while the void `onAttached` is forwarded to the +// event loop and receives an `EventBus` it owns. Also covers the +// reference-counted `EventBus` (dispose, double dispose, use after dispose, +// an adopted second reference outliving the original), the `Delivery` enum, +// the buffered `Message` record decoded inside a callback, the lazy +// `messages()` iterator (including an abandoned iteration), the optional +// `lastMessage()`, the Future-backed `publishLater`, Dart exceptions in +// value methods surfacing to the caller as the foreign code (-4), and an +// exception in a void method reaching the zone that passed the subscriber. + +import 'dart:async'; + +import 'package:events/events.dart' as ev; + +import 'support.dart'; + +/// Records every callback. `skipTopic` routes as skip, `stop` as +/// acceptAndStop, `failTopic` throws from `route`, and `failOnMessage` +/// throws from `onMessage`. +class RecordingSubscriber extends ev.Subscriber { RecordingSubscriber(this.name, {this.skipTopic = '', this.failTopic = '', this.failOnMessage = false}); @@ -34,35 +31,32 @@ class RecordingSubscriber extends wv.Subscriber { final bool failOnMessage; final List routed = []; - final List received = []; + final List received = []; int attachedCalls = 0; int? subscribersSeenOnAttach; - wv.EventBus? adoptedBus; + ev.EventBus? adoptedBus; @override - wv.Delivery route(String topic) { + ev.Delivery route(String topic) { routed.add(topic); if (topic == failTopic) throw StateError('$name rejected topic $topic'); - if (topic == skipTopic) return wv.Delivery.skip; - if (topic == 'stop') return wv.Delivery.acceptAndStop; - return wv.Delivery.accept; + if (topic == skipTopic) return ev.Delivery.skip; + if (topic == 'stop') return ev.Delivery.acceptAndStop; + return ev.Delivery.accept; } @override - int onMessage(wv.Message message) { + int onMessage(ev.Message message) { if (failOnMessage) throw ArgumentError('$name cannot take ${message.text}'); received.add(message); return received.length; } @override - void onAttached(wv.EventBus bus) { + void onAttached(ev.EventBus bus) { attachedCalls++; - // The handed-in object is live and usable right here: the producer calls - // onAttached before it appends this subscriber, so the count excludes us. subscribersSeenOnAttach = bus.subscriberCount(); - // The reference is ours. One subscriber keeps it (released at the end of - // main); the others drop theirs right away. + // The reference is ours: the keeper holds on to it, the others drop it. if (name == 'keeper') { adoptedBus = bus; } else { @@ -71,218 +65,136 @@ class RecordingSubscriber extends wv.Subscriber { } } -Future main() async { - // routeOnce: a free function taking the callback interface, whose Delivery - // return crosses back as the C enum discriminant. - final probe = RecordingSubscriber('probe', skipTopic: 'quiet'); - expect(wv.routeOnce(probe, 'quiet') == wv.Delivery.skip, 'routeOnce skip'); - expect(wv.routeOnce(probe, 'news') == wv.Delivery.accept, 'routeOnce accept'); - expect(wv.routeOnce(probe, 'stop') == wv.Delivery.acceptAndStop, - 'routeOnce acceptAndStop'); - expect(probe.routed.join(',') == 'quiet,news,stop', - 'routeOnce delivered each topic (got ${probe.routed})'); - expect(probe.attachedCalls == 0, 'routeOnce never attaches'); +/// A subscriber whose void `onAttached` throws. +class FailingAttachSubscriber extends ev.Subscriber { + @override + ev.Delivery route(String topic) => ev.Delivery.skip; - // Enum value mapping matches the producer's discriminants. - expect(wv.Delivery.accept.value == 0 && - wv.Delivery.skip.value == 1 && - wv.Delivery.acceptAndStop.value == 2, - 'Delivery discriminants'); - expect(wv.Delivery.fromValue(2) == wv.Delivery.acceptAndStop, - 'Delivery.fromValue'); + @override + int onMessage(ev.Message message) => 0; - // A fresh bus: no subscribers, no messages, no last message. - final bus = wv.EventBus(); - expect(bus.subscriberCount() == 0, 'fresh bus has no subscribers'); - expect(bus.messages().isEmpty, 'fresh bus has no messages'); - expect(bus.lastMessage() == null, 'fresh bus lastMessage is null'); + @override + void onAttached(ev.EventBus bus) { + bus.dispose(); + throw StateError('refuses to attach'); + } +} - // subscribe: onAttached fires synchronously with the bus object, and the - // return value is the new subscriber count. +Future run() async { + // A free function taking the callback interface; the Delivery return + // crosses back as its discriminant. + final probe = RecordingSubscriber('probe', skipTopic: 'quiet', failTopic: 'boom'); + expect(ev.routeOnce(probe, 'quiet') == ev.Delivery.skip, 'routeOnce skip'); + expect(ev.routeOnce(probe, 'news') == ev.Delivery.accept, 'routeOnce accept'); + expect(ev.routeOnce(probe, 'stop') == ev.Delivery.acceptAndStop, + 'routeOnce acceptAndStop'); + final routeFailure = expectThrows( + () => ev.routeOnce(probe, 'boom'), 'routeOnce failure'); + expect(routeFailure.code == ev.NativeException.foreignCode, + 'Result-returning callback failure is -4 (got ${routeFailure.code})'); + expect(routeFailure.message.contains('probe rejected topic boom'), + 'foreign message (got ${routeFailure.message})'); + expect(probe.routed.join(',') == 'quiet,news,stop,boom', 'routeOnce log'); + expect(ev.Delivery.fromValue(2) == ev.Delivery.acceptAndStop, 'fromValue'); + + final bus = ev.EventBus(); + expect(bus.subscriberCount() == 0, 'fresh bus'); + expect(bus.messages().isEmpty, 'no messages'); + expect(bus.lastMessage() == null, 'no last message'); + + // subscribe returns the new count; the void onAttached arrives on the + // event loop with an EventBus reference the subscriber owns. final keeper = RecordingSubscriber('keeper', skipTopic: 'quiet'); final second = RecordingSubscriber('second'); - expect(bus.subscribe(keeper) == 1, 'first subscribe returns 1'); - expect(keeper.attachedCalls == 1, 'keeper attached once'); - expect(keeper.subscribersSeenOnAttach == 0, - 'keeper saw 0 subscribers inside onAttached ' - '(got ${keeper.subscribersSeenOnAttach})'); - expect(keeper.adoptedBus != null, 'keeper adopted the bus reference'); - expect(bus.subscribe(second) == 2, 'second subscribe returns 2'); - expect(second.attachedCalls == 1, 'second attached once'); - expect(second.subscribersSeenOnAttach == 1, - 'second saw 1 subscriber inside onAttached'); - expect(bus.subscriberCount() == 2, 'subscriberCount == 2'); - - // The adopted wrapper is a second reference to the same object. - expect(keeper.adoptedBus!.subscriberCount() == 2, - 'adopted bus sees the same subscribers'); - - // publish: both accept -> 2; the Message record decodes inside onMessage - // with every field (seq, topic, text, tags list). - expect(bus.publish('news', 'hello', ['x', 'y']) == 2, - 'publish news accepted by 2'); - expect(keeper.received.length == 1 && second.received.length == 1, - 'both received news'); - final m = keeper.received.first; - expect(m.seq == 1, 'first message seq == 1 (got ${m.seq})'); - expect(m.topic == 'news', 'message topic'); - expect(m.text == 'hello', 'message text'); - expect(m.tags.length == 2 && m.tags[0] == 'x' && m.tags[1] == 'y', - 'message tags (got ${m.tags})'); - expect(second.received.first.seq == 1 && second.received.first.text == 'hello', - 'second decoded the same message'); - - // Skip: the keeper routes `quiet` as skip, so only `second` accepts. - expect(bus.publish('quiet', 'psst', []) == 1, 'publish quiet -> 1'); - expect(keeper.received.length == 1, 'keeper skipped quiet'); - expect(second.received.length == 2 && second.received.last.seq == 2, - 'second received quiet with seq 2'); - expect(second.received.last.tags.isEmpty, 'empty tags list decodes empty'); - - // AcceptAndStop: the keeper (first in line) accepts and stops delivery, so - // `second` never sees it. - expect(bus.publish('stop', 'last', []) == 1, 'publish stop -> 1'); - expect(keeper.received.length == 2 && keeper.received.last.text == 'last', - 'keeper received stop'); - expect(second.received.length == 2, 'second not reached after stop'); - expect(keeper.routed.join(',') == 'news,quiet,stop', 'keeper routing log'); - expect(second.routed.join(',') == 'news,quiet', 'second routing log'); - - // Iterator: lazy Iterable over the native iterator, in publish order; a - // second iteration launches a fresh native iterator, and an abandoned - // partial iteration is harmless. - final texts = bus.messages().toList(); - expect(texts.join('|') == 'hello|psst|last', - 'messages in order (got $texts)'); - expect(bus.messages().length == 3, 'second iteration yields 3 again'); - expect(bus.messages().first == 'hello', 'partial iteration'); + expect(bus.subscribe(keeper) == 1, 'first subscribe'); + expect(bus.subscribe(second) == 2, 'second subscribe'); + await settle(() => keeper.attachedCalls == 1 && second.attachedCalls == 1, + 'onAttached delivered'); + expect(keeper.subscribersSeenOnAttach == 2 && keeper.adoptedBus != null, + 'keeper adopted a live bus (saw ${keeper.subscribersSeenOnAttach})'); + expect(keeper.adoptedBus!.subscriberCount() == 2, 'adopted bus is the same'); + + // publish: route and onMessage run synchronously during the call. + expect(bus.publish('news', 'hello', ['x', 'y']) == 2, 'publish news'); + final m = keeper.received.single; + expect(m.seq == 1 && m.topic == 'news' && m.text == 'hello', 'message'); + expect(m.tags.join(',') == 'x,y', 'message tags'); + expect(bus.publish('quiet', 'psst', []) == 1, 'publish quiet'); + expect(second.received.last.seq == 2 && second.received.last.tags.isEmpty, + 'second received quiet'); + expect(bus.publish('stop', 'last', []) == 1, 'publish stop'); + expect(keeper.received.last.text == 'last' && second.received.length == 2, + 'acceptAndStop'); + + // A lazy iterator; each iteration is a fresh native iterator, and an + // abandoned one is released by its finalizer. + expect(bus.messages().join('|') == 'hello|psst|last', 'messages'); + expect(bus.messages().first == 'hello', 'abandoned iteration'); expect(bus.messages().skip(1).first == 'psst', 'skip then first'); + final last = bus.lastMessage()!; + expect(last.seq == 3 && last.text == 'last', 'lastMessage'); - // Optional record return. - final last = bus.lastMessage(); - expect(last != null, 'lastMessage present'); - expect(last!.seq == 3 && last.topic == 'stop' && last.text == 'last', - 'lastMessage is the stop message'); - - // Foreign error from route: a Dart exception inside the callback aborts the - // publish and surfaces to the caller as the generic exception carrying the - // foreign code and the exception text; the VM does not crash, and the bus - // stays usable. Earlier subscribers already accepted the message. + // A Dart exception in route (a Result-returning producer method) aborts + // publish with the foreign code; earlier subscribers already accepted. final rejecting = RecordingSubscriber('rejecting', failTopic: 'boom'); - expect(bus.subscribe(rejecting) == 3, 'third subscribe returns 3'); - try { - bus.publish('boom', 'x', []); - throw StateError('expected WeaveFFIException for rejected topic'); - } on wv.WeaveFFIException catch (e) { - expect(e.code == wv.WeaveFFIException.foreignCode, - 'foreign code -4 (got ${e.code})'); - expect(e.code == -4, 'foreignCode constant is -4'); - expect(e.message.contains('rejecting rejected topic boom'), - 'foreign message carries the Dart exception text (got ${e.message})'); - } + expect(bus.subscribe(rejecting) == 3, 'third subscribe'); + final rejected = expectThrows( + () => bus.publish('boom', 'x', []), 'route throws'); + expect(rejected.code == -4 && rejected.message.contains('rejected topic boom'), + 'route failure (got $rejected)'); expect(keeper.received.length == 3 && second.received.length == 3, - 'subscribers ahead of the failing one still accepted boom'); - expect(rejecting.received.isEmpty, 'rejecting never received'); - expect(bus.messages().length == 4, 'the aborted publish was still logged'); - expect(bus.lastMessage()!.text == 'x', 'lastMessage is the aborted one'); - expect(bus.publish('ok', 'y', ['t']) == 3, - 'bus usable after a foreign error'); - expect(rejecting.received.length == 1 && rejecting.received.first.seq == 5, - 'rejecting received ok with seq 5'); + 'earlier subscribers accepted boom'); + expect(bus.publish('ok', 'y', ['t']) == 3, 'bus usable afterwards'); - // Foreign error from onMessage (a different vtable slot and exception type). + // A Dart exception in onMessage (a plain-return producer method). final fragile = RecordingSubscriber('fragile', failOnMessage: true); - expect(bus.subscribe(fragile) == 4, 'fourth subscribe returns 4'); - try { - bus.publish('any', 'payload', []); - throw StateError('expected WeaveFFIException for failing onMessage'); - } on wv.WeaveFFIException catch (e) { - expect(e.code == wv.WeaveFFIException.foreignCode, - 'onMessage foreign code -4 (got ${e.code})'); - expect(e.message.contains('fragile cannot take payload'), - 'onMessage foreign message (got ${e.message})'); - } - expect(fragile.routed.length == 1, 'fragile was routed once'); - expect(bus.subscriberCount() == 4, 'a failing subscriber stays subscribed'); - - // Foreign error from onAttached: subscribe itself fails and the subscriber - // is not retained. - final unattachable = FailingAttachSubscriber(); - try { - bus.subscribe(unattachable); - throw StateError('expected WeaveFFIException for failing onAttached'); - } on wv.WeaveFFIException catch (e) { - expect(e.code == -4, 'onAttached foreign code -4 (got ${e.code})'); - expect(e.message.contains('refuses to attach'), - 'onAttached foreign message (got ${e.message})'); - } - expect(bus.subscriberCount() == 4, 'failed subscribe did not retain'); + expect(bus.subscribe(fragile) == 4, 'fourth subscribe'); + final failed = expectThrows( + () => bus.publish('any', 'payload', []), 'onMessage throws'); + expect(failed.code == ev.NativeException.foreignCode && + failed.message.contains('fragile cannot take payload'), + 'onMessage failure (got $failed)'); + + // An exception in a void method is an uncaught error of the zone that + // passed the subscriber; the call itself succeeded. + final zoneErrors = []; + runZonedGuarded(() { + expect(bus.subscribe(FailingAttachSubscriber()) == 5, 'fifth subscribe'); + }, (e, _) => zoneErrors.add(e)); + await settle(() => zoneErrors.isNotEmpty, 'void method error reaches zone'); + expect(zoneErrors.single.toString().contains('refuses to attach'), + 'zone error (got $zoneErrors)'); - // clearSubscribers drops every retained subscriber; publishing then - // reaches nobody but is still logged. bus.clearSubscribers(); - expect(bus.subscriberCount() == 0, 'cleared subscribers'); - expect(bus.publish('news', 'nobody', []) == 0, - 'publish with no subscribers -> 0'); - expect(keeper.received.length == 5, - 'cleared keeper receives nothing (got ${keeper.received.length})'); + expect(bus.subscriberCount() == 0, 'cleared'); + expect(bus.publish('news', 'nobody', []) == 0, 'no subscribers'); - // Async: publishLater runs on a producer thread and settles a Future. The - // generated Dart callback trampolines are isolate-local, so subscribers - // can't be driven from that thread; with none attached the call resolves - // with 0 accepted and the message is logged. - final laterCount = await bus.publishLater('later', 'zzz'); - expect(laterCount == 0, 'publishLater resolves with 0 (got $laterCount)'); - expect(bus.lastMessage()!.text == 'zzz' && bus.lastMessage()!.seq == 8, - 'publishLater logged its message (got ${bus.lastMessage()!.text})'); - expect(bus.messages().length == 8, 'eight messages logged'); - final second1 = await bus.publishLater('later', 'again'); - expect(second1 == 0 && bus.messages().length == 9, 'second publishLater'); + // publishLater runs on a producer thread. Value-returning callback + // methods only run on the isolate's thread, so it is driven with no + // subscribers attached. + expect(await bus.publishLater('later', 'zzz') == 0, 'publishLater'); + expect(bus.lastMessage()!.text == 'zzz', 'publishLater logged'); - // Object lifetime: dispose the original wrapper; the adopted reference - // keeps the native bus alive and fully usable. Double dispose is safe and - // use after dispose is a StateError rather than a native fault. + // Lifetime: the adopted reference outlives the original wrapper. bus.dispose(); bus.dispose(); - try { - bus.subscriberCount(); - throw StateError('expected StateError after dispose'); - } on StateError catch (e) { - expect(e.message.contains('dispose'), 'use after dispose message'); - } + final gone = expectThrows(() => bus.subscriberCount(), 'disposed'); + expect(gone.message.contains('dispose'), 'use after dispose message'); final adopted = keeper.adoptedBus!; - expect(adopted.messages().length == 9, - 'adopted reference outlives the original wrapper'); - expect(adopted.publish('final', 'bye', []) == 0, - 'publish through the adopted reference'); - expect(adopted.lastMessage()!.text == 'bye', 'adopted lastMessage'); - adopted.dispose(); + expect(adopted.lastMessage()!.text == 'zzz', 'adopted bus alive'); + expect(adopted.publish('final', 'bye', []) == 0, 'adopted publish'); adopted.dispose(); - // A bus that is dropped while still holding subscribers releases them on - // its last reference (the consumer's `free` entries run through a listener - // NativeCallable once the event loop turns). - final short = wv.EventBus(); + // A bus dropped while holding subscribers releases them. + final short = ev.EventBus(); final tenant = RecordingSubscriber('tenant'); short.subscribe(tenant); - tenant.adoptedBus?.dispose(); + await settle(() => tenant.attachedCalls == 1, 'tenant attached'); short.dispose(); - await Future.delayed(Duration.zero); - - print('dart/events: OK'); } -/// A subscriber whose `onAttached` throws, so `subscribe` itself fails. -class FailingAttachSubscriber extends wv.Subscriber { - @override - wv.Delivery route(String topic) => wv.Delivery.accept; - - @override - int onMessage(wv.Message message) => 0; - - @override - void onAttached(wv.EventBus bus) { - bus.dispose(); - throw StateError('refuses to attach'); - } +Future main() async { + await run(); + await expectNoLeaks('events'); + print('dart/events: OK'); } diff --git a/conformance/dart/kvstore.dart b/conformance/dart/kvstore.dart index 5853b7cd..51224a17 100644 --- a/conformance/dart/kvstore.dart +++ b/conformance/dart/kvstore.dart @@ -14,15 +14,14 @@ // the same native object, `fork()`, `Store?` in and out of `larger`, the // `StoreInfo` record carrying a `Store` field plus an optional `Store`, // `openMany` returning a list of objects, `totalCount` taking a list of -// objects and a Dart-built record holding objects), the Future-returning -// `compact`, and dispose / double dispose / use after dispose on every -// wrapper. Throws (non-zero exit) on any mismatch. +// objects and a Dart-built record holding objects), the Future-returning, +// cancellable `compact`, dispose / double dispose / use after dispose on +// every wrapper, and the producer's leak counters at exit. Throws (non-zero +// exit) on any mismatch. -import 'package:__PKG__/__LIB__.dart' as wv; +import 'package:kvstore/kvstore.dart' as wv; -void expect(bool cond, String msg) { - if (!cond) throw StateError('assertion failed: $msg'); -} +import 'support.dart'; /// Records every eviction; returns false (detaching itself) once it has seen /// `keepFor` evictions, and throws when `fail` is set. @@ -45,7 +44,7 @@ class RecordingListener extends wv.EvictionListener { List get keys => evictions.map((e) => e.$1.key).toList(); } -Future main() async { +Future run() async { // Fallible constructor: an empty path reports the IoError domain code // through the typed exception hierarchy. try { @@ -55,7 +54,7 @@ Future main() async { expect(e.code == 1004, 'IoError code == 1004 (got ${e.code})'); expect(e.message == 'I/O failure', 'IoError message (got ${e.message})'); expect(e is wv.KvException, 'IoException extends KvException'); - expect(e is wv.WeaveFFIException, 'IoException extends the generic brand'); + expect(e is wv.NativeException, 'IoException extends the generic brand'); } final store = wv.Store.open('/tmp/conformance-kvstore-dart'); @@ -210,10 +209,10 @@ Future main() async { 'put doomed'); try { store.delete('doomed'); - throw StateError('expected WeaveFFIException from throwing listener'); - } on wv.WeaveFFIException catch (e) { + throw StateError('expected NativeException from throwing listener'); + } on wv.NativeException catch (e) { expect(e is! wv.KvException, 'foreign error is not a domain error'); - expect(e.code == wv.WeaveFFIException.foreignCode && e.code == -4, + expect(e.code == wv.NativeException.foreignCode && e.code == -4, 'foreign code -4 (got ${e.code})'); expect(e.message.contains('hostile refuses eviction of doomed'), 'foreign message carries the exception text (got ${e.message})'); @@ -224,7 +223,7 @@ Future main() async { try { store.delete('doomed2'); throw StateError('expected a second foreign error'); - } on wv.WeaveFFIException catch (e) { + } on wv.NativeException catch (e) { expect(e.code == -4, 'listener still attached after failing'); } store.clearEvictionListener(); @@ -384,5 +383,25 @@ Future main() async { throw StateError('expected StateError after dispose'); } on StateError catch (_) {} + await expectCompactCancellation(); +} + +/// `compact` is cancellable: a token cancelled before launch completes the +/// call with CancelledException, and an unused token changes nothing. +Future expectCompactCancellation() async { + final store = wv.Store.open('/tmp/conformance-kvstore-dart-cancel'); + final idle = wv.CancelToken(); + expect(await store.compact(cancelToken: idle) == 0, 'compact with a token'); + final token = wv.CancelToken()..cancel(); + final cancelled = await expectThrowsAsync( + () => store.compact(cancelToken: token), 'cancelled compact'); + expect(cancelled.code == -5 && cancelled is! wv.KvException, + 'cancellation is not a domain error'); + store.dispose(); +} + +Future main() async { + await run(); + await expectNoLeaks('kvstore'); print('dart/kvstore: OK'); } diff --git a/conformance/dart/run.sh b/conformance/dart/run.sh new file mode 100755 index 00000000..e89553dd --- /dev/null +++ b/conformance/dart/run.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Conformance lanes for the dart target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane compiles and runs one +# consumer against one sample and must exit 0. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# Run one consumer as its own small app that depends on the generated +# package through a `path:` dependency, exactly as a user would. The library +# comes from the generated loader's `{PREFIX}_LIBRARY` override. +dart_consumer() { + local sample="$1" script="$2" + local pkgdir="$GENROOT/$sample/dart" + local pkg app + pkg=$(sed -n 's/^name: //p' "$pkgdir/pubspec.yaml" | head -1) + app="$OUT/dart/$sample" + rm -rf "$app" + mkdir -p "$app/bin" + cat > "$app/pubspec.yaml" <=3.12.0 <4.0.0' +dependencies: + $pkg: + path: $pkgdir +EOF + cp "$ROOT/conformance/dart/$script" "$app/bin/main.dart" + cp "$ROOT/conformance/dart/support.dart" "$app/bin/support.dart" + ( cd "$app" && { dart pub get --offline >/dev/null 2>&1 || dart pub get >/dev/null; } ) \ + || { echo "dart pub get failed" >&2; return 1; } + ( cd "$app" && env "$(library_env "$sample")=$(sample_lib "$sample")" dart run bin/main.dart ) +} + +dart_calculator() { dart_consumer calculator calculator.dart; } +dart_contacts() { dart_consumer contacts contacts.dart; } +dart_events() { dart_consumer events events.dart; } +dart_kvstore() { dart_consumer kvstore kvstore.dart; } +dart_async_demo() { dart_consumer async-demo async_demo.dart; } +dart_codec() { dart_consumer codec codec.dart; } + +lane dart-calculator dart_calculator +lane dart-contacts dart_contacts +lane dart-events dart_events +lane dart-kvstore dart_kvstore +lane dart-async-demo dart_async_demo +lane dart-codec dart_codec + +finish_lanes diff --git a/conformance/dart/shapes.dart b/conformance/dart/shapes.dart deleted file mode 100644 index 86a896e7..00000000 --- a/conformance/dart/shapes.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Conformance consumer: shapes sample, Dart target. -// -// Drives the generated rich (algebraic) enum surface: the sealed `Shape` -// class, its per-variant subclasses (`ShapeCircle(...)`) with plain final -// fields, and the free functions that take and return `Shape` as value -// buffers. Also covers the expanded numerics (f32 fields, u8 field, u64 -// return). Mirrors the assertions in conformance/c/shapes.c and -// conformance/cpp/shapes.cpp. Throws (non-zero exit) on any mismatch; prints -// `dart/shapes: OK` on success. -// -// Library selection follows the harness convention: the generated package name -// and library basename are substituted into the import sentinels, and the -// producer cdylib is chosen at runtime via the WEAVEFFI_LIBRARY env var read by -// the generated _openLibrary(). - -import 'package:__PKG__/__LIB__.dart' as wv; - -void expect(bool cond, String msg) { - if (!cond) throw StateError('assertion failed: $msg'); -} - -bool near(num a, num b) => (a - b).abs() < 1e-6; - -void main() { - // Empty (unit variant): no fields. - final wv.Shape empty = wv.ShapeEmpty(); - expect(empty is wv.ShapeEmpty, 'empty variant'); - - // Circle (f64 payload). - final circle = wv.ShapeCircle(2.5); - expect(near(circle.radius, 2.5), 'circle radius == 2.5'); - - // Rectangle (two f32 payloads). - final rect = wv.ShapeRectangle(3.0, 4.0); - expect(near(rect.width, 3.0), 'rectangle width == 3.0'); - expect(near(rect.height, 4.0), 'rectangle height == 4.0'); - - // Labeled (string + u8 payload). - final labeled = wv.ShapeLabeled('hex', 6); - expect(labeled.label == 'hex', 'labeled label == hex'); - expect(labeled.count == 6, 'labeled count == 6'); - - // describe: dispatch on the active variant of the buffered parameter. - expect(wv.describe(circle) == 'circle(r=2.5)', 'describe circle'); - - // scale: rich enum in and out; the result decodes to the matching subclass. - final big = wv.scale(circle, 4.0); - switch (big) { - case wv.ShapeCircle(:final radius): - expect(near(radius, 10.0), 'scaled radius == 10.0'); - default: - throw StateError('scaled shape is not a circle (got $big)'); - } - - // numerics: list in, u64 out. - final total = wv.sumBytes([250, 250, 250, 250]); - expect(total == 1000, 'sum_bytes == 1000 (got $total)'); - - print('dart/shapes: OK'); -} diff --git a/conformance/dart/support.dart b/conformance/dart/support.dart new file mode 100644 index 00000000..f18cabe6 --- /dev/null +++ b/conformance/dart/support.dart @@ -0,0 +1,71 @@ +// Shared helpers for the Dart conformance consumers. run.sh copies this file +// next to each consumer's main.dart. + +import 'dart:ffi'; +import 'dart:io'; + +/// Throws when [cond] is false. +void expect(bool cond, String msg) { + if (!cond) throw StateError('assertion failed: $msg'); +} + +/// Expects [body] to throw a [T] and returns it. +T expectThrows(void Function() body, String msg) { + try { + body(); + } on T catch (e) { + return e; + } + throw StateError('assertion failed: $msg (nothing thrown)'); +} + +/// Expects the future from [body] to fail with a [T] and returns it. +Future expectThrowsAsync( + Future Function() body, String msg) async { + try { + await body(); + } on T catch (e) { + return e; + } + throw StateError('assertion failed: $msg (nothing thrown)'); +} + +/// The value or error [future] completes with. Attaching it right away +/// keeps an early error from counting as unhandled. +Future outcome(Future future) => + future.then((v) => v, onError: (Object e) => e); + +/// Turns the event loop until [done] holds, failing after [timeout]. +Future settle(bool Function() done, String msg, + {Duration timeout = const Duration(seconds: 10)}) async { + final watch = Stopwatch()..start(); + while (!done()) { + if (watch.elapsed > timeout) throw StateError('timed out: $msg'); + await Future.delayed(const Duration(milliseconds: 2)); + } +} + +/// Checks the producer's leak counters through raw `dart:ffi`: live objects, +/// callbacks, iterators, cancel tokens, and returned allocations must all +/// drop to zero. Garbage is churned between checks so the GC runs and the +/// bindings' finalizers release whatever was left to them. +Future expectNoLeaks(String prefix) async { + final env = '${prefix.toUpperCase()}_LIBRARY'; + final lib = DynamicLibrary.open(Platform.environment[env]!); + final live = lib.lookupFunction( + '${prefix}_debug_live'); + List counts() => [for (var k = 0; k <= 4; k++) live(k)]; + final watch = Stopwatch()..start(); + var junk = []; + while (counts().any((c) => c != 0)) { + if (watch.elapsed > const Duration(seconds: 20)) { + throw StateError('leaked native resources ' + '[objects, callbacks, iterators, tokens, allocations]: ${counts()}'); + } + for (var i = 0; i < 20000; i++) { + junk.add(List.filled(64, i)); + } + junk = []; + await Future.delayed(const Duration(milliseconds: 5)); + } +} diff --git a/conformance/dotnet/AsyncDemo.cs b/conformance/dotnet/AsyncDemo.cs index b6c9fc86..ae2c2cc3 100644 --- a/conformance/dotnet/AsyncDemo.cs +++ b/conformance/dotnet/AsyncDemo.cs @@ -1,19 +1,19 @@ // Conformance consumer: async-demo sample, .NET target. // -// Drives the Task-backed async surface end to end: RunTask settled from the -// producer's worker thread and decoded from a value buffer into the plain -// TaskResult class, the typed TaskException (Code == TaskException.InvalidName) -// thrown for an empty name, the buffered list-of-records round trip through -// RunBatch, the direct-scalar RunNTasks, the sync CancelTask, and -// ActiveCallbacks settling to zero once every task body has completed. The -// producer cdylib is resolved by absolute path via a DllImportResolver -// reading WEAVEFFI_LIBRARY. +// Drives the Task-based async surface of the generated AsyncDemo project: +// RunTask completing from a producer thread with a decoded TaskResult, the +// typed TaskException for an empty name, the buffered list round trip +// through RunBatch, the direct scalar RunNTasks, and real cancellation of +// Wait through a CancellationToken (cancelled mid-flight, cancelled before +// launch, and left to time out). Ends by asserting the producer's leak +// counters are zero. using System; +using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; -using WeaveFFI; +using AsyncDemo; internal static class Program { @@ -26,25 +26,15 @@ static void Expect(bool cond, string msg) } } - static async Task Main() + static async Task Run() { - var lib = Environment.GetEnvironmentVariable("WEAVEFFI_LIBRARY"); - NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, asm, search) => - { - if (name == "weaveffi" && !string.IsNullOrEmpty(lib)) - return NativeLibrary.Load(lib); - return IntPtr.Zero; - }); - - // Async record return: the Task resolves with a TaskResult decoded - // from the completion callback's value buffer. + // Async record return. var result = await Tasks.RunTask("alpha"); Expect(result.Id > 0, "RunTask assigns an id"); Expect(result.Value == "completed: alpha", $"RunTask value (got {result.Value})"); Expect(result.Success, "RunTask success flag"); - // Typed async error: the empty name faults the Task with the domain - // exception carrying its stable code. + // Typed async error. try { await Tasks.RunTask(""); @@ -53,6 +43,7 @@ static async Task Main() catch (TaskException e) { Expect(e.Code == TaskException.InvalidName, $"InvalidName code == 1 (got {e.Code})"); + Expect(e is NativeException, "typed exception extends NativeException"); } // Buffered list-of-records both ways. @@ -61,18 +52,78 @@ static async Task Main() batch.Select(r => r.Value).SequenceEqual( new[] { "completed: a", "completed: b", "completed: c" }), $"RunBatch values (got [{string.Join(", ", batch.Select(r => r.Value))}])"); - Expect(batch.All(r => r.Success), "RunBatch success flags"); + Expect((await Tasks.RunBatch(new string[0])).Length == 0, "RunBatch of nothing"); - // Direct scalar through the async callback. - Expect(await Tasks.RunNTasks(7) == 7, "RunNTasks echoes n"); + // Direct scalar, many in flight at once. + var many = await Task.WhenAll(Enumerable.Range(0, 64).Select(i => Tasks.RunNTasks(i))); + Expect(many.SequenceEqual(Enumerable.Range(0, 64)), "RunNTasks echoes n under load"); - // Sync functions beside the async ones. - Expect(!Tasks.CancelTask(1), "CancelTask reports not cancelled"); + // A cancellable call left alone completes after its timeout. + Expect(await Tasks.Wait(20) == 20, "Wait(20) completes with 20"); + using (var unused = new CancellationTokenSource()) + { + Expect(await Tasks.Wait(10, unused.Token) == 10, "Wait with an untouched token"); + } - // Every spawned task body has completed by the time its callback fires. + // Cancelling mid-flight cancels the native call: the task completes + // as canceled long before the 60 s timeout. + var clock = Stopwatch.StartNew(); + using (var cts = new CancellationTokenSource()) + { + var pending = Tasks.Wait(60_000, cts.Token); + await Task.Delay(50); + Expect(!pending.IsCompleted, "Wait(60 s) is still pending"); + cts.Cancel(); + try + { + await pending; + Expect(false, "expected OperationCanceledException"); + } + catch (OperationCanceledException) + { + } + Expect(pending.IsCanceled, "the task is canceled"); + } + Expect(clock.Elapsed < TimeSpan.FromSeconds(10), $"cancel didn't wait for the timeout ({clock.Elapsed})"); + + // A token cancelled before the call never launches it. + using (var cts = new CancellationTokenSource()) + { + cts.Cancel(); + var task = Tasks.Wait(60_000, cts.Token); + Expect(task.IsCanceled, "pre-cancelled Wait is canceled immediately"); + } + + // Cancelling after a timer through CancelAfter, several at once. + using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(30))) + { + var waits = Enumerable.Range(0, 8).Select(_ => Tasks.Wait(60_000, cts.Token)).ToArray(); + foreach (var w in waits) + { + try + { + await w; + Expect(false, "expected cancellation"); + } + catch (OperationCanceledException) + { + } + } + } + + // Every spawned task body has finished. + for (var i = 0; i < 100 && Tasks.ActiveCallbacks() != 0; i++) + { + await Task.Delay(20); + } Expect(Tasks.ActiveCallbacks() == 0, "ActiveCallbacks settles to zero"); + } - Console.WriteLine("dotnet async-demo conformance: OK"); + static int Main() + { + Run().GetAwaiter().GetResult(); + LeakCheck.AssertNoLeaks("async_demo"); + Console.WriteLine("dotnet/async-demo: OK"); return 0; } } diff --git a/conformance/dotnet/Codec.cs b/conformance/dotnet/Codec.cs index 3b1ab23c..05002bbf 100644 --- a/conformance/dotnet/Codec.cs +++ b/conformance/dotnet/Codec.cs @@ -1,7 +1,7 @@ // Conformance consumer: codec sample, .NET target. // -// Round-trips every value-buffer wire shape through the generated P/Invoke -// wrapper (Codec.cs, namespace Codec) against the producer's oracle: the +// Round-trips every value-buffer wire shape through the generated Codec +// project against the producer's oracle: the // canonical Scalars and Composite fixtures are decoded and checked field by // field (producer encodes, consumer decodes), handed back through Verify* // (consumer encodes, producer decodes) and Roundtrip* (both), then rebuilt @@ -12,19 +12,18 @@ // objects in a required field, an optional, and a list: each encoded token is // a fresh clone, PrimaryOf returns a wrapper to the same native object as the // holder's Primary, and every wrapper is disposed (double Dispose safe, -// ObjectDisposedException after). The producer cdylib is resolved by absolute -// path via a DllImportResolver reading WEAVEFFI_LIBRARY. +// ObjectDisposedException after). Strings with interior NULs cross the +// function boundary intact, since strings are (pointer, length) pairs. +// Wrapper equality is native object identity. Ends by asserting the +// producer's leak counters are zero. // -// The harness compiles the generated source into this assembly, so the -// wrapper's `internal` Handle property is reachable and used to prove two -// wrappers point at the same native object. The IDL sets the .NET namespace -// to `Codec` and the module is also `codec`, so the free-function class is -// `Codec.Codec`; `using static` imports those statics. +// The namespace is `Codec` and the module is also `codec`, so the +// free-function class is `Codec.Codec`; `using static` imports its statics. using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using Codec; using static Codec.Codec; @@ -155,14 +154,15 @@ static void ExpectCompositesEqual(Composite a, Composite b, string what) static int Main() { - var lib = Environment.GetEnvironmentVariable("WEAVEFFI_LIBRARY"); - NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, asm, search) => - { - if (name == "weaveffi" && !string.IsNullOrEmpty(lib)) - return NativeLibrary.Load(lib); - return IntPtr.Zero; - }); + Run(); + LeakCheck.AssertNoLeaks("codec"); + Console.WriteLine("dotnet/codec: OK"); + return 0; + } + [MethodImpl(MethodImplOptions.NoInlining)] + static void Run() + { // --- Scalars: producer encodes, consumer decodes; then back. --- var scalars = SampleScalars(); CheckScalars(scalars, "sample_scalars"); @@ -183,7 +183,7 @@ static int Main() { Expect(e.Code == CodecException.Mismatch, $"Mismatch code == 1 (got {e.Code})"); Expect(e.Code == 1, "Mismatch constant is 1"); - Expect(e is WeaveFFIException, "typed exception extends the brand exception"); + Expect(e is NativeException, "typed exception extends the brand exception"); } // Scalars at the extremes, including float edge values whose bit @@ -330,7 +330,12 @@ static int Main() Expect(map.Count == 3 && map[""] == long.MinValue && map["k"] == 0 && map["ключ"] == long.MaxValue, "map contents"); Expect(RoundtripString("") == "", "empty string"); Expect(RoundtripString("héllo wörld ✓ 🎉") == "héllo wörld ✓ 🎉", "unicode string"); + var nul = RoundtripString("a\0b\0"); + Expect(nul == "a\0b\0" && nul.Length == 4, "interior NULs survive the function boundary"); + Expect(RoundtripString("\0") == "\0", "a lone NUL survives"); Expect(RoundtripBytes(new byte[0]).Length == 0, "empty bytes"); + ReadOnlySpan span = stackalloc byte[] { 7, 0, 9 }; + Expect(RoundtripBytes(span).SequenceEqual(new byte[] { 7, 0, 9 }), "bytes from a span"); Expect(RoundtripBytes(new byte[] { 0, 127, 128, 255 }).SequenceEqual(new byte[] { 0, 127, 128, 255 }), "bytes"); Expect(RoundtripI64(long.MinValue) == long.MinValue, "i64::MIN direct"); Expect(RoundtripI64(long.MaxValue) == long.MaxValue, "i64::MAX direct"); @@ -350,7 +355,7 @@ static int Main() Expect(holder.Primary.Value() == 10, "holder.primary"); Expect(holder.Spare != null && holder.Spare.Value() == 11, "holder.spare"); Expect(holder.Many.Select(t => t.Value()).SequenceEqual(new long[] { 12, 13, 14 }), "holder.many"); - Expect(holder.Many.Select(t => t.Handle).Distinct().Count() == 3, "holder.many are distinct objects"); + Expect(holder.Many.Select(t => t).Distinct().Count() == 3, "holder.many are distinct objects"); Expect(SumHolder(holder) == 10 + 11 + 12 + 13 + 14, "sum_holder"); // Encoding cloned each token, so the holder is still fully usable. Expect(SumHolder(holder) == 60, "sum_holder again after re-encoding"); @@ -358,7 +363,7 @@ static int Main() var primary = PrimaryOf(holder); Expect(!ReferenceEquals(primary, holder.Primary), "primary_of is a new wrapper"); - Expect(primary.Handle == holder.Primary.Handle, "primary_of wraps the same native object"); + Expect(primary.Equals(holder.Primary), "primary_of wraps the same native object"); Expect(primary.Value() == 10, "primary_of value"); Expect(SamePrimary(holder, holder), "same_primary with itself"); var other = MakeHolder(10, true); @@ -378,7 +383,7 @@ static int Main() var own = new Holder(t1, t2, new[] { t3, t1 }); Expect(SumHolder(own) == 1 + 2 + long.MinValue + 1, "sum_holder over consumer tokens"); var ownPrimary = PrimaryOf(own); - Expect(ownPrimary.Handle == t1.Handle && ownPrimary.Value() == 1, "primary_of consumer token"); + Expect(ownPrimary.Equals(t1) && ownPrimary.Value() == 1, "primary_of consumer token"); // Reference counting: dropping one wrapper leaves the others valid; // double Dispose is a no-op; a disposed wrapper throws. @@ -414,8 +419,5 @@ static int Main() { Expect(scoped.Value() == 99, "scoped token"); } - - Console.WriteLine("dotnet/codec: OK"); - return 0; } } diff --git a/conformance/dotnet/Contacts.cs b/conformance/dotnet/Contacts.cs index e4629eae..f91b50d2 100644 --- a/conformance/dotnet/Contacts.cs +++ b/conformance/dotnet/Contacts.cs @@ -1,20 +1,19 @@ // Conformance consumer: contacts sample, .NET target. // -// Drives the generated P/Invoke surface (WeaveFFI.cs): the ContactBook -// interface class (real `new` constructor, instance methods, Dispose lowering -// to the destroy symbol), enum marshalling, the plain value-class Contact -// decoded from value buffers (properties, no IDisposable), UTF-8 string -// params, optional strings (null email) crossing as buffered `string?` -// parameters, list-of-record returns decoded from one buffer, the bool -// return, and the typed ContactsException error path (InvalidName=1, -// NotFound=2). The producer cdylib is resolved by absolute path via a -// DllImportResolver reading WEAVEFFI_LIBRARY, mirroring the override the -// Python/Ruby/Dart backends use. +// Drives the generated Contacts project: the ContactBook interface (real +// constructor, instance methods, Dispose), the ContactType enum, the plain +// Contact record decoded from value buffers, optional strings, list returns, +// and the typed ContactsException (InvalidName=1, NotFound=2). The nested +// contacts.groups module (ContactsGroups) takes and returns the parent's +// enum and interface and reports the parent's error domain; the sibling +// directory root (Directory) shares the Contact record and has its own +// DirectoryException. Ends by asserting the producer's leak counters are +// zero. using System; using System.Linq; -using System.Runtime.InteropServices; -using WeaveFFI; +using System.Runtime.CompilerServices; +using Contacts; internal static class Program { @@ -27,46 +26,70 @@ static void Expect(bool cond, string msg) } } - static int Main() + [MethodImpl(MethodImplOptions.NoInlining)] + static void Run() { - var lib = Environment.GetEnvironmentVariable("WEAVEFFI_LIBRARY"); - NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, asm, search) => - { - if (name == "weaveffi" && !string.IsNullOrEmpty(lib)) - return NativeLibrary.Load(lib); - return IntPtr.Zero; - }); - using (var book = new ContactBook()) { - // Contact is a plain value class now: no Dispose, just properties. var alice = book.Add("Alice", "Smith", "alice@example.com", ContactType.Work); - long aliceId = alice.Id; - Expect(aliceId > 0, "alice id positive"); - Expect(alice.FirstName == "Alice", "first name"); - Expect(alice.LastName == "Smith", "last name"); + Expect(alice.Id > 0, "alice id positive"); + Expect(alice.FirstName == "Alice" && alice.LastName == "Smith", "names"); Expect(alice.Email == "alice@example.com", "email"); Expect(alice.ContactType == ContactType.Work, "contact type"); - // Optional string: a missing email round-trips as null. var bob = book.Add("Bob", "Jones", null, ContactType.Personal); Expect(bob.Email == null, "bob email null"); - Expect(bob.ContactType == ContactType.Personal, "bob contact type"); + var carol = book.Add("Carol", "Adams", null, ContactType.Work); - var fetched = book.Get(aliceId); - Expect(fetched.FirstName == "Alice", "get returns alice"); + Expect(book.Get(alice.Id).FirstName == "Alice", "get returns alice"); + Expect(book.Count() == 3, "count == 3"); + var names = book.List().Select(p => p.FirstName).OrderBy(s => s).ToArray(); + Expect(names.SequenceEqual(new[] { "Alice", "Bob", "Carol" }), "list names"); - Expect(book.Count() == 2, "count == 2"); + // The nested module uses the parent's enum and interface. + Expect(ContactsGroups.CountOfType(book, ContactType.Work) == 2, "two work contacts"); + Expect(ContactsGroups.CountOfType(book, ContactType.Other) == 0, "no other contacts"); + Expect(ContactsGroups.DominantType(book) == ContactType.Work, "work dominates"); + using (var work = ContactsGroups.SplitByType(book, ContactType.Work)) + { + Expect(work.Count() == 2, "split copied the work contacts"); + Expect(!work.Equals(book), "split is a new book"); + Expect(work.List().All(c => c.ContactType == ContactType.Work), "split holds only work"); + } + Expect(ContactsGroups.FirstOfType(book, ContactType.Personal).FirstName == "Bob", + "first personal contact"); + try + { + ContactsGroups.FirstOfType(book, ContactType.Other); + Expect(false, "expected ContactsException from the nested module"); + } + catch (ContactsException e) + { + Expect(e.Code == ContactsException.NotFound, "nested module reports the parent's NotFound"); + } - var everyone = book.List(); - Expect(everyone.Length == 2, "list length == 2"); - var names = everyone.Select(p => p.FirstName).OrderBy(s => s).ToArray(); - Expect(names[0] == "Alice" && names[1] == "Bob", "list names"); + // The sibling root shares the Contact record. + var card = Directory.Card(alice); + Expect(card.DisplayName == "Smith, Alice", $"card display name (got {card.DisplayName})"); + Expect(card.Initials == "AS" && card.HasEmail, "card initials and email"); + Expect(!Directory.Card(bob).HasEmail, "bob has no email"); + var sorted = Directory.Sorted(book.List()); + Expect(sorted.Select(c => c.LastName).SequenceEqual(new[] { "Adams", "Jones", "Smith" }), + "sorted by last name"); + try + { + Directory.Sorted(new Contact[0]); + Expect(false, "expected DirectoryException"); + } + catch (DirectoryException e) + { + Expect(e.Code == DirectoryException.Empty, "Empty code == 1"); + } - Expect(book.Remove(aliceId) == true, "remove returns true"); - Expect(book.Count() == 1, "count == 1 after remove"); + Expect(book.Remove(alice.Id), "remove returns true"); + Expect(!book.Remove(alice.Id), "second remove returns false"); + Expect(book.Count() == 2, "count == 2 after remove"); - // Typed errors: the domain exception carries the declared code. try { book.Add("", "Smith", null, ContactType.Personal); @@ -75,8 +98,8 @@ static int Main() catch (ContactsException e) { Expect(e.Code == ContactsException.InvalidName, "InvalidName code == 1"); + Expect(e.Message == "name must not be empty", $"InvalidName message (got '{e.Message}')"); } - try { book.Get(9999); @@ -85,10 +108,35 @@ static int Main() catch (ContactsException e) { Expect(e.Code == ContactsException.NotFound, "NotFound code == 2"); - Expect(e is WeaveFFIException, "typed exception extends the brand exception"); + Expect(e is NativeException, "typed exception extends NativeException"); } + Expect(carol.Id > 0, "carol kept"); + } + + // A disposed book refuses further calls. + var gone = new ContactBook(); + gone.Dispose(); + gone.Dispose(); + try + { + gone.Count(); + Expect(false, "expected ObjectDisposedException"); + } + catch (ObjectDisposedException) + { } + // Books left to the garbage collector are released by finalizers. + for (var i = 0; i < 10; i++) + { + new ContactBook().Add("Temp", "Book", null, ContactType.Other); + } + } + + static int Main() + { + Run(); + LeakCheck.AssertNoLeaks("contacts"); Console.WriteLine("dotnet/contacts: OK"); return 0; } diff --git a/conformance/dotnet/Events.cs b/conformance/dotnet/Events.cs index 594c0a81..cb81a128 100644 --- a/conformance/dotnet/Events.cs +++ b/conformance/dotnet/Events.cs @@ -1,29 +1,26 @@ // Conformance consumer: events sample, .NET target. // -// Drives the ABI 2 surface of the generated P/Invoke wrapper (WeaveFFI.cs): -// the `ISubscriber` callback interface implemented in C# and adapted to the -// producer's vtable by the generated trampolines (Route steering Publish's -// accepted count through the `Delivery` enum, OnMessage decoding the buffered -// `Message` record, OnAttached adopting a reference-counted `EventBus` handed -// through the callback), the `EventBus` object class (constructor, Task-based -// PublishLater, once-enumerable Messages iterator, optional LastMessage, -// IDisposable release with a safe double Dispose and ObjectDisposedException -// afterward), the free function RouteOnce, and a subscriber that throws, -// which the trampolines report as the generated WeaveFFIException carrying -// ForeignErrorCode (-4) without crashing the runtime. The producer cdylib is -// resolved by absolute path via a DllImportResolver reading WEAVEFFI_LIBRARY. -// -// The harness compiles the generated source into this assembly, so the -// wrapper's `internal` Handle property is reachable and used to prove two -// wrappers point at the same native object. +// Drives the generated Events project: the `ISubscriber` callback interface +// implemented in C# and adapted to the producer's vtable by the generated +// trampolines (Route, whose producer side returns `Result<_, ForeignError>`, +// steering Publish through the `Delivery` enum; OnMessage decoding the +// buffered `Message` record; OnAttached adopting an `EventBus` handed through +// the callback), the `EventBus` class (constructor, Task-based PublishLater, +// single-use Messages iterator, optional LastMessage, Dispose), the free +// function RouteOnce, subscribers that throw (reported to the caller as +// NativeException with ForeignErrorCode -4 for both the Result-returning +// and the plain callback methods), callbacks fired from producer threads, +// and wrapper equality as native identity. Ends by asserting the producer's +// leak counters are zero. using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; -using WeaveFFI; +using Events; +using EventsApi = Events.Events; internal sealed class TestSubscriber : ISubscriber { @@ -106,16 +103,16 @@ static WeakReference SubscribeTransient(EventBus bus) return new WeakReference(sub); } - static async Task Main() + static int Main() { - var lib = Environment.GetEnvironmentVariable("WEAVEFFI_LIBRARY"); - NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, asm, search) => - { - if (name == "weaveffi" && !string.IsNullOrEmpty(lib)) - return NativeLibrary.Load(lib); - return IntPtr.Zero; - }); + Run().GetAwaiter().GetResult(); + LeakCheck.AssertNoLeaks("events"); + Console.WriteLine("dotnet/events: OK"); + return 0; + } + static async Task Run() + { var bus = new EventBus(); Expect(bus.SubscriberCount() == 0, "fresh bus has no subscribers"); Expect(bus.LastMessage() == null, "fresh bus has no last message"); @@ -130,7 +127,7 @@ static async Task Main() Expect(a.Attached == 1 && b.Attached == 1, "OnAttached fired once per subscriber"); Expect(a.AttachedCount == 0, $"a saw 0 subscribers at attach (got {a.AttachedCount})"); Expect(b.AttachedCount == 1, $"b saw 1 subscriber at attach (got {b.AttachedCount})"); - Expect(a.KeptBus != null && a.KeptBus.Handle == bus.Handle, + Expect(a.KeptBus != null && a.KeptBus.Equals(bus), "bus handed to OnAttached is the same native object"); Expect(bus.SubscriberCount() == 2, "subscriber count == 2"); @@ -194,9 +191,9 @@ static async Task Main() // Free function taking the callback interface without a bus. var probe = new TestSubscriber("quiet", ""); - Expect(Events.RouteOnce(probe, "quiet") == Delivery.Skip, "RouteOnce skip"); - Expect(Events.RouteOnce(probe, "stop") == Delivery.AcceptAndStop, "RouteOnce accept-and-stop"); - Expect(Events.RouteOnce(probe, "x") == Delivery.Accept, "RouteOnce accept"); + Expect(EventsApi.RouteOnce(probe, "quiet") == Delivery.Skip, "RouteOnce skip"); + Expect(EventsApi.RouteOnce(probe, "stop") == Delivery.AcceptAndStop, "RouteOnce accept-and-stop"); + Expect(EventsApi.RouteOnce(probe, "x") == Delivery.Accept, "RouteOnce accept"); Expect(probe.Routed.SequenceEqual(new[] { "quiet", "stop", "x" }), "RouteOnce routed topics"); Expect(probe.Attached == 0, "RouteOnce never attaches"); @@ -207,11 +204,11 @@ static async Task Main() try { bus.Publish("boom", "x", new string[0]); - Expect(false, "expected WeaveFFIException from throwing Route"); + Expect(false, "expected NativeException from throwing Route"); } - catch (WeaveFFIException e) + catch (NativeException e) { - Expect(e.Code == WeaveFFIException.ForeignErrorCode, + Expect(e.Code == NativeException.ForeignErrorCode, $"foreign error code == -4 (got {e.Code})"); Expect(e.Code == -4, "ForeignErrorCode constant is -4"); Expect(e.Message.Contains("subscriber rejected topic boom"), @@ -220,11 +217,11 @@ static async Task Main() try { bus.Publish("ok", "explode", new string[0]); - Expect(false, "expected WeaveFFIException from throwing OnMessage"); + Expect(false, "expected NativeException from throwing OnMessage"); } - catch (WeaveFFIException e) + catch (NativeException e) { - Expect(e.Code == WeaveFFIException.ForeignErrorCode, + Expect(e.Code == NativeException.ForeignErrorCode, $"OnMessage foreign error code == -4 (got {e.Code})"); Expect(e.Message.Contains("subscriber exploded"), $"OnMessage foreign error message (got '{e.Message}')"); @@ -232,12 +229,12 @@ static async Task Main() Expect(bus.Publish("ok", "y", new string[0]) == 3, "bus still delivers after a foreign error"); try { - Events.RouteOnce(c, "boom"); - Expect(false, "expected WeaveFFIException from RouteOnce"); + EventsApi.RouteOnce(c, "boom"); + Expect(false, "expected NativeException from RouteOnce"); } - catch (WeaveFFIException e) + catch (NativeException e) { - Expect(e.Code == WeaveFFIException.ForeignErrorCode, "RouteOnce foreign error code"); + Expect(e.Code == NativeException.ForeignErrorCode, "RouteOnce foreign error code"); } // ClearSubscribers drops the producer's references; the consumer's @@ -278,7 +275,36 @@ static async Task Main() Expect(scoped.Publish("t", "u", new string[0]) == 0, "scoped bus publishes"); } - Console.WriteLine("dotnet/events: OK"); - return 0; + // Callbacks arrive on whichever thread calls in, several at once, and + // from the producer's own threads for async publishes. + var counter = new CountingSubscriber(); + using (var busy = new EventBus()) + { + busy.Subscribe(counter); + await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => Task.Run(() => + { + for (var j = 0; j < 50; j++) + { + busy.Publish("t", "m", new string[0]); + } + }))); + Expect(counter.Count == 400, $"400 concurrent deliveries (got {counter.Count})"); + var laters = await Task.WhenAll(Enumerable.Range(0, 16).Select(_ => busy.PublishLater("t", "later"))); + Expect(laters.All(n => n == 1), "every async publish delivered once"); + Expect(counter.Count == 416, $"416 deliveries in all (got {counter.Count})"); + } } } + +internal sealed class CountingSubscriber : ISubscriber +{ + private long _count; + + public long Count => Interlocked.Read(ref _count); + + public Delivery Route(string topic) => Delivery.Accept; + + public long OnMessage(Message message) => Interlocked.Increment(ref _count); + + public void OnAttached(EventBus bus) => bus.Dispose(); +} diff --git a/conformance/dotnet/Kvstore.cs b/conformance/dotnet/Kvstore.cs index 5cb6fdaf..00693cb6 100644 --- a/conformance/dotnet/Kvstore.cs +++ b/conformance/dotnet/Kvstore.cs @@ -1,33 +1,28 @@ // Conformance consumer: kvstore sample, .NET target. // -// Full-surface drive of the generated P/Invoke wrapper (Kvstore.cs, namespace -// Kvstore) at ABI 2: the reference-counted Store object class (static Open +// Full-surface drive of the generated Kvstore project: the Store class (static Open // factory throwing the typed KvException IoError=1004 on an empty path, // instance methods put/get/delete/list_keys/count/clear, the deprecated -// legacy_put, the Task-returning Compact, the DefaultCapacity static), the +// legacy_put, the cancellable Task-returning Compact, the DefaultCapacity static), the // optional buffered `Entry?` return decoded into a plain value class, direct // value-class construction of Entry, the IEnumerable-backed ListKeys iterator, // the cross-module KvStats.GetStats, the `IEvictionListener` callback // interface implemented in C# (fired synchronously on delete and on an // expired read, detached when it returns false, replaced and cleared, and a -// throwing listener surfacing to the caller as WeaveFFIException with +// throwing listener surfacing to the caller as NativeException with // ForeignErrorCode -4), the object graph (Share returning a second wrapper to // the same native object, Fork, `Store?` both ways through Larger, the // StoreInfo record carrying Store objects, OpenMany returning a list of // objects, TotalCount taking a list of objects plus an optional record with // objects), the typed KvException codes (KeyNotFound=1001, Expired=1002), and // IDisposable release (safe double Dispose, ObjectDisposedException after). -// The producer cdylib is resolved by absolute path via a DllImportResolver -// reading WEAVEFFI_LIBRARY. -// -// The harness compiles the generated source into this assembly, so the -// wrapper's `internal` Handle property is reachable and used to prove two -// wrappers point at the same native object. +// Wrapper equality is native object identity. Ends by asserting the +// producer's leak counters are zero. using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Kvstore; @@ -71,16 +66,16 @@ static void Expect(bool cond, string msg) } } - static async Task Main() + static int Main() { - var lib = Environment.GetEnvironmentVariable("WEAVEFFI_LIBRARY"); - NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, asm, search) => - { - if (name == "weaveffi" && !string.IsNullOrEmpty(lib)) - return NativeLibrary.Load(lib); - return IntPtr.Zero; - }); + Run().GetAwaiter().GetResult(); + LeakCheck.AssertNoLeaks("kvstore"); + Console.WriteLine("dotnet/kvstore: OK"); + return 0; + } + static async Task Run() + { // Typed constructor error: an empty path reports KvError::IoError // through the domain exception. try @@ -139,7 +134,7 @@ static async Task Main() { Expect(e.Code == KvException.KeyNotFound, $"KeyNotFound code == 1001 (got {e.Code})"); Expect(e.Message == "key not found", $"KeyNotFound message (got '{e.Message}')"); - Expect(e is WeaveFFIException, "typed exception extends the brand exception"); + Expect(e is NativeException, "typed exception extends the brand exception"); } // Entry is a plain value class: non-empty list and map fields live @@ -204,15 +199,15 @@ static async Task Main() try { store.Delete("doomed"); - Expect(false, "expected WeaveFFIException from throwing listener"); + Expect(false, "expected NativeException from throwing listener"); } catch (KvException) { Expect(false, "foreign error must not wear the domain exception type"); } - catch (WeaveFFIException e) + catch (NativeException e) { - Expect(e.Code == WeaveFFIException.ForeignErrorCode, $"foreign error code == -4 (got {e.Code})"); + Expect(e.Code == NativeException.ForeignErrorCode, $"foreign error code == -4 (got {e.Code})"); Expect(e.Message.Contains("listener refused doomed"), $"foreign error carries the exception message (got '{e.Message}')"); } @@ -240,7 +235,7 @@ static async Task Main() // usable. var twin = store.Share(); Expect(!ReferenceEquals(twin, store), "Share returns a distinct wrapper"); - Expect(twin.Handle == store.Handle, "Share wraps the same native pointer"); + Expect(twin.Equals(store), "Share wraps the same native pointer"); Expect(twin.Count() == 2, "twin sees the same entries"); Expect(twin.Put("via-twin", payload, EntryKind.Persistent, null), "put via twin"); Expect(store.Count() == 3 && store.Get("via-twin") != null, "store sees the twin's put"); @@ -258,7 +253,7 @@ static async Task Main() // Fork: an independent copy. var forked = store.Fork(); - Expect(forked.Handle != store.Handle, "Fork is a different native object"); + Expect(!forked.Equals(store), "Fork is a different native object"); Expect(forked.Count() == 3, "fork copied live entries"); Expect(forked.Put("only-in-fork", payload, EntryKind.Volatile, null), "put into fork"); Expect(forked.Count() == 4 && store.Count() == 3, "fork and original diverge"); @@ -267,14 +262,14 @@ static async Task Main() var empty = Store.Open("/tmp/conformance-kvstore-dotnet-empty"); Expect(empty.Larger(null) == null, "empty.Larger(null) is null"); var bigger = empty.Larger(store); - Expect(bigger != null && bigger.Handle == store.Handle, "empty.Larger(store) is store"); + Expect(bigger != null && bigger.Equals(store), "empty.Larger(store) is store"); Expect(bigger.Count() == 3, "returned larger store is usable"); bigger.Dispose(); var own = store.Larger(null); - Expect(own != null && own.Handle == store.Handle, "store.Larger(null) is store itself"); + Expect(own != null && own.Equals(store), "store.Larger(null) is store itself"); own.Dispose(); var forkWins = store.Larger(forked); - Expect(forkWins != null && forkWins.Handle == forked.Handle, "store.Larger(fork) is the fork"); + Expect(forkWins != null && forkWins.Equals(forked), "store.Larger(fork) is the fork"); forkWins.Dispose(); Expect(store.Count() == 3 && forked.Count() == 4, "both stores alive after Larger releases"); @@ -282,11 +277,11 @@ static async Task Main() var info = store.Describe("primary", null); Expect(info.Label == "primary", "describe label"); Expect(info.Count == 3, $"describe count == 3 (got {info.Count})"); - Expect(info.Store.Handle == store.Handle, "describe().Store is the same native object"); + Expect(info.Store.Equals(store), "describe().Store is the same native object"); Expect(info.Mirror == null, "describe().Mirror absent"); Expect(info.Store.Count() == 3, "describe().Store is usable"); var mirrored = store.Describe("mirrored", forked); - Expect(mirrored.Mirror != null && mirrored.Mirror.Handle == forked.Handle, + Expect(mirrored.Mirror != null && mirrored.Mirror.Equals(forked), "describe().Mirror is the fork"); Expect(mirrored.Mirror.Count() == 4, "describe().Mirror is usable"); @@ -294,7 +289,7 @@ static async Task Main() // optional record as parameters (each encoded token is a fresh clone). var many = Store.OpenMany(new[] { "/tmp/many-a", "/tmp/many-b" }); Expect(many.Length == 2, "open_many returns two stores"); - Expect(many[0].Handle != many[1].Handle, "open_many stores are distinct"); + Expect(!many[0].Equals(many[1]), "open_many stores are distinct"); Expect(many[0].Count() == 0 && many[1].Count() == 0, "open_many stores start empty"); Expect(many[0].Put("m", payload, EntryKind.Volatile, null), "put into many[0]"); Expect(Store.TotalCount(many, null) == 1, "total_count over the list"); @@ -323,6 +318,19 @@ static async Task Main() Expect(reclaimed == 3, $"compact reclaimed 3 bytes (got {reclaimed})"); Expect(store.Count() == 3, "live entries survive compact"); Expect((await forked.Compact()) == 0, "nothing to compact in the fork"); + using (var cts = new CancellationTokenSource()) + { + Expect((await forked.Compact(cts.Token)) == 0, "compact with a live token"); + cts.Cancel(); + try + { + await forked.Compact(cts.Token); + Expect(false, "expected a canceled compact"); + } + catch (OperationCanceledException) + { + } + } store.Clear(); Expect(store.Count() == 0, "store empty after clear"); @@ -363,8 +371,5 @@ static async Task Main() { Expect(scoped.Count() == 0, "scoped store opens empty"); } - - Console.WriteLine("dotnet/kvstore: OK"); - return 0; } } diff --git a/conformance/dotnet/LeakCheck.cs b/conformance/dotnet/LeakCheck.cs new file mode 100644 index 00000000..2cccd6bf --- /dev/null +++ b/conformance/dotnet/LeakCheck.cs @@ -0,0 +1,52 @@ +// Shared by every .NET conformance consumer: after the consumer has dropped +// its wrappers, force the garbage collector and finalizers, then assert that +// the producer's leak counters (live objects, foreign callbacks, iterators, +// cancel tokens, and returned allocations) are all back to zero. The +// counter export is reached through raw FFI on the same library the +// generated bindings load ({PREFIX}_LIBRARY). + +using System; +using System.Runtime.InteropServices; +using System.Threading; + +internal static unsafe class LeakCheck +{ + private static readonly string[] Kinds = + { "objects", "callbacks", "iterators", "cancel tokens", "allocations" }; + + public static void AssertNoLeaks(string prefix) + { + var path = Environment.GetEnvironmentVariable(prefix.ToUpperInvariant() + "_LIBRARY"); + var lib = NativeLibrary.Load(path!); + var live = (delegate* unmanaged[Cdecl])NativeLibrary.GetExport(lib, prefix + "_debug_live"); + var counts = new ulong[Kinds.Length]; + // Releasing one object can release others (a bus drops its + // subscribers), and async completions finish on producer threads, so + // settle for a moment before failing. + for (var attempt = 0; attempt < 100; attempt++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + var clean = true; + for (var kind = 0; kind < Kinds.Length; kind++) + { + counts[kind] = live(kind); + clean &= counts[kind] == 0; + } + if (clean) + { + return; + } + Thread.Sleep(20); + } + for (var kind = 0; kind < Kinds.Length; kind++) + { + if (counts[kind] != 0) + { + Console.Error.WriteLine($"leak: {counts[kind]} live {Kinds[kind]}"); + } + } + Environment.Exit(1); + } +} diff --git a/conformance/dotnet/Shapes.cs b/conformance/dotnet/Shapes.cs deleted file mode 100644 index dfb5ef96..00000000 --- a/conformance/dotnet/Shapes.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Conformance consumer: shapes sample, .NET target. -// -// Drives the generated P/Invoke surface for rich (algebraic) enums (Shapes.cs, -// namespace Shapes): the abstract `Shape` class with one sealed nested class -// per variant (`Shape.Empty`, `Shape.Circle`, ...), variant construction via -// the nested constructors, discrimination via C# pattern matching, and the -// per-variant properties (`Radius`, `Width`, `Label`, ...). Shapes cross the -// ABI as serialized value buffers behind the free functions that take and -// return `Shape` by value. Also covers the expanded numerics (f32 fields, u8 -// field, list in / u64 out) and the plain C-style `Channel` enum. The -// producer cdylib is resolved by absolute path via a DllImportResolver reading -// WEAVEFFI_LIBRARY, mirroring the other backends. -// -// The IDL sets the .NET namespace to `Shapes` and the module is also `shapes`, -// so the generated free-function class is `Shapes.Shapes`; `using static` -// imports those statics (a bare `Shapes.X` would bind `Shapes` to the -// namespace). Returns non-zero on any failed assertion. - -using System; -using System.Runtime.InteropServices; -using Shapes; -using static Shapes.Shapes; - -internal static class Program -{ - static void Expect(bool cond, string msg) - { - if (!cond) - { - Console.Error.WriteLine($"assertion failed: {msg}"); - Environment.Exit(1); - } - } - - static int Main() - { - var lib = Environment.GetEnvironmentVariable("WEAVEFFI_LIBRARY"); - NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, asm, search) => - { - if (name == "weaveffi" && !string.IsNullOrEmpty(lib)) - return NativeLibrary.Load(lib); - return IntPtr.Zero; - }); - - // Unit variant. - Shape empty = new Shape.Empty(); - Expect(empty is Shape.Empty, "empty variant"); - - // f64 payload. - var circle = new Shape.Circle(2.5); - Expect(Math.Abs(circle.Radius - 2.5) < 1e-9, "circle radius"); - - // Free functions: Shape in, string/Shape out. - Expect(Describe(circle) == "circle(r=2.5)", "describe(circle)"); - - Shape scaled = Scale(circle, 4.0); - Expect(scaled is Shape.Circle, "scaled variant"); - var big = (Shape.Circle)scaled; - Expect(Math.Abs(big.Radius - 10.0) < 1e-9, "scaled radius"); - - // Two f32 payloads. - var rect = new Shape.Rectangle(3.0f, 4.0f); - Expect(Math.Abs(rect.Width - 3.0f) < 1e-6f, "rect width"); - Expect(Math.Abs(rect.Height - 4.0f) < 1e-6f, "rect height"); - - // string + u8 payload, round-tripped through the producer to prove - // the variant encodes and decodes intact. - Shape labeledBack = Scale(new Shape.Labeled("hex", 6), 2.0); - Expect(labeledBack is Shape.Labeled, "labeled variant survives round trip"); - var labeled = (Shape.Labeled)labeledBack; - Expect(labeled.Label == "hex", "labeled label"); - Expect(labeled.Count == 6, "labeled count"); - - // Pattern matching discriminates variants like any C# sum type. - string kind = scaled switch - { - Shape.Empty _ => "empty", - Shape.Circle _ => "circle", - Shape.Rectangle _ => "rectangle", - Shape.Labeled _ => "labeled", - _ => "unknown", - }; - Expect(kind == "circle", "switch pattern match"); - - // Numerics: list in, u64 out. - ulong total = SumBytes(new byte[] { 250, 250, 250, 250 }); - Expect(total == 1000UL, $"sum_bytes == 1000 (got {total})"); - - // Plain C-style enum lowers by value. - Channel ch = Channel.Green; - Expect((int)ch == 1, "plain enum value"); - - Console.WriteLine("dotnet/shapes: OK"); - return 0; - } -} diff --git a/conformance/dotnet/run.sh b/conformance/dotnet/run.sh new file mode 100755 index 00000000..a9fc5052 --- /dev/null +++ b/conformance/dotnet/run.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Conformance lanes for the dotnet target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane compiles and runs one +# consumer against one sample and must exit 0. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# .NET: a console app with a ProjectReference to the generated project, used +# as generated. The generated loader finds the producer through +# {PREFIX}_LIBRARY. The app targets the installed SDK's framework so it runs +# on the installed runtime. +dotnet_consumer() { + local sample="$1" src="$2" + local proj="$OUT/dotnet-$sample" + local tfm csproj + tfm="net$(dotnet --version | cut -d. -f1).0" + csproj=$(ls "$GENROOT/$sample/dotnet"/*.csproj | head -1) + rm -rf "$proj" + mkdir -p "$proj" + cp "$ROOT/conformance/dotnet/$src" "$proj/Program.cs" + cp "$ROOT/conformance/dotnet/LeakCheck.cs" "$proj/LeakCheck.cs" + cat > "$proj/conformance.csproj" < + + Exe + $tfm + disable + true + disable + + + + + +EOF + ( cd "$proj" \ + && env "$(library_env "$sample")=$(sample_lib "$sample")" \ + dotnet run -c Release --nologo -v quiet 2>&1 ) +} + +dotnet_contacts() { dotnet_consumer contacts Contacts.cs; } + +dotnet_events() { dotnet_consumer events Events.cs; } + +dotnet_kvstore() { dotnet_consumer kvstore Kvstore.cs; } + +dotnet_async_demo() { dotnet_consumer async-demo AsyncDemo.cs; } + +dotnet_codec() { dotnet_consumer codec Codec.cs; } + +lane dotnet-contacts dotnet_contacts +lane dotnet-events dotnet_events +lane dotnet-kvstore dotnet_kvstore +lane dotnet-async-demo dotnet_async_demo +lane dotnet-codec dotnet_codec + +finish_lanes diff --git a/conformance/go/async_demo.go b/conformance/go/async_demo.go index bf73e3ed..b7f4468e 100644 --- a/conformance/go/async_demo.go +++ b/conformance/go/async_demo.go @@ -1,63 +1,105 @@ // Conformance consumer: async-demo sample, Go target. // -// Drives the blocking async bridge end to end: RunTask parks the calling -// goroutine on a channel until the producer's completion callback fires and -// decodes the TaskResult struct from a value buffer, an empty name reports -// the typed *TaskError (code 1, matched via errors.As), RunBatch round-trips -// a list of records through value buffers both ways, RunNTasks returns a -// direct scalar through the callback, CancelTask is a plain sync call, and -// ActiveCallbacks is back to zero once every task body has completed. Exits 0 -// on success; aborts (non-zero) on any mismatch. +// Drives the async bridge end to end: every async wrapper takes a +// context.Context and blocks until the producer's completion callback fires. +// RunTask decodes the TaskResult record from a value buffer, an empty name +// reports the typed *TaskError (matched via errors.As), RunBatch round-trips +// a list of records both ways, and RunNTasks returns a direct scalar. +// +// Cancellation: Wait is cancellable. Left alone it completes after its +// timeout; cancelling its context (or letting a deadline expire) cancels the +// native token, and the call returns promptly with the context's error. An +// already-cancelled context never launches. Exits 0 on success; aborts +// (non-zero) on any mismatch. package main import ( + "context" "errors" "fmt" - "os" + "time" wv "__MODPATH__" ) -func expect(cond bool, msg string) { - if !cond { - fmt.Fprintln(os.Stderr, "assertion failed: "+msg) - os.Exit(1) - } -} - func main() { - // Async record return: blocks until the worker-thread callback delivers - // the encoded TaskResult. - result, err := wv.RunTask("alpha") - expect(err == nil, "run_task succeeds") + ctx := context.Background() + + // Async record return. + result, err := wv.RunTask(ctx, "alpha") + expect(err == nil, fmt.Sprintf("run_task succeeds (got %v)", err)) expect(result.Id > 0, "run_task assigns an id") expect(result.Value == "completed: alpha", fmt.Sprintf("run_task value (got %q)", result.Value)) expect(result.Success, "run_task success flag") // Typed async error: the empty name reports TaskError InvalidName. - _, err = wv.RunTask("") - expect(err != nil, "empty name reports an error") + _, err = wv.RunTask(ctx, "") var taskErr *wv.TaskError expect(errors.As(err, &taskErr), fmt.Sprintf("typed *TaskError (got %T)", err)) expect(taskErr.Code == wv.TaskErrorInvalidName, fmt.Sprintf("InvalidName carries code 1 (got %d)", taskErr.Code)) // Buffered list-of-records both ways. - batch := wv.RunBatch([]string{"a", "b", "c"}) - expect(len(batch) == 3, fmt.Sprintf("run_batch returns 3 results (got %d)", len(batch))) + batch, err := wv.RunBatch(ctx, []string{"a", "b", "c"}) + expect(err == nil && len(batch) == 3, fmt.Sprintf("run_batch returns 3 results (got %d, %v)", len(batch), err)) for i, want := range []string{"completed: a", "completed: b", "completed: c"} { expect(batch[i].Value == want, fmt.Sprintf("run_batch[%d] value (got %q)", i, batch[i].Value)) expect(batch[i].Success, "run_batch success flag") } // Direct scalar through the async callback. - expect(wv.RunNTasks(7) == 7, "run_n_tasks echoes n") + n, err := wv.RunNTasks(ctx, 7) + expect(err == nil && n == 7, "run_n_tasks echoes n") + + // A cancellable call left alone completes normally. + waited, err := wv.Wait(ctx, 20) + expect(err == nil && waited == 20, fmt.Sprintf("wait(20) completes (got %d, %v)", waited, err)) - // Sync functions beside the async ones. - expect(!wv.CancelTask(1), "cancel_task reports not cancelled") + // Cancelling the context cancels the native call promptly. + cctx, cancel := context.WithCancel(ctx) + time.AfterFunc(50*time.Millisecond, cancel) + start := time.Now() + _, err = wv.Wait(cctx, 30_000) + elapsed := time.Since(start) + expect(errors.Is(err, context.Canceled), fmt.Sprintf("cancelled wait returns context.Canceled (got %v)", err)) + expect(elapsed < 5*time.Second, fmt.Sprintf("cancellation is prompt (took %v)", elapsed)) - // Every spawned task body has completed by the time its callback fires. + // A deadline cancels the same way and reports DeadlineExceeded. + dctx, stop := context.WithTimeout(ctx, 50*time.Millisecond) + _, err = wv.Wait(dctx, 30_000) + stop() + expect(errors.Is(err, context.DeadlineExceeded), fmt.Sprintf("deadline yields DeadlineExceeded (got %v)", err)) + + // An already-cancelled context never launches the call. + done, cancelNow := context.WithCancel(ctx) + cancelNow() + _, err = wv.Wait(done, 30_000) + expect(errors.Is(err, context.Canceled), "pre-cancelled wait returns context.Canceled") + _, err = wv.RunTask(done, "never") + expect(errors.Is(err, context.Canceled), "pre-cancelled run_task returns context.Canceled") + + // Many concurrent cancellations all settle. + errs := make(chan error, 16) + for range 16 { + go func() { + c, cancel := context.WithTimeout(ctx, 10*time.Millisecond) + defer cancel() + _, err := wv.Wait(c, 30_000) + errs <- err + }() + } + for range 16 { + expect(errors.Is(<-errs, context.DeadlineExceeded), "concurrent cancellation") + } + + // Every task body has finished: cancelled ones were dropped by the + // runtime before their completion fired. + deadline := time.Now().Add(5 * time.Second) + for wv.ActiveCallbacks() != 0 && time.Now().Before(deadline) { + time.Sleep(5 * time.Millisecond) + } expect(wv.ActiveCallbacks() == 0, "active_callbacks settles to zero") - fmt.Println("go async-demo conformance: OK") + expectNoLeaks(wv.DebugLive) + fmt.Println("go/async-demo: OK") } diff --git a/conformance/go/calculator.go b/conformance/go/calculator.go new file mode 100644 index 00000000..4c5ea887 --- /dev/null +++ b/conformance/go/calculator.go @@ -0,0 +1,35 @@ +// Conformance consumer: calculator sample, Go target. +// +// The minimal surface: plain scalar functions, the throwing Div with its +// typed *CalcError, and a string round trip that keeps interior NUL bytes and +// non-ASCII text intact through the (pointer, length) string ABI. Exits 0 on +// success; aborts (non-zero) on any mismatch. + +package main + +import ( + "errors" + "fmt" + + wv "__MODPATH__" +) + +func main() { + expect(wv.Add(2, 3) == 5, "add") + expect(wv.Mul(-4, 6) == -24, "mul") + + q, err := wv.Div(17, 5) + expect(err == nil && q == 3, fmt.Sprintf("div (got %d, %v)", q, err)) + _, err = wv.Div(1, 0) + var cerr *wv.CalcError + expect(errors.As(err, &cerr), fmt.Sprintf("div by zero yields *CalcError (got %T %v)", err, err)) + expect(cerr.Code == wv.CalcErrorDivisionByZero, "division-by-zero code") + expect(cerr.Message == "division by zero", fmt.Sprintf("message (got %q)", cerr.Message)) + + for _, s := range []string{"", "hello", "a\x00b\x00", "héllo ✓ 😀"} { + expect(wv.Echo(s) == s, fmt.Sprintf("echo %q", s)) + } + + expectNoLeaks(wv.DebugLive) + fmt.Println("go/calculator: OK") +} diff --git a/conformance/go/codec.go b/conformance/go/codec.go index 37eea906..6ce25545 100644 --- a/conformance/go/codec.go +++ b/conformance/go/codec.go @@ -22,19 +22,11 @@ import ( "errors" "fmt" "math" - "os" "strings" wv "__MODPATH__" ) -func expect(cond bool, msg string) { - if !cond { - fmt.Fprintln(os.Stderr, "assertion failed:", msg) - os.Exit(1) - } -} - // Bit-exact float comparison so NaN payloads and the sign of zero count. func sameF64(a, b float64) bool { return math.Float64bits(a) == math.Float64bits(b) } func sameF32(a, b float32) bool { return math.Float32bits(a) == math.Float32bits(b) } @@ -188,12 +180,6 @@ func expectSameComposite(a, b wv.Composite, what string) { expect(same, fmt.Sprintf("%s: field %s differs\n a: %+v\n b: %+v", what, field, a, b)) } -func catchPanic(f func()) (v any) { - defer func() { v = recover() }() - f() - return nil -} - func ptrI64(v int64) *int64 { return &v } func ptrStr(v string) *string { return &v } func ptrBool(v bool) *bool { return &v } @@ -394,6 +380,8 @@ func main() { expect(len(wv.RoundtripMap(nil)) == 0, "roundtrip_map(empty)") expect(wv.RoundtripString("héllo wörld ✓ 😀") == "héllo wörld ✓ 😀", "roundtrip_string unicode") expect(wv.RoundtripString("") == "", "roundtrip_string empty") + expect(wv.RoundtripString("a\x00b\x00") == "a\x00b\x00", "roundtrip_string keeps interior NUL bytes") + expect(wv.RoundtripString("\x00") == "\x00", "roundtrip_string of a lone NUL") expect(sameBytes(wv.RoundtripBytes([]byte{0, 255, 128}), []byte{0, 255, 128}), "roundtrip_bytes") expect(len(wv.RoundtripBytes([]byte{})) == 0, "roundtrip_bytes empty") expect(wv.RoundtripI64(math.MinInt64) == math.MinInt64, "roundtrip_i64 MinInt64") @@ -482,5 +470,6 @@ func main() { mp.Close() mp.Close() + expectNoLeaks(wv.DebugLive) fmt.Println("go/codec: OK") } diff --git a/conformance/go/common.go b/conformance/go/common.go new file mode 100644 index 00000000..5b1dd0f6 --- /dev/null +++ b/conformance/go/common.go @@ -0,0 +1,47 @@ +// Shared helpers for the Go conformance consumers. run.sh copies this file +// next to each consumer's main.go. + +package main + +import ( + "fmt" + "os" + "runtime" + "time" +) + +func expect(cond bool, msg string) { + if !cond { + fmt.Fprintln(os.Stderr, "assertion failed:", msg) + os.Exit(1) + } +} + +// catchPanic runs f and returns the recovered panic value (nil if none). +func catchPanic(f func()) (v any) { + defer func() { v = recover() }() + f() + return nil +} + +// expectNoLeaks drives the garbage collector until every finalizer has run +// and then asserts that the producer holds no live objects, callbacks, +// iterators, cancel tokens, or returned allocations. +func expectNoLeaks(debugLive func(int32) uint64) { + kinds := []string{"objects", "callbacks", "iterators", "cancel tokens", "allocations"} + deadline := time.Now().Add(10 * time.Second) + for { + runtime.GC() + leaked := "" + for k, name := range kinds { + if n := debugLive(int32(k)); n != 0 { + leaked += fmt.Sprintf(" %s=%d", name, n) + } + } + if leaked == "" { + return + } + expect(time.Now().Before(deadline), "live native resources at exit:"+leaked) + time.Sleep(10 * time.Millisecond) + } +} diff --git a/conformance/go/contacts.go b/conformance/go/contacts.go index 91713f2d..3ccbabde 100644 --- a/conformance/go/contacts.go +++ b/conformance/go/contacts.go @@ -6,27 +6,27 @@ // decoded from value buffers, optional strings (pointer email) present and // absent, list-of-record returns, the throws split (plain returns for // non-throwing methods), and the typed ContactsError domain via errors.As. +// +// Cross-module types: the nested contacts.groups module takes and returns +// the parent's ContactType enum and ContactBook interface and reports the +// parent's ContactsError; the sibling directory root shares the Contact +// record and has its own DirectoryError. Its `card` function keeps the +// module prefix (DirectoryCard) because the Card record owns that name in the +// flat Go package. package main import ( "errors" "fmt" - "os" "sort" wv "__MODPATH__" ) -func expect(cond bool, msg string) { - if !cond { - fmt.Fprintln(os.Stderr, "assertion failed:", msg) - os.Exit(1) - } -} - func main() { book := wv.NewContactBook() + expect(wv.DebugLive(0) >= 1, "the producer counts live objects (leak checks are on)") email := "alice@example.com" alice, err := book.Add("Alice", "Smith", &email, wv.ContactTypeWork) @@ -43,7 +43,7 @@ func main() { expect(errors.As(err, &cerr), "empty name yields a *ContactsError") expect(cerr.Code == wv.ContactsErrorInvalidName, fmt.Sprintf("invalid name code == 1 (got %d)", cerr.Code)) - expect(cerr.Message == "name must not be empty", "invalid name default message") + expect(cerr.Message == "name must not be empty", "invalid name message") // Optional string: a missing email round-trips as a nil pointer. bob, err := book.Add("Bob", "Jones", nil, wv.ContactTypePersonal) @@ -71,6 +71,45 @@ func main() { expect(cerr.Code == wv.ContactsErrorNotFound, fmt.Sprintf("not found code == 2 (got %d)", cerr.Code)) - book.Close() + // ── contacts.groups: the parent's enum, interface, and error domain ── + _, err = book.Add("Ann", "Lee", nil, wv.ContactTypeWork) + expect(err == nil, "add ann") + _, err = book.Add("Bo", "Kim", nil, wv.ContactTypeWork) + expect(err == nil, "add bo") + expect(wv.CountOfType(book, wv.ContactTypeWork) == 2, "count_of_type(Work) == 2") + expect(wv.CountOfType(book, wv.ContactTypePersonal) == 1, "count_of_type(Personal) == 1") + expect(wv.DominantType(book) == wv.ContactTypeWork, "dominant_type is Work") + + work := wv.SplitByType(book, wv.ContactTypeWork) + expect(work != nil && work.Count() == 2, "split_by_type returns a new book with two contacts") + expect(book.Count() == 3, "the source book is unchanged") + first, err := wv.FirstOfType(work, wv.ContactTypeWork) + expect(err == nil && first.FirstName == "Ann", fmt.Sprintf("first_of_type(Work) (got %+v, %v)", first, err)) + _, err = wv.FirstOfType(work, wv.ContactTypeOther) + cerr = nil + expect(errors.As(err, &cerr) && cerr.Code == wv.ContactsErrorNotFound, + fmt.Sprintf("the nested module reports the parent's NotFound (got %v)", err)) + expect(work.Close() == nil, "close the split book") + + // ── directory: a sibling root sharing the Contact record ── + zoe := wv.Contact{Id: 7, FirstName: "Zoe", LastName: "Quinn", ContactType: wv.ContactTypePersonal} + card := wv.DirectoryCard(zoe) + expect(card == wv.Card{DisplayName: "Quinn, Zoe", Initials: "ZQ", HasEmail: false}, + fmt.Sprintf("directory card (got %+v)", card)) + sorted, err := wv.Sorted(book.List()) + expect(err == nil && len(sorted) == 3, fmt.Sprintf("sorted (got %v)", err)) + expect(sorted[0].LastName == "Jones" && sorted[1].LastName == "Kim" && sorted[2].LastName == "Lee", + fmt.Sprintf("sorted by last name (got %+v)", sorted)) + _, err = wv.Sorted(nil) + var derr *wv.DirectoryError + expect(errors.As(err, &derr) && derr.Code == wv.DirectoryErrorEmpty, + fmt.Sprintf("empty list yields *DirectoryError (got %T %v)", err, err)) + expect(derr.Message == "no contacts to list", "directory error message") + + // Close is idempotent, and a closed wrapper traps on use. + expect(book.Close() == nil && book.Close() == nil, "double close") + expect(catchPanic(func() { book.Count() }) != nil, "use after Close panics") + + expectNoLeaks(wv.DebugLive) fmt.Println("go/contacts: OK") } diff --git a/conformance/go/events.go b/conformance/go/events.go index 8b098142..6662342a 100644 --- a/conformance/go/events.go +++ b/conformance/go/events.go @@ -1,6 +1,6 @@ // Conformance consumer: events sample, Go target. // -// Exercises the ABI 2 surface end to end: a Go type implementing the +// Exercises the callback surface end to end: a Go type implementing the // generated Subscriber callback interface (crossing as a cgo.Handle plus a // static vtable of //export trampolines), the reference-counted EventBus // object (including a second wrapper handed back through OnAttached, its @@ -9,18 +9,21 @@ // borrowed value buffer inside a callback, the async PublishLater bridge, // the lazy iter.Seq behind Messages, the optional LastMessage, the free // function RouteOnce, and the foreign-error path: a subscriber that panics -// surfaces to the caller as a recoverable *WeaveFFIError with code -4 and -// leaves the bus usable. Finally ClearSubscribers must let the producer's +// surfaces to the caller as a recoverable *Error with code -4 and +// leaves the bus usable. Route returns Result on the +// producer side, so its failure comes back to the bus as a value that the +// bus propagates; OnMessage returns a plain value, so its failure aborts the +// call directly. Both look the same from Go. Finally ClearSubscribers must let the producer's // `free` entry run, which is observed through a Go finalizer on the -// implementation once its handle is deleted. Exits 0 on success; aborts -// (non-zero) on any mismatch. +// implementation once its handle is deleted, and nothing leaks. Exits 0 on +// success; aborts (non-zero) on any mismatch. package main import ( + "context" "errors" "fmt" - "os" "runtime" "strings" "sync/atomic" @@ -29,13 +32,6 @@ import ( wv "__MODPATH__" ) -func expect(cond bool, msg string) { - if !cond { - fmt.Fprintln(os.Stderr, "assertion failed:", msg) - os.Exit(1) - } -} - // subState is the observable side of a subscriber. It's held separately // from the implementing value so the implementation itself can become // unreachable (and be finalized) once the producer frees its handle. @@ -89,6 +85,21 @@ func (r *recorder) OnAttached(bus *wv.EventBus) { r.st.bus = bus } +// closingSub closes a bus wrapper from inside a callback, while the call +// that triggered the callback is still using that wrapper. +type closingSub struct { + target *wv.EventBus +} + +func (c *closingSub) Route(string) wv.Delivery { + expect(c.target.Close() == nil, "close inside a callback") + return wv.DeliveryAccept +} + +func (c *closingSub) OnMessage(wv.Message) int64 { return 1 } + +func (c *closingSub) OnAttached(bus *wv.EventBus) { bus.Close() } + // attach subscribes a fresh recorder and returns only its state. The // recorder value stays reachable solely through the cgo.Handle the // producer holds, so its finalizer fires once the producer calls `free`. @@ -99,21 +110,14 @@ func attach(bus *wv.EventBus, name, skip string, fails bool, freed *atomic.Int32 return st, bus.Subscribe(r) } -// catchPanic runs f and returns the recovered panic value (nil if none). -func catchPanic(f func()) (v any) { - defer func() { v = recover() }() - f() - return nil -} - // expectForeignError asserts that v is the generated brand error carrying // FOREIGN_ERROR_CODE (-4) and the Go panic text. func expectForeignError(v any, needle string, what string) { expect(v != nil, what+": expected a panic") err, isErr := v.(error) expect(isErr, fmt.Sprintf("%s: panic value is an error (got %T)", what, v)) - var ferr *wv.WeaveFFIError - expect(errors.As(err, &ferr), fmt.Sprintf("%s: *WeaveFFIError (got %T: %v)", what, v, v)) + var ferr *wv.Error + expect(errors.As(err, &ferr), fmt.Sprintf("%s: *Error (got %T: %v)", what, v, v)) expect(ferr.Code == -4, fmt.Sprintf("%s: code -4 (got %d)", what, ferr.Code)) expect(strings.Contains(ferr.Message, needle), fmt.Sprintf("%s: message carries the Go panic text (got %q)", what, ferr.Message)) @@ -180,8 +184,8 @@ func main() { // Async: the producer publishes from its own thread, and the callback // trampolines run there; the wrapper parks the goroutine until done. - delivered = bus.PublishLater("async", "later") - expect(delivered == 2, fmt.Sprintf("publish_later accepted by both (got %d)", delivered)) + delivered, err := bus.PublishLater(context.Background(), "async", "later") + expect(err == nil && delivered == 2, fmt.Sprintf("publish_later accepted by both (got %d)", delivered)) expect(len(a.received) == 3 && a.received[2].Seq == 4 && a.received[2].Topic == "async", "a received the async message") expect(len(b.received) == 3 && b.received[2].Text == "later", "b received the async message") @@ -239,6 +243,13 @@ func main() { delivered = bus.Publish("after", "nobody", nil) expect(delivered == 0, "no subscribers accept after clear") + // Close racing an in-flight call: the subscriber closes the wrapper + // Publish is running on; the reference is released once Publish returns. + racy := wv.NewEventBus() + racy.Subscribe(&closingSub{target: racy}) + expect(racy.Publish("x", "y", nil) == 1, "publish completes although its wrapper closed mid-call") + expect(catchPanic(func() { racy.SubscriberCount() }) != nil, "the closed wrapper traps afterwards") + // Every wrapper releases its own reference; Close is idempotent. a.bus.Close() a.bus.Close() @@ -247,5 +258,7 @@ func main() { f.bus.Close() bus.Close() bus.Close() + + expectNoLeaks(wv.DebugLive) fmt.Println("go/events: OK") } diff --git a/conformance/go/kvstore.go b/conformance/go/kvstore.go index 4aa5a5ec..6eac63f9 100644 --- a/conformance/go/kvstore.go +++ b/conformance/go/kvstore.go @@ -12,11 +12,11 @@ // optional TTL and prefix parameters, and the nested kv.stats submodule // borrowing the Store across the module boundary. // -// ABI 2 surface: a Go type implementing the EvictionListener callback +// Callbacks and objects: a Go type implementing the EvictionListener callback // interface (called with the decoded Entry and the EvictionReason, its bool // return detaching it, replacement and clear releasing the old handle, and a // panicking implementation surfacing to the throwing caller as a -// *WeaveFFIError with code -4), plus reference-counted objects everywhere: +// *Error with code -4), plus reference-counted objects everywhere: // Share() aliasing the same store, Fork() copying it, Store? both ways in // Larger, a Store inside the StoreInfo record (Describe), a list of stores // returned by StoreOpenMany, and stores encoded into a parameter buffer by @@ -25,25 +25,19 @@ package main import ( + "context" "errors" "fmt" - "os" "runtime" "sort" "strings" + "sync" "sync/atomic" "time" wv "__MODPATH__" ) -func expect(cond bool, msg string) { - if !cond { - fmt.Fprintln(os.Stderr, "assertion failed:", msg) - os.Exit(1) - } -} - type eviction struct { key string value []byte @@ -250,8 +244,8 @@ func main() { l4 := listen(store, 1000, &freed) put(store, "boom", payload, nil) _, err = store.Delete("boom") - var ferr *wv.WeaveFFIError - expect(errors.As(err, &ferr), fmt.Sprintf("panicking listener yields *WeaveFFIError (got %T %v)", err, err)) + var ferr *wv.Error + expect(errors.As(err, &ferr), fmt.Sprintf("panicking listener yields *Error (got %T %v)", err, err)) expect(ferr.Code == -4, fmt.Sprintf("foreign error code -4 (got %d)", ferr.Code)) expect(strings.Contains(ferr.Message, "listener refused boom"), fmt.Sprintf("foreign error carries the panic text (got %q)", ferr.Message)) @@ -342,10 +336,33 @@ func main() { // Async: an immediately-expired entry gives compact 3 bytes to reclaim; // the cgo trampoline bridges the producer's worker thread to a channel. put(store, "doomed", payload, ptrInt64(0)) - reclaimed, err := store.Compact() + reclaimed, err := store.Compact(context.Background()) expect(err == nil, "compact async") expect(reclaimed == 3, fmt.Sprintf("compact reclaimed 3 bytes (got %d)", reclaimed)) expect(store.Count() == 4, "store count after compact") + cancelled, cancel := context.WithCancel(context.Background()) + cancel() + _, err = store.Compact(cancelled) + expect(errors.Is(err, context.Canceled), fmt.Sprintf("compact with a cancelled context (got %v)", err)) + + // Close racing calls on other goroutines: each call either completes or + // panics with "used after Close"; none sees a freed store. + racer, err := wv.OpenStore("/tmp/race") + expect(err == nil, "open racer") + var wg sync.WaitGroup + for range 8 { + wg.Add(1) + go func() { + defer wg.Done() + for range 500 { + if catchPanic(func() { racer.Count() }) != nil { + return + } + } + }() + } + racer.Close() + wg.Wait() // Plain void method, then release every object; Close is idempotent. store.Clear() @@ -362,6 +379,9 @@ func main() { forked.Close() store.Close() store.Close() + expect(catchPanic(func() { store.Count() }) != nil, "use after Close panics") + + expectNoLeaks(wv.DebugLive) fmt.Println("go/kvstore: OK") } diff --git a/conformance/go/run.sh b/conformance/go/run.sh new file mode 100755 index 00000000..8ff03114 --- /dev/null +++ b/conformance/go/run.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Conformance lanes for the go target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane compiles and runs one +# consumer against one sample and must exit 0. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# Run a Go consumer in a throwaway module that requires the generated module +# as-is (a `replace` points at its directory). The module path follows the +# package identity or the configured `module_path`, so it's read from the +# generated go.mod and substituted for the consumer's `__MODPATH__` import +# sentinel. The generated module ships its C header and links +# `-l`; only the library directory is supplied. +go_consumer() { + local sample="$1" src="$2" + local moddir modpath + modpath=$(sed -n 's/^module //p' "$GENROOT/$sample/go/go.mod" | head -1) + moddir="$OUT/go-$sample" + rm -rf "$moddir" + mkdir -p "$moddir" + sed "s#__MODPATH__#$modpath#g" "$ROOT/conformance/go/$src" > "$moddir/main.go" + cp "$ROOT/conformance/go/common.go" "$moddir/common.go" + cat > "$moddir/go.mod" < $GENROOT/$sample/go +EOF + # The loader path is set here because macOS strips DYLD_LIBRARY_PATH when + # the lane timeout launches this script. + ( cd "$moddir" \ + && export GOPROXY=off GOSUMDB=off GOFLAGS=-mod=mod CGO_LDFLAGS="-L$LIBDIR" \ + LD_LIBRARY_PATH="$LIBDIR:${LD_LIBRARY_PATH:-}" \ + DYLD_LIBRARY_PATH="$LIBDIR:${DYLD_LIBRARY_PATH:-}" \ + && go vet . \ + && go run . ) +} + +go_calculator() { go_consumer calculator calculator.go; } + +go_contacts() { go_consumer contacts contacts.go; } + +go_events() { go_consumer events events.go; } + +go_kvstore() { go_consumer kvstore kvstore.go; } + +go_async_demo() { go_consumer async-demo async_demo.go; } + +go_codec() { go_consumer codec codec.go; } + +lane go-calculator go_calculator +lane go-contacts go_contacts +lane go-events go_events +lane go-kvstore go_kvstore +lane go-async-demo go_async_demo +lane go-codec go_codec + +finish_lanes diff --git a/conformance/go/shapes.go b/conformance/go/shapes.go deleted file mode 100644 index fde0dd48..00000000 --- a/conformance/go/shapes.go +++ /dev/null @@ -1,86 +0,0 @@ -// Conformance consumer: shapes sample, Go target. -// -// Drives the generated cgo bindings for rich (algebraic) enums as a sealed -// sum type: the Shape interface with one value struct per variant -// (ShapeEmpty, ShapeCircle, ...), constructed as plain Go composite literals -// and inspected with type assertions, plus the free functions that pack a -// Shape into a value buffer on the way in and unpack one on the way out. The -// free functions are non-throwing, so they have plain returns. Also covers -// the expanded numerics (f32 fields, u8 field, []byte in, uint64 out). Exits -// 0 on success; aborts (non-zero) on any failed assertion. - -package main - -import ( - "fmt" - "math" - "os" - - wv "__MODPATH__" -) - -func expect(cond bool, msg string) { - if !cond { - fmt.Fprintln(os.Stderr, "assertion failed:", msg) - os.Exit(1) - } -} - -func main() { - // Unit variant: an empty struct satisfying the sealed interface. - var empty wv.Shape = wv.ShapeEmpty{} - _, isEmpty := empty.(wv.ShapeEmpty) - expect(isEmpty, "empty is ShapeEmpty") - - // f64 payload. - circle := wv.ShapeCircle{Radius: 2.5} - expect(math.Abs(circle.Radius-2.5) < 1e-9, "circle radius 2.5") - - // Two f32 payloads. - rect := wv.ShapeRectangle{Width: 3.0, Height: 4.0} - expect(math.Abs(float64(rect.Width)-3.0) < 1e-6, "rectangle width 3.0") - expect(math.Abs(float64(rect.Height)-4.0) < 1e-6, "rectangle height 4.0") - - // string + u8 payload. - labeled := wv.ShapeLabeled{Label: "hex", Count: 6} - expect(labeled.Label == "hex", "labeled label \"hex\"") - expect(labeled.Count == 6, "labeled count 6") - - // Free functions (non-throwing, plain returns): Shape in, string/Shape - // out. Each call round-trips the variant through the value buffer. - expect(wv.Describe(empty) == "empty", "describe(empty) == \"empty\"") - expect(wv.Describe(circle) == "circle(r=2.5)", "describe(circle) == \"circle(r=2.5)\"") - expect(wv.Describe(rect) == "rectangle(3x4)", "describe(rect) == \"rectangle(3x4)\"") - expect(wv.Describe(labeled) == "labeled(hex x6)", "describe(labeled) == \"labeled(hex x6)\"") - - big := wv.Scale(circle, 4.0) - bigCircle, isCircle := big.(wv.ShapeCircle) - expect(isCircle, "scaled shape is ShapeCircle") - expect(math.Abs(bigCircle.Radius-10.0) < 1e-9, "scaled radius 10.0") - - // Scaling the unit variant round-trips the tag-only encoding. - scaledEmpty := wv.Scale(empty, 2.0) - _, isEmpty = scaledEmpty.(wv.ShapeEmpty) - expect(isEmpty, "scaled empty stays ShapeEmpty") - - // A type switch covers every variant of the sealed sum type. Scaling a - // labeled shape leaves its payload untouched, so the string + u8 fields - // round-trip through the buffer unchanged. - scaledLabeled := wv.Scale(labeled, 2.0) - switch s := scaledLabeled.(type) { - case wv.ShapeLabeled: - expect(s.Label == "hex", "scaled labeled keeps its label") - expect(s.Count == 6, "scaled labeled keeps its count") - case wv.ShapeEmpty, wv.ShapeCircle, wv.ShapeRectangle: - expect(false, "scale(labeled) returned the wrong variant") - } - - // A C-style enum keeps its plain int32 constants. - expect(wv.ChannelGreen == 1, "Channel constants keep their discriminants") - - // Numerics: list in, u64 out. - total := wv.SumBytes([]byte{250, 250, 250, 250}) - expect(total == 1000, "sum_bytes == 1000") - - fmt.Println("go/shapes: OK") -} diff --git a/conformance/kotlin/async_demo.kt b/conformance/kotlin/async_demo.kt index bfd21e45..3c14c8b1 100644 --- a/conformance/kotlin/async_demo.kt +++ b/conformance/kotlin/async_demo.kt @@ -1,63 +1,108 @@ -// Conformance consumer: async-demo sample, Android/Kotlin (JNI) target. +// Conformance consumer: async-demo sample, Kotlin (JVM via JNI) target. // // Drives the suspend-function async surface end to end: `runTask` resumed -// through a WeaveContinuation from the producer's worker thread and decoded -// from a value buffer into the TaskResult data class, the typed -// TaskException.InvalidName thrown for an empty name, the buffered -// list-of-records round trip through `runBatch`, the direct-scalar -// `runNTasks`, the sync `cancelTask`, and `activeCallbacks` settling to zero -// once every task body has completed. Compiled in-module with the generated -// `WeaveFFI.kt`; the JNI bridge loads as `libweaveffi` from java.library.path. +// through a `NativeCompletion` from the producer's worker thread and decoded +// from a value buffer into the `TaskResult` data class, the typed +// `TaskException.InvalidName` for an empty name, the buffered list-of-records +// round trip through `runBatch`, the direct-scalar `runNTasks`, and the +// cancellable `wait`: it completes normally when left alone, and cancelling +// the awaiting coroutine (directly or through `withTimeout`) cancels the +// native token, so the producer drops the pending work long before its +// timeout. At exit it checks that every native resource was released. @file:JvmName("Main") -import com.weaveffi.TaskException -import com.weaveffi.WeaveFFI -import com.weaveffi.WeaveFFIException +import async_demo.FfiException +import async_demo.JniBridge +import async_demo.TaskException +import async_demo.Tasks +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.async +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking -import kotlin.system.exitProcess +import kotlinx.coroutines.withTimeout -fun expect(cond: Boolean, msg: String) { - if (!cond) { - System.err.println("assertion failed: $msg") - exitProcess(1) +/** Poll `activeCallbacks` until it reads zero or `ms` milliseconds pass. */ +fun settled(ms: Long): Boolean { + val deadline = System.currentTimeMillis() + ms + while (System.currentTimeMillis() < deadline) { + if (Tasks.activeCallbacks() == 0L) return true + Thread.sleep(5) } + return Tasks.activeCallbacks() == 0L } -fun main() = runBlocking { +fun run() = runBlocking { // Async record return: the suspend fun resumes with a TaskResult decoded - // from the completion callback's value buffer. - val result = WeaveFFI.runTask("alpha") + // from the completion's value buffer. + val result = Tasks.runTask("alpha") expect(result.id > 0, "runTask assigns an id") expect(result.value == "completed: alpha", "runTask value (got ${result.value})") expect(result.success, "runTask success flag") - // Typed async error: the empty name resumes by throwing the typed - // subclass carrying its stable code. - try { - WeaveFFI.runTask("") - expect(false, "expected TaskException.InvalidName for empty name") + // Typed async error: the empty name throws the typed subclass. + val invalid = try { + Tasks.runTask("") + null } catch (e: TaskException.InvalidName) { - expect(e.code == 1, "InvalidName carries code 1 (got ${e.code})") - expect(e is TaskException, "subclass of TaskException") - expect(e is WeaveFFIException, "subclass of the brand exception") + e } + expect(invalid != null, "expected TaskException.InvalidName for an empty name") + expect(invalid!!.code == 1, "InvalidName carries code 1 (got ${invalid.code})") + expect(invalid is FfiException, "InvalidName is an FfiException") // Buffered list-of-records both ways. - val batch = WeaveFFI.runBatch(listOf("a", "b", "c")) + val batch = Tasks.runBatch(listOf("a", "b", "c")) expect( batch.map { it.value } == listOf("completed: a", "completed: b", "completed: c"), "runBatch values (got ${batch.map { it.value }})", ) expect(batch.all { it.success }, "runBatch success flags") - // Direct scalar through the async callback. - expect(WeaveFFI.runNTasks(7) == 7, "runNTasks echoes n") + // Direct scalar through the async completion, many at once. + expect(Tasks.runNTasks(7) == 7, "runNTasks echoes n") + val many = (1..50).map { n -> async { Tasks.runNTasks(n) } }.map { it.await() } + expect(many == (1..50).toList(), "concurrent runNTasks") - // Sync functions beside the async ones. - expect(!WeaveFFI.cancelTask(1L), "cancelTask reports not cancelled") + // A cancellable call left alone completes normally. + expect(Tasks.wait(20L) == 20L, "wait(20) completes with 20") - // Every spawned task body has completed by the time its callback fires. - expect(WeaveFFI.activeCallbacks() == 0L, "activeCallbacks settles to zero") + // Cancelling the awaiting coroutine cancels the native token: the call + // ends with CancellationException at once, and the producer drops the + // pending future (and its in-flight guard) instead of waiting 10 s. + val started = System.currentTimeMillis() + val job = async { Tasks.wait(10_000L) } + delay(50) + expect(Tasks.activeCallbacks() == 1L, "wait is in flight (got ${Tasks.activeCallbacks()})") + job.cancel() + val cancelled = try { + job.await() + null + } catch (e: CancellationException) { + e + } + expect(cancelled != null, "a cancelled wait throws CancellationException") + expect(settled(2_000), "the producer dropped the cancelled wait (active ${Tasks.activeCallbacks()})") + expect(System.currentTimeMillis() - started < 5_000, "cancellation didn't wait for the timeout") + + // withTimeout cancels the same way. + val timedOut = try { + withTimeout(50) { Tasks.wait(10_000L) } + null + } catch (e: TimeoutCancellationException) { + e + } + expect(timedOut != null, "withTimeout cancels wait") + expect(settled(2_000), "the producer dropped the timed-out wait") + expect(System.currentTimeMillis() - started < 5_000, "timeouts didn't wait for the producer's timer") + + // Every spawned task body has completed by the time its completion fires. + expect(Tasks.activeCallbacks() == 0L, "activeCallbacks settles to zero") +} - println("kotlin async-demo conformance: OK") +fun main() { + run() + println("kotlin/async-demo: OK") + expectNoLeaks { JniBridge.debug_live(it) } + println("kotlin/async-demo: no leaks") } diff --git a/conformance/kotlin/codec.kt b/conformance/kotlin/codec.kt index 1bf59dce..970c2d78 100644 --- a/conformance/kotlin/codec.kt +++ b/conformance/kotlin/codec.kt @@ -1,56 +1,35 @@ // Conformance consumer: codec sample, Kotlin (JVM via JNI) target. // // Drives the value-buffer round-trip oracle through the generated Kotlin -// encoder and decoder (`WeaveBufferWriter`/`WeaveBufferReader` plus the -// per-type `pack*`/`unpack*` routines). For `Scalars` and `Composite` the -// consumer decodes the producer's canonical fixture and checks every field -// against concrete values, hands the same value back through `verify*` (which -// proves the Kotlin encoder produced exactly the bytes Rust encodes), and -// compares `roundtrip*` output field by field. It then builds its own values -// from scratch with edge cases (empty strings, lists, maps, and byte arrays; -// present-but-empty optionals; BMP and supplementary unicode; the i8..u64 -// extremes, with unsigned values carried in Kotlin's signed types; NaN, both -// infinities, negative zero, and a subnormal) and round-trips them, exercises -// every `Shape` variant alone and inside lists and optionals, checks the -// typed `CodecException.Mismatch` from a rejected fixture, and exercises -// `Holder` (objects inside records, optionals, and lists: `sumHolder`, -// `primaryOf` returning the same native object as `holder.primary`, -// `samePrimary`, and clean release of every adopted reference with double -// `close()` safe and use after close rejected). Compiled in-module with the -// generated `WeaveFFI.kt`, so the `internal` buffer helpers and `handle` are -// reachable. +// encoder and decoder (`BufferWriter`/`BufferReader` plus the per-type +// `pack*`/`unpack*` routines). For `Scalars` and `Composite` the consumer +// decodes the producer's canonical fixture and checks every field, hands the +// same value back through `verify*` (proving the Kotlin encoder produced +// exactly the bytes Rust encodes), and compares `roundtrip*` output field by +// field. It then round-trips its own edge cases (empty strings, lists, maps, +// and byte arrays; present-but-empty optionals; BMP and supplementary +// unicode; strings with interior NULs, both inside buffers and as direct +// arguments; the i8..u64 extremes; NaN, infinities, negative zero, and a +// subnormal), every `Shape` variant, the typed `CodecException.Mismatch`, +// and `Holder` (objects inside records, optionals, and lists, with clean +// release of every adopted reference). At exit it checks that every native +// resource was released. @file:JvmName("Main") -import com.weaveffi.CodecException -import com.weaveffi.Color -import com.weaveffi.Composite -import com.weaveffi.Holder -import com.weaveffi.Scalars -import com.weaveffi.Shape -import com.weaveffi.Token -import com.weaveffi.WeaveFFI -import com.weaveffi.WeaveFFIException -import com.weaveffi.packComposite -import com.weaveffi.unpackComposite -import com.weaveffi.weaveDecode -import com.weaveffi.weaveEncode -import kotlin.system.exitProcess - -fun expect(cond: Boolean, msg: String) { - if (!cond) { - System.err.println("assertion failed: $msg") - exitProcess(1) - } -} - -/** Run `block` and return the exception it threw, or null if it completed. */ -inline fun thrownBy(block: () -> Unit): Throwable? = - try { - block() - null - } catch (e: Throwable) { - e - } +import codec.CodecException +import codec.Color +import codec.Composite +import codec.Holder +import codec.Scalars +import codec.Shape +import codec.Token +import codec.Codec +import codec.JniBridge +import codec.FfiException +import codec.packComposite +import codec.unpackComposite +import codec.decodeBuffer +import codec.encodeBuffer /** The canonical `Scalars` fixture, spelled with Kotlin's signed carriers for the unsigned fields. */ fun canonicalScalars(): Scalars = Scalars( @@ -95,7 +74,7 @@ fun compositeEquals(a: Composite, b: Composite): Boolean = fun checkScalars() { val canonical = canonicalScalars() - val sample = WeaveFFI.sampleScalars() + val sample = Codec.sampleScalars() // Producer encodes, consumer decodes: every field, with the unsigned ones // read back through their signed carriers. expect(sample.i8_value.toInt() == -8, "i8 (got ${sample.i8_value})") @@ -113,17 +92,17 @@ fun checkScalars() { expect(sample == canonical, "sample equals the locally built canonical Scalars") // Consumer encodes, producer decodes and compares with its canonical. - expect(WeaveFFI.verifyScalars(sample), "verifyScalars(sample)") - expect(WeaveFFI.verifyScalars(canonical), "verifyScalars(locally built canonical)") - expect(WeaveFFI.roundtripScalars(sample) == sample, "roundtripScalars(sample) equals sample") + expect(Codec.verifyScalars(sample), "verifyScalars(sample)") + expect(Codec.verifyScalars(canonical), "verifyScalars(locally built canonical)") + expect(Codec.roundtripScalars(sample) == sample, "roundtripScalars(sample) equals sample") // A rejected fixture is the typed domain error. - val mismatch = thrownBy { WeaveFFI.verifyScalars(canonical.copy(flag = false)) } + val mismatch = thrownBy { Codec.verifyScalars(canonical.copy(flag = false)) } expect(mismatch is CodecException.Mismatch, "verifyScalars(modified) throws Mismatch (got $mismatch)") - expect((mismatch as WeaveFFIException).code == 1, "Mismatch code 1 (got ${mismatch.code})") + expect((mismatch as FfiException).code == 1, "Mismatch code 1 (got ${mismatch.code})") expect(mismatch.message == "value does not match the canonical fixture", "Mismatch message (got ${mismatch.message})") - expect(thrownBy { WeaveFFI.verifyScalars(canonical.copy(u64_value = 0L)) } is CodecException.Mismatch, "u64 difference detected") - expect(thrownBy { WeaveFFI.verifyScalars(canonical.copy(color = Color.Red)) } is CodecException.Mismatch, "enum difference detected") + expect(thrownBy { Codec.verifyScalars(canonical.copy(u64_value = 0L)) } is CodecException.Mismatch, "u64 difference detected") + expect(thrownBy { Codec.verifyScalars(canonical.copy(color = Color.Red)) } is CodecException.Mismatch, "enum difference detected") // From scratch, at the extremes of every width, plus the special floats. val edge = Scalars( @@ -140,7 +119,7 @@ fun checkScalars() { flag = false, color = Color.Red, ) - val edgeBack = WeaveFFI.roundtripScalars(edge) + val edgeBack = Codec.roundtripScalars(edge) expect(edgeBack.i8_value == Byte.MIN_VALUE, "i8 min round-trips") expect(edgeBack.u8_value.toUByte() == UByte.MAX_VALUE, "u8 255 round-trips") expect(edgeBack.i16_value == Short.MIN_VALUE, "i16 min round-trips") @@ -168,14 +147,14 @@ fun checkScalars() { f64_value = Double.POSITIVE_INFINITY, color = Color.Green, ) - expect(WeaveFFI.roundtripScalars(maxes) == maxes, "max Scalars round-trip") + expect(Codec.roundtripScalars(maxes) == maxes, "max Scalars round-trip") val subnormal = edge.copy(f32_value = Float.MIN_VALUE, f64_value = Double.MIN_VALUE) - expect(WeaveFFI.roundtripScalars(subnormal) == subnormal, "subnormal floats round-trip") + expect(Codec.roundtripScalars(subnormal) == subnormal, "subnormal floats round-trip") } fun checkComposite() { val canonical = canonicalScalars() - val sample = WeaveFFI.sampleComposite() + val sample = Codec.sampleComposite() expect(sample.name == "héllo wörld ✓", "name (got ${sample.name})") expect(sample.blob.contentEquals(byteArrayOf(0, 1, 2, 253.toByte(), 254.toByte(), 255.toByte())), "blob (got ${sample.blob.toList()})") expect(sample.some_i64 == Long.MIN_VALUE, "some_i64 is i64::MIN (got ${sample.some_i64})") @@ -206,25 +185,25 @@ fun checkComposite() { expect(sample.colors == listOf(Color.Red, Color.Green, Color.Blue), "colors (got ${sample.colors})") // Consumer encodes, producer decodes and compares. - expect(WeaveFFI.verifyComposite(sample), "verifyComposite(sample)") - val back = WeaveFFI.roundtripComposite(sample) + expect(Codec.verifyComposite(sample), "verifyComposite(sample)") + val back = Codec.roundtripComposite(sample) expect(compositeEquals(back, sample), "roundtripComposite(sample) equals sample") // The local encoder and decoder agree with each other too. - val local = weaveDecode(weaveEncode { w -> packComposite(w, sample) }) { r -> unpackComposite(r) } + val local = decodeBuffer(encodeBuffer { w -> packComposite(w, sample) }) { r -> unpackComposite(r) } expect(compositeEquals(local, sample), "local encode/decode round trip") // The producer's rendering is a debugging aid: check it saw our unicode. - val described = WeaveFFI.describeComposite(sample) + val described = Codec.describeComposite(sample) expect(described.contains("héllo wörld ✓"), "describeComposite carries the name (got $described)") expect(described.contains("Labeled { label: \"tag\", count: 3 }"), "describeComposite carries the shape") // Any single change is detected by the producer. val changed = sample.copy(sparse = listOf(true, true, false)) - val mismatch = thrownBy { WeaveFFI.verifyComposite(changed) } + val mismatch = thrownBy { Codec.verifyComposite(changed) } expect(mismatch is CodecException.Mismatch, "verifyComposite(changed) throws Mismatch (got $mismatch)") - expect(thrownBy { WeaveFFI.verifyComposite(sample.copy(none_i64 = 0L)) } is CodecException.Mismatch, "present vs absent optional detected") - expect(thrownBy { WeaveFFI.verifyComposite(sample.copy(maybe_list = null)) } is CodecException.Mismatch, "absent list detected") - expect(thrownBy { WeaveFFI.verifyComposite(sample.copy(some_text = null)) } is CodecException.Mismatch, "absent empty string detected") + expect(thrownBy { Codec.verifyComposite(sample.copy(none_i64 = 0L)) } is CodecException.Mismatch, "present vs absent optional detected") + expect(thrownBy { Codec.verifyComposite(sample.copy(maybe_list = null)) } is CodecException.Mismatch, "absent list detected") + expect(thrownBy { Codec.verifyComposite(sample.copy(some_text = null)) } is CodecException.Mismatch, "absent empty string detected") // From scratch: everything empty or absent, with supplementary unicode. val bare = Composite( @@ -246,8 +225,8 @@ fun checkComposite() { sparse = listOf(), colors = listOf(), ) - val bareBack = WeaveFFI.roundtripComposite(bare) - expect(compositeEquals(bareBack, bare), "bare Composite round-trips (got ${WeaveFFI.describeComposite(bareBack)})") + val bareBack = Codec.roundtripComposite(bare) + expect(compositeEquals(bareBack, bare), "bare Composite round-trips (got ${Codec.describeComposite(bareBack)})") expect(bareBack.maybe_list == null && bareBack.some_text == null, "absent optionals stay absent") val full = Composite( @@ -269,14 +248,14 @@ fun checkComposite() { sparse = listOf(null, null, true), colors = listOf(Color.Blue, Color.Blue, Color.Red), ) - val fullBack = WeaveFFI.roundtripComposite(full) - expect(compositeEquals(fullBack, full), "full Composite round-trips (got ${WeaveFFI.describeComposite(fullBack)})") + val fullBack = Codec.roundtripComposite(full) + expect(compositeEquals(fullBack, full), "full Composite round-trips (got ${Codec.describeComposite(fullBack)})") expect(fullBack.maybe_list != null && fullBack.maybe_list.isEmpty(), "present-but-empty bytes stay present") expect(fullBack.empty[0].isNaN() && fullBack.empty[2].toRawBits() == (-0.0).toRawBits(), "special doubles inside a list") expect((fullBack.shapes[0] as Shape.Circle).radius.isNaN(), "NaN inside a rich enum") expect((fullBack.shapes[1] as Shape.Rect).width.toRawBits() == (-0.0f).toRawBits(), "-0.0f inside a rich enum") expect(fullBack.name == full.name, "supplementary characters and NUL survive inside a buffer") - expect(WeaveFFI.describeComposite(full).contains("日本語 🚀 \\0 emoji and NUL"), "producer decoded the unicode name") + expect(Codec.describeComposite(full).contains("日本語 🚀 \\0 emoji and NUL"), "producer decoded the unicode name") } fun checkShapes() { @@ -290,95 +269,95 @@ fun checkShapes() { Shape.Nested(canonical.copy(flag = false), null), ) for (s in all) { - expect(WeaveFFI.roundtripShape(s) == s, "roundtripShape($s)") + expect(Codec.roundtripShape(s) == s, "roundtripShape($s)") } - expect(WeaveFFI.roundtripShapes(all) == all, "roundtripShapes(all)") - expect(WeaveFFI.roundtripShapes(listOf()).isEmpty(), "roundtripShapes(empty)") - expect(WeaveFFI.roundtripShape(Shape.Empty) === Shape.Empty, "Empty decodes to the singleton") - expect(WeaveFFI.describeShape(Shape.Empty) == "Empty", "describe Empty") - expect(WeaveFFI.describeShape(Shape.Circle(2.5)) == "Circle { radius: 2.5 }", "describe Circle (got ${WeaveFFI.describeShape(Shape.Circle(2.5))})") - expect(WeaveFFI.describeShape(Shape.Rect(1.0f, 0.5f)) == "Rect { width: 1.0, height: 0.5 }", "describe Rect") - expect(WeaveFFI.describeShape(Shape.Labeled("tag", 3)) == "Labeled { label: \"tag\", count: 3 }", "describe Labeled") - val nested = WeaveFFI.describeShape(Shape.Nested(canonical, null)) + expect(Codec.roundtripShapes(all) == all, "roundtripShapes(all)") + expect(Codec.roundtripShapes(listOf()).isEmpty(), "roundtripShapes(empty)") + expect(Codec.roundtripShape(Shape.Empty) === Shape.Empty, "Empty decodes to the singleton") + expect(Codec.describeShape(Shape.Empty) == "Empty", "describe Empty") + expect(Codec.describeShape(Shape.Circle(2.5)) == "Circle { radius: 2.5 }", "describe Circle (got ${Codec.describeShape(Shape.Circle(2.5))})") + expect(Codec.describeShape(Shape.Rect(1.0f, 0.5f)) == "Rect { width: 1.0, height: 0.5 }", "describe Rect") + expect(Codec.describeShape(Shape.Labeled("tag", 3)) == "Labeled { label: \"tag\", count: 3 }", "describe Labeled") + val nested = Codec.describeShape(Shape.Nested(canonical, null)) expect(nested.startsWith("Nested { inner: Scalars { i8_value: -8, u8_value: 200,") && nested.endsWith("note: None }"), "describe Nested (got $nested)") - expect(WeaveFFI.describeShape(Shape.Labeled("🚀", -1)) == "Labeled { label: \"🚀\", count: -1 }", "describe Labeled with emoji") + expect(Codec.describeShape(Shape.Labeled("🚀", -1)) == "Labeled { label: \"🚀\", count: -1 }", "describe Labeled with emoji") } fun checkPrimitives() { - expect(WeaveFFI.roundtripOptI64(null) == null, "roundtripOptI64(null)") - expect(WeaveFFI.roundtripOptI64(Long.MIN_VALUE) == Long.MIN_VALUE, "roundtripOptI64(min)") - expect(WeaveFFI.roundtripOptI64(0L) == 0L, "roundtripOptI64(0)") - expect(WeaveFFI.roundtripMap(mapOf()) == mapOf(), "roundtripMap(empty)") + expect(Codec.roundtripOptI64(null) == null, "roundtripOptI64(null)") + expect(Codec.roundtripOptI64(Long.MIN_VALUE) == Long.MIN_VALUE, "roundtripOptI64(min)") + expect(Codec.roundtripOptI64(0L) == 0L, "roundtripOptI64(0)") + expect(Codec.roundtripMap(mapOf()) == mapOf(), "roundtripMap(empty)") val m = mapOf("" to 0L, "a" to -1L, "héllo 🚀" to Long.MAX_VALUE) - expect(WeaveFFI.roundtripMap(m) == m, "roundtripMap (got ${WeaveFFI.roundtripMap(m)})") + expect(Codec.roundtripMap(m) == m, "roundtripMap (got ${Codec.roundtripMap(m)})") // Direct strings: BMP and supplementary characters both survive the JNI // crossing (the bridge converts to standard UTF-8, not modified UTF-8). for (s in listOf("", "plain", "héllo wörld ✓", "rocket 🚀 end", "🚀", "\uFFFF")) { - expect(WeaveFFI.roundtripString(s) == s, "roundtripString(${s.toByteArray().toList()})") + expect(Codec.roundtripString(s) == s, "roundtripString(${s.toByteArray().toList()})") } - // A C string can't carry U+0000: the producer reports a marshalling error - // rather than truncating. - val nul = thrownBy { WeaveFFI.roundtripString("a\u0000b") } - expect(nul is WeaveFFIException && nul.code == -3, "embedded NUL is rejected with code -3 (got $nul)") - expect(WeaveFFI.roundtripBytes(byteArrayOf()).isEmpty(), "roundtripBytes(empty)") + // Strings cross as UTF-8 bytes plus a length, so interior NULs survive. + for (s in listOf("a\u0000b", "\u0000", "\u0000\u0000end", "\uD83D\uDE80\u0000\uD83D\uDE80")) { + expect(Codec.roundtripString(s) == s, "roundtripString with NUL (${s.toByteArray().toList()})") + } + expect(Codec.roundtripBytes(byteArrayOf()).isEmpty(), "roundtripBytes(empty)") val allBytes = ByteArray(256) { it.toByte() } - expect(WeaveFFI.roundtripBytes(allBytes).contentEquals(allBytes), "roundtripBytes(0..255)") + expect(Codec.roundtripBytes(allBytes).contentEquals(allBytes), "roundtripBytes(0..255)") - expect(WeaveFFI.roundtripI64(Long.MIN_VALUE) == Long.MIN_VALUE, "roundtripI64(min)") - expect(WeaveFFI.roundtripI64(Long.MAX_VALUE) == Long.MAX_VALUE, "roundtripI64(max)") - expect(WeaveFFI.roundtripI64(-1L) == -1L, "roundtripI64(-1)") - expect(WeaveFFI.roundtripU64(ULong.MAX_VALUE.toLong()).toULong() == ULong.MAX_VALUE, "roundtripU64(max)") - expect(WeaveFFI.roundtripU64(Long.MIN_VALUE).toULong() == 9_223_372_036_854_775_808UL, "roundtripU64(2^63)") - expect(WeaveFFI.roundtripU64(0L) == 0L, "roundtripU64(0)") - expect(WeaveFFI.roundtripF64(Double.NaN).isNaN(), "roundtripF64(NaN)") - expect(WeaveFFI.roundtripF64(Double.POSITIVE_INFINITY) == Double.POSITIVE_INFINITY, "roundtripF64(+inf)") - expect(WeaveFFI.roundtripF64(Double.NEGATIVE_INFINITY) == Double.NEGATIVE_INFINITY, "roundtripF64(-inf)") - expect(WeaveFFI.roundtripF64(-0.0).toRawBits() == (-0.0).toRawBits(), "roundtripF64(-0.0) keeps the sign") - expect(WeaveFFI.roundtripF64(Double.MIN_VALUE) == Double.MIN_VALUE, "roundtripF64(subnormal)") - expect(WeaveFFI.roundtripF64(Double.MAX_VALUE) == Double.MAX_VALUE, "roundtripF64(max)") - expect(WeaveFFI.roundtripBool(true) && !WeaveFFI.roundtripBool(false), "roundtripBool") - expect(WeaveFFI.roundtripColor(Color.Blue) == Color.Blue, "roundtripColor(Blue)") - expect(WeaveFFI.roundtripColor(Color.Red) == Color.Red, "roundtripColor(Red)") + expect(Codec.roundtripI64(Long.MIN_VALUE) == Long.MIN_VALUE, "roundtripI64(min)") + expect(Codec.roundtripI64(Long.MAX_VALUE) == Long.MAX_VALUE, "roundtripI64(max)") + expect(Codec.roundtripI64(-1L) == -1L, "roundtripI64(-1)") + expect(Codec.roundtripU64(ULong.MAX_VALUE.toLong()).toULong() == ULong.MAX_VALUE, "roundtripU64(max)") + expect(Codec.roundtripU64(Long.MIN_VALUE).toULong() == 9_223_372_036_854_775_808UL, "roundtripU64(2^63)") + expect(Codec.roundtripU64(0L) == 0L, "roundtripU64(0)") + expect(Codec.roundtripF64(Double.NaN).isNaN(), "roundtripF64(NaN)") + expect(Codec.roundtripF64(Double.POSITIVE_INFINITY) == Double.POSITIVE_INFINITY, "roundtripF64(+inf)") + expect(Codec.roundtripF64(Double.NEGATIVE_INFINITY) == Double.NEGATIVE_INFINITY, "roundtripF64(-inf)") + expect(Codec.roundtripF64(-0.0).toRawBits() == (-0.0).toRawBits(), "roundtripF64(-0.0) keeps the sign") + expect(Codec.roundtripF64(Double.MIN_VALUE) == Double.MIN_VALUE, "roundtripF64(subnormal)") + expect(Codec.roundtripF64(Double.MAX_VALUE) == Double.MAX_VALUE, "roundtripF64(max)") + expect(Codec.roundtripBool(true) && !Codec.roundtripBool(false), "roundtripBool") + expect(Codec.roundtripColor(Color.Blue) == Color.Blue, "roundtripColor(Blue)") + expect(Codec.roundtripColor(Color.Red) == Color.Red, "roundtripColor(Red)") expect(Color.Blue.value == 7 && Color.fromValue(7) == Color.Blue, "Color discriminants") } fun checkHolders() { // Objects inside a record decoded from a buffer: each token is one adopted // strong reference wrapped in a Token. - val holder = WeaveFFI.makeHolder(10L, true) + val holder = Codec.makeHolder(10L, true) expect(holder.primary.value() == 10L, "primary value (got ${holder.primary.value()})") expect(holder.spare != null && holder.spare.value() == 11L, "spare value (got ${holder.spare?.value()})") expect(holder.many.map { it.value() } == listOf(12L, 13L, 14L), "many values (got ${holder.many.map { it.value() }})") - expect(holder.many.map { it.handle }.toSet().size == 3, "many are distinct objects") + expect(holder.many.map { it.handle.address }.toSet().size == 3, "many are distinct objects") // Encoding a holder mints one fresh reference per token, so the wrappers // stay valid after the producer consumed the buffer. - expect(WeaveFFI.sumHolder(holder) == 10L + 11L + 12L + 13L + 14L, "sumHolder (got ${WeaveFFI.sumHolder(holder)})") - expect(WeaveFFI.sumHolder(holder) == 60L, "sumHolder again (wrappers still alive)") + expect(Codec.sumHolder(holder) == 10L + 11L + 12L + 13L + 14L, "sumHolder (got ${Codec.sumHolder(holder)})") + expect(Codec.sumHolder(holder) == 60L, "sumHolder again (wrappers still alive)") expect(holder.primary.value() == 10L, "primary still usable after encoding") // An object return is the same native object as the record's field. - val primary = WeaveFFI.primaryOf(holder) + val primary = Codec.primaryOf(holder) expect(primary !== holder.primary, "primaryOf returns a new wrapper") - expect(primary.handle == holder.primary.handle, "primaryOf wraps the same native object") + expect(primary.handle.address == holder.primary.handle.address, "primaryOf wraps the same native object") expect(primary.value() == 10L, "primaryOf value") - expect(WeaveFFI.samePrimary(holder, holder), "samePrimary(holder, holder)") - val other = WeaveFFI.makeHolder(10L, false) + expect(Codec.samePrimary(holder, holder), "samePrimary(holder, holder)") + val other = Codec.makeHolder(10L, false) expect(other.spare == null, "spare absent when not requested") expect(other.primary.value() == 10L, "other primary value equal but ...") - expect(!WeaveFFI.samePrimary(holder, other), "... samePrimary distinguishes distinct objects") - expect(WeaveFFI.sumHolder(other) == 10L + 12L + 13L + 14L, "sumHolder without spare") + expect(!Codec.samePrimary(holder, other), "... samePrimary distinguishes distinct objects") + expect(Codec.sumHolder(other) == 10L + 12L + 13L + 14L, "sumHolder without spare") // A holder assembled in Kotlin from consumer-created tokens, sharing one // token across positions. val shared = Token(100L) val mine = Holder(primary = shared, spare = shared, many = listOf(shared, Token(1L), primary)) - expect(WeaveFFI.sumHolder(mine) == 100L + 100L + 100L + 1L + 10L, "sumHolder over a Kotlin-built holder") - expect(WeaveFFI.samePrimary(mine, Holder(shared, null, listOf())), "samePrimary across Kotlin-built holders") - expect(WeaveFFI.samePrimary(Holder(primary, null, listOf()), holder), "samePrimary through primaryOf's wrapper") - val mineBack = WeaveFFI.primaryOf(mine) - expect(mineBack.handle == shared.handle && mineBack.value() == 100L, "primaryOf a Kotlin-built holder") + expect(Codec.sumHolder(mine) == 100L + 100L + 100L + 1L + 10L, "sumHolder over a Kotlin-built holder") + expect(Codec.samePrimary(mine, Holder(shared, null, listOf())), "samePrimary across Kotlin-built holders") + expect(Codec.samePrimary(Holder(primary, null, listOf()), holder), "samePrimary through primaryOf's wrapper") + val mineBack = Codec.primaryOf(mine) + expect(mineBack.handle.address == shared.handle.address && mineBack.value() == 100L, "primaryOf a Kotlin-built holder") // Release everything. Closing one wrapper over a shared object leaves the // others valid; double close is safe; use after close throws; closing a @@ -387,7 +366,7 @@ fun checkHolders() { primary.close() expect(thrownBy { primary.value() } is IllegalStateException, "closed token rejects use") expect(holder.primary.value() == 10L, "the record's wrapper survives closing primaryOf's wrapper") - expect(thrownBy { WeaveFFI.sumHolder(mine) } is IllegalStateException, "encoding a holder with a closed token throws") + expect(thrownBy { Codec.sumHolder(mine) } is IllegalStateException, "encoding a holder with a closed token throws") mineBack.close() expect(shared.value() == 100L, "shared token alive after closing its second wrapper") shared.close() @@ -398,15 +377,15 @@ fun checkHolders() { holder.many.forEach { it.close() } other.primary.close() other.many.forEach { it.close() } - expect(thrownBy { WeaveFFI.sumHolder(holder) } is IllegalStateException, "fully closed holder rejects encoding") + expect(thrownBy { Codec.sumHolder(holder) } is IllegalStateException, "fully closed holder rejects encoding") // Wrappers that are never closed are released by the Cleaner; create a // batch and let them go. - repeat(50) { WeaveFFI.makeHolder(it.toLong(), true) } + repeat(50) { Codec.makeHolder(it.toLong(), true) } System.gc() } -fun main() { +fun run() { checkScalars() checkComposite() checkShapes() @@ -414,3 +393,9 @@ fun main() { checkHolders() println("kotlin/codec: OK") } + +fun main() { + run() + expectNoLeaks { JniBridge.debug_live(it) } + println("kotlin/codec: no leaks") +} diff --git a/conformance/kotlin/common.kt b/conformance/kotlin/common.kt new file mode 100644 index 00000000..0e1354b9 --- /dev/null +++ b/conformance/kotlin/common.kt @@ -0,0 +1,49 @@ +// Helpers shared by the Kotlin conformance consumers; each lane compiles +// this file together with its consumer. + +import java.lang.ref.WeakReference +import kotlin.system.exitProcess + +fun expect(cond: Boolean, msg: String) { + if (!cond) { + System.err.println("assertion failed: $msg") + exitProcess(1) + } +} + +/** Run `block` and return the exception it threw, or null if it completed. */ +inline fun thrownBy(block: () -> Unit): Throwable? = + try { + block() + null + } catch (e: Throwable) { + e + } + +/** Spin the collector until `ref` clears or we give up. */ +fun collected(ref: WeakReference<*>): Boolean { + for (i in 0 until 200) { + if (ref.get() == null) return true + System.gc() + System.runFinalization() + Thread.sleep(5) + } + return ref.get() == null +} + +/** + * Force collection (so the cleaner releases every unreachable wrapper and + * iterator) until the producer's leak counters all read zero: 0 objects, + * 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + */ +fun expectNoLeaks(live: (Int) -> Long) { + val kinds = listOf("objects", "callbacks", "iterators", "cancel tokens", "allocations") + for (i in 0 until 400) { + if (kinds.indices.all { live(it) == 0L }) return + System.gc() + System.runFinalization() + Thread.sleep(5) + } + val counts = kinds.indices.joinToString { "${kinds[it]}=${live(it)}" } + expect(false, "native resources still live at exit: $counts") +} diff --git a/conformance/kotlin/contacts.kt b/conformance/kotlin/contacts.kt index de68d4c7..6b4366b4 100644 --- a/conformance/kotlin/contacts.kt +++ b/conformance/kotlin/contacts.kt @@ -1,44 +1,31 @@ -// Conformance consumer: contacts sample, Android/Kotlin (JNI) target. +// Conformance consumer: contacts sample, Kotlin (JVM via JNI) target. // -// Exercises the original struct/enum/optional surface: `ContactBook` is a -// generated Closeable class (companion `invoke` for the canonical `new` -// constructor, instance methods, destroy through `close()`), `Contact` is a -// plain data class decoded from a value buffer (the nullable email rides -// inside the same buffer), enums cross as `ContactType` values, and -// `ContactsError` is a typed exception domain (`ContactsException` sealed -// subclasses extending the generic `WeaveFFIException`, raised through the -// payload-aware `fromCode` factory). Asserts the typed-error paths -// (InvalidName from an empty first name, NotFound from a missing id), record -// materialization with data-class value equality, the buffered nullable email -// parameter and field, list-of-record returns, boolean returns, and -// per-object state. Compiled in-module with the generated `WeaveFFI.kt`, so -// the `internal` constructor surface is reachable. +// Exercises the struct/enum/optional surface across three modules: +// `ContactBook` is a generated `AutoCloseable` class (companion `invoke` for +// the `new` constructor, instance methods, release through `close()`), +// `Contact` is a data class decoded from a value buffer (the nullable email +// rides inside it), enums cross as `ContactType` values, and `ContactsError` +// is a typed exception domain (`ContactsException` sealed subclasses +// extending `FfiException`). The nested `contacts.groups` module +// (`Contacts.Groups`) takes the parent's interface and C-style enum, returns +// a new parent-module object and record, and reports the parent's domain; the +// sibling top-level `directory` module (`Directory`) takes the `contacts` +// record by value, alone and in a list, and reports its own domain. At exit +// it checks that every native resource was released. @file:JvmName("Main") -import com.weaveffi.Contact -import com.weaveffi.ContactBook -import com.weaveffi.ContactType -import com.weaveffi.ContactsException -import com.weaveffi.WeaveFFIException -import kotlin.system.exitProcess +import contacts.Card +import contacts.Contact +import contacts.ContactBook +import contacts.ContactType +import contacts.Contacts +import contacts.ContactsException +import contacts.Directory +import contacts.DirectoryException +import contacts.FfiException +import contacts.JniBridge -fun expect(cond: Boolean, msg: String) { - if (!cond) { - System.err.println("assertion failed: $msg") - exitProcess(1) - } -} - -/** Run `block` and return the exception it threw, or null if it completed. */ -inline fun thrownBy(block: () -> Unit): Throwable? = - try { - block() - null - } catch (e: Throwable) { - e - } - -fun main() { +fun checkBook() { ContactBook().use { book -> expect(book.count() == 0, "fresh book empty") @@ -75,16 +62,14 @@ fun main() { expect(!book.remove(alice.id), "second remove returns false") expect(book.count() == 1, "count == 1 after remove") - // Typed error from a method: a missing id reports NotFound (2), which - // is both the sealed domain type and the generic brand exception. + // Typed error from a method: a missing id reports NotFound (2). val missingErr = thrownBy { book.get(9999L) } expect( missingErr is ContactsException.NotFound, "get(9999) throws ContactsException.NotFound (got $missingErr)" ) - expect(missingErr is ContactsException, "NotFound is a ContactsException") - expect(missingErr is WeaveFFIException, "NotFound is a WeaveFFIException") - val missingCode = (missingErr as? WeaveFFIException)?.code + expect(missingErr is FfiException, "NotFound is an FfiException") + val missingCode = (missingErr as? FfiException)?.code expect(missingCode == 2, "NotFound code 2 (got $missingCode)") // Typed error: an empty first name is rejected with InvalidName (1) @@ -94,7 +79,7 @@ fun main() { invalidErr is ContactsException.InvalidName, "add(\"\") throws ContactsException.InvalidName (got $invalidErr)" ) - val invalidCode = (invalidErr as? WeaveFFIException)?.code + val invalidCode = (invalidErr as? FfiException)?.code expect(invalidCode == 1, "InvalidName code 1 (got $invalidCode)") expect(book.count() == 1, "failed add leaves count == 1") @@ -104,6 +89,56 @@ fun main() { expect(book.count() == 1, "first book unaffected") } } +} + +fun checkGroups() { + ContactBook().use { book -> + book.add("Ann", "Lee", null, ContactType.Work) + book.add("Ben", "Ito", "ben@example.com", ContactType.Work) + book.add("Cy", "Ng", null, ContactType.Personal) + + // The nested module takes the parent's interface and enum. + expect(Contacts.Groups.countOfType(book, ContactType.Work) == 2, "two work contacts") + expect(Contacts.Groups.countOfType(book, ContactType.Other) == 0, "no other contacts") + expect(Contacts.Groups.dominantType(book) == ContactType.Work, "work dominates") + // It returns a new object of the parent module's interface type. + Contacts.Groups.splitByType(book, ContactType.Work).use { work -> + expect(work.count() == 2, "split book holds the work contacts (got ${work.count()})") + expect(work.list().all { it.contact_type == ContactType.Work }, "split book is all work") + expect(book.count() == 3, "source book unchanged") + } + + // And the parent's record and error domain. + val first = Contacts.Groups.firstOfType(book, ContactType.Personal) + expect(first.first_name == "Cy", "first personal contact (got $first)") + val none = thrownBy { Contacts.Groups.firstOfType(book, ContactType.Other) } + expect(none is ContactsException.NotFound, "firstOfType(Other) throws NotFound (got $none)") + } + ContactBook().use { empty -> + expect(Contacts.Groups.dominantType(empty) == ContactType.Personal, "empty book defaults to Personal") + } +} + +fun checkDirectory() { + // A sibling top-level module taking the `contacts` record by value. + val ann = Contact(1L, "Ann", "Lee", "ann@example.com", ContactType.Work) + val cy = Contact(2L, "Cy", "Ng", null, ContactType.Personal) + expect(Directory.card(ann) == Card("Lee, Ann", "AL", true), "card(ann) (got ${Directory.card(ann)})") + expect(Directory.card(cy) == Card("Ng, Cy", "CN", false), "card(cy)") + val zed = Contact(3L, "Zed", "Abe", null, ContactType.Other) + expect(Directory.sorted(listOf(cy, ann, zed)) == listOf(zed, ann, cy), "sorted by last name") + val empty = thrownBy { Directory.sorted(listOf()) } + expect(empty is DirectoryException.Empty, "sorted(empty) throws DirectoryException.Empty (got $empty)") + expect((empty as FfiException).code == 1, "Empty code 1") + expect(empty !is ContactsException, "the directory domain is its own") +} + +fun main() { + checkBook() + checkGroups() + checkDirectory() println("kotlin/contacts: OK") + expectNoLeaks { JniBridge.debug_live(it) } + println("kotlin/contacts: no leaks") } diff --git a/conformance/kotlin/events.kt b/conformance/kotlin/events.kt index 19160d93..871720ab 100644 --- a/conformance/kotlin/events.kt +++ b/conformance/kotlin/events.kt @@ -1,61 +1,32 @@ // Conformance consumer: events sample, Kotlin (JVM via JNI) target. // -// Exercises the ABI 2 callback-interface and object surface: `Subscriber` is -// a generated Kotlin interface the consumer implements (the JNI vtable -// trampolines pin the implementing object with a GlobalRef and dispatch -// through the `SubscriberJni` shims), `EventBus` is a reference-counted -// `AutoCloseable` wrapper (companion `invoke` for `new`, `close()` releases -// one strong reference, the `Cleaner` is the backstop), and `Message` is a -// data class decoded from a value buffer. Asserts that every callback method -// is invoked with the right arguments (including the bus object handed to -// `onAttached`, which is usable and independently closeable), that `Delivery` -// return values steer `publish`'s accepted count, that a Kotlin exception -// thrown inside a callback surfaces to the caller as `WeaveFFIException` with -// code -4 without unwinding through the JVM, the `suspend` async `publishLater` -// driven with `runBlocking`, the iterator-backed `messages()` drained as a -// `Sequence`, the nullable `lastMessage()`, `routeOnce` without a bus, and the -// close semantics (double `close()` is safe, use after close throws). -// Compiled in-module with the generated `WeaveFFI.kt`. +// Exercises the callback-interface and object surface: `Subscriber` is a +// generated Kotlin interface the consumer implements (the JNI vtable +// trampolines pin the implementing object with a global reference and call +// the `JniBridge` dispatch shims from any producer thread), `EventBus` is a +// reference-counted `AutoCloseable` wrapper, and `Message` is a data class +// decoded from a value buffer. Asserts that every callback method is invoked +// with the right arguments (including the bus object handed to +// `onAttached`, which is usable and independently closeable), that +// `Delivery` return values steer `publish`'s accepted count, that a Kotlin +// exception thrown inside a callback (including `route`, which the producer +// declares as returning `Result<_, ForeignError>`) surfaces to the caller as +// `FfiException` code -4, the `suspend` `publishLater`, the +// `NativeIterator`-backed `messages()`, the nullable `lastMessage()`, +// `Events.routeOnce` without a bus, close semantics, and, at exit, that +// every native resource was released. @file:JvmName("Main") -import com.weaveffi.Delivery -import com.weaveffi.EventBus -import com.weaveffi.EventsEventBusMessagesIterator -import com.weaveffi.Message -import com.weaveffi.Subscriber -import com.weaveffi.WeaveFFI -import com.weaveffi.WeaveFFIException +import events.Delivery +import events.EventBus +import events.Message +import events.Subscriber +import events.Events +import events.JniBridge +import events.FfiException import java.lang.ref.WeakReference -import kotlin.system.exitProcess import kotlinx.coroutines.runBlocking -fun expect(cond: Boolean, msg: String) { - if (!cond) { - System.err.println("assertion failed: $msg") - exitProcess(1) - } -} - -/** Run `block` and return the exception it threw, or null if it completed. */ -inline fun thrownBy(block: () -> Unit): Throwable? = - try { - block() - null - } catch (e: Throwable) { - e - } - -/** Spin the collector until `ref` clears or we give up. */ -fun collected(ref: WeakReference<*>): Boolean { - for (i in 0 until 200) { - if (ref.get() == null) return true - System.gc() - System.runFinalization() - Thread.sleep(5) - } - return ref.get() == null -} - /** * A subscriber that records every callback. `skipTopic` is routed as Skip, * `stopTopic` as AcceptAndStop, `failTopic` throws from `route`, and @@ -105,7 +76,7 @@ class RecordingSubscriber( /** Route once through a subscriber nothing else references, returning a weak handle to it. */ fun routeOnceThrowaway(): WeakReference { val sub = RecordingSubscriber("throwaway") - expect(WeaveFFI.routeOnce(sub, "t") == Delivery.Accept, "routeOnce throwaway") + expect(Events.routeOnce(sub, "t") == Delivery.Accept, "routeOnce throwaway") return WeakReference(sub) } @@ -117,7 +88,7 @@ fun subscribeThrowaway(bus: EventBus): WeakReference { return WeakReference(sub) } -fun main() { +fun run() { val bus = EventBus() expect(bus.subscriberCount() == 0L, "fresh bus has no subscribers") expect(bus.lastMessage() == null, "fresh bus has no last message") @@ -180,7 +151,7 @@ fun main() { expect(byHand == texts, "manual iteration matches") expect(thrownBy { it.next() } is NoSuchElementException, "exhausted iterator throws NoSuchElementException") // An abandoned iterator can be closed early, and closed twice. - val partial = bus.messages() as EventsEventBusMessagesIterator + val partial = bus.messages() expect(partial.next() == "nobody home", "partial iterator first element") partial.close() partial.close() @@ -191,9 +162,9 @@ fun main() { expect(last == Message(5L, "later", "async hello", listOf()), "lastMessage (got $last)") // routeOnce: a free function taking the callback interface without a bus. - expect(WeaveFFI.routeOnce(quiet, "quiet") == Delivery.Skip, "routeOnce quiet -> Skip") - expect(WeaveFFI.routeOnce(stopper, "stop") == Delivery.AcceptAndStop, "routeOnce stop -> AcceptAndStop") - expect(WeaveFFI.routeOnce(tail, "anything") == Delivery.Accept, "routeOnce -> Accept") + expect(Events.routeOnce(quiet, "quiet") == Delivery.Skip, "routeOnce quiet -> Skip") + expect(Events.routeOnce(stopper, "stop") == Delivery.AcceptAndStop, "routeOnce stop -> AcceptAndStop") + expect(Events.routeOnce(tail, "anything") == Delivery.Accept, "routeOnce -> Accept") expect(quiet.routed.last() == "quiet", "routeOnce reached route()") // The producer drops its only reference when routeOnce returns, so the // GlobalRef pinning a throwaway subscriber is released and it becomes @@ -203,7 +174,7 @@ fun main() { // The bus handed to onAttached is a distinct wrapper over the same // object: usable, and closing it leaves the caller's wrapper alive. val kept = tail.keptBus!! - expect(kept.handle == bus.handle, "onAttached bus is the same native object") + expect(kept.handle.address == bus.handle.address, "onAttached bus is the same native object") expect(kept.subscriberCount() == 3L, "kept bus reads the live subscriber count") kept.close() kept.close() @@ -213,7 +184,7 @@ fun main() { stopper.keptBus!!.close() // Foreign errors: a Kotlin exception thrown from any callback method - // surfaces to the caller as WeaveFFIException(-4) carrying the throwable's + // surfaces to the caller as FfiException(-4) carrying the throwable's // text, and the JVM keeps running. Use a fresh bus for exact counts. EventBus().use { bus2 -> val ok = RecordingSubscriber("ok") @@ -223,8 +194,8 @@ fun main() { ok.keptBus!!.close() rejecter.keptBus!!.close() val routeErr = thrownBy { bus2.publish("boom", "x", listOf()) } - expect(routeErr is WeaveFFIException, "throwing route() surfaces as WeaveFFIException (got $routeErr)") - expect((routeErr as WeaveFFIException).code == -4, "foreign error code -4 (got ${routeErr.code})") + expect(routeErr is FfiException, "throwing route() surfaces as FfiException (got $routeErr)") + expect((routeErr as FfiException).code == -4, "foreign error code -4 (got ${routeErr.code})") expect( routeErr.message?.contains("rejected boom") == true, "foreign error carries the Kotlin message (got ${routeErr.message})" @@ -240,20 +211,20 @@ fun main() { bus2.subscribe(onMsg) onMsg.keptBus!!.close() val msgErr = thrownBy { bus2.publish("any", "z", listOf()) } - expect(msgErr is WeaveFFIException && msgErr.code == -4, "throwing onMessage() surfaces as -4 (got $msgErr)") + expect(msgErr is FfiException && msgErr.code == -4, "throwing onMessage() surfaces as -4 (got $msgErr)") expect(msgErr?.message?.contains("failed on message") == true, "onMessage error text (got ${msgErr?.message})") // Throwing from onAttached aborts subscribe itself and the subscriber // isn't retained. val refuser = RecordingSubscriber("refuser", failOnAttached = true) val attachErr = thrownBy { bus2.subscribe(refuser) } - expect(attachErr is WeaveFFIException && attachErr.code == -4, "throwing onAttached() surfaces as -4 (got $attachErr)") + expect(attachErr is FfiException && attachErr.code == -4, "throwing onAttached() surfaces as -4 (got $attachErr)") expect(bus2.subscriberCount() == 3L, "refused subscriber not retained (got ${bus2.subscriberCount()})") // routeOnce also propagates the foreign error. - val onceErr = thrownBy { WeaveFFI.routeOnce(rejecter, "boom") } - expect(onceErr is WeaveFFIException && onceErr.code == -4, "routeOnce propagates -4 (got $onceErr)") - expect(WeaveFFI.routeOnce(rejecter, "calm") == Delivery.Accept, "rejecter still works for other topics") + val onceErr = thrownBy { Events.routeOnce(rejecter, "boom") } + expect(onceErr is FfiException && onceErr.code == -4, "routeOnce propagates -4 (got $onceErr)") + expect(Events.routeOnce(rejecter, "calm") == Delivery.Accept, "rejecter still works for other topics") } // clearSubscribers drops every retained subscriber (each free runs), and @@ -275,3 +246,9 @@ fun main() { println("kotlin/events: OK") } + +fun main() { + run() + expectNoLeaks { JniBridge.debug_live(it) } + println("kotlin/events: no leaks") +} diff --git a/conformance/kotlin/kvstore.kt b/conformance/kotlin/kvstore.kt index 13db3ef0..0fb527d5 100644 --- a/conformance/kotlin/kvstore.kt +++ b/conformance/kotlin/kvstore.kt @@ -2,73 +2,45 @@ // // Exercises the reference-counted interface surface: `Store` is a generated // `AutoCloseable` class (companion factory `open`, instance methods, statics -// `defaultCapacity`/`openMany`/`totalCount`, `close()` releases one strong -// reference and the `Cleaner` is the backstop), `Entry`, `Stats`, and -// `StoreInfo` are data classes decoded from value buffers (`StoreInfo` carries -// a `Store` object and a nullable one as fields), and `KvError` is a typed -// exception domain (`KvException` sealed subclasses extending the generic -// `WeaveFFIException`). Asserts the typed-error paths (IoError from -// `open("")` and `openMany`, KeyNotFound, Expired), the ABI 2 object graph -// (`share()` returns a wrapper over the same object so writes through one are -// visible through the other, `fork()` is independent, `larger(null)`, +// `defaultCapacity`/`openMany`/`totalCount`; `close()` releases one strong +// reference and the phantom-reference cleaner is the backstop), `Entry`, +// `Stats`, and `StoreInfo` are data classes decoded from value buffers +// (`StoreInfo` carries a `Store` object and a nullable one as fields), and +// `KvError` is a typed exception domain (`KvException` sealed subclasses +// extending `FfiException`). Asserts the typed-error paths (IoError from +// `open("")` and `openMany`, KeyNotFound, Expired), the object graph +// (`share()` wraps the same object, `fork()` is independent, `larger(null)`, // `describe().store`, `openMany`, `totalCount` with objects inside lists and // records), the consumer-implemented `EvictionListener` callback interface -// (every eviction arrives with the decoded `Entry` and the right -// `EvictionReason`; returning false detaches; replacing or clearing releases -// the previous listener; a Kotlin exception thrown inside `onEvict` surfaces -// to the caller as `WeaveFFIException` code -4 without crashing the JVM), -// plus the existing surface: record materialization, buffered optional -// parameters, the iterator-backed `listKeys`, the `Entry` pack/unpack round -// trip, the nested `kv.stats` module, the deprecated `legacyPut`, the suspend -// `compact` driven with `runBlocking`, and close semantics (double `close()` -// safe, use after close throws). Compiled in-module with the generated -// `WeaveFFI.kt`, so the `internal` helpers and `handle` are reachable. +// (decoded `Entry` and `EvictionReason`; returning false detaches; replacing +// or clearing releases the previous listener; a Kotlin exception thrown +// inside `onEvict` surfaces as `FfiException` code -4), record +// materialization, buffered optional parameters, the `NativeIterator`-backed +// `listKeys`, the `Entry` pack/unpack round trip, the nested `kv.stats` +// module (`Kv.StatsModule`, renamed beside the `Stats` record), the +// deprecated `legacyPut`, the cancellable suspend `compact`, close semantics, +// and, at exit, that every native resource was released. Compiled against +// the generated bindings jar with `-Xfriend-paths`, so `internal` helpers are +// reachable. @file:JvmName("Main") -import com.weaveffi.Entry -import com.weaveffi.EntryKind -import com.weaveffi.EvictionListener -import com.weaveffi.EvictionReason -import com.weaveffi.KvException -import com.weaveffi.Store -import com.weaveffi.StoreInfo -import com.weaveffi.WeaveFFI -import com.weaveffi.WeaveFFIException -import com.weaveffi.packEntry -import com.weaveffi.unpackEntry -import com.weaveffi.weaveDecode -import com.weaveffi.weaveEncode +import kvstore.Entry +import kvstore.EntryKind +import kvstore.EvictionListener +import kvstore.EvictionReason +import kvstore.KvException +import kvstore.Store +import kvstore.StoreInfo +import kvstore.JniBridge +import kvstore.Kv +import kvstore.FfiException +import kvstore.packEntry +import kvstore.unpackEntry +import kvstore.decodeBuffer +import kvstore.encodeBuffer import java.lang.ref.WeakReference -import kotlin.system.exitProcess import kotlinx.coroutines.runBlocking -fun expect(cond: Boolean, msg: String) { - if (!cond) { - System.err.println("assertion failed: $msg") - exitProcess(1) - } -} - -/** Run `block` and return the exception it threw, or null if it completed. */ -inline fun thrownBy(block: () -> Unit): Throwable? = - try { - block() - null - } catch (e: Throwable) { - e - } - -/** Spin the collector until `ref` clears or we give up. */ -fun collected(ref: WeakReference<*>): Boolean { - for (i in 0 until 200) { - if (ref.get() == null) return true - System.gc() - System.runFinalization() - Thread.sleep(5) - } - return ref.get() == null -} - /** * A listener that records every eviction. It detaches itself (returns false) * once it has seen `keepAfter` evictions, and throws when the evicted key is @@ -111,15 +83,15 @@ fun attachSelfDetaching(store: Store, payload: ByteArray): WeakReference packEntry(w, built) }) { r -> unpackEntry(r) } + val builtBack = decodeBuffer(encodeBuffer { w -> packEntry(w, built) }) { r -> unpackEntry(r) } expect(builtBack.tags == listOf("hot", "fast"), "built tags") expect(builtBack.metadata == mapOf("source" to "test", "env" to "prod"), "built metadata") expect(builtBack.value.contentEquals(payload), "built value bytes") expect(builtBack.expires_at == null, "built expires_at null") val empty = Entry(8L, "empty", payload, 1L, 99L, listOf(), emptyMap()) - val emptyBack = weaveDecode(weaveEncode { w -> packEntry(w, empty) }) { r -> unpackEntry(r) } + val emptyBack = decodeBuffer(encodeBuffer { w -> packEntry(w, empty) }) { r -> unpackEntry(r) } expect(emptyBack.tags.isEmpty() && emptyBack.metadata.isEmpty(), "empty collections") expect(emptyBack.expires_at == 99L, "empty expires_at present") // kv.stats submodule: free function taking the interface (borrowed // handle) and returning a buffered record. - val stats = WeaveFFI.getStats(store) + val stats = Kv.StatsModule.getStats(store) expect(stats.total_entries == 2L, "stats total entries == 2") expect(stats.total_bytes == 6L, "stats total bytes == 6 (got ${stats.total_bytes})") expect(stats.expired_entries == 0L, "stats expired == 0") @@ -253,7 +225,7 @@ fun main() { expect(store.count() == 0L, "store empty after self-detach test") // A Kotlin exception thrown from onEvict surfaces to the caller as - // WeaveFFIException(-4), not as a KvException, even though `delete` + // FfiException(-4), not as a KvException, even though `delete` // has the KvError domain; the JVM keeps running, the entry is gone // (the producer removes before notifying), and the listener stays // attached. @@ -261,9 +233,9 @@ fun main() { store.setEvictionListener(thrower) expect(store.put("boom", payload, EntryKind.Persistent, null), "put boom") val foreign = thrownBy { store.delete("boom") } - expect(foreign is WeaveFFIException, "throwing onEvict surfaces as WeaveFFIException (got $foreign)") + expect(foreign is FfiException, "throwing onEvict surfaces as FfiException (got $foreign)") expect(foreign !is KvException, "foreign error is not a domain error") - expect((foreign as WeaveFFIException).code == -4, "foreign error code -4 (got ${foreign.code})") + expect((foreign as FfiException).code == -4, "foreign error code -4 (got ${foreign.code})") expect(foreign.message?.contains("refused boom") == true, "foreign message text (got ${foreign.message})") expect(store.count() == 0L, "boom was removed before the listener ran") expect(store.put("calm", payload, EntryKind.Persistent, null), "store usable after foreign error") @@ -276,7 +248,7 @@ fun main() { expect(store.put("one", payload, EntryKind.Persistent, null), "put one") val shared = store.share() expect(shared !== store, "share() returns a distinct wrapper") - expect(shared.handle == store.handle, "share() wraps the same native object") + expect(shared.handle.address == store.handle.address, "share() wraps the same native object") expect(shared.count() == 1L, "shared sees existing entries") expect(shared.put("two", payload, EntryKind.Persistent, null), "put through shared") expect(store.count() == 2L, "write through shared is visible through the original") @@ -287,7 +259,7 @@ fun main() { expect(store.count() == 2L, "original still alive after closing the shared wrapper") val forked = store.fork() - expect(forked.handle != store.handle, "fork() is a new object") + expect(forked.handle.address != store.handle.address, "fork() is a new object") expect(forked.count() == 2L, "fork copies live entries") expect(forked.put("three", payload, EntryKind.Persistent, null), "put into fork") expect(forked.count() == 3L && store.count() == 2L, "fork is independent") @@ -297,15 +269,15 @@ fun main() { Store.open("/tmp/empty").use { emptyStore -> expect(emptyStore.larger(null) == null, "empty.larger(null) is null") val own = store.larger(null) - expect(own != null && own.handle == store.handle, "store.larger(null) is the store itself") + expect(own != null && own.handle.address == store.handle.address, "store.larger(null) is the store itself") own!!.close() val bigger = store.larger(forked) - expect(bigger != null && bigger.handle == forked.handle, "store.larger(fork) is the fork") + expect(bigger != null && bigger.handle.address == forked.handle.address, "store.larger(fork) is the fork") bigger!!.close() val self = forked.larger(emptyStore) - expect(self != null && self.handle == forked.handle, "fork.larger(empty) is the fork") + expect(self != null && self.handle.address == forked.handle.address, "fork.larger(empty) is the fork") self!!.close() - expect(store.larger(emptyStore)!!.use { it.handle } == store.handle, "store.larger(empty) is the store") + expect(store.larger(emptyStore)!!.use { it.handle.address } == store.handle.address, "store.larger(empty) is the store") } // Objects inside a record, with the optional absent and present. @@ -313,17 +285,17 @@ fun main() { expect(info.label == "primary", "describe label") expect(info.count == 2L, "describe count (got ${info.count})") expect(info.mirror == null, "describe mirror absent") - expect(info.store.handle == store.handle, "describe().store is the described object") + expect(info.store.handle.address == store.handle.address, "describe().store is the described object") expect(info.store.count() == 2L, "describe().store is usable") val mirrored = store.describe("mirrored", forked) val mirror = mirrored.mirror - expect(mirror != null && mirror.handle == forked.handle, "describe mirror present") + expect(mirror != null && mirror.handle.address == forked.handle.address, "describe mirror present") expect(mirror!!.count() == 3L, "mirror usable") // A list of objects as a return, and the typed error from the static. val many = Store.openMany(listOf("/a", "/b", "/c")) expect(many.size == 3, "openMany returns 3 stores (got ${many.size})") - expect(many.map { it.handle }.toSet().size == 3, "openMany stores are distinct") + expect(many.map { it.handle.address }.toSet().size == 3, "openMany stores are distinct") expect(many[0].put("m", payload, EntryKind.Persistent, null), "put into openMany[0]") expect(many[1].put("n", payload, EntryKind.Persistent, null), "put into openMany[1]") expect(many.map { it.count() } == listOf(1L, 1L, 0L), "openMany counts") @@ -338,7 +310,7 @@ fun main() { expect(Store.totalCount(listOf(store, forked), info) == 7L, "totalCount with extra record") expect(Store.totalCount(listOf(forked, forked), mirrored) == 8L, "totalCount repeats and mirror record") expect(store.count() == 2L && forked.count() == 3L, "stores still alive after being encoded") - expect(many.all { it.handle != 0L }, "openMany wrappers still alive after being encoded") + expect(many.all { !it.handle.isClosed }, "openMany wrappers still alive after being encoded") // Release every wrapper we minted; the originals keep working. info.store.close() @@ -363,7 +335,13 @@ fun main() { closed.close() expect(thrownBy { closed.count() } is IllegalStateException, "use after close throws") expect(thrownBy { closed.share() } is IllegalStateException, "share after close throws") - expect(thrownBy { WeaveFFI.getStats(closed) } is IllegalStateException, "borrowing a closed store throws") + expect(thrownBy { Kv.StatsModule.getStats(closed) } is IllegalStateException, "borrowing a closed store throws") println("kotlin/kvstore: OK") } + +fun main() { + run() + expectNoLeaks { JniBridge.debug_live(it) } + println("kotlin/kvstore: no leaks") +} diff --git a/conformance/kotlin/run.sh b/conformance/kotlin/run.sh new file mode 100755 index 00000000..666aa74d --- /dev/null +++ b/conformance/kotlin/run.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Conformance lanes for the kotlin target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane compiles and runs one +# consumer against one sample and must exit 0. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# Kotlin (JVM): build the generated JNI shim with its own CMakeLists.txt +# against the producer in $LIBDIR, compile the generated Kotlin sources on +# their own into a bindings jar, compile the consumer (plus common.kt) +# against that jar (`-Xfriend-paths` makes its `internal` helpers visible), +# then run on the JVM with the shim on java.library.path and the producer +# named through the generated loader's {PREFIX}_LIBRARY override. +kotlin_consumer() { + local sample="$1" src="$2" + local gen="$GENROOT/$sample/kotlin" + local b="$OUT/kotlin-$sample" + local lib=${sample//-/_} + local dir_var + dir_var="$(echo "$lib" | tr '[:lower:]' '[:upper:]')_LIBRARY_DIR" + rm -rf "$b"; mkdir -p "$b" + + local kc real coro + kc=$(command -v kotlinc) || { echo "kotlinc not found" >&2; return 1; } + real=$(readlink -f "$kc" 2>/dev/null || echo "$kc") + coro=$(ls "$(dirname "$real")/../libexec/lib/kotlinx-coroutines-core-jvm.jar" \ + "$(dirname "$real")/../lib/kotlinx-coroutines-core-jvm.jar" 2>/dev/null | head -1) + [ -n "$coro" ] || { echo "kotlinx-coroutines-core-jvm.jar not found" >&2; return 1; } + + cmake -S "$gen/src/main/cpp" -B "$b/shim" -D"$dir_var=$LIBDIR" >"$b/cmake.log" 2>&1 \ + && cmake --build "$b/shim" >>"$b/cmake.log" 2>&1 \ + || { cat "$b/cmake.log" >&2; echo "JNI shim build failed" >&2; return 1; } + find "$gen/src/main/kotlin" -name '*.kt' > "$b/sources.txt" + kotlinc -cp "$coro" -d "$b/bindings.jar" @"$b/sources.txt" 2>"$b/kotlinc.log" \ + || { cat "$b/kotlinc.log" >&2; echo "kotlinc failed on the bindings" >&2; return 1; } + kotlinc "$ROOT/conformance/kotlin/common.kt" "$ROOT/conformance/kotlin/$src" \ + -cp "$b/bindings.jar:$coro" -Xfriend-paths="$b/bindings.jar" -d "$b/app.jar" 2>"$b/kotlinc.log" \ + || { cat "$b/kotlinc.log" >&2; echo "kotlinc failed on the consumer" >&2; return 1; } + local kstdlib + kstdlib=$(ls "$(dirname "$real")/../libexec/lib/kotlin-stdlib.jar" \ + "$(dirname "$real")/../lib/kotlin-stdlib.jar" 2>/dev/null | head -1) + env "$(library_env "$sample")=$(sample_lib "$sample")" \ + java -Djava.library.path="$b/shim" -cp "$b/app.jar:$b/bindings.jar:$coro:$kstdlib" Main +} + +kotlin_contacts() { kotlin_consumer contacts contacts.kt; } + +kotlin_events() { kotlin_consumer events events.kt; } + +kotlin_kvstore() { kotlin_consumer kvstore kvstore.kt; } + +kotlin_async_demo() { kotlin_consumer async-demo async_demo.kt; } + +kotlin_codec() { kotlin_consumer codec codec.kt; } + +lane kotlin-contacts kotlin_contacts +lane kotlin-events kotlin_events +lane kotlin-kvstore kotlin_kvstore +lane kotlin-async-demo kotlin_async_demo +lane kotlin-codec kotlin_codec + +finish_lanes diff --git a/conformance/kotlin/shapes.kt b/conformance/kotlin/shapes.kt deleted file mode 100644 index 7aaa9462..00000000 --- a/conformance/kotlin/shapes.kt +++ /dev/null @@ -1,78 +0,0 @@ -// Conformance consumer: shapes sample, Android/Kotlin (JNI) target. -// -// Exercises the rich (algebraic) enum surface as value types: `Shape` is a -// generated sealed class (`Empty` object, `Circle`/`Rectangle`/`Labeled` data -// classes) that crosses JNI as a value buffer packed on the Kotlin side, so -// there are no native handles and nothing to close. Drives every variant -// through `describe` (buffered rich enum in, string out) and `scale` (buffered -// in and out, decoded back into the sealed hierarchy), covering unit -// (`Empty`), f64 (`Circle`), two f32 (`Rectangle`), and string + u8 -// (`Labeled`) payloads, plus the C-style `Channel` enum's plain discriminants -// and the expanded numerics (`sumBytes`: bytes in, u64 out). Mirrors the C -// consumer's assertions. Compiled in-module with the generated `WeaveFFI.kt`. -@file:JvmName("Main") - -import com.weaveffi.Channel -import com.weaveffi.Shape -import com.weaveffi.WeaveFFI -import kotlin.math.abs -import kotlin.system.exitProcess - -fun expect(cond: Boolean, msg: String) { - if (!cond) { - System.err.println("assertion failed: $msg") - exitProcess(1) - } -} - -fun main() { - // describe dispatches on the active variant: each call packs the sealed - // value into a buffer, and the producer formats it. - expect(WeaveFFI.describe(Shape.Empty) == "empty", "describe empty") - - val circle = Shape.Circle(2.5) - val circleDesc = WeaveFFI.describe(circle) - expect(circleDesc == "circle(r=2.5)", "describe circle (got $circleDesc)") - - val rect = Shape.Rectangle(3.0f, 4.0f) - val rectDesc = WeaveFFI.describe(rect) - expect(rectDesc == "rectangle(3x4)", "describe rectangle (got $rectDesc)") - - val labeled = Shape.Labeled("hex", 6.toByte()) - val labeledDesc = WeaveFFI.describe(labeled) - expect(labeledDesc == "labeled(hex x6)", "describe labeled (got $labeledDesc)") - - // scale: rich enum in and out; the return buffer decodes back into the - // sealed hierarchy with the payload fields intact. - val big = WeaveFFI.scale(circle, 4.0) - expect(big is Shape.Circle, "scaled circle variant (got $big)") - val bigRadius = (big as Shape.Circle).radius - expect(abs(bigRadius - 10.0) < 1e-9, "scaled radius (got $bigRadius)") - - val bigRect = WeaveFFI.scale(rect, 2.0) - expect(bigRect is Shape.Rectangle, "scaled rectangle variant (got $bigRect)") - val r = bigRect as Shape.Rectangle - expect(abs(r.width - 6.0f) < 1e-6f, "scaled width (got ${r.width})") - expect(abs(r.height - 8.0f) < 1e-6f, "scaled height (got ${r.height})") - - // The string + u8 payload round-trips unchanged (scale leaves Labeled - // alone), and data-class equality compares the decoded fields. - val sameLabeled = WeaveFFI.scale(labeled, 2.0) - expect(sameLabeled == labeled, "labeled round-trips (got $sameLabeled)") - - // The unit variant round-trips to the shared singleton. - expect(WeaveFFI.scale(Shape.Empty, 2.0) == Shape.Empty, "empty round-trips") - - // A C-style enum keeps its plain int discriminants. - expect(Channel.Green.value == 1, "Channel.Green == 1") - expect(Channel.fromValue(2) == Channel.Blue, "Channel.fromValue(2) is Blue") - - // numerics: bytes in, u64 out. 250 wraps to a signed -6 byte but the JNI - // shim reinterprets it as uint8_t, so the producer sums 250 * 4 == 1000. - val total = WeaveFFI.sumBytes( - byteArrayOf(250.toByte(), 250.toByte(), 250.toByte(), 250.toByte()) - ) - expect(total == 1000L, "sumBytes (got $total)") - - println("kotlin/shapes: OK") -} diff --git a/conformance/lib.sh b/conformance/lib.sh new file mode 100644 index 00000000..d02712e0 --- /dev/null +++ b/conformance/lib.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Shared environment and helpers for the per-language conformance scripts. +# +# conformance/run.sh exports ROOT, TARGET_DIR, LIBDIR, GENROOT, OUT, EXT, and +# RESULTS before invoking each conformance//run.sh; a language script can +# also be run directly after a full run has built the producers and generated +# the bindings once. +# +# Each lane runs under a timeout (LANE_TIMEOUT seconds, default 300) so one +# deadlocked consumer fails its lane instead of hanging the whole run. + +ROOT=${ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)} +if [ -z "${TARGET_DIR:-}" ]; then + TARGET_DIR=$(cd "$ROOT" && cargo metadata --format-version 1 --no-deps 2>/dev/null \ + | tr ',' '\n' | grep '"target_directory"' | head -1 \ + | sed 's/.*"target_directory":"//; s/"$//') + TARGET_DIR=${TARGET_DIR:-$ROOT/target} +fi +LIBDIR=${LIBDIR:-$TARGET_DIR/debug} +GENROOT=${GENROOT:-$TARGET_DIR/conformance-gen} +OUT=${OUT:-$TARGET_DIR/conformance-build} +RESULTS=${RESULTS:-$OUT/results.txt} +LANE_TIMEOUT=${LANE_TIMEOUT:-300} +mkdir -p "$OUT" +cd "$ROOT" || exit 1 + +case "$(uname -s)" in + Darwin) EXT=dylib; export DYLD_LIBRARY_PATH="$LIBDIR:${DYLD_LIBRARY_PATH:-}" ;; + MINGW*|MSYS*|CYGWIN*) EXT=dll; export PATH="$LIBDIR:$PATH" ;; + *) EXT=so; export LD_LIBRARY_PATH="$LIBDIR:${LD_LIBRARY_PATH:-}" ;; +esac + +export ROOT TARGET_DIR LIBDIR GENROOT OUT RESULTS LANE_TIMEOUT EXT + +LANES_FAILED=0 + +# Is lane `$1` selected by ONLY / SKIP (comma-separated lane names or +# language names)? +selected() { + local t=$1 lang=${1%%-*} + if [ -n "${ONLY:-}" ]; then + [[ ",$ONLY," == *",$t,"* || ",$ONLY," == *",$lang,"* ]] + else + [[ ",${SKIP:-}," != *",$t,"* && ",${SKIP:-}," != *",$lang,"* ]] + fi +} + +# Run a command with a wall-clock limit (portable: macOS has no `timeout`). +with_timeout() { + local secs=$1 + shift + perl -e 'my $s = shift; $SIG{ALRM} = sub { kill "TERM", -$$; exit 124 }; setpgrp(0, 0); alarm $s; my $r = system(@ARGV); exit($r == -1 ? 127 : $r >> 8)' "$secs" "$@" +} + +# lane : run one consumer lane, record OK/FAIL in $RESULTS. +lane() { + local name=$1 fn=$2 + if ! selected "$name"; then + echo "[SKIP] $name" + return 0 + fi + echo "==> $name" + export -f "$fn" 2>/dev/null + with_timeout "$LANE_TIMEOUT" bash -c "$(declare -f); $fn" + local status=$? + if [ $status -eq 0 ]; then + echo "[OK] $name" + echo "OK $name" >> "$RESULTS" + else + [ $status -eq 124 ] && echo "lane timed out after ${LANE_TIMEOUT}s" >&2 + echo "[FAIL] $name" >&2 + echo "FAIL $name" >> "$RESULTS" + LANES_FAILED=$((LANES_FAILED + 1)) + fi +} + +finish_lanes() { + [ "$LANES_FAILED" -eq 0 ] +} + +# The platform file name of a sample's cdylib. Cargo maps `-` in crate names +# to `_` in artifact names (async-demo -> libasync_demo). +sample_lib() { + local n=${1//-/_} + case "$EXT" in + dylib) echo "$LIBDIR/lib$n.dylib" ;; + dll) echo "$LIBDIR/$n.dll" ;; + *) echo "$LIBDIR/lib$n.so" ;; + esac +} + +# The library-path override environment variable a generated loader honors: +# `{PREFIX}_LIBRARY`, where the prefix is the crate's library name. +library_env() { + local n=${1//-/_} + echo "$(echo "$n" | tr '[:lower:]' '[:upper:]')_LIBRARY" +} diff --git a/conformance/node/async_demo.js b/conformance/node/async_demo.js deleted file mode 100644 index 1810f53c..00000000 --- a/conformance/node/async_demo.js +++ /dev/null @@ -1,74 +0,0 @@ -// Conformance consumer: async-demo sample, Node (N-API) target. -// -// Drives the Promise-backed async surface end to end: `runTask` resolving -// with a TaskResult object decoded from the completion callback's value -// buffer, the typed rejection (InvalidNameError extending TaskError extending -// WeaveFFIError) for an empty name, the buffered list-of-records round trip -// through `runBatch`, the direct-scalar `runNTasks`, the sync `cancelTask`, -// and `activeCallbacks` settling to zero once every task body has completed. -// The harness passes the built addon via WV_ADDON; the generated loader -// honors WEAVEFFI_ADDON, and the generated index.js lives in the sibling -// conformance-gen tree. - -'use strict'; - -const path = require('path'); - -const ADDON = path.resolve(process.env.WV_ADDON); -process.env.WEAVEFFI_ADDON = ADDON; -// WV_ADDON = /conformance-build/node-async-demo/build/Release/index.node; -// the generated files sit at /conformance-gen/async-demo/node/. -const wv = require( - path.resolve(ADDON, '../../../../..', 'conformance-gen', 'async-demo', 'node', 'index.js') -); - -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - process.exit(1); - } -} - -(async () => { - // Async record return: the Promise resolves with a plain TaskResult object. - const result = await wv.runTask('alpha'); - expect(typeof result.id === 'bigint' && result.id > 0n, 'runTask assigns a BigInt id'); - expect(result.value === 'completed: alpha', `runTask value (got ${result.value})`); - expect(result.success === true, 'runTask success flag'); - - // Typed async rejection: the empty name settles with InvalidNameError. - try { - await wv.runTask(''); - expect(false, 'expected InvalidNameError for empty name'); - } catch (e) { - expect(e instanceof wv.InvalidNameError, `typed subclass (got ${e.constructor.name})`); - expect(e instanceof wv.TaskError, 'subclass of TaskError'); - expect(e instanceof wv.WeaveFFIError, 'subclass of the brand error'); - expect(e.code === 1, `InvalidName carries code 1 (got ${e.code})`); - } - - // Buffered list-of-records both ways. - const batch = await wv.runBatch(['a', 'b', 'c']); - expect( - batch.map((r) => r.value).join('|') === 'completed: a|completed: b|completed: c', - 'runBatch values' - ); - expect(batch.every((r) => r.success), 'runBatch success flags'); - - // Direct scalar through the async callback. - expect((await wv.runNTasks(7)) === 7, 'runNTasks echoes n'); - - // Sync functions beside the async ones. - expect(wv.cancelTask(1) === false, 'cancelTask reports not cancelled'); - - // Every spawned task body has completed by the time its callback fires. - // i64 returns are BigInt. - const active = wv.activeCallbacks(); - expect(typeof active === 'bigint', `activeCallbacks is a BigInt (got ${typeof active})`); - expect(active === 0n, `activeCallbacks settles to zero (got ${active})`); - - console.log('node async-demo conformance: OK'); -})().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/conformance/node/async_demo.mjs b/conformance/node/async_demo.mjs new file mode 100644 index 00000000..718b9235 --- /dev/null +++ b/conformance/node/async_demo.mjs @@ -0,0 +1,62 @@ +// Conformance consumer: async-demo sample (node and wasm lanes). +// +// Promise-returning async functions: a record result, a typed rejection, a +// list of records, a direct result, and the cancellable `wait`, which only +// completes early when cancelled: aborting its `AbortSignal` cancels the +// native token and the promise rejects with `CancelledError` (code -5). On +// WebAssembly every async call completes before its launcher returns, so +// only an already-aborted signal can cancel one there. + +import { expect, finish, isWasm, load, rejects, same } from './harness.mjs'; + +const api = await load('async-demo'); +const { tasks, AsyncDemoError, CancelledError } = api; + +async function main() { + same(await tasks.runTask('alpha'), { id: 1n, value: 'completed: alpha', success: true }, 'runTask'); + await rejects( + tasks.runTask(''), + (e) => e instanceof tasks.InvalidNameError && e instanceof tasks.TaskError && e instanceof AsyncDemoError && e.code === 1, + 'a typed async rejection', + ); + same( + (await tasks.runBatch(['a', 'b', 'c'])).map((r) => r.value), + ['completed: a', 'completed: b', 'completed: c'], + 'runBatch', + ); + expect((await tasks.runNTasks(7)) === 7, 'runNTasks'); + const many = await Promise.all(Array.from({ length: 50 }, (_, i) => tasks.runNTasks(i))); + same(many, Array.from({ length: 50 }, (_, i) => i), 'concurrent calls settle independently'); + await rejects(tasks.runNTasks('7'), (e) => e instanceof TypeError, 'async arguments are type-checked'); + + // Cancellation. The sample's timer reads the clock, which traps on + // wasm32-unknown-unknown, so there only a call cancelled before it starts + // can run. + if (!isWasm(api)) { + expect((await tasks.wait(5n)) === 5n, 'wait completes after its timeout'); + expect((await tasks.wait(5n, {})) === 5n, 'wait without a signal'); + } + const aborted = new AbortController(); + aborted.abort(); + await rejects( + tasks.wait(60_000n, { signal: aborted.signal }), + (e) => e instanceof CancelledError && e instanceof AsyncDemoError && e.code === -5, + 'an already-aborted signal cancels the call', + ); + if (!isWasm(api)) { + const controller = new AbortController(); + const started = Date.now(); + const pending = tasks.wait(60_000n, { signal: controller.signal }); + setTimeout(() => controller.abort(), 20); + await rejects(pending, (e) => e instanceof CancelledError && e.code === -5, 'aborting mid-call cancels it'); + expect(Date.now() - started < 10_000, 'cancellation completes promptly'); + // A signal aborted after the call completed is harmless. + const late = new AbortController(); + expect((await tasks.wait(1n, { signal: late.signal })) === 1n, 'completes before the abort'); + late.abort(); + } + expect(tasks.activeCallbacks() === 0n, 'every task body has finished'); +} + +await main(); +await finish(api, 'async-demo'); diff --git a/conformance/node/codec.js b/conformance/node/codec.js deleted file mode 100644 index a5758cdf..00000000 --- a/conformance/node/codec.js +++ /dev/null @@ -1,351 +0,0 @@ -// Conformance consumer: codec sample, Node (N-API) target. -// -// Checks the generated value-buffer codec (index.js) against the producer's -// round-trip oracle in both directions: `sample*` fixtures decoded field by -// field against concrete expected values (producer encodes, consumer -// decodes), `verify*` accepting the same fixture back (consumer encodes, -// producer decodes), `roundtrip*` returning consumer-built values with edge -// cases (empty strings, lists, and maps; unicode; i64/u64 extremes as BigInt; -// NaN, +/-Infinity, and -0 doubles; every Shape variant), the typed -// MismatchError, and objects inside buffers through `Holder` (a Token field, -// an optional Token, a list of Tokens; `primaryOf` returning the same object; -// `samePrimary`; every wrapper released with an idempotent `close()`). The -// harness passes the built addon via WV_ADDON; the generated loader honors -// WEAVEFFI_ADDON. - -'use strict'; - -const assert = require('assert'); -const path = require('path'); - -const ADDON = path.resolve(process.env.WV_ADDON); -process.env.WEAVEFFI_ADDON = ADDON; -// WV_ADDON = /conformance-build/node-codec/build/Release/index.node; -// the generated files sit at /conformance-gen/codec/node/. -const wv = require( - path.resolve(ADDON, '../../../../..', 'conformance-gen', 'codec', 'node', 'index.js') -); - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} -// Structural equality with SameValue semantics for primitives: BigInt by -// value, NaN equal to NaN, -0 distinct from +0, Buffer by contents. -function same(actual, expected, msg) { - try { - assert.deepStrictEqual(actual, expected); - } catch (e) { - console.error('assertion failed: ' + msg); - console.error(e.message); - failures++; - } -} -function throwsMismatch(fn, msg) { - try { - fn(); - expect(false, msg + ': expected MismatchError'); - } catch (e) { - expect(e instanceof wv.MismatchError, msg + ': MismatchError instance (got ' + (e && e.constructor.name) + ')'); - expect(e instanceof wv.CodecError, msg + ': extends CodecError'); - expect(e instanceof wv.WeaveFFIError, msg + ': extends WeaveFFIError'); - expect(e.code === 1 && wv.MismatchError.CODE === 1, msg + ': code 1'); - } -} - -// The C-style enum is exported as a frozen object (forward and reverse -// mappings), the runtime value `types.d.ts` declares as `export enum`. -const Color = wv.Color; -expect(Color.Red === 0 && Color.Green === 1 && Color.Blue === 7, 'Color values'); -expect(Color[7] === 'Blue', 'Color reverse mapping'); - -// --- Scalars ---------------------------------------------------------------- -const canonicalScalars = { - i8_value: -8, - u8_value: 200, - i16_value: -16000, - u16_value: 60000, - i32_value: -2000000000, - u32_value: 4000000000, - i64_value: -9007199254740993n, - u64_value: 18446744073709551615n, - f32_value: 1.5, - f64_value: -2.25e100, - flag: true, - color: Color.Blue, -}; -const scalars = wv.sampleScalars(); -same(scalars, canonicalScalars, 'sampleScalars matches the canonical fixture'); -expect(typeof scalars.i64_value === 'bigint', 'i64 field is a BigInt'); -expect(typeof scalars.u64_value === 'bigint', 'u64 field is a BigInt'); -expect(typeof scalars.i32_value === 'number', 'i32 field is a number'); -expect(scalars.i64_value === -9007199254740993n, 'i64 beyond 2^53 is exact'); -expect(wv.verifyScalars(scalars) === true, 'verifyScalars accepts the decoded fixture'); -expect(wv.verifyScalars(canonicalScalars) === true, 'verifyScalars accepts a hand-built fixture'); -same(wv.roundtripScalars(scalars), canonicalScalars, 'roundtripScalars preserves every field'); -throwsMismatch(() => wv.verifyScalars({ ...canonicalScalars, u64_value: 18446744073709551614n }), 'u64 off by one'); -throwsMismatch(() => wv.verifyScalars({ ...canonicalScalars, flag: false }), 'flag flipped'); -throwsMismatch(() => wv.verifyScalars({ ...canonicalScalars, color: Color.Red }), 'color changed'); - -// Consumer-built extremes, including doubles JS can express but JSON can't. -const extremes = { - i8_value: -128, - u8_value: 255, - i16_value: -32768, - u16_value: 65535, - i32_value: -2147483648, - u32_value: 4294967295, - i64_value: -9223372036854775808n, - u64_value: 0n, - f32_value: -0, - f64_value: NaN, - flag: false, - color: Color.Red, -}; -const extremesBack = wv.roundtripScalars(extremes); -same(extremesBack, extremes, 'extreme scalars round-trip'); -expect(Object.is(extremesBack.f32_value, -0), 'f32 -0 keeps its sign'); -expect(Number.isNaN(extremesBack.f64_value), 'f64 NaN survives'); -const maxes = { - ...extremes, - i8_value: 127, - i16_value: 32767, - i32_value: 2147483647, - i64_value: 9223372036854775807n, - u64_value: 18446744073709551615n, - f32_value: Infinity, - f64_value: -Infinity, - color: Color.Green, -}; -same(wv.roundtripScalars(maxes), maxes, 'max scalars round-trip'); -// A number for a 64-bit field is accepted by the encoder (BigInt(v)). -same(wv.roundtripScalars({ ...extremes, i64_value: 42, u64_value: 7 }), { ...extremes, i64_value: 42n, u64_value: 7n }, 'number literals widen to BigInt on the way back'); - -// Direct-family 64-bit params and returns are BigInt end to end. -for (const v of [0n, 1n, -1n, 9007199254740993n, -9007199254740993n, 9223372036854775807n, -9223372036854775808n]) { - const back = wv.roundtripI64(v); - expect(typeof back === 'bigint' && back === v, `roundtripI64(${v}) (got ${back})`); -} -expect(wv.roundtripI64(5) === 5n, 'roundtripI64 accepts a number and returns a BigInt'); -for (const v of [0n, 1n, 9223372036854775808n, 18446744073709551615n]) { - const back = wv.roundtripU64(v); - expect(typeof back === 'bigint' && back === v, `roundtripU64(${v}) (got ${back})`); -} -for (const bad of [9223372036854775808n, -9223372036854775809n]) { - try { - wv.roundtripI64(bad); - expect(false, `roundtripI64(${bad}) should reject an out-of-range BigInt`); - } catch (e) { - expect(e instanceof RangeError, `out-of-range i64 is a RangeError (got ${e && e.constructor.name})`); - } -} -try { - wv.roundtripU64(-1n); - expect(false, 'roundtripU64(-1n) should reject'); -} catch (e) { - expect(e instanceof RangeError, `negative u64 is a RangeError (got ${e && e.constructor.name})`); -} - -// Doubles by value. -for (const v of [0, -0, 1.5, -2.25e100, 5e-324, Number.MAX_VALUE, Infinity, -Infinity]) { - expect(Object.is(wv.roundtripF64(v), v), `roundtripF64(${v})`); -} -expect(Number.isNaN(wv.roundtripF64(NaN)), 'roundtripF64(NaN)'); -expect(wv.roundtripBool(true) === true && wv.roundtripBool(false) === false, 'roundtripBool'); -expect(wv.roundtripColor(Color.Blue) === 7, 'roundtripColor(Blue) == 7'); -expect(wv.roundtripColor(Color.Red) === 0, 'roundtripColor(Red) == 0'); - -// Strings and bytes at the top level. -for (const s of ['', 'ascii', 'héllo wörld ✓', '日本語', 'emoji 🎉 pair', 'a'.repeat(70000)]) { - expect(wv.roundtripString(s) === s, `roundtripString(${JSON.stringify(s)})`); -} -const allBytes = Buffer.from(Array.from({ length: 256 }, (_, i) => i)); -expect(wv.roundtripBytes(allBytes).equals(allBytes), 'roundtripBytes(0..255)'); -const emptyBytes = wv.roundtripBytes(Buffer.alloc(0)); -expect(Buffer.isBuffer(emptyBytes) && emptyBytes.length === 0, 'roundtripBytes(empty) is an empty Buffer'); - -// Optionals and maps at the top level. -expect(wv.roundtripOptI64(null) === null, 'roundtripOptI64(null)'); -expect(wv.roundtripOptI64(undefined) === null, 'roundtripOptI64(undefined) is null'); -expect(wv.roundtripOptI64(-9223372036854775808n) === -9223372036854775808n, 'roundtripOptI64(i64::MIN)'); -expect(wv.roundtripOptI64(0n) === 0n, 'roundtripOptI64(0n) is present'); -same(wv.roundtripMap({}), {}, 'roundtripMap({})'); -same(wv.roundtripMap({ a: 1n, '': -2n, 'ключ': 9223372036854775807n }), { a: 1n, '': -2n, 'ключ': 9223372036854775807n }, 'roundtripMap with odd keys'); - -// --- Shape (rich enum) ------------------------------------------------------ -const shapeCases = [ - { tag: 'Empty' }, - { tag: 'Circle', radius: 2.5 }, - { tag: 'Circle', radius: -0 }, - { tag: 'Rect', width: 1.0, height: 0.5 }, - { tag: 'Labeled', label: 'tag', count: 3 }, - { tag: 'Labeled', label: '', count: -2147483648 }, - { tag: 'Nested', inner: canonicalScalars, note: 'n' }, - { tag: 'Nested', inner: extremes, note: null }, -]; -for (const s of shapeCases) { - same(wv.roundtripShape(s), s, `roundtripShape(${s.tag})`); -} -same(wv.roundtripShapes(shapeCases), shapeCases, 'roundtripShapes(all variants)'); -same(wv.roundtripShapes([]), [], 'roundtripShapes([])'); -expect(wv.describeShape({ tag: 'Empty' }) === 'Empty', 'describeShape(Empty)'); -expect(wv.describeShape({ tag: 'Circle', radius: 2.5 }) === 'Circle { radius: 2.5 }', `describeShape(Circle) (got ${wv.describeShape({ tag: 'Circle', radius: 2.5 })})`); -expect(wv.describeShape({ tag: 'Labeled', label: 'tag', count: 3 }) === 'Labeled { label: "tag", count: 3 }', 'describeShape(Labeled)'); -const nestedBack = wv.roundtripShape({ tag: 'Nested', inner: extremes, note: null }); -expect(Number.isNaN(nestedBack.inner.f64_value) && nestedBack.note === null, 'nested NaN and absent note'); -try { - wv.roundtripShape({ tag: 'Hexagon' }); - expect(false, 'unknown tag should throw'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -2, `unknown tag is a runtime trap (got ${e && e.code})`); -} - -// --- Composite -------------------------------------------------------------- -const canonicalComposite = { - name: 'héllo wörld ✓', - blob: Buffer.from([0, 1, 2, 253, 254, 255]), - some_i64: -9223372036854775808n, - none_i64: null, - some_text: '', - names: ['a', '', 'ccc'], - matrix: [[1, 2, 3], [], [-4]], - empty: [], - by_name: { one: 1n, two: 2n, neg: -3n }, - by_id: { '-1': canonicalScalars, '42': { ...canonicalScalars, flag: false } }, - scalars: canonicalScalars, - shape: { tag: 'Labeled', label: 'tag', count: 3 }, - shapes: [ - { tag: 'Empty' }, - { tag: 'Circle', radius: 2.5 }, - { tag: 'Rect', width: 1.0, height: 0.5 }, - { tag: 'Labeled', label: '', count: -1 }, - { tag: 'Nested', inner: canonicalScalars, note: 'n' }, - ], - maybe_shape: { tag: 'Nested', inner: canonicalScalars, note: null }, - maybe_list: Buffer.from([9, 8]), - sparse: [true, null, false], - colors: [Color.Red, Color.Green, Color.Blue], -}; -const composite = wv.sampleComposite(); -same(composite, canonicalComposite, 'sampleComposite matches the canonical fixture'); -expect(Buffer.isBuffer(composite.blob), 'blob is a Buffer'); -expect(Buffer.isBuffer(composite.maybe_list), 'maybe_list is a Buffer'); -expect(typeof composite.some_i64 === 'bigint', 'some_i64 is a BigInt'); -expect(typeof composite.by_name.one === 'bigint', 'map values are BigInt'); -expect(composite.by_id[42].flag === false && composite.by_id[-1].flag === true, 'i32-keyed map indexes by number'); -expect(wv.verifyComposite(composite) === true, 'verifyComposite accepts the decoded fixture'); -expect(wv.verifyComposite(canonicalComposite) === true, 'verifyComposite accepts a hand-built fixture'); -same(wv.roundtripComposite(composite), canonicalComposite, 'roundtripComposite preserves every field'); -const described = wv.describeComposite(composite); -expect(typeof described === 'string' && described.startsWith('Composite {') && described.includes('name: "héllo wörld ✓"'), `describeComposite renders (got ${described.slice(0, 40)}...)`); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, sparse: [true, true, false] }), 'sparse changed'); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, none_i64: 0n }), 'absent optional made present'); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, by_name: { one: 1n, two: 2n } }), 'map entry missing'); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, name: 'hello world' }), 'unicode name changed'); - -// A consumer-built composite with edge values in every position. -const edge = { - name: '', - blob: Buffer.alloc(0), - some_i64: 9223372036854775807n, - none_i64: -1n, - some_text: null, - names: [], - matrix: [[], [-2147483648, 2147483647]], - empty: [NaN, -0, Infinity, -Infinity, 5e-324], - by_name: {}, - by_id: { '-2147483648': extremes, '0': maxes, '2147483647': canonicalScalars }, - scalars: extremes, - shape: { tag: 'Empty' }, - shapes: [], - maybe_shape: null, - maybe_list: null, - sparse: [null, null], - colors: [], -}; -const edgeBack = wv.roundtripComposite(edge); -same(edgeBack, edge, 'edge composite round-trips'); -expect(Number.isNaN(edgeBack.empty[0]) && Object.is(edgeBack.empty[1], -0), 'NaN and -0 inside a list'); -expect(edgeBack.by_id[-2147483648].i64_value === -9223372036854775808n, 'i32::MIN map key with i64::MIN value'); -const bigList = { - ...edge, - names: Array.from({ length: 1000 }, (_, i) => 'name-' + i), - blob: allBytes, - maybe_list: Buffer.from([255]), - sparse: Array.from({ length: 333 }, (_, i) => (i % 3 === 0 ? null : i % 3 === 1)), -}; -same(wv.roundtripComposite(bigList), bigList, 'large composite round-trips (writer growth)'); - -// --- Holder: objects inside buffers ----------------------------------------- -const holder = wv.makeHolder(10n, true); -expect(holder.primary instanceof wv.Token, 'holder.primary is a Token'); -expect(holder.primary.value() === 10n, `primary value 10n (got ${holder.primary.value()})`); -expect(holder.spare instanceof wv.Token && holder.spare.value() === 11n, 'spare is Token(11)'); -expect(Array.isArray(holder.many) && holder.many.length === 3, 'many has 3 tokens'); -same(holder.many.map((t) => t.value()), [12n, 13n, 14n], 'many values 12..14'); -expect(wv.sumHolder(holder) === 60n, `sumHolder == 60n (got ${wv.sumHolder(holder)})`); -// Encoding clones each handle, so the wrappers are still usable afterwards. -expect(holder.primary.value() === 10n && holder.many[2].value() === 14n, 'tokens usable after sumHolder'); -expect(wv.sumHolder(holder) === 60n, 'sumHolder again (fresh clones each call)'); - -// primaryOf returns a new wrapper over the same underlying object. -const primary = wv.primaryOf(holder); -expect(primary instanceof wv.Token && primary !== holder.primary, 'primaryOf returns a distinct wrapper'); -expect(primary.value() === 10n, 'primaryOf value 10n'); -expect(wv.samePrimary(holder, { primary, spare: null, many: [] }) === true, 'primaryOf aliases holder.primary'); -expect(wv.samePrimary(holder, holder) === true, 'samePrimary(holder, holder)'); -const other = wv.makeHolder(10n, true); -expect(wv.samePrimary(holder, other) === false, 'distinct holders with equal values are different objects'); -expect(wv.sumHolder(other) === 60n, 'other sums to 60n as well'); - -const noSpare = wv.makeHolder(-5n, false); -expect(noSpare.spare === null, 'spare absent is null'); -expect(noSpare.primary.value() === -5n, 'negative base'); -expect(wv.sumHolder(noSpare) === -5n + -3n + -2n + -1n, 'sumHolder without spare'); - -// Consumer-constructed tokens in every buffered position. -const t1 = new wv.Token(100n); -const t2 = new wv.Token(200); -const t3 = new wv.Token(-9223372036854775808n); -expect(t1.value() === 100n && t2.value() === 200n && t3.value() === -9223372036854775808n, 'constructed token values'); -const mine = { primary: t1, spare: t2, many: [t3, t1, t1] }; -expect(wv.sumHolder(mine) === 100n + 200n + -9223372036854775808n + 100n + 100n, 'sumHolder over consumer tokens'); -expect(wv.sumHolder({ primary: t1, spare: null, many: [] }) === 100n, 'sumHolder with no spare and empty many'); -expect(wv.samePrimary(mine, { primary: t1, spare: null, many: [] }) === true, 'same consumer token in two holders'); -expect(wv.samePrimary(mine, { primary: t2, spare: null, many: [] }) === false, 'different consumer tokens'); -const primaryMine = wv.primaryOf(mine); -expect(primaryMine.value() === 100n, 'primaryOf a consumer holder'); - -// Release everything: close is idempotent, and a closed token can't be -// encoded (borrow after close traps with -3) or used. -const everyToken = [ - holder.primary, holder.spare, ...holder.many, primary, - other.primary, other.spare, ...other.many, - noSpare.primary, ...noSpare.many, - t1, t2, t3, primaryMine, -]; -for (const t of everyToken) t.close(); -for (const t of everyToken) t.close(); -try { - holder.primary.value(); - expect(false, 'expected throw for use after close'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -3, `use after close is code -3 (got ${e && e.code})`); -} -try { - wv.sumHolder(holder); - expect(false, 'expected throw for encoding a closed object'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -3, `closed object in a buffer is code -3 (got ${e && e.code})`); -} -if (typeof Symbol.dispose === 'symbol') { - const disposable = new wv.Token(1n); - disposable[Symbol.dispose](); - disposable[Symbol.dispose](); -} - -if (failures > 0) process.exit(1); -console.log('node/codec: OK'); diff --git a/conformance/node/codec.mjs b/conformance/node/codec.mjs new file mode 100644 index 00000000..b5f13e86 --- /dev/null +++ b/conformance/node/codec.mjs @@ -0,0 +1,227 @@ +// Conformance consumer: codec sample (node and wasm lanes). +// +// The value-buffer codec against the producer's oracle in both directions: +// `sample*` fixtures decoded field by field, `verify*` accepting them back, +// and `roundtrip*` returning consumer-built edge cases (empty and unicode +// strings, strings with an interior NUL, 64-bit extremes, NaN, the +// infinities and -0, every Shape variant), the typed MismatchError, range +// and type checks, and objects inside buffers through `Holder`. + +import { expect, finish, load, same, throws } from './harness.mjs'; + +const api = await load('codec'); +const { codec, CodecError } = api; +const { Color } = codec; + +const bytes = (...xs) => new Uint8Array(xs); + +function mismatch(fn, msg) { + throws( + fn, + (e) => e instanceof codec.MismatchError && e instanceof codec.CodecError && e instanceof CodecError && e.code === 1, + msg, + ); +} + +const canonicalScalars = { + i8_value: -8, + u8_value: 200, + i16_value: -16000, + u16_value: 60000, + i32_value: -2000000000, + u32_value: 4000000000, + i64_value: -9007199254740993n, + u64_value: 18446744073709551615n, + f32_value: 1.5, + f64_value: -2.25e100, + flag: true, + color: Color.Blue, +}; +const extremes = { + i8_value: -128, + u8_value: 255, + i16_value: -32768, + u16_value: 65535, + i32_value: -2147483648, + u32_value: 4294967295, + i64_value: -9223372036854775808n, + u64_value: 0n, + f32_value: -0, + f64_value: NaN, + flag: false, + color: Color.Red, +}; +const maxes = { + ...extremes, + i8_value: 127, + i16_value: 32767, + i32_value: 2147483647, + i64_value: 9223372036854775807n, + u64_value: 18446744073709551615n, + f32_value: Infinity, + f64_value: -Infinity, + color: Color.Green, +}; + +function scalars() { + expect(Color.Red === 0 && Color.Blue === 7 && Color[7] === 'Blue', 'Color values'); + const s = codec.sampleScalars(); + same(s, canonicalScalars, 'sampleScalars'); + expect(codec.verifyScalars(s) && codec.verifyScalars(canonicalScalars), 'verifyScalars'); + same(codec.roundtripScalars(canonicalScalars), canonicalScalars, 'roundtripScalars'); + mismatch(() => codec.verifyScalars({ ...canonicalScalars, u64_value: 18446744073709551614n }), 'u64 off by one'); + mismatch(() => codec.verifyScalars({ ...canonicalScalars, flag: false }), 'flag flipped'); + const back = codec.roundtripScalars(extremes); + same(back, extremes, 'extreme scalars'); + expect(Object.is(back.f32_value, -0) && Number.isNaN(back.f64_value), '-0 and NaN survive'); + same(codec.roundtripScalars(maxes), maxes, 'max scalars'); + same( + codec.roundtripScalars({ ...extremes, i64_value: 42, u64_value: 7 }), + { ...extremes, i64_value: 42n, u64_value: 7n }, + 'integral numbers widen to bigint', + ); + throws(() => codec.roundtripScalars({ ...extremes, u64_value: -1n }), (e) => e instanceof RangeError, 'an out-of-range bigint field'); + throws(() => codec.roundtripScalars({ ...extremes, i32_value: '1' }), (e) => e instanceof TypeError, 'a string for a number field'); + throws(() => codec.roundtripScalars({ ...extremes, flag: 1 }), (e) => e instanceof TypeError, 'a number for a bool field'); + + for (const v of [0n, -1n, 9007199254740993n, 9223372036854775807n, -9223372036854775808n]) { + expect(codec.roundtripI64(v) === v, `roundtripI64(${v})`); + } + expect(codec.roundtripI64(5) === 5n, 'roundtripI64 accepts an integral number'); + for (const v of [0n, 9223372036854775808n, 18446744073709551615n]) { + expect(codec.roundtripU64(v) === v, `roundtripU64(${v})`); + } + for (const bad of [9223372036854775808n, -9223372036854775809n]) { + throws(() => codec.roundtripI64(bad), (e) => e instanceof RangeError, `roundtripI64(${bad})`); + } + throws(() => codec.roundtripU64(-1n), (e) => e instanceof RangeError, 'roundtripU64(-1n)'); + throws(() => codec.roundtripI64(1.5), (e) => e instanceof TypeError, 'a fraction for an i64'); + for (const v of [0, -0, 1.5, 5e-324, Number.MAX_VALUE, Infinity, -Infinity]) { + expect(Object.is(codec.roundtripF64(v), v), `roundtripF64(${v})`); + } + expect(Number.isNaN(codec.roundtripF64(NaN)), 'roundtripF64(NaN)'); + expect(codec.roundtripBool(true) === true && codec.roundtripBool(false) === false, 'roundtripBool'); + throws(() => codec.roundtripBool(1), (e) => e instanceof TypeError, 'a number for a bool'); + expect(codec.roundtripColor(Color.Blue) === 7, 'roundtripColor'); +} + +function strings() { + for (const s of ['', 'ascii', 'héllo wörld ✓', '日本語', 'emoji 🎉 pair', 'nul\0inside\0', '\0', 'a'.repeat(70000)]) { + expect(codec.roundtripString(s) === s, `roundtripString(${JSON.stringify(s.slice(0, 20))})`); + } + throws(() => codec.roundtripString(5), (e) => e instanceof TypeError, 'a number for a string'); + const all = new Uint8Array(256).map((_, i) => i); + same(codec.roundtripBytes(all), all, 'roundtripBytes(0..255)'); + const empty = codec.roundtripBytes(new Uint8Array(0)); + expect(empty instanceof Uint8Array && empty.length === 0, 'empty bytes'); + same(codec.roundtripBytes(all.subarray(10, 20)), all.slice(10, 20), 'a Uint8Array view'); + throws(() => codec.roundtripBytes([1, 2]), (e) => e instanceof TypeError, 'an array for bytes'); + expect(codec.roundtripOptI64(null) === null && codec.roundtripOptI64(undefined) === null, 'absent optional'); + expect(codec.roundtripOptI64(0n) === 0n, 'present zero'); + same(codec.roundtripMap({}), {}, 'empty map'); + same(codec.roundtripMap({ a: 1n, '': -2n, 'ключ': 9223372036854775807n }), { a: 1n, '': -2n, 'ключ': 9223372036854775807n }, 'odd keys'); + same(codec.roundtripMap(new Map([['k', 3n]])), { k: 3n }, 'a Map argument'); +} + +function shapes() { + const cases = [ + { tag: 'Empty' }, + { tag: 'Circle', radius: 2.5 }, + { tag: 'Circle', radius: -0 }, + { tag: 'Rect', width: 1, height: 0.5 }, + { tag: 'Labeled', label: 'tag', count: 3 }, + { tag: 'Labeled', label: '', count: -2147483648 }, + { tag: 'Nested', inner: canonicalScalars, note: 'n' }, + { tag: 'Nested', inner: extremes, note: null }, + ]; + for (const s of cases) same(codec.roundtripShape(s), s, `roundtripShape(${s.tag})`); + same(codec.roundtripShapes(cases), cases, 'roundtripShapes'); + expect(codec.describeShape({ tag: 'Circle', radius: 2.5 }) === 'Circle { radius: 2.5 }', 'describeShape'); + throws(() => codec.roundtripShape({ tag: 'Hexagon' }), (e) => e instanceof TypeError, 'an unknown tag'); +} + +function composites() { + const canonical = { + name: 'héllo wörld ✓', + blob: bytes(0, 1, 2, 253, 254, 255), + some_i64: -9223372036854775808n, + none_i64: null, + some_text: '', + names: ['a', '', 'ccc'], + matrix: [[1, 2, 3], [], [-4]], + empty: [], + by_name: { one: 1n, two: 2n, neg: -3n }, + by_id: { '-1': canonicalScalars, 42: { ...canonicalScalars, flag: false } }, + scalars: canonicalScalars, + shape: { tag: 'Labeled', label: 'tag', count: 3 }, + shapes: [ + { tag: 'Empty' }, + { tag: 'Circle', radius: 2.5 }, + { tag: 'Rect', width: 1, height: 0.5 }, + { tag: 'Labeled', label: '', count: -1 }, + { tag: 'Nested', inner: canonicalScalars, note: 'n' }, + ], + maybe_shape: { tag: 'Nested', inner: canonicalScalars, note: null }, + maybe_list: bytes(9, 8), + sparse: [true, null, false], + colors: [Color.Red, Color.Green, Color.Blue], + }; + const c = codec.sampleComposite(); + same(c, canonical, 'sampleComposite'); + expect(codec.verifyComposite(c) && codec.verifyComposite(canonical), 'verifyComposite'); + same(codec.roundtripComposite(c), canonical, 'roundtripComposite'); + expect(codec.describeComposite(c).startsWith('Composite {'), 'describeComposite'); + mismatch(() => codec.verifyComposite({ ...canonical, sparse: [true, true, false] }), 'sparse changed'); + mismatch(() => codec.verifyComposite({ ...canonical, by_name: { one: 1n, two: 2n } }), 'map entry missing'); + const edge = { + name: '', + blob: new Uint8Array(0), + some_i64: 9223372036854775807n, + none_i64: -1n, + some_text: null, + names: [], + matrix: [[], [-2147483648, 2147483647]], + empty: [NaN, -0, Infinity, -Infinity, 5e-324], + by_name: {}, + by_id: { '-2147483648': extremes, 0: maxes }, + scalars: extremes, + shape: { tag: 'Empty' }, + shapes: [], + maybe_shape: null, + maybe_list: null, + sparse: [null, null], + colors: [], + }; + same(codec.roundtripComposite(edge), edge, 'edge composite'); + const big = { ...edge, names: Array.from({ length: 1000 }, (_, i) => 'name-' + i), blob: new Uint8Array(70000).fill(7) }; + same(codec.roundtripComposite(big), big, 'a large composite'); +} + +function holders() { + const holder = codec.makeHolder(10n, true); + expect(holder.primary instanceof codec.Token && holder.primary.value() === 10n, 'an object field'); + expect(holder.spare.value() === 11n && holder.many.length === 3, 'optional and list object fields'); + expect(codec.sumHolder(holder) === 60n && codec.sumHolder(holder) === 60n, 'encoding clones each object'); + const primary = codec.primaryOf(holder); + expect(primary !== holder.primary && primary.value() === 10n, 'a new wrapper over the same object'); + expect(codec.samePrimary(holder, { primary, spare: null, many: [] }), 'samePrimary'); + const other = codec.makeHolder(10n, false); + expect(!codec.samePrimary(holder, other) && other.spare === null, 'distinct objects'); + const t1 = new codec.Token(100n); + const t2 = new codec.Token(-9223372036854775808n); + expect(codec.sumHolder({ primary: t1, spare: t2, many: [t1, t1] }) === 300n - 9223372036854775808n, 'consumer-built objects'); + for (const t of [holder.primary, holder.spare, ...holder.many, primary, other.primary, ...other.many, t1, t2]) { + t.close(); + } + throws(() => holder.primary.value(), (e) => e instanceof CodecError && e.code === -3, 'use after close'); + throws(() => codec.sumHolder(holder), (e) => e instanceof CodecError && e.code === -3, 'a closed object in a buffer'); + // A holder dropped without closing its objects is released by the GC. + codec.makeHolder(1n, true); +} + +scalars(); +strings(); +shapes(); +composites(); +holders(); +await finish(api, 'codec'); diff --git a/conformance/node/contacts.js b/conformance/node/contacts.js deleted file mode 100644 index bafa6745..00000000 --- a/conformance/node/contacts.js +++ /dev/null @@ -1,112 +0,0 @@ -// Conformance consumer: contacts sample, Node (N-API) target. -// -// Drives the generated wrapper layer (index.js) end to end: the ContactBook -// interface class (canonical `new` constructor, instance methods, per-object -// state), struct returns materialized as real JS objects, optional strings -// (null email), enum-as-int params, list-of-struct returns, the bool return, -// the typed error surface (NotFoundError / InvalidNameError extending -// ContactsError extending WeaveFFIError, each carrying its stable code), and -// the idempotent `close()` of the reference-counted wrapper. The -// harness passes the built addon via WV_ADDON; the generated loader honors -// WEAVEFFI_ADDON, and the generated index.js lives in the sibling -// conformance-gen tree. - -'use strict'; - -const path = require('path'); - -const ADDON = path.resolve(process.env.WV_ADDON); -process.env.WEAVEFFI_ADDON = ADDON; -// WV_ADDON = /conformance-build/node-contacts/build/Release/index.node; -// the generated files sit at /conformance-gen/contacts/node/. -const wv = require( - path.resolve(ADDON, '../../../../..', 'conformance-gen', 'contacts', 'node', 'index.js') -); - -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - process.exit(1); - } -} - -// The C-style enum is exported as a frozen object (forward and reverse -// mappings), the runtime value `types.d.ts` declares as `export enum`. -const ContactType = wv.ContactType; -expect(ContactType.Personal === 0 && ContactType.Work === 1 && ContactType.Other === 2, 'ContactType values'); -expect(ContactType[1] === 'Work', 'ContactType reverse mapping'); - -const book = new wv.ContactBook(); -expect(book instanceof wv.ContactBook, 'book is a ContactBook instance'); - -// add() returns the stored record with its assigned id (struct return). -const alice = book.add('Alice', 'Smith', 'alice@example.com', ContactType.Work); -expect(typeof alice === 'object' && alice !== null, 'add returns an object'); -expect(alice.id > 0, 'alice id positive'); - -const c = book.get(alice.id); -expect(typeof c === 'object' && c !== null, 'get returns an object'); -expect(c.first_name === 'Alice', 'first_name (got ' + c.first_name + ')'); -expect(c.last_name === 'Smith', 'last_name'); -expect(c.email === 'alice@example.com', 'email'); -expect(c.contact_type === ContactType.Work, 'contact_type'); - -// Optional string: a missing email round-trips as null. -const bob = book.add('Bob', 'Jones', null, ContactType.Personal); -const cb = book.get(bob.id); -expect(cb.email === null, 'bob email null (got ' + cb.email + ')'); -expect(cb.contact_type === ContactType.Personal, 'bob contact_type'); - -expect(book.count() === 2, 'count == 2'); - -const everyone = book.list(); -expect(Array.isArray(everyone) && everyone.length === 2, 'list length == 2'); -const names = everyone.map((p) => p.first_name).sort(); -expect(names[0] === 'Alice' && names[1] === 'Bob', 'list names'); - -expect(book.remove(alice.id) === true, 'remove returns true'); -expect(book.count() === 1, 'count == 1 after remove'); -expect(book.remove(alice.id) === false, 'second remove returns false'); - -// Each ContactBook object owns its own state. -const other = new wv.ContactBook(); -expect(other.count() === 0, 'fresh book empty'); -expect(book.count() === 1, 'first book unaffected'); - -// Typed errors: a missing id throws the NotFound class (code 2), an empty -// name the InvalidName class (code 1); both are instances of the domain -// class and the generic brand. -try { - book.get(9999); - expect(false, 'expected throw for missing contact'); -} catch (e) { - expect(e instanceof wv.NotFoundError, 'NotFoundError instance (got ' + e.name + ')'); - expect(e instanceof wv.ContactsError, 'NotFoundError extends ContactsError'); - expect(e instanceof wv.WeaveFFIError, 'NotFoundError extends WeaveFFIError'); - expect(e.code === 2, 'NotFound code == 2 (got ' + e.code + ')'); - expect(wv.NotFoundError.CODE === 2, 'NotFoundError.CODE == 2'); - expect(typeof e.errorMessage === 'string' && e.errorMessage.length > 0, 'error has message'); -} - -try { - book.add('', 'Nameless', null, ContactType.Other); - expect(false, 'expected throw for empty name'); -} catch (e) { - expect(e instanceof wv.InvalidNameError, 'InvalidNameError instance (got ' + e.name + ')'); - expect(e instanceof wv.ContactsError, 'InvalidNameError extends ContactsError'); - expect(e instanceof wv.WeaveFFIError, 'InvalidNameError extends WeaveFFIError'); - expect(e.code === 1, 'InvalidName code == 1 (got ' + e.code + ')'); -} - -// Explicit release; close() is idempotent and a closed wrapper rejects use. -other.close(); -other.close(); -book.close(); -try { - book.count(); - expect(false, 'expected throw for use after close'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -3, 'use after close is a marshalling error (got ' + e.code + ')'); -} - -console.log('node/contacts: OK'); diff --git a/conformance/node/contacts.mjs b/conformance/node/contacts.mjs new file mode 100644 index 00000000..a2fa12fb --- /dev/null +++ b/conformance/node/contacts.mjs @@ -0,0 +1,119 @@ +// Conformance consumer: contacts sample (node and wasm lanes). +// +// The ContactBook class (canonical constructor, methods, per-object state), +// records as plain objects, an optional string, a C-style enum, a list of +// records, the typed error classes (NotFoundError and InvalidNameError +// extending the `contacts.ContactsError` domain and the package root), the +// nested `contacts.groups` module taking and returning the parent's enum and +// interface and reporting the parent's error domain, the sibling +// `directory` root taking a `contacts` record, and `close()`. + +import { expect, finish, isWasm, load, same, throws } from './harness.mjs'; + +const api = await load('contacts'); +const { contacts, directory, ContactsError } = api; +const { ContactType } = contacts; + +function main() { + expect(ContactType.Personal === 0 && ContactType.Work === 1 && ContactType.Other === 2, 'ContactType values'); + expect(ContactType[1] === 'Work', 'ContactType reverse mapping'); + expect(Object.isFrozen(contacts) && Object.isFrozen(contacts.groups), 'namespaces are frozen'); + + const book = new contacts.ContactBook(); + expect(book instanceof contacts.ContactBook, 'book is a ContactBook'); + expect(api.__debugLive(0) === 1n, 'the leak counters see the live object'); + + const alice = book.add('Alice', 'Smith', 'alice@example.com', ContactType.Work); + expect(typeof alice.id === 'bigint' && alice.id > 0n, 'add returns the stored record with a bigint id'); + same( + book.get(alice.id), + { id: alice.id, first_name: 'Alice', last_name: 'Smith', email: 'alice@example.com', contact_type: ContactType.Work }, + 'get returns every field', + ); + const bob = book.add('Bob', 'Jones', null, ContactType.Personal); + expect(book.get(bob.id).email === null, 'a missing email round-trips as null'); + const carol = book.add('Carol', 'Adams', null, ContactType.Work); + expect(book.count() === 3, 'count is a number'); + same(book.list().map((c) => c.first_name).sort(), ['Alice', 'Bob', 'Carol'], 'list returns every record'); + + // The nested module uses the parent's enum and interface. + expect(contacts.groups.countOfType(book, ContactType.Work) === 2, 'countOfType(Work)'); + expect(contacts.groups.dominantType(book) === ContactType.Work, 'dominantType returns the parent enum'); + const work = contacts.groups.splitByType(book, ContactType.Work); + expect(work instanceof contacts.ContactBook, 'splitByType returns the parent interface'); + expect(work.count() === 2 && book.count() === 3, 'the split book is a new object'); + expect(contacts.groups.firstOfType(book, ContactType.Personal).first_name === 'Bob', 'firstOfType'); + throws( + () => contacts.groups.firstOfType(work, ContactType.Other), + (e) => e instanceof contacts.NotFoundError && e instanceof contacts.ContactsError && e.code === 2, + 'the nested module reports the parent domain', + ); + work.close(); + + // The sibling root takes a record declared in `contacts`. + same( + directory.card(book.get(alice.id)), + { display_name: 'Smith, Alice', initials: 'AS', has_email: true }, + 'directory.card takes a contacts record', + ); + same( + directory.sorted(book.list()).map((c) => c.last_name), + ['Adams', 'Jones', 'Smith'], + 'directory.sorted round-trips a list of contacts records', + ); + throws( + () => directory.sorted([]), + (e) => e instanceof directory.EmptyError && e instanceof directory.DirectoryError && e instanceof ContactsError && e.code === 1, + 'directory reports its own domain', + ); + expect(!(new directory.EmptyError() instanceof contacts.ContactsError), 'the two domains are unrelated'); + expect(directory.EmptyError.CODE === 1 && new directory.EmptyError().message === 'no contacts to list', 'default code message'); + + expect(book.remove(carol.id) === true && book.remove(carol.id) === false, 'remove'); + + throws( + () => book.get(9999n), + (e) => e instanceof contacts.NotFoundError && e instanceof ContactsError && e.code === 2 && e.message === 'contact not found', + 'missing contact', + ); + throws( + () => book.add('', 'Nameless', null, ContactType.Other), + (e) => e instanceof contacts.InvalidNameError && e.code === 1, + 'empty name', + ); + throws(() => book.add('X', 'Y', null, 'Work'), (e) => e instanceof TypeError, 'an enum must be a number'); + + const other = new contacts.ContactBook(); + expect(other.count() === 0 && book.count() === 2, 'each book owns its state'); + other.close(); + other.close(); + book.close(); + throws(() => book.count(), (e) => e instanceof ContactsError && e.code === -3, 'use after close'); +} + +// On Node.js the addon keeps its state per environment, so a worker thread +// can load and use the bindings alongside the main thread. +async function worker() { + const { Worker } = await import('node:worker_threads'); + const url = new URL('./node_modules/contacts/index.js', import.meta.url).href; + const thread = new Worker( + `const { parentPort } = require('node:worker_threads'); + import(${JSON.stringify(url)}).then(({ contacts }) => { + const book = new contacts.ContactBook(); + book.add('Wanda', 'Worker', null, contacts.ContactType.Work); + parentPort.postMessage(contacts.groups.countOfType(book, contacts.ContactType.Work)); + book.close(); + });`, + { eval: true }, + ); + const count = await new Promise((resolve, reject) => { + thread.once('message', resolve); + thread.once('error', reject); + }); + expect(count === 1, `a worker thread uses the bindings (got ${count})`); + await thread.terminate(); +} + +main(); +if (!isWasm(api)) await worker(); +await finish(api, 'contacts'); diff --git a/conformance/node/events.js b/conformance/node/events.js deleted file mode 100644 index b2202c9f..00000000 --- a/conformance/node/events.js +++ /dev/null @@ -1,247 +0,0 @@ -// Conformance consumer: events sample, Node (N-API) target. -// -// Drives the ABI 2 callback-interface surface through the generated wrapper -// layer (index.js): a `Subscriber` implemented as a plain JS object (route -> -// Delivery, onMessage -> running count, onAttached receiving the EventBus -// object the producer hands over), the reference-counted `EventBus` class -// (constructor, methods, idempotent `close()`, use-after-close trap), the -// `Delivery` return steering `publish`'s accepted count, the Promise-backed -// `publishLater` whose callbacks hop from the producer thread onto the JS -// thread, the lazy iterable `messages()`, the nullable `lastMessage()` -// record, the free function `routeOnce`, and a subscriber that throws: the -// exception surfaces to the caller as a WeaveFFIError with code -4 (foreign -// error) without aborting the process. i64 values cross as BigInt. The -// harness passes the built addon via WV_ADDON; the generated loader honors -// WEAVEFFI_ADDON. - -'use strict'; - -const path = require('path'); - -const ADDON = path.resolve(process.env.WV_ADDON); -process.env.WEAVEFFI_ADDON = ADDON; -// WV_ADDON = /conformance-build/node-events/build/Release/index.node; -// the generated files sit at /conformance-gen/events/node/. -const wv = require( - path.resolve(ADDON, '../../../../..', 'conformance-gen', 'events', 'node', 'index.js') -); - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} - -// The C-style enum is exported as a frozen object with forward and reverse -// mappings, the runtime value `types.d.ts` declares as `export enum`. -const Delivery = wv.Delivery; -expect(Delivery.Accept === 0 && Delivery.Skip === 1 && Delivery.AcceptAndStop === 2, 'Delivery values'); -expect(Delivery[2] === 'AcceptAndStop', 'Delivery reverse mapping'); -expect(Object.isFrozen(Delivery), 'Delivery is frozen'); - -// A subscriber as a class: `route` steers delivery per topic, `onMessage` -// records what it saw and returns its running count (an i64, so a BigInt), -// and `onAttached` adopts the bus object the producer hands over. -class RecordingSubscriber { - constructor(name, opts) { - this.name = name; - this.skipTopic = (opts && opts.skip) || null; - this.stopTopic = (opts && opts.stop) || null; - this.throwTopic = (opts && opts.throwOn) || null; - this.routed = []; - this.received = []; - this.attachedBuses = []; - this.attachedCounts = []; - } - route(topic) { - this.routed.push(topic); - if (topic === this.throwTopic) { - throw new Error('subscriber ' + this.name + ' rejects ' + topic); - } - if (topic === this.skipTopic) return Delivery.Skip; - if (topic === this.stopTopic) return Delivery.AcceptAndStop; - return Delivery.Accept; - } - onMessage(message) { - this.received.push(message); - return BigInt(this.received.length); - } - onAttached(bus) { - // The object is usable right away: `subscribe` attaches before it - // appends, so the count seen here is the pre-subscribe count. - expect(bus instanceof wv.EventBus, this.name + ': onAttached receives an EventBus'); - this.attachedCounts.push(bus.subscriberCount()); - this.attachedBuses.push(bus); - } -} - -(async () => { - const bus = new wv.EventBus(); - expect(bus instanceof wv.EventBus, 'constructor returns an EventBus'); - expect(bus.subscriberCount() === 0n, 'fresh bus has no subscribers'); - expect(bus.lastMessage() === null, 'lastMessage is null before any publish'); - expect([...bus.messages()].length === 0, 'messages() empty before any publish'); - - // subscribe() returns the new subscriber count and calls onAttached with - // the bus object (one strong reference transferred to the consumer). - const a = new RecordingSubscriber('a', { skip: 'quiet', stop: 'stop' }); - const b = new RecordingSubscriber('b'); - expect(bus.subscribe(a) === 1n, 'subscribe a returns 1n'); - expect(bus.subscribe(b) === 2n, 'subscribe b returns 2n'); - expect(bus.subscriberCount() === 2n, 'subscriberCount == 2n'); - expect(a.attachedBuses.length === 1, 'a attached once'); - expect(a.attachedCounts[0] === 0n, `a saw 0 subscribers at attach (got ${a.attachedCounts[0]})`); - expect(b.attachedCounts[0] === 1n, `b saw 1 subscriber at attach (got ${b.attachedCounts[0]})`); - - // Both accept: two deliveries, one Message each with every field decoded. - const n1 = bus.publish('news', 'hello', ['x', 'y']); - expect(typeof n1 === 'bigint', 'publish returns a BigInt'); - expect(n1 === 2n, `publish(news) accepted by 2 (got ${n1})`); - expect(a.received.length === 1 && b.received.length === 1, 'both received news'); - const m = a.received[0]; - expect(m.seq === 1n, `message seq is 1n (got ${m.seq})`); - expect(m.topic === 'news', 'message topic'); - expect(m.text === 'hello', 'message text'); - expect( - Array.isArray(m.tags) && m.tags.length === 2 && m.tags[0] === 'x' && m.tags[1] === 'y', - `message tags (got ${JSON.stringify(m.tags)})` - ); - expect(a.routed.length === 1 && a.routed[0] === 'news', 'route called with topic'); - - // Skip steers the count: a skips "quiet", only b accepts. - const n2 = bus.publish('quiet', 'psst', []); - expect(n2 === 1n, `publish(quiet) accepted by 1 (got ${n2})`); - expect(a.received.length === 1, 'a skipped quiet'); - expect(b.received.length === 2 && b.received[1].seq === 2n, 'b received quiet with seq 2n'); - expect(b.received[1].tags.length === 0, 'empty tags list decodes as []'); - - // AcceptAndStop: a accepts and the bus stops before reaching b. - const n3 = bus.publish('stop', 'last', ['z']); - expect(n3 === 1n, `publish(stop) accepted by 1 (got ${n3})`); - expect(a.received.length === 2 && a.received[1].text === 'last', 'a received stop'); - expect(b.received.length === 2, 'b not reached after AcceptAndStop'); - expect(b.routed.length === 2, 'b.route not consulted after stop'); - - // The adopted bus is the same underlying object: it observes the same log. - const viaA = a.attachedBuses[0]; - expect(viaA.subscriberCount() === 2n, 'adopted bus sees both subscribers'); - const viaAMsgs = [...viaA.messages()]; - expect( - viaAMsgs.join('|') === 'hello|psst|last', - `adopted bus shares the log (got ${JSON.stringify(viaAMsgs)})` - ); - const lastViaA = viaA.lastMessage(); - expect(lastViaA !== null && lastViaA.seq === 3n && lastViaA.text === 'last', 'adopted bus lastMessage'); - // Releasing the adopted references leaves the original alive. - for (const adopted of a.attachedBuses.concat(b.attachedBuses)) { - adopted.close(); - adopted.close(); - } - expect(bus.subscriberCount() === 2n, 'bus alive after adopted wrappers closed'); - - // Async publish: the producer thread calls route/onMessage, which hop to - // the JS thread while this frame awaits. - const later = bus.publishLater('news', 'later'); - expect(later instanceof Promise, 'publishLater returns a Promise'); - const n4 = await later; - expect(n4 === 2n, `publishLater accepted by 2 (got ${n4})`); - expect(a.received.length === 3 && a.received[2].text === 'later', 'a received the async message'); - expect(b.received.length === 3 && b.received[2].seq === 4n, 'b received the async message'); - expect(a.received[2].tags.length === 0, 'publishLater sends no tags'); - - // Iterator and nullable record. - const msgs = [...bus.messages()]; - expect( - msgs.join('|') === 'hello|psst|last|later', - `messages() in order (got ${JSON.stringify(msgs)})` - ); - const last = bus.lastMessage(); - expect(last !== null && typeof last === 'object', 'lastMessage present'); - expect(last.seq === 4n && last.topic === 'news' && last.text === 'later', 'lastMessage fields'); - expect(Array.isArray(last.tags) && last.tags.length === 0, 'lastMessage tags empty'); - - // Early return closes the underlying producer iterator without draining. - const it = bus.messages()[Symbol.iterator](); - const first = it.next(); - expect(!first.done && first.value === 'hello', 'lazy first element'); - expect(typeof it.return === 'function', 'iterator has return()'); - it.return(); - expect(it.next().done === true, 'iterator done after return()'); - - // A subscriber whose route throws: the producer aborts publish and the - // caller sees the foreign error (code -4) carrying the thrown message. - const bad = new RecordingSubscriber('bad', { throwOn: 'boom' }); - expect(bus.subscribe(bad) === 3n, 'subscribe bad returns 3n'); - try { - bus.publish('boom', 'x', []); - expect(false, 'expected publish to throw when a subscriber throws'); - } catch (e) { - expect(e instanceof wv.WeaveFFIError, `foreign error is a WeaveFFIError (got ${e && e.constructor.name})`); - expect(e.code === -4, `foreign error code is -4 (got ${e.code})`); - expect( - typeof e.errorMessage === 'string' && e.errorMessage.includes('rejects boom'), - `foreign error carries the JS message (got ${JSON.stringify(e.errorMessage)})` - ); - } - // a and b ran before bad and still accepted; the bus is intact. - expect(a.received.length === 4 && b.received.length === 4, 'earlier subscribers still delivered'); - expect(bus.subscriberCount() === 3n, 'bus intact after foreign error'); - expect(bus.publish('ok', 'y', []) === 3n, 'publish works again after foreign error'); - - // The same through the async path: the Promise rejects with code -4. - try { - await bus.publishLater('boom', 'z'); - expect(false, 'expected publishLater to reject when a subscriber throws'); - } catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -4, `async foreign error code -4 (got ${e && e.code})`); - expect(e.errorMessage.includes('rejects boom'), 'async foreign error carries the JS message'); - } - - // A wrong return type from the callback is also a foreign error. - const weird = { route() { return 'not a number'; }, onMessage() { return 0n; }, onAttached() {} }; - try { - wv.routeOnce(weird, 'any'); - expect(false, 'expected routeOnce to throw for a bad return type'); - } catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -4, `bad return type is code -4 (got ${e && e.code})`); - } - - // routeOnce: a free function taking the callback interface; no bus involved. - const solo = new RecordingSubscriber('solo', { skip: 'quiet' }); - expect(wv.routeOnce(solo, 'quiet') === Delivery.Skip, 'routeOnce(quiet) == Skip'); - expect(wv.routeOnce(solo, 'news') === Delivery.Accept, 'routeOnce(news) == Accept'); - expect(solo.routed.length === 2 && solo.attachedBuses.length === 0, 'routeOnce calls route only'); - // A plain object literal works as an implementation too. - const literal = { route: () => Delivery.AcceptAndStop, onMessage: () => 1n, onAttached: () => {} }; - expect(wv.routeOnce(literal, 'x') === Delivery.AcceptAndStop, 'object literal subscriber'); - - // clearSubscribers drops every retained subscriber. - bus.clearSubscribers(); - expect(bus.subscriberCount() === 0n, 'subscriberCount == 0n after clear'); - expect(bus.publish('news', 'nobody', []) === 0n, 'publish with no subscribers accepts 0'); - const all = [...bus.messages()]; - expect(all.length === 8, `log records every publish, aborted ones included (got ${all.length})`); - - // Idempotent close; use after close is a marshalling error (-3). - bus.close(); - bus.close(); - try { - bus.subscriberCount(); - expect(false, 'expected throw for use after close'); - } catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -3, `use after close is code -3 (got ${e && e.code})`); - } - if (typeof Symbol.dispose === 'symbol') { - const disposable = new wv.EventBus(); - disposable[Symbol.dispose](); - disposable[Symbol.dispose](); - } - - if (failures > 0) process.exit(1); - console.log('node/events: OK'); -})().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/conformance/node/events.mjs b/conformance/node/events.mjs new file mode 100644 index 00000000..2430feae --- /dev/null +++ b/conformance/node/events.mjs @@ -0,0 +1,139 @@ +// Conformance consumer: events sample (node and wasm lanes). +// +// A `Subscriber` callback interface implemented by a class and by object +// literals: `route` (whose producer-side trait method returns +// `Result`) steering delivery, `onMessage` returning +// a bigint, and `onAttached` receiving the EventBus object. Also the +// EventBus class, `publishLater` (whose callbacks run on a producer thread +// on Node.js and hop to the JavaScript thread), the lazy `messages()` +// iterator, a nullable record, `routeOnce`, failures thrown by an +// implementation surfacing as code -4, and leak-free teardown. + +import { expect, finish, isWasm, load, rejects, same, throws } from './harness.mjs'; + +const api = await load('events'); +const { events, EventsError } = api; +const { Delivery } = events; + +class Recorder { + constructor(name, opts = {}) { + this.name = name; + this.opts = opts; + this.routed = []; + this.received = []; + this.attached = []; + } + + route(topic) { + this.routed.push(topic); + if (topic === this.opts.throwOn) throw new Error(`subscriber ${this.name} rejects ${topic}`); + if (topic === this.opts.skip) return Delivery.Skip; + if (topic === this.opts.stop) return Delivery.AcceptAndStop; + return Delivery.Accept; + } + + onMessage(message) { + this.received.push(message); + return BigInt(this.received.length); + } + + onAttached(bus) { + expect(bus instanceof events.EventBus, `${this.name}: onAttached receives an EventBus`); + this.attached.push(bus.subscriberCount()); + // The adopted wrapper is this subscriber's to release. + bus.close(); + } +} + +async function main() { + expect(Delivery.Accept === 0 && Delivery.Skip === 1 && Delivery.AcceptAndStop === 2, 'Delivery values'); + + const bus = new events.EventBus(); + expect(bus.subscriberCount() === 0n && bus.lastMessage() === null, 'fresh bus'); + same([...bus.messages()], [], 'no messages yet'); + + const a = new Recorder('a', { skip: 'quiet', stop: 'stop' }); + const b = new Recorder('b'); + expect(bus.subscribe(a) === 1n && bus.subscribe(b) === 2n, 'subscribe returns the count'); + same(a.attached, [0n], 'a attached before it was added'); + same(b.attached, [1n], 'b attached after a'); + + expect(bus.publish('news', 'hello', ['x', 'y']) === 2n, 'both accept'); + same(a.received[0], { seq: 1n, topic: 'news', text: 'hello', tags: ['x', 'y'] }, 'the message record'); + expect(bus.publish('quiet', 'psst', []) === 1n && a.received.length === 1, 'Skip'); + expect(bus.publish('stop', 'last', ['z']) === 1n && b.routed.length === 2, 'AcceptAndStop'); + + const later = bus.publishLater('news', 'later'); + expect(later instanceof Promise, 'publishLater returns a Promise'); + expect((await later) === 2n, 'publishLater resolves with the accepted count'); + expect(a.received[2].text === 'later' && b.received[2].seq === 4n, 'async delivery'); + + same([...bus.messages()], ['hello', 'psst', 'last', 'later'], 'messages() in order'); + const it = bus.messages(); + expect(it.next().value === 'hello', 'lazy first element'); + it.return(); + expect(it.next().done === true, 'done after return()'); + for (const text of bus.messages()) { + if (text === 'psst') break; + } + same(bus.lastMessage(), { seq: 4n, topic: 'news', text: 'later', tags: [] }, 'lastMessage'); + + // `route` returns a Result on the producer side: a thrown exception comes + // back to the bus as an Err, which it propagates as code -4. + const bad = new Recorder('bad', { throwOn: 'boom' }); + expect(bus.subscribe(bad) === 3n, 'subscribe bad'); + throws( + () => bus.publish('boom', 'x', []), + (e) => e instanceof EventsError && e.code === -4 && e.message.includes('rejects boom'), + 'a Result-returning callback failure', + ); + expect(a.received.length === 4 && b.received.length === 4, 'earlier subscribers still delivered'); + expect(bus.publish('ok', 'y', []) === 3n, 'the bus works after a callback failure'); + await rejects( + bus.publishLater('boom', 'z'), + (e) => e instanceof EventsError && e.code === -4 && e.message.includes('rejects boom'), + 'an async callback failure', + ); + + // `onMessage` returns a plain value: a failure there aborts the call. + const failing = { route: () => Delivery.Accept, onMessage() { throw new TypeError('no thanks'); }, onAttached: (bus) => bus.close() }; + const solo = new events.EventBus(); + solo.subscribe(failing); + throws( + () => solo.publish('t', 'x', []), + (e) => e instanceof EventsError && e.code === -4 && e.message.includes('no thanks'), + 'a plain-return callback failure', + ); + // A wrong return type is a failure of the implementation too. + const weird = { route: () => 'not a number', onMessage: () => 0n, onAttached: () => {} }; + throws(() => events.routeOnce(weird, 'x'), (e) => e instanceof EventsError && e.code === -4, 'a bad return type'); + throws(() => events.routeOnce(null, 'x'), (e) => e instanceof TypeError, 'a missing implementation'); + expect(events.routeOnce(new Recorder('r', { skip: 'quiet' }), 'quiet') === Delivery.Skip, 'routeOnce'); + expect(events.routeOnce({ route: () => Delivery.AcceptAndStop }, 'x') === Delivery.AcceptAndStop, 'an object literal'); + + if (!isWasm(api)) { + // A callback made on a producer thread runs on this thread while the + // caller awaits, so it may use the bindings freely. + const nested = { route: () => Delivery.Accept, onMessage: (m) => BigInt(bus.subscriberCount()) + m.seq, onAttached: (b) => b.close() }; + const busy = new events.EventBus(); + busy.subscribe(nested); + expect((await busy.publishLater('t', 'x')) === 1n, 'a producer-thread callback can call back in'); + busy.close(); + } + + // Release everything. The subscribers are freed when their bus is. + solo.clearSubscribers(); + solo.close(); + bus.clearSubscribers(); + expect(bus.subscriberCount() === 0n && bus.publish('news', 'nobody', []) === 0n, 'clearSubscribers'); + bus.close(); + bus.close(); + throws(() => bus.subscriberCount(), (e) => e instanceof EventsError && e.code === -3, 'use after close'); + const disposed = new events.EventBus(); + disposed[Symbol.dispose](); + disposed[Symbol.dispose](); + throws(() => disposed.subscriberCount(), (e) => e.code === -3, 'Symbol.dispose releases'); +} + +await main(); +await finish(api, 'events'); diff --git a/conformance/node/harness.mjs b/conformance/node/harness.mjs new file mode 100644 index 00000000..b79225e7 --- /dev/null +++ b/conformance/node/harness.mjs @@ -0,0 +1,85 @@ +// Shared helpers of the JavaScript conformance consumers. The node and wasm +// targets generate the same ES module (only the transport differs), so one +// consumer per sample runs in both lanes: each lane installs the generated +// package into a scratch project and runs the consumer there with +// `node --expose-gc`. + +import assert from 'node:assert'; + +let failures = 0; + +export function expect(cond, msg) { + if (!cond) { + console.error('assertion failed: ' + msg); + failures++; + } +} + +/** Structural equality with SameValue semantics (BigInt, NaN, -0, bytes). */ +export function same(actual, expected, msg) { + try { + assert.deepStrictEqual(actual, expected); + } catch (e) { + console.error('assertion failed: ' + msg); + console.error(e.message); + failures++; + } +} + +/** Expect `fn()` to throw an error satisfying `check`. */ +export function throws(fn, check, msg) { + try { + fn(); + expect(false, `${msg}: expected a throw`); + } catch (e) { + expect(check(e), `${msg} (got ${e && e.constructor && e.constructor.name}: ${e && e.message})`); + } +} + +/** Expect `promise` to reject with an error satisfying `check`. */ +export async function rejects(promise, check, msg) { + try { + await promise; + expect(false, `${msg}: expected a rejection`); + } catch (e) { + expect(check(e), `${msg} (got ${e && e.constructor && e.constructor.name}: ${e && e.message})`); + } +} + +/** + * Import the package installed next to the consumer (by path, since a + * sample may share its name with a Node.js builtin such as `events`) and, + * on WebAssembly, initialize it. + */ +export async function load(pkg) { + const api = await import(new URL(`./node_modules/${pkg}/index.js`, import.meta.url)); + if (typeof api.init === 'function') await api.init(); + return api; +} + +/** `true` when running against the WebAssembly bindings. */ +export function isWasm(api) { + return typeof api.init === 'function'; +} + +const KINDS = ['objects', 'callbacks', 'iterators', 'cancel tokens', 'returned buffers']; + +/** + * Force garbage collection until every wrapper the consumer dropped has been + * finalized, require every native leak counter back at zero, and exit. + */ +export async function finish(api, label) { + let live = []; + for (let round = 0; round < 100; round++) { + globalThis.gc(); + await new Promise((resolve) => setTimeout(resolve, 5)); + live = KINDS.map((_, kind) => api.__debugLive(kind)); + if (live.every((n) => n === 0n)) break; + } + live.forEach((n, kind) => expect(n === 0n, `${n} ${KINDS[kind]} still live at exit`)); + if (failures > 0) { + console.error(`${label}: ${failures} failure(s)`); + process.exit(1); + } + console.log(`${label}: OK`); +} diff --git a/conformance/node/kvstore.js b/conformance/node/kvstore.js deleted file mode 100644 index 8ebee464..00000000 --- a/conformance/node/kvstore.js +++ /dev/null @@ -1,371 +0,0 @@ -// Conformance consumer: kvstore sample, Node (N-API) target. -// -// Drives the generated wrapper layer (index.js) end to end: the -// reference-counted Store class (static `open` factory, instance methods, -// the static `defaultCapacity`, idempotent `close()`, use-after-close trap), -// the typed error surface (KeyNotFoundError / ExpiredError / IoError -// extending KvError extending WeaveFFIError, each carrying its stable code), -// record materialization with complex fields (`Buffer` bytes, nullable -// BigInt, list, map), the iterator-backed `listKeys`, the deprecated -// `legacyPut`, the `kv.stats` submodule taking the interface by reference, -// the synchronous `EvictionListener` callback interface implemented as a JS -// class (return value detaches, replace and clear, a throwing listener -// surfacing to the caller as code -4), objects in every buffered position -// (`share()` aliasing the same object, `fork()`, `larger(null)`, -// `describe().store`, `openMany`, `totalCount` with a record that carries -// objects), and the Promise-returning `compact`. i64 values cross as BigInt. -// The harness passes the built addon via WV_ADDON; the generated loader -// honors WEAVEFFI_ADDON. - -'use strict'; - -const path = require('path'); - -const ADDON = path.resolve(process.env.WV_ADDON); -process.env.WEAVEFFI_ADDON = ADDON; -// WV_ADDON = /conformance-build/node-kvstore/build/Release/index.node; -// the generated files sit at /conformance-gen/kvstore/node/. -const wv = require( - path.resolve(ADDON, '../../../../..', 'conformance-gen', 'kvstore', 'node', 'index.js') -); - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} - -// C-style enums are exported as frozen objects (forward and reverse mappings). -const EntryKind = wv.EntryKind; -const EvictionReason = wv.EvictionReason; -expect(EntryKind.Volatile === 0 && EntryKind.Persistent === 1 && EntryKind.Encrypted === 2, 'EntryKind values'); -expect(EvictionReason.Deleted === 0 && EvictionReason.Expired === 1, 'EvictionReason values'); -expect(EvictionReason[1] === 'Expired', 'EvictionReason reverse mapping'); - -// An eviction listener as a JS class: records every notification and keeps -// receiving until `stopAfter` notifications have arrived. -class Listener { - constructor(name, stopAfter) { - this.name = name; - this.stopAfter = stopAfter === undefined ? Infinity : stopAfter; - this.seen = []; - } - onEvict(entry, reason) { - this.seen.push({ key: entry.key, reason, entry }); - return this.seen.length < this.stopAfter; - } -} - -// Typed error on the factory: an empty path is rejected with the IoError -// class (code 1004; `IoError` already ends in `Error`, so no suffix stacking). -try { - wv.Store.open(''); - expect(false, 'expected throw for empty path'); -} catch (e) { - expect(e instanceof wv.IoError, 'IoError instance (got ' + e.name + ')'); - expect(e instanceof wv.KvError, 'IoError extends KvError'); - expect(e instanceof wv.WeaveFFIError, 'IoError extends WeaveFFIError'); - expect(e.code === 1004, 'IoError code == 1004 (got ' + e.code + ')'); - expect(wv.IoError.CODE === 1004, 'IoError.CODE == 1004'); -} - -const store = wv.Store.open('/tmp/conformance-kvstore-node'); -expect(store instanceof wv.Store, 'open returns a Store instance'); - -// Static method; i64 returns are BigInt. -const cap = wv.Store.defaultCapacity(); -expect(typeof cap === 'bigint', 'defaultCapacity is a BigInt'); -expect(cap === 1000000n, `defaultCapacity == 1000000n (got ${cap})`); - -const payload = Buffer.from([1, 2, 3]); -expect(store.put('alpha', payload, EntryKind.Persistent, null) === true, 'put alpha'); -expect(store.put('beta', payload, EntryKind.Volatile, 3600n) === true, 'put beta with BigInt ttl'); -expect(store.put('gamma', payload, EntryKind.Encrypted, 7200) === true, 'put gamma with number ttl'); - -expect(store.count() === 3n, `count == 3n (got ${store.count()})`); - -// Iterator-backed method return: a lazy iterable streaming keys in sorted -// order (one producer next per step), optionally filtered by prefix. -const keys = [...store.listKeys(null)]; -expect( - keys.join(',') === 'alpha,beta,gamma', - `listKeys yields sorted keys (got ${JSON.stringify(keys)})` -); -const filtered = [...store.listKeys('al')]; -expect( - filtered.length === 1 && filtered[0] === 'alpha', - `listKeys honors the prefix (got ${JSON.stringify(filtered)})` -); -expect([...store.listKeys('zzz')].length === 0, 'listKeys with no match is empty'); -const keyIt = store.listKeys(null)[Symbol.iterator](); -expect(keyIt.next().value === 'alpha', 'lazy first key'); -keyIt.return(); -expect(keyIt.next().done === true, 'iterator done after return()'); - -// Record materialization with complex fields. -const alpha = store.get('alpha'); -expect(typeof alpha === 'object' && alpha !== null, 'get alpha object'); -expect(typeof alpha.id === 'bigint' && alpha.id === 1n, `entry id is 1n (got ${alpha.id})`); -expect(alpha.key === 'alpha', 'entry key'); -expect(typeof alpha.created_at === 'bigint' && alpha.created_at > 0n, 'created_at is a positive BigInt'); - -// Bytes getter -> Buffer. -expect(Buffer.isBuffer(alpha.value), 'entry value is Buffer'); -expect(alpha.value.equals(payload), 'entry value bytes'); - -// Optional-scalar getter: alpha had no TTL -> null. -expect(alpha.expires_at === null, 'alpha expires_at null'); - -// List getter (empty) -> array; map getter (empty) -> object. -expect(Array.isArray(alpha.tags) && alpha.tags.length === 0, 'alpha tags empty array'); -expect( - typeof alpha.metadata === 'object' && - alpha.metadata !== null && - Object.keys(alpha.metadata).length === 0, - 'alpha metadata empty object' -); - -// beta had a TTL, so the nullable-scalar getter yields a BigInt. -const beta = store.get('beta'); -expect(typeof beta.expires_at === 'bigint', 'beta expires_at BigInt'); -expect(beta.expires_at === beta.created_at + 3600n, 'beta expires_at == created_at + 3600'); -expect(beta.id === 2n, 'beta id 2n'); - -// Typed error on a method: a missing key throws the KeyNotFound class. -try { - store.get('missing'); - expect(false, 'expected throw for missing key'); -} catch (e) { - expect(e instanceof wv.KeyNotFoundError, 'KeyNotFoundError instance (got ' + e.name + ')'); - expect(e instanceof wv.KvError, 'KeyNotFoundError extends KvError'); - expect(e instanceof wv.WeaveFFIError, 'KeyNotFoundError extends WeaveFFIError'); - expect(e.code === 1001, 'KeyNotFound code == 1001 (got ' + e.code + ')'); - expect(wv.KeyNotFoundError.CODE === 1001, 'KeyNotFoundError.CODE == 1001'); - expect(e.errorMessage === 'key not found', `default message (got ${JSON.stringify(e.errorMessage)})`); -} - -// Deprecated method still works. -expect(store.legacyPut('legacy', payload) === true, 'legacyPut inserts'); -expect(store.count() === 4n, 'count == 4n after legacyPut'); -expect(store.delete('legacy') === true, 'delete legacy'); -expect(store.delete('legacy') === false, 'second delete returns false'); - -// kv.stats submodule: takes the Store instance by reference. -const st = wv.getStats(store); -expect(st.total_entries === 3n, `stats total entries == 3n (got ${st.total_entries})`); -expect(st.total_bytes === 9n, `stats total bytes == 9n (got ${st.total_bytes})`); -expect(st.expired_entries === 0n, 'stats expired entries == 0n'); - -// --- Eviction listener (synchronous callback interface) --------------------- -const l1 = new Listener('l1'); -store.setEvictionListener(l1); -expect(store.delete('gamma') === true, 'delete gamma'); -expect(l1.seen.length === 1, `listener fired synchronously (got ${l1.seen.length})`); -expect(l1.seen[0].key === 'gamma', 'evicted key gamma'); -expect(l1.seen[0].reason === EvictionReason.Deleted, `reason Deleted (got ${l1.seen[0].reason})`); -expect(l1.seen[0].entry.id === 3n && l1.seen[0].entry.value.equals(payload), 'evicted entry decoded'); - -// An expired entry is evicted on read: get() throws ExpiredError and the -// listener sees reason Expired. -expect(store.put('doomed', Buffer.from([9]), EntryKind.Volatile, -1n) === true, 'put doomed'); -try { - store.get('doomed'); - expect(false, 'expected ExpiredError'); -} catch (e) { - expect(e instanceof wv.ExpiredError && e.code === 1002, `ExpiredError code 1002 (got ${e && e.code})`); - expect(e instanceof wv.KvError, 'ExpiredError extends KvError'); -} -expect(l1.seen.length === 2 && l1.seen[1].key === 'doomed', 'listener saw the expiry'); -expect(l1.seen[1].reason === EvictionReason.Expired, `reason Expired (got ${l1.seen[1].reason})`); -expect(l1.seen[1].entry.expires_at !== null && l1.seen[1].entry.expires_at < l1.seen[1].entry.created_at, 'expired entry carries its past expiry'); - -// Replacing the listener: the new one receives, the old one doesn't. -const l2 = new Listener('l2', 1); -store.setEvictionListener(l2); -expect(store.put('x1', payload, EntryKind.Volatile, null) && store.put('x2', payload, EntryKind.Volatile, null), 'put x1 x2'); -expect(store.delete('x1') === true, 'delete x1'); -expect(l2.seen.length === 1 && l2.seen[0].key === 'x1', 'replacement listener fired'); -expect(l1.seen.length === 2, 'replaced listener no longer fires'); -// l2 returned false: it detached itself, so the next eviction is unobserved. -expect(store.delete('x2') === true, 'delete x2'); -expect(l2.seen.length === 1, 'listener detached after returning false'); - -// A listener that throws: delete() aborts with the foreign error (code -4) -// and the process keeps running; the entry is gone regardless. -const thrower = { - onEvict(entry) { - throw new TypeError('listener refuses ' + entry.key); - }, -}; -store.setEvictionListener(thrower); -expect(store.put('x3', payload, EntryKind.Volatile, null) === true, 'put x3'); -try { - store.delete('x3'); - expect(false, 'expected delete to throw when the listener throws'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError, `foreign error is a WeaveFFIError (got ${e && e.constructor.name})`); - expect(!(e instanceof wv.KvError), 'foreign error is not a domain error'); - expect(e.code === -4, `foreign error code -4 (got ${e.code})`); - expect(e.errorMessage.includes('listener refuses x3'), `carries the JS message (got ${JSON.stringify(e.errorMessage)})`); -} -expect(store.count() === 2n, 'x3 removed despite the listener failure'); -// A wrong return type is a foreign error too. -store.setEvictionListener({ onEvict: () => 'yes' }); -expect(store.put('x4', payload, EntryKind.Volatile, null) === true, 'put x4'); -try { - store.delete('x4'); - expect(false, 'expected delete to throw for a bad listener return'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -4, `bad return type is code -4 (got ${e && e.code})`); -} -store.clearEvictionListener(); -expect(store.put('x5', payload, EntryKind.Volatile, null) === true, 'put x5'); -expect(store.delete('x5') === true, 'delete after clearEvictionListener'); -expect(store.count() === 2n, 'count back to 2n'); - -// --- Objects in every position ---------------------------------------------- -// share(): a second wrapper over the same object. Writes through one are -// visible through the other, and closing one leaves the other alive. -const shared = store.share(); -expect(shared instanceof wv.Store, 'share returns a Store'); -expect(shared !== store, 'share returns a distinct wrapper'); -expect(shared.count() === 2n, 'shared sees the same entries'); -expect(shared.put('via-shared', payload, EntryKind.Volatile, null) === true, 'put through shared'); -expect(store.count() === 3n, 'write through share visible through the original'); -expect(store.get('via-shared').key === 'via-shared', 'entry readable through the original'); -const l3 = new Listener('l3'); -shared.setEvictionListener(l3); -expect(store.delete('via-shared') === true, 'delete through the original'); -expect(l3.seen.length === 1 && l3.seen[0].key === 'via-shared', 'listener set through share fires for the original'); -store.clearEvictionListener(); -shared.close(); -shared.close(); -expect(store.count() === 2n, 'original alive after shared wrapper closed'); - -// fork(): an independent copy. -const forked = store.fork(); -expect(forked.count() === 2n, 'fork copies live entries'); -expect(forked.put('only-in-fork', payload, EntryKind.Volatile, null) === true, 'put in fork'); -expect(forked.count() === 3n && store.count() === 2n, 'fork is independent'); -expect(forked.get('alpha').id === 1n, 'fork preserves entry ids'); - -// larger(): Store? both ways. -const empty = wv.Store.open('/tmp/empty'); -expect(empty.larger(null) === null, 'larger(null) on an empty store is null'); -expect(empty.larger(undefined) === null, 'larger(undefined) on an empty store is null'); -const own = store.larger(null); -expect(own instanceof wv.Store && own.count() === 2n, 'larger(null) on a non-empty store is itself'); -const bigger = store.larger(forked); -expect(bigger instanceof wv.Store && bigger.count() === 3n, 'larger(forked) picks the fork'); -const stillOwn = store.larger(empty); -expect(stillOwn.count() === 2n, 'larger(empty) picks self'); -own.close(); -bigger.close(); -stillOwn.close(); -expect(store.count() === 2n && forked.count() === 3n, 'sources alive after result wrappers closed'); - -// describe(): a record carrying an object field and an optional object. -const info = store.describe('primary', null); -expect(info.label === 'primary', 'describe label'); -expect(info.count === 2n, `describe count 2n (got ${info.count})`); -expect(info.store instanceof wv.Store, 'describe().store is a Store'); -expect(info.mirror === null, 'describe().mirror is null when absent'); -expect(info.store.count() === 2n, 'describe().store is usable'); -expect(info.store.put('via-info', payload, EntryKind.Volatile, null) === true, 'put through describe().store'); -expect(store.count() === 3n, 'describe().store aliases the original'); -const infoWithMirror = store.describe('mirrored', forked); -expect(infoWithMirror.mirror instanceof wv.Store, 'describe().mirror is a Store when present'); -expect(infoWithMirror.mirror.count() === 3n, 'mirror is usable'); -expect(infoWithMirror.mirror.get('only-in-fork').key === 'only-in-fork', 'mirror aliases the fork'); - -// openMany(): a list of objects as a return; a bad path fails the whole call. -const many = wv.Store.openMany(['/tmp/m1', '/tmp/m2', '/tmp/m3']); -expect(Array.isArray(many) && many.length === 3, `openMany returns 3 stores (got ${many && many.length})`); -expect(many.every((s) => s instanceof wv.Store && s.count() === 0n), 'openMany stores are fresh'); -expect(many[0].put('m', payload, EntryKind.Volatile, null) === true, 'put in many[0]'); -expect(many[1].count() === 0n, 'openMany stores are distinct'); -try { - wv.Store.openMany(['/tmp/ok', '']); - expect(false, 'expected openMany to throw for an empty path'); -} catch (e) { - expect(e instanceof wv.IoError && e.code === 1004, `openMany IoError (got ${e && e.code})`); -} -expect(wv.Store.openMany([]).length === 0, 'openMany([]) is empty'); - -// totalCount(): a list of objects as a parameter plus an optional record -// that carries objects (the encoder clones each handle for the buffer). -expect(wv.Store.totalCount(many, null) === 1n, 'totalCount(many) == 1n'); -expect(wv.Store.totalCount([], null) === 0n, 'totalCount([]) == 0n'); -expect(wv.Store.totalCount([store, forked], null) === 6n, `totalCount([store, forked]) == 6n`); -const total = wv.Store.totalCount(many, info); -expect(total === 4n, `totalCount(many, info) == 4n (got ${total})`); -const total2 = wv.Store.totalCount([], infoWithMirror); -expect(total2 === 3n, `totalCount([], infoWithMirror) == 3n (got ${total2})`); -// Hand-built record with the mirror present. -const total3 = wv.Store.totalCount([empty], { label: 'x', store: forked, mirror: store, count: 0n }); -expect(total3 === 3n, `totalCount with hand-built StoreInfo == 3n (got ${total3})`); -// Every source is still alive: encoding borrowed clones, not the wrappers. -expect(store.count() === 3n && forked.count() === 3n && many[0].count() === 1n, 'sources alive after totalCount'); - -// Release the object graph; every wrapper closes independently. -info.store.close(); -infoWithMirror.store.close(); -infoWithMirror.mirror.close(); -for (const s of many) s.close(); -for (const s of many) s.close(); -empty.close(); -forked.close(); -expect(store.count() === 3n, 'original alive after releasing every alias'); -try { - forked.count(); - expect(false, 'expected throw for use after close'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -3, `use after close is code -3 (got ${e && e.code})`); -} -try { - wv.Store.totalCount([forked], null); - expect(false, 'expected throw for a closed object inside a list'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -3, `closed object in a buffer is code -3 (got ${e && e.code})`); -} -try { - wv.getStats(forked); - expect(false, 'expected throw for a closed object passed by reference'); -} catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -3, `closed object by reference is code -3 (got ${e && e.code})`); -} - -(async () => { - // Async: an immediately-expired entry gives compact 3 bytes to reclaim; the - // promise settles via a TSFN from the producer's worker thread. - expect(store.delete('via-info') === true, 'delete via-info'); - expect(store.put('dead', payload, EntryKind.Volatile, 0n) === true, 'put dead'); - const st2 = wv.getStats(store); - expect(st2.expired_entries === 1n, `one expired entry pending (got ${st2.expired_entries})`); - const pending = store.compact(); - expect(pending instanceof Promise, 'compact returns a Promise'); - const reclaimed = await pending; - expect(typeof reclaimed === 'bigint', 'compact resolves with a BigInt'); - expect(reclaimed === 3n, `compact reclaimed 3 bytes (got ${reclaimed})`); - expect((await store.compact()) === 0n, 'second compact reclaims nothing'); - expect(store.count() === 2n, 'alpha and beta remain'); - store.clear(); - expect(store.count() === 0n, 'store empty after clear'); - - store.close(); - store.close(); - try { - await store.compact(); - expect(false, 'expected compact on a closed store to throw'); - } catch (e) { - expect(e instanceof wv.WeaveFFIError && e.code === -3, `compact after close is code -3 (got ${e && e.code})`); - } - - if (failures > 0) process.exit(1); - console.log('node/kvstore: OK'); -})().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/conformance/node/kvstore.mjs b/conformance/node/kvstore.mjs new file mode 100644 index 00000000..14549f2b --- /dev/null +++ b/conformance/node/kvstore.mjs @@ -0,0 +1,176 @@ +// Conformance consumer: kvstore sample (node and wasm lanes). +// +// The Store class (static factories, methods, statics, close()), the typed +// errors of the `kv` domain, records with bytes, optional, list, and map +// fields, the lazy `listKeys` iterator, the `kv.stats` submodule, the +// `EvictionListener` callback interface, objects in every buffered position +// (`share`, `fork`, `larger`, `describe`, `openMany`, `totalCount`), the +// cancellable async `compact`, and leak-free teardown. + +import { expect, finish, isWasm, load, rejects, same, throws } from './harness.mjs'; + +const api = await load('kvstore'); +const { kv, KvstoreError, CancelledError } = api; +const { EntryKind, EvictionReason } = kv; + +class Listener { + constructor(stopAfter = Infinity) { + this.stopAfter = stopAfter; + this.seen = []; + } + + onEvict(entry, reason) { + this.seen.push({ key: entry.key, reason, entry }); + return this.seen.length < this.stopAfter; + } +} + +const isClosed = (e) => e instanceof KvstoreError && e.code === -3; + +async function main() { + expect(EntryKind.Persistent === 1 && EvictionReason[1] === 'Expired', 'enums'); + throws( + () => kv.Store.open(''), + (e) => e instanceof kv.IoError && e instanceof kv.KvError && e instanceof KvstoreError && e.code === 1004 && kv.IoError.CODE === 1004, + 'a typed error from a factory', + ); + throws(() => new kv.Store(), (e) => e instanceof TypeError, 'Store has no public constructor'); + + const store = kv.Store.open('/tmp/conformance-kvstore'); + expect(kv.Store.defaultCapacity() === 1000000n, 'a static method'); + const payload = new Uint8Array([1, 2, 3]); + expect(store.put('alpha', payload, EntryKind.Persistent, null), 'put without a ttl'); + expect(store.put('beta', payload, EntryKind.Volatile, 3600n), 'put with a bigint ttl'); + expect(store.put('gamma', payload, EntryKind.Encrypted, 7200), 'put with a number ttl'); + expect(store.count() === 3n, 'count'); + + same([...store.listKeys(null)], ['alpha', 'beta', 'gamma'], 'listKeys'); + same([...store.listKeys('al')], ['alpha'], 'listKeys with a prefix'); + const keys = store.listKeys(null); + expect(keys.next().value === 'alpha', 'lazy first key'); + keys.close(); + expect(keys.next().done, 'a closed iterator is done'); + store.listKeys(null).next(); + + const alpha = store.get('alpha'); + expect(alpha.id === 1n && alpha.key === 'alpha' && alpha.created_at > 0n, 'record fields'); + expect(alpha.value instanceof Uint8Array, 'bytes are a Uint8Array'); + same(alpha.value, payload, 'bytes round-trip'); + same([alpha.expires_at, alpha.tags, alpha.metadata], [null, [], {}], 'empty optional, list, and map'); + const beta = store.get('beta'); + expect(beta.expires_at === beta.created_at + 3600n, 'an optional bigint'); + throws( + () => store.get('missing'), + (e) => e instanceof kv.KeyNotFoundError && e.code === 1001 && e.message === 'key not found', + 'a typed error from a method', + ); + expect(store.legacyPut('legacy', payload) && store.delete('legacy') && !store.delete('legacy'), 'legacyPut and delete'); + same(kv.stats.getStats(store), { total_entries: 3n, total_bytes: 9n, expired_entries: 0n }, 'the stats submodule'); + + // The eviction listener. + const l1 = new Listener(); + store.setEvictionListener(l1); + expect(store.delete('gamma') && l1.seen.length === 1, 'the listener fires synchronously'); + expect(l1.seen[0].key === 'gamma' && l1.seen[0].reason === EvictionReason.Deleted, 'listener arguments'); + same(l1.seen[0].entry.value, payload, 'the listener decodes its record'); + expect(store.put('doomed', new Uint8Array([9]), EntryKind.Volatile, -1n), 'put an expired entry'); + throws(() => store.get('doomed'), (e) => e instanceof kv.ExpiredError && e.code === 1002, 'expired'); + expect(l1.seen[1].reason === EvictionReason.Expired, 'eviction on read'); + const l2 = new Listener(1); + store.setEvictionListener(l2); + store.put('x1', payload, EntryKind.Volatile, null); + store.put('x2', payload, EntryKind.Volatile, null); + store.delete('x1'); + store.delete('x2'); + expect(l2.seen.length === 1 && l1.seen.length === 2, 'a listener returning false detaches'); + store.setEvictionListener({ + onEvict(entry) { + throw new TypeError('listener refuses ' + entry.key); + }, + }); + store.put('x3', payload, EntryKind.Volatile, null); + throws( + () => store.delete('x3'), + (e) => e instanceof KvstoreError && !(e instanceof kv.KvError) && e.code === -4 && e.message.includes('listener refuses x3'), + 'a throwing listener fails the call with code -4', + ); + store.setEvictionListener({ onEvict: () => 'yes' }); + store.put('x4', payload, EntryKind.Volatile, null); + throws(() => store.delete('x4'), (e) => e.code === -4, 'a wrong return type is code -4'); + store.clearEvictionListener(); + expect(store.count() === 2n, 'alpha and beta remain'); + + // Objects in every position. + const shared = store.share(); + expect(shared instanceof kv.Store && shared !== store, 'share returns a new wrapper'); + shared.put('via-shared', payload, EntryKind.Volatile, null); + expect(store.count() === 3n, 'the wrappers share the object'); + shared.close(); + store.delete('via-shared'); + const forked = store.fork(); + forked.put('only-in-fork', payload, EntryKind.Volatile, null); + expect(forked.count() === 3n && store.count() === 2n, 'fork is independent'); + const empty = kv.Store.open('/tmp/empty'); + expect(empty.larger(null) === null && empty.larger(undefined) === null, 'a null object result'); + const bigger = store.larger(forked); + expect(bigger.count() === 3n, 'an object argument and result'); + bigger.close(); + const info = store.describe('primary', forked); + expect(info.label === 'primary' && info.count === 2n, 'a record with objects'); + expect(info.store instanceof kv.Store && info.mirror instanceof kv.Store, 'object fields'); + expect(info.mirror.get('only-in-fork').key === 'only-in-fork', 'an object field aliases its object'); + const many = kv.Store.openMany(['/tmp/m1', '/tmp/m2']); + expect(many.length === 2 && many.every((s) => s.count() === 0n), 'a list of objects'); + throws(() => kv.Store.openMany(['/tmp/ok', '']), (e) => e instanceof kv.IoError, 'openMany fails as a whole'); + expect(kv.Store.totalCount([store, forked], null) === 5n, 'a list of objects as an argument'); + expect(kv.Store.totalCount(many, info) === 2n, 'a record of objects as an argument'); + expect(kv.Store.totalCount([], { label: 'x', store: forked, mirror: null, count: 0n }) === 3n, 'a hand-built record'); + info.store.close(); + info.mirror.close(); + for (const s of many) s.close(); + forked.close(); + throws(() => forked.count(), isClosed, 'use after close'); + throws(() => kv.Store.totalCount([forked], null), isClosed, 'a closed object in a buffer'); + throws(() => kv.stats.getStats(forked), isClosed, 'a closed object argument'); + throws(() => kv.stats.getStats({}), (e) => e instanceof TypeError, 'a non-Store argument'); + + // A close() racing a call: the listener closes the store mid-call, and + // the release waits for the call to return. + const racing = kv.Store.open('/tmp/racing'); + racing.setEvictionListener({ + onEvict() { + racing.close(); + return true; + }, + }); + racing.put('k', payload, EntryKind.Volatile, null); + expect(racing.delete('k') === true, 'the call completes after a close() from its callback'); + throws(() => racing.count(), isClosed, 'the deferred close took effect'); + + // Async compaction, and its cancellation. + store.put('dead', payload, EntryKind.Volatile, 0n); + const pending = store.compact(); + expect(pending instanceof Promise, 'compact returns a Promise'); + expect((await pending) === 3n, 'compact reclaims the expired entry'); + const aborted = new AbortController(); + aborted.abort(); + await rejects( + store.compact({ signal: aborted.signal }), + (e) => e instanceof CancelledError && e.code === -5, + 'a cancelled compact', + ); + expect((await store.compact({ signal: new AbortController().signal })) === 0n, 'compact with a live signal'); + if (!isWasm(api)) { + // The pending call keeps the store alive past close(). + const p = store.compact(); + store.close(); + expect((await p) === 0n, 'a pending call survives close()'); + } else { + store.close(); + } + await rejects(store.compact(), isClosed, 'compact after close'); + empty.close(); +} + +await main(); +await finish(api, 'kvstore'); diff --git a/conformance/node/run.sh b/conformance/node/run.sh new file mode 100755 index 00000000..153e8731 --- /dev/null +++ b/conformance/node/run.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Conformance lanes for the node target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane runs one consumer +# against one sample and must exit 0. +# +# Each lane installs the generated package the way an npm user would: `npm +# pack` it, then `npm install` the tarball into a scratch project, which +# builds the N-API addon with node-gyp. `{PREFIX}_LIBRARY` points the build +# at the sample's library (node-gyp fetches Node.js headers on first use). +# The consumers in this directory are shared with the wasm lanes, since both +# targets generate the same JavaScript API. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +node_consumer() { + local sample="$1" src="$2" + local dir="$OUT/node-$sample" tarball + command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; return 1; } + rm -rf "$dir" + mkdir -p "$dir" + tarball=$(cd "$dir" && npm pack --silent "$GENROOT/$sample/node") \ + || { echo "npm pack failed" >&2; return 1; } + printf '{ "name": "consumer", "private": true, "type": "module" }\n' > "$dir/package.json" + cp "$ROOT/conformance/node/harness.mjs" "$ROOT/conformance/node/$src" "$dir/" + ( cd "$dir" && env "$(library_env "$sample")=$(sample_lib "$sample")" \ + npm install --no-audit --no-fund --loglevel=error "./$tarball" ) \ + || { echo "npm install (node-gyp build) failed" >&2; return 1; } + ( cd "$dir" && node --expose-gc "$src" ) +} + +node_contacts() { node_consumer contacts contacts.mjs; } + +node_events() { node_consumer events events.mjs; } + +node_kvstore() { node_consumer kvstore kvstore.mjs; } + +node_async_demo() { node_consumer async-demo async_demo.mjs; } + +node_codec() { node_consumer codec codec.mjs; } + +lane node-contacts node_contacts +lane node-events node_events +lane node-kvstore node_kvstore +lane node-async-demo node_async_demo +lane node-codec node_codec + +finish_lanes diff --git a/conformance/node/shapes.js b/conformance/node/shapes.js deleted file mode 100644 index 47f9ba5a..00000000 --- a/conformance/node/shapes.js +++ /dev/null @@ -1,71 +0,0 @@ -// Conformance consumer: shapes sample, Node (N-API) target. -// -// Drives the generated rich (algebraic) enum surface through the wrapper -// layer (index.js): shapes are plain tagged objects ({ tag: 'Circle', -// radius: 2.5 }) that the wrapper packs into value buffers on the way in -// and unpacks on the way out. Covers every variant round-tripping through -// `scale` (unit, f64, f32 pair, and string + u8 payloads), `describe` -// dispatching on the active variant, and the expanded numerics (bytes in, -// u64 out) under the default lowerCamelCase, module-prefix-stripped names. -// Mirrors conformance/c/shapes.c and conformance/cpp/shapes.cpp. Exits -// non-zero on any failed assertion; prints `node/shapes: OK` on success. - -'use strict'; - -const path = require('path'); - -const ADDON = path.resolve(process.env.WV_ADDON); -process.env.WEAVEFFI_ADDON = ADDON; -// WV_ADDON = /conformance-build/node-shapes/build/Release/index.node; -// the generated files sit at /conformance-gen/shapes/node/. -const wv = require( - path.resolve(ADDON, '../../../../..', 'conformance-gen', 'shapes', 'node', 'index.js') -); - -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - process.exit(1); - } -} - -function approx(actual, expected, eps, msg) { - expect(Math.abs(actual - expected) < eps, `${msg} (got ${actual}, want ${expected})`); -} - -// Empty (unit variant): tag only, no payload. Round-tripping through scale -// exercises pack and unpack of the bare tag. -const empty = { tag: 'Empty' }; -expect(wv.describe(empty) === 'empty', 'describe(empty)'); -const emptyBack = wv.scale(empty, 2.0); -expect(typeof emptyBack === 'object' && emptyBack !== null, 'scale returns an object'); -expect(emptyBack.tag === 'Empty', 'empty tag round-trips'); - -// Circle (f64 payload). -const circle = { tag: 'Circle', radius: 2.5 }; -expect(wv.describe(circle) === 'circle(r=2.5)', 'describe(circle)'); -const big = wv.scale(circle, 4.0); -expect(big.tag === 'Circle', 'scaled tag is Circle'); -approx(big.radius, 10.0, 1e-9, 'scaled radius'); - -// Rectangle (two f32 payloads). -const rect = { tag: 'Rectangle', width: 3.0, height: 4.0 }; -expect(wv.describe(rect) === 'rectangle(3x4)', 'describe(rectangle)'); -const grown = wv.scale(rect, 2.0); -expect(grown.tag === 'Rectangle', 'scaled tag is Rectangle'); -approx(grown.width, 6.0, 1e-6, 'scaled rectangle width'); -approx(grown.height, 8.0, 1e-6, 'scaled rectangle height'); - -// Labeled (string + u8 payload): scale leaves the payload alone, so the -// round trip checks both fields survive pack and unpack intact. -const labeled = { tag: 'Labeled', label: 'hex', count: 6 }; -expect(wv.describe(labeled) === 'labeled(hex x6)', 'describe(labeled)'); -const labeledBack = wv.scale(labeled, 3.0); -expect(labeledBack.tag === 'Labeled', 'labeled tag round-trips'); -expect(labeledBack.label === 'hex', 'labeled label is "hex"'); -expect(Number(labeledBack.count) === 6, 'labeled count is 6'); - -// Numerics: bytes in ([u8] canonicalizes to bytes, so a Buffer), u64 out. -expect(Number(wv.sumBytes(Buffer.from([250, 250, 250, 250]))) === 1000, 'sumBytes == 1000'); - -console.log('node/shapes: OK'); diff --git a/conformance/python/async_demo.py b/conformance/python/async_demo.py deleted file mode 100644 index a94e7b52..00000000 --- a/conformance/python/async_demo.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Conformance consumer: async-demo sample, Python target. - -Drives the asyncio-bridged async surface end to end: `run_task` as a -coroutine settled from the producer's worker thread and decoded from a value -buffer into the `TaskResult` dataclass, the typed `TaskError.InvalidName` -raised by a throwing coroutine, the buffered list-of-records round trip -through `run_batch`, the direct-scalar `run_n_tasks`, the sync `cancel_task`, -and `active_callbacks` returning to zero once every task body has completed. -The generated package is placed on sys.path via WV_PY; the cdylib is selected -with WEAVEFFI_LIBRARY. -""" -import asyncio -import os -import sys - -sys.path.insert(0, os.environ["WV_PY"]) - -import async_demo as wv # noqa: E402 - - -def main() -> None: - # Async record return: the coroutine resolves with a TaskResult decoded - # from the completion callback's value buffer. - result = asyncio.run(wv.run_task("alpha")) - assert isinstance(result, wv.TaskResult), type(result) - assert result.id > 0, result.id - assert result.value == "completed: alpha", result.value - assert result.success is True - - # Typed async error: an empty name settles the coroutine with the - # InvalidName subclass carrying its stable code. - try: - asyncio.run(wv.run_task("")) - raise AssertionError("expected TaskError.InvalidName for empty name") - except wv.TaskError.InvalidName as exc: - assert exc.code == 1, exc.code - assert isinstance(exc, wv.TaskError) - assert isinstance(exc, wv.WeaveFFIError) - - # Buffered list-of-records both ways: the names list crosses in one value - # buffer, the results come back in another. - batch = asyncio.run(wv.run_batch(["a", "b", "c"])) - assert [r.value for r in batch] == [ - "completed: a", - "completed: b", - "completed: c", - ], batch - assert all(r.success for r in batch) - - # Direct scalar through the async callback. - assert asyncio.run(wv.run_n_tasks(7)) == 7 - - # Sync functions beside the async ones. - assert wv.cancel_task(1) is False - - # Every spawned task body has completed by the time its callback fires. - assert wv.active_callbacks() == 0 - - print("python async-demo conformance: OK") - - -if __name__ == "__main__": - main() diff --git a/conformance/python/async_demo_consumer.py b/conformance/python/async_demo_consumer.py new file mode 100644 index 00000000..9a42c1db --- /dev/null +++ b/conformance/python/async_demo_consumer.py @@ -0,0 +1,110 @@ +"""Conformance consumer: async-demo sample, Python target. + +Drives the asyncio-bridged async surface end to end: `run_task` as a +coroutine settled from the producer's worker thread and decoded from a value +buffer into the `TaskResult` dataclass, the typed `TaskError.InvalidName` +raised by a throwing coroutine, the buffered list-of-records round trip +through `run_batch`, the direct-scalar `run_n_tasks` (many at once), and +real cancellation: cancelling the task awaiting the cancellable `wait` +cancels the native token, so the call ends promptly with CancelledError +instead of waiting out its timeout, while an uncancelled `wait` completes +with its value. Ends with `active_callbacks` back at zero and the leak check +(see harness.py). +""" +import asyncio +import time + +import async_demo as wv +from harness import Consumer + +consumer = Consumer("async-demo") +check = consumer.check + + +async def basics() -> None: + result = await wv.run_task("alpha") + check(isinstance(result, wv.TaskResult), f"run_task type {type(result)}") + check(result.id > 0 and result.value == "completed: alpha" and result.success is True, + f"run_task {result}") + + try: + await wv.run_task("") + check(False, "expected TaskError.InvalidName for an empty name") + except wv.TaskError.InvalidName as exc: + check(exc.code == 1 and isinstance(exc, wv.TaskError) and isinstance(exc, wv.Error), + f"InvalidName {exc!r}") + check(exc.message == "task name must not be empty", f"message {exc.message!r}") + + batch = await wv.run_batch(["a", "b", "c"]) + check([r.value for r in batch] == ["completed: a", "completed: b", "completed: c"], + f"run_batch {batch}") + check(await wv.run_batch([]) == [], "empty run_batch") + + results = await asyncio.gather(*(wv.run_n_tasks(i) for i in range(32))) + check(results == list(range(32)), f"run_n_tasks gather {results}") + + +async def cancellation() -> None: + # An uncancelled wait completes with the milliseconds waited. + check(await wv.wait(10) == 10, "wait(10)") + + # Cancelling the awaiting task cancels the native call: it ends right + # away with CancelledError instead of after its one-minute timeout. + start = time.monotonic() + task = asyncio.ensure_future(wv.wait(60_000)) + await asyncio.sleep(0.05) + task.cancel() + try: + await task + check(False, "expected CancelledError") + except asyncio.CancelledError: + pass + check(task.cancelled(), "task reports cancelled") + check(time.monotonic() - start < 5, "cancellation did not wait for the timeout") + + # A timeout is a cancellation too. + start = time.monotonic() + try: + await asyncio.wait_for(wv.wait(60_000), 0.05) + check(False, "expected TimeoutError") + except asyncio.TimeoutError: + pass + check(time.monotonic() - start < 5, "wait_for did not wait for the native timeout") + + # Cancelling before the call ever runs is fine as well. + task = asyncio.ensure_future(wv.wait(60_000)) + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + check(task.cancelled(), "immediately cancelled task") + + # Many concurrent cancellable calls, half of them cancelled. + tasks = [asyncio.ensure_future(wv.wait(20 if i % 2 else 60_000)) for i in range(16)] + await asyncio.sleep(0.05) + for i, t in enumerate(tasks): + if not i % 2: + t.cancel() + outcomes = await asyncio.gather(*tasks, return_exceptions=True) + for i, outcome in enumerate(outcomes): + if i % 2: + check(outcome == 20, f"wait {i} completed with {outcome!r}") + else: + check(isinstance(outcome, asyncio.CancelledError), f"wait {i} cancelled: {outcome!r}") + + +def main() -> None: + asyncio.run(basics()) + asyncio.run(cancellation()) + # Every task body has finished (or been dropped) by the time its + # completion fires. + for _ in range(50): + if wv.active_callbacks() == 0: + break + time.sleep(0.02) + check(wv.active_callbacks() == 0, "active_callbacks back at zero") + consumer.finish(wv) + + +main() diff --git a/conformance/python/calculator_consumer.py b/conformance/python/calculator_consumer.py new file mode 100644 index 00000000..22844a4e --- /dev/null +++ b/conformance/python/calculator_consumer.py @@ -0,0 +1,31 @@ +"""Conformance consumer: calculator sample, Python target. + +The smallest surface: direct `i32` arguments and returns, a throwing call +raising the typed `CalcError.DivisionByZero`, and a string round trip +(including an interior NUL and non-ASCII text, since strings cross as +pointer + length). Ends with the leak check (see harness.py). +""" +import calculator as wv +from harness import Consumer + +consumer = Consumer("calculator") +check = consumer.check + + +def main() -> None: + check(wv.add(2, 3) == 5, "add") + check(wv.mul(-4, 6) == -24, "mul") + check(wv.div(17, 5) == 3, "div") + try: + wv.div(1, 0) + check(False, "expected DivisionByZero") + except wv.CalcError.DivisionByZero as exc: + check(exc.code == 1 and isinstance(exc, wv.CalcError) and isinstance(exc, wv.Error), + f"DivisionByZero {exc!r}") + check(exc.message == "division by zero", f"message {exc.message!r}") + for s in ["", "hello", "nul\0inside", "\0", "héllo wörld ✓"]: + check(wv.echo(s) == s, f"echo {s!r}") + consumer.finish(wv) + + +main() diff --git a/conformance/python/codec.py b/conformance/python/codec_consumer.py similarity index 97% rename from conformance/python/codec.py rename to conformance/python/codec_consumer.py index 84f34fea..fd82496e 100644 --- a/conformance/python/codec.py +++ b/conformance/python/codec_consumer.py @@ -11,28 +11,22 @@ and optional families round-trip; and `Holder` carries `Token` objects in a field, an optional, and a list (`sum_holder`, `primary_of` returning the same underlying object, `same_primary`, and every wrapper released with an -idempotent `close()`). The generated package is placed on sys.path via WV_PY; -the cdylib is selected with WEAVEFFI_LIBRARY. +idempotent `close()`). Ends with the leak check (see harness.py). """ import dataclasses import gc import math -import os -import sys -sys.path.insert(0, os.environ["WV_PY"]) - -import codec as wv # noqa: E402 +import codec as wv +from harness import Consumer I64_MIN = -(1 << 63) I64_MAX = (1 << 63) - 1 U64_MAX = (1 << 64) - 1 -def check(cond: bool, what: str) -> None: - if not cond: - print(f"python/codec: FAIL: {what}", file=sys.stderr) - sys.exit(1) +consumer = Consumer("codec") +check = consumer.check def same_float(a: float, b: float) -> bool: @@ -115,7 +109,7 @@ def scalars() -> None: check(False, "expected CodecError.Mismatch") except wv.CodecError.Mismatch as exc: check(exc.code == 1 and exc.CODE == 1, f"Mismatch code {exc.code}") - check(isinstance(exc, wv.CodecError) and isinstance(exc, wv.WeaveFFIError), "hierarchy") + check(isinstance(exc, wv.CodecError) and isinstance(exc, wv.Error), "hierarchy") check(exc.message == "value does not match the canonical fixture", f"Mismatch message {exc.message!r}") check(wv.Mismatch is wv.CodecError.Mismatch, "bare Mismatch is the scoped alias") @@ -278,6 +272,8 @@ def direct_families() -> None: m = {"": 0, "a": I64_MIN, "ü": I64_MAX, "z" * 300: -1} check(wv.roundtrip_map(m) == m, "map contents") check(wv.roundtrip_string("") == "", "string ''") + for s in ("a\0b", "\0", "trailing\0", "\0\0leading"): + check(wv.roundtrip_string(s) == s, f"interior NUL string {s!r}") check(wv.roundtrip_string("héllo 日本語 🧵") == "héllo 日本語 🧵", "unicode string") check(wv.roundtrip_bytes(b"") == b"", "bytes b''") check(wv.roundtrip_bytes(bytes(range(256))) == bytes(range(256)), "all byte values") @@ -343,14 +339,14 @@ def holders() -> None: try: wv.sum_holder(broken) check(False, "expected error encoding a closed token") - except wv.WeaveFFIError as exc: + except wv.Error as exc: check("Token used after close" in exc.message, f"closed token message {exc.message!r}") check(a.value() == 1, "a still alive inside the with block") try: a.value() check(False, "expected use-after-close error") - except wv.WeaveFFIError: + except wv.Error: pass # Release every producer-built wrapper explicitly (twice), then let the @@ -368,8 +364,7 @@ def main() -> None: shapes() direct_families() holders() - gc.collect() - print("python/codec: OK") + consumer.finish(wv) main() diff --git a/conformance/python/contacts.py b/conformance/python/contacts.py deleted file mode 100644 index a514a0bb..00000000 --- a/conformance/python/contacts.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Conformance consumer: contacts sample, Python target. - -Exercises the generated ctypes wrapper end to end: the reference-counted -`ContactBook` interface (its `__init__` calls the C constructor; `close()`, -the `with` statement, or `__del__` release the reference through the destroy -symbol), enum marshalling, the `Contact` record decoded from a value buffer -into a plain dataclass with value equality, optional strings (a buffered -`string?` parameter and field), list-of-record returns, boolean returns, and -the typed `ContactsError` subclasses (`InvalidName`, `NotFound`) raised by -throwing methods. The generated module is placed on sys.path via WV_PY; the -cdylib is selected with WEAVEFFI_LIBRARY. -""" -import os -import sys - -sys.path.insert(0, os.environ["WV_PY"]) - -import contacts as wv # noqa: E402 - - -def main() -> None: - book = wv.ContactBook() - assert book.count() == 0 - - alice = book.add("Alice", "Smith", "alice@example.com", wv.ContactType.Work) - assert alice.id > 0 - assert alice.first_name == "Alice" - assert alice.last_name == "Smith" - assert alice.email == "alice@example.com" - assert alice.contact_type == wv.ContactType.Work - - # Optional string: a missing email round-trips as None. Records are - # plain dataclass values, so a fetched contact compares equal to a - # locally constructed one field by field. - bob = book.add("Bob", "Jones", None, wv.ContactType.Personal) - fetched = book.get(bob.id) - assert fetched.email is None - assert fetched.contact_type == wv.ContactType.Personal - assert fetched == wv.Contact( - id=bob.id, - first_name="Bob", - last_name="Jones", - email=None, - contact_type=wv.ContactType.Personal, - ) - - assert book.count() == 2 - everyone = book.list() - assert len(everyone) == 2 - assert {p.first_name for p in everyone} == {"Alice", "Bob"} - - # Typed error: an empty name is rejected with the InvalidName class of - # the ContactsError domain. - try: - book.add("", "Smith", None, wv.ContactType.Personal) - raise AssertionError("expected InvalidName for empty first name") - except wv.ContactsError.InvalidName as exc: - assert exc.code == 1, exc.code - assert isinstance(exc, wv.ContactsError) - assert isinstance(exc, wv.WeaveFFIError) - assert book.count() == 2 - - assert book.remove(alice.id) is True - assert book.remove(alice.id) is False - assert book.count() == 1 - - # Typed error: a missing id raises NotFound; the bare name is the same - # class as the scoped alias. - try: - book.get(9999) - raise AssertionError("expected NotFound for missing contact") - except wv.NotFound as exc: - assert exc.code == 2, exc.code - assert isinstance(exc, wv.ContactsError) - assert wv.NotFound is wv.ContactsError.NotFound - - # A second book is independent state: contacts don't leak across objects. - # Objects are context managers with an idempotent close(); a closed - # wrapper rejects further use. - with wv.ContactBook() as other: - assert other.count() == 0 - other.close() - other.close() - try: - other.count() - raise AssertionError("expected use-after-close error") - except wv.WeaveFFIError as exc: - assert "after close" in exc.message, exc.message - assert book.count() == 1 - book.close() - - print("python/contacts: OK") - - -main() diff --git a/conformance/python/contacts_consumer.py b/conformance/python/contacts_consumer.py new file mode 100644 index 00000000..dac73433 --- /dev/null +++ b/conformance/python/contacts_consumer.py @@ -0,0 +1,132 @@ +"""Conformance consumer: contacts sample, Python target. + +Exercises the generated ctypes wrapper end to end: the reference-counted +`ContactBook` interface (its `__init__` calls the C constructor; `close()`, +the `with` statement, or `__del__` release the reference), enum marshalling, +the `Contact` record decoded from a value buffer into a plain dataclass with +value equality, optional strings (a buffered `string?` parameter and field), +list-of-record returns, boolean returns, the typed `ContactsError` +subclasses raised by throwing methods, and cross-module types: the nested +`contacts.groups` module taking and returning the parent's `ContactBook`, +`ContactType`, and `Contact`, and the sibling `directory` module taking a +`Contact` record and raising its own `DirectoryError` domain. Ends with the +leak check (see harness.py). +""" +import contacts as wv +from harness import Consumer + +consumer = Consumer("contacts") +check = consumer.check + + +def book_basics() -> None: + book = wv.ContactBook() + check(book.count() == 0, "fresh book is empty") + + alice = book.add("Alice", "Smith", "alice@example.com", wv.ContactType.Work) + check(alice.id > 0 and alice.first_name == "Alice" and alice.last_name == "Smith", + f"alice {alice}") + check(alice.email == "alice@example.com" and alice.contact_type == wv.ContactType.Work, + f"alice email and type {alice}") + + # Optional string: a missing email round-trips as None. Records are + # plain dataclass values, so a fetched contact compares equal to a + # locally constructed one field by field. + bob = book.add("Bob", "Jones", None, wv.ContactType.Personal) + fetched = book.get(bob.id) + check(fetched == wv.Contact(id=bob.id, first_name="Bob", last_name="Jones", email=None, + contact_type=wv.ContactType.Personal), f"fetched {fetched}") + # Strings cross as pointer + length, so an interior NUL survives. + nul = book.add("Nul\0Name", "Tail\0", "a\0b", wv.ContactType.Other) + check(book.get(nul.id).first_name == "Nul\0Name" and nul.email == "a\0b", + f"interior NUL round trip {nul}") + check(book.remove(nul.id) is True, "remove nul") + + check(book.count() == 2, "two contacts") + everyone = book.list() + check({p.first_name for p in everyone} == {"Alice", "Bob"}, f"list {everyone}") + + # Typed errors carry stable codes and the class hierarchy. + try: + book.add("", "Smith", None, wv.ContactType.Personal) + check(False, "expected InvalidName for an empty first name") + except wv.ContactsError.InvalidName as exc: + check(exc.code == 1 and isinstance(exc, wv.ContactsError) and isinstance(exc, wv.Error), + f"InvalidName {exc!r}") + check(exc.message == "name must not be empty", f"InvalidName message {exc.message!r}") + check(book.count() == 2, "failed add left the book unchanged") + + check(book.remove(alice.id) is True, "remove alice") + check(book.remove(alice.id) is False, "remove alice twice") + check(book.count() == 1, "one contact left") + + try: + book.get(9999) + check(False, "expected NotFound for a missing contact") + except wv.NotFound as exc: + check(exc.code == 2 and isinstance(exc, wv.ContactsError), f"NotFound {exc!r}") + check(wv.NotFound is wv.ContactsError.NotFound, "bare NotFound is the scoped alias") + + # Objects are context managers with an idempotent close(); a closed + # wrapper rejects further use. + with wv.ContactBook() as other: + check(other.count() == 0, "second book is independent") + other.close() + other.close() + try: + other.count() + check(False, "expected a use-after-close error") + except wv.Error as exc: + check("after close" in exc.message, f"use-after-close message {exc.message!r}") + book.close() + + +def nested_and_sibling_modules() -> None: + book = wv.ContactBook() + work = book.add("Wanda", "Work", None, wv.ContactType.Work) + book.add("Walt", "Work", "w@example.com", wv.ContactType.Work) + book.add("Pat", "Home", None, wv.ContactType.Personal) + + # contacts.groups takes the parent module's interface and enum. + check(wv.count_of_type(book, wv.ContactType.Work) == 2, "count_of_type Work") + check(wv.count_of_type(book, wv.ContactType.Other) == 0, "count_of_type Other") + dominant = wv.dominant_type(book) + check(dominant is wv.ContactType.Work, f"dominant_type {dominant!r}") + + # ... and returns the parent's interface and record. + split = wv.split_by_type(book, wv.ContactType.Work) + check(isinstance(split, wv.ContactBook) and split.count() == 2, "split_by_type") + check(book.count() == 3, "split_by_type copies instead of moving") + first = wv.first_of_type(book, wv.ContactType.Work) + check(first == work, f"first_of_type {first}") + try: + wv.first_of_type(split, wv.ContactType.Personal) + check(False, "expected NotFound from the nested module") + except wv.ContactsError.NotFound as exc: + check(exc.code == 2, f"nested NotFound {exc!r}") + split.close() + + # The sibling directory module takes a contacts record by value. + card = wv.card(first) + check(card == wv.Card(display_name="Work, Wanda", initials="WW", has_email=False), + f"card {card}") + ordered = wv.sorted(book.list()) + check([c.first_name for c in ordered] == ["Pat", "Walt", "Wanda"], + f"sorted {[c.first_name for c in ordered]}") + try: + wv.sorted([]) + check(False, "expected DirectoryError.Empty") + except wv.DirectoryError.Empty as exc: + check(exc.code == 1 and not isinstance(exc, wv.ContactsError), + f"Empty is its own domain {exc!r}") + check(exc.message == "no contacts to list", f"Empty message {exc.message!r}") + book.close() + + +def main() -> None: + book_basics() + nested_and_sibling_modules() + consumer.finish(wv) + + +main() diff --git a/conformance/python/events.py b/conformance/python/events_consumer.py similarity index 82% rename from conformance/python/events.py rename to conformance/python/events_consumer.py index 353de9b8..047231fc 100644 --- a/conformance/python/events.py +++ b/conformance/python/events_consumer.py @@ -1,33 +1,31 @@ """Conformance consumer: events sample, Python target. -Exercises the ABI 2 events surface through the generated ctypes wrapper: the +Exercises the ABI 3 events surface through the generated ctypes wrapper: the `Subscriber` callback interface implemented by subclassing the generated ABC (every method is called with the right arguments, the `EventBus` handed to `on_attached` is a usable wrapper that owns its own strong reference), the -`Delivery` return steering `publish`'s accepted count, the reference-counted +`Delivery` return of `route` (a `Result`-returning method on the producer +side, so a raising implementation reaches the producer as a `ForeignError` +value) steering `publish`'s accepted count, the reference-counted `EventBus` object (`with` statement, idempotent `close()`, use after close rejected), the asyncio-bridged `publish_later` method, the `messages()` iterator, the `last_message()` optional record, `route_once` releasing a subscriber it doesn't retain, and a subscriber that raises a Python exception -surfacing to the caller as `WeaveFFIError` with `FOREIGN_ERROR_CODE` (-4) -without taking the interpreter down. The generated package is placed on -sys.path via WV_PY; the cdylib is selected with WEAVEFFI_LIBRARY. +surfacing to the caller as `Error` with `FOREIGN_ERROR_CODE` (-4) without +taking the interpreter down, and subscribers called from a producer thread +other than the one Python called in on. Ends with the leak check (see +harness.py). """ import asyncio import gc -import os -import sys +import threading import weakref -sys.path.insert(0, os.environ["WV_PY"]) +import events as wv +from harness import Consumer -import events as wv # noqa: E402 - - -def check(cond: bool, what: str) -> None: - if not cond: - print(f"python/events: FAIL: {what}", file=sys.stderr) - sys.exit(1) +consumer = Consumer("events") +check = consumer.check class Recorder(wv.Subscriber): @@ -163,15 +161,16 @@ def main() -> None: check(last == wv.Message(seq=4, topic="later", text="async", tags=[]), f"last_message {last}") - # A subscriber that raises from route aborts publish with the foreign - # error code, and the bus is still usable afterwards. + # A subscriber that raises from route (the Result-returning method) + # aborts publish with the foreign error code, and the bus is still + # usable afterwards. grumpy = Recorder("grumpy", fail_topic="boom", fail_text="gag") check(bus.subscribe(grumpy) == 4, "grumpy subscribed") try: bus.publish("boom", "x", []) - check(False, "expected WeaveFFIError from a raising route") - except wv.WeaveFFIError as exc: - check(exc.code == wv.WeaveFFIError.FOREIGN_ERROR_CODE == -4, + check(False, "expected Error from a raising route") + except wv.Error as exc: + check(exc.code == wv.Error.FOREIGN_ERROR_CODE == -4, f"foreign error code {exc.code}") check("grumpy rejected topic boom" in exc.message, f"foreign error message {exc.message!r}") @@ -180,8 +179,8 @@ def main() -> None: # A raise from on_message is reported the same way. try: bus.publish("ok", "gag", []) - check(False, "expected WeaveFFIError from a raising on_message") - except wv.WeaveFFIError as exc: + check(False, "expected Error from a raising on_message") + except wv.Error as exc: check(exc.code == -4, f"on_message foreign code {exc.code}") check("choked on gag" in exc.message, f"on_message message {exc.message!r}") # The log still recorded both aborted publishes. @@ -197,8 +196,8 @@ def main() -> None: exploding_ref = weakref.ref(exploding) try: bus.subscribe(exploding) - check(False, "expected WeaveFFIError from a raising on_attached") - except wv.WeaveFFIError as exc: + check(False, "expected Error from a raising on_attached") + except wv.Error as exc: check(exc.code == -4, f"on_attached foreign code {exc.code}") check("refusing to attach" in exc.message, f"on_attached message {exc.message!r}") check(bus.subscriber_count() == 4, "failed subscribe did not retain") @@ -219,8 +218,8 @@ def main() -> None: check(probe_ref() is None, "route_once released its subscriber") try: wv.route_once(Recorder("x", fail_topic="bad"), "bad") - check(False, "expected WeaveFFIError from route_once") - except wv.WeaveFFIError as exc: + check(False, "expected Error from route_once") + except wv.Error as exc: check(exc.code == -4, f"route_once foreign code {exc.code}") # Clearing the subscribers runs each free: once the consumer drops its @@ -242,7 +241,7 @@ def main() -> None: try: bus.subscriber_count() check(False, "expected use-after-close error") - except wv.WeaveFFIError as exc: + except wv.Error as exc: check("after close" in exc.message, f"use-after-close message {exc.message!r}") check(list(kept_bus.messages()) == ["hello", "psst", "last", "async", "x", "gag", "recovered"], "bus alive through the callback-adopted reference") @@ -256,7 +255,38 @@ def main() -> None: "second bus is independent") other.close() - print("python/events: OK") + foreign_threads() + consumer.finish(wv) + + +def foreign_threads() -> None: + # Python threads publishing concurrently: each publish calls back into + # the subscribers from that thread while the others hold or wait for + # the GIL, and the async completion arrives on a producer thread. + bus = wv.EventBus() + sub = Recorder("threads") + bus.subscribe(sub) + errors = [] + + def publish(n: int) -> None: + try: + for i in range(25): + bus.publish(f"t{n}", f"{n}-{i}", []) + except BaseException as exc: + errors.append(exc) + + threads = [threading.Thread(target=publish, args=(n,)) for n in range(4)] + for t in threads: + t.start() + for t in threads: + t.join(30) + check(not errors, f"publishing threads failed: {errors}") + check(len(sub.messages) == 100, f"threads delivered {len(sub.messages)}") + check(asyncio.run(bus.publish_later("async", "x")) == 1, "publish_later after threads") + bus.clear_subscribers() + for b in sub.attached: + b.close() + bus.close() main() diff --git a/conformance/python/harness.py b/conformance/python/harness.py new file mode 100644 index 00000000..953ab13a --- /dev/null +++ b/conformance/python/harness.py @@ -0,0 +1,49 @@ +"""Shared helpers for the Python conformance consumers. + +Every consumer imports the generated package as-is (conformance/python/run.sh +puts it on PYTHONPATH and selects the producer library through the loader's +`{PREFIX}_LIBRARY` override). This module adds the assertion helper, a hook +that turns any exception Python could only report as "unraisable" (raised in +a ctypes callback or a finalizer) into a failure, and the end-of-run leak +check against the producer's live-allocation counters. +""" +import gc +import sys +import types +from typing import Any, List + +_unraisable: List[str] = [] + + +def _record_unraisable(info: Any) -> None: + _unraisable.append(f"{info.exc_type.__name__}: {info.exc_value} ({info.object!r})") + + +sys.unraisablehook = _record_unraisable + + +class Consumer: + """Assertions for one consumer, labelled `python/`.""" + + def __init__(self, name: str) -> None: + self.name = name + + def check(self, cond: bool, what: str) -> None: + if not cond: + print(f"python/{self.name}: FAIL: {what}", file=sys.stderr) + sys.exit(1) + + def finish(self, package: types.ModuleType) -> None: + """Collect garbage (running every finalizer), then require that the + producer holds no live object, callback, iterator, cancel token, or + returned allocation, and that nothing was raised where Python could + not propagate it.""" + for _ in range(3): + gc.collect() + impl = sys.modules[f"{package.__name__}.{package.__name__}"] + kinds = ["objects", "callbacks", "iterators", "cancel tokens", "allocations"] + for kind, label in enumerate(kinds): + live = impl._debug_live(kind) + self.check(live == 0, f"{live} live {label} at exit") + self.check(not _unraisable, f"unraisable exceptions: {_unraisable}") + print(f"python/{self.name}: OK") diff --git a/conformance/python/inventory.py b/conformance/python/inventory.py deleted file mode 100644 index a91bf708..00000000 --- a/conformance/python/inventory.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Conformance consumer: inventory sample, Python target. - -Drives the only multi-module sample through the generated ctypes wrapper. The -`products` module contributes the `Catalog` interface (constructor, `with` -statement, idempotent `close()`), the `Product` record with an optional -string, an enum, and a list field, and the `ProductsError` domain; the -`orders` module contributes free functions over `Order`/`OrderItem` records -(a list-of-record parameter, a buffered record return) and the `OrdersError` -domain. Both domains reuse the codes 1 and 2, so each throwing callable must -map its code to its own module's classes. `add_product_to_order` takes a -`products.Product` across the module boundary. The generated package is -placed on sys.path via WV_PY; the cdylib is selected with WEAVEFFI_LIBRARY. -""" -import os -import sys - -sys.path.insert(0, os.environ["WV_PY"]) - -import inventory as wv # noqa: E402 - - -def check(cond: bool, what: str) -> None: - if not cond: - print(f"python/inventory: FAIL: {what}", file=sys.stderr) - sys.exit(1) - - -def expect(fn, cls, code: int, message: str, what: str) -> None: - try: - fn() - except cls as exc: - check(exc.code == code and exc.CODE == code, f"{what}: code {exc.code}") - check(exc.message == message, f"{what}: message {exc.message!r}") - check(isinstance(exc, wv.WeaveFFIError), f"{what}: hierarchy") - return - check(False, f"{what}: expected {cls.__name__}") - - -def products() -> wv.Product: - check(wv.Category.Electronics == 0 and wv.Category.Books == 3, "Category discriminants") - check(wv.InvalidPrice is wv.ProductsError.InvalidPrice - and wv.ProductNotFound is wv.ProductsError.ProductNotFound, "products aliases") - check(issubclass(wv.ProductsError, wv.WeaveFFIError) - and not issubclass(wv.ProductsError, wv.OrdersError), "domains are distinct classes") - - with wv.Catalog() as catalog: - widget = catalog.add_product("Widget", 9.99, wv.Category.Electronics) - check(isinstance(widget, wv.Product), "add_product returns a Product") - check(widget.id > 0 and widget.name == "Widget" and widget.price == 9.99, f"widget {widget}") - check(widget.category is wv.Category.Electronics, "category enum member") - check(widget.description is None and widget.tags == [], "absent optional / empty list") - check(widget == wv.Product(id=widget.id, name="Widget", description=None, price=9.99, - category=wv.Category.Electronics, tags=[]), - "Product value equality") - - shirt = catalog.add_product("Shirt", 29.5, wv.Category.Clothing) - phone = catalog.add_product("Phöne 📱", 499.0, wv.Category.Electronics) - check(shirt.id == widget.id + 1 and phone.id == shirt.id + 1, "ids are monotonic") - check(phone.name == "Phöne 📱", f"unicode name {phone.name!r}") - - # Typed errors in the products domain (codes 1 and 2). - expect(lambda: catalog.add_product("Free", 0.0, wv.Category.Food), - wv.ProductsError.InvalidPrice, 1, "price must be positive", "add_product(0.0)") - expect(lambda: catalog.add_product("Debt", -5.0, wv.Category.Food), - wv.ProductsError.InvalidPrice, 1, "price must be positive", "add_product(-5.0)") - expect(lambda: catalog.get_product(999), wv.ProductsError.ProductNotFound, 2, - "product not found", "get_product(999)") - - fetched = catalog.get_product(shirt.id) - check(fetched == shirt, f"get_product round-trips {fetched}") - - # List-of-record return. - electronics = catalog.search(wv.Category.Electronics) - check([p.name for p in electronics] == ["Widget", "Phöne 📱"], f"search {electronics}") - check(catalog.search(wv.Category.Books) == [], "search on an empty shelf") - - check(catalog.update_price(widget.id, 12.25) is True, "update_price existing") - check(catalog.get_product(widget.id).price == 12.25, "price updated") - check(catalog.update_price(999, 1.0) is False, "update_price missing is False") - expect(lambda: catalog.update_price(widget.id, -1.0), wv.InvalidPrice, 1, - "price must be positive", "update_price(-1.0)") - - check(catalog.remove(shirt.id) is True, "remove existing") - check(catalog.remove(shirt.id) is False, "remove missing is False") - expect(lambda: catalog.get_product(shirt.id), wv.ProductNotFound, 2, - "product not found", "get_product after remove") - - # Catalogs are independent objects. - other = wv.Catalog() - check(other.search(wv.Category.Electronics) == [], "second catalog is empty") - other.close() - other.close() - try: - other.remove(1) - check(False, "expected use-after-close error") - except wv.WeaveFFIError as exc: - check("after close" in exc.message, f"use-after-close message {exc.message!r}") - closed = catalog - gadget = catalog.get_product(phone.id) - closed.close() - try: - closed.search(wv.Category.Food) - check(False, "expected use-after-close error after with") - except wv.WeaveFFIError: - pass - return gadget - - -def orders(product: wv.Product) -> None: - check(wv.OrderNotFound is wv.OrdersError.OrderNotFound - and wv.EmptyOrder is wv.OrdersError.EmptyOrder, "orders aliases") - - # List-of-record parameter in one value buffer; buffered record return. - items = [wv.OrderItem(product_id=1, quantity=2, unit_price=10.0), - wv.OrderItem(product_id=2, quantity=1, unit_price=25.0)] - order_id = wv.create_order(items) - check(order_id > 0, f"create_order id {order_id}") - order = wv.get_order(order_id) - check(isinstance(order, wv.Order), "get_order returns an Order") - check(order.id == order_id and order.status == "pending", f"order {order}") - check(order.items == items, f"items round-trip {order.items}") - check(order.total == 45.0, f"total {order.total}") - check(order == wv.Order(id=order_id, items=items, total=45.0, status="pending"), - "Order value equality") - - # Typed errors in the orders domain: the same numeric codes as the - # products domain resolve to the orders classes. - expect(lambda: wv.create_order([]), wv.OrdersError.EmptyOrder, 2, - "order must contain at least one item", "create_order([])") - expect(lambda: wv.get_order(999), wv.OrdersError.OrderNotFound, 1, - "order not found", "get_order(999)") - try: - wv.get_order(999) - except wv.ProductsError: - check(False, "orders code 1 must not map to ProductsError.InvalidPrice") - except wv.OrdersError: - pass - - # Cross-module record parameter: a products.Product appended to an order. - check(wv.add_product_to_order(order_id, product) is True, "add_product_to_order") - order = wv.get_order(order_id) - check(len(order.items) == 3, f"items after add {order.items}") - check(order.items[2] == wv.OrderItem(product_id=product.id, quantity=1, unit_price=499.0), - f"appended item {order.items[2]}") - check(order.total == 45.0 + 499.0, f"total after add {order.total}") - check(wv.add_product_to_order(999, product) is False, "add_product_to_order missing order") - # A locally built Product with every field populated crosses too. - local = wv.Product(id=77, name="Local", description="built in Python", price=0.5, - category=wv.Category.Books, tags=["x", "yy"]) - check(wv.add_product_to_order(order_id, local) is True, "add local product") - check(wv.get_order(order_id).items[3].product_id == 77, "local product id recorded") - - check(wv.cancel_order(order_id) is True, "cancel_order") - check(wv.cancel_order(order_id) is False, "cancel_order twice is False") - check(wv.get_order(order_id).status == "cancelled", "status after cancel") - check(wv.cancel_order(999) is False, "cancel_order missing is False") - - second = wv.create_order([wv.OrderItem(product_id=3, quantity=0, unit_price=1e9)]) - check(second == order_id + 1, "order ids are monotonic") - check(wv.get_order(second).total == 0.0, "zero quantity yields zero total") - - -def main() -> None: - gadget = products() - orders(gadget) - print("python/inventory: OK") - - -main() diff --git a/conformance/python/kvstore.py b/conformance/python/kvstore_consumer.py similarity index 84% rename from conformance/python/kvstore.py rename to conformance/python/kvstore_consumer.py index d198bf1e..d2c49baf 100644 --- a/conformance/python/kvstore.py +++ b/conformance/python/kvstore_consumer.py @@ -1,6 +1,6 @@ """Conformance consumer: kvstore sample, Python target. -Full-surface drive of the generated ctypes wrapper against ABI 2: the +Full-surface drive of the generated ctypes wrapper against ABI 3: the reference-counted `Store` interface (fallible `open` factory, `with` statement, idempotent `close()`, use after close rejected, the `default_capacity` static, the deprecated `legacy_put`), typed `KvError` @@ -13,28 +13,22 @@ `FOREIGN_ERROR_CODE`), objects in every buffered position (`share()` as a second wrapper over the same object, `fork()`, `larger(None)` and `larger(other)`, `describe().store`, `open_many`, `total_count` with a -locally built `StoreInfo`), and the asyncio-bridged `compact` coroutine. The -generated package is placed on sys.path via WV_PY; the cdylib is selected -with WEAVEFFI_LIBRARY. +locally built `StoreInfo`), the asyncio-bridged cancellable `compact` +coroutine, and wrappers that stay usable while another thread closes them +mid-call. Ends with the leak check (see harness.py). """ import asyncio import gc -import os -import sys +import threading import warnings import weakref -sys.path.insert(0, os.environ["WV_PY"]) - -import kvstore as wv # noqa: E402 +import kvstore as wv +from harness import Consumer PATH = "/tmp/conformance-kvstore-py" - - -def check(cond: bool, what: str) -> None: - if not cond: - print(f"python/kvstore: FAIL: {what}", file=sys.stderr) - sys.exit(1) +consumer = Consumer("kvstore") +check = consumer.check class Listener(wv.EvictionListener): @@ -60,7 +54,7 @@ def expect_kv_error(fn, cls, code: int, what: str) -> wv.KvError: fn() except cls as exc: check(exc.code == code and exc.CODE == code, f"{what}: code {exc.code}") - check(isinstance(exc, wv.KvError) and isinstance(exc, wv.WeaveFFIError), + check(isinstance(exc, wv.KvError) and isinstance(exc, wv.Error), f"{what}: hierarchy") return exc check(False, f"{what}: expected {cls.__name__}") @@ -147,12 +141,12 @@ def basics() -> None: try: closed.count() check(False, "expected use-after-close error") - except wv.WeaveFFIError as exc: + except wv.Error as exc: check("after close" in exc.message, f"use-after-close message {exc.message!r}") try: wv.get_stats(closed) check(False, "expected use-after-close error as a parameter") - except wv.WeaveFFIError: + except wv.Error: pass @@ -206,11 +200,11 @@ def eviction_listener() -> None: store.put("fine", b"?", wv.EntryKind.Persistent, None) try: store.delete("bad") - check(False, "expected WeaveFFIError from a raising listener") + check(False, "expected Error from a raising listener") except wv.KvError: check(False, "a foreign error is not a KvError") - except wv.WeaveFFIError as exc: - check(exc.code == wv.WeaveFFIError.FOREIGN_ERROR_CODE == -4, + except wv.Error as exc: + check(exc.code == wv.Error.FOREIGN_ERROR_CODE == -4, f"foreign error code {exc.code}") check("listener refused bad" in exc.message, f"foreign message {exc.message!r}") check(store.count() == 1, "entry removed despite the listener raising") @@ -299,12 +293,12 @@ def object_graph() -> None: try: wv.Store.total_count([empty, closed_store()], None) check(False, "expected error encoding a closed store") - except wv.WeaveFFIError as exc: + except wv.Error as exc: check("Store used after close" in exc.message, f"closed store in list {exc.message!r}") try: wv.Store.total_count([], wv.StoreInfo("x", empty, closed_store(), 0)) check(False, "expected error encoding a closed mirror") - except wv.WeaveFFIError as exc: + except wv.Error as exc: check("Store used after close" in exc.message, f"closed mirror {exc.message!r}") check(empty.count() == 0, "live store usable after a rejected encoding") @@ -316,7 +310,7 @@ def object_graph() -> None: try: s.count() check(False, "closed store still usable") - except wv.WeaveFFIError: + except wv.Error: pass @@ -326,13 +320,66 @@ def closed_store() -> wv.Store: return s +class Blocking(wv.EvictionListener): + """A listener that blocks inside the producer's `delete` until released, + so the test can act while that call is in flight.""" + + def __init__(self) -> None: + self.entered = threading.Event() + self.release = threading.Event() + + def on_evict(self, entry: wv.Entry, reason: wv.EvictionReason) -> bool: + self.entered.set() + self.release.wait(10) + return True + + +def close_during_call() -> None: + # close() from another thread while a call is in flight defers the + # native release until the call returns, instead of freeing the store + # out from under it. + store = wv.Store.open(PATH) + listener = Blocking() + store.set_eviction_listener(listener) + store.put("k", b"v", wv.EntryKind.Persistent, None) + result = [] + worker = threading.Thread(target=lambda: result.append(store.delete("k"))) + worker.start() + check(listener.entered.wait(10), "listener entered") + store.close() + try: + store.count() + check(False, "a closing wrapper rejects new calls") + except wv.Error as exc: + check("after close" in exc.message, f"closing message {exc.message!r}") + listener.release.set() + worker.join(10) + check(result == [True], f"in-flight delete completed: {result}") + + +async def cancel_compact() -> None: + # compact is cancellable: cancelling the awaiting task cancels the native + # token, and the coroutine raises CancelledError (or, when compaction + # already finished, the task simply completes). + store = wv.Store.open(PATH) + task = asyncio.ensure_future(store.compact()) + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + check(task.cancelled() or task.result() == 0, "cancelled compact") + check(await store.compact() == 0, "compact still works after a cancellation") + store.close() + + def main() -> None: basics() eviction_listener() object_graph() - # Whatever wrappers were left to garbage collection release exactly once. - gc.collect() - print("python/kvstore: OK") + close_during_call() + asyncio.run(cancel_compact()) + consumer.finish(wv) main() diff --git a/conformance/python/run.sh b/conformance/python/run.sh new file mode 100755 index 00000000..ea62a728 --- /dev/null +++ b/conformance/python/run.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Conformance lanes for the python target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane runs one consumer +# against one sample and must exit 0. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# Run a Python consumer against the generated package as-is: its directory +# goes on PYTHONPATH, and the producer cdylib is selected through the +# loader's `{PREFIX}_LIBRARY` override (macOS strips DYLD_LIBRARY_PATH from +# the system python3). +py_consumer() { + local sample="$1" script="$2" + env PYTHONPATH="$GENROOT/$sample/python" \ + "$(library_env "$sample")=$(sample_lib "$sample")" \ + python3 -X dev "$ROOT/conformance/python/$script" +} + +python_calculator() { py_consumer calculator calculator_consumer.py; } + +python_contacts() { py_consumer contacts contacts_consumer.py; } + +python_events() { py_consumer events events_consumer.py; } + +python_kvstore() { py_consumer kvstore kvstore_consumer.py; } + +python_async_demo() { py_consumer async-demo async_demo_consumer.py; } + +python_codec() { py_consumer codec codec_consumer.py; } + +lane python-calculator python_calculator +lane python-contacts python_contacts +lane python-events python_events +lane python-kvstore python_kvstore +lane python-async-demo python_async_demo +lane python-codec python_codec + +finish_lanes diff --git a/conformance/python/shapes.py b/conformance/python/shapes.py deleted file mode 100644 index b02b5582..00000000 --- a/conformance/python/shapes.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Conformance consumer: shapes sample, Python target. - -Drives the generated ctypes wrapper for rich (algebraic) enums as value -types: the `Shape` base class with its nested `Tag` IntEnum and `tag` -reader, the per-variant dataclasses reachable as `Shape.Circle(...)` with -natural field names (`radius`, not `circle_radius`) and value equality, -plus the free functions that take and return `Shape` as value buffers -(module-prefix-stripped: `describe`, `scale`, `sum_bytes`). Also covers the -expanded numerics (f32 fields, u8 field, u64 return) and the plain C-style -`Channel` enum. The generated package is placed on sys.path via WV_PY; the -cdylib is selected with WEAVEFFI_LIBRARY. -""" -import os -import sys - -sys.path.insert(0, os.environ["WV_PY"]) - -import shapes as wv # noqa: E402 - - -def main() -> None: - # Unit variant: tag only. - empty = wv.Shape.Empty() - assert isinstance(empty, wv.Shape) - assert empty.tag == wv.Shape.Tag.Empty - - # f64 payload. - circle = wv.Shape.Circle(2.5) - assert circle.tag == wv.Shape.Tag.Circle - assert abs(circle.radius - 2.5) < 1e-9 - - # Two f32 payloads. - rect = wv.Shape.Rectangle(3.0, 4.0) - assert rect.tag == wv.Shape.Tag.Rectangle - assert abs(rect.width - 3.0) < 1e-6 - assert abs(rect.height - 4.0) < 1e-6 - - # string + u8 payload. - labeled = wv.Shape.Labeled("hex", 6) - assert labeled.tag == wv.Shape.Tag.Labeled - assert labeled.label == "hex" - assert labeled.count == 6 - - # Free functions: Shape in, string/Shape out. - assert wv.describe(circle) == "circle(r=2.5)" - - big = wv.scale(circle, 4.0) - assert isinstance(big, wv.Shape.Circle) - assert big.tag == wv.Shape.Tag.Circle - assert abs(big.radius - 10.0) < 1e-9 - - # Variants are dataclasses: values decoded from the producer compare - # equal to locally constructed ones. - assert big == wv.Shape.Circle(10.0) - assert wv.Shape.Labeled("hex", 6) == labeled - - # A C-style enum keeps its plain integer constants. - assert wv.Channel.Green == 1 - - # Numerics: `[u8]` canonicalizes to `bytes` in, u64 out. - assert wv.sum_bytes(bytes([250, 250, 250, 250])) == 1000 - - print("python/shapes: OK") - - -main() diff --git a/conformance/ruby/async_demo.rb b/conformance/ruby/async_demo.rb index 029429cf..fada720b 100644 --- a/conformance/ruby/async_demo.rb +++ b/conformance/ruby/async_demo.rb @@ -6,47 +6,115 @@ # TaskResult value class from a value buffer, an empty name raises the typed # TaskError::InvalidName subclass, `run_batch` round-trips a list of records # through value buffers both ways, `run_n_tasks` returns a direct scalar -# through the callback, `cancel_task` is a plain sync call, and -# `active_callbacks` is back to zero once every task body has completed. The -# cdylib is selected via WEAVEFFI_LIBRARY. +# through the callback (also from many threads at once), and the cancellable +# `wait` honors a CancelToken passed as `cancel:` (cancelled from another +# thread, or before the call) and an interrupted wait (Thread#raise), each +# raising AsyncDemo::Cancelled or the interruption long before the timeout. +# `active_callbacks` is back to zero once every task body has completed or +# been dropped. The gem is installed from the generated tree; the cdylib is +# selected through ASYNC_DEMO_LIBRARY. -$LOAD_PATH.unshift(File.join(ENV.fetch("WV_RB"), "lib")) -require "async_demo" +require_relative 'support' +require 'async_demo' -def expect(cond, msg) - raise "assertion failed: #{msg}" unless cond -end +# Polls the producer until no task body is in flight. +def settle_tasks + 100.times do + return true if AsyncDemo.active_callbacks.zero? -# Async record return: blocks until the worker-thread callback delivers the -# encoded TaskResult. -result = WeaveFFI.run_task("alpha") -expect(result.id.positive?, "run_task assigns an id") -expect(result.value == "completed: alpha", "run_task value (got #{result.value})") -expect(result.success == true, "run_task success flag") - -# Typed async error: the empty name settles with the InvalidName subclass. -begin - WeaveFFI.run_task("") - expect(false, "expected TaskError::InvalidName for empty name") -rescue WeaveFFI::TaskError::InvalidName => e - expect(e.code == 1, "InvalidName carries code 1 (got #{e.code})") - expect(e.is_a?(WeaveFFI::TaskError), "subclass of TaskError") - expect(e.is_a?(WeaveFFI::Error), "subclass of the brand error") + sleep 0.02 + end + false end -# Buffered list-of-records both ways. -batch = WeaveFFI.run_batch(%w[a b c]) -expect(batch.map(&:value) == ["completed: a", "completed: b", "completed: c"], - "run_batch values (got #{batch.map(&:value)})") -expect(batch.all?(&:success), "run_batch success flags") +run_and_check_leaks(AsyncDemo) do + expect(AsyncDemo::ABI_VERSION == 3, 'bindings target ABI revision 3') + + # Async record return: blocks until the worker-thread callback delivers the + # encoded TaskResult. + result = AsyncDemo.run_task('alpha') + expect(result.id.positive?, 'run_task assigns an id') + expect(result.value == 'completed: alpha', "run_task value (got #{result.value})") + expect(result.success == true, 'run_task success flag') + + # Typed async error: the empty name settles with the InvalidName subclass. + begin + AsyncDemo.run_task('') + expect(false, 'expected TaskError::InvalidName for empty name') + rescue AsyncDemo::TaskError::InvalidName => e + expect(e.code == 1, "InvalidName carries code 1 (got #{e.code})") + expect(e.is_a?(AsyncDemo::TaskError), 'subclass of TaskError') + expect(e.is_a?(AsyncDemo::Error), 'subclass of AsyncDemo::Error') + end + + # Buffered list-of-records both ways. + batch = AsyncDemo.run_batch(%w[a b c]) + expect(batch.map(&:value) == ['completed: a', 'completed: b', 'completed: c'], + "run_batch values (got #{batch.map(&:value)})") + expect(batch.all?(&:success), 'run_batch success flags') + expect(AsyncDemo.run_batch([]) == [], 'run_batch of nothing') -# Direct scalar through the async callback. -expect(WeaveFFI.run_n_tasks(7) == 7, "run_n_tasks echoes n") + # Direct scalar through the async callback, from many threads at once. + expect(AsyncDemo.run_n_tasks(7) == 7, 'run_n_tasks echoes n') + results = Array.new(8) { |i| Thread.new { Array.new(25) { |j| AsyncDemo.run_n_tasks(i * 100 + j) } } }.map(&:value) + expect(results.each_with_index.all? { |rs, i| rs == Array.new(25) { |j| i * 100 + j } }, + 'concurrent async calls each receive their own result') -# Sync functions beside the async ones. -expect(WeaveFFI.cancel_task(1) == false, "cancel_task reports not cancelled") + # --- Cancellation -------------------------------------------------------- + # Without cancellation, wait completes with the milliseconds waited. + expect(AsyncDemo.wait(20) == 20, 'wait completes after its timeout') + idle = AsyncDemo::CancelToken.new + expect(AsyncDemo.wait(5, cancel: idle) == 5, 'an untouched token changes nothing') + idle.close -# Every spawned task body has completed by the time its callback fires. -expect(WeaveFFI.active_callbacks.zero?, "active_callbacks settles to zero") + # Cancelling the token from another thread completes the call at once with + # AsyncDemo::Cancelled (code -5) instead of waiting out the timeout. + token = AsyncDemo::CancelToken.new + canceller = Thread.new do + sleep 0.05 + token.cancel + end + started = Time.now + begin + AsyncDemo.wait(60_000, cancel: token) + raise 'expected AsyncDemo::Cancelled' + rescue AsyncDemo::Cancelled => e + expect(e.code == -5 && e.code == AsyncDemo::CANCELLED_ERROR_CODE, "Cancelled code -5 (got #{e.code})") + expect(e.is_a?(AsyncDemo::Error), 'Cancelled is an AsyncDemo::Error') + end + expect(Time.now - started < 5, "cancellation didn't wait for the timeout") + canceller.join + expect(token.cancelled?, 'token reports cancelled') + + # A token cancelled before the call never runs the body. + begin + AsyncDemo.wait(60_000, cancel: token) + raise 'expected AsyncDemo::Cancelled for a pre-cancelled token' + rescue AsyncDemo::Cancelled => e + expect(e.code == -5, "pre-cancelled code (got #{e.code})") + end + token.close + + # Interrupting the waiting thread cancels the call's private token, so the + # producer drops its work (active_callbacks settles) instead of running out + # the timeout. + interrupted = Class.new(StandardError) + waiter = Thread.new do + AsyncDemo.wait(60_000) + :completed + rescue interrupted + :interrupted + end + sleep 0.1 + started = Time.now + waiter.raise(interrupted) + expect(waiter.value == :interrupted, 'the interrupted wait raised the interruption') + expect(settle_tasks, 'interrupting the wait cancelled the producer work') + expect(Time.now - started < 5, "the interrupted work didn't run out its timeout") + + # Sync functions beside the async ones; every task body has completed by + # the time its callback fires. + expect(settle_tasks, 'active_callbacks settles to zero') +end -puts "ruby async-demo conformance: OK" +puts 'ruby/async-demo: OK' diff --git a/conformance/ruby/calculator.rb b/conformance/ruby/calculator.rb new file mode 100644 index 00000000..3b8b9c74 --- /dev/null +++ b/conformance/ruby/calculator.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +# Conformance consumer: calculator sample, Ruby target. +# +# The minimal surface: direct scalars, the typed CalcError domain, and a +# string round trip (including non-ASCII text and an interior NUL, which +# cross the ABI as a pointer plus a length). The gem is installed from the +# generated tree; the cdylib is selected through CALCULATOR_LIBRARY. + +require_relative 'support' +require 'calculator' + +run_and_check_leaks(Calculator) do + expect(Calculator::ABI_VERSION == 3, 'bindings target ABI revision 3') + expect(Calculator.add(2, 40) == 42, 'add') + expect(Calculator.mul(6, 7) == 42, 'mul') + expect(Calculator.div(10, 2) == 5, 'div') + + begin + Calculator.div(1, 0) + raise 'expected CalcError::DivisionByZero' + rescue Calculator::CalcError::DivisionByZero => e + expect(e.code == 1, "DivisionByZero code (got #{e.code})") + expect(e.message == 'division by zero', "message (got #{e.message.inspect})") + expect(e.is_a?(Calculator::Error), 'domain errors subclass Calculator::Error') + end + + expect(Calculator.echo('hello') == 'hello', 'echo') + expect(Calculator.echo('') == '', 'echo empty') + unicode = Calculator.echo('héllo ✓ 日本') + expect(unicode == 'héllo ✓ 日本', "echo unicode (got #{unicode.inspect})") + expect(unicode.encoding == Encoding::UTF_8, 'echo returns UTF-8') + expect(Calculator.echo("a\0b") == "a\0b", 'echo keeps an interior NUL') +end + +puts 'ruby/calculator: OK' diff --git a/conformance/ruby/codec.rb b/conformance/ruby/codec.rb index d797e50a..ec4eaedc 100644 --- a/conformance/ruby/codec.rb +++ b/conformance/ruby/codec.rb @@ -5,19 +5,16 @@ # producer encodes and Ruby decodes (`sample_*`, checked field by field), Ruby # encodes and the producer decodes (`verify_*` must return true), and both # directions compose (`roundtrip_*` must equal the input). Ruby-built values -# cover the edges: empty strings, lists, and maps; non-ASCII text; i64/u64 -# extremes; Float::NAN, +/-Float::INFINITY, and -0.0; every Shape variant; and -# objects inside buffers (Holder: a Token field, an optional Token, and a list -# of Tokens; `primary_of` returns a wrapper to the SAME object as -# `holder.primary`; every wrapper closes exactly once). Exit is non-zero on -# any mismatch. The cdylib is selected via WEAVEFFI_LIBRARY. +# cover the edges: empty strings, lists, and maps; non-ASCII text; strings +# with interior NULs; i64/u64 extremes; Float::NAN, +/-Float::INFINITY, and +# -0.0; every Shape variant; and objects inside buffers (Holder: a Token +# field, an optional Token, and a list of Tokens; `primary_of` returns a +# wrapper to the SAME object as `holder.primary`; every wrapper closes +# exactly once). Exit is non-zero on any mismatch. The gem is installed from +# the generated tree; the cdylib is selected through CODEC_LIBRARY. -$LOAD_PATH.unshift(File.join(ENV.fetch("WV_RB"), "lib")) -require "codec" - -def expect(cond, msg) - raise "assertion failed: #{msg}" unless cond -end +require_relative 'support' +require 'codec' def negative_zero?(x) x.zero? && (1.0 / x) == -Float::INFINITY @@ -27,334 +24,343 @@ def negative_zero?(x) I64_MAX = 2**63 - 1 U64_MAX = 2**64 - 1 -expect(Codec::ABI_VERSION == 2, "bindings target ABI revision 2") -expect(Codec::Color::RED.zero? && Codec::Color::GREEN == 1 && Codec::Color::BLUE == 7, "Color constants") +run_and_check_leaks(Codec) do + expect(Codec::ABI_VERSION == 3, "bindings target ABI revision 3") + expect(Codec::Color::RED.zero? && Codec::Color::GREEN == 1 && Codec::Color::BLUE == 7, "Color constants") -# --- Scalars ------------------------------------------------------------- -scalars = Codec.sample_scalars -expect(scalars.is_a?(Codec::Scalars), "sample_scalars returns a Scalars") -expect(scalars.i8_value == -8, "i8 (got #{scalars.i8_value})") -expect(scalars.u8_value == 200, "u8 (got #{scalars.u8_value})") -expect(scalars.i16_value == -16_000, "i16 (got #{scalars.i16_value})") -expect(scalars.u16_value == 60_000, "u16 (got #{scalars.u16_value})") -expect(scalars.i32_value == -2_000_000_000, "i32 (got #{scalars.i32_value})") -expect(scalars.u32_value == 4_000_000_000, "u32 (got #{scalars.u32_value})") -expect(scalars.i64_value == -9_007_199_254_740_993, "i64 beyond 2^53 (got #{scalars.i64_value})") -expect(scalars.u64_value == U64_MAX, "u64::MAX (got #{scalars.u64_value})") -expect(scalars.f32_value == 1.5, "f32 (got #{scalars.f32_value})") -expect(scalars.f64_value == -2.25e100, "f64 (got #{scalars.f64_value})") -expect(scalars.flag == true, "flag") -expect(scalars.color == Codec::Color::BLUE, "color BLUE (got #{scalars.color})") + # --- Scalars ------------------------------------------------------------- + scalars = Codec.sample_scalars + expect(scalars.is_a?(Codec::Scalars), "sample_scalars returns a Scalars") + expect(scalars.i8_value == -8, "i8 (got #{scalars.i8_value})") + expect(scalars.u8_value == 200, "u8 (got #{scalars.u8_value})") + expect(scalars.i16_value == -16_000, "i16 (got #{scalars.i16_value})") + expect(scalars.u16_value == 60_000, "u16 (got #{scalars.u16_value})") + expect(scalars.i32_value == -2_000_000_000, "i32 (got #{scalars.i32_value})") + expect(scalars.u32_value == 4_000_000_000, "u32 (got #{scalars.u32_value})") + expect(scalars.i64_value == -9_007_199_254_740_993, "i64 beyond 2^53 (got #{scalars.i64_value})") + expect(scalars.u64_value == U64_MAX, "u64::MAX (got #{scalars.u64_value})") + expect(scalars.f32_value == 1.5, "f32 (got #{scalars.f32_value})") + expect(scalars.f64_value == -2.25e100, "f64 (got #{scalars.f64_value})") + expect(scalars.flag == true, "flag") + expect(scalars.color == Codec::Color::BLUE, "color BLUE (got #{scalars.color})") -expect(Codec.verify_scalars(scalars) == true, "producer accepts the re-encoded Scalars") -expect(Codec.roundtrip_scalars(scalars) == scalars, "roundtrip_scalars equals the sample") + expect(Codec.verify_scalars(scalars) == true, "producer accepts the re-encoded Scalars") + expect(Codec.roundtrip_scalars(scalars) == scalars, "roundtrip_scalars equals the sample") -# The same fixture built from scratch in Ruby verifies; one changed field is a -# typed Mismatch (code 1). -rebuilt = Codec::Scalars.new( - i8_value: -8, u8_value: 200, i16_value: -16_000, u16_value: 60_000, - i32_value: -2_000_000_000, u32_value: 4_000_000_000, - i64_value: -9_007_199_254_740_993, u64_value: U64_MAX, - f32_value: 1.5, f64_value: -2.25e100, flag: true, color: Codec::Color::BLUE -) -expect(rebuilt == scalars, "Ruby-built Scalars equals the decoded sample") -expect(Codec.verify_scalars(rebuilt) == true, "Ruby-built Scalars verifies") -changed = Codec::Scalars.new( - i8_value: -8, u8_value: 200, i16_value: -16_000, u16_value: 60_000, - i32_value: -2_000_000_000, u32_value: 4_000_000_000, - i64_value: -9_007_199_254_740_993, u64_value: U64_MAX, - f32_value: 1.5, f64_value: -2.25e100, flag: false, color: Codec::Color::BLUE -) -begin - Codec.verify_scalars(changed) - raise "expected CodecError::Mismatch" -rescue Codec::CodecError::Mismatch => e - expect(e.code == 1, "Mismatch code == 1 (got #{e.code})") - expect(e.is_a?(Codec::CodecError) && e.is_a?(Codec::Error), "Mismatch subclasses the domain and brand errors") -end + # The same fixture built from scratch in Ruby verifies; one changed field is a + # typed Mismatch (code 1). + rebuilt = Codec::Scalars.new( + i8_value: -8, u8_value: 200, i16_value: -16_000, u16_value: 60_000, + i32_value: -2_000_000_000, u32_value: 4_000_000_000, + i64_value: -9_007_199_254_740_993, u64_value: U64_MAX, + f32_value: 1.5, f64_value: -2.25e100, flag: true, color: Codec::Color::BLUE + ) + expect(rebuilt == scalars, "Ruby-built Scalars equals the decoded sample") + expect(Codec.verify_scalars(rebuilt) == true, "Ruby-built Scalars verifies") + changed = Codec::Scalars.new( + i8_value: -8, u8_value: 200, i16_value: -16_000, u16_value: 60_000, + i32_value: -2_000_000_000, u32_value: 4_000_000_000, + i64_value: -9_007_199_254_740_993, u64_value: U64_MAX, + f32_value: 1.5, f64_value: -2.25e100, flag: false, color: Codec::Color::BLUE + ) + begin + Codec.verify_scalars(changed) + raise "expected CodecError::Mismatch" + rescue Codec::CodecError::Mismatch => e + expect(e.code == 1, "Mismatch code == 1 (got #{e.code})") + expect(e.is_a?(Codec::CodecError) && e.is_a?(Codec::Error), "Mismatch subclasses the domain and Codec::Error") + end -# Every integer extreme plus float specials round-trip bit for bit. -extremes = Codec::Scalars.new( - i8_value: -128, u8_value: 255, i16_value: -32_768, u16_value: 65_535, - i32_value: -(2**31), u32_value: 2**32 - 1, - i64_value: I64_MIN, u64_value: U64_MAX, - f32_value: -Float::INFINITY, f64_value: -0.0, flag: false, color: Codec::Color::RED -) -back = Codec.roundtrip_scalars(extremes) -expect(back == extremes, "extreme Scalars round-trip") -expect(back.i64_value == I64_MIN && back.u64_value == U64_MAX, "64-bit extremes inside a record") -expect(back.f32_value == -Float::INFINITY, "f32 -inf inside a record") -expect(negative_zero?(back.f64_value), "f64 -0.0 keeps its sign inside a record") -highs = Codec::Scalars.new( - i8_value: 127, u8_value: 0, i16_value: 32_767, u16_value: 0, - i32_value: 2**31 - 1, u32_value: 0, i64_value: I64_MAX, u64_value: 0, - f32_value: Float::NAN, f64_value: Float::INFINITY, flag: true, color: Codec::Color::GREEN -) -back = Codec.roundtrip_scalars(highs) -expect(back.i8_value == 127 && back.i16_value == 32_767 && back.i32_value == 2**31 - 1, "signed maxima") -expect(back.i64_value == I64_MAX, "i64::MAX inside a record") -expect(back.u8_value.zero? && back.u16_value.zero? && back.u32_value.zero? && back.u64_value.zero?, "unsigned zeros") -expect(back.f32_value.nan?, "f32 NaN survives inside a record") -expect(back.f64_value == Float::INFINITY, "f64 +inf inside a record") -expect(back.color == Codec::Color::GREEN, "enum inside a record") + # Every integer extreme plus float specials round-trip bit for bit. + extremes = Codec::Scalars.new( + i8_value: -128, u8_value: 255, i16_value: -32_768, u16_value: 65_535, + i32_value: -(2**31), u32_value: 2**32 - 1, + i64_value: I64_MIN, u64_value: U64_MAX, + f32_value: -Float::INFINITY, f64_value: -0.0, flag: false, color: Codec::Color::RED + ) + back = Codec.roundtrip_scalars(extremes) + expect(back == extremes, "extreme Scalars round-trip") + expect(back.i64_value == I64_MIN && back.u64_value == U64_MAX, "64-bit extremes inside a record") + expect(back.f32_value == -Float::INFINITY, "f32 -inf inside a record") + expect(negative_zero?(back.f64_value), "f64 -0.0 keeps its sign inside a record") + highs = Codec::Scalars.new( + i8_value: 127, u8_value: 0, i16_value: 32_767, u16_value: 0, + i32_value: 2**31 - 1, u32_value: 0, i64_value: I64_MAX, u64_value: 0, + f32_value: Float::NAN, f64_value: Float::INFINITY, flag: true, color: Codec::Color::GREEN + ) + back = Codec.roundtrip_scalars(highs) + expect(back.i8_value == 127 && back.i16_value == 32_767 && back.i32_value == 2**31 - 1, "signed maxima") + expect(back.i64_value == I64_MAX, "i64::MAX inside a record") + expect(back.u8_value.zero? && back.u16_value.zero? && back.u32_value.zero? && back.u64_value.zero?, "unsigned zeros") + expect(back.f32_value.nan?, "f32 NaN survives inside a record") + expect(back.f64_value == Float::INFINITY, "f64 +inf inside a record") + expect(back.color == Codec::Color::GREEN, "enum inside a record") -# --- Composite ----------------------------------------------------------- -composite = Codec.sample_composite -expect(composite.is_a?(Codec::Composite), "sample_composite returns a Composite") -expect(composite.name == "héllo wörld ✓", "unicode name (got #{composite.name.inspect})") -expect(composite.name.encoding == Encoding::UTF_8, "strings decode as UTF-8") -expect(composite.blob == "\x00\x01\x02\xFD\xFE\xFF".b, "blob bytes (got #{composite.blob.inspect})") -expect(composite.blob.encoding == Encoding::BINARY, "bytes decode as BINARY") -expect(composite.some_i64 == I64_MIN, "present optional i64::MIN (got #{composite.some_i64})") -expect(composite.none_i64.nil?, "absent optional is nil") -expect(composite.some_text == "", "present optional empty string (got #{composite.some_text.inspect})") -expect(composite.names == ["a", "", "ccc"], "list of strings (got #{composite.names})") -expect(composite.matrix == [[1, 2, 3], [], [-4]], "list of lists (got #{composite.matrix})") -expect(composite.empty == [], "empty list") -expect(composite.by_name == { "one" => 1, "two" => 2, "neg" => -3 }, "string-keyed map (got #{composite.by_name})") -expect(composite.by_id.keys.sort == [-1, 42], "int-keyed map keys (got #{composite.by_id.keys})") -expect(composite.by_id[-1] == scalars, "by_id[-1] is the canonical Scalars") -expect(composite.by_id[42].flag == false && composite.by_id[42].u64_value == U64_MAX, "by_id[42] differs only in flag") -expect(composite.scalars == scalars, "nested record equals sample_scalars") -expect(composite.shape == Codec::Shape::Labeled.new(label: "tag", count: 3), "rich enum field") -expect(composite.shape.tag == 3 && composite.shape.tag == Codec::Shape::Labeled::TAG, "variant tag") -expect(composite.shapes.length == 5, "one shape per variant") -expect(composite.shapes[0] == Codec::Shape::Empty.new, "shapes[0] Empty") -expect(composite.shapes[1] == Codec::Shape::Circle.new(radius: 2.5), "shapes[1] Circle") -expect(composite.shapes[2] == Codec::Shape::Rect.new(width: 1.0, height: 0.5), "shapes[2] Rect") -expect(composite.shapes[3] == Codec::Shape::Labeled.new(label: "", count: -1), "shapes[3] Labeled") -expect(composite.shapes[4] == Codec::Shape::Nested.new(inner: scalars, note: "n"), "shapes[4] Nested") -expect(composite.shapes.map(&:tag) == [0, 1, 2, 3, 4], "variant tags in order") -expect(composite.maybe_shape == Codec::Shape::Nested.new(inner: scalars, note: nil), "optional rich enum") -expect(composite.maybe_list == "\x09\x08".b, "optional bytes list (got #{composite.maybe_list.inspect})") -expect(composite.sparse == [true, nil, false], "list of optionals (got #{composite.sparse})") -expect(composite.colors == [Codec::Color::RED, Codec::Color::GREEN, Codec::Color::BLUE], "list of enums") + # --- Composite ----------------------------------------------------------- + composite = Codec.sample_composite + expect(composite.is_a?(Codec::Composite), "sample_composite returns a Composite") + expect(composite.name == "héllo wörld ✓", "unicode name (got #{composite.name.inspect})") + expect(composite.name.encoding == Encoding::UTF_8, "strings decode as UTF-8") + expect(composite.blob == "\x00\x01\x02\xFD\xFE\xFF".b, "blob bytes (got #{composite.blob.inspect})") + expect(composite.blob.encoding == Encoding::BINARY, "bytes decode as BINARY") + expect(composite.some_i64 == I64_MIN, "present optional i64::MIN (got #{composite.some_i64})") + expect(composite.none_i64.nil?, "absent optional is nil") + expect(composite.some_text == "", "present optional empty string (got #{composite.some_text.inspect})") + expect(composite.names == ["a", "", "ccc"], "list of strings (got #{composite.names})") + expect(composite.matrix == [[1, 2, 3], [], [-4]], "list of lists (got #{composite.matrix})") + expect(composite.empty == [], "empty list") + expect(composite.by_name == { "one" => 1, "two" => 2, "neg" => -3 }, "string-keyed map (got #{composite.by_name})") + expect(composite.by_id.keys.sort == [-1, 42], "int-keyed map keys (got #{composite.by_id.keys})") + expect(composite.by_id[-1] == scalars, "by_id[-1] is the canonical Scalars") + expect(composite.by_id[42].flag == false && composite.by_id[42].u64_value == U64_MAX, "by_id[42] differs only in flag") + expect(composite.scalars == scalars, "nested record equals sample_scalars") + expect(composite.shape == Codec::Shape::Labeled.new(label: "tag", count: 3), "rich enum field") + expect(composite.shape.tag == 3 && composite.shape.tag == Codec::Shape::Labeled::TAG, "variant tag") + expect(composite.shapes.length == 5, "one shape per variant") + expect(composite.shapes[0] == Codec::Shape::Empty.new, "shapes[0] Empty") + expect(composite.shapes[1] == Codec::Shape::Circle.new(radius: 2.5), "shapes[1] Circle") + expect(composite.shapes[2] == Codec::Shape::Rect.new(width: 1.0, height: 0.5), "shapes[2] Rect") + expect(composite.shapes[3] == Codec::Shape::Labeled.new(label: "", count: -1), "shapes[3] Labeled") + expect(composite.shapes[4] == Codec::Shape::Nested.new(inner: scalars, note: "n"), "shapes[4] Nested") + expect(composite.shapes.map(&:tag) == [0, 1, 2, 3, 4], "variant tags in order") + expect(composite.maybe_shape == Codec::Shape::Nested.new(inner: scalars, note: nil), "optional rich enum") + expect(composite.maybe_list == "\x09\x08".b, "optional bytes list (got #{composite.maybe_list.inspect})") + expect(composite.sparse == [true, nil, false], "list of optionals (got #{composite.sparse})") + expect(composite.colors == [Codec::Color::RED, Codec::Color::GREEN, Codec::Color::BLUE], "list of enums") -expect(Codec.verify_composite(composite) == true, "producer accepts the re-encoded Composite") -expect(Codec.roundtrip_composite(composite) == composite, "roundtrip_composite equals the sample") -described = Codec.describe_composite(composite) -expect(described.include?("héllo wörld ✓") && described.include?("Labeled"), "describe_composite renders (got #{described[0, 60].inspect})") + expect(Codec.verify_composite(composite) == true, "producer accepts the re-encoded Composite") + expect(Codec.roundtrip_composite(composite) == composite, "roundtrip_composite equals the sample") + described = Codec.describe_composite(composite) + expect(described.include?("héllo wörld ✓") && described.include?("Labeled"), "describe_composite renders (got #{described[0, 60].inspect})") -# Rebuilt in Ruby with hashes in a different insertion order: maps are -# unordered on the wire, so the producer still sees the canonical value. -rebuilt_composite = Codec::Composite.new( - name: "héllo wörld ✓", - blob: [0, 1, 2, 253, 254, 255].pack("C*"), - some_i64: I64_MIN, - none_i64: nil, - some_text: "", - names: ["a", "", "ccc"], - matrix: [[1, 2, 3], [], [-4]], - empty: [], - by_name: { "two" => 2, "neg" => -3, "one" => 1 }, - by_id: { 42 => changed, -1 => rebuilt }, - scalars: rebuilt, - shape: Codec::Shape::Labeled.new(label: "tag", count: 3), - shapes: [ - Codec::Shape::Empty.new, - Codec::Shape::Circle.new(radius: 2.5), - Codec::Shape::Rect.new(width: 1.0, height: 0.5), - Codec::Shape::Labeled.new(label: "", count: -1), - Codec::Shape::Nested.new(inner: rebuilt, note: "n") - ], - maybe_shape: Codec::Shape::Nested.new(inner: rebuilt, note: nil), - maybe_list: "\x09\x08".b, - sparse: [true, nil, false], - colors: [Codec::Color::RED, Codec::Color::GREEN, Codec::Color::BLUE] -) -expect(rebuilt_composite == composite, "Ruby-built Composite equals the decoded sample") -expect(Codec.verify_composite(rebuilt_composite) == true, "Ruby-built Composite verifies") -tweaked = Codec::Composite.new( - name: composite.name, blob: composite.blob, some_i64: composite.some_i64, none_i64: nil, - some_text: composite.some_text, names: composite.names, matrix: composite.matrix, empty: [], - by_name: composite.by_name, by_id: composite.by_id, scalars: composite.scalars, - shape: composite.shape, shapes: composite.shapes, maybe_shape: composite.maybe_shape, - maybe_list: composite.maybe_list, sparse: [true, true, false], colors: composite.colors -) -begin - Codec.verify_composite(tweaked) - raise "expected CodecError::Mismatch for a tweaked Composite" -rescue Codec::CodecError::Mismatch => e - expect(e.code == 1, "tweaked Composite -> Mismatch (got #{e.code})") -end + # Rebuilt in Ruby with hashes in a different insertion order: maps are + # unordered on the wire, so the producer still sees the canonical value. + rebuilt_composite = Codec::Composite.new( + name: "héllo wörld ✓", + blob: [0, 1, 2, 253, 254, 255].pack("C*"), + some_i64: I64_MIN, + none_i64: nil, + some_text: "", + names: ["a", "", "ccc"], + matrix: [[1, 2, 3], [], [-4]], + empty: [], + by_name: { "two" => 2, "neg" => -3, "one" => 1 }, + by_id: { 42 => changed, -1 => rebuilt }, + scalars: rebuilt, + shape: Codec::Shape::Labeled.new(label: "tag", count: 3), + shapes: [ + Codec::Shape::Empty.new, + Codec::Shape::Circle.new(radius: 2.5), + Codec::Shape::Rect.new(width: 1.0, height: 0.5), + Codec::Shape::Labeled.new(label: "", count: -1), + Codec::Shape::Nested.new(inner: rebuilt, note: "n") + ], + maybe_shape: Codec::Shape::Nested.new(inner: rebuilt, note: nil), + maybe_list: "\x09\x08".b, + sparse: [true, nil, false], + colors: [Codec::Color::RED, Codec::Color::GREEN, Codec::Color::BLUE] + ) + expect(rebuilt_composite == composite, "Ruby-built Composite equals the decoded sample") + expect(Codec.verify_composite(rebuilt_composite) == true, "Ruby-built Composite verifies") + tweaked = Codec::Composite.new( + name: composite.name, blob: composite.blob, some_i64: composite.some_i64, none_i64: nil, + some_text: composite.some_text, names: composite.names, matrix: composite.matrix, empty: [], + by_name: composite.by_name, by_id: composite.by_id, scalars: composite.scalars, + shape: composite.shape, shapes: composite.shapes, maybe_shape: composite.maybe_shape, + maybe_list: composite.maybe_list, sparse: [true, true, false], colors: composite.colors + ) + begin + Codec.verify_composite(tweaked) + raise "expected CodecError::Mismatch for a tweaked Composite" + rescue Codec::CodecError::Mismatch => e + expect(e.code == 1, "tweaked Composite -> Mismatch (got #{e.code})") + end -# An edge-value Composite: everything empty or absent, plus float specials in -# a list and nested extremes. -edge = Codec::Composite.new( - name: "", - blob: "".b, - some_i64: I64_MAX, - none_i64: -1, - some_text: nil, - names: [], - matrix: [[], [-(2**31), 0, 2**31 - 1]], - empty: [1.5, Float::INFINITY, -Float::INFINITY, -0.0, 0.0], - by_name: {}, - by_id: { 0 => extremes }, - scalars: extremes, - shape: Codec::Shape::Empty.new, - shapes: [], - maybe_shape: nil, - maybe_list: "".b, - sparse: [nil, nil], - colors: [] -) -edge_back = Codec.roundtrip_composite(edge) -expect(edge_back == edge, "edge Composite round-trips") -expect(edge_back.name == "" && edge_back.blob == "".b && edge_back.names == [] && edge_back.by_name == {}, - "empties survive") -expect(edge_back.some_text.nil? && edge_back.maybe_shape.nil? && edge_back.some_i64 == I64_MAX && edge_back.none_i64 == -1, - "optionals in both states") -expect(edge_back.matrix == [[], [-(2**31), 0, 2**31 - 1]], "i32 extremes in a nested list (got #{edge_back.matrix})") -expect(edge_back.empty[1] == Float::INFINITY && edge_back.empty[2] == -Float::INFINITY, "infinities in a list") -expect(negative_zero?(edge_back.empty[3]) && !negative_zero?(edge_back.empty[4]), "-0.0 and 0.0 keep their signs") -expect(edge_back.maybe_list == "".b && !edge_back.maybe_list.nil?, "present empty optional bytes") -expect(edge_back.sparse == [nil, nil], "list of absent optionals") -expect(edge_back.by_id[0] == extremes, "record inside a map with an integer key") -expect(Codec.describe_composite(edge).start_with?("Composite {"), "describe_composite on the edge value") + # An edge-value Composite: everything empty or absent, plus float specials in + # a list and nested extremes. + edge = Codec::Composite.new( + name: "", + blob: "".b, + some_i64: I64_MAX, + none_i64: -1, + some_text: nil, + names: [], + matrix: [[], [-(2**31), 0, 2**31 - 1]], + empty: [1.5, Float::INFINITY, -Float::INFINITY, -0.0, 0.0], + by_name: {}, + by_id: { 0 => extremes }, + scalars: extremes, + shape: Codec::Shape::Empty.new, + shapes: [], + maybe_shape: nil, + maybe_list: "".b, + sparse: [nil, nil], + colors: [] + ) + edge_back = Codec.roundtrip_composite(edge) + expect(edge_back == edge, "edge Composite round-trips") + expect(edge_back.name == "" && edge_back.blob == "".b && edge_back.names == [] && edge_back.by_name == {}, + "empties survive") + expect(edge_back.some_text.nil? && edge_back.maybe_shape.nil? && edge_back.some_i64 == I64_MAX && edge_back.none_i64 == -1, + "optionals in both states") + expect(edge_back.matrix == [[], [-(2**31), 0, 2**31 - 1]], "i32 extremes in a nested list (got #{edge_back.matrix})") + expect(edge_back.empty[1] == Float::INFINITY && edge_back.empty[2] == -Float::INFINITY, "infinities in a list") + expect(negative_zero?(edge_back.empty[3]) && !negative_zero?(edge_back.empty[4]), "-0.0 and 0.0 keep their signs") + expect(edge_back.maybe_list == "".b && !edge_back.maybe_list.nil?, "present empty optional bytes") + expect(edge_back.sparse == [nil, nil], "list of absent optionals") + expect(edge_back.by_id[0] == extremes, "record inside a map with an integer key") + expect(Codec.describe_composite(edge).start_with?("Composite {"), "describe_composite on the edge value") -# --- Shape (rich enum) --------------------------------------------------- -[ - Codec::Shape::Empty.new, - Codec::Shape::Circle.new(radius: -0.0), - Codec::Shape::Rect.new(width: 0.25, height: -Float::INFINITY), - Codec::Shape::Labeled.new(label: "ünïcödé ✓", count: -(2**31)), - Codec::Shape::Nested.new(inner: extremes, note: ""), - Codec::Shape::Nested.new(inner: rebuilt, note: nil) -].each do |shape| - back = Codec.roundtrip_shape(shape) - expect(back == shape, "roundtrip_shape #{shape.class} (got #{Codec.describe_shape(back)})") - expect(back.class == shape.class && back.tag == shape.tag, "roundtrip_shape keeps the variant") -end -expect(negative_zero?(Codec.roundtrip_shape(Codec::Shape::Circle.new(radius: -0.0)).radius), "-0.0 in a variant") -expect(Codec.describe_shape(Codec::Shape::Empty.new) == "Empty", "describe_shape Empty") -expect(Codec.describe_shape(Codec::Shape::Circle.new(radius: 2.5)) == "Circle { radius: 2.5 }", "describe_shape Circle") -expect(Codec.describe_shape(Codec::Shape::Labeled.new(label: "x", count: 1)) == 'Labeled { label: "x", count: 1 }', - "describe_shape Labeled") -expect(Codec.roundtrip_shapes([]) == [], "empty list of shapes") -expect(Codec.roundtrip_shapes(composite.shapes) == composite.shapes, "list of every variant") -expect(Codec.roundtrip_shapes(composite.shapes).map(&:tag) == [0, 1, 2, 3, 4], "tags preserved in a list") + # --- Shape (rich enum) --------------------------------------------------- + [ + Codec::Shape::Empty.new, + Codec::Shape::Circle.new(radius: -0.0), + Codec::Shape::Rect.new(width: 0.25, height: -Float::INFINITY), + Codec::Shape::Labeled.new(label: "ünïcödé ✓", count: -(2**31)), + Codec::Shape::Nested.new(inner: extremes, note: ""), + Codec::Shape::Nested.new(inner: rebuilt, note: nil) + ].each do |shape| + back = Codec.roundtrip_shape(shape) + expect(back == shape, "roundtrip_shape #{shape.class} (got #{Codec.describe_shape(back)})") + expect(back.class == shape.class && back.tag == shape.tag, "roundtrip_shape keeps the variant") + end + expect(negative_zero?(Codec.roundtrip_shape(Codec::Shape::Circle.new(radius: -0.0)).radius), "-0.0 in a variant") + expect(Codec.describe_shape(Codec::Shape::Empty.new) == "Empty", "describe_shape Empty") + expect(Codec.describe_shape(Codec::Shape::Circle.new(radius: 2.5)) == "Circle { radius: 2.5 }", "describe_shape Circle") + expect(Codec.describe_shape(Codec::Shape::Labeled.new(label: "x", count: 1)) == 'Labeled { label: "x", count: 1 }', + "describe_shape Labeled") + expect(Codec.roundtrip_shapes([]) == [], "empty list of shapes") + expect(Codec.roundtrip_shapes(composite.shapes) == composite.shapes, "list of every variant") + expect(Codec.roundtrip_shapes(composite.shapes).map(&:tag) == [0, 1, 2, 3, 4], "tags preserved in a list") -# --- Standalone shapes: optionals, maps, strings, bytes, scalars --------- -expect(Codec.roundtrip_opt_i64(nil).nil?, "opt i64 absent") -expect(Codec.roundtrip_opt_i64(0).zero?, "opt i64 zero is present") -expect(Codec.roundtrip_opt_i64(I64_MIN) == I64_MIN, "opt i64::MIN") -expect(Codec.roundtrip_opt_i64(I64_MAX) == I64_MAX, "opt i64::MAX") -expect(Codec.roundtrip_map({}) == {}, "empty map") -unicode_map = { "" => 0, "ключ" => I64_MIN, "✓" => I64_MAX, "a" => -1 } -expect(Codec.roundtrip_map(unicode_map) == unicode_map, "unicode-keyed map with extremes") -expect(Codec.roundtrip_string("") == "", "empty string") -expect(Codec.roundtrip_string("héllo ✓ 日本") == "héllo ✓ 日本", "unicode string") -expect(Codec.roundtrip_string("héllo").encoding == Encoding::UTF_8, "returned string is UTF-8") -expect(Codec.roundtrip_bytes("".b) == "".b, "empty bytes") -all_bytes = (0..255).to_a.pack("C*") -expect(Codec.roundtrip_bytes(all_bytes) == all_bytes, "every byte value, including NUL") -expect(Codec.roundtrip_bytes(all_bytes).encoding == Encoding::BINARY, "returned bytes are BINARY") -expect(Codec.roundtrip_i64(I64_MIN) == I64_MIN, "direct i64::MIN") -expect(Codec.roundtrip_i64(I64_MAX) == I64_MAX, "direct i64::MAX") -expect(Codec.roundtrip_i64(-1) == -1, "direct -1") -expect(Codec.roundtrip_u64(U64_MAX) == U64_MAX, "direct u64::MAX") -expect(Codec.roundtrip_u64(2**63) == 2**63, "direct 2^63 stays unsigned") -expect(Codec.roundtrip_u64(0).zero?, "direct u64 zero") -expect(Codec.roundtrip_f64(Float::NAN).nan?, "direct NaN") -expect(Codec.roundtrip_f64(Float::INFINITY) == Float::INFINITY, "direct +inf") -expect(Codec.roundtrip_f64(-Float::INFINITY) == -Float::INFINITY, "direct -inf") -expect(negative_zero?(Codec.roundtrip_f64(-0.0)), "direct -0.0 keeps its sign") -expect(Codec.roundtrip_f64(Float::MAX) == Float::MAX, "direct f64::MAX") -expect(Codec.roundtrip_f64(Float::MIN) == Float::MIN, "direct smallest normal") -expect(Codec.roundtrip_f64(5e-324) == 5e-324, "direct subnormal") -expect(Codec.roundtrip_bool(true) == true && Codec.roundtrip_bool(false) == false, "direct bool") -expect(Codec.roundtrip_color(Codec::Color::BLUE) == Codec::Color::BLUE, "direct enum BLUE") -expect(Codec.roundtrip_color(Codec::Color::RED) == Codec::Color::RED, "direct enum RED") + # --- Standalone shapes: optionals, maps, strings, bytes, scalars --------- + expect(Codec.roundtrip_opt_i64(nil).nil?, "opt i64 absent") + expect(Codec.roundtrip_opt_i64(0).zero?, "opt i64 zero is present") + expect(Codec.roundtrip_opt_i64(I64_MIN) == I64_MIN, "opt i64::MIN") + expect(Codec.roundtrip_opt_i64(I64_MAX) == I64_MAX, "opt i64::MAX") + expect(Codec.roundtrip_map({}) == {}, "empty map") + unicode_map = { "" => 0, "ключ" => I64_MIN, "✓" => I64_MAX, "a" => -1 } + expect(Codec.roundtrip_map(unicode_map) == unicode_map, "unicode-keyed map with extremes") + expect(Codec.roundtrip_string("") == "", "empty string") + expect(Codec.roundtrip_string("héllo ✓ 日本") == "héllo ✓ 日本", "unicode string") + expect(Codec.roundtrip_string("héllo").encoding == Encoding::UTF_8, "returned string is UTF-8") + # Strings cross the ABI as a pointer plus a length, so an interior NUL + # survives in both directions (and inside a value buffer). + expect(Codec.roundtrip_string("a\0b") == "a\0b", "interior NUL string (got #{Codec.roundtrip_string("a\0b").inspect})") + expect(Codec.roundtrip_string("\0") == "\0", "lone NUL string") + expect(Codec.roundtrip_string("x\0✓\0") == "x\0✓\0", "NULs around non-ASCII text") + nul_map = { "k\0ey" => 1, "\0" => 2 } + expect(Codec.roundtrip_map(nul_map) == nul_map, "interior NULs in buffered map keys") + expect(Codec.roundtrip_bytes("".b) == "".b, "empty bytes") + all_bytes = (0..255).to_a.pack("C*") + expect(Codec.roundtrip_bytes(all_bytes) == all_bytes, "every byte value, including NUL") + expect(Codec.roundtrip_bytes(all_bytes).encoding == Encoding::BINARY, "returned bytes are BINARY") + expect(Codec.roundtrip_i64(I64_MIN) == I64_MIN, "direct i64::MIN") + expect(Codec.roundtrip_i64(I64_MAX) == I64_MAX, "direct i64::MAX") + expect(Codec.roundtrip_i64(-1) == -1, "direct -1") + expect(Codec.roundtrip_u64(U64_MAX) == U64_MAX, "direct u64::MAX") + expect(Codec.roundtrip_u64(2**63) == 2**63, "direct 2^63 stays unsigned") + expect(Codec.roundtrip_u64(0).zero?, "direct u64 zero") + expect(Codec.roundtrip_f64(Float::NAN).nan?, "direct NaN") + expect(Codec.roundtrip_f64(Float::INFINITY) == Float::INFINITY, "direct +inf") + expect(Codec.roundtrip_f64(-Float::INFINITY) == -Float::INFINITY, "direct -inf") + expect(negative_zero?(Codec.roundtrip_f64(-0.0)), "direct -0.0 keeps its sign") + expect(Codec.roundtrip_f64(Float::MAX) == Float::MAX, "direct f64::MAX") + expect(Codec.roundtrip_f64(Float::MIN) == Float::MIN, "direct smallest normal") + expect(Codec.roundtrip_f64(5e-324) == 5e-324, "direct subnormal") + expect(Codec.roundtrip_bool(true) == true && Codec.roundtrip_bool(false) == false, "direct bool") + expect(Codec.roundtrip_color(Codec::Color::BLUE) == Codec::Color::BLUE, "direct enum BLUE") + expect(Codec.roundtrip_color(Codec::Color::RED) == Codec::Color::RED, "direct enum RED") -# --- Holder (objects inside buffers) --------------------------------------- -holder = Codec.make_holder(10, true) -expect(holder.is_a?(Codec::Holder), "make_holder returns a Holder") -expect(holder.primary.is_a?(Codec::Token) && !holder.primary.closed?, "primary is an open Token") -expect(holder.primary.value == 10, "primary value (got #{holder.primary.value})") -expect(holder.spare.is_a?(Codec::Token) && holder.spare.value == 11, "spare value") -expect(holder.many.length == 3 && holder.many.map(&:value) == [12, 13, 14], "many values (got #{holder.many.map(&:value)})") -addresses = ([holder.primary, holder.spare] + holder.many).map { |t| t.handle.address } -expect(addresses.uniq.length == 5, "five distinct token objects") + # --- Holder (objects inside buffers) --------------------------------------- + holder = Codec.make_holder(10, true) + expect(holder.is_a?(Codec::Holder), "make_holder returns a Holder") + expect(holder.primary.is_a?(Codec::Token) && !holder.primary.closed?, "primary is an open Token") + expect(holder.primary.value == 10, "primary value (got #{holder.primary.value})") + expect(holder.spare.is_a?(Codec::Token) && holder.spare.value == 11, "spare value") + expect(holder.many.length == 3 && holder.many.map(&:value) == [12, 13, 14], "many values (got #{holder.many.map(&:value)})") + addresses = ([holder.primary, holder.spare] + holder.many).map { |t| t.handle.address } + expect(addresses.uniq.length == 5, "five distinct token objects") -# Encoding a Holder mints one reference per token, which the producer consumes. -expect(Codec.sum_holder(holder) == 10 + 11 + 12 + 13 + 14, "sum_holder (got #{Codec.sum_holder(holder)})") -expect(Codec.sum_holder(holder) == 60, "sum_holder is repeatable (tokens not consumed)") -expect(holder.primary.value == 10 && holder.spare.value == 11, "tokens usable after being encoded") + # Encoding a Holder mints one reference per token, which the producer consumes. + expect(Codec.sum_holder(holder) == 10 + 11 + 12 + 13 + 14, "sum_holder (got #{Codec.sum_holder(holder)})") + expect(Codec.sum_holder(holder) == 60, "sum_holder is repeatable (tokens not consumed)") + expect(holder.primary.value == 10 && holder.spare.value == 11, "tokens usable after being encoded") -# primary_of returns a wrapper to the SAME object as holder.primary. -primary = Codec.primary_of(holder) -expect(primary.is_a?(Codec::Token), "primary_of returns a Token") -expect(!primary.equal?(holder.primary), "primary_of returns a distinct wrapper") -expect(primary.handle.address == holder.primary.handle.address, "primary_of wraps the same object") -expect(primary.value == 10, "primary_of value") -primary.close -primary.close -expect(primary.closed?, "primary_of wrapper closed idempotently") -expect(holder.primary.value == 10, "original primary alive after closing the returned wrapper") + # primary_of returns a wrapper to the SAME object as holder.primary. + primary = Codec.primary_of(holder) + expect(primary.is_a?(Codec::Token), "primary_of returns a Token") + expect(!primary.equal?(holder.primary), "primary_of returns a distinct wrapper") + expect(primary.handle.address == holder.primary.handle.address, "primary_of wraps the same object") + expect(primary.value == 10, "primary_of value") + primary.close + primary.close + expect(primary.closed?, "primary_of wrapper closed idempotently") + expect(holder.primary.value == 10, "original primary alive after closing the returned wrapper") -# same_primary compares object identity through two encoded buffers. -expect(Codec.same_primary(holder, holder) == true, "same_primary(h, h)") -other = Codec.make_holder(10, true) -expect(other.primary.value == 10, "other holder has equal values") -expect(Codec.same_primary(holder, other) == false, "same_primary is identity, not value") -aliased = Codec::Holder.new(primary: holder.primary, spare: nil, many: []) -expect(Codec.same_primary(holder, aliased) == true, "a Ruby-built Holder sharing the wrapper is the same primary") -twin = holder.primary.dup -expect(twin.handle.address == holder.primary.handle.address, "dup of a Token wraps the same object") -expect(Codec.same_primary(holder, Codec::Holder.new(primary: twin, spare: nil, many: [])) == true, - "a dup'd wrapper is still the same object") -twin.close + # same_primary compares object identity through two encoded buffers. + expect(Codec.same_primary(holder, holder) == true, "same_primary(h, h)") + other = Codec.make_holder(10, true) + expect(other.primary.value == 10, "other holder has equal values") + expect(Codec.same_primary(holder, other) == false, "same_primary is identity, not value") + aliased = Codec::Holder.new(primary: holder.primary, spare: nil, many: []) + expect(Codec.same_primary(holder, aliased) == true, "a Ruby-built Holder sharing the wrapper is the same primary") + twin = holder.primary.dup + expect(twin.handle.address == holder.primary.handle.address, "dup of a Token wraps the same object") + expect(Codec.same_primary(holder, Codec::Holder.new(primary: twin, spare: nil, many: [])) == true, + "a dup'd wrapper is still the same object") + twin.close -# A Holder built entirely in Ruby from constructor-made tokens. -t1 = Codec::Token.new(1) -t2 = Codec::Token.new(2) -t3 = Codec::Token.new(-3) -expect(t1.value == 1 && t3.value == -3, "Token.new values") -built = Codec::Holder.new(primary: t1, spare: t2, many: [t3, t1, t1]) -expect(Codec.sum_holder(built) == 1 + 2 + -3 + 1 + 1, "sum_holder over a Ruby-built Holder (repeated object)") -expect(Codec.primary_of(built).handle.address == t1.handle.address, "primary_of a Ruby-built Holder") -expect(Codec.sum_holder(Codec::Holder.new(primary: t1, spare: nil, many: [])) == 1, "Holder with nothing optional") -expect(Codec.sum_holder(Codec::Holder.new(primary: Codec::Token.new(I64_MAX), spare: nil, many: [])) == I64_MAX, - "i64::MAX through a token") + # A Holder built entirely in Ruby from constructor-made tokens. + t1 = Codec::Token.new(1) + t2 = Codec::Token.new(2) + t3 = Codec::Token.new(-3) + expect(t1.value == 1 && t3.value == -3, "Token.new values") + built = Codec::Holder.new(primary: t1, spare: t2, many: [t3, t1, t1]) + expect(Codec.sum_holder(built) == 1 + 2 + -3 + 1 + 1, "sum_holder over a Ruby-built Holder (repeated object)") + expect(Codec.primary_of(built).handle.address == t1.handle.address, "primary_of a Ruby-built Holder") + expect(Codec.sum_holder(Codec::Holder.new(primary: t1, spare: nil, many: [])) == 1, "Holder with nothing optional") + expect(Codec.sum_holder(Codec::Holder.new(primary: Codec::Token.new(I64_MAX), spare: nil, many: [])) == I64_MAX, + "i64::MAX through a token") -# Absent optional object. -without = Codec.make_holder(0, false) -expect(without.spare.nil?, "make_holder(_, false) has no spare") -expect(without.primary.value.zero? && without.many.map(&:value) == [2, 3, 4], "values without spare") -expect(Codec.sum_holder(without) == 9, "sum without spare") + # Absent optional object. + without = Codec.make_holder(0, false) + expect(without.spare.nil?, "make_holder(_, false) has no spare") + expect(without.primary.value.zero? && without.many.map(&:value) == [2, 3, 4], "values without spare") + expect(Codec.sum_holder(without) == 9, "sum without spare") -# A closed token inside a Holder is refused before anything is encoded. -t2.close -begin - Codec.sum_holder(built) - raise "expected Error encoding a closed Token" -rescue Codec::Error => e - expect(e.message.include?("after close"), "closed token in a buffer (got #{e.message.inspect})") -end -begin - t2.value - raise "expected Error when using a closed Token" -rescue Codec::Error => e - expect(e.message.include?("after close"), "use-after-close message") -end + # A closed token inside a Holder is refused before anything is encoded. + t2.close + begin + Codec.sum_holder(built) + raise "expected Error encoding a closed Token" + rescue Codec::Error => e + expect(e.message.include?("after close"), "closed token in a buffer (got #{e.message.inspect})") + end + begin + t2.value + raise "expected Error when using a closed Token" + rescue Codec::Error => e + expect(e.message.include?("after close"), "use-after-close message") + end -# Release: every wrapper closes exactly once, double close is a no-op, and -# wrappers left to GC release without double frees. -[holder, other, without].each do |h| - h.primary.close - h.spare&.close - h.many.each(&:close) - h.primary.close - h.many.each(&:close) - expect(h.primary.closed? && h.many.all?(&:closed?), "holder tokens closed") + # Release: every wrapper closes exactly once, double close is a no-op, and + # wrappers left to GC release without double frees. + [holder, other, without].each do |h| + h.primary.close + h.spare&.close + h.many.each(&:close) + h.primary.close + h.many.each(&:close) + expect(h.primary.closed? && h.many.all?(&:closed?), "holder tokens closed") + end + t1.close + t3.close + t1.close + 200.times { |i| Codec.sum_holder(Codec.make_holder(i, i.odd?)) } + GC.start + GC.start + expect(Codec.roundtrip_i64(42) == 42, "library still healthy after GC of unclosed wrappers") end -t1.close -t3.close -t1.close -200.times { |i| Codec.sum_holder(Codec.make_holder(i, i.odd?)) } -GC.start -GC.start -expect(Codec.roundtrip_i64(42) == 42, "library still healthy after GC of unclosed wrappers") puts "ruby/codec: OK" diff --git a/conformance/ruby/contacts.rb b/conformance/ruby/contacts.rb index 01e0bfe6..b11041fc 100644 --- a/conformance/ruby/contacts.rb +++ b/conformance/ruby/contacts.rb @@ -1,102 +1,145 @@ # frozen_string_literal: true # Conformance consumer: contacts sample, Ruby target. # -# Drives the 0.7.0 value-type surface: Contact is a plain Ruby value class -# decoded from the value buffer the producer returns (the optional email is -# nil-able and the list return is a native Array), while ContactBook is a +# Drives the value-type surface: Contact is a plain Ruby value class decoded +# from the value buffer the producer returns (the optional email is nil-able +# and the list return is a native Array), while ContactBook is a # reference-counted interface wrapper whose `close` releases its strong # reference early (idempotent; the AutoPointer otherwise releases it at GC # time), and whose `dup` mints an independent wrapper. Throwing methods # raise the typed ContactsError subclasses (InvalidName=1, NotFound=2); -# non-throwing methods keep the generic WeaveFFI::Error for panics only. The -# cdylib is selected via WEAVEFFI_LIBRARY. +# non-throwing methods keep the generic Contacts::Error for panics only. The +# nested contacts.groups module uses the parent's enum, interface, and error +# domain; the sibling directory root shares the Contact record. The gem is +# installed from the generated tree; the cdylib is selected through +# CONTACTS_LIBRARY. -$LOAD_PATH.unshift(File.join(ENV.fetch("WV_RB"), "lib")) -require "contacts" +require_relative 'support' +require 'contacts' -def expect(cond, msg) - raise "assertion failed: #{msg}" unless cond -end +run_and_check_leaks(Contacts) do + expect(Contacts::ABI_VERSION == 3, "bindings target ABI revision 3") + book = Contacts::ContactBook.new -book = WeaveFFI::ContactBook.new + alice = book.add("Alice", "Smith", "alice@example.com", Contacts::ContactType::WORK) + expect(alice.is_a?(Contacts::Contact), "add returns a Contact value") + expect(alice.id.positive?, "alice id positive") + expect(alice.first_name == "Alice", "first_name") + expect(alice.last_name == "Smith", "last_name") + expect(alice.email == "alice@example.com", "email") + expect(alice.contact_type == Contacts::ContactType::WORK, "contact_type") -alice = book.add("Alice", "Smith", "alice@example.com", WeaveFFI::ContactType::WORK) -expect(alice.is_a?(WeaveFFI::Contact), "add returns a Contact value") -expect(alice.id.positive?, "alice id positive") -expect(alice.first_name == "Alice", "first_name") -expect(alice.last_name == "Smith", "last_name") -expect(alice.email == "alice@example.com", "email") -expect(alice.contact_type == WeaveFFI::ContactType::WORK, "contact_type") + # get decodes a fresh snapshot; value classes compare structurally. + expect(book.get(alice.id) == alice, "get snapshot equals added contact") -# get decodes a fresh snapshot; value classes compare structurally. -expect(book.get(alice.id) == alice, "get snapshot equals added contact") + # Optional string: a missing email round-trips as nil. + bob = book.add("Bob", "Jones", nil, Contacts::ContactType::PERSONAL) + expect(book.get(bob.id).email.nil?, "bob email nil") -# Optional string: a missing email round-trips as nil. -bob = book.add("Bob", "Jones", nil, WeaveFFI::ContactType::PERSONAL) -expect(book.get(bob.id).email.nil?, "bob email nil") + expect(book.count == 2, "count == 2") + everyone = book.list + expect(everyone.is_a?(Array), "list returns a native Array") + expect(everyone.length == 2, "list length == 2") + expect(everyone.map(&:first_name).sort == %w[Alice Bob], "list names") -expect(book.count == 2, "count == 2") -everyone = book.list -expect(everyone.is_a?(Array), "list returns a native Array") -expect(everyone.length == 2, "list length == 2") -expect(everyone.map(&:first_name).sort == %w[Alice Bob], "list names") + expect(book.remove(alice.id) == true, "remove returns true") + expect(book.count == 1, "count == 1 after remove") + expect(book.remove(alice.id) == false, "second remove returns false") -expect(book.remove(alice.id) == true, "remove returns true") -expect(book.count == 1, "count == 1 after remove") -expect(book.remove(alice.id) == false, "second remove returns false") + # Typed errors: each domain code raises its own subclass of the domain + # class, which itself subclasses the generic error. + begin + book.add("", "Smith", nil, Contacts::ContactType::PERSONAL) + raise "expected ContactsError::InvalidName for empty name" + rescue Contacts::ContactsError::InvalidName => e + expect(e.code == 1, "InvalidName code == 1 (got #{e.code})") + end -# Typed errors: each domain code raises its own subclass of the domain -# class, which itself subclasses the generic error. -begin - book.add("", "Smith", nil, WeaveFFI::ContactType::PERSONAL) - raise "expected ContactsError::InvalidName for empty name" -rescue WeaveFFI::ContactsError::InvalidName => e - expect(e.code == 1, "InvalidName code == 1 (got #{e.code})") -end + begin + book.get(9999) + raise "expected ContactsError::NotFound for missing contact" + rescue Contacts::ContactsError::NotFound => e + expect(e.code == 2, "NotFound code == 2 (got #{e.code})") + expect(e.is_a?(Contacts::ContactsError), "NotFound is a ContactsError") + expect(e.is_a?(Contacts::Error), "domain errors subclass Contacts::Error") + end -begin - book.get(9999) - raise "expected ContactsError::NotFound for missing contact" -rescue WeaveFFI::ContactsError::NotFound => e - expect(e.code == 2, "NotFound code == 2 (got #{e.code})") - expect(e.is_a?(WeaveFFI::ContactsError), "NotFound is a ContactsError") - expect(e.is_a?(WeaveFFI::Error), "domain errors subclass WeaveFFI::Error") -end + # Rescuing the domain base class catches any code in the domain. + begin + book.get(9999) + raise "expected ContactsError" + rescue Contacts::ContactsError => e + expect(e.code == 2, "domain rescue sees NotFound code (got #{e.code})") + end -# Rescuing the domain base class catches any code in the domain. -begin - book.get(9999) - raise "expected ContactsError" -rescue WeaveFFI::ContactsError => e - expect(e.code == 2, "domain rescue sees NotFound code (got #{e.code})") -end + # Each book owns independent state. `dup` produces a second wrapper holding + # its own strong reference to the same object, so state is shared and closing + # one wrapper leaves the other usable. + other = Contacts::ContactBook.new + expect(other.count.zero?, "fresh book empty") + twin = other.dup + expect(twin.handle.address == other.handle.address, "dup wraps the same object") + other.add("Carol", "White", nil, Contacts::ContactType::PERSONAL) + expect(twin.count == 1, "state visible through the dup (got #{twin.count})") + twin.close + expect(twin.closed?, "dup reports closed") + expect(!other.closed?, "original still open after closing the dup") + expect(other.count == 1, "original usable after closing the dup") + + # Explicit close releases the wrapper's reference early; a second close is a + # no-op (the AutoPointer's GC release is also a no-op afterward, so no + # double-free), and using a closed wrapper raises the brand error. + other.close + other.close + expect(other.closed?, "closed? after close") + begin + other.count + raise "expected Error when using a closed ContactBook" + rescue Contacts::Error => e + expect(e.message.include?("after close"), "use-after-close message (got #{e.message.inspect})") + end + book.close -# Each book owns independent state. `dup` produces a second wrapper holding -# its own strong reference to the same object, so state is shared and closing -# one wrapper leaves the other usable. -other = WeaveFFI::ContactBook.new -expect(other.count.zero?, "fresh book empty") -twin = other.dup -expect(twin.handle.address == other.handle.address, "dup wraps the same object") -other.add("Carol", "White", nil, WeaveFFI::ContactType::PERSONAL) -expect(twin.count == 1, "state visible through the dup (got #{twin.count})") -twin.close -expect(twin.closed?, "dup reports closed") -expect(!other.closed?, "original still open after closing the dup") -expect(other.count == 1, "original usable after closing the dup") + # contacts.groups is nested under the contacts root: its functions take and + # return the parent's ContactType enum and ContactBook interface and report + # the parent's ContactsError. + team = Contacts::ContactBook.new + team.add("Ada", "Lovelace", "ada@example.com", Contacts::ContactType::WORK) + team.add("Grace", "Hopper", nil, Contacts::ContactType::WORK) + team.add("Linus", "Pauling", nil, Contacts::ContactType::PERSONAL) + expect(Contacts.count_of_type(team, Contacts::ContactType::WORK) == 2, "count_of_type WORK") + expect(Contacts.count_of_type(team, Contacts::ContactType::OTHER).zero?, "count_of_type OTHER") + expect(Contacts.dominant_type(team) == Contacts::ContactType::WORK, "dominant_type is WORK") + work = Contacts.split_by_type(team, Contacts::ContactType::WORK) + expect(work.is_a?(Contacts::ContactBook), "split_by_type returns a ContactBook") + expect(work.handle.address != team.handle.address, "split_by_type returns a new book") + expect(work.count == 2 && team.count == 3, "split copies only WORK contacts") + first = Contacts.first_of_type(team, Contacts::ContactType::PERSONAL) + expect(first.is_a?(Contacts::Contact) && first.first_name == "Linus", "first_of_type PERSONAL") + begin + Contacts.first_of_type(work, Contacts::ContactType::PERSONAL) + raise "expected ContactsError::NotFound from the nested module" + rescue Contacts::ContactsError::NotFound => e + expect(e.code == 2, "nested module reports the parent's domain (got #{e.code})") + end -# Explicit close releases the wrapper's reference early; a second close is a -# no-op (the AutoPointer's GC release is also a no-op afterward, so no -# double-free), and using a closed wrapper raises the brand error. -other.close -other.close -expect(other.closed?, "closed? after close") -begin - other.count - raise "expected Error when using a closed ContactBook" -rescue WeaveFFI::Error => e - expect(e.message.include?("after close"), "use-after-close message (got #{e.message.inspect})") + # directory is a sibling root sharing the Contact record (a value type) and + # declaring its own DirectoryError domain. + card = Contacts.card(first) + expect(card == Contacts::Card.new(display_name: "Pauling, Linus", initials: "LP", has_email: false), + "card (got #{card.display_name.inspect} #{card.initials.inspect} #{card.has_email})") + expect(Contacts.card(team.list[0]).has_email, "card sees the email") + sorted = Contacts.sorted(team.list) + expect(sorted.map(&:last_name) == %w[Hopper Lovelace Pauling], "sorted by last name (got #{sorted.map(&:last_name)})") + begin + Contacts.sorted([]) + raise "expected DirectoryError::Empty" + rescue Contacts::DirectoryError::Empty => e + expect(e.code == 1, "DirectoryError::Empty code (got #{e.code})") + expect(!e.is_a?(Contacts::ContactsError), "the sibling root's domain is distinct") + end + work.close + team.close end -book.close puts "ruby/contacts: OK" diff --git a/conformance/ruby/events.rb b/conformance/ruby/events.rb index d8449b82..ae4a4021 100644 --- a/conformance/ruby/events.rb +++ b/conformance/ruby/events.rb @@ -1,35 +1,35 @@ # frozen_string_literal: true # Conformance consumer: events sample, Ruby target. # -# Drives the ABI 2 callback-interface surface: a Ruby class that includes the -# generated `WeaveFFI::Subscriber` mixin and overrides its three methods, a -# reference-counted `EventBus` object (constructor, `dup`, `close`, use after -# close), the bus handing a strong `EventBus` reference to the consumer -# through `on_attached`, `Delivery` return values steering `publish`'s -# accepted count, a Ruby exception inside a callback surfacing to the caller -# as `WeaveFFI::Error` with FOREIGN_ERROR_CODE (-4) without crashing the VM, -# the blocking async `publish_later` bridge, the iterator-backed `messages` -# Enumerator, the optional `last_message` record, and the release of every -# consumer implementation (the producer's `free` entry drops the registry -# slot). The cdylib is selected via WEAVEFFI_LIBRARY. - -$LOAD_PATH.unshift(File.join(ENV.fetch("WV_RB"), "lib")) -require "events" - -def expect(cond, msg) - raise "assertion failed: #{msg}" unless cond -end - -# The registry of live callback implementations is private generated state, -# but its size is the only observable proof that the producer's `free` entry -# ran (and that a consumer implementation is not leaked or freed twice). +# Drives the ABI 3 callback-interface surface: a Ruby class that includes +# the generated `Events::Subscriber` mixin and overrides its three methods, +# a reference-counted `EventBus` object (constructor, `dup`, `close`, use +# after close), the bus handing a strong `EventBus` reference to the +# consumer through `on_attached`, `Delivery` return values steering +# `publish`'s accepted count, a Ruby exception inside a callback (both the +# Result-returning `route` and the plain `on_message`) surfacing to the +# caller as `Events::Error` with FOREIGN_ERROR_CODE (-4) without crashing +# the VM, the blocking async `publish_later` bridge (whose callbacks run on +# a producer thread), the iterator-backed `messages` Enumerator, the +# optional `last_message` record, and the release of every consumer +# implementation (the producer's `free` entry drops the handle-table slot). +# The gem is installed from the generated tree; the cdylib is selected +# through EVENTS_LIBRARY. + +require_relative 'support' +require 'events' + +# The handle table is private generated state, but with no async call +# pending its size is the number of live callback implementations: the +# only observable proof that the producer's `free` entry ran (and that a +# consumer implementation is not leaked or freed twice). def live_callbacks - WeaveFFI.instance_variable_get(:@wv_cb_registry).size + Events.instance_variable_get(:@wv_handles).size end # A subscriber that records every call so the arguments can be asserted. class Recorder - include WeaveFFI::Subscriber + include Events::Subscriber attr_reader :routed, :messages, :attached @@ -46,10 +46,10 @@ def initialize(skip: nil, stop: nil, fail_on: nil, fail_in_message: nil) def route(topic) @routed << topic raise "boom: #{topic}" if topic == @fail_on - return WeaveFFI::Delivery::SKIP if topic == @skip - return WeaveFFI::Delivery::ACCEPT_AND_STOP if topic == @stop + return Events::Delivery::SKIP if topic == @skip + return Events::Delivery::ACCEPT_AND_STOP if topic == @stop - WeaveFFI::Delivery::ACCEPT + Events::Delivery::ACCEPT end def on_message(message) @@ -66,197 +66,226 @@ def on_attached(bus) # The mixin supplies NotImplementedError defaults for anything not overridden. class Bare - include WeaveFFI::Subscriber + include Events::Subscriber end -expect(WeaveFFI::FOREIGN_ERROR_CODE == -4, "FOREIGN_ERROR_CODE is -4") -expect(WeaveFFI::ABI_VERSION == 2, "bindings target ABI revision 2") -expect(WeaveFFI::Delivery::ACCEPT.zero?, "Delivery::ACCEPT == 0") -expect(WeaveFFI::Delivery::SKIP == 1, "Delivery::SKIP == 1") -expect(WeaveFFI::Delivery::ACCEPT_AND_STOP == 2, "Delivery::ACCEPT_AND_STOP == 2") - -baseline = live_callbacks - -bus = WeaveFFI::EventBus.new -expect(bus.is_a?(WeaveFFI::EventBus), "constructor returns an EventBus") -expect(!bus.closed?, "fresh bus is open") -expect(bus.subscriber_count.zero?, "fresh bus has no subscribers") -expect(bus.last_message.nil?, "last_message is nil before any publish") -expect(bus.messages.is_a?(Enumerator), "messages returns an Enumerator") -expect(bus.messages.to_a == [], "no messages before any publish") - -# subscribe: the bus calls on_attached synchronously with a strong reference -# to itself, which the consumer adopts as a fresh wrapper to the same object. -first = Recorder.new(skip: "quiet", stop: "stop") -expect(bus.subscribe(first) == 1, "first subscribe returns count 1") -expect(first.attached.length == 1, "on_attached called once (got #{first.attached.length})") -handed = first.attached[0] -expect(handed.is_a?(WeaveFFI::EventBus), "on_attached receives an EventBus wrapper") -expect(handed.handle.address == bus.handle.address, "handed bus is the same underlying object") -expect(handed.subscriber_count == 1, "handed bus is usable (got #{handed.subscriber_count})") -handed.close -handed.close -expect(handed.closed?, "handed bus wrapper closed (idempotently)") -expect(bus.subscriber_count == 1, "original bus survives closing the handed reference") - -second = Recorder.new -expect(bus.subscribe(second) == 2, "second subscribe returns count 2") -expect(second.attached.length == 1, "second on_attached called once") -second.attached[0].close -expect(live_callbacks == baseline + 2, "two live implementations registered") - -# publish: Accept from both subscribers -> 2 accepted; the Message record -# arrives fully decoded (seq, topic, text, tags). -expect(bus.publish("news", "hello", %w[x y]) == 2, "both subscribers accept news") -expect(first.routed == %w[news], "first.route saw news (got #{first.routed})") -expect(second.routed == %w[news], "second.route saw news (got #{second.routed})") -expect(first.messages.length == 1 && second.messages.length == 1, "each got one message") -msg = first.messages[0] -expect(msg.is_a?(WeaveFFI::Message), "on_message receives a Message") -expect(msg.seq == 1, "seq starts at 1 (got #{msg.seq})") -expect(msg.topic == "news", "message topic (got #{msg.topic.inspect})") -expect(msg.text == "hello", "message text (got #{msg.text.inspect})") -expect(msg.tags == %w[x y], "message tags (got #{msg.tags})") -expect(second.messages[0] == msg, "both subscribers saw the same message (structural equality)") - -# Skip from the first subscriber -> only the second accepts. -expect(bus.publish("quiet", "psst", []) == 1, "skip lowers the accepted count") -expect(first.routed == %w[news quiet], "first.route saw quiet") -expect(first.messages.length == 1, "skipped subscriber got no message") -expect(second.messages.length == 2, "second subscriber got psst") -expect(second.messages[1].seq == 2 && second.messages[1].tags == [], "second message seq 2, no tags") - -# AcceptAndStop from the first subscriber -> delivered to it, second not asked. -expect(bus.publish("stop", "last", ["t"]) == 1, "accept-and-stop halts delivery") -expect(first.messages.length == 2, "stopping subscriber still receives") -expect(first.messages[1].text == "last", "stopping subscriber got last") -expect(second.routed == %w[news quiet], "later subscriber not routed after stop (got #{second.routed})") -expect(second.messages.length == 2, "later subscriber not delivered after stop") - -# Async publish: blocks the caller until the producer-thread completion fires; -# the subscriber callbacks run from that thread through the FFI dispatcher. -expect(bus.publish_later("later", "async") == 2, "publish_later resolves with the accepted count") -expect(first.messages.length == 3 && second.messages.length == 3, "async publish delivered to both") -expect(first.messages[2].topic == "later" && first.messages[2].tags == [], "async message decoded") - -# Iterator: every text in order, lazily, with early termination releasing the -# producer iterator; and the optional last_message record. -expect(bus.messages.to_a == %w[hello psst last async], "messages iterator (got #{bus.messages.to_a})") -expect(bus.messages.first(2) == %w[hello psst], "early break through first(2)") -collected = [] -bus.messages.each { |t| collected << t.upcase } -expect(collected == %w[HELLO PSST LAST ASYNC], "block iteration") -expect(bus.messages.each_with_index.to_a.last == ["async", 3], "Enumerator composes with each_with_index") -last = bus.last_message -expect(last == WeaveFFI::Message.new(seq: 4, topic: "later", text: "async", tags: []), - "last_message (got seq=#{last.seq} topic=#{last.topic} text=#{last.text} tags=#{last.tags})") - -# A Ruby exception in route surfaces to the publishing caller as the brand -# error with FOREIGN_ERROR_CODE and the exception text; the VM and the bus -# both keep working, and the subscriber stays attached. -failing = Recorder.new(fail_on: "boom", fail_in_message: "bad-body") -expect(bus.subscribe(failing) == 3, "third subscribe returns count 3") -failing.attached[0].close -begin - bus.publish("boom", "x", []) - raise "expected WeaveFFI::Error from a raising route" -rescue WeaveFFI::Error => e - expect(e.code == WeaveFFI::FOREIGN_ERROR_CODE, "raising route -> code -4 (got #{e.code})") - expect(e.message.include?("boom: boom"), "foreign error carries the exception text (got #{e.message.inspect})") -end -expect(failing.routed == %w[boom], "failing subscriber's route ran") -expect(first.messages.length == 4, "earlier subscribers were delivered before the failure") - -# The same for an exception raised in on_message. -begin - bus.publish("body", "bad-body", []) - raise "expected WeaveFFI::Error from a raising on_message" -rescue WeaveFFI::Error => e - expect(e.code == -4, "raising on_message -> code -4 (got #{e.code})") - expect(e.message.include?("on_message rejected bad-body"), "on_message failure text (got #{e.message.inspect})") -end -expect(failing.messages.empty?, "raising on_message recorded nothing") - -expect(bus.publish("ok", "y", []) == 3, "bus still usable after foreign errors") -expect(failing.messages.length == 1 && failing.messages[0].text == "y", "failing subscriber accepts ok") -expect(bus.subscriber_count == 3, "subscriber_count == 3") - -# Non-ASCII text survives every string path: the C-string topic handed to -# route, the buffered Message, the iterator item, and the last_message record. -expect(bus.publish("тема ✓", "日本語", ["ü"]) == 3, "unicode publish accepted by all") -expect(first.routed.last == "тема ✓", "route received the unicode topic (got #{first.routed.last.inspect})") -expect(first.routed.last.encoding == Encoding::UTF_8, "route topic is UTF-8 (got #{first.routed.last.encoding})") -expect(first.messages.last.text == "日本語" && first.messages.last.tags == ["ü"], "unicode message fields") -expect(bus.messages.to_a.last == "日本語", "iterator item is unicode (got #{bus.messages.to_a.last.inspect})") -expect(bus.messages.to_a.last.encoding == Encoding::UTF_8, "iterator item is UTF-8") -expect(bus.last_message.topic == "тема ✓", "last_message topic is unicode") - -# The producer logs a message before routing, so failed publishes are logged. -expect(bus.messages.to_a == ["hello", "psst", "last", "async", "x", "bad-body", "y", "日本語"], - "log includes failed publishes (got #{bus.messages.to_a})") - -# route_once: a free function taking the callback interface; the producer -# drops its only reference on return, so `free` runs and the slot is released. -before = live_callbacks -probe = Recorder.new(skip: "quiet") -expect(WeaveFFI.route_once(probe, "quiet") == WeaveFFI::Delivery::SKIP, "route_once -> SKIP") -expect(WeaveFFI.route_once(probe, "news") == WeaveFFI::Delivery::ACCEPT, "route_once -> ACCEPT") -expect(probe.routed == %w[quiet news], "route_once called route with the topic") -expect(probe.attached.empty?, "route_once never attaches") -expect(live_callbacks == before, "route_once released its implementation (#{live_callbacks} vs #{before})") - -# A mixin default (NotImplementedError) is a foreign error too. -begin - WeaveFFI.route_once(Bare.new, "x") - raise "expected WeaveFFI::Error from the NotImplementedError default" -rescue WeaveFFI::Error => e - expect(e.code == -4, "NotImplementedError default -> code -4 (got #{e.code})") - expect(e.message.include?("route is not implemented"), "default message (got #{e.message.inspect})") -end -expect(live_callbacks == before, "failing route_once released its implementation") - -# dup mints an independent wrapper (own strong reference) to the same bus. -twin = bus.dup -expect(twin.handle.address == bus.handle.address, "dup shares the object") -expect(twin.subscriber_count == 3, "dup sees the subscribers") -twin.close -expect(bus.subscriber_count == 3, "bus survives closing the dup") - -# clear_subscribers drops every retained implementation; each `free` runs -# synchronously, and publishing to an empty bus accepts nothing. -bus.clear_subscribers -expect(bus.subscriber_count.zero?, "no subscribers after clear") -expect(live_callbacks == baseline, "every implementation released after clear (#{live_callbacks} vs #{baseline})") -expect(bus.publish("after", "nobody", []) == 0, "publish to an empty bus accepts 0") -expect(first.messages.length == 7, "cleared subscriber no longer delivered (got #{first.messages.length})") - -# A subscriber held only by the bus is freed when the bus's last reference -# goes away. The reference handed through on_attached counts, so the bus -# outlives `short.close` until that wrapper is closed too. -short = WeaveFFI::EventBus.new -tail = Recorder.new -short.subscribe(tail) -expect(live_callbacks == baseline + 1, "bus retains its subscriber") -short.close -expect(live_callbacks == baseline + 1, "the on_attached reference keeps the bus (and subscriber) alive") -expect(tail.attached[0].subscriber_count == 1, "bus usable through the on_attached reference alone") -tail.attached[0].close -expect(live_callbacks == baseline, "dropping the last bus reference frees its subscriber") - -# close is idempotent and a closed wrapper refuses further calls. -bus.close -bus.close -expect(bus.closed?, "closed? after close") -begin - bus.subscriber_count - raise "expected Error when using a closed EventBus" -rescue WeaveFFI::Error => e - expect(e.message.include?("after close"), "use-after-close message (got #{e.message.inspect})") +# A subscriber that closes a bus wrapper from inside route, while the +# publish call on that same wrapper is still in flight. +class Closer < Recorder + attr_accessor :target + + def route(topic) + target&.close + super + end end -# Ruby-side wrappers that were never closed release at GC without double -# frees; force a couple of collections to shake that out. -GC.start -GC.start +run_and_check_leaks(Events) do + expect(Events::FOREIGN_ERROR_CODE == -4, "FOREIGN_ERROR_CODE is -4") + expect(Events::ABI_VERSION == 3, "bindings target ABI revision 3") + expect(Events::Delivery::ACCEPT.zero?, "Delivery::ACCEPT == 0") + expect(Events::Delivery::SKIP == 1, "Delivery::SKIP == 1") + expect(Events::Delivery::ACCEPT_AND_STOP == 2, "Delivery::ACCEPT_AND_STOP == 2") + + baseline = live_callbacks + + bus = Events::EventBus.new + expect(bus.is_a?(Events::EventBus), "constructor returns an EventBus") + expect(!bus.closed?, "fresh bus is open") + expect(bus.subscriber_count.zero?, "fresh bus has no subscribers") + expect(bus.last_message.nil?, "last_message is nil before any publish") + expect(bus.messages.is_a?(Enumerator), "messages returns an Enumerator") + expect(bus.messages.to_a == [], "no messages before any publish") + + # subscribe: the bus calls on_attached synchronously with a strong reference + # to itself, which the consumer adopts as a fresh wrapper to the same object. + first = Recorder.new(skip: "quiet", stop: "stop") + expect(bus.subscribe(first) == 1, "first subscribe returns count 1") + expect(first.attached.length == 1, "on_attached called once (got #{first.attached.length})") + handed = first.attached[0] + expect(handed.is_a?(Events::EventBus), "on_attached receives an EventBus wrapper") + expect(handed.handle.address == bus.handle.address, "handed bus is the same underlying object") + expect(handed.subscriber_count == 1, "handed bus is usable (got #{handed.subscriber_count})") + handed.close + handed.close + expect(handed.closed?, "handed bus wrapper closed (idempotently)") + expect(bus.subscriber_count == 1, "original bus survives closing the handed reference") + + second = Recorder.new + expect(bus.subscribe(second) == 2, "second subscribe returns count 2") + expect(second.attached.length == 1, "second on_attached called once") + second.attached[0].close + expect(live_callbacks == baseline + 2, "two live implementations registered") + + # publish: Accept from both subscribers -> 2 accepted; the Message record + # arrives fully decoded (seq, topic, text, tags). + expect(bus.publish("news", "hello", %w[x y]) == 2, "both subscribers accept news") + expect(first.routed == %w[news], "first.route saw news (got #{first.routed})") + expect(second.routed == %w[news], "second.route saw news (got #{second.routed})") + expect(first.messages.length == 1 && second.messages.length == 1, "each got one message") + msg = first.messages[0] + expect(msg.is_a?(Events::Message), "on_message receives a Message") + expect(msg.seq == 1, "seq starts at 1 (got #{msg.seq})") + expect(msg.topic == "news", "message topic (got #{msg.topic.inspect})") + expect(msg.text == "hello", "message text (got #{msg.text.inspect})") + expect(msg.tags == %w[x y], "message tags (got #{msg.tags})") + expect(second.messages[0] == msg, "both subscribers saw the same message (structural equality)") + + # Skip from the first subscriber -> only the second accepts. + expect(bus.publish("quiet", "psst", []) == 1, "skip lowers the accepted count") + expect(first.routed == %w[news quiet], "first.route saw quiet") + expect(first.messages.length == 1, "skipped subscriber got no message") + expect(second.messages.length == 2, "second subscriber got psst") + expect(second.messages[1].seq == 2 && second.messages[1].tags == [], "second message seq 2, no tags") + + # AcceptAndStop from the first subscriber -> delivered to it, second not asked. + expect(bus.publish("stop", "last", ["t"]) == 1, "accept-and-stop halts delivery") + expect(first.messages.length == 2, "stopping subscriber still receives") + expect(first.messages[1].text == "last", "stopping subscriber got last") + expect(second.routed == %w[news quiet], "later subscriber not routed after stop (got #{second.routed})") + expect(second.messages.length == 2, "later subscriber not delivered after stop") + + # Async publish: blocks the caller until the producer-thread completion fires; + # the subscriber callbacks run from that thread through the FFI dispatcher. + expect(bus.publish_later("later", "async") == 2, "publish_later resolves with the accepted count") + expect(first.messages.length == 3 && second.messages.length == 3, "async publish delivered to both") + expect(first.messages[2].topic == "later" && first.messages[2].tags == [], "async message decoded") + + # Iterator: every text in order, lazily, with early termination releasing the + # producer iterator; and the optional last_message record. + expect(bus.messages.to_a == %w[hello psst last async], "messages iterator (got #{bus.messages.to_a})") + expect(bus.messages.first(2) == %w[hello psst], "early break through first(2)") + collected = [] + bus.messages.each { |t| collected << t.upcase } + expect(collected == %w[HELLO PSST LAST ASYNC], "block iteration") + expect(bus.messages.each_with_index.to_a.last == ["async", 3], "Enumerator composes with each_with_index") + last = bus.last_message + expect(last == Events::Message.new(seq: 4, topic: "later", text: "async", tags: []), + "last_message (got seq=#{last.seq} topic=#{last.topic} text=#{last.text} tags=#{last.tags})") + + # A Ruby exception in route surfaces to the publishing caller as + # Events::Error with FOREIGN_ERROR_CODE and the exception text; the VM and + # the bus both keep working, and the subscriber stays attached. route is the + # producer's Result-returning callback method: the failure reaches the + # producer as an Err value it chooses to propagate, so the error is not a + # panic (-2) and the bus's state stays consistent. + failing = Recorder.new(fail_on: "boom", fail_in_message: "bad-body") + expect(bus.subscribe(failing) == 3, "third subscribe returns count 3") + failing.attached[0].close + begin + bus.publish("boom", "x", []) + raise "expected Events::Error from a raising route" + rescue Events::Error => e + expect(e.code == Events::FOREIGN_ERROR_CODE, "raising route -> code -4 (got #{e.code})") + expect(e.code != Events::PANIC_ERROR_CODE, "a Result-returning callback failure is not a panic") + expect(e.message.include?("boom: boom"), "foreign error carries the exception text (got #{e.message.inspect})") + end + expect(failing.routed == %w[boom], "failing subscriber's route ran") + expect(first.messages.length == 4, "earlier subscribers were delivered before the failure") + expect(bus.messages.to_a.last == "x", "the failed publish was still logged") + + # The same for an exception raised in on_message. + begin + bus.publish("body", "bad-body", []) + raise "expected Events::Error from a raising on_message" + rescue Events::Error => e + expect(e.code == -4, "raising on_message -> code -4 (got #{e.code})") + expect(e.message.include?("on_message rejected bad-body"), "on_message failure text (got #{e.message.inspect})") + end + expect(failing.messages.empty?, "raising on_message recorded nothing") + + expect(bus.publish("ok", "y", []) == 3, "bus still usable after foreign errors") + expect(failing.messages.length == 1 && failing.messages[0].text == "y", "failing subscriber accepts ok") + expect(bus.subscriber_count == 3, "subscriber_count == 3") + + # Non-ASCII text survives every string path: the C-string topic handed to + # route, the buffered Message, the iterator item, and the last_message record. + expect(bus.publish("тема ✓", "日本語", ["ü"]) == 3, "unicode publish accepted by all") + expect(first.routed.last == "тема ✓", "route received the unicode topic (got #{first.routed.last.inspect})") + expect(first.routed.last.encoding == Encoding::UTF_8, "route topic is UTF-8 (got #{first.routed.last.encoding})") + expect(first.messages.last.text == "日本語" && first.messages.last.tags == ["ü"], "unicode message fields") + expect(bus.messages.to_a.last == "日本語", "iterator item is unicode (got #{bus.messages.to_a.last.inspect})") + expect(bus.messages.to_a.last.encoding == Encoding::UTF_8, "iterator item is UTF-8") + expect(bus.last_message.topic == "тема ✓", "last_message topic is unicode") + + # The producer logs a message before routing, so failed publishes are logged. + expect(bus.messages.to_a == ["hello", "psst", "last", "async", "x", "bad-body", "y", "日本語"], + "log includes failed publishes (got #{bus.messages.to_a})") + + # route_once: a free function taking the callback interface; the producer + # drops its only reference on return, so `free` runs and the slot is released. + before = live_callbacks + probe = Recorder.new(skip: "quiet") + expect(Events.route_once(probe, "quiet") == Events::Delivery::SKIP, "route_once -> SKIP") + expect(Events.route_once(probe, "news") == Events::Delivery::ACCEPT, "route_once -> ACCEPT") + expect(probe.routed == %w[quiet news], "route_once called route with the topic") + expect(probe.attached.empty?, "route_once never attaches") + expect(live_callbacks == before, "route_once released its implementation (#{live_callbacks} vs #{before})") + + # A mixin default (NotImplementedError) is a foreign error too. + begin + Events.route_once(Bare.new, "x") + raise "expected Events::Error from the NotImplementedError default" + rescue Events::Error => e + expect(e.code == -4, "NotImplementedError default -> code -4 (got #{e.code})") + expect(e.message.include?("route is not implemented"), "default message (got #{e.message.inspect})") + end + expect(live_callbacks == before, "failing route_once released its implementation") + + # dup mints an independent wrapper (own strong reference) to the same bus. + twin = bus.dup + expect(twin.handle.address == bus.handle.address, "dup shares the object") + expect(twin.subscriber_count == 3, "dup sees the subscribers") + twin.close + expect(bus.subscriber_count == 3, "bus survives closing the dup") + + # clear_subscribers drops every retained implementation; each `free` runs + # synchronously, and publishing to an empty bus accepts nothing. + bus.clear_subscribers + expect(bus.subscriber_count.zero?, "no subscribers after clear") + expect(live_callbacks == baseline, "every implementation released after clear (#{live_callbacks} vs #{baseline})") + expect(bus.publish("after", "nobody", []) == 0, "publish to an empty bus accepts 0") + expect(first.messages.length == 7, "cleared subscriber no longer delivered (got #{first.messages.length})") + + # A subscriber held only by the bus is freed when the bus's last reference + # goes away. The reference handed through on_attached counts, so the bus + # outlives `short.close` until that wrapper is closed too. + short = Events::EventBus.new + tail = Recorder.new + short.subscribe(tail) + expect(live_callbacks == baseline + 1, "bus retains its subscriber") + short.close + expect(live_callbacks == baseline + 1, "the on_attached reference keeps the bus (and subscriber) alive") + expect(tail.attached[0].subscriber_count == 1, "bus usable through the on_attached reference alone") + tail.attached[0].close + expect(live_callbacks == baseline, "dropping the last bus reference frees its subscriber") + + # Closing a wrapper while a call on it is in flight (here from a callback + # the call made) defers the release until the call returns, so the producer + # never sees its object freed mid-call. + inflight = Events::EventBus.new + closer = Closer.new + inflight.subscribe(closer) + closer.attached[0].close + closer.target = inflight + expect(inflight.publish("t", "mid-call close", []) == 1, "publish completes despite the close") + expect(inflight.closed?, "the deferred close took effect") + expect(closer.messages.length == 1, "the subscriber still received the message") + expect(live_callbacks == baseline, "releasing the bus released its subscriber") + + # close is idempotent and a closed wrapper refuses further calls. + bus.close + bus.close + expect(bus.closed?, "closed? after close") + begin + bus.subscriber_count + raise "expected Error when using a closed EventBus" + rescue Events::Error => e + expect(e.message.include?("after close"), "use-after-close message (got #{e.message.inspect})") + end + + # Ruby-side wrappers that were never closed release at GC without double + # frees; force a couple of collections to shake that out. +end puts "ruby/events: OK" diff --git a/conformance/ruby/kvstore.rb b/conformance/ruby/kvstore.rb index 2fb960e7..8379fc75 100644 --- a/conformance/ruby/kvstore.rb +++ b/conformance/ruby/kvstore.rb @@ -1,32 +1,31 @@ # frozen_string_literal: true # Conformance consumer: kvstore sample, Ruby target. # -# Full-surface drive of the ABI 2 wrapper: the reference-counted Store +# Full-surface drive of the ABI 3 wrapper: the reference-counted Store # interface (the open(path) factory constructor, sync methods, the # Enumerator-backed list_keys, the blocking compact bridge over the async -# ABI, the deprecated legacy_put, the default_capacity static, `close` and -# `dup` semantics), the typed KvError domain (codes 1001-1004) with per-code -# subclasses raised by throwing members, the Entry record as a plain value -# class decoded from a value buffer, a Ruby class including the generated -# `EvictionListener` mixin (attach, fire on delete and on expiry-on-read, -# detach by returning false, replace, clear, and a raising listener surfacing -# as the brand error with code -4), the object graph (share() wraps the SAME -# object, fork(), Store? both ways in larger(), a Store inside the StoreInfo -# record from describe(), [Store] from open_many(), and [Store] plus StoreInfo? -# as parameters to total_count()), and the cross-module -# `Kvstore.get_stats(store)`. The cdylib is selected via WEAVEFFI_LIBRARY. - -$LOAD_PATH.unshift(File.join(ENV.fetch("WV_RB"), "lib")) -require "kvstore" - -def expect(cond, msg) - raise "assertion failed: #{msg}" unless cond -end - -# Size of the generated registry of live callback implementations: the only -# observable proof that the producer's `free` entry ran exactly once. +# ABI with its cancel: token, the deprecated legacy_put, the +# default_capacity static, `close` and `dup` semantics), the typed KvError +# domain (codes 1001-1004) with per-code subclasses raised by throwing +# members, the Entry record as a plain value class decoded from a value +# buffer, a Ruby class including the generated `EvictionListener` mixin +# (attach, fire on delete and on expiry-on-read, detach by returning false, +# replace, clear, and a raising listener surfacing as Kvstore::Error with +# code -4), the object graph (share() wraps the SAME object, fork(), Store? +# both ways in larger(), a Store inside the StoreInfo record from +# describe(), [Store] from open_many(), and [Store] plus StoreInfo? as +# parameters to total_count()), and the nested module's +# `Kvstore.get_stats(store)`. The gem is installed from the generated tree; +# the cdylib is selected through KVSTORE_LIBRARY. + +require_relative 'support' +require 'kvstore' + +# Size of the generated handle table: with no async call pending, it holds +# exactly the live callback implementations, so it proves the producer's +# `free` entry ran exactly once. def live_callbacks - Kvstore.instance_variable_get(:@wv_cb_registry).size + Kvstore.instance_variable_get(:@wv_handles).size end # An eviction listener recording (key, reason) pairs; returns false (detach) @@ -52,320 +51,339 @@ def on_evict(entry, reason) end end -expect(Kvstore::ABI_VERSION == 2, "bindings target ABI revision 2") -expect(Kvstore::EvictionReason::DELETED.zero?, "EvictionReason::DELETED == 0") -expect(Kvstore::EvictionReason::EXPIRED == 1, "EvictionReason::EXPIRED == 1") - -# Interface static: a class method with no self slot. -expect(Kvstore::Store.default_capacity == 1_000_000, "default_capacity") - -# The open(path) factory throws the typed IoError (1004) on an empty path. -begin - Kvstore::Store.open("") - raise "expected KvError::IoError for empty path" -rescue Kvstore::KvError::IoError => e - expect(e.code == 1004, "IoError code == 1004 (got #{e.code})") - expect(e.is_a?(Kvstore::KvError), "IoError is a KvError") - expect(e.is_a?(Kvstore::Error), "domain errors subclass Kvstore::Error") -end +run_and_check_leaks(Kvstore) do + expect(Kvstore::ABI_VERSION == 3, "bindings target ABI revision 3") + expect(Kvstore::EvictionReason::DELETED.zero?, "EvictionReason::DELETED == 0") + expect(Kvstore::EvictionReason::EXPIRED == 1, "EvictionReason::EXPIRED == 1") + + # Interface static: a class method with no self slot. + expect(Kvstore::Store.default_capacity == 1_000_000, "default_capacity") + + # The open(path) factory throws the typed IoError (1004) on an empty path. + begin + Kvstore::Store.open("") + raise "expected KvError::IoError for empty path" + rescue Kvstore::KvError::IoError => e + expect(e.code == 1004, "IoError code == 1004 (got #{e.code})") + expect(e.is_a?(Kvstore::KvError), "IoError is a KvError") + expect(e.is_a?(Kvstore::Error), "domain errors subclass Kvstore::Error") + end -store = Kvstore::Store.open("/tmp/conformance-kvstore-rb") -expect(store.is_a?(Kvstore::Store) && !store.closed?, "open returns an open Store") -payload = "\x01\x02\x03".b - -# put takes a buffered `i64?` TTL: nil and a present value both encode. -expect(store.put("alpha", payload, Kvstore::EntryKind::PERSISTENT, nil) == true, "put alpha") -expect(store.put("beta", payload, Kvstore::EntryKind::VOLATILE, 3600) == true, "put beta") -expect(store.count == 2, "count == 2") - -# Iterator-backed list-of-string return, optionally prefix-filtered (the -# `string?` prefix is a buffered optional parameter). The Enumerator is lazy -# and releases the producer iterator on early termination. -expect(store.list_keys(nil).is_a?(Enumerator), "list_keys returns an Enumerator") -keys = store.list_keys(nil).to_a -expect(keys == %w[alpha beta], "list_keys values in sorted order (got #{keys})") -expect(store.list_keys("al").to_a == %w[alpha], "list_keys prefix filter") -expect(store.list_keys("zz").to_a == [], "list_keys unmatched prefix is empty") -expect(store.list_keys(nil).first(1) == %w[alpha], "early break through first(1)") -seen = [] -store.list_keys(nil).each { |k| seen << k.upcase } -expect(seen == %w[ALPHA BETA], "block iteration") - -# Optional record return: get decodes a buffered `Entry?` into a plain value -# object covering every complex field type. -alpha = store.get("alpha") -expect(!alpha.nil?, "get alpha present") -expect(alpha.is_a?(Kvstore::Entry), "get returns an Entry value") -expect(alpha.id.positive?, "entry id positive") -expect(alpha.key == "alpha", "entry key") -expect(alpha.value == payload, "entry value bytes") -expect(alpha.expires_at.nil?, "alpha expires_at nil") -expect(alpha.tags == [], "alpha tags empty") -expect(alpha.metadata == {}, "alpha metadata empty") - -# Repeated gets decode fresh snapshots that compare structurally equal. -expect(store.get("alpha") == alpha, "second get equals first snapshot") - -beta = store.get("beta") -expect(!beta.expires_at.nil? && beta.expires_at.positive?, "beta expires_at present") -expect(beta.expires_at - beta.created_at == 3600, "beta TTL applied (got #{beta.expires_at - beta.created_at})") - -# A missing key raises the typed KeyNotFound (1001). -begin - store.get("missing") - raise "expected KvError::KeyNotFound" -rescue Kvstore::KvError::KeyNotFound => e - expect(e.code == 1001, "KeyNotFound code == 1001 (got #{e.code})") - expect(e.message == "key not found", "KeyNotFound default message (got #{e.message.inspect})") -end + store = Kvstore::Store.open("/tmp/conformance-kvstore-rb") + expect(store.is_a?(Kvstore::Store) && !store.closed?, "open returns an open Store") + payload = "\x01\x02\x03".b + + # put takes a buffered `i64?` TTL: nil and a present value both encode. + expect(store.put("alpha", payload, Kvstore::EntryKind::PERSISTENT, nil) == true, "put alpha") + expect(store.put("beta", payload, Kvstore::EntryKind::VOLATILE, 3600) == true, "put beta") + expect(store.count == 2, "count == 2") + + # Iterator-backed list-of-string return, optionally prefix-filtered (the + # `string?` prefix is a buffered optional parameter). The Enumerator is lazy + # and releases the producer iterator on early termination. + expect(store.list_keys(nil).is_a?(Enumerator), "list_keys returns an Enumerator") + keys = store.list_keys(nil).to_a + expect(keys == %w[alpha beta], "list_keys values in sorted order (got #{keys})") + expect(store.list_keys("al").to_a == %w[alpha], "list_keys prefix filter") + expect(store.list_keys("zz").to_a == [], "list_keys unmatched prefix is empty") + expect(store.list_keys(nil).first(1) == %w[alpha], "early break through first(1)") + seen = [] + store.list_keys(nil).each { |k| seen << k.upcase } + expect(seen == %w[ALPHA BETA], "block iteration") + + # Optional record return: get decodes a buffered `Entry?` into a plain value + # object covering every complex field type. + alpha = store.get("alpha") + expect(!alpha.nil?, "get alpha present") + expect(alpha.is_a?(Kvstore::Entry), "get returns an Entry value") + expect(alpha.id.positive?, "entry id positive") + expect(alpha.key == "alpha", "entry key") + expect(alpha.value == payload, "entry value bytes") + expect(alpha.expires_at.nil?, "alpha expires_at nil") + expect(alpha.tags == [], "alpha tags empty") + expect(alpha.metadata == {}, "alpha metadata empty") + + # Repeated gets decode fresh snapshots that compare structurally equal. + expect(store.get("alpha") == alpha, "second get equals first snapshot") + + beta = store.get("beta") + expect(!beta.expires_at.nil? && beta.expires_at.positive?, "beta expires_at present") + expect(beta.expires_at - beta.created_at == 3600, "beta TTL applied (got #{beta.expires_at - beta.created_at})") + + # A missing key raises the typed KeyNotFound (1001). + begin + store.get("missing") + raise "expected KvError::KeyNotFound" + rescue Kvstore::KvError::KeyNotFound => e + expect(e.code == 1001, "KeyNotFound code == 1001 (got #{e.code})") + expect(e.message == "key not found", "KeyNotFound default message (got #{e.message.inspect})") + end -# Rescuing the domain base class catches any code in the domain. -begin - store.get("missing") - raise "expected KvError" -rescue Kvstore::KvError => e - expect(e.code == 1001, "domain rescue sees KeyNotFound (got #{e.code})") -end + # Rescuing the domain base class catches any code in the domain. + begin + store.get("missing") + raise "expected KvError" + rescue Kvstore::KvError => e + expect(e.code == 1001, "domain rescue sees KeyNotFound (got #{e.code})") + end -# The deprecated method still works (and warns on stderr). -expect(store.legacy_put("legacy", payload) == true, "legacy_put") -expect(store.delete("legacy") == true, "delete legacy") -expect(store.delete("legacy") == false, "second delete returns false") - -# Entry is a plain value class: keyword construction, non-empty native -# list/map fields, a nil-able optional, and structural equality. -built = Kvstore::Entry.new( - id: 7, - key: "built", - value: payload, - created_at: 1000, - expires_at: nil, - tags: %w[hot fast], - metadata: { "source" => "test", "env" => "prod" } -) -expect(built.tags.sort == %w[fast hot], "built tags (got #{built.tags})") -expect(built.metadata == { "source" => "test", "env" => "prod" }, "built metadata") -expect(built.expires_at.nil?, "built expires_at nil") -same = Kvstore::Entry.new( - id: 7, key: "built", value: payload, created_at: 1000, - expires_at: nil, tags: %w[hot fast], - metadata: { "source" => "test", "env" => "prod" } -) -expect(built == same, "entries compare structurally") -expect(built != alpha, "different entries are unequal") - -# The cross-module call under test: kv.stats.get_stats borrows the kv.Store -# interface pointer and returns the Stats record as a value. -stats = Kvstore.get_stats(store) -expect(stats.is_a?(Kvstore::Stats), "get_stats returns a Stats value") -expect(stats.total_entries == 2, "total_entries == 2 (got #{stats.total_entries})") -expect(stats.total_bytes == 6, "total_bytes == 6 (got #{stats.total_bytes})") -expect(stats.expired_entries == 0, "expired_entries == 0 (got #{stats.expired_entries})") - -# --- Eviction listener (callback interface) ------------------------------ -baseline = live_callbacks - -listener = Listener.new -store.set_eviction_listener(listener) -expect(live_callbacks == baseline + 1, "listener registered") - -# delete fires on_evict synchronously with the removed Entry and DELETED. -expect(store.delete("beta") == true, "delete beta") -expect(listener.seen == [["beta", Kvstore::EvictionReason::DELETED]], "eviction fired for beta (got #{listener.seen})") -evicted = listener.entries[0] -expect(evicted.is_a?(Kvstore::Entry), "on_evict receives an Entry value") -expect(evicted.value == payload && evicted.expires_at == beta.expires_at, "evicted entry matches the stored one") -expect(evicted == beta, "evicted entry equals the earlier snapshot") - -# An entry whose TTL already elapsed is evicted on read: get raises Expired -# (1002) and the listener sees EXPIRED. -expect(store.put("stale", "zz".b, Kvstore::EntryKind::VOLATILE, -1) == true, "put stale") -begin - store.get("stale") - raise "expected KvError::Expired" -rescue Kvstore::KvError::Expired => e - expect(e.code == 1002, "Expired code == 1002 (got #{e.code})") -end -expect(listener.seen.last == ["stale", Kvstore::EvictionReason::EXPIRED], "expiry eviction (got #{listener.seen.last})") -expect(listener.entries.last.value == "zz".b, "expired entry payload") -expect(store.count == 1, "stale entry gone after eviction") - -# Replacing the listener frees the previous one; clearing frees the current. -second = Listener.new -store.set_eviction_listener(second) -expect(live_callbacks == baseline + 1, "replaced listener freed, replacement registered") -store.put("gamma", payload, Kvstore::EntryKind::VOLATILE, nil) -store.delete("gamma") -expect(listener.seen.length == 2, "replaced listener no longer notified") -expect(second.seen == [["gamma", Kvstore::EvictionReason::DELETED]], "replacement notified (got #{second.seen})") -store.clear_eviction_listener -expect(live_callbacks == baseline, "clear_eviction_listener freed the listener") -store.put("delta", payload, Kvstore::EntryKind::VOLATILE, nil) -store.delete("delta") -expect(second.seen.length == 1, "no notification after clear") -store.clear_eviction_listener - -# Returning false detaches (and frees) the listener after that eviction. -brief = Listener.new(keep_after: 2) -store.set_eviction_listener(brief) -store.put("one", payload, Kvstore::EntryKind::VOLATILE, nil) -store.put("two", payload, Kvstore::EntryKind::VOLATILE, nil) -store.put("three", payload, Kvstore::EntryKind::VOLATILE, nil) -store.delete("one") -expect(live_callbacks == baseline + 1, "listener retained after returning true") -store.delete("two") -expect(live_callbacks == baseline, "listener freed after returning false") -store.delete("three") -expect(brief.seen.map(&:first) == %w[one two], "detached listener misses the third eviction (got #{brief.seen})") - -# A Ruby exception inside on_evict surfaces to the caller of delete as the -# brand error with FOREIGN_ERROR_CODE (-4), not as a KvError; the entry was -# already removed, the VM is fine, and the store keeps working. -angry = Listener.new(raise_on: "kaboom") -store.set_eviction_listener(angry) -store.put("kaboom", payload, Kvstore::EntryKind::VOLATILE, nil) -begin - store.delete("kaboom") - raise "expected Kvstore::Error from a raising listener" -rescue Kvstore::KvError - raise "a foreign error must not be mapped to a KvError" -rescue Kvstore::Error => e - expect(e.code == Kvstore::FOREIGN_ERROR_CODE, "raising listener -> code -4 (got #{e.code})") - expect(e.code == -4, "FOREIGN_ERROR_CODE is -4") - expect(e.message.include?("listener exploded on kaboom"), "foreign error text (got #{e.message.inspect})") -end -expect(store.count == 1, "entry removed despite the listener failure (got #{store.count})") -expect(store.delete("kaboom") == false, "kaboom is gone") -expect(store.put("calm", payload, Kvstore::EntryKind::VOLATILE, nil) && store.delete("calm"), - "store usable after a foreign error") -expect(angry.seen.map(&:first) == %w[kaboom calm], "raising listener stays attached (got #{angry.seen})") -store.clear_eviction_listener -expect(live_callbacks == baseline, "all listeners released") - -# --- Object graph -------------------------------------------------------- -# share() returns a wrapper to the SAME object: writes through one are visible -# through the other, and closing one wrapper leaves the object alive. -shared = store.share -expect(shared.is_a?(Kvstore::Store), "share returns a Store") -expect(!shared.equal?(store), "share returns a distinct wrapper") -expect(shared.handle.address == store.handle.address, "share wraps the same object") -expect(shared.put("via-shared", payload, Kvstore::EntryKind::VOLATILE, nil), "put through the shared wrapper") -expect(store.count == 2, "write through share visible in the original (got #{store.count})") -expect(store.get("via-shared").value == payload, "entry readable through the original") -shared.close -shared.close -expect(shared.closed?, "shared wrapper closed idempotently") -expect(store.count == 2, "object survives closing the shared wrapper") - -# dup and clone also mint independent wrappers with their own reference. -twin = store.dup -expect(twin.handle.address == store.handle.address, "dup wraps the same object") -expect(twin.count == 2, "dup sees the same entries") -twin.close -expect(store.count == 2, "object survives closing the dup") - -# fork() is a new, independent object. -forked = store.fork -expect(forked.handle.address != store.handle.address, "fork is a different object") -expect(forked.count == 2, "fork copied the live entries (got #{forked.count})") -forked.put("only-in-fork", payload, Kvstore::EntryKind::VOLATILE, nil) -expect(forked.count == 3 && store.count == 2, "fork is independent of the original") - -# larger(): Store? in and out. -empty = Kvstore::Store.open("/tmp/conformance-kvstore-rb-empty") -expect(empty.larger(nil).nil?, "larger(nil) on an empty store is nil") -own = store.larger(nil) -expect(!own.nil? && own.handle.address == store.handle.address, "larger(nil) on a non-empty store is itself") -own.close -bigger = store.larger(forked) -expect(bigger.handle.address == forked.handle.address, "larger picks the fuller store") -bigger.close -still = forked.larger(empty) -expect(still.handle.address == forked.handle.address, "larger keeps self when other is smaller") -still.close -expect(forked.count == 3 && empty.count.zero?, "larger left both stores intact") - -# describe(): a record carrying the object itself plus an optional object. -info = store.describe("primary", nil) -expect(info.is_a?(Kvstore::StoreInfo), "describe returns a StoreInfo") -expect(info.label == "primary", "info.label (got #{info.label.inspect})") -expect(info.count == 2, "info.count (got #{info.count})") -expect(info.mirror.nil?, "info.mirror nil when absent") -expect(info.store.is_a?(Kvstore::Store), "info.store is a Store wrapper") -expect(info.store.handle.address == store.handle.address, "info.store is the described object") -expect(info.store.count == 2, "info.store is usable") -mirrored = store.describe("mirrored", forked) -expect(!mirrored.mirror.nil? && mirrored.mirror.handle.address == forked.handle.address, "info.mirror is the passed store") -expect(mirrored.mirror.count == 3, "info.mirror is usable") -expect(forked.count == 3, "borrowed mirror parameter still alive") - -# open_many(): a list of objects as a return; a bad path raises the typed error -# for the whole call. -many = Kvstore::Store.open_many(["/tmp/a", "/tmp/b", "/tmp/c"]) -expect(many.is_a?(Array) && many.length == 3, "open_many returns 3 stores") -expect(many.all? { |s| s.is_a?(Kvstore::Store) && !s.closed? }, "every element is an open Store") -expect(many.map { |s| s.handle.address }.uniq.length == 3, "open_many stores are distinct objects") -many[0].put("m0", payload, Kvstore::EntryKind::VOLATILE, nil) -many[2].put("m2a", payload, Kvstore::EntryKind::VOLATILE, nil) -many[2].put("m2b", payload, Kvstore::EntryKind::VOLATILE, nil) -expect(many.map(&:count) == [1, 0, 2], "open_many stores hold independent state") -expect(Kvstore::Store.open_many([]) == [], "open_many of nothing is empty") -begin - Kvstore::Store.open_many(["/tmp/ok", ""]) - raise "expected KvError::IoError from open_many" -rescue Kvstore::KvError::IoError => e - expect(e.code == 1004, "open_many IoError code (got #{e.code})") -end + # The deprecated method still works (and warns on stderr). + expect(store.legacy_put("legacy", payload) == true, "legacy_put") + expect(store.delete("legacy") == true, "delete legacy") + expect(store.delete("legacy") == false, "second delete returns false") + + # Entry is a plain value class: keyword construction, non-empty native + # list/map fields, a nil-able optional, and structural equality. + built = Kvstore::Entry.new( + id: 7, + key: "built", + value: payload, + created_at: 1000, + expires_at: nil, + tags: %w[hot fast], + metadata: { "source" => "test", "env" => "prod" } + ) + expect(built.tags.sort == %w[fast hot], "built tags (got #{built.tags})") + expect(built.metadata == { "source" => "test", "env" => "prod" }, "built metadata") + expect(built.expires_at.nil?, "built expires_at nil") + same = Kvstore::Entry.new( + id: 7, key: "built", value: payload, created_at: 1000, + expires_at: nil, tags: %w[hot fast], + metadata: { "source" => "test", "env" => "prod" } + ) + expect(built == same, "entries compare structurally") + expect(built != alpha, "different entries are unequal") + + # The cross-module call under test: kv.stats.get_stats borrows the kv.Store + # interface pointer and returns the Stats record as a value. + stats = Kvstore.get_stats(store) + expect(stats.is_a?(Kvstore::Stats), "get_stats returns a Stats value") + expect(stats.total_entries == 2, "total_entries == 2 (got #{stats.total_entries})") + expect(stats.total_bytes == 6, "total_bytes == 6 (got #{stats.total_bytes})") + expect(stats.expired_entries == 0, "expired_entries == 0 (got #{stats.expired_entries})") + + # --- Eviction listener (callback interface) ------------------------------ + baseline = live_callbacks + + listener = Listener.new + store.set_eviction_listener(listener) + expect(live_callbacks == baseline + 1, "listener registered") + + # delete fires on_evict synchronously with the removed Entry and DELETED. + expect(store.delete("beta") == true, "delete beta") + expect(listener.seen == [["beta", Kvstore::EvictionReason::DELETED]], "eviction fired for beta (got #{listener.seen})") + evicted = listener.entries[0] + expect(evicted.is_a?(Kvstore::Entry), "on_evict receives an Entry value") + expect(evicted.value == payload && evicted.expires_at == beta.expires_at, "evicted entry matches the stored one") + expect(evicted == beta, "evicted entry equals the earlier snapshot") + + # An entry whose TTL already elapsed is evicted on read: get raises Expired + # (1002) and the listener sees EXPIRED. + expect(store.put("stale", "zz".b, Kvstore::EntryKind::VOLATILE, -1) == true, "put stale") + begin + store.get("stale") + raise "expected KvError::Expired" + rescue Kvstore::KvError::Expired => e + expect(e.code == 1002, "Expired code == 1002 (got #{e.code})") + end + expect(listener.seen.last == ["stale", Kvstore::EvictionReason::EXPIRED], "expiry eviction (got #{listener.seen.last})") + expect(listener.entries.last.value == "zz".b, "expired entry payload") + expect(store.count == 1, "stale entry gone after eviction") + + # Replacing the listener frees the previous one; clearing frees the current. + second = Listener.new + store.set_eviction_listener(second) + expect(live_callbacks == baseline + 1, "replaced listener freed, replacement registered") + store.put("gamma", payload, Kvstore::EntryKind::VOLATILE, nil) + store.delete("gamma") + expect(listener.seen.length == 2, "replaced listener no longer notified") + expect(second.seen == [["gamma", Kvstore::EvictionReason::DELETED]], "replacement notified (got #{second.seen})") + store.clear_eviction_listener + expect(live_callbacks == baseline, "clear_eviction_listener freed the listener") + store.put("delta", payload, Kvstore::EntryKind::VOLATILE, nil) + store.delete("delta") + expect(second.seen.length == 1, "no notification after clear") + store.clear_eviction_listener + + # Returning false detaches (and frees) the listener after that eviction. + brief = Listener.new(keep_after: 2) + store.set_eviction_listener(brief) + store.put("one", payload, Kvstore::EntryKind::VOLATILE, nil) + store.put("two", payload, Kvstore::EntryKind::VOLATILE, nil) + store.put("three", payload, Kvstore::EntryKind::VOLATILE, nil) + store.delete("one") + expect(live_callbacks == baseline + 1, "listener retained after returning true") + store.delete("two") + expect(live_callbacks == baseline, "listener freed after returning false") + store.delete("three") + expect(brief.seen.map(&:first) == %w[one two], "detached listener misses the third eviction (got #{brief.seen})") + + # A Ruby exception inside on_evict surfaces to the caller of delete as the + # brand error with FOREIGN_ERROR_CODE (-4), not as a KvError; the entry was + # already removed, the VM is fine, and the store keeps working. + angry = Listener.new(raise_on: "kaboom") + store.set_eviction_listener(angry) + store.put("kaboom", payload, Kvstore::EntryKind::VOLATILE, nil) + begin + store.delete("kaboom") + raise "expected Kvstore::Error from a raising listener" + rescue Kvstore::KvError + raise "a foreign error must not be mapped to a KvError" + rescue Kvstore::Error => e + expect(e.code == Kvstore::FOREIGN_ERROR_CODE, "raising listener -> code -4 (got #{e.code})") + expect(e.code == -4, "FOREIGN_ERROR_CODE is -4") + expect(e.message.include?("listener exploded on kaboom"), "foreign error text (got #{e.message.inspect})") + end + expect(store.count == 1, "entry removed despite the listener failure (got #{store.count})") + expect(store.delete("kaboom") == false, "kaboom is gone") + expect(store.put("calm", payload, Kvstore::EntryKind::VOLATILE, nil) && store.delete("calm"), + "store usable after a foreign error") + expect(angry.seen.map(&:first) == %w[kaboom calm], "raising listener stays attached (got #{angry.seen})") + store.clear_eviction_listener + expect(live_callbacks == baseline, "all listeners released") + + # --- Object graph -------------------------------------------------------- + # share() returns a wrapper to the SAME object: writes through one are visible + # through the other, and closing one wrapper leaves the object alive. + shared = store.share + expect(shared.is_a?(Kvstore::Store), "share returns a Store") + expect(!shared.equal?(store), "share returns a distinct wrapper") + expect(shared.handle.address == store.handle.address, "share wraps the same object") + expect(shared.put("via-shared", payload, Kvstore::EntryKind::VOLATILE, nil), "put through the shared wrapper") + expect(store.count == 2, "write through share visible in the original (got #{store.count})") + expect(store.get("via-shared").value == payload, "entry readable through the original") + shared.close + shared.close + expect(shared.closed?, "shared wrapper closed idempotently") + expect(store.count == 2, "object survives closing the shared wrapper") + + # dup and clone also mint independent wrappers with their own reference. + twin = store.dup + expect(twin.handle.address == store.handle.address, "dup wraps the same object") + expect(twin.count == 2, "dup sees the same entries") + twin.close + expect(store.count == 2, "object survives closing the dup") + + # fork() is a new, independent object. + forked = store.fork + expect(forked.handle.address != store.handle.address, "fork is a different object") + expect(forked.count == 2, "fork copied the live entries (got #{forked.count})") + forked.put("only-in-fork", payload, Kvstore::EntryKind::VOLATILE, nil) + expect(forked.count == 3 && store.count == 2, "fork is independent of the original") + + # larger(): Store? in and out. + empty = Kvstore::Store.open("/tmp/conformance-kvstore-rb-empty") + expect(empty.larger(nil).nil?, "larger(nil) on an empty store is nil") + own = store.larger(nil) + expect(!own.nil? && own.handle.address == store.handle.address, "larger(nil) on a non-empty store is itself") + own.close + bigger = store.larger(forked) + expect(bigger.handle.address == forked.handle.address, "larger picks the fuller store") + bigger.close + still = forked.larger(empty) + expect(still.handle.address == forked.handle.address, "larger keeps self when other is smaller") + still.close + expect(forked.count == 3 && empty.count.zero?, "larger left both stores intact") + + # describe(): a record carrying the object itself plus an optional object. + info = store.describe("primary", nil) + expect(info.is_a?(Kvstore::StoreInfo), "describe returns a StoreInfo") + expect(info.label == "primary", "info.label (got #{info.label.inspect})") + expect(info.count == 2, "info.count (got #{info.count})") + expect(info.mirror.nil?, "info.mirror nil when absent") + expect(info.store.is_a?(Kvstore::Store), "info.store is a Store wrapper") + expect(info.store.handle.address == store.handle.address, "info.store is the described object") + expect(info.store.count == 2, "info.store is usable") + mirrored = store.describe("mirrored", forked) + expect(!mirrored.mirror.nil? && mirrored.mirror.handle.address == forked.handle.address, "info.mirror is the passed store") + expect(mirrored.mirror.count == 3, "info.mirror is usable") + expect(forked.count == 3, "borrowed mirror parameter still alive") + + # open_many(): a list of objects as a return; a bad path raises the typed error + # for the whole call. + many = Kvstore::Store.open_many(["/tmp/a", "/tmp/b", "/tmp/c"]) + expect(many.is_a?(Array) && many.length == 3, "open_many returns 3 stores") + expect(many.all? { |s| s.is_a?(Kvstore::Store) && !s.closed? }, "every element is an open Store") + expect(many.map { |s| s.handle.address }.uniq.length == 3, "open_many stores are distinct objects") + many[0].put("m0", payload, Kvstore::EntryKind::VOLATILE, nil) + many[2].put("m2a", payload, Kvstore::EntryKind::VOLATILE, nil) + many[2].put("m2b", payload, Kvstore::EntryKind::VOLATILE, nil) + expect(many.map(&:count) == [1, 0, 2], "open_many stores hold independent state") + expect(Kvstore::Store.open_many([]) == [], "open_many of nothing is empty") + begin + Kvstore::Store.open_many(["/tmp/ok", ""]) + raise "expected KvError::IoError from open_many" + rescue Kvstore::KvError::IoError => e + expect(e.code == 1004, "open_many IoError code (got #{e.code})") + end -# total_count(): a list of objects and an optional record holding an object as -# parameters (the encoder mints one reference per object token). -expect(Kvstore::Store.total_count([], nil).zero?, "total_count of nothing") -expect(Kvstore::Store.total_count(many, nil) == 3, "total_count over open_many (got #{Kvstore::Store.total_count(many, nil)})") -expect(Kvstore::Store.total_count(many, info) == 5, "total_count with the described store (got #{Kvstore::Store.total_count(many, info)})") -expect(Kvstore::Store.total_count([store, store, forked], nil) == 7, "the same object may appear repeatedly") -ruby_info = Kvstore::StoreInfo.new(label: "ruby-built", store: forked, mirror: store, count: 0) -expect(Kvstore::Store.total_count([empty], ruby_info) == 3, "Ruby-built StoreInfo encodes its object fields") -expect(Kvstore::Store.total_count([], mirrored) == 2, "StoreInfo with a mirror encodes both objects") -expect(store.count == 2 && forked.count == 3 && many.map(&:count) == [1, 0, 2], - "borrowed parameter objects survive total_count") - -# --- Async --------------------------------------------------------------- -# An immediately-expired entry gives compact 3 bytes to reclaim; the wrapper -# blocks on a Queue fed from the producer's worker-thread callback. -expect(store.put("doomed", payload, Kvstore::EntryKind::VOLATILE, 0) == true, "put doomed") -reclaimed = store.compact -expect(reclaimed == 3, "compact reclaimed 3 bytes (got #{reclaimed})") -expect(store.compact.zero?, "second compact reclaims nothing") -expect(store.count == 2, "live entries untouched by compact") - -# clear drops everything left in one call. -store.clear -expect(store.count.zero?, "count == 0 after clear") -expect(store.list_keys(nil).to_a == [], "no keys after clear") - -# --- Release ------------------------------------------------------------- -# Every wrapper closes exactly once; a second close is a no-op; the object -# itself lives until its last wrapper (info.store, mirrored.store, ...) goes. -store.close -store.close -expect(store.closed?, "store closed") -begin - store.count - raise "expected Error when using a closed Store" -rescue Kvstore::Error => e - expect(e.message.include?("after close"), "use-after-close message (got #{e.message.inspect})") + # total_count(): a list of objects and an optional record holding an object as + # parameters (the encoder mints one reference per object token). + expect(Kvstore::Store.total_count([], nil).zero?, "total_count of nothing") + expect(Kvstore::Store.total_count(many, nil) == 3, "total_count over open_many (got #{Kvstore::Store.total_count(many, nil)})") + expect(Kvstore::Store.total_count(many, info) == 5, "total_count with the described store (got #{Kvstore::Store.total_count(many, info)})") + expect(Kvstore::Store.total_count([store, store, forked], nil) == 7, "the same object may appear repeatedly") + ruby_info = Kvstore::StoreInfo.new(label: "ruby-built", store: forked, mirror: store, count: 0) + expect(Kvstore::Store.total_count([empty], ruby_info) == 3, "Ruby-built StoreInfo encodes its object fields") + expect(Kvstore::Store.total_count([], mirrored) == 2, "StoreInfo with a mirror encodes both objects") + expect(store.count == 2 && forked.count == 3 && many.map(&:count) == [1, 0, 2], + "borrowed parameter objects survive total_count") + + # --- Async --------------------------------------------------------------- + # An immediately-expired entry gives compact 3 bytes to reclaim; the wrapper + # blocks on a Queue fed from the producer's worker-thread callback. + expect(store.put("doomed", payload, Kvstore::EntryKind::VOLATILE, 0) == true, "put doomed") + reclaimed = store.compact + expect(reclaimed == 3, "compact reclaimed 3 bytes (got #{reclaimed})") + expect(store.compact.zero?, "second compact reclaims nothing") + expect(store.count == 2, "live entries untouched by compact") + + # compact is cancellable: a token cancelled before the call completes it + # with Kvstore::Cancelled (code -5); an untouched token changes nothing. + token = Kvstore::CancelToken.new + expect(!token.cancelled?, "fresh token is not cancelled") + expect(store.compact(cancel: token).zero?, "compact with an idle token") + token.cancel + token.cancel + expect(token.cancelled?, "token reports cancelled") + begin + store.compact(cancel: token) + raise "expected Kvstore::Cancelled" + rescue Kvstore::Cancelled => e + expect(e.code == Kvstore::CANCELLED_ERROR_CODE && e.code == -5, "Cancelled code -5 (got #{e.code})") + expect(e.is_a?(Kvstore::Error), "Cancelled is a Kvstore::Error") + end + token.close + token.close + expect(store.count == 2, "a cancelled compact leaves the store intact") + + # clear drops everything left in one call. + store.clear + expect(store.count.zero?, "count == 0 after clear") + expect(store.list_keys(nil).to_a == [], "no keys after clear") + + # --- Release ------------------------------------------------------------- + # Every wrapper closes exactly once; a second close is a no-op; the object + # itself lives until its last wrapper (info.store, mirrored.store, ...) goes. + store.close + store.close + expect(store.closed?, "store closed") + begin + store.count + raise "expected Error when using a closed Store" + rescue Kvstore::Error => e + expect(e.message.include?("after close"), "use-after-close message (got #{e.message.inspect})") + end + expect(info.store.count.zero?, "object still alive through the record's reference") + info.store.close + mirrored.store.close + mirrored.mirror.close + expect(forked.count == 3, "fork still alive after the mirror wrapper closed") + forked.close + empty.close + many.each(&:close) + many.each(&:close) + expect(many.all?(&:closed?), "every open_many store closed") end -expect(info.store.count.zero?, "object still alive through the record's reference") -info.store.close -mirrored.store.close -mirrored.mirror.close -expect(forked.count == 3, "fork still alive after the mirror wrapper closed") -forked.close -empty.close -many.each(&:close) -many.each(&:close) -expect(many.all?(&:closed?), "every open_many store closed") -GC.start -GC.start puts "ruby/kvstore: OK" diff --git a/conformance/ruby/run.sh b/conformance/ruby/run.sh new file mode 100755 index 00000000..4e226e40 --- /dev/null +++ b/conformance/ruby/run.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Conformance lanes for the ruby target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane builds and installs +# one generated gem and runs one consumer against it, which must exit 0. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# Build the sample's generated gem with `gem build`, install it into a +# private gem home, and run the consumer against the installed gem. The +# library is selected through the generated loader's {PREFIX}_LIBRARY +# override. +rb_consumer() { + local sample="$1" script="$2" + local gemdir="$GENROOT/$sample/ruby" home="$OUT/ruby-gems" + local spec gem + spec=$(cd "$gemdir" && ls ./*.gemspec) || return 1 + gem="$OUT/ruby-${sample}.gem" + (cd "$gemdir" && gem build --silent "$spec" -o "$gem") || return 1 + gem install --silent --local --ignore-dependencies --no-document \ + --install-dir "$home" "$gem" || return 1 + env GEM_PATH="$home:$(gem env gempath)" \ + "$(library_env "$sample")=$(sample_lib "$sample")" \ + ruby -w "$ROOT/conformance/ruby/$script" +} + +ruby_calculator() { rb_consumer calculator calculator.rb; } + +ruby_contacts() { rb_consumer contacts contacts.rb; } + +ruby_events() { rb_consumer events events.rb; } + +ruby_kvstore() { rb_consumer kvstore kvstore.rb; } + +ruby_async_demo() { rb_consumer async-demo async_demo.rb; } + +ruby_codec() { rb_consumer codec codec.rb; } + +lane ruby-calculator ruby_calculator +lane ruby-contacts ruby_contacts +lane ruby-events ruby_events +lane ruby-kvstore ruby_kvstore +lane ruby-async-demo ruby_async_demo +lane ruby-codec ruby_codec + +finish_lanes diff --git a/conformance/ruby/shapes.rb b/conformance/ruby/shapes.rb deleted file mode 100644 index 1a991c72..00000000 --- a/conformance/ruby/shapes.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true -# Conformance consumer: shapes sample, Ruby target. -# -# Drives the generated rich (algebraic) enum surface: Shape is a sum type of -# plain value classes (Shape::Empty, Shape::Circle, Shape::Rectangle, and -# Shape::Labeled), each with keyword construction, field readers, an integer -# `tag` reader backed by per-variant TAG constants, and structural equality. -# The free functions encode the shape into a value buffer on the way in and -# decode the returned buffer into a fresh variant instance. Also covers the -# expanded numerics (f32 fields, u8 field, bytes in, u64 out). The cdylib is -# selected via WEAVEFFI_LIBRARY. Non-zero exit on any failed assertion. - -$LOAD_PATH.unshift(File.join(ENV.fetch("WV_RB"), "lib")) -require "shapes" - -def expect(cond, msg) - raise "assertion failed: #{msg}" unless cond -end - -# Unit variant: no fields, tag 0. -empty = Shapes::Shape::Empty.new -expect(empty.is_a?(Shapes::Shape), "Empty is a Shape") -expect(empty.tag == Shapes::Shape::Empty::TAG, "empty tag is the TAG constant (got #{empty.tag})") -expect(empty.tag.zero?, "empty tag == 0 (got #{empty.tag})") -expect(empty == Shapes::Shape::Empty.new, "unit variants compare structurally") - -# f64 payload. -circle = Shapes::Shape::Circle.new(radius: 2.5) -expect(circle.tag == Shapes::Shape::Circle::TAG, "circle tag is the TAG constant (got #{circle.tag})") -expect(circle.tag == 1, "circle tag == 1 (got #{circle.tag})") -expect((circle.radius - 2.5).abs < 1e-9, "circle radius == 2.5 (got #{circle.radius})") - -# Two f32 payloads. -rect = Shapes::Shape::Rectangle.new(width: 3.0, height: 4.0) -expect(rect.tag == 2, "rectangle tag == 2 (got #{rect.tag})") -expect((rect.width - 3.0).abs < 1e-6, "rectangle width == 3.0 (got #{rect.width})") -expect((rect.height - 4.0).abs < 1e-6, "rectangle height == 4.0 (got #{rect.height})") - -# string + u8 payload. -labeled = Shapes::Shape::Labeled.new(label: "hex", count: 6) -expect(labeled.tag == 3, "labeled tag == 3 (got #{labeled.tag})") -expect(labeled.label == "hex", "labeled label == hex (got #{labeled.label.inspect})") -expect(labeled.count == 6, "labeled count == 6 (got #{labeled.count})") - -# Free functions: Shape in (encoded into a value buffer), string/Shape out. -expect(Shapes.describe(circle) == "circle(r=2.5)", "describe(circle) (got #{Shapes.describe(circle).inspect})") -expect(Shapes.describe(rect).include?("rect"), "describe(rect) mentions rect") -expect(Shapes.describe(labeled).include?("hex"), "describe(labeled) mentions the label") - -big = Shapes.scale(circle, 4.0) -expect(big.is_a?(Shapes::Shape::Circle), "scaled variant is Circle (got #{big.class})") -expect((big.radius - 10.0).abs < 1e-9, "scaled radius == 10.0 (got #{big.radius})") -expect(big == Shapes::Shape::Circle.new(radius: 10.0), "scaled shape compares structurally") - -# A C-style enum keeps its plain integer constants. -expect(Shapes::Channel::GREEN == 1, "Channel::GREEN == 1") - -# Numerics: `[u8]` canonicalizes to `bytes`, so the parameter is a binary -# string; the u64 sum comes back as a plain Integer. -raw = [250, 250, 250, 250].pack("C*") -expect(Shapes.sum_bytes(raw) == 1000, "sum_bytes == 1000 (got #{Shapes.sum_bytes(raw)})") - -puts "ruby/shapes: OK" diff --git a/conformance/ruby/support.rb b/conformance/ruby/support.rb new file mode 100644 index 00000000..96f448af --- /dev/null +++ b/conformance/ruby/support.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Shared helpers for the Ruby conformance consumers. + +def expect(cond, msg) + raise "assertion failed: #{msg}" unless cond +end + +# Runs the consumer body on its own thread, so none of its locals survive on +# a live stack once it returns, then forces collection (and the +# AutoPointer finalizers that release unclosed wrappers) and asserts the +# producer reports no live native resources: objects, callbacks, iterators, +# cancel tokens, or returned byte allocations. +def run_and_check_leaks(mod) + Thread.new { yield }.join + kinds = %w[objects callbacks iterators tokens allocations] + live = nil + 50.times do + GC.start(full_mark: true, immediate_sweep: true) + live = (0..4).map { |k| mod._wv_debug_live(k) } + break if live.all?(&:zero?) + + sleep 0.02 + end + report = kinds.zip(live).map { |k, n| "#{k}=#{n}" }.join(' ') + expect(live.all?(&:zero?), "no live native resources at exit (#{report})") +end diff --git a/conformance/run.sh b/conformance/run.sh index f761b7b5..8fd7a3cc 100755 --- a/conformance/run.sh +++ b/conformance/run.sh @@ -1,639 +1,64 @@ #!/usr/bin/env bash # WeaveFFI conformance harness. # -# Every consumer here binds through the *generated* WeaveFFI wrappers and -# asserts concrete results, then exits non-zero on any mismatch. The harness: +# Every consumer binds through the *generated* bindings and asserts concrete +# results, exiting non-zero on any mismatch. This driver: # -# 1. builds each producer cdylib from samples/, -# 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. +# 1. builds each sample producer cdylib (with leak counters enabled), +# 2. runs `weaveffi generate` on each sample's annotated `src/lib.rs` into +# the cargo target dir, and +# 3. runs conformance//run.sh for every language, each of which +# compiles and runs one consumer per sample under a per-lane timeout. # -# This is the regression oracle for the code generators: a backend that lowers a -# type wrong, drifts a symbol name, or skips a feature fails to compile or link -# here, which snapshot tests cannot catch. +# Selection (comma-separated lane names like `python-kvstore`, or languages +# like `python`): +# ONLY=c,python-kvstore run only these +# SKIP=wasm skip these +# SKIP_GEN=1 reuse previously generated bindings +# LANE_TIMEOUT=600 per-lane limit in seconds (default 300) # -# Selection: -# ONLY=c-contacts,cpp-contacts run only these targets -# SKIP=go-contacts skip these targets -# -# Prerequisites are the per-language toolchains (cc, clang++/cmake, python3, -# node, go, ruby, swift, dotnet, dart, kotlinc/java). Missing toolchains cause -# the affected target to FAIL; skip them explicitly with SKIP=. +# A missing toolchain fails the affected lanes; skip them explicitly with +# SKIP=. set -uo pipefail - -ROOT=$(cd "$(dirname "$0")/.." && pwd) -cd "$ROOT" - -TARGET_DIR=$(cargo metadata --format-version 1 --no-deps 2>/dev/null \ - | tr ',' '\n' | grep '"target_directory"' | head -1 \ - | sed 's/.*"target_directory":"//; s/"$//') -TARGET_DIR=${TARGET_DIR:-$ROOT/target} -LIBDIR="$TARGET_DIR/debug" -GENROOT="$TARGET_DIR/conformance-gen" -OUT="$TARGET_DIR/conformance-build" -mkdir -p "$OUT" - -case "$(uname -s)" in - Darwin) EXT=dylib; export DYLD_LIBRARY_PATH="$LIBDIR:${DYLD_LIBRARY_PATH:-}" ;; - MINGW*|MSYS*|CYGWIN*) EXT=dll ;; - *) EXT=so; export LD_LIBRARY_PATH="$LIBDIR:${LD_LIBRARY_PATH:-}" ;; -esac - -PASS=0 -FAIL=0 -FAILED_TARGETS="" - -selected() { - local t=$1 - if [ -n "${ONLY:-}" ]; then - [[ ",$ONLY," == *",$t,"* ]] - else - [[ ",${SKIP:-}," != *",$t,"* ]] - fi -} - -check() { - local name=$1 - shift - if ! selected "$name"; then - echo "[SKIP] $name" - return 0 - fi - echo "==> $name" - if "$@"; then - echo "[OK] $name" - PASS=$((PASS + 1)) - else - echo "[FAIL] $name" >&2 - FAIL=$((FAIL + 1)) - FAILED_TARGETS="$FAILED_TARGETS $name" - fi -} - -# Build a producer cdylib by crate name (idempotent; cargo caches). -build_producer() { - echo "--- building producer: $1" - cargo build -q -p "$1" -} - -# Generate all-language bindings for a sample into $GENROOT/. -# SKIP_GEN=1 reuses bindings from a previous run, letting concurrent harness -# 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:-samples/$1/src/lib.rs} - if [ -n "${SKIP_GEN:-}" ]; then - echo "--- reusing bindings: $sample" - return 0 - fi - echo "--- generating bindings: $sample" - # Clean first so files renamed across generator versions (e.g. an old - # identity-named lib left beside the current one) can't shadow fresh output; - # whole-package compilers like `dart run` otherwise pick up the stale copy. - rm -rf "$GENROOT/$sample" - cargo run -q -p weaveffi-cli -- generate "$idl" -o "$GENROOT/$sample" --force -} - -# --------------------------------------------------------------------------- -# Consumers. Each function compiles + runs one (language, sample) consumer and -# returns the program's exit status. -# --------------------------------------------------------------------------- - -c_contacts() { - clang -I "$GENROOT/contacts/c" "$ROOT/conformance/c/contacts.c" \ - -L "$LIBDIR" -lcontacts -o "$OUT/c_contacts" \ - && "$OUT/c_contacts" -} - -cpp_contacts() { - clang++ -std=c++17 -I "$GENROOT/contacts/cpp" "$ROOT/conformance/cpp/contacts.cpp" \ - -L "$LIBDIR" -lcontacts -o "$OUT/cpp_contacts" \ - && "$OUT/cpp_contacts" -} - -c_events() { - clang -I "$GENROOT/events/c" "$ROOT/conformance/c/events.c" \ - -L "$LIBDIR" -levents -o "$OUT/c_events" \ - && "$OUT/c_events" -} - -cpp_events() { - clang++ -std=c++17 -I "$GENROOT/events/cpp" "$ROOT/conformance/cpp/events.cpp" \ - -L "$LIBDIR" -levents -o "$OUT/cpp_events" \ - && "$OUT/cpp_events" -} - -c_kvstore() { - clang -I "$GENROOT/kvstore/c" "$ROOT/conformance/c/kvstore.c" \ - -L "$LIBDIR" -lkvstore -o "$OUT/c_kvstore" \ - && "$OUT/c_kvstore" -} - -cpp_kvstore() { - clang++ -std=c++17 -I "$GENROOT/kvstore/cpp" "$ROOT/conformance/cpp/kvstore.cpp" \ - -L "$LIBDIR" -lkvstore -o "$OUT/cpp_kvstore" \ - && "$OUT/cpp_kvstore" -} - -c_async_demo() { - clang -I "$GENROOT/async-demo/c" "$ROOT/conformance/c/async_demo.c" \ - -L "$LIBDIR" -lasync_demo -o "$OUT/c_async_demo" \ - && "$OUT/c_async_demo" -} - -cpp_async_demo() { - clang++ -std=c++17 -I "$GENROOT/async-demo/cpp" "$ROOT/conformance/cpp/async_demo.cpp" \ - -L "$LIBDIR" -lasync_demo -o "$OUT/cpp_async_demo" \ - && "$OUT/cpp_async_demo" -} - -c_shapes() { - clang -I "$GENROOT/shapes/c" "$ROOT/conformance/c/shapes.c" \ - -L "$LIBDIR" -lshapes -lm -o "$OUT/c_shapes" \ - && "$OUT/c_shapes" -} - -c_inventory() { - clang -I "$GENROOT/inventory/c" "$ROOT/conformance/c/inventory.c" \ - -L "$LIBDIR" -linventory -o "$OUT/c_inventory" \ - && "$OUT/c_inventory" -} - -c_codec() { - clang -I "$GENROOT/codec/c" "$ROOT/conformance/c/codec.c" \ - -L "$LIBDIR" -lcodec -lm -o "$OUT/c_codec" \ - && "$OUT/c_codec" -} - -cpp_codec() { - clang++ -std=c++17 -I "$GENROOT/codec/cpp" "$ROOT/conformance/cpp/codec.cpp" \ - -L "$LIBDIR" -lcodec -o "$OUT/cpp_codec" \ - && "$OUT/cpp_codec" -} - -cpp_shapes() { - clang++ -std=c++17 -I "$GENROOT/shapes/cpp" "$ROOT/conformance/cpp/shapes.cpp" \ - -L "$LIBDIR" -lshapes -o "$OUT/cpp_shapes" \ - && "$OUT/cpp_shapes" -} - -# Producer lane: unlike every other lane (which *consumes* a prebuilt cdylib), -# this compiles a C backend that *implements* the generated header into a shared -# library under hidden default visibility (-fvisibility=hidden, the release norm -# and the MSVC default). The generated header tags every prototype with the -# WEAVEFFI_API visibility macro, so the implemented symbols stay exported; before -# that macro existed they were hidden and unusable. Regression oracle for -# https://github.com/weavefoundry/weaveffi/issues/23. Asserts both a user -# function and a runtime helper are exported (defined + external) via `nm`. -c_producer_exports() { - local incdir="$GENROOT/calculator/c" - local lib="$OUT/libcalc_producer.$EXT" - clang -shared -fPIC -fvisibility=hidden \ - -I "$incdir" "$ROOT/conformance/c/producer.c" -o "$lib" \ - || { echo "producer compile failed" >&2; return 1; } - local syms - syms=$(nm -g --defined-only "$lib" 2>/dev/null) || syms=$(nm -gU "$lib" 2>/dev/null) - for sym in weaveffi_calculator_add weaveffi_free_bytes; do - if ! printf '%s\n' "$syms" | grep -Eq "(^| )_?${sym}\$"; then - echo "symbol '$sym' not exported under hidden visibility from $lib" >&2 - printf '%s\n' "$syms" | head -40 >&2 - return 1 - fi +. "$(dirname "$0")/lib.sh" + +SAMPLES="calculator contacts events kvstore async-demo codec" +LANGS="c cpp python ruby dart go swift dotnet node kotlin wasm" + +export ROOT TARGET_DIR LIBDIR GENROOT OUT RESULTS LANE_TIMEOUT EXT +export ONLY="${ONLY:-}" SKIP="${SKIP:-}" +rm -f "$RESULTS" + +echo "--- building producers" +# shellcheck disable=SC2086 +cargo build -q $(for s in $SAMPLES; do printf -- '-p %s ' "$s"; done) || exit 1 +cargo build -q -p weaveffi-cli || exit 1 +WEAVEFFI="$TARGET_DIR/debug/weaveffi" + +if [ -z "${SKIP_GEN:-}" ]; then + for s in $SAMPLES; do + echo "--- generating bindings: $s" + rm -rf "${GENROOT:?}/$s" + "$WEAVEFFI" generate "samples/$s/src/lib.rs" -o "$GENROOT/$s" --force || exit 1 done -} - -# Resolve the platform-specific cdylib path for a sample crate. Cargo -# normalizes hyphens in crate names to underscores in the artifact name -# (async-demo -> libasync_demo). -sample_lib() { - local n=${1//-/_} - case "$(uname)" in - Darwin) echo "$LIBDIR/lib$n.dylib" ;; - *) echo "$LIBDIR/lib$n.so" ;; - esac -} - -# Run a Python consumer with the generated package on PYTHONPATH and the -# producer cdylib selected via WEAVEFFI_LIBRARY (avoids the SIP-stripped -# DYLD_LIBRARY_PATH and the backend's default `libweaveffi` name). -py_consumer() { - local sample="$1" script="$2" - WV_PY="$GENROOT/$sample/python" \ - WEAVEFFI_LIBRARY="$(sample_lib "$sample")" \ - python3 "$ROOT/conformance/python/$script" -} - -python_contacts() { py_consumer contacts contacts.py; } -python_events() { py_consumer events events.py; } -python_kvstore() { py_consumer kvstore kvstore.py; } -python_shapes() { py_consumer shapes shapes.py; } -python_async_demo() { py_consumer async-demo async_demo.py; } -python_codec() { py_consumer codec codec.py; } -python_inventory() { py_consumer inventory inventory.py; } - -# Run a Ruby consumer; same library-selection story as Python. -rb_consumer() { - local sample="$1" script="$2" - WV_RB="$GENROOT/$sample/ruby" \ - WEAVEFFI_LIBRARY="$(sample_lib "$sample")" \ - ruby "$ROOT/conformance/ruby/$script" -} - -ruby_contacts() { rb_consumer contacts contacts.rb; } -ruby_events() { rb_consumer events events.rb; } -ruby_kvstore() { rb_consumer kvstore kvstore.rb; } -ruby_shapes() { rb_consumer shapes shapes.rb; } -ruby_async_demo() { rb_consumer async-demo async_demo.rb; } -ruby_codec() { rb_consumer codec codec.rb; } - -# Run a Dart consumer from inside the generated package so `package:weaveffi` -# and the cached `ffi` dependency resolve. The cdylib comes via WEAVEFFI_LIBRARY. -dart_consumer() { - local sample="$1" script="$2" - local pkgdir="$GENROOT/$sample/dart" - mkdir -p "$pkgdir/bin" - # The package name and library file follow the resolved identity; discover - # both from the generated tree and substitute the consumer's import. - local pkg lib - pkg=$(sed -n 's/^name: //p' "$pkgdir/pubspec.yaml" | head -1) - lib=$(basename "$(ls "$pkgdir/lib"/*.dart | head -1)" .dart) - sed -e "s#__PKG__#$pkg#g" -e "s#__LIB__#$lib#g" \ - "$ROOT/conformance/dart/$script" > "$pkgdir/bin/conformance.dart" - ( cd "$pkgdir" && dart pub get >/dev/null 2>&1 ) || { echo "dart pub get failed" >&2; return 1; } - ( cd "$pkgdir" && WEAVEFFI_LIBRARY="$(sample_lib "$sample")" dart run bin/conformance.dart ) -} - -dart_contacts() { dart_consumer contacts contacts.dart; } -dart_events() { dart_consumer events events.dart; } -dart_kvstore() { dart_consumer kvstore kvstore.dart; } -dart_shapes() { dart_consumer shapes shapes.dart; } -dart_async_demo() { dart_consumer async-demo async_demo.dart; } -dart_codec() { dart_consumer codec codec.dart; } - -# A directory containing `libweaveffi.` symlinked to the sample cdylib, so -# build-time `-lweaveffi` / `#cgo -lweaveffi` resolve. On Linux the consumer then -# records `libweaveffi.so` as its DT_NEEDED (cargo cdylibs carry no SONAME, so the -# link-time basename is used verbatim) rather than the real lib's name in $LIBDIR, -# so callers must add this dir to LD_LIBRARY_PATH/DYLD_LIBRARY_PATH at run time for -# the loader to resolve the alias back to the real cdylib. -weaveffi_linkdir() { - local sample="$1" dir="$OUT/linkalias-$1" - mkdir -p "$dir" - ln -sf "$(sample_lib "$sample")" "$dir/libweaveffi.$EXT" - echo "$dir" -} - -# Run a Go consumer in a throwaway module that `replace`s the generated package. -# The generated module path follows the package identity (e.g. `contacts` or -# `github.com/example/kvstore`), so discover it from the generated go.mod and -# substitute it into the consumer's `__MODPATH__` import sentinel. cgo now emits -# `-l` matching the producer cdylib, so link straight against $LIBDIR. -go_consumer() { - local sample="$1" src="$2" - local moddir modpath - modpath=$(sed -n 's/^module //p' "$GENROOT/$sample/go/go.mod" | head -1) - moddir="$OUT/go-$sample" - rm -rf "$moddir" - mkdir -p "$moddir" - sed "s#__MODPATH__#$modpath#g" "$ROOT/conformance/go/$src" > "$moddir/main.go" - cat > "$moddir/go.mod" < $GENROOT/$sample/go -EOF - ( cd "$moddir" \ - && GOPROXY=off GOSUMDB=off GOFLAGS=-mod=mod \ - CGO_CFLAGS="-I$GENROOT/$sample/c" CGO_LDFLAGS="-L$LIBDIR" \ - LD_LIBRARY_PATH="$LIBDIR:${LD_LIBRARY_PATH:-}" \ - DYLD_LIBRARY_PATH="$LIBDIR:${DYLD_LIBRARY_PATH:-}" \ - go run . ) -} - -go_contacts() { go_consumer contacts contacts.go; } -go_events() { go_consumer events events.go; } -go_kvstore() { go_consumer kvstore kvstore.go; } -go_shapes() { go_consumer shapes shapes.go; } -go_async_demo() { go_consumer async-demo async_demo.go; } -go_codec() { go_consumer codec codec.go; } - -# Swift: assemble a throwaway SwiftPM package that vendors the generated -# WeaveFFI module plus a C shim whose module map points at the generated header -# by absolute path and links the real producer cdylib (`lib`). The -# consumer is an executable target. Runtime load is automatic: the cdylib's -# install name is an absolute path. (The generated Package.swift puts its module -# map outside Sources/, so it cannot `swift build` as-is; we vendor instead.) -swift_consumer() { - local sample="$1" src="$2" - local pkg="$OUT/swift-$sample" - rm -rf "$pkg" - # The generated Swift module name is derived from the IDL package name - # (e.g. contacts -> Contacts), with a parallel C shim module named C. - # Discover it from the generated tree rather than hard-coding "WeaveFFI". - local gen_swift mod - gen_swift=$(ls "$GENROOT/$sample/swift/Sources"/*/*.swift | head -1) - mod=$(basename "$gen_swift" .swift) - mkdir -p "$pkg/Sources/C$mod" "$pkg/Sources/$mod" "$pkg/Sources/conformance" - cp "$gen_swift" "$pkg/Sources/$mod/" - cat > "$pkg/Sources/C$mod/module.modulemap" < "$pkg/Package.swift" <&1 ) -} - -swift_contacts() { swift_consumer contacts contacts.swift; } -swift_events() { swift_consumer events events.swift; } -swift_kvstore() { swift_consumer kvstore kvstore.swift; } -swift_shapes() { swift_consumer shapes shapes.swift; } -swift_async_demo() { swift_consumer async-demo async_demo.swift; } -swift_codec() { swift_consumer codec codec.swift; } - -# .NET: compile the generated P/Invoke source together with the consumer into -# one console app. The generated file is named after the resolved identity -# (WeaveFFI.cs, Kvstore.cs, ...), so discover it from the generated tree. The -# producer cdylib is resolved at runtime by a DllImportResolver in the consumer -# reading WEAVEFFI_LIBRARY (avoids the SIP-stripped DYLD path and the backend's -# default `weaveffi` name). Targets the installed SDK's framework so no -# reference packs need downloading. -dotnet_consumer() { - local sample="$1" src="$2" - local proj="$OUT/dotnet-$sample" - local tfm gen_cs - tfm="net$(dotnet --version | cut -d. -f1).0" - gen_cs=$(ls "$GENROOT/$sample/dotnet"/*.cs | head -1) - rm -rf "$proj" - mkdir -p "$proj" - cp "$gen_cs" "$proj/Generated.cs" - cp "$ROOT/conformance/dotnet/$src" "$proj/Program.cs" - cat > "$proj/conformance.csproj" < - - Exe - $tfm - disable - true - disable - - -EOF - ( cd "$proj" \ - && WEAVEFFI_LIBRARY="$(sample_lib "$sample")" \ - dotnet run -c Release --nologo -v quiet 2>&1 ) -} - -dotnet_contacts() { dotnet_consumer contacts Contacts.cs; } -dotnet_events() { dotnet_consumer events Events.cs; } -dotnet_kvstore() { dotnet_consumer kvstore Kvstore.cs; } -dotnet_shapes() { dotnet_consumer shapes Shapes.cs; } -dotnet_async_demo() { dotnet_consumer async-demo AsyncDemo.cs; } -dotnet_codec() { dotnet_consumer codec Codec.cs; } - -# Node: build the generated N-API addon with node-gyp against the producer -# cdylib (via the `libweaveffi` link alias + include of the generated C header), -# then run the consumer with the built addon path in WV_ADDON. The addon's -# dependent cdylib resolves at runtime by its absolute install name. node-gyp -# fetches node headers on first run (cached thereafter). -node_consumer() { - local sample="$1" src="$2" - local b="$OUT/node-$sample" linkdir - linkdir="$(weaveffi_linkdir "$sample")" - rm -rf "$b" - mkdir -p "$b" - cp "$GENROOT/$sample/node/weaveffi_addon.c" "$b/" - cat > "$b/binding.gyp" </dev/null 2>&1 ) \ - || { echo "node-gyp build failed" >&2; return 1; } - WV_ADDON="$b/build/Release/index.node" \ - LD_LIBRARY_PATH="$linkdir:${LD_LIBRARY_PATH:-}" \ - DYLD_LIBRARY_PATH="$linkdir:${DYLD_LIBRARY_PATH:-}" \ - node "$ROOT/conformance/node/$src" -} - -node_contacts() { node_consumer contacts contacts.js; } -node_events() { node_consumer events events.js; } -node_kvstore() { node_consumer kvstore kvstore.js; } -node_shapes() { node_consumer shapes shapes.js; } -node_async_demo() { node_consumer async-demo async_demo.js; } -node_codec() { node_consumer codec codec.js; } +fi -# Kotlin (JVM): compile the generated JNI bridge into `libweaveffi.` (what -# `System.loadLibrary("weaveffi")` expects), linked against the producer cdylib; -# then compile the generated WeaveFFI.kt together with the consumer in one module -# (so the `internal` Entry/Stats constructors are reachable) against the -# coroutines jar bundled with kotlinc; then run on the JVM with the bridge on -# java.library.path. The producer cdylib resolves via its absolute install name -# (macOS) or LD_LIBRARY_PATH (Linux). The consumer's `main` lives in class `Main` -# (`@file:JvmName("Main")`). -kotlin_consumer() { - local sample="$1" src="$2" - local b="$OUT/kotlin-$sample" - rm -rf "$b"; mkdir -p "$b" - local jh jni_os_inc - jh="${JAVA_HOME:-$(/usr/libexec/java_home 2>/dev/null)}" - # Last resort: derive JAVA_HOME from the resolved `java` binary (Linux). - if [ -z "$jh" ] || [ ! -f "$jh/include/jni.h" ]; then - local javabin - javabin=$(readlink -f "$(command -v java)" 2>/dev/null) - [ -n "$javabin" ] && jh=$(dirname "$(dirname "$javabin")") +for lang in $LANGS; do + if [ -n "$ONLY" ] && [[ ",$ONLY," != *",$lang"[,-]* ]]; then + continue fi - [ -n "$jh" ] && [ -f "$jh/include/jni.h" ] || { echo "jni.h not found (set JAVA_HOME)" >&2; return 1; } - case "$(uname -s)" in - Darwin) jni_os_inc="$jh/include/darwin" ;; - *) jni_os_inc="$jh/include/linux" ;; - esac - # Locate the coroutines runtime shipped inside the kotlinc distribution - # (homebrew libexec/lib, plain dist lib/, snap, or sdkman layouts). - local kc real coro - kc=$(command -v kotlinc) || { echo "kotlinc not found" >&2; return 1; } - real=$(readlink -f "$kc" 2>/dev/null || echo "$kc") - coro=$(ls "$(dirname "$real")/../libexec/lib/kotlinx-coroutines-core-jvm.jar" \ - "$(dirname "$real")/../lib/kotlinx-coroutines-core-jvm.jar" 2>/dev/null | head -1) - [ -n "$coro" ] || coro=$(ls /usr/local/Cellar/kotlin/*/libexec/lib/kotlinx-coroutines-core-jvm.jar \ - /opt/homebrew/Cellar/kotlin/*/libexec/lib/kotlinx-coroutines-core-jvm.jar \ - /snap/kotlin/current/lib/kotlinx-coroutines-core-jvm.jar \ - "$HOME/.sdkman/candidates/kotlin/current/lib/kotlinx-coroutines-core-jvm.jar" 2>/dev/null | head -1) - [ -n "$coro" ] || { echo "kotlinx-coroutines-core-jvm.jar not found" >&2; return 1; } - cc -shared -fPIC "$GENROOT/$sample/kotlin/src/main/cpp/weaveffi_jni.c" \ - -I"$jh/include" -I"$jni_os_inc" -I"$GENROOT/$sample/c" \ - -L"$LIBDIR" -l"${sample//-/_}" -Wl,-rpath,"$LIBDIR" \ - -o "$b/libweaveffi.$EXT" || { echo "JNI bridge compile failed" >&2; return 1; } - kotlinc "$GENROOT/$sample/kotlin/src/main/kotlin/com/weaveffi/WeaveFFI.kt" \ - "$ROOT/conformance/kotlin/$src" -cp "$coro" -include-runtime -d "$b/app.jar" \ - >/dev/null 2>&1 || { echo "kotlinc failed" >&2; return 1; } - DYLD_LIBRARY_PATH="$LIBDIR:${DYLD_LIBRARY_PATH:-}" \ - LD_LIBRARY_PATH="$LIBDIR:${LD_LIBRARY_PATH:-}" \ - java -Djava.library.path="$b" -cp "$b/app.jar:$coro" Main -} - -kotlin_contacts() { kotlin_consumer contacts contacts.kt; } -kotlin_events() { kotlin_consumer events events.kt; } -kotlin_kvstore() { kotlin_consumer kvstore kvstore.kt; } -kotlin_shapes() { kotlin_consumer shapes shapes.kt; } -kotlin_async_demo() { kotlin_consumer async-demo async_demo.kt; } -kotlin_codec() { kotlin_consumer codec codec.kt; } - -# Wasm: compile the producer to wasm32-unknown-unknown and drive the generated -# ESM bindings from Node. The generated JS glue expects weaveffi_alloc/dealloc -# plus an exported, growable __indirect_function_table, wired by the workspace -# .cargo/config.toml (--export-table/--growable-table). Async and listener -# wrappers build host trampolines with WebAssembly.Function, which needs the -# type-reflection flag. -# Unlike the cdylib lanes there is no FFI library to load: the .wasm is fully -# self-contained and the consumer reads it via WV_WASM, with WV_JS pointing at -# the generated module. -wasm_consumer() { - local sample="$1" src="$2" - command -v node >/dev/null 2>&1 || { echo "node not found" >&2; return 1; } - rustup target list --installed 2>/dev/null | grep -qx 'wasm32-unknown-unknown' \ - || { echo "wasm32-unknown-unknown target missing (rustup target add wasm32-unknown-unknown)" >&2; return 1; } - echo "--- building wasm producer: $sample" - cargo build -q -p "$sample" --release --target wasm32-unknown-unknown \ - || { echo "wasm32 build failed" >&2; return 1; } - local wasm="$TARGET_DIR/wasm32-unknown-unknown/release/${sample//-/_}.wasm" - [ -f "$wasm" ] || { echo "wasm artifact not found: $wasm" >&2; return 1; } - WV_WASM="$wasm" WV_JS="$GENROOT/$sample/wasm/weaveffi_wasm.js" \ - node --experimental-wasm-type-reflection "$ROOT/conformance/wasm/$src" -} - -wasm_contacts() { wasm_consumer contacts contacts.mjs; } -wasm_events() { wasm_consumer events events.mjs; } -wasm_kvstore() { wasm_consumer kvstore kvstore.mjs; } -wasm_shapes() { wasm_consumer shapes shapes.mjs; } -wasm_async_demo() { wasm_consumer async-demo async_demo.mjs; } -wasm_codec() { wasm_consumer codec codec.mjs; } - -# --------------------------------------------------------------------------- -# Producers + generation -# --------------------------------------------------------------------------- -build_producer contacts -generate contacts -build_producer events -generate events -build_producer kvstore -generate kvstore -build_producer shapes -generate shapes -build_producer async-demo -generate async-demo -build_producer codec -generate codec -build_producer inventory -generate inventory -# Header-only: the producer-export lane implements this in C, no Rust cdylib. -generate calculator - -# --------------------------------------------------------------------------- -# Run matrix: every language runs the events lane (a callback interface, a -# reference-counted object passed back through the callback, iterators), the -# kvstore lane (objects in records/lists/optionals, builders, async, an -# eviction callback interface), the async-demo lane (module-level async -# functions, typed async errors, buffered async list-of-record returns), the -# codec lane (every value-buffer wire shape round-tripped through the producer, -# including object tokens inside buffers), and shapes (rich enums); contacts -# covers the original struct/enum/optional surface. The inventory lanes (C and -# Python) cover the only multi-module sample: nested modules with cross-module -# type references and per-module error domains. -# --------------------------------------------------------------------------- -check c-contacts c_contacts -check c-events c_events -check c-kvstore c_kvstore -check cpp-contacts cpp_contacts -check cpp-events cpp_events -check cpp-kvstore cpp_kvstore -check c-shapes c_shapes -check cpp-shapes cpp_shapes -check c-producer-exports c_producer_exports -check python-shapes python_shapes -check ruby-shapes ruby_shapes -check go-shapes go_shapes -check dotnet-shapes dotnet_shapes -check dart-shapes dart_shapes -check swift-shapes swift_shapes -check node-shapes node_shapes -check kotlin-shapes kotlin_shapes -check wasm-shapes wasm_shapes -check python-contacts python_contacts -check python-events python_events -check python-kvstore python_kvstore -check ruby-contacts ruby_contacts -check ruby-events ruby_events -check ruby-kvstore ruby_kvstore -check dart-contacts dart_contacts -check dart-events dart_events -check dart-kvstore dart_kvstore -check go-contacts go_contacts -check go-events go_events -check go-kvstore go_kvstore -check swift-contacts swift_contacts -check swift-events swift_events -check swift-kvstore swift_kvstore -check dotnet-contacts dotnet_contacts -check dotnet-events dotnet_events -check dotnet-kvstore dotnet_kvstore -check node-contacts node_contacts -check node-events node_events -check node-kvstore node_kvstore -check kotlin-contacts kotlin_contacts -check kotlin-events kotlin_events -check kotlin-kvstore kotlin_kvstore -check wasm-contacts wasm_contacts -check wasm-events wasm_events -check wasm-kvstore wasm_kvstore -check c-async-demo c_async_demo -check cpp-async-demo cpp_async_demo -check python-async-demo python_async_demo -check ruby-async-demo ruby_async_demo -check go-async-demo go_async_demo -check dart-async-demo dart_async_demo -check swift-async-demo swift_async_demo -check dotnet-async-demo dotnet_async_demo -check node-async-demo node_async_demo -check kotlin-async-demo kotlin_async_demo -check wasm-async-demo wasm_async_demo -check c-codec c_codec -check cpp-codec cpp_codec -check python-codec python_codec -check ruby-codec ruby_codec -check go-codec go_codec -check dart-codec dart_codec -check swift-codec swift_codec -check dotnet-codec dotnet_codec -check node-codec node_codec -check kotlin-codec kotlin_codec -check wasm-codec wasm_codec -check c-inventory c_inventory -check python-inventory python_inventory + if [[ ",$SKIP," == *",$lang,"* ]]; then + echo "[SKIP] $lang" + continue + fi + bash "$ROOT/conformance/$lang/run.sh" +done +PASS=$(grep -c '^OK ' "$RESULTS" 2>/dev/null || true) +FAIL=$(grep -c '^FAIL ' "$RESULTS" 2>/dev/null || true) echo -echo "conformance: $PASS passed, $FAIL failed" -if [ "$FAIL" -ne 0 ]; then - echo "failed:$FAILED_TARGETS" >&2 +echo "conformance: ${PASS:-0} passed, ${FAIL:-0} failed" +if [ "${FAIL:-0}" -ne 0 ]; then + echo "failed: $(grep '^FAIL ' "$RESULTS" | cut -d' ' -f2 | tr '\n' ' ')" >&2 exit 1 fi diff --git a/conformance/swift/async_demo.swift b/conformance/swift/async_demo.swift index b72139b4..50dc3519 100644 --- a/conformance/swift/async_demo.swift +++ b/conformance/swift/async_demo.swift @@ -1,16 +1,19 @@ // Conformance consumer: async-demo sample, Swift target. // -// Drives the CheckedContinuation-backed async surface end to end: -// `Tasks.runTask` as a throwing async static resumed from the producer's -// worker thread and decoded from a value buffer into the plain `TaskResult` -// struct, the typed `TaskError.invalidName` case (with its stable -// `errorCode`) thrown for an empty name, the buffered list-of-records round -// trip through the non-throwing async `runBatch`, the direct-scalar -// `runNTasks`, the sync `cancelTask`, and `activeCallbacks` settling to zero -// once every task body has completed. +// Drives the checked-continuation async surface end to end: `Tasks.runTask` +// as a throwing async static resumed from the producer's worker thread and +// decoded from a value buffer into the `TaskResult` struct, the typed +// `TaskError.invalidName` case thrown for an empty name, the buffered +// list-of-records round trip through the non-throwing async `runBatch`, and +// the direct-scalar `runNTasks`. The cancellable `wait` completes after its +// timeout when left alone; cancelling the awaiting task cancels the native +// token, so the call ends at once with `CancellationError`, including when +// the task was cancelled before the call started. `activeCallbacks` settles +// to zero, and at exit every cancel token and allocation has been released. -import Foundation +import CAsyncDemo import AsyncDemo +import Foundation func fail(_ msg: String) -> Never { FileHandle.standardError.write(Data("assertion failed: \(msg)\n".utf8)) @@ -21,46 +24,103 @@ func expect(_ cond: Bool, _ msg: String) { if !cond { fail(msg) } } -// Async record return: resumed with a TaskResult decoded from the -// completion callback's value buffer. -do { - let result = try await Tasks.runTask(name: "alpha") - expect(result.id > 0, "runTask assigns an id") - expect(result.value == "completed: alpha", "runTask value (got \(result.value))") - expect(result.success, "runTask success flag") -} catch { - fail("runTask threw unexpectedly: \(error)") +/// Every live-object counter the producer keeps must settle at zero. +func assertNoLeaks() { + let kinds = ["objects", "callbacks", "iterators", "cancel tokens", "allocations"] + var live: [UInt64] = [] + for _ in 0..<200 { + live = (0..<5).map { async_demo_debug_live(Int32($0)) } + if live.allSatisfy({ $0 == 0 }) { return } + usleep(10_000) + } + for (kind, n) in live.enumerated() where n != 0 { + fail("\(n) live \(kinds[kind]) at exit") + } } -// Typed async error: the empty name resumes by throwing the typed case. -do { - _ = try await Tasks.runTask(name: "") - fail("expected TaskError.invalidName for empty name") -} catch let e as TaskError { - guard case .invalidName = e else { - fail("expected .invalidName (got \(e))") +/// Await `task`, which must end with `CancellationError` within five seconds. +func expectCancelled(_ task: Task, _ what: String) async { + let start = Date() + do { + let value = try await task.value + fail("\(what): expected CancellationError, got \(value)") + } catch is CancellationError { + } catch { + fail("\(what): expected CancellationError, got \(error)") } - expect(e.errorCode == 1, "invalidName carries code 1 (got \(e.errorCode))") -} catch { - fail("expected TaskError (got \(error))") + expect(Date().timeIntervalSince(start) < 5, "\(what): didn't wait for the timeout") } -// Buffered list-of-records both ways through the non-throwing async. -let batch = await Tasks.runBatch(names: ["a", "b", "c"]) -expect( - batch.map { $0.value } == ["completed: a", "completed: b", "completed: c"], - "runBatch values" -) -expect(batch.allSatisfy { $0.success }, "runBatch success flags") +func run() async { + // Async record return: resumed with a TaskResult decoded from the + // completion callback's value buffer. + do { + let result = try await Tasks.runTask(name: "alpha") + expect(result.id > 0, "runTask assigns an id") + expect(result.value == "completed: alpha", "runTask value (got \(result.value))") + expect(result.success, "runTask success flag") + } catch { + fail("runTask threw unexpectedly: \(error)") + } + + // Typed async error: the empty name resumes by throwing the typed case. + do { + _ = try await Tasks.runTask(name: "") + fail("expected TaskError.invalidName for empty name") + } catch let e as TaskError { + guard case let .invalidName(message) = e else { fail("expected .invalidName (got \(e))") } + expect(e.errorCode == 1, "invalidName carries code 1 (got \(e.errorCode))") + expect(message == "task name must not be empty", "invalidName message (got \(message))") + } catch { + fail("expected TaskError (got \(error))") + } -// Direct scalar through the async callback. -let n = await Tasks.runNTasks(n: 7) -expect(n == 7, "runNTasks echoes n") + // Buffered list-of-records both ways through the non-throwing async. + let batch = await Tasks.runBatch(names: ["a", "b", "c"]) + expect(batch.map { $0.value } == ["completed: a", "completed: b", "completed: c"], "runBatch values") + expect(batch.allSatisfy { $0.success }, "runBatch success flags") + expect(await Tasks.runBatch(names: []).isEmpty, "runBatch of nothing") -// Sync functions beside the async ones. -expect(!Tasks.cancelTask(id: 1), "cancelTask reports not cancelled") + // Direct scalar through the async callback, many in flight at once. + let sums = await withTaskGroup(of: Int32.self) { group in + for i in 0..<32 { + group.addTask { await Tasks.runNTasks(n: Int32(i)) } + } + var total: Int32 = 0 + for await n in group { total += n } + return total + } + expect(sums == (0..<32).reduce(0, +), "runNTasks echoes n (sum \(sums))") -// Every spawned task body has completed by the time its callback fires. -expect(Tasks.activeCallbacks() == 0, "activeCallbacks settles to zero") + // Cancellable: left alone, `wait` completes after its timeout. + do { + let waited = try await Tasks.wait(timeoutMs: 20) + expect(waited == 20, "wait completes with its timeout (got \(waited))") + } catch { + fail("uncancelled wait threw: \(error)") + } + + // Cancelling the awaiting task cancels the native token mid-call. + let running = Task { try await Tasks.wait(timeoutMs: 60_000) } + try? await Task.sleep(nanoseconds: 50_000_000) + running.cancel() + await expectCancelled(running, "cancel mid-call") + + // A task cancelled before the call starts never runs the body. + let early = Task { () async throws -> Int64 in + while !Task.isCancelled { await Task.yield() } + return try await Tasks.wait(timeoutMs: 60_000) + } + early.cancel() + await expectCancelled(early, "cancel before the call") + + // Every spawned task body finishes, cancelled ones included. + for _ in 0..<100 where Tasks.activeCallbacks() != 0 { + try? await Task.sleep(nanoseconds: 10_000_000) + } + expect(Tasks.activeCallbacks() == 0, "activeCallbacks settles to zero") +} -print("swift async-demo conformance: OK") +await run() +assertNoLeaks() +print("swift/async-demo: OK") diff --git a/conformance/swift/codec.swift b/conformance/swift/codec.swift index f9b8924f..8949186e 100644 --- a/conformance/swift/codec.swift +++ b/conformance/swift/codec.swift @@ -11,11 +11,14 @@ // and maps; unicode; Int64/UInt64 extremes; NaN, infinities, and negative // zero). `Shape` covers every rich-enum variant, and `Holder` covers object // tokens inside buffers: a field, an optional, and a list, each carrying one -// strong reference that the wrapper's deinit releases. Exits non-zero on any +// strong reference that the wrapper's deinit releases. Strings with interior +// NULs survive, since strings cross as pointer and length. At exit, every +// native object and allocation has been released. Exits non-zero on any // mismatch. -import Foundation +import CCodec import Codec +import Foundation func fail(_ msg: String) -> Never { FileHandle.standardError.write(Data("assertion failed: \(msg)\n".utf8)) @@ -26,7 +29,21 @@ func expect(_ cond: Bool, _ msg: String) { if !cond { fail(msg) } } -// --- Equality helpers (the generated structs and rich enum are not Equatable). +/// Every live-object counter the producer keeps must settle at zero. +func assertNoLeaks() { + let kinds = ["objects", "callbacks", "iterators", "cancel tokens", "allocations"] + var live: [UInt64] = [] + for _ in 0..<200 { + live = (0..<5).map { codec_debug_live(Int32($0)) } + if live.allSatisfy({ $0 == 0 }) { return } + usleep(10_000) + } + for (kind, n) in live.enumerated() where n != 0 { + fail("\(n) live \(kinds[kind]) at exit") + } +} + +// --- Equality helpers: NaN and negative zero need bit-pattern comparisons. func scalarsEqual(_ a: Scalars, _ b: Scalars) -> Bool { a.i8Value == b.i8Value && a.u8Value == b.u8Value @@ -82,274 +99,284 @@ func compositeEqual(_ a: Composite, _ b: Composite) -> Bool { return true } -// --- Scalars ------------------------------------------------------------------ - -let canonical = Scalars( - i8Value: -8, u8Value: 200, i16Value: -16_000, u16Value: 60_000, - i32Value: -2_000_000_000, u32Value: 4_000_000_000, - i64Value: -9_007_199_254_740_993, u64Value: UInt64.max, - f32Value: 1.5, f64Value: -2.25e100, flag: true, color: .blue) - -let scalars = Codec.sampleScalars() -expect(scalars.i8Value == -8, "i8 (got \(scalars.i8Value))") -expect(scalars.u8Value == 200, "u8 (got \(scalars.u8Value))") -expect(scalars.i16Value == -16_000, "i16 (got \(scalars.i16Value))") -expect(scalars.u16Value == 60_000, "u16 (got \(scalars.u16Value))") -expect(scalars.i32Value == -2_000_000_000, "i32 (got \(scalars.i32Value))") -expect(scalars.u32Value == 4_000_000_000, "u32 (got \(scalars.u32Value))") -expect(scalars.i64Value == -9_007_199_254_740_993, "i64 (got \(scalars.i64Value))") -expect(scalars.u64Value == UInt64.max, "u64 (got \(scalars.u64Value))") -expect(scalars.f32Value == 1.5, "f32 (got \(scalars.f32Value))") -expect(scalars.f64Value == -2.25e100, "f64 (got \(scalars.f64Value))") -expect(scalars.flag == true, "flag") -expect(scalars.color == .blue, "color (got \(scalars.color))") -expect(scalarsEqual(scalars, canonical), "sampleScalars matches the canonical fixture") - -do { - expect(try Codec.verifyScalars(value: scalars) == true, "producer accepts the re-encoded sample") - expect(try Codec.verifyScalars(value: canonical) == true, "producer accepts a locally built canonical") -} catch { - fail("verifyScalars threw: \(error)") -} -expect(scalarsEqual(Codec.roundtripScalars(value: scalars), scalars), "roundtripScalars is the identity") - -// A single changed field is a typed CodecError.mismatch (code 1). -var tweaked = canonical -tweaked.u64Value -= 1 -do { - _ = try Codec.verifyScalars(value: tweaked) - fail("expected CodecError.mismatch for a tweaked Scalars") -} catch let e as CodecError { - guard case let .mismatch(message) = e else { fail("expected .mismatch, got \(e)") } - expect(e.errorCode == 1, "mismatch code == 1 (got \(e.errorCode))") - expect(message == "value does not match the canonical fixture", "mismatch message (got \(message))") -} catch { - fail("expected CodecError, got \(error)") -} +func run() { + // --- Scalars ------------------------------------------------------------------ -// Scalars built from scratch at the extremes, with non-finite floats and a -// negative zero, survive a round trip bit for bit. -let extremes = Scalars( - i8Value: Int8.min, u8Value: UInt8.max, i16Value: Int16.max, u16Value: UInt16.max, - i32Value: Int32.min, u32Value: UInt32.max, i64Value: Int64.min, u64Value: 1 << 63, - f32Value: -0.0, f64Value: Double.nan, flag: false, color: .red) -let extremesBack = Codec.roundtripScalars(value: extremes) -expect(extremesBack.i64Value == Int64.min && extremesBack.u64Value == 1 << 63, "64-bit extremes") -expect(extremesBack.f32Value == 0 && extremesBack.f32Value.sign == .minus, "negative zero f32 keeps its sign") -expect(extremesBack.f64Value.isNaN, "NaN f64 round-trips") -expect(scalarsEqual(extremesBack, extremes), "extreme Scalars round-trip bit for bit") -let infinities = Scalars( - i8Value: Int8.max, u8Value: 0, i16Value: Int16.min, u16Value: 0, i32Value: Int32.max, u32Value: 0, - i64Value: Int64.max, u64Value: 0, f32Value: Float.infinity, f64Value: -Double.infinity, - flag: true, color: .green) -expect(scalarsEqual(Codec.roundtripScalars(value: infinities), infinities), "infinities round-trip") - -// --- Composite ---------------------------------------------------------------- - -let composite = Codec.sampleComposite() -expect(composite.name == "héllo wörld ✓", "name (got \(composite.name))") -expect(composite.blob == Data([0, 1, 2, 253, 254, 255]), "blob (got \(Array(composite.blob)))") -expect(composite.someI64 == Int64.min, "someI64 (got \(String(describing: composite.someI64)))") -expect(composite.noneI64 == nil, "noneI64 nil") -expect(composite.someText == "", "someText is a present empty string (got \(String(describing: composite.someText)))") -expect(composite.names == ["a", "", "ccc"], "names (got \(composite.names))") -expect(composite.matrix == [[1, 2, 3], [], [-4]], "matrix (got \(composite.matrix))") -expect(composite.empty.isEmpty, "empty list") -expect(composite.byName == ["one": 1, "two": 2, "neg": -3], "byName (got \(composite.byName))") -expect(composite.byId.count == 2, "byId has two entries (got \(composite.byId.count))") -expect(composite.byId[-1].map { scalarsEqual($0, canonical) } == true, "byId[-1] is the canonical Scalars") -expect(composite.byId[42]?.flag == false, "byId[42] has flag false") -var flagless = canonical -flagless.flag = false -expect(composite.byId[42].map { scalarsEqual($0, flagless) } == true, "byId[42] otherwise canonical") -expect(scalarsEqual(composite.scalars, canonical), "nested scalars") -expect(shapeEqual(composite.shape, .labeled(label: "tag", count: 3)), "shape (got \(composite.shape))") -expect(composite.shapes.count == 5, "five shapes (got \(composite.shapes.count))") -expect(shapesEqual(composite.shapes, [ - .empty, - .circle(radius: 2.5), - .rect(width: 1.0, height: 0.5), - .labeled(label: "", count: -1), - .nested(inner: canonical, note: "n"), -]), "shapes one of each variant (got \(composite.shapes))") -expect(optionalShapeEqual(composite.maybeShape, .nested(inner: canonical, note: nil)), - "maybeShape is a nested variant with an absent note (got \(String(describing: composite.maybeShape)))") -expect(composite.maybeList == Data([9, 8]), "maybeList (got \(String(describing: composite.maybeList)))") -expect(composite.sparse == [true, nil, false], "sparse (got \(composite.sparse))") -expect(composite.colors == [.red, .green, .blue], "colors (got \(composite.colors))") - -do { - expect(try Codec.verifyComposite(value: composite) == true, "producer accepts the re-encoded composite") -} catch { - fail("verifyComposite threw: \(error); producer saw: \(Codec.describeComposite(value: composite))") -} -expect(compositeEqual(Codec.roundtripComposite(value: composite), composite), "roundtripComposite is the identity") -let described = Codec.describeComposite(value: composite) -expect(described.hasPrefix("Composite {") && described.contains("héllo wörld ✓"), "describeComposite (got \(described))") - -// A changed nested value is rejected. -var changed = composite -changed.sparse[1] = true -do { - _ = try Codec.verifyComposite(value: changed) - fail("expected CodecError.mismatch for a changed composite") -} catch let e as CodecError { - expect(e.errorCode == 1, "composite mismatch code == 1") -} catch { - fail("expected CodecError, got \(error)") -} + let canonical = Scalars( + i8Value: -8, u8Value: 200, i16Value: -16_000, u16Value: 60_000, + i32Value: -2_000_000_000, u32Value: 4_000_000_000, + i64Value: -9_007_199_254_740_993, u64Value: UInt64.max, + f32Value: 1.5, f64Value: -2.25e100, flag: true, color: .blue) -// A Composite built from scratch with edge values: empty text, bytes, lists, -// and maps; unicode beyond the BMP; present-but-empty optionals; and -// non-finite, negative-zero, and subnormal floats inside nested positions. -let homemade = Composite( - name: "", - blob: Data(), - someI64: Int64.max, - noneI64: -1, - someText: nil, - names: ["日本語", "🚀 rocket", "", "\u{0}nul"], - matrix: [[], [Int32.min, Int32.max], []], - empty: [Double.nan, -0.0, Double.infinity, -Double.infinity, Double.leastNonzeroMagnitude], - byName: [:], - byId: [Int32.min: extremes, 0: canonical, Int32.max: infinities], - scalars: extremes, - shape: .empty, - shapes: [], - maybeShape: nil, - maybeList: Data(), - sparse: [nil, nil, true], - colors: [.blue, .blue, .red, .green]) -let homemadeBack = Codec.roundtripComposite(value: homemade) -expect(homemadeBack.name.isEmpty && homemadeBack.blob.isEmpty, "empty string and bytes") -expect(homemadeBack.someText == nil, "absent optional string") -expect(homemadeBack.maybeList != nil && homemadeBack.maybeList!.isEmpty, "present empty optional bytes") -expect(homemadeBack.names == homemade.names, "unicode names (got \(homemadeBack.names))") -expect(homemadeBack.matrix == [[], [Int32.min, Int32.max], []], "matrix with empty rows") -expect(homemadeBack.empty[0].isNaN, "NaN inside a list") -expect(homemadeBack.empty[1] == 0 && homemadeBack.empty[1].sign == .minus, "negative zero inside a list") -expect(homemadeBack.empty[2] == Double.infinity && homemadeBack.empty[3] == -Double.infinity, "infinities inside a list") -expect(homemadeBack.empty[4] == Double.leastNonzeroMagnitude, "subnormal double inside a list") -expect(homemadeBack.byName.isEmpty && homemadeBack.byId.count == 3, "empty map, three-entry map") -expect(homemadeBack.byId[Int32.min].map { scalarsEqual($0, extremes) } == true, "Int32.min key") -expect(homemadeBack.shapes.isEmpty && homemadeBack.maybeShape == nil, "empty shapes, absent maybeShape") -expect(compositeEqual(homemadeBack, homemade), "homemade Composite round-trips") -expect(Codec.describeComposite(value: homemade).contains("日本語"), "describe renders unicode") -do { - _ = try Codec.verifyComposite(value: homemade) - fail("homemade composite is not the canonical one") -} catch let e as CodecError { - expect(e.errorCode == 1, "homemade composite mismatch code == 1") -} catch { - fail("expected CodecError, got \(error)") -} + let scalars = Codec.sampleScalars() + expect(scalars.i8Value == -8, "i8 (got \(scalars.i8Value))") + expect(scalars.u8Value == 200, "u8 (got \(scalars.u8Value))") + expect(scalars.i16Value == -16_000, "i16 (got \(scalars.i16Value))") + expect(scalars.u16Value == 60_000, "u16 (got \(scalars.u16Value))") + expect(scalars.i32Value == -2_000_000_000, "i32 (got \(scalars.i32Value))") + expect(scalars.u32Value == 4_000_000_000, "u32 (got \(scalars.u32Value))") + expect(scalars.i64Value == -9_007_199_254_740_993, "i64 (got \(scalars.i64Value))") + expect(scalars.u64Value == UInt64.max, "u64 (got \(scalars.u64Value))") + expect(scalars.f32Value == 1.5, "f32 (got \(scalars.f32Value))") + expect(scalars.f64Value == -2.25e100, "f64 (got \(scalars.f64Value))") + expect(scalars.flag == true, "flag") + expect(scalars.color == .blue, "color (got \(scalars.color))") + expect(scalarsEqual(scalars, canonical), "sampleScalars matches the canonical fixture") -// --- Shape (rich enum) --------------------------------------------------------- - -let allShapes: [Shape] = [ - .empty, - .circle(radius: -0.0), - .rect(width: Float.nan, height: -Float.infinity), - .labeled(label: "ünïcödé ✓", count: Int32.min), - .nested(inner: extremes, note: ""), - .nested(inner: canonical, note: nil), -] -for s in allShapes { - expect(shapeEqual(Codec.roundtripShape(value: s), s), "roundtripShape identity for \(s)") -} -expect(shapesEqual(Codec.roundtripShapes(value: allShapes), allShapes), "roundtripShapes identity") -expect(Codec.roundtripShapes(value: []).isEmpty, "roundtripShapes of an empty list") -expect(Codec.describeShape(value: .empty) == "Empty", "describe Empty") -expect(Codec.describeShape(value: .circle(radius: 2.5)) == "Circle { radius: 2.5 }", "describe Circle") -expect(Codec.describeShape(value: .rect(width: 1.0, height: 0.5)) == "Rect { width: 1.0, height: 0.5 }", - "describe Rect") -expect(Codec.describeShape(value: .labeled(label: "tag", count: 3)) == "Labeled { label: \"tag\", count: 3 }", - "describe Labeled") -expect(Codec.describeShape(value: .nested(inner: canonical, note: nil)).hasPrefix("Nested { inner: Scalars {"), - "describe Nested") - -// --- Direct, string, bytes, and standalone buffered families -------------------- - -expect(Codec.roundtripOptI64(value: nil) == nil, "optional i64 absent") -expect(Codec.roundtripOptI64(value: Int64.min) == Int64.min, "optional i64 present") -expect(Codec.roundtripOptI64(value: 0) == 0, "optional i64 zero is present, not absent") -expect(Codec.roundtripMap(value: [:]).isEmpty, "empty map") -let bigMap: [String: Int64] = ["": 0, "a": Int64.max, "ключ": Int64.min, "z": -1] -expect(Codec.roundtripMap(value: bigMap) == bigMap, "map with empty and unicode keys") -expect(Codec.roundtripString(value: "") == "", "empty string") -expect(Codec.roundtripString(value: "héllo wörld ✓ 🚀") == "héllo wörld ✓ 🚀", "unicode string") -expect(Codec.roundtripBytes(value: Data()).isEmpty, "empty bytes") -let allBytes = Data((0...255).map { UInt8($0) }) -expect(Codec.roundtripBytes(value: allBytes) == allBytes, "every byte value") -expect(Codec.roundtripI64(value: Int64.min) == Int64.min, "i64 min") -expect(Codec.roundtripI64(value: Int64.max) == Int64.max, "i64 max") -expect(Codec.roundtripI64(value: -1) == -1, "i64 -1") -expect(Codec.roundtripU64(value: UInt64.max) == UInt64.max, "u64 max") -expect(Codec.roundtripU64(value: 1 << 63) == 1 << 63, "u64 above 2^63") -expect(Codec.roundtripU64(value: 0) == 0, "u64 zero") -expect(Codec.roundtripF64(value: Double.nan).isNaN, "f64 NaN") -expect(Codec.roundtripF64(value: Double.infinity) == Double.infinity, "f64 +inf") -expect(Codec.roundtripF64(value: -Double.infinity) == -Double.infinity, "f64 -inf") -let negZero = Codec.roundtripF64(value: -0.0) -expect(negZero == 0 && negZero.sign == .minus, "f64 negative zero keeps its sign") -expect(Codec.roundtripF64(value: Double.leastNonzeroMagnitude) == Double.leastNonzeroMagnitude, "f64 subnormal") -expect(Codec.roundtripF64(value: -2.25e100) == -2.25e100, "f64 large negative") -expect(Codec.roundtripBool(value: true) == true && Codec.roundtripBool(value: false) == false, "bool") -expect(Codec.roundtripColor(value: .blue) == .blue, "color blue (discriminant 7)") -expect(Codec.roundtripColor(value: .red) == .red, "color red") - -// --- Holder: objects inside buffers ------------------------------------------- - -let holder = Codec.makeHolder(base: 10, withSpare: true) -expect(holder.primary.value() == 10, "primary token (got \(holder.primary.value()))") -expect(holder.spare?.value() == 11, "spare token (got \(String(describing: holder.spare?.value())))") -expect(holder.many.map { $0.value() } == [12, 13, 14], "many tokens (got \(holder.many.map { $0.value() }))") -// Encoding clones every token; the producer adopts and drops those clones, -// and the wrappers remain valid for the next call. -expect(Codec.sumHolder(holder: holder) == 60, "sumHolder (got \(Codec.sumHolder(holder: holder)))") -expect(Codec.sumHolder(holder: holder) == 60, "sumHolder again: no reference was consumed") -expect(holder.primary.value() == 10 && holder.many[2].value() == 14, "tokens still alive after encoding") - -// primaryOf returns the very object stored in holder.primary. -let primary = Codec.primaryOf(holder: holder) -expect(primary.value() == 10, "primaryOf value") -let rebuilt = Holder(primary: primary, spare: nil, many: []) -expect(Codec.samePrimary(a: holder, b: rebuilt), "primaryOf returned the same object as holder.primary") -expect(Codec.samePrimary(a: holder, b: holder), "a holder shares its own primary") -expect(!Codec.samePrimary(a: holder, b: Codec.makeHolder(base: 10, withSpare: true)), - "a fresh holder with equal values is a different object") -expect(Codec.sumHolder(holder: rebuilt) == 10, "sumHolder over a consumer-built holder") - -let bare = Codec.makeHolder(base: 0, withSpare: false) -expect(bare.spare == nil, "withSpare false yields no spare") -expect(bare.primary.value() == 0 && bare.many.map { $0.value() } == [2, 3, 4], "bare tokens") -expect(Codec.sumHolder(holder: bare) == 9, "sumHolder without spare (got \(Codec.sumHolder(holder: bare)))") - -// Consumer-created tokens, including one object referenced twice from the -// same buffer (two clones, two adoptions). -let shared = Token(value: 100) -let mine = Holder(primary: shared, spare: Token(value: 5), many: [shared, Token(value: 1), shared]) -expect(Codec.sumHolder(holder: mine) == 100 + 5 + 100 + 1 + 100, "sumHolder over shared tokens") -expect(Codec.samePrimary(a: mine, b: Holder(primary: shared, spare: nil, many: [])), "shared primary identity") -expect(shared.value() == 100, "shared token alive after three encodings") -expect(Codec.sumHolder(holder: Holder(primary: Token(value: -7), spare: nil, many: [])) == -7, "minimal holder") - -// Release: every wrapper's deinit releases its strong reference, including -// the ones adopted from a decoded buffer and the one returned by primaryOf. -weak var weakPrimary: Token? -weak var weakSpare: Token? -weak var weakMany: Token? -weak var weakReturned: Token? -do { - let h = Codec.makeHolder(base: 20, withSpare: true) - weakPrimary = h.primary - weakSpare = h.spare - weakMany = h.many[0] - let p = Codec.primaryOf(holder: h) - weakReturned = p - expect(p.value() == 20 && Codec.sumHolder(holder: h) == 20 + 21 + 22 + 23 + 24, "scoped holder usable") + do { + expect(try Codec.verifyScalars(value: scalars) == true, "producer accepts the re-encoded sample") + expect(try Codec.verifyScalars(value: canonical) == true, "producer accepts a locally built canonical") + } catch { + fail("verifyScalars threw: \(error)") + } + expect(scalarsEqual(Codec.roundtripScalars(value: scalars), scalars), "roundtripScalars is the identity") + + // A single changed field is a typed CodecError.mismatch (code 1). + var tweaked = canonical + tweaked.u64Value -= 1 + do { + _ = try Codec.verifyScalars(value: tweaked) + fail("expected CodecError.mismatch for a tweaked Scalars") + } catch let e as CodecError { + guard case let .mismatch(message) = e else { fail("expected .mismatch, got \(e)") } + expect(e.errorCode == 1, "mismatch code == 1 (got \(e.errorCode))") + expect(message == "value does not match the canonical fixture", "mismatch message (got \(message))") + } catch { + fail("expected CodecError, got \(error)") + } + + // Scalars built from scratch at the extremes, with non-finite floats and a + // negative zero, survive a round trip bit for bit. + let extremes = Scalars( + i8Value: Int8.min, u8Value: UInt8.max, i16Value: Int16.max, u16Value: UInt16.max, + i32Value: Int32.min, u32Value: UInt32.max, i64Value: Int64.min, u64Value: 1 << 63, + f32Value: -0.0, f64Value: Double.nan, flag: false, color: .red) + let extremesBack = Codec.roundtripScalars(value: extremes) + expect(extremesBack.i64Value == Int64.min && extremesBack.u64Value == 1 << 63, "64-bit extremes") + expect(extremesBack.f32Value == 0 && extremesBack.f32Value.sign == .minus, "negative zero f32 keeps its sign") + expect(extremesBack.f64Value.isNaN, "NaN f64 round-trips") + expect(scalarsEqual(extremesBack, extremes), "extreme Scalars round-trip bit for bit") + let infinities = Scalars( + i8Value: Int8.max, u8Value: 0, i16Value: Int16.min, u16Value: 0, i32Value: Int32.max, u32Value: 0, + i64Value: Int64.max, u64Value: 0, f32Value: Float.infinity, f64Value: -Double.infinity, + flag: true, color: .green) + expect(scalarsEqual(Codec.roundtripScalars(value: infinities), infinities), "infinities round-trip") + + // --- Composite ---------------------------------------------------------------- + + let composite = Codec.sampleComposite() + expect(composite.name == "héllo wörld ✓", "name (got \(composite.name))") + expect(composite.blob == Data([0, 1, 2, 253, 254, 255]), "blob (got \(Array(composite.blob)))") + expect(composite.someI64 == Int64.min, "someI64 (got \(String(describing: composite.someI64)))") + expect(composite.noneI64 == nil, "noneI64 nil") + expect(composite.someText == "", "someText is a present empty string (got \(String(describing: composite.someText)))") + expect(composite.names == ["a", "", "ccc"], "names (got \(composite.names))") + expect(composite.matrix == [[1, 2, 3], [], [-4]], "matrix (got \(composite.matrix))") + expect(composite.empty.isEmpty, "empty list") + expect(composite.byName == ["one": 1, "two": 2, "neg": -3], "byName (got \(composite.byName))") + expect(composite.byId.count == 2, "byId has two entries (got \(composite.byId.count))") + expect(composite.byId[-1].map { scalarsEqual($0, canonical) } == true, "byId[-1] is the canonical Scalars") + expect(composite.byId[42]?.flag == false, "byId[42] has flag false") + var flagless = canonical + flagless.flag = false + expect(composite.byId[42].map { scalarsEqual($0, flagless) } == true, "byId[42] otherwise canonical") + expect(scalarsEqual(composite.scalars, canonical), "nested scalars") + expect(shapeEqual(composite.shape, .labeled(label: "tag", count: 3)), "shape (got \(composite.shape))") + expect(composite.shapes.count == 5, "five shapes (got \(composite.shapes.count))") + expect(shapesEqual(composite.shapes, [ + .empty, + .circle(radius: 2.5), + .rect(width: 1.0, height: 0.5), + .labeled(label: "", count: -1), + .nested(inner: canonical, note: "n"), + ]), "shapes one of each variant (got \(composite.shapes))") + expect(optionalShapeEqual(composite.maybeShape, .nested(inner: canonical, note: nil)), + "maybeShape is a nested variant with an absent note (got \(String(describing: composite.maybeShape)))") + expect(composite.maybeList == Data([9, 8]), "maybeList (got \(String(describing: composite.maybeList)))") + expect(composite.sparse == [true, nil, false], "sparse (got \(composite.sparse))") + expect(composite.colors == [.red, .green, .blue], "colors (got \(composite.colors))") + + do { + expect(try Codec.verifyComposite(value: composite) == true, "producer accepts the re-encoded composite") + } catch { + fail("verifyComposite threw: \(error); producer saw: \(Codec.describeComposite(value: composite))") + } + expect(compositeEqual(Codec.roundtripComposite(value: composite), composite), "roundtripComposite is the identity") + let described = Codec.describeComposite(value: composite) + expect(described.hasPrefix("Composite {") && described.contains("héllo wörld ✓"), "describeComposite (got \(described))") + + // A changed nested value is rejected. + var changed = composite + changed.sparse[1] = true + do { + _ = try Codec.verifyComposite(value: changed) + fail("expected CodecError.mismatch for a changed composite") + } catch let e as CodecError { + expect(e.errorCode == 1, "composite mismatch code == 1") + } catch { + fail("expected CodecError, got \(error)") + } + + // A Composite built from scratch with edge values: empty text, bytes, lists, + // and maps; unicode beyond the BMP; present-but-empty optionals; and + // non-finite, negative-zero, and subnormal floats inside nested positions. + let homemade = Composite( + name: "", + blob: Data(), + someI64: Int64.max, + noneI64: -1, + someText: nil, + names: ["日本語", "🚀 rocket", "", "\u{0}nul"], + matrix: [[], [Int32.min, Int32.max], []], + empty: [Double.nan, -0.0, Double.infinity, -Double.infinity, Double.leastNonzeroMagnitude], + byName: [:], + byId: [Int32.min: extremes, 0: canonical, Int32.max: infinities], + scalars: extremes, + shape: .empty, + shapes: [], + maybeShape: nil, + maybeList: Data(), + sparse: [nil, nil, true], + colors: [.blue, .blue, .red, .green]) + let homemadeBack = Codec.roundtripComposite(value: homemade) + expect(homemadeBack.name.isEmpty && homemadeBack.blob.isEmpty, "empty string and bytes") + expect(homemadeBack.someText == nil, "absent optional string") + expect(homemadeBack.maybeList != nil && homemadeBack.maybeList!.isEmpty, "present empty optional bytes") + expect(homemadeBack.names == homemade.names, "unicode names (got \(homemadeBack.names))") + expect(homemadeBack.matrix == [[], [Int32.min, Int32.max], []], "matrix with empty rows") + expect(homemadeBack.empty[0].isNaN, "NaN inside a list") + expect(homemadeBack.empty[1] == 0 && homemadeBack.empty[1].sign == .minus, "negative zero inside a list") + expect(homemadeBack.empty[2] == Double.infinity && homemadeBack.empty[3] == -Double.infinity, "infinities inside a list") + expect(homemadeBack.empty[4] == Double.leastNonzeroMagnitude, "subnormal double inside a list") + expect(homemadeBack.byName.isEmpty && homemadeBack.byId.count == 3, "empty map, three-entry map") + expect(homemadeBack.byId[Int32.min].map { scalarsEqual($0, extremes) } == true, "Int32.min key") + expect(homemadeBack.shapes.isEmpty && homemadeBack.maybeShape == nil, "empty shapes, absent maybeShape") + expect(compositeEqual(homemadeBack, homemade), "homemade Composite round-trips") + expect(Codec.describeComposite(value: homemade).contains("日本語"), "describe renders unicode") + do { + _ = try Codec.verifyComposite(value: homemade) + fail("homemade composite is not the canonical one") + } catch let e as CodecError { + expect(e.errorCode == 1, "homemade composite mismatch code == 1") + } catch { + fail("expected CodecError, got \(error)") + } + + // --- Shape (rich enum) --------------------------------------------------------- + + let allShapes: [Shape] = [ + .empty, + .circle(radius: -0.0), + .rect(width: Float.nan, height: -Float.infinity), + .labeled(label: "ünïcödé ✓", count: Int32.min), + .nested(inner: extremes, note: ""), + .nested(inner: canonical, note: nil), + ] + for s in allShapes { + expect(shapeEqual(Codec.roundtripShape(value: s), s), "roundtripShape identity for \(s)") + } + expect(shapesEqual(Codec.roundtripShapes(value: allShapes), allShapes), "roundtripShapes identity") + expect(Codec.roundtripShapes(value: []).isEmpty, "roundtripShapes of an empty list") + expect(Codec.describeShape(value: .empty) == "Empty", "describe Empty") + expect(Codec.describeShape(value: .circle(radius: 2.5)) == "Circle { radius: 2.5 }", "describe Circle") + expect(Codec.describeShape(value: .rect(width: 1.0, height: 0.5)) == "Rect { width: 1.0, height: 0.5 }", + "describe Rect") + expect(Codec.describeShape(value: .labeled(label: "tag", count: 3)) == "Labeled { label: \"tag\", count: 3 }", + "describe Labeled") + expect(Codec.describeShape(value: .nested(inner: canonical, note: nil)).hasPrefix("Nested { inner: Scalars {"), + "describe Nested") + + // --- Direct, string, bytes, and standalone buffered families -------------------- + + expect(Codec.roundtripOptI64(value: nil) == nil, "optional i64 absent") + expect(Codec.roundtripOptI64(value: Int64.min) == Int64.min, "optional i64 present") + expect(Codec.roundtripOptI64(value: 0) == 0, "optional i64 zero is present, not absent") + expect(Codec.roundtripMap(value: [:]).isEmpty, "empty map") + let bigMap: [String: Int64] = ["": 0, "a": Int64.max, "ключ": Int64.min, "z": -1] + expect(Codec.roundtripMap(value: bigMap) == bigMap, "map with empty and unicode keys") + expect(Codec.roundtripString(value: "") == "", "empty string") + expect(Codec.roundtripString(value: "héllo wörld ✓ 🚀") == "héllo wörld ✓ 🚀", "unicode string") + // Strings cross as pointer and length, so an interior NUL survives both + // directions, alone and inside a buffered record. + let withNul = "before\u{0}after\u{0}" + expect(Codec.roundtripString(value: withNul) == withNul, "interior NUL string round-trips") + expect(Codec.roundtripString(value: "\u{0}") == "\u{0}", "lone NUL string round-trips") + expect(Codec.roundtripComposite(value: homemade).names[3] == "\u{0}nul", "NUL inside a record field") + expect(Codec.roundtripBytes(value: Data()).isEmpty, "empty bytes") + let allBytes = Data((0...255).map { UInt8($0) }) + expect(Codec.roundtripBytes(value: allBytes) == allBytes, "every byte value") + expect(Codec.roundtripI64(value: Int64.min) == Int64.min, "i64 min") + expect(Codec.roundtripI64(value: Int64.max) == Int64.max, "i64 max") + expect(Codec.roundtripI64(value: -1) == -1, "i64 -1") + expect(Codec.roundtripU64(value: UInt64.max) == UInt64.max, "u64 max") + expect(Codec.roundtripU64(value: 1 << 63) == 1 << 63, "u64 above 2^63") + expect(Codec.roundtripU64(value: 0) == 0, "u64 zero") + expect(Codec.roundtripF64(value: Double.nan).isNaN, "f64 NaN") + expect(Codec.roundtripF64(value: Double.infinity) == Double.infinity, "f64 +inf") + expect(Codec.roundtripF64(value: -Double.infinity) == -Double.infinity, "f64 -inf") + let negZero = Codec.roundtripF64(value: -0.0) + expect(negZero == 0 && negZero.sign == .minus, "f64 negative zero keeps its sign") + expect(Codec.roundtripF64(value: Double.leastNonzeroMagnitude) == Double.leastNonzeroMagnitude, "f64 subnormal") + expect(Codec.roundtripF64(value: -2.25e100) == -2.25e100, "f64 large negative") + expect(Codec.roundtripBool(value: true) == true && Codec.roundtripBool(value: false) == false, "bool") + expect(Codec.roundtripColor(value: .blue) == .blue, "color blue (discriminant 7)") + expect(Codec.roundtripColor(value: .red) == .red, "color red") + + // --- Holder: objects inside buffers ------------------------------------------- + + let holder = Codec.makeHolder(base: 10, withSpare: true) + expect(holder.primary.value() == 10, "primary token (got \(holder.primary.value()))") + expect(holder.spare?.value() == 11, "spare token (got \(String(describing: holder.spare?.value())))") + expect(holder.many.map { $0.value() } == [12, 13, 14], "many tokens (got \(holder.many.map { $0.value() }))") + // Encoding clones every token; the producer adopts and drops those clones, + // and the wrappers remain valid for the next call. + expect(Codec.sumHolder(holder: holder) == 60, "sumHolder (got \(Codec.sumHolder(holder: holder)))") + expect(Codec.sumHolder(holder: holder) == 60, "sumHolder again: no reference was consumed") + expect(holder.primary.value() == 10 && holder.many[2].value() == 14, "tokens still alive after encoding") + + // primaryOf returns the very object stored in holder.primary. + let primary = Codec.primaryOf(holder: holder) + expect(primary.value() == 10, "primaryOf value") + let rebuilt = Holder(primary: primary, spare: nil, many: []) + expect(Codec.samePrimary(a: holder, b: rebuilt), "primaryOf returned the same object as holder.primary") + expect(Codec.samePrimary(a: holder, b: holder), "a holder shares its own primary") + expect(!Codec.samePrimary(a: holder, b: Codec.makeHolder(base: 10, withSpare: true)), + "a fresh holder with equal values is a different object") + expect(Codec.sumHolder(holder: rebuilt) == 10, "sumHolder over a consumer-built holder") + + let bare = Codec.makeHolder(base: 0, withSpare: false) + expect(bare.spare == nil, "withSpare false yields no spare") + expect(bare.primary.value() == 0 && bare.many.map { $0.value() } == [2, 3, 4], "bare tokens") + expect(Codec.sumHolder(holder: bare) == 9, "sumHolder without spare (got \(Codec.sumHolder(holder: bare)))") + + // Consumer-created tokens, including one object referenced twice from the + // same buffer (two clones, two adoptions). + let shared = Token(value: 100) + let mine = Holder(primary: shared, spare: Token(value: 5), many: [shared, Token(value: 1), shared]) + expect(Codec.sumHolder(holder: mine) == 100 + 5 + 100 + 1 + 100, "sumHolder over shared tokens") + expect(Codec.samePrimary(a: mine, b: Holder(primary: shared, spare: nil, many: [])), "shared primary identity") + expect(shared.value() == 100, "shared token alive after three encodings") + expect(Codec.sumHolder(holder: Holder(primary: Token(value: -7), spare: nil, many: [])) == -7, "minimal holder") + + // Release: every wrapper's deinit releases its strong reference, including + // the ones adopted from a decoded buffer and the one returned by primaryOf. + weak var weakPrimary: Token? + weak var weakSpare: Token? + weak var weakMany: Token? + weak var weakReturned: Token? + do { + let h = Codec.makeHolder(base: 20, withSpare: true) + weakPrimary = h.primary + weakSpare = h.spare + weakMany = h.many[0] + let p = Codec.primaryOf(holder: h) + weakReturned = p + expect(p.value() == 20 && Codec.sumHolder(holder: h) == 20 + 21 + 22 + 23 + 24, "scoped holder usable") + } + expect(weakPrimary == nil && weakSpare == nil && weakMany == nil && weakReturned == nil, + "every token wrapper was deinitialized when the holder left scope") } -expect(weakPrimary == nil && weakSpare == nil && weakMany == nil && weakReturned == nil, - "every token wrapper was deinitialized when the holder left scope") +run() +assertNoLeaks() print("swift/codec: OK") diff --git a/conformance/swift/contacts.swift b/conformance/swift/contacts.swift index 270f8658..3b104d5b 100644 --- a/conformance/swift/contacts.swift +++ b/conformance/swift/contacts.swift @@ -1,17 +1,19 @@ // Conformance consumer: contacts sample, Swift target. // -// Binds through the generated `Contacts` module and asserts the 0.7.0 -// interface surface: `ContactBook` as a final class whose `new` constructor is -// a plain `init()`, throwing methods (`add`, `get`) that raise the typed +// Binds through the generated `Contacts` package and asserts the interface +// surface: `ContactBook` as a final class whose `new` constructor is a plain +// `init()`, throwing methods (`add`, `get`) that raise the typed // `ContactsError` domain enum, non-throwing methods (`list`, `remove`, -// `count`) called without `try`, real argument labels, enum marshalling, and -// `Contact` as a plain struct decoded from a value buffer: stored properties -// (no getters), an optional string field (nil email), and a list-of-struct -// return decoded to `[Contact]`. The typed-error asserts pin both the case -// and the numeric code carried by `errorCode`. +// `count`) called without `try`, enum marshalling, and `Contact` as a +// `Hashable` struct decoded from a value buffer. The nested `Contacts.Groups` +// namespace takes and returns the parent module's `ContactType` enum and +// `ContactBook` interface and throws the parent's domain; the sibling +// `Directory` root shares the `Contact` record and has its own domain. At +// exit, every native object and allocation has been released. -import Foundation +import CContacts import Contacts +import Foundation func fail(_ msg: String) -> Never { FileHandle.standardError.write(Data("assertion failed: \(msg)\n".utf8)) @@ -22,19 +24,34 @@ func expect(_ cond: Bool, _ msg: String) { if !cond { fail(msg) } } -do { +/// Every live-object counter the producer keeps must settle at zero. +func assertNoLeaks() { + let kinds = ["objects", "callbacks", "iterators", "cancel tokens", "allocations"] + var live: [UInt64] = [] + for _ in 0..<200 { + live = (0..<5).map { contacts_debug_live(Int32($0)) } + if live.allSatisfy({ $0 == 0 }) { return } + usleep(10_000) + } + for (kind, n) in live.enumerated() where n != 0 { + fail("\(n) live \(kinds[kind]) at exit") + } +} + +func run() throws { let book = ContactBook() + expect(contacts_debug_live(0) == 1, "the leak counters see the live book") let alice = try book.add( firstName: "Alice", lastName: "Smith", email: "alice@example.com", contactType: .work) expect(alice.id > 0, "alice id positive") - // The record is a value type: `get` decodes a fresh Contact struct whose - // stored properties round-trip the producer's field values. + // The record is a value type: `get` decodes a fresh Contact struct equal + // to the one `add` returned. let c = try book.get(id: alice.id) - expect(c.firstName == "Alice", "firstName") - expect(c.lastName == "Smith", "lastName") + expect(c == alice, "get returns the added contact") + expect(c.firstName == "Alice" && c.lastName == "Smith", "names") expect(c.email == "alice@example.com", "email") expect(c.contactType == .work, "contactType") @@ -42,7 +59,7 @@ do { let bob = try book.add(firstName: "Bob", lastName: "Jones", email: nil, contactType: .personal) let cb = try book.get(id: bob.id) expect(cb.email == nil, "bob email nil") - expect(cb.contactType == .personal, "bob contactType") + expect(Set([alice, bob, cb]).count == 2, "Contact is Hashable") // The memberwise init builds a local value without touching the ABI. let local = Contact(id: 42, firstName: "Carol", lastName: "Doe", email: nil, contactType: .other) @@ -51,12 +68,7 @@ do { // Non-throwing methods need no `try`. expect(book.count() == 2, "count == 2") let everyone = book.list() - expect(everyone.count == 2, "list count == 2") - let names = everyone.map { $0.firstName }.sorted() - expect(names == ["Alice", "Bob"], "list names") - - expect(book.remove(id: alice.id) == true, "remove returns true") - expect(book.count() == 1, "count == 1 after remove") + expect(everyone.map { $0.firstName }.sorted() == ["Alice", "Bob"], "list names") // A missing id raises the typed domain error's notFound case (code 2). do { @@ -65,7 +77,7 @@ do { } catch let e as ContactsError { guard case let .notFound(message) = e else { fail("expected .notFound, got \(e)") } expect(e.errorCode == 2, "notFound code == 2 (got \(e.errorCode))") - expect(!message.isEmpty, "notFound message non-empty") + expect(message == "contact not found", "notFound message (got \(message))") } // An empty name raises the invalidName case (code 1). @@ -76,10 +88,48 @@ do { guard case .invalidName = e else { fail("expected .invalidName, got \(e)") } expect(e.errorCode == 1, "invalidName code == 1 (got \(e.errorCode))") } - expect(book.count() == 1, "rejected add stores nothing") + expect(book.count() == 2, "rejected add stores nothing") + + // --- Nested module: the parent's enum, interface, and error domain ------ + _ = try book.add(firstName: "Dana", lastName: "Wu", email: nil, contactType: .work) + expect(Contacts.Groups.countOfType(book: book, contactType: .work) == 2, "two work contacts") + expect(Contacts.Groups.countOfType(book: book, contactType: .other) == 0, "no other contacts") + expect(Contacts.Groups.dominantType(book: book) == .work, "work dominates") + let work = Contacts.Groups.splitByType(book: book, contactType: .work) + expect(work.count() == 2, "split book holds the work contacts") + expect(work.list().allSatisfy { $0.contactType == .work }, "split book is all work") + expect(book.count() == 3, "splitting leaves the source book intact") + let firstWork = try Contacts.Groups.firstOfType(book: book, contactType: .work) + expect(firstWork.firstName == "Alice", "first work contact (got \(firstWork.firstName))") + do { + _ = try Contacts.Groups.firstOfType(book: work, contactType: .personal) + fail("expected ContactsError.notFound from the nested module") + } catch ContactsError.notFound { + } + + // --- Sibling root: shares the Contact record, has its own domain -------- + let card = Directory.card(contact: alice) + expect(card == Card(displayName: "Smith, Alice", initials: "AS", hasEmail: true), + "card (got \(card))") + expect(!Directory.card(contact: bob).hasEmail, "bob's card has no email") + let sorted = try Directory.sorted(contacts: [alice, bob, local]) + expect(sorted.map { $0.lastName } == ["Doe", "Jones", "Smith"], "sorted by last name") + do { + _ = try Directory.sorted(contacts: []) + fail("expected DirectoryError.empty") + } catch let e as DirectoryError { + guard case .empty = e else { fail("expected .empty, got \(e)") } + expect(e.errorCode == 1, "empty code == 1") + } - print("swift/contacts: OK") + expect(book.remove(id: alice.id) == true, "remove returns true") + expect(book.count() == 2, "count == 2 after remove") +} + +do { + try run() } catch { - FileHandle.standardError.write(Data("unexpected error: \(error)\n".utf8)) - exit(1) + fail("unexpected error: \(error)") } +assertNoLeaks() +print("swift/contacts: OK") diff --git a/conformance/swift/events.swift b/conformance/swift/events.swift index 6f583350..ef48b4a9 100644 --- a/conformance/swift/events.swift +++ b/conformance/swift/events.swift @@ -1,22 +1,24 @@ // Conformance consumer: events sample, Swift target. // -// Binds through the generated `Events` module and drives the ABI 2 surface: -// the `Subscriber` callback interface implemented as a Swift class (every -// method observed with its arguments, including the `EventBus` object the -// producer hands to `onAttached`, which is usable and released), `Delivery` -// return values steering `publish`'s accepted count, the reference-counted -// `EventBus` object, the `publishLater` async wrapper, `messages()` as a lazy -// Sequence, the `lastMessage()` optional record, and the free function -// `routeOnce`. Release is observed through weak references: a subscriber's -// box is freed when the bus drops it, and the bus wrapper handed to a callback -// is destroyed when the consumer lets go of it. The events module declares no -// error domain, so every wrapper is non-throwing and traps on a failure -// (including a subscriber that throws, which the producer reports as code -// -4); the throwing-callback path is exercised in the kvstore lane, whose -// wrappers do throw. Exits non-zero on any mismatch. - -import Foundation +// Binds through the generated `Events` package and drives the `Subscriber` +// callback interface implemented as a Swift class (every method observed +// with its arguments, including the `EventBus` object the producer hands to +// `onAttached`), `Delivery` return values steering `publish`'s accepted +// count, the reference-counted `EventBus` object, the `publishLater` async +// wrapper, `messages()` as a lazy Sequence, the `lastMessage()` optional +// record, and the free function `routeOnce`. Release is observed through +// weak references. +// +// `route` is the `Result`-returning callback method: a subscriber that +// throws from it makes the producer fail the call with the foreign code -4. +// The events module declares no error domain, so `publish` doesn't throw and +// the failure stops the process with a message naming the code; a child +// process (this executable run with `trap-route`) proves that. At exit, +// every native object, callback, iterator, and allocation has been released. + +import CEvents import Events +import Foundation func fail(_ msg: String) -> Never { FileHandle.standardError.write(Data("assertion failed: \(msg)\n".utf8)) @@ -27,15 +29,35 @@ func expect(_ cond: Bool, _ msg: String) { if !cond { fail(msg) } } +/// Every live-object counter the producer keeps must settle at zero. +func assertNoLeaks() { + let kinds = ["objects", "callbacks", "iterators", "cancel tokens", "allocations"] + var live: [UInt64] = [] + for _ in 0..<200 { + live = (0..<5).map { events_debug_live(Int32($0)) } + if live.allSatisfy({ $0 == 0 }) { return } + usleep(10_000) + } + for (kind, n) in live.enumerated() where n != 0 { + fail("\(n) live \(kinds[kind]) at exit") + } +} + +struct RouteRejected: Error, LocalizedError { + let topic: String + var errorDescription: String? { "route rejected \(topic)" } +} + /// A consumer-side subscriber. Records every callback so the test can assert /// the arguments the producer passed, and counts live instances so the /// producer's `free` (which releases the generated box, and with it the last -/// strong reference to this object) is observable. -final class TestSubscriber: Subscriber { +/// strong reference to this object) is observable. Callbacks arrive on the +/// calling thread here, so plain mutable state is fine. +final class TestSubscriber: Subscriber, @unchecked Sendable { static var live = 0 - let name: String let skipTopic: String + let failTopic: String let keepBus: Bool var routed: [String] = [] var received: [Message] = [] @@ -44,13 +66,12 @@ final class TestSubscriber: Subscriber { var countSeenOnAttach: Int64 = -1 /// The bus adopted from `onAttached` when `keepBus` is set. var attachedBus: EventBus? - /// A weak view of the bus wrapper handed to `onAttached`, to prove the - /// wrapper (and its strong reference) was released when not kept. + /// A weak view of the bus wrapper handed to `onAttached`. weak var weakAttachedBus: EventBus? - init(name: String, skipTopic: String = "", keepBus: Bool = false) { - self.name = name + init(skipTopic: String = "", failTopic: String = "", keepBus: Bool = false) { self.skipTopic = skipTopic + self.failTopic = failTopic self.keepBus = keepBus TestSubscriber.live += 1 } @@ -59,6 +80,7 @@ final class TestSubscriber: Subscriber { func route(topic: String) throws -> Delivery { routed.append(topic) + if topic == failTopic { throw RouteRejected(topic: topic) } if topic == skipTopic { return .skip } if topic == "stop" { return .acceptAndStop } return .accept @@ -79,127 +101,145 @@ final class TestSubscriber: Subscriber { } } -// --- Empty bus --------------------------------------------------------------- -var bus: EventBus? = EventBus() -expect(bus!.subscriberCount() == 0, "new bus has no subscribers") -expect(bus!.lastMessage() == nil, "new bus has no last message") -expect(Array(bus!.messages()).isEmpty, "new bus has no messages") - -// --- subscribe: onAttached receives a usable bus object ---------------------- -var a: TestSubscriber? = TestSubscriber(name: "a", skipTopic: "quiet") -var b: TestSubscriber? = TestSubscriber(name: "b", keepBus: true) -weak var weakA = a -weak var weakB = b - -expect(bus!.subscribe(subscriber: a!) == 1, "first subscribe returns 1") -expect(a!.attachedCount == 1, "a attached once") -expect(a!.countSeenOnAttach == 0, "a saw an empty bus on attach (got \(a!.countSeenOnAttach))") -expect(a!.weakAttachedBus == nil, "bus wrapper handed to a was released after onAttached") -expect(bus!.subscriberCount() == 1, "bus still alive after callback released its reference") - -expect(bus!.subscribe(subscriber: b!) == 2, "second subscribe returns 2") -expect(b!.attachedCount == 1, "b attached once") -expect(b!.countSeenOnAttach == 1, "b saw one subscriber on attach (got \(b!.countSeenOnAttach))") -expect(b!.attachedBus != nil && b!.weakAttachedBus != nil, "b kept the adopted bus") -expect(bus!.subscriberCount() == 2, "subscriberCount == 2") - -// --- publish: Delivery steers the accepted count ----------------------------- -expect(bus!.publish(topic: "news", text: "hello", tags: ["x", "y"]) == 2, - "both subscribers accept 'news'") -expect(a!.routed == ["news"] && b!.routed == ["news"], "route asked once per subscriber") -expect(a!.received.count == 1 && b!.received.count == 1, "one delivery each") -let first = a!.received[0] -expect(first.seq == 1, "seq starts at 1 (got \(first.seq))") -expect(first.topic == "news", "topic (got \(first.topic))") -expect(first.text == "hello", "text (got \(first.text))") -expect(first.tags == ["x", "y"], "tags (got \(first.tags))") - -// a skips 'quiet'; b accepts it. -expect(bus!.publish(topic: "quiet", text: "psst", tags: []) == 1, "skip lowers the accepted count") -expect(a!.routed == ["news", "quiet"], "a was asked about 'quiet' (got \(a!.routed))") -expect(a!.received.count == 1, "a did not receive the skipped message") -expect(b!.received.count == 2 && b!.received[1].seq == 2 && b!.received[1].tags.isEmpty, - "b received the second message with empty tags") - -// AcceptAndStop from the first subscriber stops delivery to the rest. -let c = TestSubscriber(name: "c") -expect(bus!.subscribe(subscriber: c) == 3, "third subscribe returns 3") -expect(bus!.publish(topic: "stop", text: "last", tags: ["z"]) == 1, "acceptAndStop delivers once") -expect(a!.received.count == 2 && a!.received[1].text == "last", "a took the stop message") -expect(b!.routed == ["news", "quiet"], "b was not asked after the stop (got \(b!.routed))") -expect(c.routed.isEmpty && c.received.isEmpty, "c was never reached") - -// --- messages(): iterator as a lazy Sequence --------------------------------- -let texts = Array(bus!.messages()) -expect(texts == ["hello", "psst", "last"], "messages in order (got \(texts))") -var joined: [String] = [] -for m in bus!.messages() { joined.append(m.uppercased()) } -expect(joined == ["HELLO", "PSST", "LAST"], "for-in over the sequence") -expect(bus!.messages().map { $0.count } == [5, 4, 4], "map over the sequence") -// Abandoning early releases the producer iterator through deinit. -expect(bus!.messages().first(where: { $0.hasPrefix("p") }) == "psst", "early exit") - -// --- lastMessage(): optional record ------------------------------------------ -let last = bus!.lastMessage() -expect(last != nil, "lastMessage present") -expect(last!.seq == 3 && last!.topic == "stop" && last!.text == "last" && last!.tags == ["z"], - "lastMessage fields (got \(last!))") - -// --- publishLater: async wrapper awaited from the producer's thread --------- -let later = await bus!.publishLater(topic: "news", text: "later") -expect(later == 3, "publishLater accepted by all three (got \(later))") -expect(bus!.lastMessage()?.text == "later", "async publish logged") -expect(bus!.lastMessage()?.seq == 4, "async publish took seq 4") -expect(c.received.count == 1 && c.received[0].tags.isEmpty, "c received the async message") - -// --- The bus adopted in onAttached is the same object -------------------------- -let kept = b!.attachedBus! -expect(kept.subscriberCount() == 3, "kept bus sees all three subscribers") -expect(kept.publish(topic: "via-kept", text: "same object", tags: []) == 3, - "publish through the kept wrapper reaches every subscriber") -expect(bus!.lastMessage()?.topic == "via-kept", "publish via kept wrapper visible through the original") -expect(Array(bus!.messages()).count == 5, "five messages logged") - -// --- routeOnce: a callback passed to a free function is freed on return ------- -weak var weakD: TestSubscriber? -do { - let d = TestSubscriber(name: "d", skipTopic: "quiet") - weakD = d - expect(Events.routeOnce(subscriber: d, topic: "quiet") == .skip, "routeOnce skip") - expect(Events.routeOnce(subscriber: d, topic: "stop") == .acceptAndStop, "routeOnce acceptAndStop") - expect(Events.routeOnce(subscriber: d, topic: "other") == .accept, "routeOnce accept") - expect(d.routed == ["quiet", "stop", "other"], "routeOnce asked route each time") - expect(d.attachedCount == 0, "routeOnce never attaches") +/// Child-process mode: a subscriber whose `route` throws makes the +/// non-throwing `publish` stop the process. +if CommandLine.arguments.dropFirst().first == "trap-route" { + let bus = EventBus() + _ = bus.subscribe(subscriber: TestSubscriber(failTopic: "boom")) + _ = bus.publish(topic: "boom", text: "never delivered", tags: []) + print("publish returned after a failing route") + exit(0) } -expect(weakD == nil, "routeOnce released its subscriber (free ran)") - -// --- clearSubscribers releases every subscriber ------------------------------ -a = nil -b = nil -expect(weakA != nil && weakB != nil, "the bus retains a and b after the consumer drops them") -bus!.clearSubscribers() -expect(bus!.subscriberCount() == 0, "no subscribers after clear") -expect(weakA == nil, "a freed by clearSubscribers") -expect(weakB == nil, "b (and the bus reference it kept) freed by clearSubscribers") -expect(bus!.lastMessage()?.topic == "via-kept", "bus still alive after b's kept reference was released") -expect(bus!.publish(topic: "news", text: "nobody", tags: []) == 0, "publish with no subscribers") -expect(c.received.count == 2, "c is no longer delivered to (got \(c.received.count))") - -// --- Destroying the bus frees a retained subscriber -------------------------- -weak var weakE: TestSubscriber? -weak var weakBus2: EventBus? -do { - let bus2 = EventBus() - weakBus2 = bus2 - let e = TestSubscriber(name: "e") - weakE = e - expect(bus2.subscribe(subscriber: e) == 1, "bus2 subscribe") - expect(bus2.publish(topic: "t", text: "m", tags: []) == 1, "bus2 publish") + +func runTrapRouteChild() { + let child = Process() + child.executableURL = URL(fileURLWithPath: CommandLine.arguments[0]) + child.arguments = ["trap-route"] + let stderr = Pipe() + child.standardError = stderr + child.standardOutput = Pipe() + do { + try child.run() + } catch { + fail("could not start the trap-route child: \(error)") + } + let output = String(decoding: stderr.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self) + child.waitUntilExit() + expect(child.terminationReason == .uncaughtSignal, + "a throwing route stops the process (status \(child.terminationStatus))") + expect(output.contains("failed with code -4: route rejected boom"), + "the fatal error names the foreign code and message (got \(output))") } -expect(weakBus2 == nil, "bus2 wrapper deinitialized") -expect(weakE == nil, "dropping the last bus reference freed its subscriber") -bus = nil -expect(TestSubscriber.live == 1, "only c is still alive (got \(TestSubscriber.live))") +func run() async { + // --- Empty bus ----------------------------------------------------------- + var bus: EventBus? = EventBus() + expect(bus!.subscriberCount() == 0, "new bus has no subscribers") + expect(bus!.lastMessage() == nil, "new bus has no last message") + expect(Array(bus!.messages()).isEmpty, "new bus has no messages") + + // --- subscribe: onAttached receives a usable bus object ------------------ + var a: TestSubscriber? = TestSubscriber(skipTopic: "quiet") + var b: TestSubscriber? = TestSubscriber(keepBus: true) + weak var weakA = a + weak var weakB = b + + expect(bus!.subscribe(subscriber: a!) == 1, "first subscribe returns 1") + expect(a!.attachedCount == 1, "a attached once") + expect(a!.countSeenOnAttach == 0, "a saw an empty bus on attach (got \(a!.countSeenOnAttach))") + expect(a!.weakAttachedBus == nil, "bus wrapper handed to a was released after onAttached") + + expect(bus!.subscribe(subscriber: b!) == 2, "second subscribe returns 2") + expect(b!.countSeenOnAttach == 1, "b saw one subscriber on attach (got \(b!.countSeenOnAttach))") + expect(b!.attachedBus != nil && b!.weakAttachedBus != nil, "b kept the adopted bus") + expect(bus!.subscriberCount() == 2, "subscriberCount == 2") + + // --- publish: Delivery steers the accepted count ------------------------- + expect(bus!.publish(topic: "news", text: "hello", tags: ["x", "y"]) == 2, + "both subscribers accept 'news'") + expect(a!.routed == ["news"] && b!.routed == ["news"], "route asked once per subscriber") + let first = a!.received[0] + expect(first == Message(seq: 1, topic: "news", text: "hello", tags: ["x", "y"]), + "first message (got \(first))") + + // a skips 'quiet'; b accepts it. + expect(bus!.publish(topic: "quiet", text: "psst", tags: []) == 1, "skip lowers the accepted count") + expect(a!.received.count == 1, "a did not receive the skipped message") + expect(b!.received.count == 2 && b!.received[1].seq == 2 && b!.received[1].tags.isEmpty, + "b received the second message with empty tags") + + // AcceptAndStop from the first subscriber stops delivery to the rest. + let c = TestSubscriber() + expect(bus!.subscribe(subscriber: c) == 3, "third subscribe returns 3") + expect(bus!.publish(topic: "stop", text: "last", tags: ["z"]) == 1, "acceptAndStop delivers once") + expect(a!.received.count == 2 && a!.received[1].text == "last", "a took the stop message") + expect(b!.routed == ["news", "quiet"], "b was not asked after the stop (got \(b!.routed))") + expect(c.routed.isEmpty && c.received.isEmpty, "c was never reached") + + // --- messages(): iterator as a lazy Sequence ----------------------------- + expect(Array(bus!.messages()) == ["hello", "psst", "last"], "messages in order") + var joined: [String] = [] + for m in bus!.messages() { joined.append(m.uppercased()) } + expect(joined == ["HELLO", "PSST", "LAST"], "for-in over the sequence") + // Abandoning early releases the producer iterator through deinit. + expect(bus!.messages().first(where: { $0.hasPrefix("p") }) == "psst", "early exit") + + // --- lastMessage(): optional record -------------------------------------- + expect(bus!.lastMessage() == Message(seq: 3, topic: "stop", text: "last", tags: ["z"]), + "lastMessage fields") + + // --- publishLater: async wrapper resumed from a producer thread ---------- + let later = await bus!.publishLater(topic: "news", text: "later") + expect(later == 3, "publishLater accepted by all three (got \(later))") + expect(bus!.lastMessage()?.seq == 4, "async publish took seq 4") + expect(c.received.count == 1, "c received the async message") + + // --- The bus adopted in onAttached is the same object -------------------- + let kept = b!.attachedBus! + expect(kept.publish(topic: "via-kept", text: "same object", tags: []) == 3, + "publish through the kept wrapper reaches every subscriber") + expect(bus!.lastMessage()?.topic == "via-kept", "publish via kept wrapper visible through the original") + + // --- routeOnce: a callback passed to a free function is freed on return -- + weak var weakD: TestSubscriber? + do { + let d = TestSubscriber(skipTopic: "quiet") + weakD = d + expect(Events.routeOnce(subscriber: d, topic: "quiet") == .skip, "routeOnce skip") + expect(Events.routeOnce(subscriber: d, topic: "stop") == .acceptAndStop, "routeOnce acceptAndStop") + expect(Events.routeOnce(subscriber: d, topic: "other") == .accept, "routeOnce accept") + expect(d.attachedCount == 0, "routeOnce never attaches") + } + expect(weakD == nil, "routeOnce released its subscriber (free ran)") + + // --- clearSubscribers releases every subscriber -------------------------- + a = nil + b = nil + expect(weakA != nil && weakB != nil, "the bus retains a and b after the consumer drops them") + bus!.clearSubscribers() + expect(weakA == nil, "a freed by clearSubscribers") + expect(weakB == nil, "b (and the bus reference it kept) freed by clearSubscribers") + expect(bus!.publish(topic: "news", text: "nobody", tags: []) == 0, "publish with no subscribers") + + // --- Destroying the bus frees a retained subscriber ---------------------- + weak var weakE: TestSubscriber? + weak var weakBus2: EventBus? + do { + let bus2 = EventBus() + weakBus2 = bus2 + let e = TestSubscriber() + weakE = e + expect(bus2.subscribe(subscriber: e) == 1, "bus2 subscribe") + } + expect(weakBus2 == nil, "bus2 wrapper deinitialized") + expect(weakE == nil, "dropping the last bus reference freed its subscriber") + + bus = nil + expect(TestSubscriber.live == 1, "only c is still alive (got \(TestSubscriber.live))") +} +await run() +runTrapRouteChild() +expect(TestSubscriber.live == 0, "every subscriber was released (got \(TestSubscriber.live))") +assertNoLeaks() print("swift/events: OK") diff --git a/conformance/swift/kvstore.swift b/conformance/swift/kvstore.swift index 711c4ee1..e2ea5353 100644 --- a/conformance/swift/kvstore.swift +++ b/conformance/swift/kvstore.swift @@ -1,6 +1,6 @@ // Conformance consumer: kvstore sample, Swift target. // -// Binds through the generated `Kvstore` module and exercises the ABI 2 +// Binds through the generated `Kvstore` package and exercises the full // surface: `Store` as a reference-counted final class opened via the throwing // static factory `Store.open(path:)`, throwing methods raising the typed // `KvError` domain enum (put/get/delete/listKeys), non-throwing methods @@ -10,15 +10,17 @@ // `compact()` async method, the `EvictionListener` callback interface // implemented as a Swift class (arguments observed, detach by returning // false, replace/clear releasing the previous listener, a thrown error -// surfacing to the caller as `WeaveFFIError` with code -4), and the object -// graph: `share()` returning a wrapper to the same object, `fork()`, +// surfacing to the caller as `KvstoreRuntimeError` with code -4), and the +// object graph: `share()` returning a wrapper to the same object, `fork()`, // `larger(other:)` with `Store?` both ways, `describe(label:mirror:)` // returning a record that carries objects, `openMany` returning a list of // objects, and `totalCount` taking objects inside a list and a record. // Release is observed through weak references to the wrappers, whose deinit // calls the generated destroy. Typed-error asserts pin the case and the -// numeric code (keyNotFound 1001, expired 1002, ioError 1004). +// numeric code (keyNotFound 1001, expired 1002, ioError 1004). At exit, every +// native object, callback, iterator, token, and allocation has been released. +import CKvstore import Foundation import Kvstore @@ -31,6 +33,20 @@ func expect(_ cond: Bool, _ msg: String) { if !cond { fail(msg) } } +/// Every live-object counter the producer keeps must settle at zero. +func assertNoLeaks() { + let kinds = ["objects", "callbacks", "iterators", "cancel tokens", "allocations"] + var live: [UInt64] = [] + for _ in 0..<200 { + live = (0..<5).map { kvstore_debug_live(Int32($0)) } + if live.allSatisfy({ $0 == 0 }) { return } + usleep(10_000) + } + for (kind, n) in live.enumerated() where n != 0 { + fail("\(n) live \(kinds[kind]) at exit") + } +} + struct ListenerFailure: Error, LocalizedError { let key: String var errorDescription: String? { "listener rejected \(key)" } @@ -39,7 +55,7 @@ struct ListenerFailure: Error, LocalizedError { /// A consumer-side eviction listener. Records every (key, reason) pair, /// detaches itself by returning false once `keepAfter` evictions were seen, /// and throws for `failKey` so the foreign-error path can be observed. -final class Listener: EvictionListener { +final class Listener: EvictionListener, @unchecked Sendable { var evicted: [(key: String, reason: EvictionReason)] = [] var lastEntry: Entry? let keepAfter: Int @@ -184,7 +200,7 @@ do { do { _ = try store.delete(key: "boom") fail("expected a foreign error from the throwing listener") - } catch let e as WeaveFFIError { + } catch let e as KvstoreRuntimeError { expect(e.errorCode == -4, "foreign error code == -4 (got \(e.errorCode))") expect(e.localizedDescription.contains("listener rejected boom"), "foreign error carries the thrown message (got \(e.localizedDescription))") @@ -198,7 +214,7 @@ do { do { _ = try store.get(key: "boom") fail("expected a foreign error from the throwing listener on expiry") - } catch let e as WeaveFFIError { + } catch let e as KvstoreRuntimeError { expect(e.errorCode == -4, "expiry foreign error code == -4 (got \(e.errorCode))") } store.clearEvictionListener() @@ -334,7 +350,9 @@ do { } // No explicit close: every Store deinit calls the generated destroy. - print("swift/kvstore: OK") } catch { fail("threw: \(error)") } + +assertNoLeaks() +print("swift/kvstore: OK") diff --git a/conformance/swift/run.sh b/conformance/swift/run.sh new file mode 100755 index 00000000..43417783 --- /dev/null +++ b/conformance/swift/run.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Conformance lanes for the swift target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane compiles and runs one +# consumer against one sample and must exit 0. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +# Swift: a throwaway executable package depends on the generated SwiftPM +# package as-is through a local path dependency. The generated C module links +# the producer library by name, so the only extra input is the linker search +# path. The cdylib's install name is absolute, so it loads without an rpath. +swift_consumer() { + local sample="$1" src="$2" + local gen="$GENROOT/$sample/swift" + local pkg="$OUT/swift-$sample" + local cdir mod + cdir=$(ls -d "$gen"/Sources/C*/ | head -1) + mod=$(basename "$cdir") + mod=${mod#C} + rm -rf "$pkg" + mkdir -p "$pkg/Sources/conformance" + cp "$ROOT/conformance/swift/$src" "$pkg/Sources/conformance/main.swift" + cat > "$pkg/Package.swift" <&1 && .build/debug/conformance ) +} + +swift_contacts() { swift_consumer contacts contacts.swift; } + +swift_events() { swift_consumer events events.swift; } + +swift_kvstore() { swift_consumer kvstore kvstore.swift; } + +swift_async_demo() { swift_consumer async-demo async_demo.swift; } + +swift_codec() { swift_consumer codec codec.swift; } + +lane swift-contacts swift_contacts +lane swift-events swift_events +lane swift-kvstore swift_kvstore +lane swift-async-demo swift_async_demo +lane swift-codec swift_codec + +finish_lanes diff --git a/conformance/swift/shapes.swift b/conformance/swift/shapes.swift deleted file mode 100644 index 706f77ac..00000000 --- a/conformance/swift/shapes.swift +++ /dev/null @@ -1,69 +0,0 @@ -// Conformance consumer: shapes sample, Swift target. -// -// Binds through the generated `Shapes` module and drives the rich (algebraic) -// enum `Shape`, now a native Swift enum with associated values: unit variant -// (`.empty`), single payload (`.circle(radius:)`), multiple payloads -// (`.rectangle(width:height:)`), and mixed string + u8 payloads -// (`.labeled(label:count:)`). Variants are constructed locally (no factories, -// no `try`) and destructured with pattern matching; the value-buffer encoding -// is exercised by the free functions that take and return a `Shape` -// (`Shapes.describe(shape:)` and `Shapes.scale(shape:factor:)`, non-throwing -// and called without `try`) plus the expanded numerics (`sumBytes`: Data in, -// UInt64 out). Mirrors the C and C++ consumers; exits non-zero on any -// mismatch and prints `swift/shapes: OK` on success. - -import Foundation -import Shapes - -func fail(_ msg: String) -> Never { - FileHandle.standardError.write(Data("assertion failed: \(msg)\n".utf8)) - exit(1) -} - -func expect(_ cond: Bool, _ msg: String) { - if !cond { fail(msg) } -} - -// Empty (unit variant): tag only. `describe` proves the encoded tag reached -// the producer intact. -let empty = Shape.empty -guard case .empty = empty else { fail("empty variant") } -expect(Shapes.describe(shape: empty) == "empty", "describe empty") - -// Circle (f64 payload) round-trips through the producer and back. -let circle = Shape.circle(radius: 2.5) -guard case let .circle(radius) = circle else { fail("circle variant") } -expect(abs(radius - 2.5) < 1e-9, "circle radius (got \(radius))") - -// Rectangle (two f32 payloads). -let rect = Shape.rectangle(width: 3.0, height: 4.0) -guard case let .rectangle(width, height) = rect else { fail("rectangle variant") } -expect(abs(width - 3.0) < 1e-6, "rectangle width (got \(width))") -expect(abs(height - 4.0) < 1e-6, "rectangle height (got \(height))") - -// Labeled (string + u8 payload). -let labeled = Shape.labeled(label: "hex", count: 6) -guard case let .labeled(label, count) = labeled else { fail("labeled variant") } -expect(label == "hex", "labeled label (got \(label))") -expect(count == 6, "labeled count (got \(count))") - -// describe: the producer dispatches on the active variant; non-throwing, -// no `try`. -let desc = Shapes.describe(shape: circle) -expect(desc == "circle(r=2.5)", "describe (got \(desc))") - -// scale: rich enum in and out, decoded back into a Swift enum value. -let big = Shapes.scale(shape: circle, factor: 4.0) -guard case let .circle(bigRadius) = big else { fail("scaled variant (got \(big))") } -expect(abs(bigRadius - 10.0) < 1e-9, "scaled radius (got \(bigRadius))") - -// A multi-payload variant survives the round trip too. -let bigRect = Shapes.scale(shape: rect, factor: 2.0) -guard case let .rectangle(w2, h2) = bigRect else { fail("scaled rect variant (got \(bigRect))") } -expect(abs(w2 - 6.0) < 1e-6 && abs(h2 - 8.0) < 1e-6, "scaled rect (got \(w2)x\(h2))") - -// numerics: Data in, UInt64 out. -let total = Shapes.sumBytes(values: Data([250, 250, 250, 250])) -expect(total == 1000, "sum_bytes (got \(total))") - -print("swift/shapes: OK") diff --git a/conformance/wasm/async_demo.mjs b/conformance/wasm/async_demo.mjs deleted file mode 100644 index 67aebd84..00000000 --- a/conformance/wasm/async_demo.mjs +++ /dev/null @@ -1,117 +0,0 @@ -// Conformance consumer: async-demo sample, Wasm (wasm32-unknown-unknown) target. -// -// Drives the Promise-backed async surface of the generated ESM bindings end -// to end: `tasks.runTask` settled through the function-table callback -// trampoline and decoded from a value buffer into a plain object (i64 fields -// as BigInt), the typed rejection (InvalidName extending TaskError extending -// WeaveFFIError) for an empty name, the buffered list-of-records round trip -// through `runBatch`, the direct-scalar `runNTasks`, the sync `cancelTask`, -// and `activeCallbacks` settling to zero once every task body has completed. -// -// A wasm32-unknown-unknown build has no threads, so the runtime's default -// spawner drives each future to completion inline, inside the `_async` -// export: the completion trampoline has already fired, and the Promise is -// already settled, by the time the wrapper returns it. Nothing needs to be -// pumped; `await` only hops the microtask queue. This consumer pins that -// down (completion observed before `await`, ordering preserved, many -// concurrent Promises) while staying correct under a spawner that defers. -// -// Inputs come from the harness: -// WV_WASM: path to the compiled async_demo.wasm -// WV_JS: path to the generated weaveffi_wasm.js (ESM) -// Run with: node --experimental-wasm-type-reflection (for WebAssembly.Function). - -import fs from 'fs'; - -const WASM = process.env.WV_WASM; -const JS = process.env.WV_JS; -if (!WASM || !JS) { - console.error('WV_WASM and WV_JS must be set'); - process.exit(2); -} - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} - -globalThis.fetch = async (url) => ({ arrayBuffer: async () => fs.readFileSync(url) }); - -const mod = await import(JS); -const api = await mod.loadWeaveffiWasm(WASM); -const { WeaveFFIError, TaskError, InvalidName } = mod; -expect(typeof TaskError === 'function' && InvalidName.CODE === 1 && TaskError.InvalidName === InvalidName, 'TaskError domain exported'); - -// Async record return: the Promise resolves with a plain object whose i64 -// field decodes as BigInt. -const pending = api.tasks.runTask('alpha'); -expect(pending instanceof Promise, 'runTask returns a Promise'); -// Inline completion: the task body already ran before the wrapper returned. -expect(api.tasks.activeCallbacks() === 0n, 'inline completion: no callback in flight after launch'); -const result = await pending; -expect(typeof result.id === 'bigint' && result.id > 0n, 'runTask assigns an id (BigInt)'); -expect(result.value === 'completed: alpha', `runTask value (got ${result.value})`); -expect(result.success === true, 'runTask success flag'); - -// Typed async rejection: the empty name settles with the InvalidName subclass. -let rejected = null; -try { - await api.tasks.runTask(''); -} catch (e) { - rejected = e; -} -expect(rejected !== null, 'expected InvalidName for empty name'); -expect(rejected instanceof InvalidName, `typed subclass (got ${rejected && rejected.constructor.name})`); -expect(rejected instanceof TaskError && rejected instanceof WeaveFFIError, 'subclass of TaskError and the brand error'); -expect(rejected && rejected.code === 1, `InvalidName carries code 1 (got ${rejected && rejected.code})`); -// A rejection through `.catch` too (no await involved). -let caught = null; -await api.tasks.runTask('').catch((e) => { caught = e; }); -expect(caught instanceof InvalidName, 'rejection observable through .catch'); - -// The module keeps working after a rejection. -const beta = await api.tasks.runTask('beta'); -expect(beta.value === 'completed: beta' && beta.id > result.id, 'ids keep increasing after a rejection'); - -// Buffered list-of-records both ways. -const batch = await api.tasks.runBatch(['a', 'b', 'c']); -expect(Array.isArray(batch) && batch.length === 3, 'runBatch returns three records'); -expect( - batch.map((r) => r.value).join('|') === 'completed: a|completed: b|completed: c', - 'runBatch values' -); -expect(batch.every((r) => r.success && typeof r.id === 'bigint'), 'runBatch success flags and BigInt ids'); -expect((await api.tasks.runBatch([])).length === 0, 'runBatch of nothing'); -const unicodeBatch = await api.tasks.runBatch(['héllo ✓', '']); -expect(unicodeBatch[0].value === 'completed: héllo ✓', 'unicode names survive the async buffer'); -expect(unicodeBatch[1].value === 'completed: ' && unicodeBatch[1].success === true, 'runBatch does not validate names'); - -// Direct scalar through the async callback. -expect((await api.tasks.runNTasks(7)) === 7, 'runNTasks echoes n'); -expect((await api.tasks.runNTasks(0)) === 0, 'runNTasks(0)'); -expect((await api.tasks.runNTasks(-3)) === -3, 'runNTasks keeps the sign of an i32'); - -// Many launches at once: every Promise settles independently and in order. -const many = await Promise.all(Array.from({ length: 50 }, (_, i) => api.tasks.runTask('t' + i))); -expect(many.length === 50 && many.every((r, i) => r.value === 'completed: t' + i), 'fifty concurrent runTask calls settle with their own results'); -expect(many.every((r, i) => i === 0 || r.id > many[i - 1].id), 'ids assigned in launch order'); -const mixed = await Promise.allSettled([api.tasks.runTask('ok'), api.tasks.runTask(''), api.tasks.runNTasks(2)]); -expect(mixed[0].status === 'fulfilled' && mixed[1].status === 'rejected' && mixed[2].status === 'fulfilled', 'interleaved fulfilments and rejections'); -expect(mixed[1].reason instanceof InvalidName, 'interleaved rejection keeps its type'); - -// Sync functions beside the async ones. -expect(api.tasks.cancelTask(1) === false, 'cancelTask reports not cancelled'); -expect(api.tasks.cancelTask(1n) === false, 'cancelTask accepts a BigInt id'); -expect(typeof api.tasks.cancelTask(99) === 'boolean', 'cancelTask returns a boolean'); - -// Every spawned task body has completed by the time its callback fires. -expect(api.tasks.activeCallbacks() === 0n, 'activeCallbacks settles to zero'); - -if (failures > 0) { - console.error(`wasm/async-demo: ${failures} failure(s)`); - process.exit(1); -} -console.log('wasm/async-demo: OK'); diff --git a/conformance/wasm/codec.mjs b/conformance/wasm/codec.mjs deleted file mode 100644 index ef1ca869..00000000 --- a/conformance/wasm/codec.mjs +++ /dev/null @@ -1,443 +0,0 @@ -// Conformance consumer: codec sample, Wasm (wasm32-unknown-unknown) target. -// -// Checks the generated value-buffer codec (weaveffi_wasm.js) against the -// producer's round-trip oracle in both directions: `sample*` fixtures decoded -// field by field against concrete expected values (producer encodes, consumer -// decodes), `verify*` accepting the same fixture back (consumer encodes, -// producer decodes), `roundtrip*` returning consumer-built values with edge -// cases (empty strings, lists, and maps; unicode; i64/u64 extremes as BigInt, -// both by value across the wasm boundary and inside buffers; NaN, +/-Infinity, -// and -0 doubles; every Shape variant), the typed `Mismatch` error, and -// objects inside buffers through `Holder` (a Token field, an optional Token, -// a list of Tokens; `primaryOf` returning a wrapper to the same object; -// `samePrimary`; every wrapper released with an idempotent `close()`). -// -// Inputs come from the harness: -// WV_WASM: path to the compiled codec.wasm -// WV_JS: path to the generated weaveffi_wasm.js (ESM) -// Run with: node --experimental-wasm-type-reflection (for WebAssembly.Function). - -import fs from 'fs'; -import assert from 'node:assert'; - -const WASM = process.env.WV_WASM; -const JS = process.env.WV_JS; -if (!WASM || !JS) { - console.error('WV_WASM and WV_JS must be set'); - process.exit(2); -} - -// Node has no file:// fetch; shim it so the generated loader can read the .wasm. -globalThis.fetch = async (url) => ({ arrayBuffer: async () => fs.readFileSync(url) }); - -const mod = await import(JS); -const api = await mod.loadWeaveffiWasm(WASM); -const wv = api.codec; -const { WeaveFFIError, CodecError, Mismatch, Color } = mod; -const { Token } = wv; - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} -// Structural equality with SameValue semantics for primitives: BigInt by -// value, NaN equal to NaN, -0 distinct from +0, Uint8Array by contents. -function same(actual, expected, msg) { - try { - assert.deepStrictEqual(actual, expected); - } catch (e) { - console.error('assertion failed: ' + msg); - console.error(e.message); - failures++; - } -} -function throwsMismatch(fn, msg) { - try { - fn(); - expect(false, msg + ': expected Mismatch'); - } catch (e) { - expect(e instanceof Mismatch, msg + ': Mismatch instance (got ' + (e && e.constructor.name) + ')'); - expect(e instanceof CodecError, msg + ': extends CodecError'); - expect(e instanceof WeaveFFIError, msg + ': extends WeaveFFIError'); - expect(e.code === 1 && Mismatch.CODE === 1 && CodecError.Mismatch === Mismatch, msg + ': code 1'); - } -} -const u8 = (...bytes) => new Uint8Array(bytes); - -// The C-style enum is a frozen object of declared values. -expect(Color.Red === 0 && Color.Green === 1 && Color.Blue === 7, 'Color values'); -expect(Object.isFrozen(Color), 'Color is frozen'); -expect(typeof Token === 'function', 'Token class exposed on api.codec'); - -// --- Scalars ---------------------------------------------------------------- -const canonicalScalars = { - i8_value: -8, - u8_value: 200, - i16_value: -16000, - u16_value: 60000, - i32_value: -2000000000, - u32_value: 4000000000, - i64_value: -9007199254740993n, - u64_value: 18446744073709551615n, - f32_value: 1.5, - f64_value: -2.25e100, - flag: true, - color: Color.Blue, -}; -const scalars = wv.sampleScalars(); -same(scalars, canonicalScalars, 'sampleScalars matches the canonical fixture'); -expect(typeof scalars.i64_value === 'bigint', 'i64 field is a BigInt'); -expect(typeof scalars.u64_value === 'bigint', 'u64 field is a BigInt'); -expect(typeof scalars.i32_value === 'number' && typeof scalars.u32_value === 'number', '32-bit fields are numbers'); -expect(typeof scalars.flag === 'boolean', 'bool field is a boolean'); -expect(scalars.i64_value === -9007199254740993n, 'i64 beyond 2^53 is exact'); -expect(scalars.u64_value === 18446744073709551615n, 'u64::MAX is exact'); -expect(wv.verifyScalars(scalars) === true, 'verifyScalars accepts the decoded fixture'); -expect(wv.verifyScalars(canonicalScalars) === true, 'verifyScalars accepts a hand-built fixture'); -same(wv.roundtripScalars(scalars), canonicalScalars, 'roundtripScalars preserves every field'); -throwsMismatch(() => wv.verifyScalars({ ...canonicalScalars, u64_value: 18446744073709551614n }), 'u64 off by one'); -throwsMismatch(() => wv.verifyScalars({ ...canonicalScalars, i64_value: -9007199254740992n }), 'i64 off by one'); -throwsMismatch(() => wv.verifyScalars({ ...canonicalScalars, flag: false }), 'flag flipped'); -throwsMismatch(() => wv.verifyScalars({ ...canonicalScalars, color: Color.Red }), 'color changed'); -throwsMismatch(() => wv.verifyScalars({ ...canonicalScalars, f32_value: 1.5000001 }), 'f32 changed'); - -// Consumer-built extremes, including doubles JS can express but JSON can't. -const extremes = { - i8_value: -128, - u8_value: 255, - i16_value: -32768, - u16_value: 65535, - i32_value: -2147483648, - u32_value: 4294967295, - i64_value: -9223372036854775808n, - u64_value: 0n, - f32_value: -0, - f64_value: NaN, - flag: false, - color: Color.Red, -}; -const extremesBack = wv.roundtripScalars(extremes); -same(extremesBack, extremes, 'extreme scalars round-trip'); -expect(Object.is(extremesBack.f32_value, -0), 'f32 -0 keeps its sign'); -expect(Number.isNaN(extremesBack.f64_value), 'f64 NaN survives'); -const maxes = { - ...extremes, - i8_value: 127, - i16_value: 32767, - i32_value: 2147483647, - i64_value: 9223372036854775807n, - u64_value: 18446744073709551615n, - f32_value: Infinity, - f64_value: -Infinity, - color: Color.Green, -}; -same(wv.roundtripScalars(maxes), maxes, 'max scalars round-trip'); -// A number for a 64-bit field is accepted by the encoder (BigInt(v)). -same( - wv.roundtripScalars({ ...extremes, i64_value: 42, u64_value: 7 }), - { ...extremes, i64_value: 42n, u64_value: 7n }, - 'number literals widen to BigInt on the way back' -); -// A non-integral number can't become a BigInt: the encoder throws before -// anything crosses the boundary. -try { - wv.roundtripScalars({ ...extremes, i64_value: 1.5 }); - expect(false, 'fractional i64 should throw'); -} catch (e) { - expect(e instanceof RangeError, `fractional i64 is a RangeError (got ${e && e.constructor.name})`); -} - -// Direct-family 64-bit params and returns are BigInt end to end: wasm i64 -// maps to BigInt in both directions with no precision loss. -for (const v of [0n, 1n, -1n, 9007199254740993n, -9007199254740993n, 9223372036854775807n, -9223372036854775808n]) { - const back = wv.roundtripI64(v); - expect(typeof back === 'bigint' && back === v, `roundtripI64(${v}) (got ${back})`); -} -expect(wv.roundtripI64(5) === 5n, 'roundtripI64 accepts a number and returns a BigInt'); -expect(wv.roundtripI64(-5) === -5n, 'roundtripI64 accepts a negative number'); -for (const v of [0n, 1n, 9223372036854775808n, 18446744073709551615n]) { - const back = wv.roundtripU64(v); - expect(typeof back === 'bigint' && back === v, `roundtripU64(${v}) (got ${back})`); -} -// Out-of-range BigInts follow the JS-to-wasm i64 coercion (ToBigInt64, -// modulo 2^64) rather than throwing; pin that down so a change is visible. -expect(wv.roundtripI64(9223372036854775808n) === -9223372036854775808n, 'i64 wraps modulo 2^64 at the wasm boundary'); -expect(wv.roundtripU64(-1n) === 18446744073709551615n, 'u64 wraps modulo 2^64 at the wasm boundary'); -try { - wv.roundtripI64(1.5); - expect(false, 'fractional i64 param should throw'); -} catch (e) { - expect(e instanceof RangeError, `fractional i64 param is a RangeError (got ${e && e.constructor.name})`); -} -try { - wv.roundtripI64('nope'); - expect(false, 'non-numeric i64 param should throw'); -} catch (e) { - expect(e instanceof SyntaxError, `non-numeric i64 param is a SyntaxError (got ${e && e.constructor.name})`); -} - -// Doubles by value: wasm f64 is a JS number, so every bit pattern survives. -for (const v of [0, -0, 1.5, -2.25e100, 5e-324, Number.MAX_VALUE, Number.MIN_VALUE, Number.EPSILON, Infinity, -Infinity]) { - expect(Object.is(wv.roundtripF64(v), v), `roundtripF64(${v})`); -} -expect(Number.isNaN(wv.roundtripF64(NaN)), 'roundtripF64(NaN)'); -expect(wv.roundtripBool(true) === true && wv.roundtripBool(false) === false, 'roundtripBool'); -expect(typeof wv.roundtripBool(true) === 'boolean', 'roundtripBool returns a boolean, not an i32'); -expect(wv.roundtripColor(Color.Blue) === 7, 'roundtripColor(Blue) == 7'); -expect(wv.roundtripColor(Color.Red) === 0, 'roundtripColor(Red) == 0'); -expect(wv.roundtripColor(Color.Green) === Color.Green, 'roundtripColor(Green)'); - -// Strings and bytes at the top level: NUL-terminated UTF-8 in, owned -// C string out (freed by the glue), and byte buffers as Uint8Array. -for (const s of ['', 'ascii', 'héllo wörld ✓', '日本語', 'emoji 🎉 pair', 'a'.repeat(70000), '\u{10FFFF}']) { - expect(wv.roundtripString(s) === s, `roundtripString(${JSON.stringify(s.slice(0, 20))}...)`); -} -const allBytes = new Uint8Array(Array.from({ length: 256 }, (_, i) => i)); -const allBack = wv.roundtripBytes(allBytes); -expect(allBack instanceof Uint8Array, 'roundtripBytes returns a Uint8Array'); -same(allBack, allBytes, 'roundtripBytes(0..255)'); -const emptyBytes = wv.roundtripBytes(new Uint8Array(0)); -expect(emptyBytes instanceof Uint8Array && emptyBytes.length === 0, 'roundtripBytes(empty) is an empty Uint8Array'); -same(wv.roundtripBytes([1, 2, 3]), u8(1, 2, 3), 'roundtripBytes accepts a plain array'); -same(wv.roundtripBytes(Buffer.from([7, 8])), u8(7, 8), 'roundtripBytes accepts a Buffer'); -const big = new Uint8Array(200000); -for (let i = 0; i < big.length; i++) big[i] = (i * 31) & 0xff; -same(wv.roundtripBytes(big), big, 'roundtripBytes(200000 bytes) (memory growth)'); -// The returned copy is detached from linear memory: mutating it doesn't -// affect later calls, and it survives memory growth. -const copy = wv.roundtripBytes(u8(1, 2, 3)); -copy[0] = 99; -same(wv.roundtripBytes(u8(1, 2, 3)), u8(1, 2, 3), 'returned bytes are an owned copy'); - -// Optionals and maps at the top level. -expect(wv.roundtripOptI64(null) === null, 'roundtripOptI64(null)'); -expect(wv.roundtripOptI64(undefined) === null, 'roundtripOptI64(undefined) is null'); -expect(wv.roundtripOptI64(-9223372036854775808n) === -9223372036854775808n, 'roundtripOptI64(i64::MIN)'); -expect(wv.roundtripOptI64(9223372036854775807n) === 9223372036854775807n, 'roundtripOptI64(i64::MAX)'); -expect(wv.roundtripOptI64(0n) === 0n, 'roundtripOptI64(0n) is present'); -expect(wv.roundtripOptI64(0) === 0n, 'roundtripOptI64(0) is present (a number widens)'); -same(wv.roundtripMap({}), {}, 'roundtripMap({})'); -same( - wv.roundtripMap({ a: 1n, '': -2n, 'ключ': 9223372036854775807n, z: -9223372036854775808n }), - { a: 1n, '': -2n, 'ключ': 9223372036854775807n, z: -9223372036854775808n }, - 'roundtripMap with odd keys and extremes' -); -same(wv.roundtripMap({ n: 3 }), { n: 3n }, 'roundtripMap widens number values to BigInt'); - -// --- Shape (rich enum) ------------------------------------------------------ -const shapeCases = [ - { tag: 'Empty' }, - { tag: 'Circle', radius: 2.5 }, - { tag: 'Circle', radius: -0 }, - { tag: 'Circle', radius: NaN }, - { tag: 'Circle', radius: -Infinity }, - { tag: 'Rect', width: 1.0, height: 0.5 }, - { tag: 'Rect', width: -0, height: Infinity }, - { tag: 'Labeled', label: 'tag', count: 3 }, - { tag: 'Labeled', label: '', count: -2147483648 }, - { tag: 'Labeled', label: 'ünïcödé ✓', count: 2147483647 }, - { tag: 'Nested', inner: canonicalScalars, note: 'n' }, - { tag: 'Nested', inner: extremes, note: null }, - { tag: 'Nested', inner: maxes, note: '' }, -]; -for (const s of shapeCases) { - same(wv.roundtripShape(s), s, `roundtripShape(${JSON.stringify(s, (_, v) => (typeof v === 'bigint' ? v.toString() : v)).slice(0, 60)})`); -} -same(wv.roundtripShapes(shapeCases), shapeCases, 'roundtripShapes(all variants)'); -same(wv.roundtripShapes([]), [], 'roundtripShapes([])'); -expect(wv.describeShape({ tag: 'Empty' }) === 'Empty', 'describeShape(Empty)'); -expect(wv.describeShape({ tag: 'Circle', radius: 2.5 }) === 'Circle { radius: 2.5 }', `describeShape(Circle) (got ${wv.describeShape({ tag: 'Circle', radius: 2.5 })})`); -expect(wv.describeShape({ tag: 'Labeled', label: 'tag', count: 3 }) === 'Labeled { label: "tag", count: 3 }', 'describeShape(Labeled)'); -const nestedBack = wv.roundtripShape({ tag: 'Nested', inner: extremes, note: null }); -expect(Number.isNaN(nestedBack.inner.f64_value) && nestedBack.note === null, 'nested NaN and absent note'); -expect(Object.is(nestedBack.inner.f32_value, -0), 'nested f32 -0 keeps its sign'); -// An unknown tag is rejected by the encoder before anything is sent. -try { - wv.roundtripShape({ tag: 'Hexagon' }); - expect(false, 'unknown tag should throw'); -} catch (e) { - expect(e instanceof WeaveFFIError && e.code === -3, `unknown tag is a marshalling error (got ${e && e.code})`); - expect(e.message.includes('Hexagon'), 'unknown tag error names the tag'); -} -// The module keeps working after a consumer-side encode failure. -same(wv.roundtripShape({ tag: 'Empty' }), { tag: 'Empty' }, 'roundtripShape works after an encode failure'); - -// --- Composite -------------------------------------------------------------- -const canonicalComposite = { - name: 'héllo wörld ✓', - blob: u8(0, 1, 2, 253, 254, 255), - some_i64: -9223372036854775808n, - none_i64: null, - some_text: '', - names: ['a', '', 'ccc'], - matrix: [[1, 2, 3], [], [-4]], - empty: [], - by_name: { one: 1n, two: 2n, neg: -3n }, - by_id: { '-1': canonicalScalars, '42': { ...canonicalScalars, flag: false } }, - scalars: canonicalScalars, - shape: { tag: 'Labeled', label: 'tag', count: 3 }, - shapes: [ - { tag: 'Empty' }, - { tag: 'Circle', radius: 2.5 }, - { tag: 'Rect', width: 1.0, height: 0.5 }, - { tag: 'Labeled', label: '', count: -1 }, - { tag: 'Nested', inner: canonicalScalars, note: 'n' }, - ], - maybe_shape: { tag: 'Nested', inner: canonicalScalars, note: null }, - maybe_list: u8(9, 8), - sparse: [true, null, false], - colors: [Color.Red, Color.Green, Color.Blue], -}; -const composite = wv.sampleComposite(); -same(composite, canonicalComposite, 'sampleComposite matches the canonical fixture'); -expect(composite.blob instanceof Uint8Array, 'blob is a Uint8Array'); -expect(composite.maybe_list instanceof Uint8Array, 'maybe_list is a Uint8Array'); -expect(typeof composite.some_i64 === 'bigint', 'some_i64 is a BigInt'); -expect(composite.none_i64 === null, 'absent optional is null'); -expect(composite.some_text === '', 'present empty optional string is ""'); -expect(typeof composite.by_name.one === 'bigint', 'map values are BigInt'); -expect(composite.by_id[42].flag === false && composite.by_id[-1].flag === true, 'i32-keyed map indexes by number'); -expect(composite.sparse[1] === null && composite.sparse[0] === true, 'list of optionals'); -expect(wv.verifyComposite(composite) === true, 'verifyComposite accepts the decoded fixture'); -expect(wv.verifyComposite(canonicalComposite) === true, 'verifyComposite accepts a hand-built fixture'); -same(wv.roundtripComposite(composite), canonicalComposite, 'roundtripComposite preserves every field'); -const described = wv.describeComposite(composite); -expect( - typeof described === 'string' && described.startsWith('Composite {') && described.includes('name: "héllo wörld ✓"'), - `describeComposite renders (got ${described.slice(0, 40)}...)` -); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, sparse: [true, true, false] }), 'sparse changed'); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, none_i64: 0n }), 'absent optional made present'); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, by_name: { one: 1n, two: 2n } }), 'map entry missing'); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, name: 'hello world' }), 'unicode name changed'); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, blob: u8(0, 1, 2, 253, 254) }), 'blob shortened'); -throwsMismatch(() => wv.verifyComposite({ ...canonicalComposite, maybe_shape: null }), 'optional shape removed'); -// The module keeps working after a typed error. -expect(wv.verifyComposite(composite) === true, 'verifyComposite works after Mismatch errors'); - -// A consumer-built composite with edge values in every position. -const edge = { - name: '', - blob: new Uint8Array(0), - some_i64: 9223372036854775807n, - none_i64: -1n, - some_text: null, - names: [], - matrix: [[], [-2147483648, 2147483647]], - empty: [NaN, -0, Infinity, -Infinity, 5e-324], - by_name: {}, - by_id: { '-2147483648': extremes, '0': maxes, '2147483647': canonicalScalars }, - scalars: extremes, - shape: { tag: 'Empty' }, - shapes: [], - maybe_shape: null, - maybe_list: null, - sparse: [null, null], - colors: [], -}; -const edgeBack = wv.roundtripComposite(edge); -same(edgeBack, edge, 'edge composite round-trips'); -expect(Number.isNaN(edgeBack.empty[0]) && Object.is(edgeBack.empty[1], -0), 'NaN and -0 inside a list'); -expect(edgeBack.by_id[-2147483648].i64_value === -9223372036854775808n, 'i32::MIN map key with i64::MIN value'); -expect(edgeBack.by_id[0].u64_value === 18446744073709551615n, 'u64::MAX inside a map value'); -const bigList = { - ...edge, - names: Array.from({ length: 1000 }, (_, i) => 'name-' + i), - blob: allBytes, - maybe_list: u8(255), - sparse: Array.from({ length: 333 }, (_, i) => (i % 3 === 0 ? null : i % 3 === 1)), - by_name: Object.fromEntries(Array.from({ length: 500 }, (_, i) => ['k' + i, BigInt(i) * 1000000007n])), -}; -same(wv.roundtripComposite(bigList), bigList, 'large composite round-trips (writer growth)'); - -// --- Holder: objects inside buffers ----------------------------------------- -const holder = wv.makeHolder(10n, true); -expect(holder.primary instanceof Token, 'holder.primary is a Token'); -expect(holder.primary._handle > 0, 'holder.primary wraps a live handle'); -expect(holder.primary.value() === 10n, `primary value 10n (got ${holder.primary.value()})`); -expect(holder.spare instanceof Token && holder.spare.value() === 11n, 'spare is Token(11)'); -expect(Array.isArray(holder.many) && holder.many.length === 3, 'many has 3 tokens'); -same(holder.many.map((t) => t.value()), [12n, 13n, 14n], 'many values 12..14'); -expect(new Set([holder.primary, holder.spare, ...holder.many].map((t) => t._handle)).size === 5, 'five distinct objects'); -expect(wv.sumHolder(holder) === 60n, `sumHolder == 60n (got ${wv.sumHolder(holder)})`); -// Encoding clones each handle, so the wrappers are still usable afterwards. -expect(holder.primary.value() === 10n && holder.many[2].value() === 14n, 'tokens usable after sumHolder'); -expect(wv.sumHolder(holder) === 60n, 'sumHolder again (fresh clones each call)'); - -// primaryOf returns a new wrapper over the same underlying object. -const primary = wv.primaryOf(holder); -expect(primary instanceof Token && primary !== holder.primary, 'primaryOf returns a distinct wrapper'); -expect(primary._handle === holder.primary._handle, 'primaryOf points at the same object'); -expect(primary.value() === 10n, 'primaryOf value 10n'); -expect(wv.samePrimary(holder, { primary, spare: null, many: [] }) === true, 'primaryOf aliases holder.primary'); -expect(wv.samePrimary(holder, holder) === true, 'samePrimary(holder, holder)'); -const other = wv.makeHolder(10n, true); -expect(wv.samePrimary(holder, other) === false, 'distinct holders with equal values are different objects'); -expect(wv.sumHolder(other) === 60n, 'other sums to 60n as well'); - -const noSpare = wv.makeHolder(-5n, false); -expect(noSpare.spare === null, 'spare absent is null'); -expect(noSpare.primary.value() === -5n, 'negative base'); -expect(wv.sumHolder(noSpare) === -5n + -3n + -2n + -1n, 'sumHolder without spare'); -const wrapped = wv.makeHolder(9223372036854775807n, true); -expect(wrapped.primary.value() === 9223372036854775807n, 'i64::MAX base'); -expect(wrapped.spare.value() === -9223372036854775808n, 'spare wraps to i64::MIN'); - -// Consumer-constructed tokens in every buffered position. -const t1 = new Token(100n); -const t2 = new Token(200); -const t3 = new Token(-9223372036854775808n); -expect(t1.value() === 100n && t2.value() === 200n && t3.value() === -9223372036854775808n, 'constructed token values'); -const mine = { primary: t1, spare: t2, many: [t3, t1, t1] }; -expect(wv.sumHolder(mine) === 100n + 200n + -9223372036854775808n + 100n + 100n, 'sumHolder over consumer tokens'); -expect(wv.sumHolder({ primary: t1, spare: null, many: [] }) === 100n, 'sumHolder with no spare and empty many'); -expect(wv.sumHolder({ primary: t1, spare: undefined, many: undefined }) === 100n, 'undefined spare and many are absent/empty'); -expect(wv.samePrimary(mine, { primary: t1, spare: null, many: [] }) === true, 'same consumer token in two holders'); -expect(wv.samePrimary(mine, { primary: t2, spare: null, many: [] }) === false, 'different consumer tokens'); -const primaryMine = wv.primaryOf(mine); -expect(primaryMine._handle === t1._handle && primaryMine.value() === 100n, 'primaryOf a consumer holder'); -expect(t1.value() === 100n, 't1 alive after being sent three times and returned once'); - -// Release everything: close is idempotent, and a closed token can't be -// encoded (borrow after close is -3) or used. -const everyToken = [ - holder.primary, holder.spare, ...holder.many, primary, - other.primary, other.spare, ...other.many, - noSpare.primary, ...noSpare.many, - wrapped.primary, wrapped.spare, ...wrapped.many, - t1, t2, t3, primaryMine, -]; -for (const t of everyToken) t.close(); -for (const t of everyToken) expect(t._handle === 0, 'closed token is zeroed'); -for (const t of everyToken) t.close(); -try { - holder.primary.value(); - expect(false, 'expected throw for use after close'); -} catch (e) { - expect(e instanceof WeaveFFIError && e.code === -3, `use after close is code -3 (got ${e && e.code})`); -} -try { - wv.sumHolder(holder); - expect(false, 'expected throw for encoding a closed object'); -} catch (e) { - expect(e instanceof WeaveFFIError && e.code === -3, `closed object in a buffer is code -3 (got ${e && e.code})`); -} -// The module is unaffected by those consumer-side failures. -const fresh = wv.makeHolder(1n, false); -expect(wv.sumHolder(fresh) === 1n + 3n + 4n + 5n, 'module usable after closed-object errors'); -for (const t of [fresh.primary, ...fresh.many]) t.close(); -const disposeSym = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); -const disposable = new Token(1n); -expect(typeof disposable[disposeSym] === 'function', 'Token implements Symbol.dispose'); -disposable[disposeSym](); -expect(disposable._handle === 0, 'Symbol.dispose zeroes the handle'); -disposable[disposeSym](); - -if (failures > 0) { - console.error(`wasm/codec: ${failures} failure(s)`); - process.exit(1); -} -console.log('wasm/codec: OK'); diff --git a/conformance/wasm/contacts.mjs b/conformance/wasm/contacts.mjs deleted file mode 100644 index 3768d975..00000000 --- a/conformance/wasm/contacts.mjs +++ /dev/null @@ -1,151 +0,0 @@ -// Conformance consumer: contacts sample, Wasm (wasm32-unknown-unknown) target. -// -// Drives the generated ESM bindings (loadWeaveffiWasm) against the real -// producer compiled to wasm. Exercises the original struct/enum/optional -// surface: the reference-counted `ContactBook` interface class (canonical -// `new` constructor, instance methods passing the handle as the implicit self -// argument, idempotent `close()` and `Symbol.dispose` releasing the wrapper's -// reference, use after close as a -3 error), records decoded from value buffers -// into plain objects (`Contact` with its i64 id as BigInt, optional email, -// and enum-as-int contact_type), NUL-terminated string args, the buffered -// `string?` parameter (null email), list-of-record returns, the bool return, -// and the typed error domain (`InvalidName` / `NotFound` subclasses of -// `ContactsError` with stable codes, thrown by `throws` wrappers). -// -// Inputs come from the harness: -// WV_WASM: path to the compiled contacts.wasm -// WV_JS: path to the generated weaveffi_wasm.js (ESM) -// Run with: node --experimental-wasm-type-reflection (for WebAssembly.Function). - -import fs from 'fs'; - -const WASM = process.env.WV_WASM; -const JS = process.env.WV_JS; -if (!WASM || !JS) { - console.error('WV_WASM and WV_JS must be set'); - process.exit(2); -} - -// Node has no file:// fetch; shim it so the generated loader can read the .wasm. -globalThis.fetch = async (url) => ({ arrayBuffer: async () => fs.readFileSync(url) }); - -const mod = await import(JS); -const api = await mod.loadWeaveffiWasm(WASM); - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} - -const ContactType = mod.ContactType; -expect(ContactType && ContactType.Work === 1, 'enum ContactType exported'); - -// Typed error surface: module-scope classes with the domain hierarchy. -const { WeaveFFIError, ContactsError, InvalidName, NotFound } = mod; -expect(typeof WeaveFFIError === 'function', 'WeaveFFIError exported'); -expect(typeof ContactsError === 'function', 'ContactsError exported'); -expect(InvalidName.CODE === 1, 'InvalidName.CODE === 1'); -expect(NotFound.CODE === 2, 'NotFound.CODE === 2'); -expect(ContactsError.NotFound === NotFound, 'per-code class aliased on the domain'); - -// The interface class hangs off the module object. -const ContactBook = api.contacts.ContactBook; -expect(typeof ContactBook === 'function', 'ContactBook class exposed on api.contacts'); - -// Canonical `new` constructor returns a wrapped owned handle. -const book = new ContactBook(); -expect(book instanceof ContactBook, 'new -> instanceof ContactBook'); -expect(book._handle > 0, 'new -> non-null handle'); -expect(book.count() === 0, 'fresh book empty'); - -// add() returns the stored record decoded into a plain object; the i64 id -// crosses as BigInt and the enum as its integer discriminant. -const alice = book.add('Alice', 'Smith', 'alice@example.com', ContactType.Work); -expect(typeof alice === 'object' && alice !== null, 'add returns an object'); -expect(typeof alice.id === 'bigint' && alice.id > 0n, 'alice.id positive (i64)'); - -const c = book.get(alice.id); -expect(c.first_name === 'Alice', 'first_name (got ' + c.first_name + ')'); -expect(c.last_name === 'Smith', 'last_name'); -expect(c.email === 'alice@example.com', 'email'); -expect(c.contact_type === ContactType.Work, 'contact_type'); - -// Optional string: a missing email round-trips as null. -const bob = book.add('Bob', 'Jones', null, ContactType.Personal); -const cb = book.get(bob.id); -expect(cb.email === null, 'bob email null (got ' + cb.email + ')'); -expect(cb.contact_type === ContactType.Personal, 'bob contact_type'); - -expect(book.count() === 2, 'count == 2'); - -const everyone = book.list(); -expect(Array.isArray(everyone) && everyone.length === 2, 'list length == 2'); -const names = everyone.map((p) => p.first_name).sort(); -expect(names[0] === 'Alice' && names[1] === 'Bob', 'list names'); - -expect(book.remove(alice.id) === true, 'remove returns true'); -expect(book.remove(alice.id) === false, 'second remove returns false'); -expect(book.count() === 1, 'count == 1 after remove'); - -// Each ContactBook object owns its own state. -const other = new ContactBook(); -expect(other.count() === 0, 'fresh second book empty'); -expect(book.count() === 1, 'first book unaffected'); - -// Typed errors: a missing id throws the NotFound class (code 2), an empty -// name the InvalidName class (code 1); both are instances of the domain -// class and the generic brand. -let getErr = null; -try { book.get(9999); } catch (e) { getErr = e; } -expect(getErr instanceof NotFound, 'get missing -> instanceof NotFound'); -expect(getErr instanceof ContactsError, 'get missing -> instanceof ContactsError'); -expect(getErr instanceof WeaveFFIError, 'get missing -> instanceof WeaveFFIError'); -expect(getErr && getErr.code === 2, 'get missing -> code 2'); -expect(getErr && typeof getErr.message === 'string' && getErr.message.length > 0, 'get missing -> has message'); - -let addErr = null; -try { book.add('', 'Nameless', null, ContactType.Other); } catch (e) { addErr = e; } -expect(addErr instanceof InvalidName, 'empty name -> instanceof InvalidName'); -expect(addErr instanceof ContactsError, 'empty name -> instanceof ContactsError'); -expect(addErr && addErr.code === 1, 'empty name -> code 1'); -expect(book.count() === 1, 'failed add leaves count == 1'); - -// A number id widens to the i64 BigInt the ABI expects. -expect(book.get(bob.id).first_name === 'Bob', 'get by BigInt id'); -expect(book.get(Number(bob.id)).first_name === 'Bob', 'get by number id widens to i64'); - -// Disposal: close() releases this wrapper's reference exactly once; later -// calls are no-ops, and any use of the closed wrapper is a -3 error. -other.close(); -expect(other._handle === 0, 'close() zeroes the handle'); -other.close(); -book.close(); -book.close(); -let closedErr = null; -try { book.count(); } catch (e) { closedErr = e; } -expect(closedErr instanceof WeaveFFIError && closedErr.code === -3, `use after close is code -3 (got ${closedErr && closedErr.code})`); -expect(!(closedErr instanceof ContactsError), 'use after close is not a domain error'); - -// Symbol.dispose is the same release path (for `using` declarations). -const disposeSym = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); -const disposable = new ContactBook(); -disposable.add('Carol', 'King', null, ContactType.Other); -expect(typeof disposable[disposeSym] === 'function', 'wrapper implements Symbol.dispose'); -disposable[disposeSym](); -expect(disposable._handle === 0, 'Symbol.dispose zeroes the handle'); -disposable[disposeSym](); - -// The module keeps working after use-after-close errors. -const again = new ContactBook(); -expect(again.count() === 0 && again.add('Dan', 'Lee', 'dan@example.com', ContactType.Work).id > 0n, 'fresh book after closes'); -again.close(); - -if (failures === 0) { - console.log('wasm/contacts: OK'); -} else { - console.error(`wasm/contacts: ${failures} failure(s)`); - process.exit(1); -} diff --git a/conformance/wasm/events.mjs b/conformance/wasm/events.mjs deleted file mode 100644 index a0005b1f..00000000 --- a/conformance/wasm/events.mjs +++ /dev/null @@ -1,319 +0,0 @@ -// Conformance consumer: events sample, Wasm (wasm32-unknown-unknown) target. -// -// Drives the ABI 2 callback-interface surface of the generated ESM glue -// (loadWeaveffiWasm): a `Subscriber` implemented as a plain JS object whose -// methods the producer reaches through function-table trampolines (route -> -// Delivery, onMessage -> running count as BigInt, onAttached receiving the -// EventBus object the producer hands over), the reference-counted `EventBus` -// class (constructor, methods, idempotent `close()`, `Symbol.dispose`, -// use-after-close trap), the `Delivery` return steering `publish`'s accepted -// count, the Promise-backed `publishLater` (the default wasm32 spawner drives -// the future inline, so the Promise is settled before `await`), the lazy -// iterable `messages()`, the nullable `lastMessage()` record, the free -// function `routeOnce`, and subscribers that throw: wasm32 has no unwinding, -// so the runtime records the failure, the producer runs to completion on the -// callback's default return, the glue refuses any further callback during that -// call, and the wrapper throws a WeaveFFIError with code -4 carrying the JS -// message, leaving the bus usable. -// -// Inputs come from the harness: -// WV_WASM: path to the compiled events.wasm -// WV_JS: path to the generated weaveffi_wasm.js (ESM) -// Run with: node --experimental-wasm-type-reflection (for WebAssembly.Function). - -import fs from 'fs'; - -const WASM = process.env.WV_WASM; -const JS = process.env.WV_JS; -if (!WASM || !JS) { - console.error('WV_WASM and WV_JS must be set'); - process.exit(2); -} - -// Node has no file:// fetch; shim it so the generated loader can read the .wasm. -globalThis.fetch = async (url) => ({ arrayBuffer: async () => fs.readFileSync(url) }); - -const mod = await import(JS); -const api = await mod.loadWeaveffiWasm(WASM); - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} - -const { WeaveFFIError, Delivery } = mod; -const { EventBus } = api.events; -expect(typeof WeaveFFIError === 'function', 'WeaveFFIError exported'); -expect(typeof EventBus === 'function', 'EventBus class exposed on api.events'); -expect(Delivery.Accept === 0 && Delivery.Skip === 1 && Delivery.AcceptAndStop === 2, 'Delivery enum values'); - -// A subscriber as a class: `route` steers delivery per topic, `onMessage` -// records what it saw and returns its running count (an i64, so a BigInt), -// and `onAttached` adopts the bus object the producer hands over. -class RecordingSubscriber { - constructor(name, opts) { - this.name = name; - this.skipTopic = (opts && opts.skip) || null; - this.stopTopic = (opts && opts.stop) || null; - this.throwTopic = (opts && opts.throwOn) || null; - this.routed = []; - this.received = []; - this.attachedBuses = []; - this.attachedCounts = []; - } - route(topic) { - this.routed.push(topic); - if (topic === this.throwTopic) { - throw new Error('subscriber ' + this.name + ' rejects ' + topic); - } - if (topic === this.skipTopic) return Delivery.Skip; - if (topic === this.stopTopic) return Delivery.AcceptAndStop; - return Delivery.Accept; - } - onMessage(message) { - this.received.push(message); - return BigInt(this.received.length); - } - onAttached(bus) { - // The object is usable right away (a re-entrant call into the module - // while `subscribe` is on the stack): `subscribe` attaches before it - // appends, so the count seen here is the pre-subscribe count. - expect(bus instanceof EventBus, this.name + ': onAttached receives an EventBus'); - expect(bus._handle > 0, this.name + ': adopted bus has a live handle'); - this.attachedCounts.push(bus.subscriberCount()); - this.attachedBuses.push(bus); - } -} - -const bus = new EventBus(); -expect(bus instanceof EventBus, 'constructor returns an EventBus'); -expect(bus._handle > 0, 'constructor adopts a non-null handle'); -expect(bus.subscriberCount() === 0n, 'fresh bus has no subscribers'); -expect(bus.lastMessage() === null, 'lastMessage is null before any publish'); -expect([...bus.messages()].length === 0, 'messages() empty before any publish'); - -// subscribe() returns the new subscriber count and calls onAttached with -// the bus object (one strong reference transferred to the consumer). -const a = new RecordingSubscriber('a', { skip: 'quiet', stop: 'stop' }); -const b = new RecordingSubscriber('b'); -expect(bus.subscribe(a) === 1n, 'subscribe a returns 1n'); -expect(bus.subscribe(b) === 2n, 'subscribe b returns 2n'); -expect(bus.subscriberCount() === 2n, 'subscriberCount == 2n'); -expect(a.attachedBuses.length === 1, 'a attached once'); -expect(a.attachedCounts[0] === 0n, `a saw 0 subscribers at attach (got ${a.attachedCounts[0]})`); -expect(b.attachedCounts[0] === 1n, `b saw 1 subscriber at attach (got ${b.attachedCounts[0]})`); -// The adopted wrapper is a distinct reference to the same producer object. -expect(a.attachedBuses[0] !== bus, 'adopted wrapper is a distinct JS object'); -expect(a.attachedBuses[0]._handle === bus._handle, 'adopted wrapper points at the same object'); - -// Both accept: two deliveries, one Message each with every field decoded. -const n1 = bus.publish('news', 'hello', ['x', 'y']); -expect(typeof n1 === 'bigint', 'publish returns a BigInt'); -expect(n1 === 2n, `publish(news) accepted by 2 (got ${n1})`); -expect(a.received.length === 1 && b.received.length === 1, 'both received news'); -const m = a.received[0]; -expect(m.seq === 1n, `message seq is 1n (got ${m.seq})`); -expect(m.topic === 'news', 'message topic'); -expect(m.text === 'hello', 'message text'); -expect( - Array.isArray(m.tags) && m.tags.length === 2 && m.tags[0] === 'x' && m.tags[1] === 'y', - `message tags (got ${JSON.stringify(m.tags)})` -); -expect(a.routed.length === 1 && a.routed[0] === 'news', 'route called with topic'); - -// Skip steers the count: a skips "quiet", only b accepts. -const n2 = bus.publish('quiet', 'psst', []); -expect(n2 === 1n, `publish(quiet) accepted by 1 (got ${n2})`); -expect(a.received.length === 1, 'a skipped quiet'); -expect(b.received.length === 2 && b.received[1].seq === 2n, 'b received quiet with seq 2n'); -expect(b.received[1].tags.length === 0, 'empty tags list decodes as []'); - -// AcceptAndStop: a accepts and the bus stops before reaching b. -const n3 = bus.publish('stop', 'last', ['z']); -expect(n3 === 1n, `publish(stop) accepted by 1 (got ${n3})`); -expect(a.received.length === 2 && a.received[1].text === 'last', 'a received stop'); -expect(b.received.length === 2, 'b not reached after AcceptAndStop'); -expect(b.routed.length === 2, 'b.route not consulted after stop'); - -// The adopted bus is the same underlying object: it observes the same log. -const viaA = a.attachedBuses[0]; -expect(viaA.subscriberCount() === 2n, 'adopted bus sees both subscribers'); -const viaAMsgs = [...viaA.messages()]; -expect( - viaAMsgs.join('|') === 'hello|psst|last', - `adopted bus shares the log (got ${JSON.stringify(viaAMsgs)})` -); -const lastViaA = viaA.lastMessage(); -expect(lastViaA !== null && lastViaA.seq === 3n && lastViaA.text === 'last', 'adopted bus lastMessage'); -// Releasing the adopted references (twice each) leaves the original alive. -for (const adopted of a.attachedBuses.concat(b.attachedBuses)) { - adopted.close(); - expect(adopted._handle === 0, 'close() zeroes the adopted handle'); - adopted.close(); -} -expect(bus.subscriberCount() === 2n, 'bus alive after adopted wrappers closed'); -expect(bus.publish('news', 'still', []) === 2n, 'bus still delivers after adopted wrappers closed'); - -// Async publish: the wasm32 default spawner drives the future inline, so the -// subscribers have already run when the Promise is handed back; `await` -// only hops the microtask queue. -const later = bus.publishLater('news', 'later'); -expect(later instanceof Promise, 'publishLater returns a Promise'); -expect(a.received.length === 4 && a.received[3].text === 'later', 'inline completion: a received before await'); -const n4 = await later; -expect(n4 === 2n, `publishLater accepted by 2 (got ${n4})`); -expect(b.received.length === 4 && b.received[3].seq === 5n, 'b received the async message'); -expect(a.received[3].tags.length === 0, 'publishLater sends no tags'); - -// Iterator and nullable record. -const msgs = [...bus.messages()]; -expect( - msgs.join('|') === 'hello|psst|last|still|later', - `messages() in order (got ${JSON.stringify(msgs)})` -); -const last = bus.lastMessage(); -expect(last !== null && typeof last === 'object', 'lastMessage present'); -expect(last.seq === 5n && last.topic === 'news' && last.text === 'later', 'lastMessage fields'); -expect(Array.isArray(last.tags) && last.tags.length === 0, 'lastMessage tags empty'); - -// Lazy iteration: one producer step per next(); early return() closes the -// producer iterator without draining, and for...of break does the same. -const it = bus.messages()[Symbol.iterator](); -const first = it.next(); -expect(!first.done && first.value === 'hello', 'lazy first element'); -expect(typeof it.return === 'function', 'iterator has return()'); -it.return(); -expect(it.next().done === true, 'iterator done after return()'); -let seen = 0; -for (const text of bus.messages()) { - seen++; - if (text === 'psst') break; -} -expect(seen === 2, `for...of break after two steps (got ${seen})`); - -// A subscriber whose route throws. There is no unwinding on wasm32, so the -// producer's `publish` keeps running on route's default return (0 = Accept) -// and would call `onMessage` next; the glue refuses that call with the same -// foreign error, and the thunk reports code -4 with the thrown message in -// place of the result. -const bad = new RecordingSubscriber('bad', { throwOn: 'boom' }); -expect(bus.subscribe(bad) === 3n, 'subscribe bad returns 3n'); -let foreign = null; -try { - bus.publish('boom', 'x', []); -} catch (e) { - foreign = e; -} -expect(foreign !== null, 'publish throws when a subscriber throws'); -expect(foreign instanceof WeaveFFIError, `foreign error is a WeaveFFIError (got ${foreign && foreign.constructor.name})`); -expect(!(foreign instanceof WebAssembly.RuntimeError), 'no trap leaks out of the call'); -expect(foreign && foreign.code === -4, `foreign error code is -4 (got ${foreign && foreign.code})`); -expect( - foreign && typeof foreign.message === 'string' && foreign.message.includes('subscriber bad rejects boom'), - `foreign error carries the JS message (got ${JSON.stringify(foreign && foreign.message)})` -); -// a and b ran before bad and still accepted; the bus is intact and keeps -// working (no lock is held across the callback, so nothing stays locked). -expect(a.received.length === 5 && b.received.length === 5, 'earlier subscribers still delivered'); -expect(bus.subscriberCount() === 3n, 'bus intact after foreign error'); -expect(bus.publish('ok', 'y', []) === 3n, 'publish works again after foreign error'); -expect(bad.received.length === 1 && bad.received[0].text === 'y', `throwing subscriber still subscribed and was not consulted again after failing (received ${bad.received.map((m) => m.text).join(',')})`); -expect(bus.lastMessage().text === 'y', 'lastMessage works after foreign error'); - -// The same through the async path: the failure is recorded during the inline -// launch, and the Promise rejects with code -4. -let asyncForeign = null; -try { - await bus.publishLater('boom', 'z'); -} catch (e) { - asyncForeign = e; -} -expect(asyncForeign instanceof WeaveFFIError && asyncForeign.code === -4, `async foreign error code -4 (got ${asyncForeign && asyncForeign.code})`); -expect(asyncForeign && asyncForeign.message.includes('rejects boom'), 'async foreign error carries the JS message'); -expect((await bus.publishLater('ok', 'again')) === 3n, 'publishLater works again after the rejection'); - -// A non-BigInt return from onMessage (an i64 slot) fails the coercion inside -// the trampoline, which is a foreign error too. -const weird = { route: () => Delivery.Accept, onMessage: () => undefined, onAttached: () => {} }; -expect(bus.subscribe(weird) === 4n, 'subscribe weird returns 4n'); -let badReturn = null; -try { - bus.publish('any', 'w', []); -} catch (e) { - badReturn = e; -} -expect(badReturn instanceof WeaveFFIError && badReturn.code === -4, `bad return type is code -4 (got ${badReturn && badReturn.code})`); -expect(bus.subscriberCount() === 4n, 'bus intact after bad return'); - -// routeOnce: a free function taking the callback interface; no bus involved. -const solo = new RecordingSubscriber('solo', { skip: 'quiet' }); -expect(api.events.routeOnce(solo, 'quiet') === Delivery.Skip, 'routeOnce(quiet) == Skip'); -expect(api.events.routeOnce(solo, 'news') === Delivery.Accept, 'routeOnce(news) == Accept'); -expect(solo.routed.length === 2 && solo.attachedBuses.length === 0, 'routeOnce calls route only'); -// A plain object literal works as an implementation too. -const literal = { route: () => Delivery.AcceptAndStop, onMessage: () => 1n, onAttached: () => {} }; -expect(api.events.routeOnce(literal, 'x') === Delivery.AcceptAndStop, 'object literal subscriber'); -// A throwing route through the free function is the same -4. -let onceErr = null; -try { - api.events.routeOnce(new RecordingSubscriber('once', { throwOn: 't' }), 't'); -} catch (e) { - onceErr = e; -} -expect(onceErr instanceof WeaveFFIError && onceErr.code === -4, `routeOnce foreign error is -4 (got ${onceErr && onceErr.code})`); - -// clearSubscribers drops every retained subscriber (each `free(ctx)` entry -// runs synchronously); publishing then accepts nothing. -bus.clearSubscribers(); -expect(bus.subscriberCount() === 0n, 'subscriberCount == 0n after clear'); -expect(bus.publish('news', 'nobody', []) === 0n, 'publish with no subscribers accepts 0'); -expect( - a.received.length === 9 && b.received.length === 9 && bad.received.length === 3, - `cleared subscribers no longer receive (got ${a.received.length}/${b.received.length}/${bad.received.length})` -); -const all = [...bus.messages()]; -expect(all.length === 11, `log records every publish, aborted ones included (got ${all.length})`); -expect(all[5] === 'x' && all[7] === 'z' && all[9] === 'w', 'aborted publishes were logged before delivery'); - -// Idempotent close; use after close is a marshalling error (-3), including -// through the async path. -bus.close(); -expect(bus._handle === 0, 'close() zeroes the handle'); -bus.close(); -let closedErr = null; -try { - bus.subscriberCount(); -} catch (e) { - closedErr = e; -} -expect(closedErr instanceof WeaveFFIError && closedErr.code === -3, `use after close is code -3 (got ${closedErr && closedErr.code})`); -let closedAsync = null; -try { - await bus.publishLater('x', 'y'); -} catch (e) { - closedAsync = e; -} -expect(closedAsync instanceof WeaveFFIError && closedAsync.code === -3, `async use after close is code -3 (got ${closedAsync && closedAsync.code})`); - -// Symbol.dispose releases the same way (twice is safe). -const disposeSym = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); -const disposable = new EventBus(); -expect(typeof disposable[disposeSym] === 'function', 'wrapper implements Symbol.dispose'); -disposable[disposeSym](); -expect(disposable._handle === 0, 'Symbol.dispose zeroes the handle'); -disposable[disposeSym](); - -// A bus that still holds subscribers when its last reference is released -// frees them without calling back into JS in a broken way. -const shortLived = new EventBus(); -shortLived.subscribe(new RecordingSubscriber('short')); -shortLived.close(); - -if (failures > 0) { - console.error(`wasm/events: ${failures} failure(s)`); - process.exit(1); -} -console.log('wasm/events: OK'); diff --git a/conformance/wasm/kvstore.mjs b/conformance/wasm/kvstore.mjs deleted file mode 100644 index 15acf96f..00000000 --- a/conformance/wasm/kvstore.mjs +++ /dev/null @@ -1,379 +0,0 @@ -// Conformance consumer: kvstore sample, Wasm (wasm32-unknown-unknown) target. -// -// Drives the generated ESM bindings (loadWeaveffiWasm) against the real -// producer compiled to wasm. Exercises the ABI 2 surface end to end: the -// reference-counted `Store` interface class (static `open` factory, instance -// methods, idempotent `close()`, use-after-close trap, `Symbol.dispose`), the -// typed `KvError` domain (subclasses with stable codes thrown by `throws` -// wrappers, including through a rejected Promise), objects travelling every -// way the schema allows (`share()` returning a wrapper to the same object, -// `fork()` returning a new one, `Store?` in and out of `larger`, a record that -// carries an object and a nullable object from `describe`, a list of objects -// from `openMany`, a list of objects and an optional record holding an object -// into `totalCount`), records decoded from value buffers into plain objects -// (`Entry` with its bytes, optional-scalar, list, and map fields; `Stats` -// from the kv.stats submodule), buffered optional parameters (`i64?` TTL, -// `string?` prefix), a lazy iterator-backed string stream (listKeys), the -// Promise-backed `compact` (the wasm32 default spawner drives the future -// inline), and the `EvictionListener` callback interface implemented as a -// JS object reached through function-table trampolines: `Deleted` and -// `Expired` evictions, detaching by returning false, replacement, and a -// listener that throws, whose abort reaches JS as a translated -4 error while -// the store stays usable. -// -// Inputs come from the harness: -// WV_WASM: path to the compiled kvstore.wasm -// WV_JS: path to the generated weaveffi_wasm.js (ESM) -// Run with: node --experimental-wasm-type-reflection (for WebAssembly.Function). - -import fs from 'fs'; - -const WASM = process.env.WV_WASM; -const JS = process.env.WV_JS; -if (!WASM || !JS) { - console.error('WV_WASM and WV_JS must be set'); - process.exit(2); -} - -// Node has no file:// fetch; shim it so the generated loader can read the .wasm. -globalThis.fetch = async (url) => ({ arrayBuffer: async () => fs.readFileSync(url) }); - -const mod = await import(JS); -const api = await mod.loadWeaveffiWasm(WASM); - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} - -const { WeaveFFIError, KvError, KeyNotFound, Expired, StoreFull, IoError, EntryKind, EvictionReason } = mod; -const { Store } = api.kv; -const { getStats } = api.kv.stats; -expect(typeof Store === 'function', 'Store class exposed on api.kv'); -expect(typeof getStats === 'function', 'kv.stats.getStats exposed as a nested namespace'); -expect(typeof WeaveFFIError === 'function' && typeof KvError === 'function', 'error classes exported'); - -// The error domain: a hierarchy with stable codes, reachable both as named -// exports and as statics on the domain class. -expect(Object.getPrototypeOf(KvError) === WeaveFFIError, 'KvError extends WeaveFFIError'); -expect(KeyNotFound.CODE === 1001 && KvError.KeyNotFound === KeyNotFound, 'KeyNotFound code 1001'); -expect(Expired.CODE === 1002 && KvError.Expired === Expired, 'Expired code 1002'); -expect(StoreFull.CODE === 1003 && KvError.StoreFull === StoreFull, 'StoreFull code 1003'); -expect(IoError.CODE === 1004 && KvError.IoError === IoError, 'IoError code 1004'); -const probe = new KeyNotFound(); -expect(probe instanceof KvError && probe instanceof WeaveFFIError && probe instanceof Error, 'error prototype chain'); -expect(probe.code === 1001 && probe.message === 'WeaveFFI error 1001: key not found', `default message from the schema doc (got ${JSON.stringify(probe.message)})`); -expect(EntryKind.Volatile === 0 && EntryKind.Persistent === 1 && EntryKind.Encrypted === 2, 'EntryKind values'); -expect(EvictionReason.Deleted === 0 && EvictionReason.Expired === 1, 'EvictionReason values'); -expect(Object.isFrozen(EntryKind) && Object.isFrozen(EvictionReason), 'enums are frozen'); - -// Fallible factory: an empty path is the typed IoError. -let openErr = null; -try { - Store.open(''); -} catch (e) { - openErr = e; -} -expect(openErr instanceof IoError, `Store.open('') throws IoError (got ${openErr && openErr.constructor.name})`); -expect(openErr && openErr.code === 1004 && openErr instanceof KvError, 'IoError has code 1004 and is a KvError'); -expect(openErr && openErr.message === 'WeaveFFI error 1004: I/O failure', `IoError carries the code and schema message (got ${JSON.stringify(openErr && openErr.message)})`); - -const store = Store.open('/tmp/conformance.db'); -expect(store instanceof Store, 'open returns a Store'); -expect(store._handle > 0, 'open adopts a non-null handle'); -expect(Store.defaultCapacity() === 1000000n, `defaultCapacity is 1000000n (got ${Store.defaultCapacity()})`); -expect(store.count() === 0n, 'fresh store is empty'); - -// put / get with every Entry field decoded. -const enc = new TextEncoder(); -expect(store.put('alpha', enc.encode('one'), EntryKind.Persistent, null) === true, 'put alpha'); -expect(store.put('beta', enc.encode('two'), EntryKind.Volatile, 3600n) === true, 'put beta with TTL'); -expect(store.put('alpha', enc.encode('uno'), EntryKind.Persistent, undefined) === true, 'replace alpha'); -expect(store.count() === 2n, `count is 2n (got ${store.count()})`); -const alpha = store.get('alpha'); -expect(alpha !== null && typeof alpha === 'object', 'get returns a plain record'); -expect(typeof alpha.id === 'bigint' && alpha.id === 3n, `entry id is the third generated id (got ${alpha.id})`); -expect(alpha.key === 'alpha', 'entry key'); -expect(alpha.value instanceof Uint8Array && new TextDecoder().decode(alpha.value) === 'uno', 'entry value bytes'); -expect(typeof alpha.created_at === 'bigint' && alpha.created_at > 0n, 'created_at populated'); -expect(alpha.expires_at === null, 'no TTL decodes as null expires_at'); -expect(Array.isArray(alpha.tags) && alpha.tags.length === 0, 'tags is an empty list'); -expect(alpha.metadata !== null && typeof alpha.metadata === 'object' && Object.keys(alpha.metadata).length === 0, 'metadata is an empty map'); -const beta = store.get('beta'); -expect(beta.expires_at !== null && beta.expires_at === beta.created_at + 3600n, `TTL decodes into expires_at (got ${beta.expires_at})`); -expect(beta.id === 2n, 'beta kept its id'); - -// Typed errors from a method: missing key, then the same via delete's bool. -let notFound = null; -try { - store.get('missing'); -} catch (e) { - notFound = e; -} -expect(notFound instanceof KeyNotFound && notFound.code === 1001, `get(missing) throws KeyNotFound (got ${notFound && notFound.constructor.name})`); -expect(notFound && notFound.message.endsWith('key not found'), `KeyNotFound carries the schema message (got ${JSON.stringify(notFound && notFound.message)})`); -expect(store.delete('missing') === false, 'delete(missing) is false, not an error'); - -// Iterators: sorted, prefix-filtered, lazy, with early exit. -expect(store.put('alpine', enc.encode('x'), EntryKind.Persistent, null), 'put alpine'); -expect([...store.listKeys(null)].join(',') === 'alpha,alpine,beta', `listKeys(null) sorted (got ${[...store.listKeys(null)]})`); -expect([...store.listKeys(undefined)].join(',') === 'alpha,alpine,beta', 'listKeys(undefined) is the same as null'); -expect([...store.listKeys('alp')].join(',') === 'alpha,alpine', `listKeys('alp') (got ${[...store.listKeys('alp')]})`); -expect([...store.listKeys('zzz')].length === 0, 'listKeys with no match is empty'); -const keysIt = store.listKeys(null)[Symbol.iterator](); -expect(keysIt.next().value === 'alpha', 'lazy first key'); -keysIt.return(); -expect(keysIt.next().done === true, 'iterator done after return()'); -let stepped = 0; -for (const _k of store.listKeys(null)) { - stepped++; - if (stepped === 2) break; -} -expect(stepped === 2, 'for...of break'); - -// Stats from the nested submodule take the object as a borrowed parameter. -const stats = getStats(store); -expect(stats.total_entries === 3n, `total_entries 3n (got ${stats.total_entries})`); -expect(stats.total_bytes === 7n, `total_bytes 7n (got ${stats.total_bytes})`); -expect(stats.expired_entries === 0n, 'no expired entries'); - -// share(): a second wrapper to the same producer object. fork(): a copy. -const shared = store.share(); -expect(shared instanceof Store && shared !== store, 'share returns a distinct wrapper'); -expect(shared._handle === store._handle, 'share points at the same object'); -expect(shared.count() === 3n, 'shared sees the same entries'); -expect(shared.put('gamma', enc.encode('g'), EntryKind.Volatile, null), 'put through shared'); -expect(store.count() === 4n, 'original observes the put through shared'); -const forked = store.fork(); -expect(forked instanceof Store && forked._handle !== store._handle, 'fork returns a new object'); -expect(forked.count() === 4n, 'fork copies live entries'); -expect(forked.put('delta', enc.encode('d'), EntryKind.Volatile, null), 'put into fork'); -expect(forked.count() === 5n && store.count() === 4n, 'fork is independent'); -expect(forked.get('gamma').id === store.get('gamma').id, 'fork preserved ids'); -shared.close(); -shared.close(); -expect(shared._handle === 0, 'closed shared wrapper is zeroed'); -expect(store.count() === 4n, 'original alive after the shared wrapper is closed'); - -// Nullable objects both ways. -const empty = Store.open('/tmp/empty.db'); -expect(empty.larger(null) === null, 'larger(null) on an empty store is null'); -expect(empty.larger(undefined) === null, 'larger(undefined) same as null'); -const bigger = store.larger(forked); -expect(bigger instanceof Store && bigger._handle === forked._handle, 'larger returns the other when it has more'); -const own = store.larger(empty); -expect(own instanceof Store && own._handle === store._handle, 'larger returns self when it has more'); -const alone = store.larger(null); -expect(alone instanceof Store && alone._handle === store._handle, 'larger(null) returns self when non-empty'); -expect(bigger.count() === 5n && own.count() === 4n, 'returned wrappers are usable'); -bigger.close(); -own.close(); -alone.close(); -expect(store.count() === 4n && forked.count() === 5n, 'closing returned wrappers keeps the originals alive'); - -// A record carrying an object and a nullable object. -const info = store.describe('primary', forked); -expect(info.label === 'primary', 'describe label'); -expect(info.store instanceof Store && info.store._handle === store._handle, 'record.store is a wrapper to self'); -expect(info.mirror instanceof Store && info.mirror._handle === forked._handle, 'record.mirror is the passed object'); -expect(info.count === 4n, `record.count (got ${info.count})`); -const infoNoMirror = empty.describe('secondary', null); -expect(infoNoMirror.mirror === null, 'absent mirror decodes as null'); -expect(infoNoMirror.store._handle === empty._handle && infoNoMirror.count === 0n, 'secondary record'); - -// A list of objects out, a list of objects and an optional record in. -const many = Store.openMany(['/tmp/a.db', '/tmp/b.db', '/tmp/c.db']); -expect(Array.isArray(many) && many.length === 3, `openMany returns three stores (got ${many && many.length})`); -expect(many.every((s) => s instanceof Store && s._handle > 0), 'each opened store is a live wrapper'); -expect(new Set(many.map((s) => s._handle)).size === 3, 'opened stores are distinct objects'); -expect(many[0].put('k', enc.encode('v'), EntryKind.Persistent, null), 'put into many[0]'); -expect(many[1].put('k', enc.encode('v'), EntryKind.Persistent, null), 'put into many[1]'); -expect(Store.totalCount(many, null) === 2n, `totalCount(many, null) is 2n (got ${Store.totalCount(many, null)})`); -expect(Store.totalCount([], null) === 0n, 'totalCount of nothing'); -expect(Store.totalCount([store, forked], info) === 13n, `totalCount with an extra record (got ${Store.totalCount([store, forked], info)})`); -expect(Store.totalCount([store, forked], { label: 'x', store: empty, mirror: null, count: 0 }) === 9n, 'extra record with a null mirror and a number count'); -let openManyErr = null; -try { - Store.openMany(['/tmp/ok.db', '']); -} catch (e) { - openManyErr = e; -} -expect(openManyErr instanceof IoError, 'openMany propagates the typed error'); -expect(store.count() === 4n && forked.count() === 5n && empty.count() === 0n, 'stores alive after being marshalled as parameters'); -for (const s of many) s.close(); -info.store.close(); -info.mirror.close(); -infoNoMirror.store.close(); -expect(store.count() === 4n && forked.count() === 5n && empty.count() === 0n, 'stores alive after closing every borrowed wrapper'); - -// Eviction listener: a JS object with `onEvict(entry, reason) -> bool`. -class Listener { - constructor(name) { - this.name = name; - this.evictions = []; - this.keepGoing = true; - this.throwOn = null; - } - onEvict(entry, reason) { - this.evictions.push({ key: entry.key, reason, id: entry.id }); - if (entry.key === this.throwOn) { - throw new Error('listener ' + this.name + ' refuses ' + entry.key); - } - return this.keepGoing; - } -} -const listener = new Listener('first'); -store.setEvictionListener(listener); -expect(store.delete('alpine') === true, 'delete alpine'); -expect(listener.evictions.length === 1, 'delete notified the listener'); -expect(listener.evictions[0].key === 'alpine' && listener.evictions[0].reason === EvictionReason.Deleted, 'Deleted eviction carries the entry'); -expect(typeof listener.evictions[0].id === 'bigint', 'entry inside the callback is fully decoded'); -// An already-expired entry is evicted on read: Expired error plus notification. -expect(store.put('stale', enc.encode('old'), EntryKind.Volatile, -1n), 'put stale with negative TTL'); -expect(getStats(store).expired_entries === 1n, 'stats count the expired entry before eviction'); -let expired = null; -try { - store.get('stale'); -} catch (e) { - expired = e; -} -expect(expired instanceof Expired && expired.code === 1002, `get(stale) throws Expired (got ${expired && expired.constructor.name})`); -expect(listener.evictions.length === 2 && listener.evictions[1].key === 'stale' && listener.evictions[1].reason === EvictionReason.Expired, 'Expired eviction notified'); -expect(store.count() === 3n, 'expired entry evicted'); -// Returning false detaches the listener (the producer frees it). -listener.keepGoing = false; -expect(store.delete('gamma') === true, 'delete gamma'); -expect(listener.evictions.length === 3, 'listener saw the eviction that detached it'); -expect(store.put('tmp', enc.encode('t'), EntryKind.Volatile, null) && store.delete('tmp'), 'delete after detach'); -expect(listener.evictions.length === 3, 'detached listener no longer notified'); -// Replacement: the second listener takes over. -const second = new Listener('second'); -const third = new Listener('third'); -store.setEvictionListener(second); -store.setEvictionListener(third); -expect(store.put('tmp2', enc.encode('t'), EntryKind.Volatile, null) && store.delete('tmp2'), 'delete after replacement'); -expect(second.evictions.length === 0 && third.evictions.length === 1, 'only the current listener is notified'); -// A plain object literal works too, and clearEvictionListener detaches it. -let literalHits = 0; -store.setEvictionListener({ onEvict: () => { literalHits++; return true; } }); -expect(store.put('tmp3', enc.encode('t'), EntryKind.Volatile, null) && store.delete('tmp3'), 'delete with literal listener'); -expect(literalHits === 1, 'object-literal listener notified'); -store.clearEvictionListener(); -expect(store.put('tmp4', enc.encode('t'), EntryKind.Volatile, null) && store.delete('tmp4'), 'delete after clear'); -expect(literalHits === 1, 'cleared listener no longer notified'); -store.clearEvictionListener(); - -// A throwing listener: no unwinding on wasm32, so the runtime records the -// failure, `delete` runs to completion, and the thunk reports the foreign -// error (-4) carrying the JS message instead of the result. The entry was -// already removed and the store keeps working. -const thrower = new Listener('thrower'); -thrower.throwOn = 'doomed'; -store.setEvictionListener(thrower); -expect(store.put('doomed', enc.encode('x'), EntryKind.Volatile, null), 'put doomed'); -let foreign = null; -try { - store.delete('doomed'); -} catch (e) { - foreign = e; -} -expect(foreign instanceof WeaveFFIError, `throwing listener surfaces a WeaveFFIError (got ${foreign && foreign.constructor.name})`); -expect(!(foreign instanceof WebAssembly.RuntimeError) && !(foreign instanceof KvError), 'foreign error is neither a trap nor a domain error'); -expect(foreign && foreign.code === -4, `foreign error code is -4 (got ${foreign && foreign.code})`); -expect(foreign && foreign.message.includes('listener thrower refuses doomed'), `foreign error carries the JS message (got ${JSON.stringify(foreign && foreign.message)})`); -expect(thrower.evictions.length === 1, 'listener ran once'); -expect(store.count() === 2n, `store intact after foreign error (got ${store.count()})`); -let afterNotFound = null; -try { - store.get('doomed'); -} catch (e) { - afterNotFound = e; -} -expect(afterNotFound instanceof KeyNotFound, 'entry was removed before the listener threw'); -expect(store.put('again', enc.encode('y'), EntryKind.Persistent, null) && store.get('again').key === 'again', 'store usable after foreign error'); -// Without unwinding the producer observed `onEvict`'s default return (false, -// "detach me") after the throw, so unlike a native build the listener is no -// longer attached. This is the one place the abort-build semantics differ. -expect(store.delete('again') === true && thrower.evictions.length === 1, `throwing listener was detached by its default return on wasm32 (evictions ${thrower.evictions.length})`); -store.setEvictionListener(thrower); -expect(store.put('again2', enc.encode('y'), EntryKind.Persistent, null) && store.delete('again2') === true && thrower.evictions.length === 2, 'listener re-attached and delivering after the foreign error'); -// Expired path through the same throwing listener. -thrower.throwOn = 'stale2'; -expect(store.put('stale2', enc.encode('z'), EntryKind.Volatile, -5n), 'put stale2'); -let foreignExpired = null; -try { - store.get('stale2'); -} catch (e) { - foreignExpired = e; -} -expect(foreignExpired instanceof WeaveFFIError && foreignExpired.code === -4, `foreign error on the expiry path is -4 (got ${foreignExpired && foreignExpired.code})`); -expect([...store.listKeys(null)].join(',') === 'alpha,beta', `keys after the foreign errors (got ${[...store.listKeys(null)]})`); -store.clearEvictionListener(); - -// Async compact: the wasm32 default spawner completes inline, so the Promise -// is already settled when returned; `await` just observes it. -expect(store.put('exp1', enc.encode('12345'), EntryKind.Volatile, -10n), 'put exp1'); -expect(store.put('exp2', enc.encode('678'), EntryKind.Volatile, -10n), 'put exp2'); -expect(getStats(store).expired_entries === 2n, 'two expired entries before compact'); -const compacting = store.compact(); -expect(compacting instanceof Promise, 'compact returns a Promise'); -expect(getStats(store).expired_entries === 0n, 'inline completion: compacted before await'); -const reclaimed = await compacting; -expect(reclaimed === 8n, `compact reclaims 8n bytes (got ${reclaimed})`); -expect((await store.compact()) === 0n, 'second compact reclaims nothing'); -expect(store.count() === 2n, 'live entries untouched by compact'); - -// Deprecated method still works (documented as @deprecated in the glue). -expect(store.legacyPut('legacy', enc.encode('l')) === true, 'legacyPut'); -expect(store.get('legacy').expires_at === null, 'legacyPut stores a non-expiring entry'); -store.clear(); -expect(store.count() === 0n && [...store.listKeys(null)].length === 0, 'clear empties the store'); - -// Idempotent close; use after close is a marshalling error (-3), including -// through the Promise path. -forked.close(); -forked.close(); -empty.close(); -store.close(); -expect(store._handle === 0, 'close() zeroes the handle'); -store.close(); -let closedErr = null; -try { - store.count(); -} catch (e) { - closedErr = e; -} -expect(closedErr instanceof WeaveFFIError && closedErr.code === -3, `use after close is code -3 (got ${closedErr && closedErr.code})`); -let closedAsync = null; -try { - await store.compact(); -} catch (e) { - closedAsync = e; -} -expect(closedAsync instanceof WeaveFFIError && closedAsync.code === -3, `async use after close is code -3 (got ${closedAsync && closedAsync.code})`); -let closedStats = null; -try { - getStats(store); -} catch (e) { - closedStats = e; -} -expect(closedStats instanceof WeaveFFIError && closedStats.code === -3, 'borrowing a closed object as a parameter is code -3'); - -// Symbol.dispose releases the same way (twice is safe); a store closed while -// a listener is attached frees that listener. -const disposeSym = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); -const disposable = Store.open('/tmp/disposable.db'); -disposable.setEvictionListener(new Listener('dangling')); -expect(typeof disposable[disposeSym] === 'function', 'wrapper implements Symbol.dispose'); -disposable[disposeSym](); -expect(disposable._handle === 0, 'Symbol.dispose zeroes the handle'); -disposable[disposeSym](); - -if (failures > 0) { - console.error(`wasm/kvstore: ${failures} failure(s)`); - process.exit(1); -} -console.log('wasm/kvstore: OK'); diff --git a/conformance/wasm/run.sh b/conformance/wasm/run.sh new file mode 100755 index 00000000..f7bb954c --- /dev/null +++ b/conformance/wasm/run.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Conformance lanes for the wasm target. Run through conformance/run.sh, +# which builds the sample producers, generates bindings, and exports the +# shared environment (see conformance/lib.sh); each lane runs one consumer +# against one sample and must exit 0. +# +# Each lane compiles the sample for wasm32-unknown-unknown (the workspace's +# .cargo/config.toml links it with --export-table --growable-table), installs +# the generated package into a scratch project with `npm pack` and `npm +# install`, and runs the consumer under plain `node`, with `{PREFIX}_LIBRARY` +# naming the .wasm that `init()` loads. The consumers are the node lanes' +# (conformance/node/*.mjs): both targets generate the same JavaScript API. +set -uo pipefail +. "$(dirname "$0")/../lib.sh" + +wasm_consumer() { + local sample="$1" src="$2" + local dir="$OUT/wasm-$sample" tarball + command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; return 1; } + rustup target list --installed 2>/dev/null | grep -qx 'wasm32-unknown-unknown' \ + || { echo "wasm32-unknown-unknown target missing (rustup target add wasm32-unknown-unknown)" >&2; return 1; } + cargo build -q -p "$sample" --release --target wasm32-unknown-unknown \ + || { echo "wasm32 build failed" >&2; return 1; } + local wasm="$TARGET_DIR/wasm32-unknown-unknown/release/${sample//-/_}.wasm" + [ -f "$wasm" ] || { echo "wasm artifact not found: $wasm" >&2; return 1; } + rm -rf "$dir" + mkdir -p "$dir" + tarball=$(cd "$dir" && npm pack --silent "$GENROOT/$sample/wasm") \ + || { echo "npm pack failed" >&2; return 1; } + printf '{ "name": "consumer", "private": true, "type": "module" }\n' > "$dir/package.json" + cp "$ROOT/conformance/node/harness.mjs" "$ROOT/conformance/node/$src" "$dir/" + ( cd "$dir" && npm install --no-audit --no-fund --loglevel=error "./$tarball" ) \ + || { echo "npm install failed" >&2; return 1; } + ( cd "$dir" && env "$(library_env "$sample")=$wasm" node --expose-gc "$src" ) +} + +wasm_contacts() { wasm_consumer contacts contacts.mjs; } + +wasm_events() { wasm_consumer events events.mjs; } + +wasm_kvstore() { wasm_consumer kvstore kvstore.mjs; } + +wasm_async_demo() { wasm_consumer async-demo async_demo.mjs; } + +wasm_codec() { wasm_consumer codec codec.mjs; } + +lane wasm-contacts wasm_contacts +lane wasm-events wasm_events +lane wasm-kvstore wasm_kvstore +lane wasm-async-demo wasm_async_demo +lane wasm-codec wasm_codec + +finish_lanes diff --git a/conformance/wasm/shapes.mjs b/conformance/wasm/shapes.mjs deleted file mode 100644 index 9e63affd..00000000 --- a/conformance/wasm/shapes.mjs +++ /dev/null @@ -1,110 +0,0 @@ -// Conformance consumer: shapes sample, Wasm (wasm32-unknown-unknown) target. -// -// Drives the generated ESM bindings (loadWeaveffiWasm) against the real producer -// compiled to wasm, exercising rich (algebraic) enums as value types: plain -// tagged objects ({ tag: 'Circle', radius: 2.5 }) serialized into value -// buffers at the boundary, with no wrapper class, factories, or free(). -// Covers every variant shape (unit, f64, two f32s, string + u8) through -// describe (enum in, string out) and scale (enum in and out), the consumer-side -// rejection of an unknown variant tag, plus the expanded numerics ([u8] in, -// u64 out as BigInt). Mirrors conformance/c/shapes.c. -// -// Inputs come from the harness: -// WV_WASM: path to the compiled shapes.wasm -// WV_JS: path to the generated weaveffi_wasm.js (ESM) - -import fs from 'fs'; - -const WASM = process.env.WV_WASM; -const JS = process.env.WV_JS; -if (!WASM || !JS) { - console.error('WV_WASM and WV_JS must be set'); - process.exit(2); -} - -// Node has no file:// fetch; shim it so the generated loader can read the .wasm. -globalThis.fetch = async (url) => ({ arrayBuffer: async () => fs.readFileSync(url) }); - -const mod = await import(JS); -const api = await mod.loadWeaveffiWasm(WASM); - -let failures = 0; -function expect(cond, msg) { - if (!cond) { - console.error('assertion failed: ' + msg); - failures++; - } -} -function approx(a, b, eps) { - return typeof a === 'number' && Math.abs(a - b) < eps; -} - -// Plain C-style enum still crosses by value as a frozen discriminant object. -expect(mod.Channel && mod.Channel.Green === 1, 'plain enum Channel exported'); - -// describe: rich enum in (a plain tagged object), string out; dispatches on -// the active variant. -const circle = { tag: 'Circle', radius: 2.5 }; -expect(api.shapes.describe(circle) === 'circle(r=2.5)', 'describe(circle)'); -expect(api.shapes.describe({ tag: 'Empty' }) === 'empty', 'describe(empty)'); -expect( - /rect/.test(api.shapes.describe({ tag: 'Rectangle', width: 3.0, height: 4.0 })), - 'describe(rectangle)' -); -expect( - /hex/.test(api.shapes.describe({ tag: 'Labeled', label: 'hex', count: 6 })), - 'describe(labeled)' -); - -// scale: rich enum in and out. The circle's f64 payload round-trips scaled. -const big = api.shapes.scale(circle, 4.0); -expect(big.tag === 'Circle', "scaled.tag === 'Circle'"); -expect(approx(big.radius, 10.0, 1e-9), 'scaled.radius == 10.0'); - -// Rectangle: two f32 payloads survive the round trip (scale by 2). -const rect2 = api.shapes.scale({ tag: 'Rectangle', width: 3.0, height: 4.0 }, 2.0); -expect(rect2.tag === 'Rectangle', "rect2.tag === 'Rectangle'"); -expect(approx(rect2.width, 6.0, 1e-6), 'rect2.width == 6.0'); -expect(approx(rect2.height, 8.0, 1e-6), 'rect2.height == 8.0'); - -// Labeled: the string + u8 payload passes through scale unchanged. -const labeled2 = api.shapes.scale({ tag: 'Labeled', label: 'hex', count: 6 }, 9.0); -expect(labeled2.tag === 'Labeled', "labeled2.tag === 'Labeled'"); -expect(labeled2.label === 'hex', "labeled2.label == 'hex'"); -expect(labeled2.count === 6, 'labeled2.count == 6'); - -// Empty: a unit variant is tag-only in both directions. -const empty2 = api.shapes.scale({ tag: 'Empty' }, 3.0); -expect(empty2.tag === 'Empty', "empty2.tag === 'Empty'"); - -// An unknown variant tag is a consumer-side marshalling failure, rejected -// before anything crosses the boundary. -let tagErr = null; -try { api.shapes.describe({ tag: 'Pentagon' }); } catch (e) { tagErr = e; } -expect(tagErr instanceof mod.WeaveFFIError, 'unknown tag -> WeaveFFIError'); -expect(tagErr && tagErr.code === -3, `unknown tag -> marshalling code -3 (got ${tagErr && tagErr.code})`); -expect(tagErr && tagErr.message.includes('Pentagon'), 'unknown tag error names the tag'); -expect(api.shapes.describe({ tag: 'Empty' }) === 'empty', 'module usable after the rejected tag'); - -// Floating-point edge values inside the rich enum payloads survive the round -// trip: NaN, infinities, and -0 in the f64 and f32 slots. -const nanCircle = api.shapes.scale({ tag: 'Circle', radius: NaN }, 1.0); -expect(Number.isNaN(nanCircle.radius), 'NaN radius survives'); -const negZero = api.shapes.scale({ tag: 'Circle', radius: -0 }, 1.0); -expect(Object.is(negZero.radius, -0), '-0 radius keeps its sign'); -const inf = api.shapes.scale({ tag: 'Rectangle', width: Infinity, height: -Infinity }, 1.0); -expect(inf.width === Infinity && inf.height === -Infinity, 'infinities survive in f32 slots'); - -// numerics: [u8] in (canonicalized to bytes), u64 out (BigInt); -// lowerCamelCase wrapper name. -expect(api.shapes.sumBytes([250, 250, 250, 250]) === 1000n, 'sumBytes == 1000n'); -expect(typeof api.shapes.sumBytes([1]) === 'bigint', 'sumBytes returns a BigInt'); -expect(api.shapes.sumBytes(new Uint8Array([255, 255])) === 510n, 'sumBytes accepts a Uint8Array'); -expect(api.shapes.sumBytes([]) === 0n, 'sumBytes of nothing is 0n'); - -if (failures === 0) { - console.log('wasm/shapes: OK'); -} else { - console.error(`wasm/shapes: ${failures} failure(s)`); - process.exit(1); -} diff --git a/crates/weaveffi-abi/Cargo.toml b/crates/weaveffi-abi/Cargo.toml index 6a795a81..e7ce5dd5 100644 --- a/crates/weaveffi-abi/Cargo.toml +++ b/crates/weaveffi-abi/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "weaveffi-abi" -version = "0.23.0" +version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true @@ -9,6 +10,12 @@ description = "Stable C ABI runtime types and helpers for WeaveFFI" keywords = ["ffi", "abi", "c-abi", "bindings", "codegen"] categories = ["development-tools::ffi"] +[features] +# Count live objects, callbacks, iterators, cancel tokens, and returned byte +# allocations so `{prefix}_debug_live` can report leaks. Off by default +# because every counted operation pays for an atomic update. +leak-check = [] + [lints] workspace = true diff --git a/crates/weaveffi-abi/src/buffer.rs b/crates/weaveffi-abi/src/buffer.rs index 09a1e2e0..9e05ebca 100644 --- a/crates/weaveffi-abi/src/buffer.rs +++ b/crates/weaveffi-abi/src/buffer.rs @@ -7,7 +7,7 @@ //! than as an opaque object pointer or parallel arrays. Parameters are //! borrowed for the duration of the call (the consumer owns and frees its own //! encoding); returns are producer-allocated and released by the consumer -//! with `weaveffi_free_bytes` after decoding. +//! with `{prefix}_free_bytes` after decoding. //! //! # Encoding //! @@ -47,6 +47,11 @@ //! Encoded lengths and counts are `u32`, capping any single string, byte //! buffer, or collection at `u32::MAX` entries; [`BufferWriter`] panics past //! that bound rather than truncating. +//! +//! Lists of bytes and fixed-width numbers (every integer and float type, but +//! not `bool`, whose bytes must be validated) encode and decode with a single +//! copy on little-endian targets, and [`encode_value`] sizes its allocation +//! exactly up front from [`BufferValue::encoded_len`]. use std::sync::Arc; @@ -89,12 +94,25 @@ impl BufferWriter { Self::default() } + /// Create an empty writer with room for `capacity` bytes. + #[must_use] + pub fn with_capacity(capacity: usize) -> Self { + Self { + buf: Vec::with_capacity(capacity), + } + } + /// Consume the writer and return the encoded bytes. #[must_use] pub fn finish(self) -> Vec { self.buf } + /// Append already-encoded bytes verbatim. + pub fn write_raw(&mut self, bytes: &[u8]) { + self.buf.extend_from_slice(bytes); + } + /// Write a `bool` as one byte (`0` or `1`). pub fn write_bool(&mut self, v: bool) { self.buf.push(u8::from(v)); @@ -165,12 +183,19 @@ impl BufferWriter { /// Write a string as a `u32` byte length followed by its UTF-8 bytes. /// Interior NUL bytes round-trip unchanged (the format is not /// NUL-terminated). + /// + /// # Panics + /// + /// Panics when the string is longer than `u32::MAX` bytes. pub fn write_string(&mut self, v: &str) { - self.write_len(v.len()); - self.buf.extend_from_slice(v.as_bytes()); + self.write_bytes(v.as_bytes()); } /// Write a byte buffer as a `u32` length followed by the raw bytes. + /// + /// # Panics + /// + /// Panics when the buffer is longer than `u32::MAX` bytes. pub fn write_bytes(&mut self, v: &[u8]) { self.write_len(v.len()); self.buf.extend_from_slice(v); @@ -337,17 +362,15 @@ impl<'a> BufferReader<'a> { ])) } - /// Read a length or element count (a `u32`). + /// Read the byte length of a string or byte buffer (a `u32`). /// /// # Errors /// /// Returns an error when the buffer is exhausted or the decoded length - /// exceeds the bytes remaining (which would make follow-up reads fail - /// anyway; rejecting here gives a clearer error). + /// exceeds the bytes remaining, which would make the follow-up read fail + /// anyway; rejecting here gives a clearer error. pub fn read_len(&mut self) -> Result { - let len = self.read_u32()? as usize; - // A length can never exceed what is left in the buffer: even the - // densest elements occupy at least one byte each. + let len = self.read_count()?; if len > self.remaining() { return Err(BufferDecodeError { context: "length prefix exceeds remaining buffer", @@ -356,6 +379,30 @@ impl<'a> BufferReader<'a> { Ok(len) } + /// Read a collection's element count (a `u32`). + /// + /// Unlike [`read_len`](Self::read_len) this doesn't bound the count by + /// the bytes remaining: an element's encoding can be empty (a record with + /// no fields), so a large count is legitimate. Decoders bound their + /// preallocation by [`remaining`](Self::remaining) instead, and a hostile + /// count fails as soon as the elements run out. + /// + /// # Errors + /// + /// Returns an error when the buffer is exhausted. + pub fn read_count(&mut self) -> Result { + Ok(self.read_u32()? as usize) + } + + /// Read `n` raw bytes. + /// + /// # Errors + /// + /// Returns an error when fewer than `n` bytes remain. + pub fn read_raw(&mut self, n: usize) -> Result<&'a [u8], BufferDecodeError> { + self.take(n, "raw bytes") + } + /// Read a string: `u32` byte length + UTF-8 bytes. /// /// # Errors @@ -365,9 +412,11 @@ impl<'a> BufferReader<'a> { pub fn read_string(&mut self) -> Result { let len = self.read_len()?; let bytes = self.take(len, "string bytes")?; - String::from_utf8(bytes.to_vec()).map_err(|_| BufferDecodeError { - context: "string is not valid UTF-8", - }) + std::str::from_utf8(bytes) + .map(str::to_owned) + .map_err(|_| BufferDecodeError { + context: "string is not valid UTF-8", + }) } /// Read a byte buffer: `u32` length + raw bytes. @@ -412,15 +461,36 @@ impl<'a> BufferReader<'a> { } } +/// Proof that a type's buffer encoding is exactly its little-endian +/// in-memory representation, which lets lists of it encode and decode with +/// one copy. +/// +/// Only this crate can construct it, so only the built-in integer and float +/// implementations of [`BufferValue`] can opt into the fast path; a type with +/// invalid bit patterns (`bool`, an enum) can never be copied in unchecked. +#[derive(Debug, Clone, Copy)] +pub struct FixedWidth(()); + /// A value that can serialize itself into (and decode itself from) the /// WeaveFFI buffer format. /// /// The `#[weaveffi::record]`, `#[weaveffi::enumeration]`, and /// `#[weaveffi::error]` expansions implement this for annotated types, and -/// blanket implementations below cover primitives, `String`, `Vec`, -/// `Option`, `Vec`, the map types, and `Arc` (an interface object -/// token), so nested composites compose automatically. +/// blanket implementations below cover primitives, `String`, `Option`, +/// `Vec` (bytes are `Vec`), the map types, and `Arc` (an interface +/// object token), so nested composites compose automatically. pub trait BufferValue: Sized { + /// `Some` when this type's encoding is its little-endian memory layout + /// (see [`FixedWidth`]). Leave the default. + const FIXED_WIDTH: Option = None; + + /// The exact number of bytes [`write_value`](Self::write_value) appends, + /// used to size encodings up front. The default of `0` is a valid (if + /// slower) hint; every built-in and generated implementation is exact. + fn encoded_len(&self) -> usize { + 0 + } + /// Append this value's encoding to `w`. fn write_value(&self, w: &mut BufferWriter); @@ -433,10 +503,53 @@ pub trait BufferValue: Sized { fn read_value(r: &mut BufferReader<'_>) -> Result; } +/// Marks a type that crosses the C ABI as a value buffer: a +/// `#[weaveffi::record]` or a rich `#[weaveffi::enumeration]`. +/// +/// The `#[weaveffi::module]` expansion asserts it for every type a module +/// names but doesn't declare in its own module tree, because such a type is +/// lowered as a value buffer without the macro seeing its declaration. A +/// C-style enum, an interface, or a callback interface from another tree +/// would cross differently in the generated header, so it must fail to +/// compile instead of silently mismatching. +#[diagnostic::on_unimplemented( + message = "`{Self}` is declared in a different `#[weaveffi::module]` tree and isn't a record or rich enum", + label = "declared outside this module tree", + note = "a `#[weaveffi::module]` only sees the declarations inside its own tree, so it can pass a type from another tree only as a value buffer (a record or rich enum)", + note = "nest the modules under one `#[weaveffi::module]` root (as inner `mod`s) so the macro can see the declaration" +)] +pub trait ByValue {} + macro_rules! scalar_buffer_value { ($($t:ty => ($write:ident, $read:ident)),* $(,)?) => { $( impl BufferValue for $t { + fn encoded_len(&self) -> usize { + std::mem::size_of::<$t>() + } + fn write_value(&self, w: &mut BufferWriter) { + w.$write(*self); + } + fn read_value(r: &mut BufferReader<'_>) -> Result { + r.$read() + } + } + )* + }; +} + +macro_rules! fixed_width_buffer_value { + ($($t:ty => ($write:ident, $read:ident)),* $(,)?) => { + $( + impl BufferValue for $t { + const FIXED_WIDTH: Option = if cfg!(target_endian = "little") { + Some(FixedWidth(())) + } else { + None + }; + fn encoded_len(&self) -> usize { + std::mem::size_of::<$t>() + } fn write_value(&self, w: &mut BufferWriter) { w.$write(*self); } @@ -450,6 +563,9 @@ macro_rules! scalar_buffer_value { scalar_buffer_value! { bool => (write_bool, read_bool), +} + +fixed_width_buffer_value! { i8 => (write_i8, read_i8), u8 => (write_u8, read_u8), i16 => (write_i16, read_i16), @@ -463,6 +579,9 @@ scalar_buffer_value! { } impl BufferValue for String { + fn encoded_len(&self) -> usize { + 4 + self.len() + } fn write_value(&self, w: &mut BufferWriter) { w.write_string(self); } @@ -477,6 +596,9 @@ impl BufferValue for String { /// guarantee this). A zero token is a contract violation and decodes as an /// error. impl BufferValue for Arc { + fn encoded_len(&self) -> usize { + 8 + } fn write_value(&self, w: &mut BufferWriter) { w.write_u64(crate::object::object_to_token(self)); } @@ -492,6 +614,9 @@ impl BufferValue for Arc { } impl BufferValue for Option { + fn encoded_len(&self) -> usize { + 1 + self.as_ref().map_or(0, BufferValue::encoded_len) + } fn write_value(&self, w: &mut BufferWriter) { match self { Some(v) => { @@ -511,23 +636,76 @@ impl BufferValue for Option { } impl BufferValue for Vec { + fn encoded_len(&self) -> usize { + if T::FIXED_WIDTH.is_some() { + return 4 + std::mem::size_of_val(self.as_slice()); + } + 4 + self.iter().map(BufferValue::encoded_len).sum::() + } + fn write_value(&self, w: &mut BufferWriter) { w.write_len(self.len()); + if let Some(proof) = T::FIXED_WIDTH { + w.write_raw(fixed_width_bytes(proof, self)); + return; + } for item in self { item.write_value(w); } } + fn read_value(r: &mut BufferReader<'_>) -> Result { - let len = r.read_len()?; - let mut out = Vec::with_capacity(len.min(r.remaining())); - for _ in 0..len { + let count = r.read_count()?; + if let Some(proof) = T::FIXED_WIDTH { + let size = count + .checked_mul(std::mem::size_of::()) + .ok_or(BufferDecodeError { + context: "list length overflows", + })?; + let bytes = r.take(size, "fixed-width list")?; + return Ok(fixed_width_from_bytes(proof, bytes, count)); + } + let mut out = Vec::with_capacity(count.min(r.remaining())); + for _ in 0..count { out.push(T::read_value(r)?); } Ok(out) } } +/// View a fixed-width slice as its encoded bytes. +fn fixed_width_bytes(_proof: FixedWidth, items: &[T]) -> &[u8] { + // SAFETY: `FixedWidth` is only constructed for the integer and float + // types on little-endian targets, which have no padding and whose memory + // layout is exactly their encoding, so every byte is initialized. + unsafe { std::slice::from_raw_parts(items.as_ptr().cast::(), std::mem::size_of_val(items)) } +} + +/// Copy `count` fixed-width elements out of their encoded bytes. +fn fixed_width_from_bytes( + _proof: FixedWidth, + bytes: &[u8], + count: usize, +) -> Vec { + let mut out = Vec::::with_capacity(count); + // SAFETY: `FixedWidth` guarantees every bit pattern of `T` is valid and + // that its encoding is its memory layout; `bytes` holds exactly `count` + // elements, the destination has room for them, and a byte-wise copy has + // no alignment requirement. + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::(), bytes.len()); + out.set_len(count); + } + out +} + impl BufferValue for std::collections::BTreeMap { + fn encoded_len(&self) -> usize { + 4 + self + .iter() + .map(|(k, v)| k.encoded_len() + v.encoded_len()) + .sum::() + } fn write_value(&self, w: &mut BufferWriter) { w.write_len(self.len()); for (k, v) in self { @@ -536,7 +714,7 @@ impl BufferValue for std::collections::BTr } } fn read_value(r: &mut BufferReader<'_>) -> Result { - let len = r.read_len()?; + let len = r.read_count()?; let mut out = Self::new(); for _ in 0..len { let k = K::read_value(r)?; @@ -550,6 +728,12 @@ impl BufferValue for std::collections::BTr impl BufferValue for std::collections::HashMap { + fn encoded_len(&self) -> usize { + 4 + self + .iter() + .map(|(k, v)| k.encoded_len() + v.encoded_len()) + .sum::() + } fn write_value(&self, w: &mut BufferWriter) { w.write_len(self.len()); for (k, v) in self { @@ -558,7 +742,7 @@ impl BufferValue } } fn read_value(r: &mut BufferReader<'_>) -> Result { - let len = r.read_len()?; + let len = r.read_count()?; let mut out = Self::with_capacity(len.min(r.remaining())); for _ in 0..len { let k = K::read_value(r)?; @@ -569,10 +753,11 @@ impl BufferValue } } -/// Encode one [`BufferValue`] into a fresh byte buffer. +/// Encode one [`BufferValue`] into a fresh byte buffer, allocated once at +/// the size [`BufferValue::encoded_len`] reports. #[must_use] pub fn encode_value(value: &T) -> Vec { - let mut w = BufferWriter::new(); + let mut w = BufferWriter::with_capacity(value.encoded_len()); value.write_value(&mut w); w.finish() } @@ -701,4 +886,61 @@ mod tests { let bytes = [2, 0, 0, 0, 0xFF, 0xFE]; assert!(decode_value::(&bytes).is_err()); } + + #[derive(Debug, PartialEq)] + struct NoFields; + + impl BufferValue for NoFields { + fn write_value(&self, _w: &mut BufferWriter) {} + fn read_value(_r: &mut BufferReader<'_>) -> Result { + Ok(NoFields) + } + } + + #[test] + fn zero_sized_elements_allow_counts_beyond_the_remaining_bytes() { + roundtrip(vec![NoFields, NoFields, NoFields]); + assert_eq!(encode_value(&vec![NoFields, NoFields]), [2, 0, 0, 0]); + } + + #[test] + fn encoded_len_is_exact() { + fn check(v: T) { + let bytes = encode_value(&v); + assert_eq!(bytes.len(), v.encoded_len()); + assert_eq!(bytes.capacity(), bytes.len()); + } + check(vec![1u16, 2, 3]); + check(vec![1.5f64]); + check(vec![true, false]); + check(Some("text".to_string())); + check(vec![Some(vec![1i64]), None]); + let mut m = BTreeMap::new(); + m.insert("k".to_string(), vec![Some(1u8)]); + check(m); + } + + #[test] + fn fixed_width_lists_use_the_scalar_layout() { + let v = vec![1i32, -2, 0x0102_0304]; + let mut slow = BufferWriter::new(); + slow.write_len(v.len()); + for x in &v { + slow.write_i32(*x); + } + assert_eq!(encode_value(&v), slow.finish()); + roundtrip(v); + roundtrip(vec![f32::MIN, 0.0, f32::MAX]); + roundtrip(Vec::::new()); + } + + #[test] + fn truncated_fixed_width_list_is_rejected() { + let mut bytes = encode_value(&vec![7u32, 8]); + bytes.pop(); + assert!(decode_value::>(&bytes).is_err()); + let huge = [0xFF, 0xFF, 0xFF, 0xFF]; + assert!(decode_value::>(&huge).is_err()); + assert!(decode_value::>(&huge).is_err()); + } } diff --git a/crates/weaveffi-abi/src/callback.rs b/crates/weaveffi-abi/src/callback.rs index 34f26c97..f04a5d39 100644 --- a/crates/weaveffi-abi/src/callback.rs +++ b/crates/weaveffi-abi/src/callback.rs @@ -11,33 +11,30 @@ //! calls `free(ctx)` exactly once. //! //! A consumer implementation that fails reports through the method's -//! `out_err` slot with [`FOREIGN_ERROR_CODE`](crate::FOREIGN_ERROR_CODE). The -//! generated trait method can't return that error (callback methods never -//! `throws`), so [`check_foreign_error`] hands it to [`raise_foreign_error`], -//! which delivers it to the enclosing thunk by one of two routes: +//! `out_err` slot (normally with [`FOREIGN_ERROR_CODE`](crate::FOREIGN_ERROR_CODE)), +//! and [`foreign_status`] turns that slot into a `Result`. What happens next +//! depends on how the producer declared the trait method: //! -//! * On a `panic = "unwind"` build (every native target by default) it -//! unwinds with a [`ForeignError`] payload, aborting the producer's call at -//! the point of failure. The thunk's `catch_unwind` recognizes the payload -//! and reports it with the consumer's message. -//! * On a `panic = "abort"` build (notably `wasm32-unknown-unknown`, where -//! unwinding is unavailable) it records the failure in a thread-local slot -//! and returns, so the producer's code keeps running on the vtable entry's -//! default return value. Every thunk checks [`take_foreign_error`] after the -//! producer returns and reports the recorded failure instead of the result. -//! The first failure recorded wins; later ones on the same call are dropped. -//! -//! Both routes surface the same `out_err` code and message to the original -//! caller, so consumers see identical behaviour. Producers that need the -//! abort-build route to be correct must tolerate a callback method returning -//! its type's zero value once the consumer has failed; well-behaved producers -//! already do, because the consumer could return that value on purpose. - -use std::cell::RefCell; +//! * A method returning `Result` gets the failure as an +//! `Err` and nothing unwinds. This is the recommended spelling. +//! * A method returning a plain `T` can't return the failure, so +//! [`raise_foreign_error`] delivers it to the enclosing thunk. On a +//! `panic = "unwind"` build (every native target by default) it unwinds +//! with a [`ForeignError`] payload, and the thunk's `catch_unwind` reports +//! it with the consumer's message. On a `panic = "abort"` build (notably +//! `wasm32-unknown-unknown`) it records the failure for the innermost +//! active thunk on this thread (see [`ThunkScope`]) and returns the vtable +//! entry's default value; the thunk reports the recorded failure in place +//! of its result. With no thunk active (the producer called back from a +//! thread of its own) the failure is written to stderr and dropped, so it +//! can never leak into a later, unrelated call. + +use std::cell::{Cell, RefCell}; use std::ffi::c_void; +use std::marker::PhantomData; use std::sync::Arc; -use crate::weaveffi_error; +use crate::FfiError; /// Implemented by every generated vtable struct so [`ForeignCallback`] can /// find the trailing `free` entry without knowing the method layout. @@ -116,6 +113,7 @@ impl ForeignCallback { if vtable.is_null() { return None; } + crate::leak::track(crate::leak::CALLBACKS, 1); Some(Self { ctx, vtable }) } @@ -138,81 +136,138 @@ impl ForeignCallback { impl Drop for ForeignCallback { fn drop(&mut self) { let free = self.vtable().free(); + crate::leak::track(crate::leak::CALLBACKS, -1); // SAFETY: `ctx` is handed back to the consumer's own release hook // exactly once, as the ABI contract requires. unsafe { free(self.ctx) }; } } -/// The unwind payload [`check_foreign_error`] raises when a consumer -/// callback-interface implementation reports a failure. +/// A failure a consumer's callback-interface implementation reported. /// -/// The `#[weaveffi::module]` thunks catch it and report -/// [`FOREIGN_ERROR_CODE`](crate::FOREIGN_ERROR_CODE) with `message` to the -/// original caller, so the consumer's own error text round-trips through the -/// producer. +/// A trait method declared to return `Result` receives it as +/// an `Err`. For a method returning a plain `T` it's the unwind payload +/// [`raise_foreign_error`] raises, which the `#[weaveffi::module]` thunks +/// catch and report with `code` and `message`, so the consumer's own error +/// text round-trips through the producer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ForeignError { /// The code the consumer wrote to `out_err` (normally /// [`FOREIGN_ERROR_CODE`](crate::FOREIGN_ERROR_CODE)). pub code: i32, - /// The consumer's message, copied out of `out_err` before it was cleared. + /// The consumer's message, copied out of `out_err` before it was freed. pub message: String, } impl std::fmt::Display for ForeignError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.message) + f.write_str(&self.message) } } impl std::error::Error for ForeignError {} +/// A `ForeignError` is reported with its own code and message, so a producer +/// function can propagate a callback failure with `?` when its error type is +/// `ForeignError`. +impl crate::ErrorReport for ForeignError { + fn code(&self) -> i32 { + self.code + } + fn message(&self) -> String { + self.message.clone() + } +} + thread_local! { - /// The failure recorded by [`raise_foreign_error`] on a `panic = "abort"` - /// build, waiting for the enclosing thunk to pick it up. + /// How many thunks are running on this thread (nested calls stack). + static THUNK_DEPTH: Cell = const { Cell::new(0) }; + /// The failure recorded for the innermost running thunk on a + /// `panic = "abort"` build, waiting for that thunk to pick it up. static PENDING_FOREIGN: RefCell> = const { RefCell::new(None) }; } -/// Inspect the `out_err` slot a vtable entry wrote and abort the producer -/// call if the consumer reported a failure. +/// Marks a WeaveFFI thunk (or one poll of an async call's future) as running +/// on the current thread, for the lifetime of the value. +/// +/// A deferred foreign failure ([`defer_foreign_error`]) is only recorded +/// while a scope is active, and belongs to the innermost one: entering a +/// scope sets aside whatever an outer scope had pending and dropping it puts +/// that back, so a nested call (a consumer callback that calls into the +/// producer again) can neither steal nor leak its caller's failure. Read the +/// scope's own failure with [`take_foreign_error`] before it drops. +#[derive(Debug)] +pub struct ThunkScope { + outer: Option, + // Thread-local bookkeeping: the scope must end on the thread it began. + _not_send: PhantomData<*const ()>, +} + +impl ThunkScope { + /// Enter a scope on the current thread. + #[must_use] + pub fn enter() -> Self { + THUNK_DEPTH.with(|d| d.set(d.get() + 1)); + let outer = PENDING_FOREIGN.with(|slot| slot.borrow_mut().take()); + Self { + outer, + _not_send: PhantomData, + } + } + + /// Whether any scope is active on the current thread. + #[must_use] + pub fn is_active() -> bool { + THUNK_DEPTH.with(Cell::get) > 0 + } +} + +impl Drop for ThunkScope { + fn drop(&mut self) { + THUNK_DEPTH.with(|d| d.set(d.get().saturating_sub(1))); + let outer = self.outer.take(); + PENDING_FOREIGN.with(|slot| *slot.borrow_mut() = outer); + } +} + +/// Read the `out_err` slot a vtable entry wrote: `Ok(())` when the consumer +/// succeeded, otherwise the consumer's failure (whose allocations `err`'s +/// drop then releases). /// -/// On success (`code == 0`) this is a no-op. Otherwise it copies the message, -/// releases the error's allocations, and raises a [`ForeignError`] through -/// [`raise_foreign_error`]. +/// A positive code is reported as +/// [`FOREIGN_ERROR_CODE`](crate::FOREIGN_ERROR_CODE), since it must not +/// masquerade as one of the producer's domain errors on the outer call; the +/// reserved negative codes are kept. /// -/// # Panics +/// # Errors /// -/// On a `panic = "unwind"` build this unwinds with a [`ForeignError`] payload -/// whenever `err.code != 0`. Generated thunks always run inside -/// `catch_unwind`, which converts the payload into an `out_err` report. -pub fn check_foreign_error(mut err: weaveffi_error) { +/// Returns the consumer's failure when `err.code` isn't `0`. +pub fn foreign_status(err: &FfiError) -> Result<(), ForeignError> { if err.code == 0 { - return; + return Ok(()); } - // Consumers report with `FOREIGN_ERROR_CODE`; any other trap code is kept, - // but a positive code must not masquerade as one of the producer's domain - // errors on the outer call. let code = if err.code < 0 { err.code } else { crate::FOREIGN_ERROR_CODE }; - let message = crate::c_ptr_to_string(err.message) - .unwrap_or_else(|| "callback interface implementation failed".to_string()); - crate::error_clear(&mut err); - raise_foreign_error(ForeignError { code, message }); + // SAFETY: the consumer fills `out_err` only through `{prefix}_error_set`, + // so a non-null message is a NUL-terminated string this runtime owns. + let message = unsafe { err.message_str() } + .unwrap_or("callback interface implementation failed") + .to_string(); + Err(ForeignError { code, message }) } -/// Deliver a consumer-side failure to the thunk that is running the current -/// producer call. +/// Deliver a consumer-side failure from a callback method that can't return +/// it (one declared to return a plain `T`). /// /// On a `panic = "unwind"` build this never returns: it unwinds with `err` as -/// the payload via [`std::panic::resume_unwind`], so the panic hook does not -/// fire (this is control flow, not a bug report). On a `panic = "abort"` build -/// it records `err` for [`take_foreign_error`] and returns, keeping the first -/// failure if one is already pending. Generated code always follows a call to -/// this function with a fallback value so that both builds type-check. +/// the payload via [`std::panic::resume_unwind`], so the panic hook doesn't +/// fire (this is control flow, not a bug report). On a `panic = "abort"` +/// build it calls [`defer_foreign_error`] and returns. Generated code always +/// follows a call to this function with a fallback value so that both builds +/// type-check. /// /// # Panics /// @@ -222,19 +277,28 @@ pub fn raise_foreign_error(err: ForeignError) { { std::panic::resume_unwind(Box::new(err)); } - #[cfg(panic = "abort")] + #[cfg(not(panic = "unwind"))] { defer_foreign_error(err); } } -/// Record `err` as this thread's pending foreign failure without unwinding, -/// keeping an already-pending failure if there is one. +/// Record `err` for the innermost [`ThunkScope`] on this thread without +/// unwinding, keeping an already-recorded failure if there is one. With no +/// scope active the failure is written to stderr and dropped. /// -/// This is the `panic = "abort"` half of [`raise_foreign_error`]; it is public -/// so that tests and unusual producers can exercise the deferred route on any +/// This is the `panic = "abort"` half of [`raise_foreign_error`]; it's public +/// so tests and unusual producers can exercise the deferred route on any /// build. pub fn defer_foreign_error(err: ForeignError) { + if !ThunkScope::is_active() { + eprintln!( + "weaveffi: a callback interface implementation failed outside any exported call \ + (code {}): {}", + err.code, err.message + ); + return; + } PENDING_FOREIGN.with(|slot| { let mut slot = slot.borrow_mut(); if slot.is_none() { @@ -243,13 +307,13 @@ pub fn defer_foreign_error(err: ForeignError) { }); } -/// Take the foreign failure recorded on this thread by [`defer_foreign_error`], -/// if any. +/// Take the failure recorded for the innermost [`ThunkScope`] on this +/// thread, if any. /// -/// Generated thunks call this after the producer's code returns and, when it -/// yields `Some`, report the failure through `out_err` instead of the result. -/// On a `panic = "unwind"` build this is normally `None`, because the failure -/// unwound past the producer's code instead of being recorded. +/// Generated thunks call this right after the producer's code returns and, +/// when it yields `Some`, report the failure through `out_err` instead of +/// the result. On a `panic = "unwind"` build it's normally `None`, because a +/// failure unwinds past the producer's code instead of being recorded. #[must_use] pub fn take_foreign_error() -> Option { PENDING_FOREIGN.with(|slot| slot.borrow_mut().take()) @@ -262,7 +326,7 @@ mod tests { #[repr(C)] struct TestVtable { - ping: unsafe extern "C" fn(*mut c_void, i32, *mut weaveffi_error) -> i32, + ping: unsafe extern "C" fn(*mut c_void, i32, *mut FfiError) -> i32, free: unsafe extern "C" fn(*mut c_void), } @@ -272,63 +336,88 @@ mod tests { } } - static FREED: AtomicUsize = AtomicUsize::new(0); - - unsafe extern "C" fn ping(_ctx: *mut c_void, x: i32, out_err: *mut weaveffi_error) -> i32 { + unsafe extern "C" fn ping(_ctx: *mut c_void, x: i32, out_err: *mut FfiError) -> i32 { if x < 0 { - crate::error_set(out_err, crate::FOREIGN_ERROR_CODE, "negative"); + unsafe { crate::error_set(out_err, crate::FOREIGN_ERROR_CODE, "negative") }; return 0; } - crate::error_set_ok(out_err); x * 2 } - unsafe extern "C" fn free(_ctx: *mut c_void) { - FREED.fetch_add(1, Ordering::SeqCst); + /// Each test owns its counter: the context points at it, so tests running + /// in parallel never observe each other's releases. + unsafe extern "C" fn free(ctx: *mut c_void) { + unsafe { &*ctx.cast::() }.fetch_add(1, Ordering::SeqCst); } static VTABLE: TestVtable = TestVtable { ping, free }; - fn call(cb: &ForeignCallback, x: i32) -> i32 { - let mut err = weaveffi_error::default(); + fn ctx(freed: &AtomicUsize) -> *mut c_void { + std::ptr::from_ref(freed).cast_mut().cast() + } + + fn call(cb: &ForeignCallback, x: i32) -> Result { + let mut err = FfiError::default(); let out = unsafe { (cb.vtable().ping)(cb.ctx(), x, &mut err) }; - check_foreign_error(err); - out + foreign_status(&err).map(|()| out) } #[test] fn calls_through_and_frees_once() { - let before = FREED.load(Ordering::SeqCst); - let cb = - Arc::new(unsafe { ForeignCallback::from_raw(std::ptr::null_mut(), &VTABLE) }.unwrap()); - assert_eq!(call(&cb, 21), 42); + let freed = AtomicUsize::new(0); + let cb = Arc::new(unsafe { ForeignCallback::from_raw(ctx(&freed), &VTABLE) }.unwrap()); + assert_eq!(call(&cb, 21), Ok(42)); let second = Arc::clone(&cb); drop(cb); - assert_eq!(FREED.load(Ordering::SeqCst), before); + assert_eq!(freed.load(Ordering::SeqCst), 0); drop(second); - assert_eq!(FREED.load(Ordering::SeqCst), before + 1); + assert_eq!(freed.load(Ordering::SeqCst), 1); } #[test] - fn foreign_failure_unwinds_with_the_message() { - let cb = unsafe { ForeignCallback::from_raw(std::ptr::null_mut(), &VTABLE) }.unwrap(); - let payload = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| call(&cb, -1))).unwrap_err(); - let fe = payload - .downcast_ref::() - .expect("ForeignError payload"); - assert_eq!(fe.code, crate::FOREIGN_ERROR_CODE); - assert_eq!(fe.message, "negative"); + fn foreign_failure_is_returned() { + let freed = AtomicUsize::new(0); + let cb = unsafe { ForeignCallback::from_raw(ctx(&freed), &VTABLE) }.unwrap(); + let err = call(&cb, -1).unwrap_err(); + assert_eq!(err.code, crate::FOREIGN_ERROR_CODE); + assert_eq!(err.message, "negative"); + } + + #[test] + fn raised_failure_unwinds_with_the_payload() { + let payload = std::panic::catch_unwind(|| { + raise_foreign_error(ForeignError { + code: crate::FOREIGN_ERROR_CODE, + message: "raised".into(), + }); + }) + .unwrap_err(); + let fe = payload.downcast_ref::().unwrap(); + assert_eq!(fe.message, "raised"); + } + + #[test] + fn positive_codes_are_reported_as_foreign() { + let err = crate::FfiError::new(7, "domain-looking"); + assert_eq!( + foreign_status(&err).unwrap_err().code, + crate::FOREIGN_ERROR_CODE + ); + let err = crate::FfiError::new(crate::MARSHAL_ERROR_CODE, "kept"); + assert_eq!( + foreign_status(&err).unwrap_err().code, + crate::MARSHAL_ERROR_CODE + ); } trait Pinger: Send + Sync { - fn ping(&self, x: i32) -> i32; + fn ping(&self, x: i32) -> Result; } struct ForeignPinger(ForeignCallback); impl Pinger for ForeignPinger { - fn ping(&self, x: i32) -> i32 { + fn ping(&self, x: i32) -> Result { call(&self.0, x) } } @@ -342,12 +431,12 @@ mod tests { #[test] fn lift_callback_builds_the_trait_object() { - let before = FREED.load(Ordering::SeqCst); + let freed = AtomicUsize::new(0); let pinger: Arc = - unsafe { lift_callback(std::ptr::null_mut(), &VTABLE) }.expect("non-null vtable"); - assert_eq!(pinger.ping(4), 8); + unsafe { lift_callback(ctx(&freed), &VTABLE) }.expect("non-null vtable"); + assert_eq!(pinger.ping(4), Ok(8)); drop(pinger); - assert_eq!(FREED.load(Ordering::SeqCst), before + 1); + assert_eq!(freed.load(Ordering::SeqCst), 1); assert!( unsafe { lift_callback::(std::ptr::null_mut(), std::ptr::null()) } .is_none() @@ -356,6 +445,7 @@ mod tests { #[test] fn deferred_failures_keep_the_first_and_are_taken_once() { + let _scope = ThunkScope::enter(); assert!(take_foreign_error().is_none()); defer_foreign_error(ForeignError { code: crate::FOREIGN_ERROR_CODE, @@ -366,13 +456,47 @@ mod tests { message: "second".into(), }); let taken = take_foreign_error().expect("a pending failure"); - assert_eq!(taken.code, crate::FOREIGN_ERROR_CODE); assert_eq!(taken.message, "first"); assert!(take_foreign_error().is_none()); } + #[test] + fn deferral_without_a_scope_is_dropped() { + assert!(!ThunkScope::is_active()); + defer_foreign_error(ForeignError { + code: crate::FOREIGN_ERROR_CODE, + message: "nobody is listening".into(), + }); + let _scope = ThunkScope::enter(); + assert!(take_foreign_error().is_none()); + } + + #[test] + fn nested_scopes_keep_their_own_failures() { + let outer = ThunkScope::enter(); + defer_foreign_error(ForeignError { + code: crate::FOREIGN_ERROR_CODE, + message: "outer".into(), + }); + { + let _inner = ThunkScope::enter(); + assert!(take_foreign_error().is_none()); + defer_foreign_error(ForeignError { + code: crate::FOREIGN_ERROR_CODE, + message: "inner".into(), + }); + } + assert_eq!( + take_foreign_error().map(|e| e.message), + Some("outer".into()) + ); + drop(outer); + assert!(!ThunkScope::is_active()); + } + #[test] fn deferred_failures_are_thread_local() { + let _scope = ThunkScope::enter(); defer_foreign_error(ForeignError { code: crate::FOREIGN_ERROR_CODE, message: "mine".into(), diff --git a/crates/weaveffi-abi/src/cancel.rs b/crates/weaveffi-abi/src/cancel.rs new file mode 100644 index 00000000..4e2f559e --- /dev/null +++ b/crates/weaveffi-abi/src/cancel.rs @@ -0,0 +1,320 @@ +//! Reference-counted cancel tokens and the future combinator that makes +//! cancellation of an async call observable. +//! +//! A consumer creates a token with `{prefix}_cancel_token_create` and passes +//! it to a cancellable async launcher. The launcher takes its own reference +//! (a [`CancelToken`]), so the consumer may cancel and destroy its reference +//! at any time afterwards, even before the call completes. Cancelling wakes +//! every future registered on the token; the launcher's [`Cancellable`] +//! wrapper then drops the producer's future and the call completes with +//! [`CANCELLED_ERROR_CODE`](crate::CANCELLED_ERROR_CODE). + +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, PoisonError}; +use std::task::{Context, Poll, Waker}; + +/// The shared state behind a cancel token (`{prefix}_cancel_token` in C, an +/// opaque type). +/// +/// A `*mut FfiCancelToken` handed to C is an [`Arc`] turned into a raw +/// pointer, so the consumer's reference and every [`CancelToken`] the +/// producer holds keep the state alive independently. +#[derive(Debug, Default)] +pub struct FfiCancelToken { + cancelled: AtomicBool, + wakers: Mutex>, +} + +impl FfiCancelToken { + fn cancel(&self) { + self.cancelled.store(true, Ordering::SeqCst); + let wakers = std::mem::take(&mut *self.lock_wakers()); + for (_, waker) in wakers { + waker.wake(); + } + } + + fn is_cancelled(&self) -> bool { + self.cancelled.load(Ordering::SeqCst) + } + + fn lock_wakers(&self) -> std::sync::MutexGuard<'_, Vec<(u64, Waker)>> { + self.wakers.lock().unwrap_or_else(PoisonError::into_inner) + } + + /// Store (or refresh) the waker registered under `id`. + fn register(&self, id: u64, waker: &Waker) { + let mut wakers = self.lock_wakers(); + match wakers.iter_mut().find(|(k, _)| *k == id) { + Some((_, w)) => { + if !w.will_wake(waker) { + w.clone_from(waker); + } + } + None => wakers.push((id, waker.clone())), + } + } + + fn deregister(&self, id: u64) { + self.lock_wakers().retain(|(k, _)| *k != id); + } +} + +/// The body of `{prefix}_cancel_token_create`: a fresh, uncancelled token +/// holding one reference, which the consumer releases with +/// [`cancel_token_destroy`]. +#[must_use] +pub fn cancel_token_create() -> *mut FfiCancelToken { + crate::leak::track(crate::leak::TOKENS, 1); + Arc::into_raw(Arc::new(FfiCancelToken::default())).cast_mut() +} + +/// The body of `{prefix}_cancel_token_cancel`: request cancellation and wake +/// every future waiting on the token. Idempotent; a null token is a no-op. +/// +/// # Safety +/// +/// `token` must be null or a token from [`cancel_token_create`] whose +/// consumer reference hasn't been destroyed. +pub unsafe fn cancel_token_cancel(token: *mut FfiCancelToken) { + // SAFETY: the caller guarantees a live token or null. + if let Some(t) = unsafe { token.as_ref() } { + t.cancel(); + } +} + +/// The body of `{prefix}_cancel_token_is_cancelled`. A null token reads as +/// never cancelled. +/// +/// # Safety +/// +/// Same contract as [`cancel_token_cancel`]. +#[must_use] +pub unsafe fn cancel_token_is_cancelled(token: *const FfiCancelToken) -> bool { + // SAFETY: the caller guarantees a live token or null. + unsafe { token.as_ref() }.is_some_and(FfiCancelToken::is_cancelled) +} + +/// The body of `{prefix}_cancel_token_destroy`: release the consumer's +/// reference. In-flight calls keep their own references, so this is safe to +/// call while a call that received the token is still running. A null token +/// is a no-op. +/// +/// # Safety +/// +/// `token` must be null or a token from [`cancel_token_create`], and the +/// consumer must not use it again. +pub unsafe fn cancel_token_destroy(token: *mut FfiCancelToken) { + if token.is_null() { + return; + } + crate::leak::track(crate::leak::TOKENS, -1); + // SAFETY: `token` came from `Arc::into_raw` and its reference is + // released exactly once. + unsafe { Arc::decrement_strong_count(token.cast_const()) }; +} + +/// The producer's handle on a consumer's cancel token, accepted as the final +/// parameter of a `#[weaveffi::cancellable]` `async fn`. +/// +/// The handle holds its own reference to the token, so it stays valid after +/// the consumer destroys its reference. Cancellation is enforced by the +/// runtime (the producer's future is dropped as soon as the token fires); a +/// producer polls [`is_cancelled`](Self::is_cancelled) only when it wants to +/// clean up cooperatively, for example between batches of work on another +/// thread. +#[derive(Debug, Default)] +pub struct CancelToken { + state: Option>, +} + +impl CancelToken { + /// Take a producer reference on the token a launcher received. A null + /// token yields a handle that's never cancelled. + /// + /// This is the entry point the `#[weaveffi::module]` expansion calls; + /// producers receive an already-built token. + /// + /// # Safety + /// + /// `raw` must be null or a live token from [`cancel_token_create`]. + #[doc(hidden)] + #[must_use] + pub unsafe fn from_raw(raw: *const FfiCancelToken) -> Self { + if raw.is_null() { + return Self::default(); + } + // SAFETY: `raw` came from `Arc::into_raw` and is live, so adding a + // reference and adopting it is sound. + let state = unsafe { + Arc::increment_strong_count(raw); + Arc::from_raw(raw) + }; + crate::leak::track(crate::leak::TOKENS, 1); + Self { state: Some(state) } + } + + /// Whether the consumer has requested cancellation. + #[must_use] + pub fn is_cancelled(&self) -> bool { + self.state + .as_deref() + .is_some_and(FfiCancelToken::is_cancelled) + } +} + +impl Drop for CancelToken { + fn drop(&mut self) { + if self.state.is_some() { + crate::leak::track(crate::leak::TOKENS, -1); + } + } +} + +impl Clone for CancelToken { + fn clone(&self) -> Self { + if self.state.is_some() { + crate::leak::track(crate::leak::TOKENS, 1); + } + Self { + state: self.state.clone(), + } + } +} + +static NEXT_REGISTRATION: AtomicU64 = AtomicU64::new(1); + +/// A future that resolves to `Some(output)` when `inner` completes, or to +/// `None` as soon as the token is cancelled, dropping `inner` first. +/// +/// Async launchers for `cancellable` functions wrap the producer's future in +/// one of these. The waker is registered on the token before the flag is +/// checked, so a cancellation racing a poll is never missed. +#[derive(Debug)] +pub struct Cancellable { + inner: Option>>, + token: CancelToken, + registration: u64, +} + +impl Cancellable { + /// Race `fut` against `token`. A handle on a null token never cancels. + pub fn new(fut: F, token: CancelToken) -> Self { + Self { + inner: Some(Box::pin(fut)), + token, + registration: NEXT_REGISTRATION.fetch_add(1, Ordering::Relaxed), + } + } +} + +impl Future for Cancellable { + type Output = Option; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = &mut *self; + if let Some(state) = this.token.state.as_deref() { + state.register(this.registration, cx.waker()); + if state.is_cancelled() { + this.inner = None; + state.deregister(this.registration); + return Poll::Ready(None); + } + } + let Some(inner) = this.inner.as_mut() else { + // Already resolved; a well-behaved executor never polls again. + return Poll::Pending; + }; + match inner.as_mut().poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(out) => { + this.inner = None; + if let Some(state) = this.token.state.as_deref() { + state.deregister(this.registration); + } + Poll::Ready(Some(out)) + } + } + } +} + +impl Drop for Cancellable { + fn drop(&mut self) { + if let Some(state) = self.token.state.as_deref() { + state.deregister(self.registration); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lifecycle_and_null_safety() { + let token = cancel_token_create(); + assert!(!unsafe { cancel_token_is_cancelled(token) }); + unsafe { cancel_token_cancel(token) }; + unsafe { cancel_token_cancel(token) }; + assert!(unsafe { cancel_token_is_cancelled(token) }); + unsafe { cancel_token_destroy(token) }; + + unsafe { + cancel_token_cancel(std::ptr::null_mut()); + assert!(!cancel_token_is_cancelled(std::ptr::null())); + cancel_token_destroy(std::ptr::null_mut()); + } + assert!(!unsafe { CancelToken::from_raw(std::ptr::null()) }.is_cancelled()); + } + + #[test] + fn producer_reference_outlives_the_consumer() { + let token = cancel_token_create(); + let handle = unsafe { CancelToken::from_raw(token) }; + unsafe { cancel_token_cancel(token) }; + unsafe { cancel_token_destroy(token) }; + assert!(handle.is_cancelled()); + } + + #[test] + fn cancelling_wakes_and_drops_the_inner_future() { + struct Flag(Arc); + impl Drop for Flag { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } + } + + let token = cancel_token_create(); + let handle = unsafe { CancelToken::from_raw(token) }; + let dropped = Arc::new(AtomicBool::new(false)); + let guard = Flag(Arc::clone(&dropped)); + let fut = Cancellable::new( + async move { + let _guard = guard; + std::future::pending::<()>().await; + }, + handle, + ); + let raw = token as usize; + let canceller = std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(20)); + unsafe { cancel_token_cancel(raw as *mut FfiCancelToken) }; + unsafe { cancel_token_destroy(raw as *mut FfiCancelToken) }; + }); + assert!(crate::block_on(fut).is_none()); + assert!(dropped.load(Ordering::SeqCst)); + canceller.join().unwrap(); + } + + #[test] + fn an_uncancelled_future_completes() { + let handle = CancelToken::default(); + assert_eq!( + crate::block_on(Cancellable::new(async { 3 }, handle)), + Some(3) + ); + } +} diff --git a/crates/weaveffi-abi/src/convert.rs b/crates/weaveffi-abi/src/convert.rs index bf532078..3eb9d7f1 100644 --- a/crates/weaveffi-abi/src/convert.rs +++ b/crates/weaveffi-abi/src/convert.rs @@ -1,119 +1,194 @@ -//! Marshalling helpers that bridge owned Rust values and the C ABI slots. +//! Marshalling helpers that bridge Rust values and the C ABI's `(ptr, len)` +//! slots. //! -//! These functions are the audited home of every `unsafe` pointer operation a -//! WeaveFFI producer performs. The [`weaveffi-macros`](https://docs.rs/weaveffi-macros) -//! `#[weaveffi::module]` expansion wires the generated `extern "C"` thunks to -//! these helpers; producers never write the marshalling by hand. Keeping the -//! conversions in one place (rather than re-deriving them per generated symbol) -//! is what lets the runtime guarantee memory ownership rules consistently: +//! These functions are the audited home of the pointer operations a WeaveFFI +//! producer performs on strings, bytes, and value buffers. The +//! `#[weaveffi::module]` expansion wires the generated thunks to them: //! -//! * **lift** functions (`c_* -> Rust`) borrow or copy a foreign-supplied slot -//! into a Rust value for the duration of a call; they never take ownership -//! of caller memory. -//! * **lower** functions (`Rust -> c_*`) hand an owned, heap-allocated value to -//! the foreign caller, who later releases it through the matching -//! `weaveffi_free_*` / `*_destroy` entry point. +//! * **lift** functions read a borrowed `(ptr, len)` parameter. They never +//! take ownership of caller memory; the borrowing variants +//! ([`lift_str`], [`lift_byte_slice`]) don't even copy. +//! * **lower** functions hand an owned value to the consumer as a +//! producer-allocated `(ptr, len)` run, which the consumer releases with +//! `{prefix}_free_bytes` (see [`free_bytes`]). //! -//! Strings cross as NUL-terminated pointers via -//! [`string_to_c_ptr`](crate::string_to_c_ptr) (freed with -//! `weaveffi_free_string`); bytes and serialized value buffers cross as a -//! `(ptr, len)` pair via [`lower_bytes`] (freed with `weaveffi_free_bytes`); -//! everything composite crosses inside a value buffer (see [`crate::buffer`]), -//! so there are no per-shape array helpers here. +//! Strings, bytes, and serialized value buffers all share this one +//! representation: a string is its UTF-8 bytes, never NUL-terminated, so an +//! interior NUL round-trips intact. A null pointer is valid only with a +//! length of `0` and denotes the empty run. -// ── Lifting: C ABI slot -> Rust value ───────────────────────────────────── - -/// Copy a foreign byte buffer (`ptr` + `len`) into an owned `Vec`. +/// Borrow a `(ptr, len)` parameter as a byte slice for a caller-chosen +/// lifetime, without copying. /// -/// A null `ptr` (or `len == 0`) yields an empty vector. The returned vector -/// owns its bytes; the caller's buffer is left untouched. +/// Returns `None` when `ptr` is null but `len` isn't `0`, which the thunk +/// reports as a marshalling failure. /// /// # Safety /// -/// When `ptr` is non-null it must point to at least `len` initialized bytes -/// that stay valid for the duration of the call. +/// When `ptr` is non-null it must point to `len` initialized bytes that stay +/// valid and unmodified for the whole lifetime `'a`. Generated thunks bound +/// `'a` by the call, matching the contract that parameters are borrowed for +/// the call's duration. #[must_use] -pub unsafe fn lift_bytes(ptr: *const u8, len: usize) -> Vec { - if ptr.is_null() || len == 0 { - return Vec::new(); +pub unsafe fn lift_byte_slice<'a>(ptr: *const u8, len: usize) -> Option<&'a [u8]> { + if ptr.is_null() { + return (len == 0).then_some(&[][..]); } - // SAFETY: caller guarantees `ptr` covers `len` initialized bytes. - unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec() + // SAFETY: the caller guarantees `ptr` covers `len` bytes valid for `'a`. + Some(unsafe { std::slice::from_raw_parts(ptr, len) }) } -/// Borrow a foreign byte buffer (`ptr` + `len`) as a `&[u8]` slice for the -/// lifetime `'a` the caller chooses. +/// Copy a `(ptr, len)` parameter into an owned `Vec`. /// -/// A null `ptr` (or `len == 0`) yields an empty slice. No copy is made, so this -/// is the marshalling for a borrowed `&[u8]` parameter. +/// Returns `None` when `ptr` is null but `len` isn't `0`. /// /// # Safety /// -/// When `ptr` is non-null it must point to at least `len` initialized bytes -/// that remain valid and immutable for the entire chosen lifetime `'a`. +/// Same contract as [`lift_byte_slice`], for the duration of the call. #[must_use] -pub unsafe fn lift_byte_slice<'a>(ptr: *const u8, len: usize) -> &'a [u8] { - if ptr.is_null() || len == 0 { - return &[]; - } - // SAFETY: caller guarantees `ptr` covers `len` bytes valid for `'a`. - unsafe { std::slice::from_raw_parts(ptr, len) } +pub unsafe fn lift_bytes(ptr: *const u8, len: usize) -> Option> { + // SAFETY: forwarded from the caller. + unsafe { lift_byte_slice(ptr, len) }.map(<[u8]>::to_vec) +} + +/// Borrow a `(ptr, len)` string parameter as a `&str`, validating UTF-8, +/// without copying. +/// +/// Returns `None` when `ptr` is null but `len` isn't `0`, or when the bytes +/// aren't valid UTF-8. +/// +/// # Safety +/// +/// Same contract as [`lift_byte_slice`]. +#[must_use] +pub unsafe fn lift_str<'a>(ptr: *const u8, len: usize) -> Option<&'a str> { + // SAFETY: forwarded from the caller. + std::str::from_utf8(unsafe { lift_byte_slice(ptr, len) }?).ok() } -// ── Lowering: owned Rust value -> C ABI slot ───────────────────────────── +/// Copy a `(ptr, len)` string parameter into an owned `String`, validating +/// UTF-8. +/// +/// Returns `None` when `ptr` is null but `len` isn't `0`, or when the bytes +/// aren't valid UTF-8. +/// +/// # Safety +/// +/// Same contract as [`lift_byte_slice`], for the duration of the call. +#[must_use] +pub unsafe fn lift_string(ptr: *const u8, len: usize) -> Option { + // SAFETY: forwarded from the caller. + unsafe { lift_str(ptr, len) }.map(str::to_owned) +} -/// Lower an owned byte buffer into a heap allocation the caller frees with -/// `weaveffi_free_bytes`, writing the byte count through `out_len`. +/// Turn an owned byte vector into the producer-allocated `(ptr, len)` run a +/// return slot or async result carries, to be released with +/// [`free_bytes`]. An empty vector yields `(null, 0)` and allocates nothing. /// -/// An empty buffer yields a null pointer and a length of `0`. The allocation is -/// a boxed slice, matching the layout [`free_bytes`](crate::free_bytes) -/// reconstructs. +/// When the vector's capacity equals its length (as it does for every +/// encoding [`encode_value`](crate::encode_value) produces) the conversion +/// reuses the allocation as is; otherwise it's shrunk to fit first. +#[must_use] +pub fn bytes_into_raw(data: Vec) -> (*const u8, usize) { + if data.is_empty() { + return (std::ptr::null(), 0); + } + let len = data.len(); + let ptr = Box::into_raw(data.into_boxed_slice()) + .cast::() + .cast_const(); + crate::leak::track(crate::leak::ALLOCATIONS, 1); + (ptr, len) +} + +/// Lower an owned byte vector into a return value plus the trailing +/// `size_t* out_len` slot. See [`bytes_into_raw`]. /// /// # Safety /// -/// `out_len`, when non-null, must point to a writable `usize`. +/// `out_len` must be null or point to a writable `usize`. pub unsafe fn lower_bytes(data: Vec, out_len: *mut usize) -> *const u8 { + let (ptr, len) = bytes_into_raw(data); if !out_len.is_null() { - // SAFETY: caller guarantees `out_len` is writable when non-null. - unsafe { *out_len = data.len() }; + // SAFETY: the caller guarantees `out_len` is writable when non-null. + unsafe { *out_len = len }; } - if data.is_empty() { - return std::ptr::null(); + ptr +} + +/// Lower an owned string as its UTF-8 bytes, exactly like [`lower_bytes`]. +/// +/// # Safety +/// +/// Same contract as [`lower_bytes`]. +pub unsafe fn lower_string(s: String, out_len: *mut usize) -> *const u8 { + // SAFETY: forwarded from the caller. + unsafe { lower_bytes(s.into_bytes(), out_len) } +} + +/// The body of `{prefix}_free_bytes`: release a run returned by this runtime +/// (a string, bytes, or a value buffer). A null `ptr` is a no-op. +/// +/// # Safety +/// +/// `ptr` must be null or a pointer this runtime returned together with +/// `len`, released exactly once. +pub unsafe fn free_bytes(ptr: *mut u8, len: usize) { + if ptr.is_null() { + return; } - let boxed = data.into_boxed_slice(); - Box::into_raw(boxed) as *const u8 + crate::leak::track(crate::leak::ALLOCATIONS, -1); + // SAFETY: `bytes_into_raw` produced `ptr` from a `Box<[u8]>` of `len` + // bytes, and the caller hands it back exactly once. + drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) }); } #[cfg(test)] mod tests { use super::*; - use crate::free_bytes; #[test] fn bytes_roundtrip() { - let data = vec![1u8, 2, 3, 4]; let mut len = 0usize; - let ptr = unsafe { lower_bytes(data.clone(), &mut len) } as *mut u8; + let ptr = unsafe { lower_bytes(vec![1u8, 2, 3, 4], &mut len) }; assert_eq!(len, 4); - let back = unsafe { lift_bytes(ptr, len) }; - assert_eq!(back, data); - free_bytes(ptr, len); + assert_eq!(unsafe { lift_bytes(ptr, len) }, Some(vec![1, 2, 3, 4])); + unsafe { free_bytes(ptr.cast_mut(), len) }; } #[test] - fn empty_bytes_is_null() { + fn empty_is_null_and_null_is_empty() { let mut len = 99usize; let ptr = unsafe { lower_bytes(Vec::new(), &mut len) }; assert!(ptr.is_null()); assert_eq!(len, 0); + assert_eq!(unsafe { lift_str(std::ptr::null(), 0) }, Some("")); + assert_eq!(unsafe { lift_bytes(std::ptr::null(), 0) }, Some(vec![])); + unsafe { free_bytes(std::ptr::null_mut(), 0) }; + } + + #[test] + fn null_with_a_length_is_rejected() { + assert!(unsafe { lift_byte_slice(std::ptr::null(), 3) }.is_none()); + assert!(unsafe { lift_string(std::ptr::null(), 1) }.is_none()); + } + + #[test] + fn strings_borrow_and_keep_interior_nul() { + let text = "a\0b \u{1F980}"; + let s = unsafe { lift_str(text.as_ptr(), text.len()) }.unwrap(); + assert!(std::ptr::eq(s.as_ptr(), text.as_ptr())); + assert_eq!(s, text); + + let mut len = 0usize; + let ptr = unsafe { lower_string(text.to_string(), &mut len) }; + assert_eq!(unsafe { lift_string(ptr, len) }.as_deref(), Some(text)); + unsafe { free_bytes(ptr.cast_mut(), len) }; } #[test] - fn lift_byte_slice_is_borrow() { - let data = [9u8, 8, 7]; - let s = unsafe { lift_byte_slice(data.as_ptr(), data.len()) }; - assert_eq!(s, &data); - let empty = unsafe { lift_byte_slice::<'static>(std::ptr::null(), 0) }; - assert!(empty.is_empty()); + fn invalid_utf8_is_rejected() { + let bad = [0xFFu8, 0xFE]; + assert!(unsafe { lift_str(bad.as_ptr(), bad.len()) }.is_none()); } } diff --git a/crates/weaveffi-abi/src/error.rs b/crates/weaveffi-abi/src/error.rs new file mode 100644 index 00000000..54236fdf --- /dev/null +++ b/crates/weaveffi-abi/src/error.rs @@ -0,0 +1,510 @@ +//! The error struct every fallible symbol reports through, the reserved +//! runtime codes, and the [`ErrorReport`] mapping from producer errors. +//! +//! A synchronous call takes a caller-owned `{prefix}_error* out_err` slot; an +//! async completion receives a heap-boxed one it releases with +//! `{prefix}_error_free`. Either way the producer allocates the message (a +//! NUL-terminated C string) and the optional payload (a value buffer), so the +//! producer is also the one that frees them. + +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; + +use crate::callback::ForeignError; + +/// The reserved error code for an **untyped producer error**: the default +/// [`ErrorReport::code`], used by `Result` and other error types +/// that don't map onto a declared domain code. +pub const GENERIC_ERROR_CODE: i32 = -1; + +/// The reserved error code reporting a producer **panic**. +/// +/// Generated thunks wrap the producer call in `catch_unwind`; a panic is +/// reported through `out_err` with this code so the consumer can distinguish +/// "the producer has a bug" from any declared domain error. Validation rejects +/// error domains that try to claim a reserved code (`0`, which means success, +/// or any negative value). +pub const PANIC_ERROR_CODE: i32 = -2; + +/// The reserved error code reporting a **marshalling failure**: an argument +/// that could not be lifted at the boundary (a null pointer with a non-zero +/// length, a non-UTF-8 string, an out-of-range enum discriminant, or a +/// malformed value buffer). +/// +/// Both sides are generated from the same IDL, so a marshalling failure is a +/// producer/consumer contract violation, not a domain error; wrappers surface +/// it through the same trap channel as [`PANIC_ERROR_CODE`]. +pub const MARSHAL_ERROR_CODE: i32 = -3; + +/// The reserved error code reporting that a **consumer callback-interface +/// implementation failed**. +/// +/// A consumer's method implementation that raises reports it through the +/// vtable entry's `out_err` slot with this code (via `{prefix}_error_set`); +/// the producer aborts the call it was making and the original caller +/// observes this code with the consumer's message. See +/// [`callback`](crate::callback). +pub const FOREIGN_ERROR_CODE: i32 = -4; + +/// The reserved error code reporting that an async call was **cancelled**: +/// its cancel token fired, or the executor dropped the future before it +/// completed. The completion callback still fires exactly once, with this +/// code. +pub const CANCELLED_ERROR_CODE: i32 = -5; + +/// The error struct passed across the C ABI boundary (`{prefix}_error` in C). +/// +/// `code == 0` means success and every pointer is null. Otherwise `message` +/// is a NUL-terminated UTF-8 string and `payload_ptr`/`payload_len` +/// optionally hold the matched error code's fields serialized in the +/// [`buffer`](crate::buffer) format. The producer allocates both, and +/// [`error_clear`] (the body of `{prefix}_error_clear`) frees them. +/// +/// The struct owns its allocations, so it must not be copied bitwise while +/// it holds any; a copy would double-free on clear. +#[repr(C)] +#[derive(Debug)] +pub struct FfiError { + /// Status code. `0` means success; a positive value is a domain error + /// code and a negative value is one of the reserved runtime codes. + pub code: i32, + /// Owned, NUL-terminated UTF-8 message describing the failure, or null + /// when [`code`](Self::code) is `0`. + pub message: *const c_char, + /// Owned value buffer holding the matched error code's payload fields, or + /// null when the code declares no fields. + pub payload_ptr: *const u8, + /// Byte length of [`payload_ptr`](Self::payload_ptr); `0` when null. + pub payload_len: usize, +} + +impl Default for FfiError { + fn default() -> Self { + Self { + code: 0, + message: ptr::null(), + payload_ptr: ptr::null(), + payload_len: 0, + } + } +} + +// SAFETY: an `FfiError` exclusively owns the message and payload it points +// to (both are producer heap allocations with no thread affinity), so moving +// it to another thread moves that ownership with it. Async launchers rely on +// this to carry a boxed error from the future to the completion. +unsafe impl Send for FfiError {} + +impl FfiError { + /// Build an owned error with `code` and `message`. Interior NUL bytes are + /// dropped from the message, since C reads it as a NUL-terminated string. + #[must_use] + pub fn new(code: i32, message: &str) -> Self { + let mut err = Self::default(); + err.set(code, message); + err + } + + /// Build the error an [`ErrorReport`] value describes, including its + /// serialized payload. + #[must_use] + pub fn from_report(e: &E) -> Self { + let mut err = Self::new(e.code(), &e.message()); + err.set_payload(e.payload()); + err + } + + /// Build the error a caught unwind payload describes: a [`ForeignError`] + /// payload keeps its own code and message, and anything else is a + /// producer panic reported with [`PANIC_ERROR_CODE`]. + #[must_use] + pub fn from_panic(payload: &(dyn std::any::Any + Send)) -> Self { + if let Some(f) = payload.downcast_ref::() { + return Self::new(f.code, &f.message); + } + Self::new( + PANIC_ERROR_CODE, + &format!("producer panicked: {}", panic_message(payload)), + ) + } + + /// The error a cancelled async call completes with. + #[must_use] + pub fn cancelled() -> Self { + Self::new(CANCELLED_ERROR_CODE, "cancelled") + } + + /// The message as a string slice, or `None` when it's null or not UTF-8. + /// + /// # Safety + /// + /// `message` must be null or point to a NUL-terminated string that stays + /// valid for the borrow, which holds for any error this runtime filled. + #[must_use] + pub unsafe fn message_str(&self) -> Option<&str> { + if self.message.is_null() { + return None; + } + // SAFETY: the caller guarantees `message` is NUL-terminated and live. + unsafe { CStr::from_ptr(self.message) }.to_str().ok() + } + + /// Release the message and payload, leaving the error in the success + /// state. + fn release(&mut self) { + if !self.message.is_null() { + // SAFETY: every non-null message was produced by + // `CString::into_raw` in `set`, and is released exactly once + // because the pointer is nulled right after. + unsafe { drop(CString::from_raw(self.message.cast_mut())) }; + self.message = ptr::null(); + } + if !self.payload_ptr.is_null() { + // SAFETY: every non-null payload was produced by + // `bytes_into_raw` in `set_payload` with this length. + unsafe { crate::free_bytes(self.payload_ptr.cast_mut(), self.payload_len) }; + self.payload_ptr = ptr::null(); + } + self.payload_len = 0; + self.code = 0; + } + + // `CString::new` is infallible here: interior NUL bytes are removed first. + #[allow(clippy::missing_panics_doc)] + fn set(&mut self, code: i32, message: &str) { + self.release(); + self.code = code; + let message = if message.as_bytes().contains(&0) { + message.replace('\0', "") + } else { + message.to_owned() + }; + self.message = CString::new(message) + .expect("interior NUL bytes were removed") + .into_raw(); + } + + fn set_payload(&mut self, payload: Vec) { + let (ptr, len) = crate::convert::bytes_into_raw(payload); + self.payload_ptr = ptr; + self.payload_len = len; + } +} + +impl Drop for FfiError { + fn drop(&mut self) { + self.release(); + } +} + +/// Fill the caller's `out_err` slot with `code` and a copy of `message`, +/// releasing anything it held before. A null `out_err` is a no-op. +/// +/// # Safety +/// +/// `out_err` must be null or point to a valid, writable [`FfiError`] whose +/// pointers are null or owned by this runtime. +pub unsafe fn error_set(out_err: *mut FfiError, code: i32, message: &str) { + // SAFETY: forwarded from the caller. + if let Some(err) = unsafe { out_err.as_mut() } { + err.set(code, message); + } +} + +/// Move `value` into the caller's `out_err` slot, releasing anything it held +/// before. A null `out_err` drops `value`. +/// +/// # Safety +/// +/// Same contract as [`error_set`]. +pub unsafe fn error_store(out_err: *mut FfiError, value: FfiError) { + // SAFETY: forwarded from the caller; the old value is dropped (and its + // allocations released) by the assignment. + if let Some(err) = unsafe { out_err.as_mut() } { + *err = value; + } +} + +/// The body of `{prefix}_error_set`: fill `out_err` from a borrowed C string +/// the consumer owns. +/// +/// Callback-interface implementations call it to report a failure so that +/// the message is allocated by the producer, which is the side that frees +/// it. A null or non-UTF-8 `message` yields an empty message; the code is +/// always recorded. +/// +/// # Safety +/// +/// Same contract as [`error_set`] for `out_err`. `message` must be null or +/// point to a NUL-terminated string that stays valid for the call. +pub unsafe fn error_set_c(out_err: *mut FfiError, code: i32, message: *const c_char) { + let text = if message.is_null() { + "" + } else { + // SAFETY: the caller guarantees a NUL-terminated string. + unsafe { CStr::from_ptr(message) }.to_str().unwrap_or("") + }; + // SAFETY: forwarded from the caller. + unsafe { error_set(out_err, code, text) }; +} + +/// The body of `{prefix}_error_clear`: free any message and payload and set +/// the code to `0`. A null `err` is a no-op. +/// +/// # Safety +/// +/// Same contract as [`error_set`]. +pub unsafe fn error_clear(err: *mut FfiError) { + // SAFETY: forwarded from the caller. + if let Some(err) = unsafe { err.as_mut() } { + err.release(); + } +} + +/// The body of `{prefix}_error_free`: release a heap-boxed error delivered +/// through an async completion, including the box. A null `err` is a no-op. +/// +/// # Safety +/// +/// `err` must be null or a pointer this runtime handed to a completion +/// callback (see [`boxed_error`]), released exactly once. +pub unsafe fn error_free(err: *mut FfiError) { + if !err.is_null() { + // SAFETY: the caller guarantees `err` came from `Box::into_raw`. + drop(unsafe { Box::from_raw(err) }); + } +} + +/// Box `err` for an async completion callback, which hands ownership to the +/// consumer (released with `{prefix}_error_free`). +#[must_use] +pub fn boxed_error(err: FfiError) -> *mut FfiError { + Box::into_raw(Box::new(err)) +} + +/// Maps a producer error onto the ABI's `(code, message, payload)` triple. +/// +/// A fallible `#[weaveffi::export]` function returning `Result` reports +/// `Err(e)` through its trailing `out_err` slot using this trait. `String` and +/// `&str` errors are covered out of the box with the generic code `-1`, so +/// `Result` needs no extra code. The `#[weaveffi::error]` +/// expansion implements it for an error domain's enum: the code is the +/// variant's discriminant, the message is the enum's +/// [`Display`](std::fmt::Display) output, and the payload is the variant's +/// fields. +/// +/// To implement the trait by hand: +/// +/// ``` +/// use weaveffi_abi::ErrorReport; +/// +/// enum KvError { +/// KeyNotFound, +/// Io(String), +/// } +/// +/// impl ErrorReport for KvError { +/// fn code(&self) -> i32 { +/// match self { +/// KvError::KeyNotFound => 1001, +/// KvError::Io(_) => 1004, +/// } +/// } +/// fn message(&self) -> String { +/// match self { +/// KvError::KeyNotFound => "key not found".to_string(), +/// KvError::Io(detail) => format!("I/O error: {detail}"), +/// } +/// } +/// } +/// ``` +pub trait ErrorReport { + /// The non-zero status code written to [`FfiError::code`]. Defaults to + /// [`GENERIC_ERROR_CODE`]. + fn code(&self) -> i32 { + GENERIC_ERROR_CODE + } + + /// The human-readable message written to [`FfiError::message`]. + fn message(&self) -> String; + + /// The serialized payload fields written to [`FfiError::payload_ptr`], or + /// an empty vector for a code with no fields. + fn payload(&self) -> Vec { + Vec::new() + } +} + +impl ErrorReport for String { + fn message(&self) -> String { + self.clone() + } +} + +impl ErrorReport for &str { + fn message(&self) -> String { + (*self).to_string() + } +} + +impl ErrorReport for Box { + fn message(&self) -> String { + self.to_string() + } +} + +impl ErrorReport for Box { + fn message(&self) -> String { + self.to_string() + } +} + +/// Best-effort extraction of a panic payload's message (`&str`, `String`, +/// and [`ForeignError`] payloads; anything else yields a fixed placeholder). +#[must_use] +pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { + if let Some(s) = payload.downcast_ref::<&str>() { + (*s).to_string() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else if let Some(f) = payload.downcast_ref::() { + f.message.clone() + } else { + "producer panicked".to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn message(err: &FfiError) -> Option { + // SAFETY: every error in these tests is filled by this module. + unsafe { err.message_str() }.map(str::to_string) + } + + #[test] + fn default_is_ok() { + let err = FfiError::default(); + assert_eq!(err.code, 0); + assert!(err.message.is_null()); + assert!(err.payload_ptr.is_null()); + } + + #[test] + fn set_replaces_and_clear_resets() { + let mut err = FfiError::default(); + unsafe { error_set(&mut err, 1, "first") }; + unsafe { error_set(&mut err, 2, "second") }; + assert_eq!(err.code, 2); + assert_eq!(message(&err).as_deref(), Some("second")); + unsafe { error_clear(&mut err) }; + assert_eq!(err.code, 0); + assert!(err.message.is_null()); + } + + #[test] + fn null_slots_are_no_ops() { + unsafe { + error_set(ptr::null_mut(), 1, "x"); + error_set_c(ptr::null_mut(), 1, ptr::null()); + error_clear(ptr::null_mut()); + error_free(ptr::null_mut()); + error_store(ptr::null_mut(), FfiError::new(3, "dropped")); + } + } + + #[test] + fn interior_nul_is_removed_from_messages() { + let err = FfiError::new(1, "hel\0lo"); + assert_eq!(message(&err).as_deref(), Some("hello")); + } + + #[test] + fn set_c_copies_a_borrowed_message() { + let mut err = FfiError::default(); + let msg = CString::new("from the consumer").unwrap(); + unsafe { error_set_c(&mut err, FOREIGN_ERROR_CODE, msg.as_ptr()) }; + drop(msg); + assert_eq!(err.code, FOREIGN_ERROR_CODE); + assert_eq!(message(&err).as_deref(), Some("from the consumer")); + unsafe { error_set_c(&mut err, 3, ptr::null()) }; + assert_eq!(err.code, 3); + assert_eq!(message(&err).as_deref(), Some("")); + } + + enum DomainError { + NotFound, + Io(String), + } + + impl ErrorReport for DomainError { + fn code(&self) -> i32 { + match self { + DomainError::NotFound => 1001, + DomainError::Io(_) => 1004, + } + } + fn message(&self) -> String { + match self { + DomainError::NotFound => "not found".to_string(), + DomainError::Io(detail) => format!("io: {detail}"), + } + } + fn payload(&self) -> Vec { + match self { + DomainError::NotFound => Vec::new(), + DomainError::Io(detail) => crate::encode_value(detail), + } + } + } + + #[test] + fn reports_carry_code_message_and_payload() { + let err = FfiError::from_report(&DomainError::NotFound); + assert_eq!(err.code, 1001); + assert_eq!(message(&err).as_deref(), Some("not found")); + assert!(err.payload_ptr.is_null()); + + let err = FfiError::from_report(&DomainError::Io("disk".into())); + assert_eq!(err.code, 1004); + let payload = unsafe { std::slice::from_raw_parts(err.payload_ptr, err.payload_len) }; + assert_eq!(crate::decode_value::(payload).unwrap(), "disk"); + + let mut slot = FfiError::default(); + unsafe { error_store(&mut slot, err) }; + assert_eq!(slot.code, 1004); + } + + #[test] + fn string_errors_use_the_generic_code() { + let e = "boom".to_string(); + assert_eq!(ErrorReport::code(&e), GENERIC_ERROR_CODE); + assert_eq!(ErrorReport::message(&e), "boom"); + assert_eq!(ErrorReport::code(&"boom"), GENERIC_ERROR_CODE); + } + + #[test] + fn panic_payloads_map_to_panic_or_foreign_codes() { + let payload: Box = Box::new(ForeignError { + code: FOREIGN_ERROR_CODE, + message: "listener threw".into(), + }); + let err = FfiError::from_panic(&*payload); + assert_eq!(err.code, FOREIGN_ERROR_CODE); + assert_eq!(message(&err).as_deref(), Some("listener threw")); + + let payload: Box = Box::new("bug"); + let err = FfiError::from_panic(&*payload); + assert_eq!(err.code, PANIC_ERROR_CODE); + assert_eq!(message(&err).as_deref(), Some("producer panicked: bug")); + } + + #[test] + fn boxed_errors_free_through_error_free() { + let raw = boxed_error(FfiError::cancelled()); + assert_eq!(unsafe { (*raw).code }, CANCELLED_ERROR_CODE); + unsafe { error_free(raw) }; + } +} diff --git a/crates/weaveffi-abi/src/iter.rs b/crates/weaveffi-abi/src/iter.rs new file mode 100644 index 00000000..5442a18d --- /dev/null +++ b/crates/weaveffi-abi/src/iter.rs @@ -0,0 +1,147 @@ +//! Lazily pulled iterators: the producer's [`Iter`] and the opaque handle a +//! launcher returns for it. + +use std::sync::{Mutex, PoisonError}; + +/// An owned, type-erased iterator returned by a producer function whose IDL +/// return type is `iter`. +/// +/// Build one from any iterator with [`Iter::new`]. The `#[weaveffi::module]` +/// expansion boxes it behind an opaque [`IterHandle`], pulls one element per +/// `_next` call, and drops it in `_destroy`. Pulling elements lazily (rather +/// than materializing a `Vec`) is what distinguishes an `iter` return from +/// a `[T]` (list) return. +pub struct Iter { + inner: Box + Send>, +} + +impl Iter { + /// Wrap any `Send + 'static` iterator as a WeaveFFI iterator. + /// + /// Accepts anything `IntoIterator`, so `Iter::new(vec)`, `Iter::new(0..n)`, + /// and `Iter::new(map.into_values())` all work. + pub fn new(iter: I) -> Self + where + I: IntoIterator, + I::IntoIter: Send + 'static, + { + Self { + inner: Box::new(iter.into_iter()), + } + } +} + +impl Iterator for Iter { + type Item = T; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl std::fmt::Debug for Iter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Iter").finish_non_exhaustive() + } +} + +/// The opaque iterator handle a launcher returns (`{prefix}_..._Iterator*` +/// in C). +/// +/// `_next` is internally synchronized: the iterator sits behind a mutex, so +/// concurrent calls on one handle are safe (though they race for elements). +#[derive(Debug)] +pub struct IterHandle { + inner: Mutex>, +} + +/// Box `iter` behind a new handle for the consumer, who releases it with +/// `_destroy` (see [`iter_destroy`]). +#[must_use] +pub fn iter_into_raw(iter: Iter) -> *mut IterHandle { + crate::leak::track(crate::leak::ITERATORS, 1); + Box::into_raw(Box::new(IterHandle { + inner: Mutex::new(iter), + })) +} + +/// Pull the next element from a handle: `Some(None)` when the iterator is +/// exhausted, `None` when `handle` is null. +/// +/// A producer panic from an earlier `next` doesn't wedge the handle: the +/// mutex's poison flag is ignored, and the iterator keeps whatever state the +/// panic left it in. +/// +/// # Safety +/// +/// `handle` must be null or a live pointer from [`iter_into_raw`] that isn't +/// destroyed during the call. +pub unsafe fn iter_next(handle: *const IterHandle) -> Option> { + // SAFETY: the caller guarantees a live handle or null. + let handle = unsafe { handle.as_ref() }?; + let mut iter = handle.inner.lock().unwrap_or_else(PoisonError::into_inner); + Some(iter.next()) +} + +/// The body of every iterator's `_destroy` symbol: drop the iterator and its +/// handle. A null handle is a no-op, and a panicking `Drop` is swallowed +/// because a destructor has no `out_err` slot and must never unwind into C. +/// +/// # Safety +/// +/// `handle` must be null or a live pointer from [`iter_into_raw`], released +/// exactly once. +pub unsafe fn iter_destroy(handle: *mut IterHandle) { + if handle.is_null() { + return; + } + crate::leak::track(crate::leak::ITERATORS, -1); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + // SAFETY: the caller hands back a pointer from `Box::into_raw` + // exactly once. + drop(unsafe { Box::from_raw(handle) }); + })); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pulls_until_exhausted() { + let h = iter_into_raw(Iter::new(vec![1, 2])); + unsafe { + assert_eq!(iter_next(h), Some(Some(1))); + assert_eq!(iter_next(h), Some(Some(2))); + assert_eq!(iter_next(h), Some(None)); + assert_eq!(iter_next::(std::ptr::null()), None); + iter_destroy(h); + iter_destroy::(std::ptr::null_mut()); + } + } + + #[test] + fn concurrent_next_yields_each_element_once() { + let h = iter_into_raw(Iter::new(0..10_000u32)); + let addr = h as usize; + let threads: Vec<_> = (0..8) + .map(|_| { + std::thread::spawn(move || { + let h = addr as *const IterHandle; + let mut got = Vec::new(); + while let Some(Some(x)) = unsafe { iter_next(h) } { + got.push(x); + } + got + }) + }) + .collect(); + let mut all: Vec = threads + .into_iter() + .flat_map(|t| t.join().unwrap()) + .collect(); + all.sort_unstable(); + assert_eq!(all, (0..10_000).collect::>()); + unsafe { iter_destroy(h) }; + } +} diff --git a/crates/weaveffi-abi/src/leak.rs b/crates/weaveffi-abi/src/leak.rs new file mode 100644 index 00000000..c23e39b2 --- /dev/null +++ b/crates/weaveffi-abi/src/leak.rs @@ -0,0 +1,90 @@ +//! Debug leak counters behind the `leak-check` cargo feature. +//! +//! With the feature on, the runtime counts every resource it hands to a +//! consumer and every release, so a test harness can assert that a consumer +//! released everything it was given (after forcing its garbage collector or +//! finalizers to run). `export_runtime!` exports the counters as +//! `uint64_t {prefix}_debug_live(int32_t kind)`. +//! +//! The symbol is exported whether or not the feature is on, so a consumer +//! never has to know how the producer was built: without the feature every +//! counter reads `0`. Check [`ENABLED`] to tell "nothing is live" from +//! "nothing is counted". + +/// Whether this build counts live resources (the `leak-check` feature). +pub const ENABLED: bool = cfg!(feature = "leak-check"); + +/// Interface object references held by the consumer: one per pointer a +/// thunk returns, per `_clone`, and per object token written into a value +/// buffer; released by `_destroy` and by adopting a token. +pub const OBJECTS: i32 = 0; + +/// Consumer callback-interface implementations the producer still holds +/// (each is released through the vtable's `free` entry). +pub const CALLBACKS: i32 = 1; + +/// Iterator handles a launcher returned that haven't been destroyed. +pub const ITERATORS: i32 = 2; + +/// Cancel-token references, both the consumer's (`create` until `destroy`) +/// and the producer's (one per in-flight cancellable call). +pub const TOKENS: i32 = 3; + +/// Returned string, bytes, and value-buffer allocations (including error +/// payloads) that haven't been passed to `{prefix}_free_bytes`. +pub const ALLOCATIONS: i32 = 4; + +#[cfg(feature = "leak-check")] +mod counters { + use std::sync::atomic::{AtomicI64, Ordering}; + + static LIVE: [AtomicI64; 5] = [ + AtomicI64::new(0), + AtomicI64::new(0), + AtomicI64::new(0), + AtomicI64::new(0), + AtomicI64::new(0), + ]; + + pub(super) fn track(kind: i32, delta: i64) { + if let Some(c) = usize::try_from(kind).ok().and_then(|k| LIVE.get(k)) { + c.fetch_add(delta, Ordering::Relaxed); + } + } + + pub(super) fn live(kind: i32) -> u64 { + usize::try_from(kind) + .ok() + .and_then(|k| LIVE.get(k)) + .map_or(0, |c| u64::try_from(c.load(Ordering::Relaxed)).unwrap_or(0)) + } +} + +/// Adjust the live count of `kind` by `delta`. Compiles to nothing without +/// the `leak-check` feature. +#[inline] +pub(crate) fn track(kind: i32, delta: i64) { + #[cfg(feature = "leak-check")] + counters::track(kind, delta); + #[cfg(not(feature = "leak-check"))] + let _ = (kind, delta); +} + +/// The body of `{prefix}_debug_live`: how many resources of `kind` (one of +/// [`OBJECTS`], [`CALLBACKS`], [`ITERATORS`], [`TOKENS`], or +/// [`ALLOCATIONS`]) are live right now. +/// +/// Returns `0` for an unknown kind, and for every kind when the +/// `leak-check` feature is off. +#[must_use] +pub fn debug_live(kind: i32) -> u64 { + #[cfg(feature = "leak-check")] + { + counters::live(kind) + } + #[cfg(not(feature = "leak-check"))] + { + let _ = kind; + 0 + } +} diff --git a/crates/weaveffi-abi/src/lib.rs b/crates/weaveffi-abi/src/lib.rs index a066ee03..53719bae 100644 --- a/crates/weaveffi-abi/src/lib.rs +++ b/crates/weaveffi-abi/src/lib.rs @@ -1,895 +1,117 @@ -//! C ABI runtime: error struct, memory helpers, reference-counted objects, -//! callback-interface vtables, the value-buffer codec, and the async spawner. +//! C ABI runtime: the error struct, memory helpers, reference-counted +//! objects, cancel tokens, callback-interface vtables, iterators, the +//! value-buffer codec, and the async executor hook. //! -//! The normative description of the contract this crate implements is -//! `docs/src/reference/abi.md` in the WeaveFFI repository. +//! Producers don't use this crate directly. The `#[weaveffi::module]` +//! expansion and `weaveffi::export_runtime!()` call into it (re-exported as +//! `weaveffi::abi`), so every `unsafe` pointer operation a producer performs +//! has one audited home. The normative description of the contract it +//! implements is `docs/src/reference/abi.md` in the WeaveFFI repository. #![deny(missing_docs)] #![warn(clippy::missing_errors_doc)] #![warn(clippy::missing_panics_doc)] +#![warn(clippy::missing_safety_doc)] #![warn(clippy::doc_markdown)] -#![allow(non_camel_case_types)] #![allow(unsafe_code)] -#![allow(clippy::not_unsafe_ptr_arg_deref)] pub mod buffer; pub mod callback; +pub mod cancel; pub mod convert; -mod macros; +pub mod error; +pub mod iter; +pub mod leak; pub mod object; pub mod spawn; pub use buffer::{ decode_value, encode_value, BufferDecodeError, BufferReader, BufferValue, BufferWriter, + ByValue, FixedWidth, }; pub use callback::{ - check_foreign_error, defer_foreign_error, lift_callback, raise_foreign_error, - take_foreign_error, CallbackInterface, ForeignCallback, ForeignError, Vtable, + defer_foreign_error, foreign_status, lift_callback, raise_foreign_error, take_foreign_error, + CallbackInterface, ForeignCallback, ForeignError, ThunkScope, Vtable, }; -pub use convert::{lift_byte_slice, lift_bytes, lower_bytes}; +pub use cancel::{ + cancel_token_cancel, cancel_token_create, cancel_token_destroy, cancel_token_is_cancelled, + CancelToken, Cancellable, FfiCancelToken, +}; +pub use convert::{ + bytes_into_raw, free_bytes, lift_byte_slice, lift_bytes, lift_str, lift_string, lower_bytes, + lower_string, +}; +pub use error::{ + boxed_error, error_clear, error_free, error_set, error_set_c, error_store, panic_message, + ErrorReport, FfiError, CANCELLED_ERROR_CODE, FOREIGN_ERROR_CODE, GENERIC_ERROR_CODE, + MARSHAL_ERROR_CODE, PANIC_ERROR_CODE, +}; +pub use iter::{iter_destroy, iter_into_raw, iter_next, Iter, IterHandle}; +pub use leak::debug_live; pub use object::{ lower_object, lower_object_opt, object_arc, object_clone, object_destroy, object_from_token, object_ref, object_to_token, }; -pub use spawn::{set_spawner, spawn, BoxFuture, CatchUnwind, Spawner, SpawnerAlreadySet}; - -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; -use std::ptr; -use std::sync::atomic::{AtomicBool, Ordering}; +pub use spawn::{ + block_on, run_async, set_spawner, spawn, BoxFuture, CatchUnwind, Spawner, SpawnerAlreadySet, +}; /// 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, the object or callback-interface -/// conventions, 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. -/// -/// Revision 2 introduced reference-counted objects (`_clone`), object tokens -/// inside value buffers, callback-interface vtables, `weaveffi_error_set`, -/// and the foreign error code, and removed handles and the arena. -pub const ABI_VERSION: u32 = 2; - -/// Return [`ABI_VERSION`]; the body behind the exported -/// `weaveffi_abi_version` thunk. -#[must_use] -pub const fn abi_version() -> u32 { - ABI_VERSION -} - -/// Error struct passed across the C ABI boundary. -/// -/// # Safety -/// -/// - `message` is a NUL-terminated UTF-8 C string allocated by Rust and must be -/// released by calling `weaveffi_error_clear` or `weaveffi_free_string`. -/// - `payload_ptr`, when non-null, is a Rust-allocated value buffer of -/// `payload_len` bytes (the matched error code's fields serialized in the -/// [`buffer`] format), released by `weaveffi_error_clear`. -/// - This struct must not be copied while it owns a message or payload -/// pointer, as that would lead to double-free on clear. -#[repr(C)] -#[derive(Debug)] -pub struct weaveffi_error { - /// Status code. `0` means success; any non-zero value indicates failure. - pub code: i32, - /// Owned, NUL-terminated UTF-8 message describing the failure, or null when - /// [`code`](Self::code) is `0`. Freed by `weaveffi_error_clear`. - pub message: *const c_char, - /// Owned value buffer holding the matched error code's payload fields - /// serialized in the [`buffer`] format, or null when the code declares no - /// fields. Freed by `weaveffi_error_clear`. - pub payload_ptr: *const u8, - /// Byte length of [`payload_ptr`](Self::payload_ptr); `0` when null. - pub payload_len: usize, -} - -impl Default for weaveffi_error { - fn default() -> Self { - Self { - code: 0, - message: ptr::null(), - payload_ptr: ptr::null(), - payload_len: 0, - } - } -} - -/// Release any owned message and payload on `err`, leaving both null. Does not -/// touch the code. -fn release_error_allocations(err: &mut weaveffi_error) { - if !err.message.is_null() { - // SAFETY: message was allocated via `CString::into_raw` in this module - unsafe { drop(CString::from_raw(err.message as *mut c_char)) }; - err.message = ptr::null(); - } - if !err.payload_ptr.is_null() { - free_bytes(err.payload_ptr as *mut u8, err.payload_len); - err.payload_ptr = ptr::null(); - } - err.payload_len = 0; -} - -/// Set the error to OK (code = 0) and free any prior message and payload. -pub fn error_set_ok(out_err: *mut weaveffi_error) { - if out_err.is_null() { - return; - } - // SAFETY: pointer checked for null above - let err = unsafe { &mut *out_err }; - release_error_allocations(err); - err.code = 0; -} - -/// Populate an error with the given code and message (copying message), -/// clearing any prior payload. -// `CString::new` is infallible here because interior NUL bytes are stripped -// from `message` immediately below, so there is no reachable panic to document. -#[allow(clippy::missing_panics_doc)] -pub fn error_set(out_err: *mut weaveffi_error, code: i32, message: &str) { - if out_err.is_null() { - return; - } - // SAFETY: pointer checked for null above - let err = unsafe { &mut *out_err }; - release_error_allocations(err); - err.code = code; - let owned_message = message.replace('\0', ""); - let cstr = CString::new(owned_message).expect("CString::new sanitized input"); - err.message = cstr.into_raw(); -} - -/// The body of the exported `weaveffi_error_set` thunk: fill `out_err` from a -/// borrowed C string the consumer owns. -/// -/// Callback-interface implementations call this to report a failure without -/// allocating the message with a foreign allocator (the producer frees -/// `message` with its own). A null or non-UTF-8 `message` yields an empty -/// message; the code is always recorded. -pub fn error_set_c(out_err: *mut weaveffi_error, code: i32, message: *const c_char) { - let message = c_ptr_to_string(message).unwrap_or_default(); - error_set(out_err, code, &message); -} - -/// Populate an error with a code, message, and an owned payload buffer (the -/// matched error code's fields serialized in the [`buffer`] format). -/// -/// An empty `payload` leaves the payload slots null, matching a code that -/// declares no fields. -pub fn error_set_with_payload( - out_err: *mut weaveffi_error, - code: i32, - message: &str, - payload: Vec, -) { - error_set(out_err, code, message); - if out_err.is_null() || payload.is_empty() { - return; - } - // SAFETY: `out_err` checked for null above. - let err = unsafe { &mut *out_err }; - let mut len = 0usize; - // SAFETY: `&mut len` is a writable usize. - err.payload_ptr = unsafe { convert::lower_bytes(payload, &mut len) }; - err.payload_len = len; -} - -/// Maps a producer error onto the ABI's `(code, message)` pair. -/// -/// A fallible `#[weaveffi::export]` function returning `Result` reports -/// `Err(e)` through its trailing `out_err` slot by writing -/// [`ErrorReport::code`] and [`ErrorReport::message`] into the caller's -/// [`weaveffi_error`]. `String` and `&str` errors are covered out of the box -/// with the generic code `-1`, so `Result` needs no extra code. -/// -/// The `#[weaveffi::error]` expansion implements this trait for an IDL error -/// domain's enum, surfacing the domain's named codes so consumers can react -/// to each case. Because there's no blanket implementation, your error type -/// is free to also derive or implement [`Display`](std::fmt::Display) and -/// [`std::error::Error`] (for example via `thiserror`). To implement the -/// trait by hand: -/// -/// ``` -/// use weaveffi_abi::ErrorReport; -/// -/// enum KvError { -/// KeyNotFound, -/// Io(String), -/// } -/// -/// impl ErrorReport for KvError { -/// fn code(&self) -> i32 { -/// match self { -/// KvError::KeyNotFound => 1001, -/// KvError::Io(_) => 1004, -/// } -/// } -/// fn message(&self) -> String { -/// match self { -/// KvError::KeyNotFound => "key not found".to_string(), -/// KvError::Io(detail) => format!("I/O error: {detail}"), -/// } -/// } -/// } -/// ``` -pub trait ErrorReport { - /// The non-zero status code written to [`weaveffi_error::code`]. Defaults to - /// [`GENERIC_ERROR_CODE`]. - fn code(&self) -> i32 { - GENERIC_ERROR_CODE - } - - /// The human-readable message written to [`weaveffi_error::message`]. - fn message(&self) -> String; - - /// The serialized payload fields written to - /// [`weaveffi_error::payload_ptr`], or an empty vector for a code with no - /// fields. The `#[weaveffi::error]` expansion overrides this for variants - /// that carry data; the default reports no payload. - fn payload(&self) -> Vec { - Vec::new() - } -} - -impl ErrorReport for String { - fn message(&self) -> String { - self.clone() - } -} - -impl ErrorReport for &str { - fn message(&self) -> String { - (*self).to_string() - } -} - -impl ErrorReport for Box { - fn message(&self) -> String { - self.to_string() - } -} - -impl ErrorReport for Box { - fn message(&self) -> String { - self.to_string() - } -} - -/// Convenience adapter: map a `Result` to `Option` by writing into `out_err`. -/// -/// `Err(e)` is reported through [`ErrorReport`], so the generic `-1` code is -/// used for [`Display`](std::fmt::Display) errors and the domain code (plus -/// any payload) for types that implement [`ErrorReport`] directly. -pub fn result_to_out_err( - result: Result, - out_err: *mut weaveffi_error, -) -> Option { - match result { - Ok(value) => { - error_set_ok(out_err); - Some(value) - } - Err(e) => { - error_set_with_payload(out_err, e.code(), &e.message(), e.payload()); - None - } - } -} - -/// The reserved error code for an **untyped producer error**: the default -/// [`ErrorReport::code`], used by `Result` and other error types -/// that don't map onto a declared domain code. -pub const GENERIC_ERROR_CODE: i32 = -1; - -/// The reserved error code reporting a producer **panic**. -/// -/// Generated thunks wrap the producer call in `catch_unwind`; a panic is -/// reported through `out_err` with this code so the consumer can distinguish -/// "the producer has a bug" from any declared domain error. Validation rejects -/// error domains that try to claim a reserved code (`0`, which means success, -/// or any negative value). -pub const PANIC_ERROR_CODE: i32 = -2; - -/// The reserved error code reporting a **marshalling failure**: an argument -/// that could not be lifted at the boundary (a null or invalid pointer, a -/// non-UTF-8 string, an out-of-range enum discriminant, or a malformed value -/// buffer). +/// Every producer exports it as `{prefix}_abi_version()` (via +/// `weaveffi::export_runtime!()`), and every generated consumer compares it +/// with the revision it was generated against when it loads the library, +/// turning a silent memory-layout mismatch into a clear error. /// -/// Both sides are generated from the same IDL, so a marshalling failure is a -/// producer/consumer contract violation, not a domain error; wrappers surface -/// it through the same trap channel as [`PANIC_ERROR_CODE`]. -pub const MARSHAL_ERROR_CODE: i32 = -3; - -/// The reserved error code reporting that a **consumer callback-interface -/// implementation failed**. +/// The number only changes when the runtime surface (the error layout, the +/// value-buffer encoding, the object or callback-interface conventions, or +/// the set and signatures of the runtime symbols) changes incompatibly. It's +/// independent of the crate version and of the IDL schema version. /// -/// A consumer's method implementation that raises reports it through the -/// vtable entry's `out_err` slot with this code (via `weaveffi_error_set`); -/// the producer aborts the call it was making and the original caller -/// observes this code with the consumer's message. See [`callback`]. -pub const FOREIGN_ERROR_CODE: i32 = -4; - -/// Best-effort extraction of a panic payload's message (`&str`, `String`, -/// and [`ForeignError`] payloads; anything else yields a fixed placeholder). -pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { - if let Some(s) = payload.downcast_ref::<&str>() { - (*s).to_string() - } else if let Some(s) = payload.downcast_ref::() { - s.clone() - } else if let Some(f) = payload.downcast_ref::() { - f.message.clone() - } else { - "producer panicked".to_string() - } -} - -/// Report a caught unwind through `out_err`. A [`ForeignError`] payload (a -/// consumer callback-interface implementation raised, or returned a value -/// the producer could not lift) is reported with the payload's own code -/// (normally [`FOREIGN_ERROR_CODE`]) and message; any other payload is a -/// producer panic and is reported as [`PANIC_ERROR_CODE`]. Generated thunks -/// call this from their `catch_unwind` error arm. -pub fn error_set_panic(out_err: *mut weaveffi_error, payload: &(dyn std::any::Any + Send)) { - if let Some(f) = payload.downcast_ref::() { - error_set(out_err, f.code, &f.message); - return; - } - error_set( - out_err, - PANIC_ERROR_CODE, - &format!("producer panicked: {}", panic_message(payload)), - ); -} - -/// Allocate a new C string from a Rust string, returning an owned pointer. -/// Caller must later free with `weaveffi_free_string` or `weaveffi_error_clear`. -// `CString::new` is infallible here because interior NUL bytes are stripped -// before the call, so there is no reachable panic to document. -#[allow(clippy::missing_panics_doc)] -pub fn string_to_c_ptr(s: impl AsRef) -> *const c_char { - let s = s.as_ref(); - let sanitized = if s.as_bytes().contains(&0) { - s.replace('\0', "") - } else { - s.to_owned() - }; - let cstr = CString::new(sanitized).expect("string_to_c_ptr: unexpected NUL after sanitization"); - cstr.into_raw() -} - -/// Free a C string previously allocated by this runtime. -pub fn free_string(ptr: *const c_char) { - if ptr.is_null() { - return; - } - // SAFETY: pointer must be returned from `CString::into_raw` - unsafe { drop(CString::from_raw(ptr as *mut c_char)) }; -} - -/// Free a byte buffer previously allocated by Rust and returned to foreign code. -pub fn free_bytes(ptr: *mut u8, len: usize) { - if ptr.is_null() { - return; - } - // SAFETY: reconstructs the original Box<[u8]> for deallocation - unsafe { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len))) }; -} +/// Revision 3 prefixed every runtime symbol with the library's own C prefix, +/// passed strings as `(ptr, len)` UTF-8 runs freed with `{prefix}_free_bytes` +/// (removing `free_string`), made cancel tokens reference counted, and added +/// [`CANCELLED_ERROR_CODE`] and the per-module contract checksums. +pub const ABI_VERSION: u32 = 3; /// Fixed alignment used for every Wasm linear-memory allocation handed to JS. /// -/// 8 bytes over-aligns scalar/byte buffers but is required for the `{i32 ptr, -/// i32 len}` and wider return slots that JS reads back through `DataView`. +/// 8 bytes over-aligns scalar and byte buffers but is required for the +/// `{i32 ptr, i32 len}` and wider return slots that JS reads back through +/// `DataView`. #[cfg(target_arch = "wasm32")] const WASM_ALLOC_ALIGN: usize = 8; -/// Allocate `size` bytes in this module's Wasm linear memory. +/// The body of `{prefix}_alloc` (`wasm32` only): allocate `size` bytes in this +/// module's linear memory. /// -/// The Wasm backend has no host-provided allocator, so generated JS glue calls -/// the `weaveffi_alloc` thunk (emitted by [`export_runtime!`]) to stage input -/// strings/byte buffers and to reserve struct-return (`sret`) slots. The caller -/// must release the block with [`wasm_dealloc`] using the *same* `size`. +/// The Wasm backend has no host-provided allocator, so the generated JS glue +/// stages input strings and buffers, and reserves return slots, through this. +/// The caller releases the block with [`wasm_dealloc`] using the same `size`. #[cfg(target_arch = "wasm32")] +#[must_use] pub fn wasm_alloc(size: usize) -> *mut u8 { let size = size.max(1); - let layout = std::alloc::Layout::from_size_align(size, WASM_ALLOC_ALIGN) - .expect("weaveffi_alloc: invalid layout"); - // SAFETY: `size >= 1` and the alignment is a non-zero power of two. - unsafe { std::alloc::alloc(layout) } -} - -/// Release a block previously returned by [`wasm_alloc`]. -/// -/// `size` must match the original allocation request (JS retains it). -#[cfg(target_arch = "wasm32")] -pub fn wasm_dealloc(ptr: *mut u8, size: usize) { - if ptr.is_null() { - return; - } - let size = size.max(1); - let layout = std::alloc::Layout::from_size_align(size, WASM_ALLOC_ALIGN) - .expect("weaveffi_dealloc: invalid layout"); - // SAFETY: `ptr` came from `wasm_alloc` with this exact layout. - unsafe { std::alloc::dealloc(ptr, layout) }; -} - -/// Clear an error by freeing any message and zeroing fields. -pub fn error_clear(err: *mut weaveffi_error) { - error_set_ok(err); -} - -/// Free a heap-boxed error delivered through an async completion callback. -/// -/// Async launchers hand the callback an *owned* `weaveffi_error` allocated on -/// the heap (unlike the caller-owned `out_err` slot of synchronous calls), so -/// consumers can defer reading it past the callback's return. Once the -/// consumer has copied what it needs, it releases the message, the payload, -/// and the box itself through this function. Passing null is a safe no-op. -pub fn error_free(err: *mut weaveffi_error) { - if err.is_null() { - return; + match std::alloc::Layout::from_size_align(size, WASM_ALLOC_ALIGN) { + // SAFETY: the layout has a non-zero size. + Ok(layout) => unsafe { std::alloc::alloc(layout) }, + Err(_) => std::ptr::null_mut(), } - error_clear(err); - // SAFETY: async launchers allocate the error with `Box::new`, and the - // consumer calls this exactly once with that pointer. - unsafe { drop(Box::from_raw(err)) }; } -/// Opaque cancellation token passed across the C ABI boundary. -/// -/// Foreign callers obtain a token via `weaveffi_cancel_token_create`, signal -/// cancellation with `weaveffi_cancel_token_cancel`, and release it with -/// `weaveffi_cancel_token_destroy`. -#[repr(C)] -pub struct weaveffi_cancel_token { - cancelled: AtomicBool, -} - -/// Allocate a new cancel token. The caller owns the returned pointer and must -/// eventually call `weaveffi_cancel_token_destroy`. -pub fn cancel_token_create() -> *mut weaveffi_cancel_token { - Box::into_raw(Box::new(weaveffi_cancel_token { - cancelled: AtomicBool::new(false), - })) -} - -/// Signal cancellation on the token (thread-safe). -pub fn cancel_token_cancel(token: *mut weaveffi_cancel_token) { - if token.is_null() { - return; - } - // SAFETY: pointer checked for null above - let t = unsafe { &*token }; - t.cancelled.store(true, Ordering::Release); -} - -/// Check whether the token has been cancelled (thread-safe). -pub fn cancel_token_is_cancelled(token: *const weaveffi_cancel_token) -> bool { - if token.is_null() { - return false; - } - // SAFETY: pointer checked for null above - let t = unsafe { &*token }; - t.cancelled.load(Ordering::Acquire) -} - -/// Destroy a cancel token previously created by `cancel_token_create`. -pub fn cancel_token_destroy(token: *mut weaveffi_cancel_token) { - if token.is_null() { - return; - } - // SAFETY: pointer was returned from `Box::into_raw` in `cancel_token_create` - unsafe { drop(Box::from_raw(token)) }; -} - -/// A safe, `Send` view of a foreign [`weaveffi_cancel_token`] handed to a -/// cancellable `async fn`. -/// -/// A producer marks an exported `async fn` `#[weaveffi::cancellable]` and -/// accepts a `CancelToken` as its final parameter; the `#[weaveffi::module]` -/// expansion lifts the launcher's `cancel_token` slot into one of these and -/// moves it onto the worker thread. The function polls [`CancelToken::is_cancelled`] -/// at safe points and returns early (typically `Err`) when cancellation is observed. -/// The token carries no parameter in the IDL: it is part of the async calling -/// convention, not the function's logical signature. -/// -/// # Safety -/// -/// The wrapped pointer is owned by the foreign caller, which (per the cancel -/// token contract) keeps it alive until the completion callback fires, so -/// reading the atomic flag from the worker thread is sound. The wrapper is -/// therefore `Send`/`Sync`. -pub struct CancelToken { - raw: *const weaveffi_cancel_token, -} - -// SAFETY: the wrapped token is an atomic flag behind a pointer the foreign -// caller keeps valid for the whole async operation; reading it from the worker -// thread the launcher spawns races only on the atomic, which is synchronized. -unsafe impl Send for CancelToken {} -// SAFETY: see the `Send` impl; `is_cancelled` is a shared atomic load. -unsafe impl Sync for CancelToken {} - -impl CancelToken { - /// Wrap a raw cancel-token pointer. A null pointer is permitted and reads as - /// "never cancelled". - /// - /// This is the entry point the `#[weaveffi::module]` expansion calls; it is - /// `#[doc(hidden)]` because producers receive an already-built token. - #[doc(hidden)] - #[must_use] - pub fn from_raw(raw: *const weaveffi_cancel_token) -> Self { - Self { raw } - } - - /// Whether the foreign caller has requested cancellation. - #[must_use] - pub fn is_cancelled(&self) -> bool { - cancel_token_is_cancelled(self.raw) - } -} - -/// An owned, type-erased iterator handed across the C ABI boundary. -/// -/// A producer function whose IDL return type is `iter` returns a -/// `weaveffi::Iter`, built from any iterator with [`Iter::new`]. The -/// `#[weaveffi::module]` expansion boxes it behind an opaque iterator handle, -/// pulls one element per `_next` call, and drops it in `_destroy`. Pulling -/// elements lazily (rather than materializing a `Vec`) is what distinguishes an -/// `iter` return from a `[T]` (list) return. -pub struct Iter { - inner: Box + Send>, -} - -impl Iter { - /// Wrap any `Send + 'static` iterator as a WeaveFFI iterator handle. - /// - /// Accepts anything `IntoIterator`, so `Iter::new(vec)`, `Iter::new(0..n)`, - /// and `Iter::new(map.into_values())` all work. - pub fn new(iter: I) -> Self - where - I: IntoIterator, - I::IntoIter: Send + 'static, - { - Self { - inner: Box::new(iter.into_iter()), - } - } -} - -impl Iterator for Iter { - type Item = T; - - fn next(&mut self) -> Option { - self.inner.next() - } -} - -/// Drive a future to completion on the current thread, blocking until it -/// resolves. -/// -/// This is the minimal, dependency-free executor behind the default -/// [`Spawner`]: each async launch runs its future on a fresh thread through -/// this function, then invokes the completion callback with the result. It -/// parks the thread between polls and wakes on `Waker::wake`, so a future -/// that yields (for example, one awaiting a channel woken from another thread) -/// makes progress without busy-spinning. There is no reactor, so a future that -/// depends on an external runtime's I/O driver (such as Tokio's) needs that -/// runtime installed with [`set_spawner`] instead. -/// -/// # Examples -/// -/// ``` -/// let n = weaveffi_abi::block_on(async { 1 + 2 }); -/// assert_eq!(n, 3); -/// ``` -pub fn block_on(fut: F) -> F::Output { - use std::sync::Arc; - use std::task::{Context, Poll, Wake, Waker}; - use std::thread::{self, Thread}; - - struct ThreadWaker(Thread); - impl Wake for ThreadWaker { - fn wake(self: Arc) { - self.0.unpark(); - } - fn wake_by_ref(self: &Arc) { - self.0.unpark(); - } - } - - let mut fut = Box::pin(fut); - let waker = Waker::from(Arc::new(ThreadWaker(thread::current()))); - let mut cx = Context::from_waker(&waker); - loop { - match fut.as_mut().poll(&mut cx) { - Poll::Ready(out) => return out, - Poll::Pending => thread::park(), - } - } -} - -/// Convert a NUL-terminated C string pointer to an owned `String`. -/// Returns `None` if `ptr` is null or not valid UTF-8. -pub fn c_ptr_to_string(ptr: *const c_char) -> Option { - if ptr.is_null() { - return None; - } - // SAFETY: caller guarantees `ptr` points to a NUL-terminated string - let c = unsafe { CStr::from_ptr(ptr) }; - c.to_str().ok().map(|s| s.to_owned()) -} - -/// Borrow a NUL-terminated C string as a `&str` without copying. -/// Returns `None` if `ptr` is null or the bytes aren't valid UTF-8. -/// -/// This is the zero-copy lift for a producer parameter spelled `&str`: the -/// generated thunk borrows the caller's buffer for the duration of the call -/// instead of allocating an owned `String`. +/// The body of `{prefix}_dealloc` (`wasm32` only): release a block from +/// [`wasm_alloc`]. /// /// # Safety /// -/// `ptr` must be null or point to a NUL-terminated string that stays valid -/// (and unmodified) for the caller-chosen lifetime `'a`. Generated thunks -/// bound that lifetime by the call, matching the C contract that string -/// arguments are borrowed for the call's duration. -pub unsafe fn c_ptr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> { +/// `ptr` must be null or a block [`wasm_alloc`] returned for this same +/// `size`, released exactly once. +#[cfg(target_arch = "wasm32")] +pub unsafe fn wasm_dealloc(ptr: *mut u8, size: usize) { if ptr.is_null() { - return None; - } - // SAFETY: the caller guarantees `ptr` is NUL-terminated and valid for 'a. - let c = unsafe { CStr::from_ptr(ptr) }; - c.to_str().ok() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn string_roundtrip_and_free() { - let ptr = string_to_c_ptr("hello world"); - assert!(!ptr.is_null()); - let recovered = c_ptr_to_string(ptr).unwrap(); - assert_eq!(recovered, "hello world"); - free_string(ptr); - } - - #[test] - fn free_string_null_is_safe() { - free_string(ptr::null()); - } - - #[test] - fn free_bytes_null_is_safe() { - free_bytes(ptr::null_mut(), 0); - } - - #[test] - fn bytes_alloc_and_free() { - let data: Vec = vec![1, 2, 3, 4, 5]; - let len = data.len(); - let boxed = data.into_boxed_slice(); - let ptr = Box::into_raw(boxed) as *mut u8; - free_bytes(ptr, len); - } - - #[test] - fn error_default_is_ok() { - let err = weaveffi_error::default(); - assert_eq!(err.code, 0); - assert!(err.message.is_null()); - } - - #[test] - fn error_set_and_clear() { - let mut err = weaveffi_error::default(); - error_set(&mut err, -1, "something went wrong"); - assert_eq!(err.code, -1); - assert!(!err.message.is_null()); - let msg = c_ptr_to_string(err.message).unwrap(); - assert_eq!(msg, "something went wrong"); - error_clear(&mut err); - assert_eq!(err.code, 0); - assert!(err.message.is_null()); - } - - #[test] - fn error_clear_null_is_safe() { - error_clear(ptr::null_mut()); - } - - #[test] - fn error_set_ok_frees_prior_message() { - let mut err = weaveffi_error::default(); - error_set(&mut err, 1, "first"); - error_set_ok(&mut err); - assert_eq!(err.code, 0); - assert!(err.message.is_null()); - } - - #[test] - fn error_set_replaces_prior_message() { - let mut err = weaveffi_error::default(); - error_set(&mut err, 1, "first"); - error_set(&mut err, 2, "second"); - assert_eq!(err.code, 2); - let msg = c_ptr_to_string(err.message).unwrap(); - assert_eq!(msg, "second"); - error_clear(&mut err); - } - - #[test] - fn result_to_out_err_ok_path() { - let mut err = weaveffi_error::default(); - let val: Result = Ok(42); - let opt = result_to_out_err(val, &mut err); - assert_eq!(opt, Some(42)); - assert_eq!(err.code, 0); - assert!(err.message.is_null()); - } - - #[test] - fn result_to_out_err_error_path() { - let mut err = weaveffi_error::default(); - let val: Result = Err("bad input".to_string()); - let opt = result_to_out_err(val, &mut err); - assert_eq!(opt, None); - assert_eq!(err.code, -1); - let msg = c_ptr_to_string(err.message).unwrap(); - assert_eq!(msg, "bad input"); - error_clear(&mut err); - } - - // A domain error type carrying its own `ErrorReport` impl. It could also - // implement `Display` and `std::error::Error` freely; there's no blanket - // impl to collide with. - enum DomainError { - NotFound, - Io(String), - } - - impl ErrorReport for DomainError { - fn code(&self) -> i32 { - match self { - DomainError::NotFound => 1001, - DomainError::Io(_) => 1004, - } - } - fn message(&self) -> String { - match self { - DomainError::NotFound => "not found".to_string(), - DomainError::Io(detail) => format!("io: {detail}"), - } - } - } - - #[test] - fn error_set_c_copies_a_borrowed_message() { - let mut err = weaveffi_error::default(); - let msg = CString::new("from the consumer").unwrap(); - error_set_c(&mut err, FOREIGN_ERROR_CODE, msg.as_ptr()); - drop(msg); - assert_eq!(err.code, FOREIGN_ERROR_CODE); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "from the consumer"); - error_clear(&mut err); - error_set_c(&mut err, 3, ptr::null()); - assert_eq!(err.code, 3); - assert_eq!(c_ptr_to_string(err.message).unwrap(), ""); - error_clear(&mut err); - } - - #[test] - fn foreign_error_payload_is_reported_as_foreign_code() { - let mut err = weaveffi_error::default(); - let payload: Box = Box::new(ForeignError { - code: FOREIGN_ERROR_CODE, - message: "listener threw".into(), - }); - error_set_panic(&mut err, &*payload); - assert_eq!(err.code, FOREIGN_ERROR_CODE); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "listener threw"); - error_clear(&mut err); - - let payload: Box = Box::new("bug"); - error_set_panic(&mut err, &*payload); - assert_eq!(err.code, PANIC_ERROR_CODE); - assert_eq!( - c_ptr_to_string(err.message).unwrap(), - "producer panicked: bug" - ); - error_clear(&mut err); - } - - #[test] - fn error_report_string_uses_generic_code() { - let e = "boom".to_string(); - assert_eq!(ErrorReport::code(&e), GENERIC_ERROR_CODE); - assert_eq!(ErrorReport::message(&e), "boom"); - assert_eq!(ErrorReport::code(&"boom"), -1); - assert_eq!(ErrorReport::message(&"boom"), "boom"); - } - - #[test] - fn error_report_coexists_with_display_impls() { - // The point of dropping the blanket impl: a domain error can derive - // or implement `Display`/`Error` and still carry its own codes. - #[derive(Debug)] - struct Both; - impl std::fmt::Display for Both { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "both") - } - } - impl std::error::Error for Both {} - impl ErrorReport for Both { - fn code(&self) -> i32 { - 7 - } - fn message(&self) -> String { - "both".to_string() - } - } - assert_eq!(ErrorReport::code(&Both), 7); - } - - #[test] - fn c_ptr_to_str_borrows_without_copying() { - let owned = CString::new("borrowed").unwrap(); - // SAFETY: `owned` outlives the borrow. - let s = unsafe { c_ptr_to_str(owned.as_ptr()) }.unwrap(); - assert_eq!(s, "borrowed"); - assert_eq!(unsafe { c_ptr_to_str(ptr::null()) }, None); - } - - #[test] - fn error_report_domain_error_carries_its_code() { - let mut err = weaveffi_error::default(); - let val: Result = Err(DomainError::NotFound); - assert_eq!(result_to_out_err(val, &mut err), None); - assert_eq!(err.code, 1001); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "not found"); - error_clear(&mut err); - - let val: Result = Err(DomainError::Io("disk".to_string())); - assert_eq!(result_to_out_err(val, &mut err), None); - assert_eq!(err.code, 1004); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "io: disk"); - error_clear(&mut err); - } - - #[test] - fn string_with_interior_nul_is_sanitized() { - let ptr = string_to_c_ptr("hel\0lo"); - let recovered = c_ptr_to_string(ptr).unwrap(); - assert_eq!(recovered, "hello"); - free_string(ptr); - } - - #[test] - fn c_ptr_to_string_null_returns_none() { - assert_eq!(c_ptr_to_string(ptr::null()), None); - } - - #[test] - fn cancel_token_lifecycle() { - let token = cancel_token_create(); - assert!(!token.is_null()); - assert!(!cancel_token_is_cancelled(token)); - cancel_token_cancel(token); - assert!(cancel_token_is_cancelled(token)); - cancel_token_destroy(token); + return; } - - #[test] - fn cancel_token_null_is_safe() { - cancel_token_cancel(ptr::null_mut()); - assert!(!cancel_token_is_cancelled(ptr::null())); - cancel_token_destroy(ptr::null_mut()); + if let Ok(layout) = std::alloc::Layout::from_size_align(size.max(1), WASM_ALLOC_ALIGN) { + // SAFETY: `ptr` came from `wasm_alloc` with this exact layout. + unsafe { std::alloc::dealloc(ptr, layout) }; } } diff --git a/crates/weaveffi-abi/src/macros.rs b/crates/weaveffi-abi/src/macros.rs deleted file mode 100644 index 3ff4debe..00000000 --- a/crates/weaveffi-abi/src/macros.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! Macros that user cdylibs invoke to expose the fixed WeaveFFI C ABI -//! runtime surface from a single line of Rust. -//! -//! Every Rust cdylib that hosts generated WeaveFFI bindings must expose -//! a small set of `#[no_mangle] extern "C"` functions that the language -//! wrappers call into: string/byte deallocation, error setting and -//! clearing, and the cancel-token lifecycle. The wrappers themselves live in -//! `weaveffi-abi` as ordinary `pub fn`s; the `extern "C"` thunks must -//! be emitted in the *consumer's* crate because `#[no_mangle]` symbols -//! in a transitive `rlib` are not guaranteed to be re-exported from a -//! cdylib. -//! -//! Use [`crate::export_runtime!`] once in your cdylib's `lib.rs` to wire all -//! of them up. - -/// Emit `#[no_mangle] extern "C"` thunks for every runtime symbol that -/// the WeaveFFI generators expect to find in the consuming cdylib. -/// -/// 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_set` -/// - `weaveffi_error_clear` -/// - `weaveffi_error_free` -/// - `weaveffi_cancel_token_create` -/// - `weaveffi_cancel_token_cancel` -/// - `weaveffi_cancel_token_is_cancelled` -/// - `weaveffi_cancel_token_destroy` -/// -/// On `wasm32` targets it additionally emits a linear-memory allocator that the -/// generated JS bindings call to stage inputs and return slots: -/// -/// - `weaveffi_alloc` -/// - `weaveffi_dealloc` -/// -/// # Example -/// -/// ```ignore -/// // Inside a cdylib's src/lib.rs -/// use weaveffi_abi as abi; -/// -/// abi::export_runtime!(); -/// -/// // ... your generated/hand-written #[no_mangle] business functions ... -/// ``` -/// -/// Invoke this macro at module scope **exactly once** in your cdylib. -/// Multiple invocations would produce duplicate symbol definitions. -#[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) - } - - #[no_mangle] - pub extern "C" fn weaveffi_free_bytes(ptr: *mut u8, len: usize) { - $crate::free_bytes(ptr, len) - } - - // Wasm has no host allocator, so the generated JS glue stages input - // buffers and `sret` slots through these thunks. They are only emitted - // for `wasm32` targets; native targets allocate on the foreign side and - // never reference them, keeping their ABI surface unchanged. - #[cfg(target_arch = "wasm32")] - #[no_mangle] - pub extern "C" fn weaveffi_alloc(size: u32) -> *mut u8 { - $crate::wasm_alloc(size as usize) - } - - #[cfg(target_arch = "wasm32")] - #[no_mangle] - pub extern "C" fn weaveffi_dealloc(ptr: *mut u8, size: u32) { - $crate::wasm_dealloc(ptr, size as usize) - } - - // Callback-interface implementations report a failure through this - // thunk so the message is copied with the producer's allocator (the - // one `weaveffi_error_clear` frees with). - #[no_mangle] - pub extern "C" fn weaveffi_error_set( - err: *mut $crate::weaveffi_error, - code: i32, - message: *const ::std::os::raw::c_char, - ) { - $crate::error_set_c(err, code, message) - } - - #[no_mangle] - pub extern "C" fn weaveffi_error_clear(err: *mut $crate::weaveffi_error) { - $crate::error_clear(err) - } - - #[no_mangle] - pub extern "C" fn weaveffi_error_free(err: *mut $crate::weaveffi_error) { - $crate::error_free(err) - } - - #[no_mangle] - pub extern "C" fn weaveffi_cancel_token_create() -> *mut $crate::weaveffi_cancel_token { - $crate::cancel_token_create() - } - - #[no_mangle] - pub extern "C" fn weaveffi_cancel_token_cancel(token: *mut $crate::weaveffi_cancel_token) { - $crate::cancel_token_cancel(token) - } - - #[no_mangle] - pub extern "C" fn weaveffi_cancel_token_is_cancelled( - token: *const $crate::weaveffi_cancel_token, - ) -> bool { - $crate::cancel_token_is_cancelled(token) - } - - #[no_mangle] - pub extern "C" fn weaveffi_cancel_token_destroy(token: *mut $crate::weaveffi_cancel_token) { - $crate::cancel_token_destroy(token) - } - }; -} diff --git a/crates/weaveffi-abi/src/object.rs b/crates/weaveffi-abi/src/object.rs index 7a23c77b..efd3ea3a 100644 --- a/crates/weaveffi-abi/src/object.rs +++ b/crates/weaveffi-abi/src/object.rs @@ -23,6 +23,7 @@ use std::sync::Arc; /// The consumer releases the reference with `{tag}_destroy`. #[must_use] pub fn lower_object(value: impl Into>) -> *mut T { + crate::leak::track(crate::leak::OBJECTS, 1); Arc::into_raw(value.into()).cast_mut() } @@ -90,15 +91,14 @@ pub unsafe fn object_clone(ptr: *const T) -> *mut T { } // SAFETY: as documented on the function. unsafe { Arc::increment_strong_count(ptr) }; + crate::leak::track(crate::leak::OBJECTS, 1); ptr.cast_mut() } /// The body of every `{tag}_destroy` symbol: release one strong reference. /// The object is dropped when the last reference goes. Null is a no-op, and /// a panicking `Drop` is swallowed because a destructor has no `out_err` -/// slot and must never unwind into C. A foreign failure that `Drop` deferred -/// by calling back into the consumer is discarded for the same reason, so it -/// can't leak into the next unrelated thunk. +/// slot and must never unwind into C. /// /// # Safety /// @@ -108,11 +108,11 @@ pub unsafe fn object_destroy(ptr: *mut T) { if ptr.is_null() { return; } + crate::leak::track(crate::leak::OBJECTS, -1); let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { // SAFETY: as documented on the function. unsafe { Arc::decrement_strong_count(ptr) }; })); - let _ = crate::take_foreign_error(); } /// The object token an interface value takes inside a value buffer: the @@ -120,6 +120,7 @@ pub unsafe fn object_destroy(ptr: *mut T) { /// reference; whoever decodes it adopts that reference. #[must_use] pub fn object_to_token(value: &Arc) -> u64 { + crate::leak::track(crate::leak::OBJECTS, 1); Arc::into_raw(Arc::clone(value)) as usize as u64 } @@ -137,6 +138,7 @@ pub unsafe fn object_from_token(token: u64) -> Option> { if ptr.is_null() { return None; } + crate::leak::track(crate::leak::OBJECTS, -1); // SAFETY: the caller guarantees the token carries one live strong // reference that has not been adopted yet. Some(unsafe { Arc::from_raw(ptr) }) diff --git a/crates/weaveffi-abi/src/spawn.rs b/crates/weaveffi-abi/src/spawn.rs index ffd7f03a..64a86642 100644 --- a/crates/weaveffi-abi/src/spawn.rs +++ b/crates/weaveffi-abi/src/spawn.rs @@ -1,9 +1,10 @@ -//! The pluggable executor async exports run on. +//! The pluggable executor async exports run on, and the launcher driver that +//! keeps the completion promise. //! //! An exported `async fn` lowers to a launcher that returns immediately and a //! completion callback that fires when the future resolves. Something has to //! drive the future in between. By default WeaveFFI drives each one on a -//! dedicated thread with [`block_on`](crate::block_on), which needs no runtime +//! dedicated thread with [`block_on`], which needs no runtime //! and is enough for CPU-bound work and futures woken from other threads. A //! producer whose futures depend on a reactor (Tokio's I/O and timers, for //! example) installs its own [`Spawner`] once at startup with [`set_spawner`]: @@ -14,15 +15,21 @@ //! let _ = weaveffi_abi::set_spawner(|fut| spawn_on_my_runtime(fut)); //! ``` //! -//! Generated launchers call [`spawn`], which routes to the installed spawner -//! or the default. Every future handed to a spawner is `Send + 'static` and -//! already wrapped so a panic inside it is caught and reported through the -//! completion callback; a spawner never observes an unwinding future. +//! Generated launchers call [`run_async`], which routes to the installed +//! spawner or the default through [`spawn`]. Every future handed to a spawner +//! is `Send + 'static` and already wrapped so a panic inside it is caught and +//! reported through the completion callback; a spawner never observes an +//! unwinding future. A spawner may also drop a future without finishing it +//! (a runtime shutting down, say): the completion still fires, with +//! [`CANCELLED_ERROR_CODE`](crate::CANCELLED_ERROR_CODE). use std::future::Future; +use std::marker::PhantomData; use std::pin::Pin; -use std::sync::OnceLock; -use std::task::{Context, Poll}; +use std::sync::{Arc, OnceLock}; +use std::task::{Context, Poll, Wake, Waker}; + +use crate::{CancelToken, Cancellable, FfiError, ForeignError, ThunkScope}; /// The type-erased future a [`Spawner`] receives. pub type BoxFuture = Pin + Send + 'static>>; @@ -80,7 +87,7 @@ pub fn set_spawner(spawner: impl Spawner) -> Result<(), SpawnerAlreadySet> { /// Run `fut` on the installed spawner, or on the default one. /// /// The default drives the future on a freshly spawned thread with -/// [`block_on`](crate::block_on). On `wasm32`, which has no threads, the +/// [`block_on`]. On `wasm32`, which has no threads, the /// future is driven inline before this returns. pub fn spawn(fut: impl Future + Send + 'static) { let fut: BoxFuture = Box::pin(fut); @@ -92,12 +99,120 @@ pub fn spawn(fut: impl Future + Send + 'static) { #[cfg(not(target_arch = "wasm32"))] fn default_spawn(fut: BoxFuture) { - std::thread::spawn(move || crate::block_on(fut)); + std::thread::spawn(move || block_on(fut)); } #[cfg(target_arch = "wasm32")] fn default_spawn(fut: BoxFuture) { - crate::block_on(fut); + block_on(fut); +} + +/// Drive a future to completion on the current thread, blocking until it +/// resolves. +/// +/// This is the minimal, dependency-free executor behind the default +/// [`Spawner`]. It parks the thread between polls and wakes on +/// `Waker::wake`, so a future that yields (for example, one awaiting a +/// channel woken from another thread) makes progress without busy-spinning. +/// There is no reactor, so a future that depends on an external runtime's +/// I/O driver (such as Tokio's) needs that runtime installed with +/// [`set_spawner`] instead. +/// +/// # Examples +/// +/// ``` +/// let n = weaveffi_abi::block_on(async { 1 + 2 }); +/// assert_eq!(n, 3); +/// ``` +pub fn block_on(fut: F) -> F::Output { + struct ThreadWaker(std::thread::Thread); + impl Wake for ThreadWaker { + fn wake(self: Arc) { + self.0.unpark(); + } + fn wake_by_ref(self: &Arc) { + self.0.unpark(); + } + } + + let mut fut = std::pin::pin!(fut); + let waker = Waker::from(Arc::new(ThreadWaker(std::thread::current()))); + let mut cx = Context::from_waker(&waker); + loop { + match fut.as_mut().poll(&mut cx) { + Poll::Ready(out) => return out, + Poll::Pending => std::thread::park(), + } + } +} + +/// Fires an async call's completion exactly once: through +/// [`complete`](Self::complete) on the normal path, or with a cancellation +/// error from `Drop` if the spawner drops the task first. +struct CompletionGuard)> { + complete: Option, + _result: PhantomData, +} + +impl)> CompletionGuard { + fn complete(mut self, result: Result) { + if let Some(c) = self.complete.take() { + c(result); + } + } +} + +impl)> Drop for CompletionGuard { + fn drop(&mut self) { + if let Some(c) = self.complete.take() { + c(Err(FfiError::cancelled())); + } + } +} + +/// Run an async export to completion and deliver its result exactly once. +/// +/// Every generated async launcher calls this. `fut` is the producer's work, +/// already mapped to `Result` (a domain error becomes its +/// [`FfiError`]); `complete` lowers the outcome into the C completion +/// callback's slots and invokes it. The runtime calls `complete` exactly +/// once: +/// +/// * with `fut`'s result when it resolves; +/// * with a [`PANIC_ERROR_CODE`](crate::PANIC_ERROR_CODE) error (or the +/// consumer's [`ForeignError`]) when polling it panics; +/// * with [`CANCELLED_ERROR_CODE`](crate::CANCELLED_ERROR_CODE) as soon as +/// `cancel` fires, after dropping `fut`; +/// * with [`CANCELLED_ERROR_CODE`](crate::CANCELLED_ERROR_CODE) when the +/// spawner drops the task before it resolves (or panics instead of +/// accepting it). +pub fn run_async(fut: Fut, cancel: Option, complete: C) +where + T: 'static, + Fut: Future> + Send + 'static, + C: FnOnce(Result) + Send + 'static, +{ + let guard = CompletionGuard { + complete: Some(complete), + _result: PhantomData, + }; + let task = async move { + let guard = guard; + let caught = CatchUnwind::new(fut); + let outcome = match cancel { + Some(token) => Cancellable::new(caught, token).await, + None => Some(caught.await), + }; + guard.complete(match outcome { + None => Err(FfiError::cancelled()), + Some(Ok(result)) => result, + Some(Err(payload)) => Err(FfiError::from_panic(&*payload)), + }); + }; + // A spawner that panics drops the task on the way out, which fires the + // completion with the cancelled code; the panic itself must not unwind + // into the launcher's C caller. + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| spawn(task))); } /// A future adapter that catches a panic from the inner future's `poll` and @@ -108,13 +223,14 @@ fn default_spawn(fut: BoxFuture) { /// `CatchUnwind::new(user_future)` and reports an `Err` through the /// completion callback with the reserved panic code. /// -/// It also closes the deferred foreign-error route for async producers: when -/// the inner future completes on a `panic = "abort"` build with a failure -/// recorded by [`defer_foreign_error`](crate::defer_foreign_error), the -/// adapter resolves to `Err` carrying that -/// [`ForeignError`](crate::ForeignError) instead of the producer's value. +/// It also closes the deferred foreign-error route for async producers: each +/// poll runs inside a [`ThunkScope`], and a failure recorded by +/// [`defer_foreign_error`](crate::defer_foreign_error) during any poll (a +/// `panic = "abort"` build) is kept with this future, so the adapter resolves +/// to `Err` carrying that [`ForeignError`] instead of the producer's value. pub struct CatchUnwind { inner: Option>>, + foreign: Option, } impl CatchUnwind { @@ -122,6 +238,7 @@ impl CatchUnwind { pub fn new(fut: F) -> Self { Self { inner: Some(Box::pin(fut)), + foreign: None, } } } @@ -130,24 +247,32 @@ impl Future for CatchUnwind { type Output = Result>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let Some(inner) = self.inner.as_mut() else { + let this = &mut *self; + let Some(inner) = this.inner.as_mut() else { // Polled after completion; a well-behaved executor never does // this, and there is nothing left to drive. return Poll::Pending; }; - let polled = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| inner.as_mut().poll(cx))); + let polled = { + let _scope = ThunkScope::enter(); + let polled = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| inner.as_mut().poll(cx))); + if let Some(foreign) = crate::take_foreign_error() { + this.foreign.get_or_insert(foreign); + } + polled + }; match polled { Ok(Poll::Pending) => Poll::Pending, Ok(Poll::Ready(out)) => { - self.inner = None; - match crate::take_foreign_error() { + this.inner = None; + match this.foreign.take() { Some(foreign) => Poll::Ready(Err(Box::new(foreign))), None => Poll::Ready(Ok(out)), } } Err(payload) => { - self.inner = None; + this.inner = None; Poll::Ready(Err(payload)) } } @@ -170,9 +295,9 @@ mod tests { #[test] fn catch_unwind_reports_panics_and_values() { - let ok = crate::block_on(CatchUnwind::new(async { 5 })); + let ok = block_on(CatchUnwind::new(async { 5 })); assert_eq!(ok.unwrap(), 5); - let err = crate::block_on(CatchUnwind::new(async { + let err = block_on(CatchUnwind::new(async { if true { panic!("boom"); } @@ -184,7 +309,7 @@ mod tests { #[test] fn catch_unwind_surfaces_a_deferred_foreign_error() { - let err = crate::block_on(CatchUnwind::new(async { + let err = block_on(CatchUnwind::new(async { crate::defer_foreign_error(crate::ForeignError { code: crate::FOREIGN_ERROR_CODE, message: "consumer failed".into(), @@ -198,4 +323,67 @@ mod tests { assert_eq!(foreign.code, crate::FOREIGN_ERROR_CODE); assert_eq!(foreign.message, "consumer failed"); } + + fn run( + fut: impl Future> + Send + 'static, + cancel: Option, + ) -> mpsc::Receiver> { + let (tx, rx) = mpsc::channel(); + run_async(fut, cancel, move |r: Result| { + tx.send(r.map_err(|e| e.code)).unwrap(); + }); + rx + } + + const WAIT: std::time::Duration = std::time::Duration::from_secs(5); + + #[test] + fn run_async_delivers_values_errors_and_panics() { + assert_eq!(run(async { Ok(4) }, None).recv_timeout(WAIT), Ok(Ok(4))); + let rx = run(async { Err(FfiError::new(9, "domain")) }, None); + assert_eq!(rx.recv_timeout(WAIT), Ok(Err(9))); + let rx = run( + async { + if true { + panic!("bug"); + } + Ok(0) + }, + None, + ); + assert_eq!(rx.recv_timeout(WAIT), Ok(Err(crate::PANIC_ERROR_CODE))); + } + + #[test] + fn run_async_completes_cancelled_when_the_token_fires() { + let raw = crate::cancel_token_create(); + let token = unsafe { CancelToken::from_raw(raw) }; + let rx = run( + async { + std::future::pending::<()>().await; + Ok(1) + }, + Some(token), + ); + unsafe { crate::cancel_token_cancel(raw) }; + unsafe { crate::cancel_token_destroy(raw) }; + assert_eq!(rx.recv_timeout(WAIT), Ok(Err(crate::CANCELLED_ERROR_CODE))); + assert!(rx + .recv_timeout(std::time::Duration::from_millis(50)) + .is_err()); + } + + #[test] + fn a_dropped_guard_completes_cancelled_once() { + let (tx, rx) = mpsc::channel(); + let guard = CompletionGuard { + complete: Some(move |r: Result<(), FfiError>| { + tx.send(r.map_err(|e| e.code)).unwrap(); + }), + _result: PhantomData, + }; + drop(guard); + assert_eq!(rx.recv(), Ok(Err(crate::CANCELLED_ERROR_CODE))); + assert!(rx.recv().is_err()); + } } diff --git a/crates/weaveffi-abi/tests/export_runtime.rs b/crates/weaveffi-abi/tests/export_runtime.rs deleted file mode 100644 index 1ecdd8b9..00000000 --- a/crates/weaveffi-abi/tests/export_runtime.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Smoke test for the `weaveffi_abi::export_runtime!()` macro. -//! -//! This integration test lives in a separate compilation unit (it is -//! linked as a Rust test binary, not a cdylib) so it cannot easily -//! observe the `#[no_mangle]` symbols' linker visibility, but it -//! *can* prove that the macro expands to valid Rust that compiles, -//! that the generated free functions are callable with realistic -//! arguments, and that the symbol names match what the WeaveFFI -//! generators emit. -//! -//! Two invocation guards keep us honest: -//! 1. Each `weaveffi_*` thunk is called directly from the test. -//! 2. We take a function-pointer reference to every symbol the macro -//! is supposed to expose, so any missing thunk turns into a compile -//! error here rather than a baffling link error in a downstream -//! cdylib. -#![allow(unsafe_code)] - -use std::ffi::CString; -use std::ptr; - -use weaveffi_abi::{self as abi, export_runtime, weaveffi_cancel_token, weaveffi_error}; - -export_runtime!(); - -#[test] -fn macro_emits_all_runtime_symbols() { - // Compile-time guarantee: every symbol the C ABI generators expect - // must be addressable from this test crate. - let _f_version: extern "C" fn() -> u32 = weaveffi_abi_version; - let _f_string: extern "C" fn(*const std::os::raw::c_char) = weaveffi_free_string; - let _f_bytes: extern "C" fn(*mut u8, usize) = weaveffi_free_bytes; - let _f_err_set: extern "C" fn(*mut weaveffi_error, i32, *const std::os::raw::c_char) = - weaveffi_error_set; - let _f_err_clear: extern "C" fn(*mut weaveffi_error) = weaveffi_error_clear; - let _f_err_free: extern "C" fn(*mut weaveffi_error) = weaveffi_error_free; - let _f_tok_create: extern "C" fn() -> *mut weaveffi_cancel_token = weaveffi_cancel_token_create; - let _f_tok_cancel: extern "C" fn(*mut weaveffi_cancel_token) = weaveffi_cancel_token_cancel; - let _f_tok_is_cancelled: extern "C" fn(*const weaveffi_cancel_token) -> bool = - weaveffi_cancel_token_is_cancelled; - let _f_tok_destroy: extern "C" fn(*mut weaveffi_cancel_token) = weaveffi_cancel_token_destroy; - assert_eq!(weaveffi_abi_version(), abi::ABI_VERSION); -} - -#[test] -fn macro_error_set_thunk_copies_the_message() { - let mut err = weaveffi_error::default(); - let msg = CString::new("consumer failure").unwrap(); - weaveffi_error_set(&mut err, abi::FOREIGN_ERROR_CODE, msg.as_ptr()); - drop(msg); - assert_eq!(err.code, abi::FOREIGN_ERROR_CODE); - assert_eq!( - abi::c_ptr_to_string(err.message).as_deref(), - Some("consumer failure") - ); - weaveffi_error_clear(&mut err); - weaveffi_error_set(ptr::null_mut(), 1, ptr::null()); -} - -#[test] -fn macro_free_string_thunk_round_trips() { - let ptr = abi::string_to_c_ptr("from-macro-test"); - assert!(!ptr.is_null()); - weaveffi_free_string(ptr); - weaveffi_free_string(ptr::null()); -} - -#[test] -fn macro_free_bytes_thunk_round_trips() { - let data = vec![1u8, 2, 3, 4].into_boxed_slice(); - let len = data.len(); - let raw = Box::into_raw(data) as *mut u8; - weaveffi_free_bytes(raw, len); - weaveffi_free_bytes(ptr::null_mut(), 0); -} - -#[test] -fn macro_error_clear_thunk_round_trips() { - let mut err = weaveffi_error::default(); - abi::error_set(&mut err, 7, "from-macro-test"); - assert_eq!(err.code, 7); - assert!(!err.message.is_null()); - - weaveffi_error_clear(&mut err); - assert_eq!(err.code, 0); - assert!(err.message.is_null()); - - weaveffi_error_clear(ptr::null_mut()); -} - -#[test] -fn macro_cancel_token_thunks_round_trip() { - let tok = weaveffi_cancel_token_create(); - assert!(!tok.is_null()); - assert!(!weaveffi_cancel_token_is_cancelled(tok)); - - weaveffi_cancel_token_cancel(tok); - assert!(weaveffi_cancel_token_is_cancelled(tok)); - - weaveffi_cancel_token_destroy(tok); - - weaveffi_cancel_token_cancel(ptr::null_mut()); - assert!(!weaveffi_cancel_token_is_cancelled(ptr::null())); - weaveffi_cancel_token_destroy(ptr::null_mut()); -} - -#[test] -fn macro_thunks_keep_weaveffi_prefix_even_when_user_renames_imports() { - // Re-import everything under a different alias to make sure the - // macro is not silently capturing identifiers from the call site. - use weaveffi_abi as renamed; - - let s = CString::new("alias-check").unwrap(); - let owned = renamed::string_to_c_ptr(s.to_str().unwrap()); - weaveffi_free_string(owned); -} diff --git a/crates/weaveffi-bridge/Cargo.toml b/crates/weaveffi-bridge/Cargo.toml deleted file mode 100644 index 62620166..00000000 --- a/crates/weaveffi-bridge/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "weaveffi-bridge" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "Extract a WeaveFFI IR Api from annotated Rust source (the shared bridge used by the proc-macros and the CLI)" -keywords = ["ffi", "codegen", "proc-macro", "bindings", "weaveffi"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } -syn = { workspace = true } - -[lints] -workspace = true diff --git a/crates/weaveffi-cli/Cargo.toml b/crates/weaveffi-cli/Cargo.toml index 96c00391..e3365836 100644 --- a/crates/weaveffi-cli/Cargo.toml +++ b/crates/weaveffi-cli/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "weaveffi-cli" -version = "0.23.0" +version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true @@ -16,29 +17,28 @@ exclude = [ [[bin]] name = "weaveffi" path = "src/main.rs" +# The binary shares its name with the `weaveffi` library crate; documenting +# both would collide in `target/doc`. +doc = false + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/weaveffi-{ target }{ archive-suffix }" +bin-dir = "weaveffi-{ target }/{ bin }{ binary-ext }" +pkg-fmt = "tgz" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" [dependencies] clap = { workspace = true } clap_complete = { workspace = true } -tracing-subscriber = { workspace = true } miette = { workspace = true } +anyhow = { workspace = true } camino = { workspace = true } serde = { workspace = true } toml = { workspace = true } -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } -weaveffi-bridge = { path = "../weaveffi-bridge", version = "0.23.0" } -weaveffi-gen-c = { path = "../weaveffi-gen-c", version = "0.23.0" } -weaveffi-gen-swift = { path = "../weaveffi-gen-swift", version = "0.23.0" } -weaveffi-gen-kotlin = { path = "../weaveffi-gen-kotlin", version = "0.23.0" } -weaveffi-gen-node = { path = "../weaveffi-gen-node", version = "0.23.0" } -weaveffi-gen-wasm = { path = "../weaveffi-gen-wasm", version = "0.23.0" } -weaveffi-gen-python = { path = "../weaveffi-gen-python", version = "0.23.0" } -weaveffi-gen-dotnet = { path = "../weaveffi-gen-dotnet", version = "0.23.0" } -weaveffi-gen-cpp = { path = "../weaveffi-gen-cpp", version = "0.23.0" } -weaveffi-gen-dart = { path = "../weaveffi-gen-dart", version = "0.23.0" } -weaveffi-gen-go = { path = "../weaveffi-gen-go", version = "0.23.0" } -weaveffi-gen-ruby = { path = "../weaveffi-gen-ruby", version = "0.23.0" } +weaveffi-model = { workspace = true, features = ["idl"] } +weaveffi-gen = { workspace = true } serde_yaml = { workspace = true } serde_json = { workspace = true } similar = { workspace = true } @@ -51,12 +51,8 @@ predicates = "3" serde_yaml = { workspace = true } serde_json = { workspace = true } toml = { workspace = true } -criterion = { workspace = true } insta = { workspace = true } -[[bench]] -name = "generate_bench" -harness = false [lints] workspace = true diff --git a/crates/weaveffi-cli/benches/generate_bench.rs b/crates/weaveffi-cli/benches/generate_bench.rs deleted file mode 100644 index d6b255b3..00000000 --- a/crates/weaveffi-cli/benches/generate_bench.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::path::Path; - -use camino::Utf8Path; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use weaveffi_core::codegen::{ConfiguredBackend, Orchestrator, Target}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::validate::validate_api; -use weaveffi_gen_c::{CConfig, CGenerator}; -use weaveffi_gen_cpp::{CppConfig, CppGenerator}; -use weaveffi_gen_dart::{DartConfig, DartGenerator}; -use weaveffi_gen_dotnet::{DotnetConfig, DotnetGenerator}; -use weaveffi_gen_go::{GoConfig, GoGenerator}; -use weaveffi_gen_kotlin::{KotlinConfig, KotlinGenerator}; -use weaveffi_gen_node::{NodeConfig, NodeGenerator}; -use weaveffi_gen_python::{PythonConfig, PythonGenerator}; -use weaveffi_gen_ruby::{RubyConfig, RubyGenerator}; -use weaveffi_gen_swift::{SwiftConfig, SwiftGenerator}; -use weaveffi_gen_wasm::{WasmConfig, WasmGenerator}; -use weaveffi_ir::ir::{ - Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, -}; -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, - 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. -fn build_large_api() -> ResolvedApi { - let modules = (0..10) - .map(|m| { - let structs: Vec = (0..5) - .map(|s| StructDef { - name: format!("M{m}Struct{s}"), - doc: None, - deprecated: None, - fields: vec![ - field("id", TypeRef::I32), - field("name", TypeRef::StringUtf8), - field("active", TypeRef::Bool), - ], - }) - .collect(); - - let enums: Vec = (0..3) - .map(|e| EnumDef { - name: format!("M{m}Enum{e}"), - doc: None, - deprecated: None, - variants: vec![variant("Alpha", 0), variant("Beta", 1), variant("Gamma", 2)], - }) - .collect(); - - let functions: Vec = (0..50) - .map(|f| Function { - name: format!("func{f}"), - doc: Some(format!("Function {f} in module {m}")), - params: vec![ - param("a", TypeRef::I32), - param("b", TypeRef::StringUtf8), - param("c", TypeRef::Named(format!("M{m}Struct0"))), - ], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Named(format!( - "M{m}Struct1" - ))))), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - }) - .collect(); - - Module { - name: format!("mod{m}"), - doc: None, - functions, - interfaces: vec![], - structs, - enums, - callback_interfaces: vec![], - errors: None, - modules: vec![], - } - }) - .collect(); - - validate_api( - Api { - version: "0.9.0".into(), - modules, - }, - None, - ) - .expect("synthetic API validates") -} - -/// The same fan-out of targets the CLI drives, each with its default config. -fn all_default_targets() -> 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(ConfiguredBackend::new( - KotlinGenerator, - KotlinConfig::default(), - )), - Box::new(ConfiguredBackend::new(NodeGenerator, NodeConfig::default())), - Box::new(ConfiguredBackend::new(WasmGenerator, WasmConfig::default())), - Box::new(ConfiguredBackend::new( - PythonGenerator, - PythonConfig::default(), - )), - Box::new(ConfiguredBackend::new( - DotnetGenerator, - DotnetConfig::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 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(); - 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 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(); - 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 targets = all_default_targets(); - c.bench_function("generate_all_large_api", |b| { - b.iter(|| run_all(&targets, &api)); - }); -} - -/// Parse and validate the canonical kitchen-sink IDL fixture so the -/// 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/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") - .unwrap_or_else(|e| panic!("parse fixture {}: {e}", path.display())); - validate_api(api, None).unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) -} - -/// 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/src/lib.rs"); - let contents = std::fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("read 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())) -} - -/// 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(&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(&targets, &api)); - }); -} - -fn bench_generate_all_parallel_vs_serial(c: &mut Criterion) { - let api = load_kitchen_sink_api(); - let targets = all_default_targets(); - - let mut group = c.benchmark_group("generate_all_kitchen_sink"); - group.bench_function("parallel", |b| { - 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 t in &targets { - t.generate(black_box(&api), out).unwrap(); - } - }); - }); - group.finish(); -} - -criterion_group!( - benches, - bench_generate_c_large_api, - bench_generate_swift_large_api, - bench_generate_all_large_api, - bench_generate_all_parallel_vs_serial, - bench_full_codegen_calculator, - bench_full_codegen_kitchen_sink, -); -criterion_main!(benches); diff --git a/crates/weaveffi-cli/src/commands/diff.rs b/crates/weaveffi-cli/src/commands/diff.rs index 6a9732d2..e06bae8f 100644 --- a/crates/weaveffi-cli/src/commands/diff.rs +++ b/crates/weaveffi-cli/src/commands/diff.rs @@ -1,133 +1,112 @@ -//! `weaveffi diff`: regenerate into a temp directory and compare against -//! the on-disk output (`--check` for CI gating with distinct exit codes). +//! `weaveffi diff`: show how regenerating would change an existing output +//! directory, without writing anything or running hooks. `--check` turns it +//! into a CI gate. + +use std::collections::{BTreeMap, BTreeSet}; use camino::Utf8Path; -use miette::{miette, IntoDiagnostic, Result, WrapErr}; +use miette::{IntoDiagnostic, Result}; use similar::TextDiff; -use std::collections::BTreeSet; -use weaveffi_core::codegen::Orchestrator; - -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 (config, api) = super::load_project(input, config_path, false)?; +use weaveffi_gen::cache; +use weaveffi_gen::codegen::relative_path; + +/// Options for [`cmd_diff`]. +pub(crate) struct DiffArgs<'a> { + pub(crate) input: Option<&'a str>, + pub(crate) out: Option<&'a str>, + pub(crate) targets: Option<&'a str>, + pub(crate) config: Option<&'a str>, + pub(crate) check: bool, + pub(crate) quiet: bool, +} - let tmp = tempfile::tempdir() - .into_diagnostic() - .wrap_err("failed to create temp directory")?; - let tmp_path = Utf8Path::from_path(tmp.path()) - .ok_or_else(|| miette!("temp directory path is not valid UTF-8"))?; +/// Exit status of `diff --check` when files differ. +const EXIT_MODIFIED: i32 = 2; +/// Exit status of `diff --check` when files would be added or removed. +const EXIT_ADDED_OR_REMOVED: i32 = 3; - let hooks = config.hooks(); - let targets = config.select_targets(None)?; +pub(crate) fn cmd_diff(args: &DiffArgs<'_>) -> Result<()> { + let project = super::load_project(args.input, args.config, false)?; + let out_dir = project.config.out_dir(args.out); + let targets = project.config.select_targets(args.targets)?; - let mut orchestrator = Orchestrator::new(); + let mut generated: BTreeMap = BTreeMap::new(); + let mut existing: BTreeSet = BTreeSet::new(); for target in &targets { - orchestrator = orchestrator.with_target(target.as_ref()); + for file in target.render(&project.api, &out_dir) { + generated.insert(relative_path(&out_dir, &file.path), file.contents); + } + // Files a previous generation recorded are the generator's; without a + // record, everything under the target's directory counts. + match cache::read_record(&out_dir, target.name()) { + Some(record) => existing.extend( + record + .files + .into_keys() + .filter(|rel| out_dir.join(rel).exists()), + ), + None => collect_files(&out_dir, &out_dir.join(target.name()), &mut existing)?, + } } - orchestrator - .run(&api, tmp_path, &hooks, true) - .map_err(|e| miette!("{:#}", e))?; - - let out_dir = Utf8Path::new(out); - let generated = collect_relative_files(tmp_path)?; - let existing = if out_dir.exists() { - collect_relative_files(out_dir)? - } else { - BTreeSet::new() - }; - - let all_paths: BTreeSet<_> = generated.union(&existing).collect(); - let mut added = 0usize; - let mut removed = 0usize; - let mut modified = 0usize; - - for rel in &all_paths { - let gen_file = tmp_path.join(rel); - let out_file = out_dir.join(rel); - - match (gen_file.exists(), out_file.exists()) { - (true, false) => { + let (mut added, mut removed, mut modified) = (0usize, 0usize, 0usize); + let all: BTreeSet<&String> = generated.keys().chain(existing.iter()).collect(); + for rel in all { + match (generated.get(rel), existing.contains(rel)) { + (Some(_), false) => { added += 1; - if !check { + if !args.check { println!("{rel}: [new file]"); } } - (false, true) => { + (None, true) => { removed += 1; - if !check { + if !args.check { println!("{rel}: [would be removed]"); } } - (true, true) => { - let gen_content = - std::fs::read_to_string(gen_file.as_std_path()).into_diagnostic()?; - let out_content = - std::fs::read_to_string(out_file.as_std_path()).into_diagnostic()?; - if gen_content != out_content { + (Some(new), true) => { + let old = + std::fs::read_to_string(out_dir.join(rel).as_std_path()).into_diagnostic()?; + if &old != new { modified += 1; - if !check { - print_unified_diff(rel, &out_content, &gen_content); + if !args.check { + print_unified_diff(rel, &old, new); } } } - _ => {} + (None, false) => {} } } - if check { + if args.check { println!("+ {added} added, - {removed} removed, ~ {modified} modified"); if added > 0 || removed > 0 { - std::process::exit(3); + std::process::exit(EXIT_ADDED_OR_REMOVED); } if modified > 0 { - std::process::exit(2); + std::process::exit(EXIT_MODIFIED); } - return Ok(()); - } - - if added == 0 && removed == 0 && modified == 0 && !quiet { + } else if added == 0 && removed == 0 && modified == 0 && !args.quiet { println!("No differences found."); } - Ok(()) } -fn collect_relative_files(base: &Utf8Path) -> Result> { - let mut files = BTreeSet::new(); - walk_dir(base, base, &mut files)?; - Ok(files) -} - -fn walk_dir(base: &Utf8Path, dir: &Utf8Path, out: &mut BTreeSet) -> Result<()> { - let entries = std::fs::read_dir(dir.as_std_path()) - .into_diagnostic() - .wrap_err_with(|| format!("failed to read directory: {}", dir))?; +/// Every file under `dir` (recursively), relative to `base`. +fn collect_files(base: &Utf8Path, dir: &Utf8Path, out: &mut BTreeSet) -> Result<()> { + let Ok(entries) = std::fs::read_dir(dir.as_std_path()) else { + return Ok(()); + }; for entry in entries { - let entry = entry.into_diagnostic()?; - let path = entry.path(); - let utf8 = Utf8Path::from_path(&path) - .ok_or_else(|| miette!("non-UTF-8 path: {:?}", path))? - .to_owned(); - if utf8.file_name() == Some(".weaveffi-cache") { + let path = entry.into_diagnostic()?.path(); + let Some(path) = Utf8Path::from_path(&path) else { continue; - } - if utf8.is_dir() { - walk_dir(base, &utf8, out)?; + }; + if path.is_dir() { + collect_files(base, path, out)?; } else { - let rel = utf8 - .strip_prefix(base) - .into_diagnostic() - .wrap_err("failed to strip prefix")? - .to_string(); - out.insert(rel); + out.insert(relative_path(base, path)); } } Ok(()) @@ -141,51 +120,3 @@ fn print_unified_diff(path: &str, old: &str, new: &str) { println!("{hunk}"); } } - -#[cfg(test)] -mod tests { - #[test] - fn diff_shows_new_files() { - let dir = tempfile::tempdir().unwrap(); - let yml = dir.path().join("api.yml"); - std::fs::write( - &yml, - concat!( - "version: \"0.9.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", - ), - ) - .unwrap(); - - let empty_out = dir.path().join("empty_out"); - std::fs::create_dir_all(&empty_out).unwrap(); - let input = yml.to_str().unwrap(); - let out_str = empty_out.to_str().unwrap(); - - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["diff", input, "--out", out_str]) - .output() - .expect("failed to run weaveffi diff"); - - let stdout = String::from_utf8_lossy(&cmd.stdout); - assert!(cmd.status.success(), "diff failed: {stdout}"); - assert!( - !stdout.is_empty(), - "diff output should not be empty for an empty output dir" - ); - for line in stdout.lines() { - assert!( - line.contains("[new file]"), - "expected [new file] in every line, got: {line}" - ); - } - } -} diff --git a/crates/weaveffi-cli/src/commands/generate.rs b/crates/weaveffi-cli/src/commands/generate.rs index a7d3b92e..834f40ef 100644 --- a/crates/weaveffi-cli/src/commands/generate.rs +++ b/crates/weaveffi-cli/src/commands/generate.rs @@ -3,28 +3,29 @@ use camino::Utf8Path; use miette::{miette, IntoDiagnostic, Result, WrapErr}; -use weaveffi_core::codegen::Orchestrator; +use weaveffi_gen::codegen::{relative_path, GenerateReport, Hook, Orchestrator}; -#[allow(clippy::too_many_arguments)] -pub(crate) fn cmd_generate( - input: &str, - out: &str, - targets: Option<&str>, - config_path: Option<&str>, - warn: bool, - force: bool, - dry_run: bool, - quiet: bool, -) -> Result<()> { - let (config, api) = super::load_project(input, config_path, warn)?; - let out_dir = Utf8Path::new(out); - let hooks = config.hooks(); - let selected = config.select_targets(targets)?; +/// Options for [`cmd_generate`]. +pub(crate) struct GenerateArgs<'a> { + pub(crate) input: Option<&'a str>, + pub(crate) out: Option<&'a str>, + pub(crate) targets: Option<&'a str>, + pub(crate) config: Option<&'a str>, + pub(crate) warn: bool, + pub(crate) force: bool, + pub(crate) dry_run: bool, + pub(crate) quiet: bool, +} + +pub(crate) fn cmd_generate(args: &GenerateArgs<'_>) -> Result<()> { + let project = super::load_project(args.input, args.config, args.warn)?; + let out_dir = project.config.out_dir(args.out); + let selected = project.config.select_targets(args.targets)?; - if dry_run { + if args.dry_run { for target in &selected { - for path in target.output_files(&api, out_dir) { - println!("{path}"); + for file in target.render(&project.api, &out_dir) { + println!("{}", relative_path(&out_dir, &file.path)); } } return Ok(()); @@ -32,30 +33,63 @@ pub(crate) fn cmd_generate( std::fs::create_dir_all(out_dir.as_std_path()) .into_diagnostic() - .wrap_err_with(|| format!("failed to create output directory: {}", out))?; + .wrap_err_with(|| format!("failed to create output directory: {out_dir}"))?; let mut orchestrator = Orchestrator::new(); for target in &selected { orchestrator = orchestrator.with_target(target.as_ref()); } - - orchestrator - .run(&api, out_dir, &hooks, force) + let global = &project.config.global; + let report = orchestrator + .run(&project.api, &out_dir, args.force, &mut |hook| { + let cmd = match hook { + Hook::BeforeWrite => global.pre_generate.as_deref(), + Hook::AfterWrite => global.post_generate.as_deref(), + }; + match cmd { + Some(cmd) => { + super::run_hook(&format!("{hook:?}"), cmd).map_err(|e| anyhow::anyhow!("{e}")) + } + None => Ok(()), + } + }) .map_err(|e| miette!("{:#}", e))?; - if !quiet { - match &config.source { - Some(src) => println!("Generated artifacts in {out} (config: {src})"), - None => println!("Generated artifacts in {out}"), - } + for w in &report.warnings { + eprintln!("warning: {w}"); + } + if !args.quiet { + println!("{}", report_summary(&report, &out_dir)); } Ok(()) } +/// One line describing what a generation run did. +fn report_summary(report: &GenerateReport, out_dir: &Utf8Path) -> String { + if report.generated.is_empty() { + return format!( + "{out_dir} is up to date ({} targets)", + report.up_to_date.len() + ); + } + let mut line = format!( + "Generated {} in {out_dir}: {} written, {} unchanged", + report.generated.join(", "), + report.written, + report.unchanged + ); + if !report.removed.is_empty() { + line.push_str(&format!(", {} stale removed", report.removed.len())); + } + if !report.up_to_date.is_empty() { + line.push_str(&format!(" ({} up to date)", report.up_to_date.join(", "))); + } + line +} + #[cfg(test)] mod tests { use super::*; - use crate::config::ProjectConfig; #[test] fn dry_run_lists_files_without_writing() { @@ -63,48 +97,21 @@ mod tests { let yml = dir.path().join("api.yml"); std::fs::write( &yml, - concat!( - "version: \"0.9.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", - ), + "version: \"0.10.0\"\nmodules:\n - name: math\n functions:\n - { name: add, params: [{ name: a, type: i32 }], return: i32 }\n", ) .unwrap(); - let out = dir.path().join("out"); - let input = yml.to_str().unwrap(); - let out_str = out.to_str().unwrap(); - - cmd_generate(input, out_str, None, None, false, false, true, false).unwrap(); + cmd_generate(&GenerateArgs { + input: yml.to_str(), + out: out.to_str(), + targets: Some("c"), + config: None, + warn: false, + force: false, + dry_run: true, + quiet: false, + }) + .unwrap(); assert!(!out.exists(), "dry-run should not create output directory"); - - let (_, api) = super::super::load_project(input, None, false).unwrap(); - let out_dir = Utf8Path::new(out_str); - 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", - "kotlin/build.gradle.kts", - "node/types.d.ts", - "wasm/weaveffi_wasm.js", - "python/weaveffi/__init__.py", - ] { - assert!( - files.iter().any(|f| f.contains(expected)), - "missing {expected}: {files:?}" - ); - } } } diff --git a/crates/weaveffi-cli/src/commands/init.rs b/crates/weaveffi-cli/src/commands/init.rs new file mode 100644 index 00000000..cc113388 --- /dev/null +++ b/crates/weaveffi-cli/src/commands/init.rs @@ -0,0 +1,203 @@ +//! `weaveffi init`: set up a project so a bare `weaveffi generate` works. +//! +//! In a Rust crate (a directory with a `Cargo.toml` that has a `[package]`), +//! it writes a `weaveffi.toml` whose `[project] input` is the crate's +//! `src/lib.rs`, then checks the three things a producer needs (a `cdylib` +//! crate type, a `weaveffi` dependency, and one `weaveffi::export_runtime!()` +//! call) and prints any that are missing. It never edits `Cargo.toml` or +//! source files. Anywhere else, it writes a starter IDL plus a +//! `weaveffi.toml` that points at it. + +use camino::{Utf8Path, Utf8PathBuf}; +use miette::{bail, IntoDiagnostic, Result, WrapErr}; +use weaveffi_model::pkg::c_ident; + +use crate::config::CONFIG_FILE_NAME; + +/// Options for [`cmd_init`]. +pub(crate) struct InitArgs<'a> { + pub(crate) dir: &'a str, + pub(crate) name: Option<&'a str>, + pub(crate) force: bool, + pub(crate) quiet: bool, +} + +pub(crate) fn cmd_init(args: &InitArgs<'_>) -> Result<()> { + let dir = Utf8PathBuf::from(args.dir); + std::fs::create_dir_all(dir.as_std_path()) + .into_diagnostic() + .wrap_err_with(|| format!("failed to create {dir}"))?; + let config_path = dir.join(CONFIG_FILE_NAME); + if config_path.exists() && !args.force { + bail!("{config_path} already exists; pass --force to overwrite it"); + } + let manifest = dir.join("Cargo.toml"); + let crate_name = std::fs::read_to_string(manifest.as_std_path()) + .ok() + .and_then(|text| text.parse::().ok()) + .and_then(|doc| { + doc.get("package")? + .get("name")? + .as_str() + .map(str::to_string) + }); + match crate_name { + Some(name) => init_rust(&dir, &config_path, &name, args.quiet), + None => init_idl(&dir, &config_path, args.name, args.quiet), + } +} + +fn write(path: &Utf8Path, contents: &str) -> Result<()> { + std::fs::write(path.as_std_path(), contents) + .into_diagnostic() + .wrap_err_with(|| format!("failed to write {path}")) +} + +fn init_rust(dir: &Utf8Path, config_path: &Utf8Path, crate_name: &str, quiet: bool) -> Result<()> { + write( + config_path, + "# WeaveFFI project configuration. See https://weaveffi.com/guides/config.html\n\ + [project]\n\ + input = \"src/lib.rs\"\n\ + out = \"bindings\"\n\ + # targets = [\"c\", \"swift\", \"kotlin\", \"python\"]\n", + )?; + if quiet { + return Ok(()); + } + println!("Wrote {config_path} for the Rust producer `{crate_name}`."); + let manifest = + std::fs::read_to_string(dir.join("Cargo.toml").as_std_path()).unwrap_or_default(); + let lib = std::fs::read_to_string(dir.join("src/lib.rs").as_std_path()).unwrap_or_default(); + let mut todo = Vec::new(); + if !manifest.contains("cdylib") { + todo.push( + "add `crate-type = [\"cdylib\"]` (and \"staticlib\" for iOS) under [lib] in Cargo.toml", + ); + } + if !manifest.contains("weaveffi") { + todo.push("run `cargo add weaveffi`"); + } + if !lib.contains("export_runtime!") { + todo.push("call `weaveffi::export_runtime!();` once in src/lib.rs"); + } + if !lib.contains("weaveffi::module") { + todo.push("annotate your API module with `#[weaveffi::module]`"); + } + if todo.is_empty() { + println!("The crate is ready: run `weaveffi generate`."); + } else { + println!("Before running `weaveffi generate`:"); + for step in todo { + println!(" - {step}"); + } + } + Ok(()) +} + +fn init_idl(dir: &Utf8Path, config_path: &Utf8Path, name: Option<&str>, quiet: bool) -> Result<()> { + let name = match name { + Some(n) => n.to_string(), + None => std::path::absolute(dir.as_std_path()) + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) + .unwrap_or_else(|| "api".to_string()), + }; + let module = c_ident(&name); + let idl_name = format!("{module}.yml"); + let idl_path = dir.join(&idl_name); + if !idl_path.exists() { + write( + &idl_path, + &format!( + "# yaml-language-server: $schema=https://weaveffi.com/weaveffi.schema.json\n\ + version: \"{version}\"\n\ + modules:\n \ + - name: {module}\n \ + structs:\n \ + - name: Greeting\n \ + fields:\n \ + - {{ name: text, type: string }}\n \ + - {{ name: count, type: u32 }}\n \ + functions:\n \ + - name: greet\n \ + doc: Build a greeting for `name`.\n \ + params:\n \ + - {{ name: name, type: string }}\n \ + return: Greeting\n", + version = weaveffi_model::ir::CURRENT_SCHEMA_VERSION, + ), + )?; + } + write( + config_path, + &format!( + "# WeaveFFI project configuration. See https://weaveffi.com/guides/config.html\n\ + [project]\n\ + input = \"{idl_name}\"\n\ + out = \"bindings\"\n\ + \n\ + [package]\n\ + name = \"{name}\"\n\ + version = \"0.1.0\"\n" + ), + )?; + if !quiet { + println!("Wrote {idl_path} and {config_path}."); + println!( + "Run `weaveffi generate --target c` to produce the header your native library \ + implements, then generate the other targets you ship." + ); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn idl_project_validates_and_generates() { + let dir = tempfile::tempdir().unwrap(); + let root = Utf8Path::from_path(dir.path()).unwrap().join("greeter"); + cmd_init(&InitArgs { + dir: root.as_str(), + name: None, + force: false, + quiet: true, + }) + .unwrap(); + let (cfg, input) = + crate::config::ProjectConfig::locate(Some(root.join(CONFIG_FILE_NAME).as_str()), None) + .unwrap(); + assert_eq!(input, root.join("greeter.yml")); + assert_eq!(cfg.package.name.as_deref(), Some("greeter")); + crate::commands::load_validated_api(input.as_str()).unwrap(); + let again = cmd_init(&InitArgs { + dir: root.as_str(), + name: None, + force: false, + quiet: true, + }); + assert!(again.is_err(), "init must not overwrite without --force"); + } + + #[test] + fn rust_project_points_at_lib_rs() { + let dir = tempfile::tempdir().unwrap(); + let root = Utf8Path::from_path(dir.path()).unwrap(); + std::fs::write(root.join("Cargo.toml"), "[package]\nname = \"kv\"\n").unwrap(); + cmd_init(&InitArgs { + dir: root.as_str(), + name: None, + force: false, + quiet: true, + }) + .unwrap(); + let cfg = crate::config::ProjectConfig::from_file(&root.join(CONFIG_FILE_NAME)).unwrap(); + assert_eq!( + cfg.project.input.as_deref(), + Some(Utf8Path::new("src/lib.rs")) + ); + } +} diff --git a/crates/weaveffi-cli/src/commands/mod.rs b/crates/weaveffi-cli/src/commands/mod.rs index c3ec281a..c2ae44b0 100644 --- a/crates/weaveffi-cli/src/commands/mod.rs +++ b/crates/weaveffi-cli/src/commands/mod.rs @@ -3,17 +3,19 @@ pub(crate) mod diff; pub(crate) mod generate; +pub(crate) mod init; pub(crate) mod package; pub(crate) mod validate; use crate::config::ProjectConfig; use crate::report::with_named_source; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; use miette::{bail, IntoDiagnostic, Report, Result, WrapErr}; -use weaveffi_core::validate::{collect_warnings, validate_api}; -use weaveffi_core::ResolvedApi; -use weaveffi_ir::ir::Api; -use weaveffi_ir::parse::parse_api_str; +use weaveffi_model::ir::Api; +use weaveffi_model::parse::parse_api_str; +use weaveffi_model::pkg::{name_from_basename, Identity, Package}; +use weaveffi_model::validate::{collect_warnings, validate_api}; +use weaveffi_model::ResolvedApi; /// Map the input file extension onto the parser's format token. pub(crate) fn input_format(in_path: &Utf8Path) -> Result<&'static str> { @@ -25,10 +27,10 @@ pub(crate) fn input_format(in_path: &Utf8Path) -> Result<&'static str> { "yml" | "yaml" => Ok("yaml"), "json" => Ok("json"), "toml" => Ok("toml"), - other => bail!( + other => Err(miette::miette!( "unsupported input format: {} (expected rs|yml|yaml|json|toml)", other - ), + )), } } @@ -36,7 +38,7 @@ pub(crate) fn input_format(in_path: &Utf8Path) -> Result<&'static str> { /// [`Api`] and the raw file contents (for snippet-rendered diagnostics). /// /// A `.rs` input is treated as annotated Rust source and lowered to the IR -/// through [`weaveffi_bridge`] (the same extraction the `#[weaveffi::module]` +/// through [`weaveffi_model::rust`] (the same extraction the `#[weaveffi::module]` /// macro uses), so generating from a producer's source and building that /// producer cannot drift. Any other extension is parsed as an IDL document /// (yaml/json/toml). @@ -46,7 +48,7 @@ pub(crate) fn load_api(input: &str) -> Result<(Api, String)> { .into_diagnostic() .wrap_err_with(|| format!("failed to read input file: {}", input))?; if in_path.extension() == Some("rs") { - let api = weaveffi_bridge::api_from_src_stringly(&contents) + let api = weaveffi_model::rust::api_from_src_stringly(&contents) .map_err(|e| miette::miette!("failed to extract API from Rust source {input}:\n{e}"))?; return Ok((api, contents)); } @@ -64,55 +66,178 @@ pub(crate) fn load_validated_api(input: &str) -> Result<(ResolvedApi, String)> { 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. +/// A loaded project: its configuration, the input it was loaded from, and +/// the validated API with the library identity attached. +pub(crate) struct Project { + pub(crate) config: ProjectConfig, + pub(crate) input: Utf8PathBuf, + pub(crate) api: ResolvedApi, +} + +/// The shared front half of `generate`, `package`, and `diff`: locate the +/// project config and input (an explicit input, or `[project] input` from the +/// nearest `weaveffi.toml`), load and validate the API, attach the library's +/// [`Identity`], and optionally print advisory warnings. pub(crate) fn load_project( - input: &str, + input: Option<&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()); +) -> Result { + let (config, input) = ProjectConfig::locate(config_path, input)?; + let identity = resolve_identity(&input, &config.package)?; + let (api, _contents) = load_validated_api(input.as_str())?; + let api = api.with_identity(identity); if warn { for w in collect_warnings(&api) { eprintln!("warning: {w}"); } } - Ok((config, api)) + Ok(Project { config, input, 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 { +/// Run one of the project's `[global]` shell hooks (`sh -c` or `cmd /C`). +pub(crate) fn run_hook(label: &str, cmd: &str) -> Result<()> { + let status = if cfg!(target_os = "windows") { + std::process::Command::new("cmd").args(["/C", cmd]).status() + } else { + std::process::Command::new("sh").arg("-c").arg(cmd).status() + } + .into_diagnostic() + .wrap_err_with(|| format!("failed to run {label} hook"))?; + if !status.success() { + bail!("{label} hook `{cmd}` failed with {status}"); + } + Ok(()) +} + +/// Resolve the library's identity. +/// +/// A Rust producer (`.rs` input) is named after its crate: the nearest +/// `Cargo.toml` at or above the input supplies the package name and metadata, +/// and the crate's library name (`[lib] name`, else the package name with `-` +/// mapped to `_`) is both the C symbol prefix the `#[weaveffi::module]` macro +/// uses and the file name Cargo gives the cdylib. `weaveffi.toml` may rename +/// the published package and override metadata, but not the prefix or the +/// library. An IDL input is named by `[package] name`, else its file stem. +pub(crate) fn resolve_identity(input: &Utf8Path, package: &Package) -> Result { if input.extension() != Some("rs") { - return None; + let stem = name_from_basename(input.as_str()); + return Ok(Identity::new(&stem, package)); + } + if package.c_prefix.is_some() || package.library.is_some() { + bail!( + "`[package] c_prefix` and `[package] library` apply only to IDL inputs: a Rust \ + producer's C prefix and library name are its crate's library name" + ); + } + let Some(cargo) = CargoPackage::find(input)? else { + // No manifest: name a `src/lib.rs`-style input after its crate + // directory, as Cargo would, rather than after the file stem. + let stem = crate_dir_name(input).unwrap_or_else(|| name_from_basename(input.as_str())); + return Ok(Identity::new(&stem, package)); + }; + let mut merged = package.clone(); + merged.version = merged.version.or(cargo.version); + merged.description = merged.description.or(cargo.description); + merged.license = merged.license.or(cargo.license); + merged.homepage = merged.homepage.or(cargo.homepage); + merged.repository = merged.repository.or(cargo.repository); + if merged.authors.is_empty() { + merged.authors = cargo.authors; } + merged.c_prefix = Some(cargo.lib_name.clone()); + merged.library = Some(cargo.lib_name); + Ok(Identity::new(&cargo.name, &merged)) +} + +/// For a Rust input named `lib.rs`, `main.rs`, or `mod.rs`, the enclosing +/// crate directory's name (skipping a `src/` component). +fn crate_dir_name(input: &Utf8Path) -> Option { if !matches!(input.file_stem(), Some("lib" | "main" | "mod")) { return None; } - let mut dir = input.parent()?; - if dir.file_name() == Some("src") { + let abs = std::path::absolute(input.as_std_path()).ok()?; + let mut dir = abs.parent()?; + if dir.file_name().is_some_and(|n| n == "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())) - }) + dir.file_name().map(|n| n.to_string_lossy().into_owned()) +} + +/// The parts of a crate's `Cargo.toml` that name a Rust producer. +#[derive(Debug, Default, PartialEq)] +struct CargoPackage { + name: String, + lib_name: String, + version: Option, + description: Option, + license: Option, + homepage: Option, + repository: Option, + authors: Vec, +} + +impl CargoPackage { + /// Find and read the nearest `Cargo.toml` with a `[package]` table at or + /// above `input`. + fn find(input: &Utf8Path) -> Result> { + let start = std::path::absolute(input.as_std_path()).into_diagnostic()?; + for dir in start.ancestors().skip(1) { + let manifest = dir.join("Cargo.toml"); + if !manifest.is_file() { + continue; + } + let text = std::fs::read_to_string(&manifest) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read {}", manifest.display()))?; + if let Some(pkg) = Self::parse(&text) + .wrap_err_with(|| format!("failed to parse {}", manifest.display()))? + { + return Ok(Some(pkg)); + } + } + Ok(None) + } + + /// Parse a manifest; `None` for a virtual (workspace-only) manifest. + fn parse(text: &str) -> Result> { + let doc: toml::Table = toml::from_str(text).into_diagnostic()?; + let Some(pkg) = doc.get("package").and_then(toml::Value::as_table) else { + return Ok(None); + }; + let field = |key: &str| { + pkg.get(key) + .and_then(toml::Value::as_str) + .map(str::to_string) + }; + let Some(name) = field("name") else { + bail!("[package] has no name"); + }; + let lib_name = doc + .get("lib") + .and_then(|l| l.get("name")) + .and_then(toml::Value::as_str) + .map_or_else(|| name.replace('-', "_"), str::to_string); + let authors = pkg + .get("authors") + .and_then(toml::Value::as_array) + .map(|a| { + a.iter() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect() + }) + .unwrap_or_default(); + Ok(Some(Self { + lib_name, + version: field("version"), + description: field("description"), + license: field("license"), + homepage: field("homepage"), + repository: field("repository"), + authors, + name, + })) + } } #[cfg(test)] @@ -120,20 +245,49 @@ 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") - ); + fn cargo_manifest_names_the_producer() { + let pkg = CargoPackage::parse( + "[package]\nname = \"my-kv\"\nversion = \"1.2.3\"\nlicense = \"MIT\"\n", + ) + .unwrap() + .unwrap(); + assert_eq!(pkg.name, "my-kv"); + assert_eq!(pkg.lib_name, "my_kv"); + assert_eq!(pkg.version.as_deref(), Some("1.2.3")); + let renamed = CargoPackage::parse("[package]\nname = \"kv\"\n[lib]\nname = \"kvcore\"\n") + .unwrap() + .unwrap(); + assert_eq!(renamed.lib_name, "kvcore"); + assert!(CargoPackage::parse("[workspace]\nmembers = []\n") + .unwrap() + .is_none()); + } + + #[test] + fn sample_producer_identity_comes_from_its_crate() { + let root = + Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../samples/kvstore/src/lib.rs"); + let id = resolve_identity(&root, &Package::default()).unwrap(); + assert_eq!(id.name, "kvstore"); + assert_eq!(id.prefix, "kvstore"); + assert_eq!(id.library, "kvstore"); + } + + #[test] + fn idl_identity_comes_from_the_file_stem_or_package() { + let id = + resolve_identity(Utf8Path::new("api/kitchen_sink.yml"), &Package::default()).unwrap(); + assert_eq!(id.prefix, "kitchen_sink"); + let pkg = Package { + name: Some("acme-kv".into()), + c_prefix: Some("akv".into()), + ..Package::default() + }; + let id = resolve_identity(Utf8Path::new("api/kv.yml"), &pkg).unwrap(); assert_eq!( - crate_dir_name(Utf8Path::new("crates/foo/lib.rs")).as_deref(), - Some("foo") + (id.name.as_str(), id.prefix.as_str(), id.library.as_str()), + ("acme-kv", "akv", "acme_kv") ); - assert_eq!(crate_dir_name(Utf8Path::new("api/kvstore.rs")), None); - assert_eq!(crate_dir_name(Utf8Path::new("api/kvstore.yml")), None); + assert!(resolve_identity(Utf8Path::new("src/lib.rs"), &pkg).is_err()); } } diff --git a/crates/weaveffi-cli/src/commands/package.rs b/crates/weaveffi-cli/src/commands/package.rs index 3247a73f..918c0a7a 100644 --- a/crates/weaveffi-cli/src/commands/package.rs +++ b/crates/weaveffi-cli/src/commands/package.rs @@ -17,9 +17,8 @@ use camino::{Utf8Path, Utf8PathBuf}; use miette::{bail, miette, IntoDiagnostic, Result, WrapErr}; use std::process::{Command, Stdio}; -use weaveffi_core::package::{summarize, write_package, PackageContext}; -use weaveffi_core::pkg; -use weaveffi_core::platform::{BinarySet, Platform}; +use weaveffi_gen::package::{summarize, write_package, PackageContext}; +use weaveffi_gen::platform::{BinarySet, Os, Platform}; /// How `weaveffi package` should obtain the native libraries it bundles. pub(crate) enum BinarySource<'a> { @@ -29,43 +28,64 @@ pub(crate) enum BinarySource<'a> { Build(&'a str), } -#[allow(clippy::too_many_arguments)] -pub(crate) fn cmd_package( - input: &str, - out: &str, - targets: Option<&str>, - config_path: Option<&str>, - binaries: Option<&str>, - build: Option<&str>, - platforms: Option<&str>, - warn: bool, - quiet: bool, -) -> Result<()> { +/// Options for [`cmd_package`]. +pub(crate) struct PackageArgs<'a> { + pub(crate) input: Option<&'a str>, + pub(crate) out: &'a str, + pub(crate) targets: Option<&'a str>, + pub(crate) config: Option<&'a str>, + pub(crate) binaries: Option<&'a str>, + pub(crate) build: Option<&'a str>, + pub(crate) platforms: Option<&'a str>, + pub(crate) warn: bool, + pub(crate) quiet: bool, +} + +pub(crate) fn cmd_package(args: &PackageArgs<'_>) -> Result<()> { + let PackageArgs { + out, + targets, + binaries, + build, + platforms, + quiet, + .. + } = *args; let source = match (binaries, build) { (Some(_), Some(_)) => { - bail!("--binaries and --build are mutually exclusive; choose one source for the native libraries") + return Err(miette::miette!("--binaries and --build are mutually exclusive; choose one source for the native libraries")) } (Some(dir), None) => BinarySource::Prebuilt(dir), (None, Some(crate_name)) => BinarySource::Build(crate_name), - (None, None) => bail!( + (None, None) => return Err(miette::miette!( "provide native libraries with --binaries (laid out as //) \ or --build to cross-compile a Rust producer" - ), + )), }; - let (config, api) = super::load_project(input, config_path, warn)?; - let in_path = Utf8Path::new(input); + let project = super::load_project(args.input, args.config, args.warn)?; + let (config, api) = (&project.config, &project.api); - let selected_platforms = parse_platforms(platforms)?; - let input_basename = in_path.file_name(); - let lib_name = pkg::resolve(&api, None, input_basename).ident_name(); + let explicit_platforms = platforms.is_some(); + let input_basename = project.input.file_name(); + let lib_name = api.identity().library.clone(); let binary_set = match source { BinarySource::Prebuilt(dir) => { - discover_prebuilt(Utf8Path::new(dir), &lib_name, &selected_platforms, quiet)? + // Without --platforms, package every platform the directory has. + let selected = match platforms { + Some(_) => parse_platforms(platforms)?, + None => Platform::ALL.to_vec(), + }; + discover_prebuilt( + Utf8Path::new(dir), + &lib_name, + &selected, + quiet || !explicit_platforms, + )? } BinarySource::Build(crate_name) => { - cross_build(crate_name, &lib_name, &selected_platforms, quiet)? + cross_build(crate_name, &lib_name, &parse_platforms(platforms)?, quiet)? } }; @@ -97,7 +117,7 @@ pub(crate) fn cmd_package( let mut packaged = 0usize; let mut skipped: Vec<&str> = Vec::new(); for gen in &selected { - match gen.package(&api, &ctx, out_dir) { + match gen.package(api, &ctx, out_dir) { Some(files) => { write_package(&files).map_err(|e| miette!("{:#}", e))?; let (text, bins) = summarize(&files); @@ -130,6 +150,10 @@ pub(crate) fn cmd_package( ); } + if selected.iter().any(|t| t.name() == "swift") { + assemble_xcframework(&project, &binary_set, out_dir, quiet)?; + } + if !quiet { println!("Packaged {packaged} target(s) into {out}"); } @@ -138,9 +162,125 @@ pub(crate) fn cmd_package( /// Parse the comma-separated `--platforms` list into [`Platform`] values, /// defaulting to the full v1 matrix when omitted. +/// The platforms to build with `--build`; without `--platforms`, just the +/// host (cross builds need targets and linkers most machines don't have, so +/// they are opt-in). +/// Fuse the Apple slices into `swift/C{Module}.xcframework` beside the +/// generated `Package.swift`, which switches to it as a binary target. +/// +/// Runs only when the set includes an iOS slice (iOS needs the XCFramework; +/// macOS alone links the bundled dylib through the system-library target). +/// Each slice is a static library: iOS slices are the crate's `staticlib` +/// output, and a macOS slice is the `lib{library}.a` next to its dylib (so a +/// producer that ships to both declares `crate-type = ["cdylib", "staticlib"]`). +/// Simulator and macOS architectures are fused with `lipo`. +fn assemble_xcframework( + project: &super::Project, + binaries: &BinarySet, + out_dir: &Utf8Path, + quiet: bool, +) -> Result<()> { + if !binaries.platforms().any(|p| p.os() == Os::Ios) { + return Ok(()); + } + if !cfg!(target_os = "macos") { + eprintln!("warning: skipping the Swift XCFramework: xcodebuild needs macOS"); + return Ok(()); + } + let identity = project.api.identity(); + let library = &identity.library; + let module = project + .config + .generators + .swift + .module_name + .clone() + .unwrap_or_else(|| identity.pascal_name()); + let static_lib = |p: Platform| -> Option { + let nb = binaries.get(p)?; + if p.os() == Os::Ios { + return Some(nb.source.clone()); + } + let sibling = nb.source.parent()?.join(format!("lib{library}.a")); + sibling.exists().then_some(sibling) + }; + let scratch = tempfile::tempdir().into_diagnostic()?; + let scratch = Utf8Path::from_path(scratch.path()) + .ok_or_else(|| miette!("temp directory path is not valid UTF-8"))? + .to_path_buf(); + let headers = scratch.join("Headers"); + std::fs::create_dir_all(headers.as_std_path()).into_diagnostic()?; + let header = format!("{library}.h"); + let model = weaveffi_model::model::BindingModel::build(&project.api); + let input_basename = project.input.file_name().unwrap_or("api"); + std::fs::write( + headers.join(&header).as_std_path(), + weaveffi_gen::targets::c::render_c_header_from_model(&model, input_basename, &header), + ) + .into_diagnostic()?; + std::fs::write( + headers.join("module.modulemap").as_std_path(), + format!("module C{module} {{\n header \"{header}\"\n export *\n}}\n"), + ) + .into_diagnostic()?; + + let mut args: Vec = vec!["-create-xcframework".into()]; + for (group, platforms) in [ + ("ios", &[Platform::IosArm64][..]), + ( + "ios-simulator", + &[Platform::IosSimArm64, Platform::IosSimX64][..], + ), + ("macos", &[Platform::MacosArm64, Platform::MacosX64][..]), + ] { + let libs: Vec = platforms.iter().filter_map(|p| static_lib(*p)).collect(); + let lib = match libs.as_slice() { + [] => continue, + [one] => one.clone(), + many => { + let fat = scratch.join(group).join(format!("lib{library}.a")); + std::fs::create_dir_all(scratch.join(group).as_std_path()).into_diagnostic()?; + let status = Command::new("lipo") + .arg("-create") + .args(many.iter().map(|p| p.as_str())) + .args(["-output", fat.as_str()]) + .status() + .into_diagnostic() + .wrap_err("failed to run lipo")?; + if !status.success() { + bail!("lipo failed to fuse the {group} slices"); + } + fat + } + }; + args.extend(["-library".into(), lib.to_string()]); + args.extend(["-headers".into(), headers.to_string()]); + } + let output = out_dir.join("swift").join(format!("C{module}.xcframework")); + if output.exists() { + std::fs::remove_dir_all(output.as_std_path()).into_diagnostic()?; + } + args.extend(["-output".into(), output.to_string()]); + let status = Command::new("xcodebuild") + .args(&args) + .stdout(Stdio::null()) + .status() + .into_diagnostic() + .wrap_err("failed to run xcodebuild")?; + if !status.success() { + bail!("xcodebuild -create-xcframework failed"); + } + if !quiet { + println!(" swift: assembled {output}"); + } + Ok(()) +} + fn parse_platforms(platforms: Option<&str>) -> Result> { let Some(list) = platforms else { - return Ok(Platform::ALL.to_vec()); + return Platform::host().map(|p| vec![p]).ok_or_else(|| { + miette!("the host is not a packaging platform; pass --platforms explicitly") + }); }; let mut out = Vec::new(); for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) { @@ -231,10 +371,10 @@ fn find_library( if let Some(hit) = matches.iter().find(|p| p.file_name() == Some(&canonical)) { Ok(Some(hit.clone())) } else { - bail!( + Err(miette::miette!( "multiple .{ext} libraries in {platform_dir}; \ name one '{canonical}' to disambiguate" - ) + )) } } } @@ -249,6 +389,7 @@ fn cross_build( quiet: bool, ) -> Result { let mut set = BinarySet::new(lib_name); + let mut failed = Vec::new(); for &platform in platforms { if !quiet { println!( @@ -257,8 +398,20 @@ fn cross_build( platform.rust_target() ); } - let lib = build_one(crate_name, platform)?; - set.insert(platform, lib); + match build_one(crate_name, platform) { + Ok(lib) => set.insert(platform, lib), + Err(e) => { + eprintln!("error: {e:?}"); + failed.push(platform.id()); + } + } + } + if !failed.is_empty() { + bail!( + "cargo build failed for {}; the other platforms built (fix or drop the failing \ + ones with --platforms)", + failed.join(", ") + ); } Ok(set) } @@ -304,11 +457,16 @@ fn build_one(crate_name: &str, platform: Platform) -> Result { if msg.get("reason").and_then(|r| r.as_str()) != Some("compiler-artifact") { continue; } + let want = if platform.os() == Os::Ios { + "staticlib" + } else { + "cdylib" + }; let is_cdylib = msg .get("target") .and_then(|t| t.get("kind")) .and_then(|k| k.as_array()) - .map(|kinds| kinds.iter().any(|k| k.as_str() == Some("cdylib"))) + .map(|kinds| kinds.iter().any(|k| k.as_str() == Some(want))) .unwrap_or(false); if !is_cdylib { continue; @@ -323,10 +481,11 @@ fn build_one(crate_name: &str, platform: Platform) -> Result { } match produced.len() { - 0 => bail!( + 0 => Err(miette::miette!( "cargo built {crate_name} for {triple} but produced no .{ext} cdylib; \ - ensure the crate declares `crate-type = [\"cdylib\"]`" - ), + ensure the crate declares `crate-type = [\"cdylib\", \"staticlib\"]` \ + (iOS slices are static libraries)" + )), 1 => Ok(produced.into_iter().next().unwrap()), _ => { // Prefer the artifact whose stem matches the crate's normalized lib name. @@ -346,8 +505,10 @@ mod tests { use super::*; #[test] - fn parse_platforms_defaults_to_full_matrix() { - assert_eq!(parse_platforms(None).unwrap(), Platform::ALL.to_vec()); + fn parse_platforms_defaults_to_the_host() { + if let Some(host) = Platform::host() { + assert_eq!(parse_platforms(None).unwrap(), vec![host]); + } } #[test] diff --git a/crates/weaveffi-cli/src/commands/validate.rs b/crates/weaveffi-cli/src/commands/validate.rs index ff6eb22d..e79831b8 100644 --- a/crates/weaveffi-cli/src/commands/validate.rs +++ b/crates/weaveffi-cli/src/commands/validate.rs @@ -2,15 +2,19 @@ //! json` output, plus advisory warnings under `--warn`. use miette::{Report, Result}; -use weaveffi_core::validate::{collect_warnings, validate_api, ValidationError, ValidationWarning}; +use weaveffi_model::validate::{ + collect_warnings, validate_api, ValidationError, ValidationWarning, +}; pub(crate) fn cmd_validate( - input: &str, + input: Option<&str>, + config: Option<&str>, warn: bool, - format: Option<&str>, + json_mode: bool, quiet: bool, ) -> Result<()> { - let json_mode = format == Some("json"); + let (_, input) = crate::config::ProjectConfig::locate(config, input)?; + let input = input.as_str(); let (api, contents) = super::load_api(input)?; match validate_api(api, Some((input, &contents))) { @@ -20,17 +24,17 @@ pub(crate) fn cmd_validate( } 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(); - let n_enums: usize = api.modules.iter().map(|m| m.enums.len()).sum(); + let counts = Counts::of(&api.modules); if json_mode { let json = serde_json::json!({ "ok": true, - "modules": n_modules, - "functions": n_functions, - "structs": n_structs, - "enums": n_enums, + "modules": counts.modules, + "functions": counts.functions, + "interfaces": counts.interfaces, + "callback_interfaces": counts.callback_interfaces, + "records": counts.records, + "enums": counts.enums, + "error_domains": counts.error_domains, "warnings": warnings.iter().map(warning_to_json).collect::>(), }); println!("{json}"); @@ -39,10 +43,7 @@ pub(crate) fn cmd_validate( eprintln!("warning: {w}"); } println!("Validation passed"); - println!( - " {} modules, {} functions, {} structs, {} enums", - n_modules, n_functions, n_structs, n_enums - ); + println!(" {}", counts.summary()); } Ok(()) } @@ -64,6 +65,61 @@ pub(crate) fn cmd_validate( } } +/// Declaration counts across a module tree, for the success summary. +#[derive(Default)] +struct Counts { + modules: usize, + functions: usize, + interfaces: usize, + callback_interfaces: usize, + records: usize, + enums: usize, + error_domains: usize, +} + +impl Counts { + fn of(modules: &[weaveffi_model::ir::Module]) -> Self { + let mut c = Self::default(); + c.add(modules); + c + } + + fn add(&mut self, modules: &[weaveffi_model::ir::Module]) { + for m in modules { + self.modules += 1; + self.functions += m.functions.len(); + self.interfaces += m.interfaces.len(); + self.callback_interfaces += m.callback_interfaces.len(); + self.records += m.structs.len(); + self.enums += m.enums.len(); + self.error_domains += usize::from(m.errors.is_some()); + self.add(&m.modules); + } + } + + fn summary(&self) -> String { + let parts = [ + (self.modules, "module", "modules"), + (self.functions, "function", "functions"), + (self.interfaces, "interface", "interfaces"), + ( + self.callback_interfaces, + "callback interface", + "callback interfaces", + ), + (self.records, "record", "records"), + (self.enums, "enum", "enums"), + (self.error_domains, "error domain", "error domains"), + ]; + parts + .iter() + .filter(|(n, ..)| *n > 0) + .map(|(n, one, many)| format!("{n} {}", if *n == 1 { one } else { many })) + .collect::>() + .join(", ") + } +} + /// 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 @@ -111,7 +167,8 @@ fn validation_error_code(err: &ValidationError) -> &'static str { ValidationError::DuplicateTypeName { .. } => "DuplicateTypeName", ValidationError::DuplicateErrorCodeName { .. } => "DuplicateErrorCodeName", ValidationError::ThrowsWithoutErrorDomain { .. } => "ThrowsWithoutErrorDomain", - ValidationError::AbiSymbolCollision { .. } => "AbiSymbolCollision", + ValidationError::SymbolCollision { .. } => "SymbolCollision", + ValidationError::CancellableNotAsync { .. } => "CancellableNotAsync", } } @@ -262,9 +319,18 @@ fn validation_error_to_json(err: &ValidationError) -> serde_json::Value { obj.insert("module".into(), Value::String(module.clone())); obj.insert("function".into(), Value::String(function.clone())); } - ValidationError::AbiSymbolCollision { module, symbol } => { - obj.insert("module".into(), Value::String(module.clone())); + ValidationError::SymbolCollision { + symbol, + first, + second, + } => { obj.insert("symbol".into(), Value::String(symbol.clone())); + obj.insert("first".into(), Value::String(first.clone())); + obj.insert("second".into(), Value::String(second.clone())); + } + ValidationError::CancellableNotAsync { module, function } => { + obj.insert("module".into(), Value::String(module.clone())); + obj.insert("function".into(), Value::String(function.clone())); } } obj.insert("message".into(), Value::String(err.to_string())); diff --git a/crates/weaveffi-cli/src/config.rs b/crates/weaveffi-cli/src/config.rs index 2a81c7ec..2fe1dc8f 100644 --- a/crates/weaveffi-cli/src/config.rs +++ b/crates/weaveffi-cli/src/config.rs @@ -2,45 +2,46 @@ //! `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: +//! A project's identity, inputs, and generator options live in one TOML file +//! at the project root: //! //! ```toml +//! [project] +//! input = "src/lib.rs" # or an IDL such as "kvstore.yml" +//! out = "bindings" +//! targets = ["c", "swift", "python"] +//! //! [package] //! name = "kvstore" //! version = "1.2.0" //! license = "MIT" //! -//! [global] -//! c_prefix = "kv" -//! //! [generators.swift] //! module_name = "KVStore" -//! -//! [generators.kotlin] -//! 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. +//! The API definition describes only the API surface. With a `[project]` +//! table, `weaveffi generate` (no arguments) works from any directory inside +//! the project, like `cargo build`. With an explicit input, the CLI finds the +//! nearest `weaveffi.toml` at or above the input's directory; `--config +//! ` names one explicitly. use camino::{Utf8Path, Utf8PathBuf}; use miette::{IntoDiagnostic, Result, WrapErr}; use serde::Deserialize; -use weaveffi_core::codegen::{ConfiguredBackend, OrchestratorHooks, Target}; -use weaveffi_core::pkg::Package; -use weaveffi_gen_c::{CConfig, CGenerator}; -use weaveffi_gen_cpp::{CppConfig, CppGenerator}; -use weaveffi_gen_dart::{DartConfig, DartGenerator}; -use weaveffi_gen_dotnet::{DotnetConfig, DotnetGenerator}; -use weaveffi_gen_go::{GoConfig, GoGenerator}; -use weaveffi_gen_kotlin::{KotlinConfig, KotlinGenerator}; -use weaveffi_gen_node::{NodeConfig, NodeGenerator}; -use weaveffi_gen_python::{PythonConfig, PythonGenerator}; -use weaveffi_gen_ruby::{RubyConfig, RubyGenerator}; -use weaveffi_gen_swift::{SwiftConfig, SwiftGenerator}; -use weaveffi_gen_wasm::{WasmConfig, WasmGenerator}; +use weaveffi_gen::codegen::{ConfiguredBackend, Target}; +use weaveffi_gen::targets::c::{CConfig, CGenerator}; +use weaveffi_gen::targets::cpp::{CppConfig, CppGenerator}; +use weaveffi_gen::targets::dart::{DartConfig, DartGenerator}; +use weaveffi_gen::targets::dotnet::{DotnetConfig, DotnetGenerator}; +use weaveffi_gen::targets::go::{GoConfig, GoGenerator}; +use weaveffi_gen::targets::kotlin::{KotlinConfig, KotlinGenerator}; +use weaveffi_gen::targets::node::{NodeConfig, NodeGenerator}; +use weaveffi_gen::targets::python::{PythonConfig, PythonGenerator}; +use weaveffi_gen::targets::ruby::{RubyConfig, RubyGenerator}; +use weaveffi_gen::targets::swift::{SwiftConfig, SwiftGenerator}; +use weaveffi_gen::targets::wasm::{WasmConfig, WasmGenerator}; +use weaveffi_model::pkg::Package; /// The file name `weaveffi generate` looks for when `--config` is absent. pub(crate) const CONFIG_FILE_NAME: &str = "weaveffi.toml"; @@ -52,8 +53,8 @@ pub(crate) const CONFIG_FILE_NAME: &str = "weaveffi.toml"; /// * 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. +/// * `stamp_input_basename` / `fan_strip_module_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 @@ -99,20 +100,6 @@ macro_rules! cli_targets { fn fan_strip_module_prefix(&mut self, value: bool) { $( $( cli_targets!(@strip self, $field, value, $strip); )? )* } - - /// 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` reaches all eleven - /// languages. - fn fan_c_prefix(&mut self, resolved: Option) { - let Some(p) = resolved else { return }; - $( - if self.$field.prefix.is_none() { - self.$field.prefix = Some(p.clone()); - } - )* - } } }; (@strip $self:ident, $field:ident, $value:ident, strip) => { @@ -125,7 +112,7 @@ cli_targets! { "cpp" => cpp: CppConfig via CppGenerator, "swift" => swift: SwiftConfig via SwiftGenerator, strip, "kotlin" => kotlin: KotlinConfig via KotlinGenerator, strip, - "node" => node: NodeConfig via NodeGenerator, strip, + "node" => node: NodeConfig via NodeGenerator, "wasm" => wasm: WasmConfig via WasmGenerator, "python" => python: PythonConfig via PythonGenerator, strip, "dotnet" => dotnet: DotnetConfig via DotnetGenerator, strip, @@ -145,21 +132,33 @@ pub(crate) struct GlobalConfig { /// once. When set, this overrides per-target sections; omit it to /// control targets individually. pub(crate) strip_module_prefix: Option, - /// Global C ABI symbol prefix (default `"weaveffi"`). Applies to every - /// target so generated consumers across all languages call the identical - /// exported symbols. A per-target `prefix` overrides this for that target. - pub(crate) c_prefix: Option, /// Shell command executed once before any generator runs. pub(crate) pre_generate: Option, /// Shell command executed once after every generator succeeds. pub(crate) post_generate: Option, } -/// The whole `weaveffi.toml`: package identity, global knobs, and per-target -/// generator options. +/// The `[project]` table: what to generate from, where to, and for which +/// targets, so a configured project needs no command-line arguments. Paths +/// are relative to the directory containing `weaveffi.toml`. +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub(crate) struct ProjectTable { + /// The API definition: annotated Rust source or an IDL document. + pub(crate) input: Option, + /// The output directory for `generate` and `diff` (default `generated`). + pub(crate) out: Option, + /// The targets to generate when `--target` is not given (default: all). + pub(crate) targets: Option>, +} + +/// The whole `weaveffi.toml`: project paths, package identity, global knobs, +/// and per-target generator options. #[derive(Debug, Default, Clone, Deserialize)] #[serde(default, deny_unknown_fields)] pub(crate) struct ProjectConfig { + /// The `[project]` table. + pub(crate) project: ProjectTable, /// The `[package]` table: distribution identity shared by every /// generated manifest. pub(crate) package: Package, @@ -210,47 +209,83 @@ impl ProjectConfig { } /// Fan `global.strip_module_prefix` out to every per-target config that - /// 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 + /// honors it, 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.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 `[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.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. + /// Resolve the project for a command: the config (explicit, discovered + /// from `input`, or discovered from the current directory) and the input + /// file (the argument, else `[project] input`), finalized against the + /// input. /// /// # Errors /// - /// Propagates errors from [`ProjectConfig::load`]. - pub(crate) fn for_input(explicit: Option<&str>, input: &Utf8Path) -> Result { - let mut cfg = Self::load(explicit, input)?; + /// Returns an error when no input is given and no discovered config + /// names one, or when a config file cannot be read or parsed. + pub(crate) fn locate( + explicit: Option<&str>, + input: Option<&str>, + ) -> Result<(Self, Utf8PathBuf)> { + let (mut cfg, input) = match input { + Some(i) => ( + Self::load(explicit, Utf8Path::new(i))?, + Utf8PathBuf::from(i), + ), + None => { + let path = match explicit { + Some(p) => Utf8PathBuf::from(p), + None => current_dir() + .ok() + .and_then(|cwd| discover(&cwd.join(CONFIG_FILE_NAME))) + .ok_or_else(|| { + miette::miette!( + "no input given and no {CONFIG_FILE_NAME} found in this directory \ + or any parent; pass an input file or run `weaveffi init`" + ) + })?, + }; + let cfg = Self::from_file(&path)?; + let Some(rel) = cfg.project.input.clone() else { + miette::bail!( + "no input given and {path} has no `[project] input`; pass an input \ + file or set `input` in the [project] table" + ); + }; + let input = cfg.relative_to_config(&rel); + (cfg, input) + } + }; cfg.finalize(input.file_name().map(str::to_string)); - Ok(cfg) + Ok((cfg, input)) + } + + /// Resolve `path` against the directory holding the config file (or the + /// current directory when there is no file). + pub(crate) fn relative_to_config(&self, path: &Utf8Path) -> Utf8PathBuf { + match self.source.as_deref().and_then(Utf8Path::parent) { + Some(dir) if path.is_relative() && !dir.as_str().is_empty() => dir.join(path), + _ => path.to_path_buf(), + } } - /// The orchestrator hooks configured in `[global]`. - pub(crate) fn hooks(&self) -> OrchestratorHooks { - OrchestratorHooks { - pre_generate: self.global.pre_generate.clone(), - post_generate: self.global.post_generate.clone(), + /// The output directory: `out` if given on the command line, else + /// `[project] out`, else `./generated`. + pub(crate) fn out_dir(&self, out: Option<&str>) -> Utf8PathBuf { + match (out, &self.project.out) { + (Some(o), _) => Utf8PathBuf::from(o), + (None, Some(o)) => self.relative_to_config(o), + (None, None) => Utf8PathBuf::from("./generated"), } } /// Build every registered target, keeping only those named in the - /// comma-separated `filter` (all of them when `filter` is `None`). + /// comma-separated `filter`, else those listed in `[project] targets`, + /// else all of them. /// /// # Errors /// @@ -258,12 +293,19 @@ impl ProjectConfig { /// 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() - }); + let wanted: Option> = match filter { + Some(t) => Some( + t.split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .collect(), + ), + None => self + .project + .targets + .as_ref() + .map(|ts| ts.iter().map(String::as_str).collect()), + }; if let Some(names) = &wanted { let unknown: Vec<&str> = names .iter() @@ -287,6 +329,12 @@ impl ProjectConfig { } } +/// The current directory as a UTF-8 path. +fn current_dir() -> std::io::Result { + Utf8PathBuf::from_path_buf(std::env::current_dir()?) + .map_err(|_| std::io::Error::other("current directory is not valid UTF-8")) +} + /// Walk from the input file's directory up to the filesystem root and return /// the first `weaveffi.toml` found. fn discover(input: &Utf8Path) -> Option { @@ -328,7 +376,6 @@ mod tests { "version = \"1.2.0\"\n", "[global]\n", "strip_module_prefix = true\n", - "c_prefix = \"kv\"\n", "[generators.swift]\n", "module_name = \"MyApp\"\n", "[generators.kotlin]\n", @@ -338,14 +385,12 @@ mod tests { 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.kotlin.package(), "com.example.myapp"); + assert_eq!(cfg.generators.swift.module_name.as_deref(), Some("MyApp")); + assert_eq!(cfg.generators.kotlin.package(), Some("com.example.myapp")); assert_eq!(cfg.global.strip_module_prefix, Some(true)); assert_eq!(cfg.source.as_deref(), Some(cfg_path.as_path())); 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(), @@ -375,7 +420,7 @@ mod tests { CONFIG_FILE_NAME, "[package]\nname = \"found\"\n", ); - let input = write(&nested, "api.yml", "version: \"0.9.0\"\nmodules: []\n"); + let input = write(&nested, "api.yml", "version: \"0.10.0\"\nmodules: []\n"); let cfg = ProjectConfig::load(None, &input).unwrap(); assert_eq!(cfg.package.name.as_deref(), Some("found")); @@ -384,11 +429,9 @@ mod tests { #[test] fn defaults_when_no_config_exists() { let dir = tempfile::tempdir().unwrap(); - let input = write(dir.path(), "api.yml", "version: \"0.9.0\"\nmodules: []\n"); + let input = write(dir.path(), "api.yml", "version: \"0.10.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.kotlin.package(), "com.weaveffi"); assert_eq!(cfg.global.strip_module_prefix, None); assert!(cfg.package.name.is_none()); } @@ -417,6 +460,34 @@ mod tests { ); } + #[test] + fn project_table_supplies_input_out_and_targets() { + let dir = tempfile::tempdir().unwrap(); + let cfg_path = write( + dir.path(), + CONFIG_FILE_NAME, + "[project]\ninput = \"api.yml\"\nout = \"bindings\"\ntargets = [\"c\", \"python\"]\n", + ); + write(dir.path(), "api.yml", "version: \"0.10.0\"\nmodules: []\n"); + let (cfg, input) = ProjectConfig::locate(Some(cfg_path.as_str()), None).unwrap(); + let root = cfg_path.parent().unwrap(); + assert_eq!(input, root.join("api.yml")); + assert_eq!(cfg.out_dir(None), root.join("bindings")); + assert_eq!(cfg.out_dir(Some("x")), Utf8PathBuf::from("x")); + let names: Vec<&str> = cfg + .select_targets(None) + .unwrap() + .iter() + .map(|t| t.name()) + .collect(); + assert_eq!(names, ["c", "python"]); + assert_eq!(cfg.select_targets(Some("go")).unwrap().len(), 1); + + let bare = write(dir.path(), "other.toml", "[package]\nname = \"x\"\n"); + let err = ProjectConfig::locate(Some(bare.as_str()), None).unwrap_err(); + assert!(format!("{err}").contains("[project] input"), "{err}"); + } + /// Every registered target's *declared* [`TargetCapabilities`] must match /// the documented feature matrix. This pins the declarations so a backend /// cannot silently claim (or drop) a gated feature: the matrix here, the @@ -430,7 +501,7 @@ mod tests { #[test] fn declared_capabilities_match_documented_matrix() { use std::collections::BTreeMap; - use weaveffi_core::capabilities::TargetCapabilities; + use weaveffi_gen::capabilities::TargetCapabilities; // Every target is full. Wasm delivers callbacks/listeners // synchronously through function-table trampolines in its standard diff --git a/crates/weaveffi-cli/src/extract.rs b/crates/weaveffi-cli/src/extract.rs index af24a941..5fc15d33 100644 --- a/crates/weaveffi-cli/src/extract.rs +++ b/crates/weaveffi-cli/src/extract.rs @@ -1,6 +1,6 @@ //! `weaveffi extract`: read annotated Rust source and emit the IDL. //! -//! This is thin CLI glue around [`weaveffi_bridge`], the single Rust-to-IR +//! This is thin CLI glue around [`weaveffi_model::rust`], the single Rust-to-IR //! bridge that the `#[weaveffi::module]` proc-macro also uses. Because both the //! macro (which builds the C ABI scaffolding) and this command read the *same* //! extraction, the emitted IDL and the producer's compiled symbols cannot @@ -15,7 +15,7 @@ use miette::{miette, IntoDiagnostic, WrapErr}; -/// Read the annotated Rust file at `input`, extract its [`Api`](weaveffi_ir::ir::Api), +/// Read the annotated Rust file at `input`, extract its [`Api`](weaveffi_model::ir::Api), /// validate, and serialize to the requested `format` (`yaml`, `json`, or `toml`). /// /// # Errors @@ -26,50 +26,46 @@ use miette::{miette, IntoDiagnostic, WrapErr}; pub(crate) fn cmd_extract( input: &str, output: Option<&str>, - format: &str, - warn: bool, + format: crate::IdlFormat, + lenient: bool, quiet: bool, ) -> miette::Result<()> { let source = std::fs::read_to_string(input) .into_diagnostic() .wrap_err_with(|| format!("failed to read source file: {input}"))?; - let api = weaveffi_bridge::api_from_src_stringly(&source) + let api = weaveffi_model::rust::api_from_src_stringly(&source) .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 and stable across runs); fall back // to the raw extracted document under `--warn`. - let api = match weaveffi_core::validate::validate_api(api.clone(), None) { + let api = match weaveffi_model::validate::validate_api(api.clone(), None) { Ok(resolved) => resolved.api().clone(), Err(e) => { - if warn { + if lenient { eprintln!("warning: {e}"); api } else { return Err(miette!( "{e:?}\n\nThe extracted API does not validate, so it would not \ generate. Fix the source (e.g. declare the referenced types), \ - or pass `--warn` to emit the IDL anyway." + or pass `--lenient` to emit the IDL anyway." )); } } }; let serialized = match format { - "yaml" | "yml" => serde_yaml::to_string(&api) + crate::IdlFormat::Yaml => serde_yaml::to_string(&api) .into_diagnostic() .wrap_err("failed to serialize API as YAML")?, - "json" => serde_json::to_string_pretty(&api) + crate::IdlFormat::Json => serde_json::to_string_pretty(&api) .into_diagnostic() .wrap_err("failed to serialize API as JSON")?, - "toml" => toml::to_string_pretty(&api) + crate::IdlFormat::Toml => toml::to_string_pretty(&api) .into_diagnostic() .wrap_err("failed to serialize API as TOML")?, - other => miette::bail!( - "unsupported output format: {} (expected yaml, json, or toml)", - other - ), }; match output { diff --git a/crates/weaveffi-cli/src/main.rs b/crates/weaveffi-cli/src/main.rs index bf76065d..29c35496 100644 --- a/crates/weaveffi-cli/src/main.rs +++ b/crates/weaveffi-cli/src/main.rs @@ -10,131 +10,161 @@ mod extract; mod report; use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; -use miette::{bail, IntoDiagnostic, Result, WrapErr}; -use tracing_subscriber::EnvFilter; -use weaveffi_ir::ir::CURRENT_SCHEMA_VERSION; +use miette::{IntoDiagnostic, Result, WrapErr}; +use weaveffi_model::ir::CURRENT_SCHEMA_VERSION; + +const INPUT_HELP: &str = "Annotated Rust source (.rs) or an IDL document (yaml|yml|json|toml); \ + defaults to `[project] input` from the nearest weaveffi.toml"; #[derive(Parser, Debug)] -#[command(name = "weaveffi", version, about = "WeaveFFI CLI")] +#[command( + name = "weaveffi", + version, + about = "Generate idiomatic bindings for 11 languages from one API definition over a stable C ABI" +)] struct Cli { - #[arg(long, global = true)] - quiet: bool, + /// Print only errors and warnings #[arg(long, short, global = true)] - verbose: bool, + quiet: bool, #[command(subcommand)] command: Commands, } -#[derive(Clone, Copy, Debug, ValueEnum)] -enum HumanJsonFormat { +/// Output format for `validate`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] +enum ReportFormat { + /// Human-readable text. Human, + /// One JSON object on stdout. Json, } -impl HumanJsonFormat { - const fn as_str(self) -> &'static str { - match self { - Self::Human => "human", - Self::Json => "json", - } - } +/// An IDL serialization format. +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] +pub(crate) enum IdlFormat { + /// YAML. + Yaml, + /// JSON. + Json, + /// TOML. + Toml, +} + +/// A schema export format. +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] +enum SchemaFormat { + /// JSON Schema (draft 7) for the IDL document. + JsonSchema, } #[derive(Subcommand, Debug)] enum Commands { + /// Create a weaveffi.toml (and a starter IDL outside a Rust crate) + Init { + /// Project directory + #[arg(default_value = ".")] + dir: String, + /// Package name for a new IDL project (default: the directory name) + #[arg(long)] + name: Option, + /// Overwrite an existing weaveffi.toml + #[arg(long)] + force: bool, + }, /// Generate bindings for every (or the selected) language target Generate { - /// 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")] - out: String, - /// Comma-separated list of targets to generate (c, cpp, swift, kotlin, node, wasm, python, dotnet, dart, go, ruby) + #[arg(help = INPUT_HELP)] + input: Option, + /// Output directory (default: `[project] out`, else ./generated) + #[arg(short, long)] + out: Option, + /// Comma-separated targets: c, cpp, swift, kotlin, node, wasm, python, dotnet, dart, go, ruby (default: `[project] targets`, else all) #[arg(short, long)] target: Option, - /// Path to weaveffi.toml (default: the nearest one at or above the input file) + /// Path to weaveffi.toml (default: the nearest one at or above the input) #[arg(long)] config: Option, - /// Print non-fatal warnings after validation + /// Print advisory lints after validation #[arg(long)] warn: bool, - /// Force regeneration, bypassing the incremental cache + /// Regenerate every target even if its inputs and files are unchanged #[arg(long)] force: bool, - /// Parse and validate only; print which files would be generated without writing them + /// Validate and list the files that would be written, without writing #[arg(long)] dry_run: bool, }, /// Validate an API definition without generating anything Validate { - /// Input: annotated Rust source (.rs) or an IDL document (yaml|yml|json|toml) - input: String, - /// Also report non-fatal warnings (advisory lints) + #[arg(help = INPUT_HELP)] + input: Option, + /// Path to weaveffi.toml (default: the nearest one at or above the input) + #[arg(long)] + config: Option, + /// Also report advisory lints #[arg(long)] warn: bool, - /// Output format: `json` for machine-readable output, otherwise human-readable + /// Output format + #[arg(long, value_enum, default_value_t = ReportFormat::Human)] + format: ReportFormat, + }, + /// Show how regenerating would change the output directory (writes nothing, runs no hooks) + Diff { + #[arg(help = INPUT_HELP)] + input: Option, + /// Output directory to compare against (default: `[project] out`, else ./generated) + #[arg(short, long)] + out: Option, + /// Comma-separated targets to compare (default: `[project] targets`, else all) + #[arg(short, long)] + target: Option, + /// Path to weaveffi.toml (default: the nearest one at or above the input) + #[arg(long)] + config: Option, + /// Print only a summary and exit 2 if files differ, 3 if files would be added or removed #[arg(long)] - format: Option, + check: bool, }, /// Assemble publishable packages that bundle prebuilt native libraries Package { - /// Input: annotated Rust source (.rs) or an IDL document (yaml|yml|json|toml) - input: String, + #[arg(help = INPUT_HELP)] + input: Option, /// Output directory for the packaged artifacts #[arg(short, long, default_value = "./dist")] out: String, - /// Comma-separated list of targets to package (e.g. node,python,dotnet) + /// Comma-separated targets to package (default: `[project] targets`, else all) #[arg(short, long)] target: Option, - /// Path to weaveffi.toml (default: the nearest one at or above the input file) + /// Path to weaveffi.toml (default: the nearest one at or above the input) #[arg(long)] config: Option, /// Directory of prebuilt native libraries laid out as `//` - /// (platform ids: darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, - /// android-arm64, android-x64, wasm32) #[arg(long)] binaries: Option, - /// Cargo package to cross-compile as the native producer (one cdylib per platform) + /// Cargo package to build as the native producer, once per platform #[arg(long)] build: Option, - /// Comma-separated platform ids to target (defaults to the full v1 matrix) + /// Comma-separated platform ids (default: the host): darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, android-arm64, android-x64, wasm32 #[arg(long)] platforms: Option, - /// Print non-fatal warnings after validation + /// Print advisory lints after validation #[arg(long)] warn: bool, }, /// Extract an IDL document from annotated Rust source Extract { - /// Path to a Rust source file to extract API definitions from + /// Rust source file to extract the API from input: String, - /// Output file path (defaults to stdout) + /// Output file (default: stdout) #[arg(short, long)] output: Option, - /// Output format: yaml (default), json, or toml - #[arg(short, long, default_value = "yaml")] - format: Option, - /// Downgrade validation errors to warnings and emit the IDL anyway. - /// Useful for bootstrapping from source that references types it does - /// not yet declare (e.g. an interface you will define later). - #[arg(long)] - warn: bool, - }, - /// Show how regenerating would change an existing output directory - Diff { - /// 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. + /// Output format + #[arg(short, long, value_enum, default_value_t = IdlFormat::Yaml)] + format: IdlFormat, + /// Emit the IDL even if it does not validate (for example, it + /// references types declared elsewhere) #[arg(long)] - check: bool, + lenient: bool, }, /// Print shell completions Completions { @@ -145,9 +175,9 @@ enum Commands { SchemaVersion, /// Print the IDL document schema Schema { - /// Schema export format (currently only json-schema is supported) - #[arg(long, default_value = "json-schema")] - format: String, + /// Schema export format + #[arg(long, value_enum, default_value_t = SchemaFormat::JsonSchema)] + format: SchemaFormat, }, } @@ -162,21 +192,16 @@ fn main() -> Result<()> { })); let cli = Cli::parse(); - - let filter = if cli.verbose { - EnvFilter::new("trace") - } else if cli.quiet { - EnvFilter::new("error") - } else { - EnvFilter::from_default_env() - }; - tracing_subscriber::fmt() - .with_env_filter(filter) - .without_time() - .init(); - let quiet = cli.quiet; match cli.command { + Commands::Init { dir, name, force } => { + commands::init::cmd_init(&commands::init::InitArgs { + dir: &dir, + name: name.as_deref(), + force, + quiet, + })? + } Commands::Generate { input, out, @@ -185,26 +210,42 @@ fn main() -> Result<()> { warn, force, dry_run, - } => commands::generate::cmd_generate( - &input, - &out, - target.as_deref(), - config.as_deref(), + } => commands::generate::cmd_generate(&commands::generate::GenerateArgs { + input: input.as_deref(), + out: out.as_deref(), + targets: target.as_deref(), + config: config.as_deref(), warn, force, dry_run, quiet, - )?, + })?, Commands::Validate { input, + config, warn, format, } => commands::validate::cmd_validate( - &input, + input.as_deref(), + config.as_deref(), warn, - format.map(HumanJsonFormat::as_str), + format == ReportFormat::Json, quiet, )?, + Commands::Diff { + input, + out, + target, + config, + check, + } => commands::diff::cmd_diff(&commands::diff::DiffArgs { + input: input.as_deref(), + out: out.as_deref(), + targets: target.as_deref(), + config: config.as_deref(), + check, + quiet, + })?, Commands::Package { input, out, @@ -214,38 +255,26 @@ fn main() -> Result<()> { build, platforms, warn, - } => commands::package::cmd_package( - &input, - &out, - target.as_deref(), - config.as_deref(), - binaries.as_deref(), - build.as_deref(), - platforms.as_deref(), + } => commands::package::cmd_package(&commands::package::PackageArgs { + input: input.as_deref(), + out: &out, + targets: target.as_deref(), + config: config.as_deref(), + binaries: binaries.as_deref(), + build: build.as_deref(), + platforms: platforms.as_deref(), warn, quiet, - )?, + })?, Commands::Extract { input, output, format, - warn, - } => extract::cmd_extract( - &input, - output.as_deref(), - format.as_deref().unwrap_or("yaml"), - warn, - quiet, - )?, - Commands::Diff { - input, - out, - config, - check, - } => commands::diff::cmd_diff(&input, out.as_deref(), config.as_deref(), check, quiet)?, + lenient, + } => extract::cmd_extract(&input, output.as_deref(), format, lenient, quiet)?, Commands::Completions { shell } => cmd_completions(shell), Commands::SchemaVersion => println!("{CURRENT_SCHEMA_VERSION}"), - Commands::Schema { format } => cmd_schema(&format)?, + Commands::Schema { format } => cmd_schema(format)?, } Ok(()) } @@ -259,21 +288,17 @@ fn cmd_completions(shell: clap_complete::Shell) { ); } -fn cmd_schema(format: &str) -> Result<()> { +fn cmd_schema(format: SchemaFormat) -> Result<()> { match format { - "json-schema" => { - let schema = schemars::schema_for!(weaveffi_ir::ir::Api); + SchemaFormat::JsonSchema => { + let schema = schemars::schema_for!(weaveffi_model::ir::Api); let json = serde_json::to_string_pretty(&schema) .into_diagnostic() .wrap_err("failed to serialize JSON Schema")?; println!("{json}"); - Ok(()) } - other => bail!( - "unsupported schema format: {} (expected 'json-schema')", - other - ), } + Ok(()) } #[cfg(test)] @@ -291,16 +316,6 @@ mod tests { ); } - #[test] - 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 completions_and_schema_version() { for (args, needle) in [ diff --git a/crates/weaveffi-cli/tests/cli_check.rs b/crates/weaveffi-cli/tests/cli/check.rs similarity index 97% rename from crates/weaveffi-cli/tests/cli_check.rs rename to crates/weaveffi-cli/tests/cli/check.rs index 1dd09f79..7952cbed 100644 --- a/crates/weaveffi-cli/tests/cli_check.rs +++ b/crates/weaveffi-cli/tests/cli/check.rs @@ -76,7 +76,7 @@ fn diff_check_fails_when_idl_changed() { write_file( &idl, concat!( - "version: \"0.9.0\"\n", + "version: \"0.10.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.9.0\"\n", + "version: \"0.10.0\"\n", "modules:\n", " - name: calc\n", " functions:\n", @@ -178,7 +178,7 @@ fn validate_json_format_outputs_object() { parsed["functions"].as_u64().unwrap() >= 1, "expected at least 1 function in calculator sample, got: {parsed}" ); - assert!(parsed.get("structs").is_some(), "missing 'structs' key"); + assert!(parsed.get("records").is_some(), "missing 'records' key"); assert!(parsed.get("enums").is_some(), "missing 'enums' key"); } @@ -189,7 +189,7 @@ fn validate_warn_json_format_outputs_warnings_array() { write_file( &path, concat!( - "version: \"0.9.0\"\n", + "version: \"0.10.0\"\n", "modules:\n", " - name: nodocs\n", " functions:\n", diff --git a/crates/weaveffi-cli/tests/cli/determinism.rs b/crates/weaveffi-cli/tests/cli/determinism.rs new file mode 100644 index 00000000..b5e9fc36 --- /dev/null +++ b/crates/weaveffi-cli/tests/cli/determinism.rs @@ -0,0 +1,32 @@ +//! Cross-generator determinism guard: rendering the kitchen-sink fixture +//! twice must produce byte-identical files from every target. Catches +//! non-deterministic iteration (e.g. `HashMap` walks) before it can flake the +//! snapshot suite. + +use std::path::Path; + +use camino::Utf8Path; +use weaveffi_model::parse::parse_api_str; +use weaveffi_model::resolved::ResolvedApi; +use weaveffi_model::validate::validate_api; + +fn load_kitchen_sink() -> ResolvedApi { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/kitchen_sink.yml"); + let contents = std::fs::read_to_string(&path).expect("read fixture"); + let api = parse_api_str(&contents, "yaml").expect("parse fixture"); + validate_api(api, None) + .expect("validate fixture") + .with_identity(weaveffi_model::pkg::Identity::named("kitchen_sink")) +} + +#[test] +fn generator_output_is_byte_identical_across_runs() { + let api = load_kitchen_sink(); + let out = Utf8Path::new("out"); + for target in weaveffi_gen::targets::all_default() { + let a = target.render(&api, out); + let b = target.render(&api, out); + assert!(!a.is_empty(), "{} produced no files", target.name()); + assert_eq!(a, b, "{} rendered differently across runs", target.name()); + } +} diff --git a/crates/weaveffi-cli/tests/cli_diagnostics.rs b/crates/weaveffi-cli/tests/cli/diagnostics.rs similarity index 97% rename from crates/weaveffi-cli/tests/cli_diagnostics.rs rename to crates/weaveffi-cli/tests/cli/diagnostics.rs index 947bdac9..8ad1175e 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.9.0\"\nmodules:\n - name: [bad\n"; + let yaml = "version: \"0.10.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.9.0" + let yaml = r#"version: "0.10.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.9.0" + let yaml = r#"version: "0.10.0" modules: - name: alpha functions: diff --git a/crates/weaveffi-cli/tests/cli_diff.rs b/crates/weaveffi-cli/tests/cli/diff.rs similarity index 100% rename from crates/weaveffi-cli/tests/cli_diff.rs rename to crates/weaveffi-cli/tests/cli/diff.rs diff --git a/crates/weaveffi-cli/tests/cli_errors.rs b/crates/weaveffi-cli/tests/cli/errors.rs similarity index 98% rename from crates/weaveffi-cli/tests/cli_errors.rs rename to crates/weaveffi-cli/tests/cli/errors.rs index f20e6db3..0b078cf7 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.9.0" +version: "0.10.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.9.0" +version: "0.10.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.9.0" +version: "0.10.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.9.0" +version: "0.10.0" modules: - name: dup functions: @@ -249,7 +249,7 @@ modules: fn validate_with_warnings() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.9.0" +version: "0.10.0" modules: - name: nodocs functions: diff --git a/crates/weaveffi-cli/tests/cli_extract.rs b/crates/weaveffi-cli/tests/cli/extract.rs similarity index 98% rename from crates/weaveffi-cli/tests/cli_extract.rs rename to crates/weaveffi-cli/tests/cli/extract.rs index 7d99cb81..f271bc10 100644 --- a/crates/weaveffi-cli/tests/cli_extract.rs +++ b/crates/weaveffi-cli/tests/cli/extract.rs @@ -1,5 +1,5 @@ //! End-to-end tests for `weaveffi extract`, which reads annotated Rust source -//! (via the shared `weaveffi-bridge`) and emits the IDL. The annotation scheme +//! (via the shared `weaveffi_model::rust`) and emits the IDL. The annotation scheme //! is the one the `#[weaveffi::module]` macro uses: a `#[weaveffi::module]` //! marks the exported namespace, and item markers (`#[weaveffi::export]`, //! `#[weaveffi::record]`, `#[weaveffi::enumeration]`, ...) tag the surface. @@ -312,8 +312,8 @@ mod sessions { ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("--warn"), - "the error should point users at --warn: {stderr}" + stderr.contains("--lenient"), + "the error should point users at --lenient: {stderr}" ); } @@ -526,13 +526,13 @@ mod legacy { assert_eq!(func["deprecated"].as_str().unwrap(), "Use add_v2 instead"); assert!( func.get("since").is_none(), - "schema 0.9.0 has no `since`; the version rides in the deprecation note only" + "schema 0.10.0 has no `since`; the version rides in the deprecation note only" ); } #[test] fn extract_rejects_mutable_reference_params() { - // Write-back parameters were removed with schema 0.9.0: every parameter + // Write-back parameters were removed with schema 0.10.0: every parameter // is borrowed or moved across the boundary, so `&mut T` has no lowering // and the extractor must refuse it with an actionable message. let (_dir, src_path) = write_src( diff --git a/crates/weaveffi-cli/tests/extract_roundtrip.rs b/crates/weaveffi-cli/tests/cli/extract_roundtrip.rs similarity index 99% rename from crates/weaveffi-cli/tests/extract_roundtrip.rs rename to crates/weaveffi-cli/tests/cli/extract_roundtrip.rs index b18505c5..c952c6e3 100644 --- a/crates/weaveffi-cli/tests/extract_roundtrip.rs +++ b/crates/weaveffi-cli/tests/cli/extract_roundtrip.rs @@ -11,8 +11,8 @@ use std::collections::BTreeMap; -use weaveffi_ir::ir::{Api, Function, InterfaceDef, Module, StructDef, TypeRef}; -use weaveffi_ir::parse::parse_api_str; +use weaveffi_model::ir::{Api, Function, InterfaceDef, Module, StructDef, TypeRef}; +use weaveffi_model::parse::parse_api_str; /// Compare user-type references by their final path segment so a /// module-qualified spelling (`shared.Token`) matches the bare one the diff --git a/crates/weaveffi-cli/tests/cli_formats.rs b/crates/weaveffi-cli/tests/cli/formats.rs similarity index 95% rename from crates/weaveffi-cli/tests/cli_formats.rs rename to crates/weaveffi-cli/tests/cli/formats.rs index 18e2fb71..744b3636 100644 --- a/crates/weaveffi-cli/tests/cli_formats.rs +++ b/crates/weaveffi-cli/tests/cli/formats.rs @@ -1,6 +1,6 @@ use std::path::Path; -use weaveffi_ir::ir::Api; +use weaveffi_model::ir::Api; fn repo_root() -> &'static Path { let manifest_dir = env!("CARGO_MANIFEST_DIR"); @@ -10,7 +10,7 @@ fn repo_root() -> &'static Path { fn load_calculator_api() -> Api { 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") + weaveffi_model::rust::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 similarity index 84% rename from crates/weaveffi-cli/tests/cli_generate.rs rename to crates/weaveffi-cli/tests/cli/generate.rs index a25d8e60..57d3bd2a 100644 --- a/crates/weaveffi-cli/tests/cli_generate.rs +++ b/crates/weaveffi-cli/tests/cli/generate.rs @@ -21,26 +21,22 @@ fn generate_produces_expected_files() { .assert() .success(); - assert!( - out_path.join("c/weaveffi.h").exists(), - "missing c/weaveffi.h" - ); - assert!( - out_path.join("swift/Package.swift").exists(), - "missing swift/Package.swift" - ); - assert!( - out_path.join("kotlin/build.gradle.kts").exists(), - "missing kotlin/build.gradle.kts" - ); - assert!( - out_path.join("node/types.d.ts").exists(), - "missing node/types.d.ts" - ); - assert!( - out_path.join("wasm/README.md").exists(), - "missing wasm/README.md" - ); + for file in [ + "c/calculator.h", + "cpp/calculator.hpp", + "swift/Package.swift", + "swift/Sources/Calculator/Calculator.swift", + "kotlin/build.gradle.kts", + "node/index.d.ts", + "wasm/index.js", + "python/calculator/calculator.py", + "dotnet/Calculator.cs", + "dart/lib/calculator.dart", + "go/go.mod", + "ruby/lib/calculator.rb", + ] { + assert!(out_path.join(file).exists(), "missing {file}"); + } } #[test] @@ -66,8 +62,8 @@ fn generate_with_target_filter() { .success(); assert!( - out_path.join("c/weaveffi.h").exists(), - "missing c/weaveffi.h" + out_path.join("c/calculator.h").exists(), + "missing c/calculator.h" ); assert!( !out_path.join("swift").exists(), @@ -144,7 +140,7 @@ fn quiet_flag_suppresses_output() { .stdout(predicate::str::is_empty()); assert!( - out_path.join("c/weaveffi.h").exists(), + out_path.join("c/calculator.h").exists(), "files should still be generated with --quiet" ); } diff --git a/crates/weaveffi-cli/tests/cli/main.rs b/crates/weaveffi-cli/tests/cli/main.rs new file mode 100644 index 00000000..13057578 --- /dev/null +++ b/crates/weaveffi-cli/tests/cli/main.rs @@ -0,0 +1,17 @@ +//! Integration tests for the `weaveffi` binary and the CLI-facing pipeline, +//! compiled as one test binary (one link step instead of one per file). The +//! per-generator snapshot corpus lives separately in `tests/snapshots.rs`. + +mod check; +mod determinism; +mod diagnostics; +mod diff; +mod errors; +mod extract; +mod extract_roundtrip; +mod formats; +mod generate; +mod no_silent_stubs; +mod package; +mod project_config; +mod schema; diff --git a/crates/weaveffi-cli/tests/no_silent_stubs.rs b/crates/weaveffi-cli/tests/cli/no_silent_stubs.rs similarity index 90% rename from crates/weaveffi-cli/tests/no_silent_stubs.rs rename to crates/weaveffi-cli/tests/cli/no_silent_stubs.rs index bab13228..1a04eca2 100644 --- a/crates/weaveffi-cli/tests/no_silent_stubs.rs +++ b/crates/weaveffi-cli/tests/cli/no_silent_stubs.rs @@ -5,7 +5,7 @@ //! paths for whole call shapes, features skipped without a word. These tests //! make that class of regression a build failure: //! -//! 1. Generator crate sources must not contain `unimplemented!(` / `todo!(`. +//! 1. Generator and model crate sources must not contain `unimplemented!(` / `todo!(`. //! (`unreachable!` stays allowed: it documents genuinely impossible states.) //! 2. The full generated output for the feature-complete sample IDLs must not //! contain stub markers. A target that cannot support a feature must either @@ -43,10 +43,10 @@ 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(); - // 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") { + // Only the generator and model crates (the code that produces + // consumer bindings) are in scope; test helpers and the CLI are free + // to use placeholder macros. + if !(name == "weaveffi-gen" || name == "weaveffi-model") { continue; } let src = path.join("src"); @@ -56,6 +56,11 @@ fn generator_sources_ban_unimplemented_and_todo() { if file.extension().is_none_or(|e| e != "rs") { continue; } + // The Rust extractor's tests parse annotated sample sources whose + // bodies are `todo!()`; that's fixture text, not a stub. + if name == "weaveffi-model" && file.ends_with("src/rust.rs") { + continue; + } let text = fs::read_to_string(&file).expect("read source file"); scanned += 1; for banned in ["unimplemented!(", "todo!("] { @@ -85,7 +90,7 @@ fn generator_sources_ban_unimplemented_and_todo() { fn is_abstract_callback_default(line: &str) -> bool { let t = line.trim_start(); t.starts_with("raise NotImplementedError, \"#{self.class}#") - || t.starts_with("# include this module to inherit NotImplementedError defaults") + || (t.starts_with('#') && t.contains("NotImplementedError")) } /// Every file generated for the feature-complete samples (structs, builders, diff --git a/crates/weaveffi-cli/tests/cli_package.rs b/crates/weaveffi-cli/tests/cli/package.rs similarity index 100% rename from crates/weaveffi-cli/tests/cli_package.rs rename to crates/weaveffi-cli/tests/cli/package.rs diff --git a/crates/weaveffi-cli/tests/cli_project_config.rs b/crates/weaveffi-cli/tests/cli/project_config.rs similarity index 77% rename from crates/weaveffi-cli/tests/cli_project_config.rs rename to crates/weaveffi-cli/tests/cli/project_config.rs index 1068583a..ef03d134 100644 --- a/crates/weaveffi-cli/tests/cli_project_config.rs +++ b/crates/weaveffi-cli/tests/cli/project_config.rs @@ -1,7 +1,7 @@ //! `weaveffi.toml` behavior end to end: automatic discovery next to (or //! above) the input, `--config` overriding discovery, `[package]` identity //! reaching every manifest, `[generators.]` options reaching their -//! backend, and `[global] c_prefix` reaching all eleven targets. +//! backend, and the identity's C prefix reaching all eleven targets. use std::fs; use std::path::{Path, PathBuf}; @@ -72,7 +72,7 @@ fn discovers_weaveffi_toml_above_the_input() { ); } - assert!(read(&out.join("cpp/weaveffi.hpp")).contains("namespace kvstore")); + assert!(read(&out.join("cpp/kvstore.hpp")).contains("namespace kvstore")); assert!(read(&out.join("dotnet/Kvstore.cs")).contains("namespace Kvstore")); assert!(out.join("swift/Sources/Kvstore/Kvstore.swift").is_file()); assert!(read(&out.join("dart/pubspec.yaml")).contains("name: kvstore")); @@ -122,7 +122,7 @@ fn explicit_config_overrides_discovery() { &["--target", "cpp,node", "--config", cfg.to_str().unwrap()], ); - assert!(read(&out.join("cpp/weaveffi.hpp")).contains("namespace elsewhere")); + assert!(read(&out.join("cpp/kvstore.hpp")).contains("namespace elsewhere")); let package_json = read(&out.join("node/package.json")); assert!( package_json.contains("\"name\": \"renamed\""), @@ -162,7 +162,7 @@ fn inline_package_block_in_idl_is_an_error() { fs::write( &idl, concat!( - "version: \"0.9.0\"\n", + "version: \"0.10.0\"\n", "package:\n", " name: legacy\n", "modules:\n", @@ -229,66 +229,69 @@ fn unknown_target_is_an_error() { 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. +/// The C symbol prefix is the library's identity: an IDL's `[package] +/// c_prefix` must reach every target, and nothing any target emits may call a +/// `weaveffi_`-prefixed symbol (two WeaveFFI libraries in one process would +/// otherwise collide). #[test] -fn global_c_prefix_propagates_to_all_targets() { - let input = repo_root().join("samples/calculator/src/lib.rs"); +fn idl_c_prefix_reaches_every_target() { let dir = tempfile::tempdir().unwrap(); + let idl = dir.path().join("calc.yml"); + fs::write( + &idl, + concat!( + "version: \"0.10.0\"\n", + "modules:\n", + " - name: calculator\n", + " functions:\n", + " - { name: add, params: [{ name: a, type: i32 }, { name: b, type: i32 }], return: i32 }\n", + " - { name: echo, params: [{ name: s, type: string }], return: string }\n", + ), + ) + .unwrap(); + fs::write( + dir.path().join("weaveffi.toml"), + "[package]\nname = \"calc\"\nc_prefix = \"myffi\"\n", + ) + .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()]); + generate(&idl, &out, &[]); 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" + "target `{target}` did not use the identity's C prefix" ); assert!( - !tree.contains("weaveffi_calculator_add"), - "target `{target}` leaked the default-prefixed user symbol" + !tree.contains("weaveffi_"), + "target `{target}` references a `weaveffi_` symbol" ); } } -/// `[generators.c] prefix` alone also fans out (to C++ at least), and the C -/// header aliases the runtime symbols to the custom prefix. +/// A Rust producer's prefix is its crate name, so `[package] c_prefix` is +/// rejected for a `.rs` input instead of generating bindings that call +/// symbols the producer never exports. #[test] -fn c_prefix_from_the_c_table_reaches_cpp() { +fn rust_producer_rejects_c_prefix() { 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_error_set weaveffi_error_set", - "#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")); + fs::write(&cfg, "[package]\nc_prefix = \"myffi\"\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("c_prefix"), "{stderr}"); } diff --git a/crates/weaveffi-cli/tests/cli_schema.rs b/crates/weaveffi-cli/tests/cli/schema.rs similarity index 100% rename from crates/weaveffi-cli/tests/cli_schema.rs rename to crates/weaveffi-cli/tests/cli/schema.rs diff --git a/crates/weaveffi-cli/tests/cli_schema_version.rs b/crates/weaveffi-cli/tests/cli_schema_version.rs deleted file mode 100644 index 7f120257..00000000 --- a/crates/weaveffi-cli/tests/cli_schema_version.rs +++ /dev/null @@ -1,15 +0,0 @@ -fn cargo_bin() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") -} - -#[test] -fn schema_version_command_prints_0_4_0() { - let output = cargo_bin().arg("schema-version").output().unwrap(); - assert!( - output.status.success(), - "schema-version failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - let stdout = String::from_utf8_lossy(&output.stdout); - assert_eq!(stdout.trim(), "0.9.0"); -} diff --git a/crates/weaveffi-cli/tests/determinism.rs b/crates/weaveffi-cli/tests/determinism.rs deleted file mode 100644 index 66477831..00000000 --- a/crates/weaveffi-cli/tests/determinism.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Cross-generator determinism guard. -//! -//! Runs every generator on the kitchen-sink fixture twice into two separate -//! tempdirs and asserts every emitted file is byte-identical between runs. -//! Catches non-deterministic iteration (e.g. `HashMap` walks) before it can -//! flake the snapshot suite. - -use std::fs; -use std::path::{Path, PathBuf}; - -use camino::Utf8Path; -use weaveffi_core::codegen::{ConfiguredBackend, Target}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::validate::validate_api; -use weaveffi_gen_c::{CConfig, CGenerator}; -use weaveffi_gen_cpp::{CppConfig, CppGenerator}; -use weaveffi_gen_dart::{DartConfig, DartGenerator}; -use weaveffi_gen_dotnet::{DotnetConfig, DotnetGenerator}; -use weaveffi_gen_go::{GoConfig, GoGenerator}; -use weaveffi_gen_kotlin::{KotlinConfig, KotlinGenerator}; -use weaveffi_gen_node::{NodeConfig, NodeGenerator}; -use weaveffi_gen_python::{PythonConfig, PythonGenerator}; -use weaveffi_gen_ruby::{RubyConfig, RubyGenerator}; -use weaveffi_gen_swift::{SwiftConfig, SwiftGenerator}; -use weaveffi_gen_wasm::{WasmConfig, WasmGenerator}; -use weaveffi_ir::parse::parse_api_str; - -const KITCHEN_SINK: &str = "kitchen_sink.yml"; - -fn load_kitchen_sink() -> ResolvedApi { - let path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests/fixtures") - .join(KITCHEN_SINK); - let contents = fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display())); - let api = parse_api_str(&contents, "yaml") - .unwrap_or_else(|e| panic!("parse fixture {}: {e}", path.display())); - validate_api(api, None).unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) -} - -fn collect_files_sorted(root: &Path) -> Vec { - 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 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"); - let gen_root = out_dir.join(gen.name()).into_std_path_buf(); - (tmp, gen_root) -} - -fn assert_byte_identical(gen: &dyn Target) { - let api = load_kitchen_sink(); - - let (_tmp_a, root_a) = run_into_tempdir(gen, &api); - let (_tmp_b, root_b) = run_into_tempdir(gen, &api); - - let files_a = collect_files_sorted(&root_a); - let files_b = collect_files_sorted(&root_b); - - assert!( - !files_a.is_empty(), - "generator {} produced no files for kitchen-sink fixture", - gen.name() - ); - - let rels_a: Vec<_> = files_a - .iter() - .map(|p| p.strip_prefix(&root_a).expect("under root").to_path_buf()) - .collect(); - let rels_b: Vec<_> = files_b - .iter() - .map(|p| p.strip_prefix(&root_b).expect("under root").to_path_buf()) - .collect(); - - assert_eq!( - rels_a, - rels_b, - "generator {} produced different file sets between runs", - gen.name() - ); - - for (a, b) in files_a.iter().zip(files_b.iter()) { - let bytes_a = fs::read(a).unwrap_or_else(|e| panic!("read {}: {e}", a.display())); - let bytes_b = fs::read(b).unwrap_or_else(|e| panic!("read {}: {e}", b.display())); - assert_eq!( - bytes_a, - bytes_b, - "generator {} produced non-deterministic output for {}", - gen.name(), - a.strip_prefix(&root_a).unwrap().display() - ); - } -} - -#[test] -fn generator_output_is_byte_identical_across_runs() { - 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(ConfiguredBackend::new( - KotlinGenerator, - KotlinConfig::default(), - )), - Box::new(ConfiguredBackend::new(NodeGenerator, NodeConfig::default())), - Box::new(ConfiguredBackend::new(WasmGenerator, WasmConfig::default())), - Box::new(ConfiguredBackend::new( - PythonGenerator, - PythonConfig::default(), - )), - Box::new(ConfiguredBackend::new( - DotnetGenerator, - DotnetConfig::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 deleted file mode 100644 index cfa50ca9..00000000 --- a/crates/weaveffi-cli/tests/docs_examples.rs +++ /dev/null @@ -1,897 +0,0 @@ -const GENERATOR_DOCS_YAML: &str = r#" -version: "0.9.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: name, type: string } - - { name: email, type: "string?" } - - { name: age, type: i32 } - - functions: - - name: create_contact - params: - - { name: first_name, type: string } - - { name: last_name, type: string } - return: Contact - - - name: find_contact - params: - - { name: id, type: "i32?" } - return: "Contact?" - - - name: list_contacts - params: [] - return: "[Contact]" - - - name: count_contacts - params: [] - return: i32 -"#; - -fn generate_all_for_docs() -> (tempfile::TempDir, std::path::PathBuf) { - let out_dir = tempfile::tempdir().expect("failed to create temp dir"); - let out_path = out_dir.path().to_path_buf(); - let yml_path = out_path.join("weaveffi.yml"); - std::fs::write(&yml_path, GENERATOR_DOCS_YAML).unwrap(); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - yml_path.to_str().unwrap(), - "-o", - out_path.join("generated").to_str().unwrap(), - ]) - .assert() - .success(); - - (out_dir, out_path) -} - -#[test] -fn doc_swift_contains_enum_declaration() { - let (_dir, out) = generate_all_for_docs(); - let swift = - std::fs::read_to_string(out.join("generated/swift/Sources/WeaveFFI/WeaveFFI.swift")) - .unwrap(); - - assert!( - swift.contains("public enum ContactType: UInt32 {"), - "Swift enum declaration missing: {swift}" - ); - assert!( - swift.contains("case personal = 0"), - "Swift enum variant lowerCamelCase missing: {swift}" - ); -} - -#[test] -fn doc_swift_contains_record_struct() { - let (_dir, out) = generate_all_for_docs(); - let swift = - std::fs::read_to_string(out.join("generated/swift/Sources/WeaveFFI/WeaveFFI.swift")) - .unwrap(); - - assert!( - swift.contains("public struct Contact {"), - "Swift record struct missing: {swift}" - ); - assert!( - swift.contains("public var name: String"), - "name stored property missing: {swift}" - ); - assert!( - swift.contains("public var email: String?"), - "optional stored property missing: {swift}" - ); - assert!( - swift.contains("public var age: Int32"), - "age stored property missing: {swift}" - ); - assert!( - swift.contains("public init(name: String, email: String?, age: Int32)"), - "memberwise init missing: {swift}" - ); -} - -#[test] -fn doc_swift_optional_and_list_returns() { - let (_dir, out) = generate_all_for_docs(); - let swift = - std::fs::read_to_string(out.join("generated/swift/Sources/WeaveFFI/WeaveFFI.swift")) - .unwrap(); - - assert!( - swift.contains("-> Contact? {"), - "optional return missing: {swift}" - ); - assert!( - swift.contains("-> [Contact] {"), - "list return missing: {swift}" - ); -} - -#[test] -fn doc_c_header_value_records_and_enum() { - let (_dir, out) = generate_all_for_docs(); - let header = std::fs::read_to_string(out.join("generated/c/weaveffi.h")).unwrap(); - - // Records are value types serialized into buffers: no opaque typedef, - // create/destroy pair, or getters. A record return is an owned buffer - // handed back as `const uint8_t*` plus `size_t* out_len`. - assert!( - !header.contains("typedef struct weaveffi_contacts_Contact"), - "records must not get opaque typedefs: {header}" - ); - assert!( - header.contains("weaveffi_contacts_ContactType_Personal = 0"), - "enum variant missing: {header}" - ); - assert!( - header.contains( - "const uint8_t* weaveffi_contacts_create_contact(const char* first_name, \ - const char* last_name, size_t* out_len, weaveffi_error* out_err);" - ), - "buffered record return prototype missing: {header}" - ); - assert!( - header.contains("const uint8_t* id_ptr, size_t id_len"), - "buffered optional param slots missing: {header}" - ); -} - -#[test] -fn doc_c_header_naming_convention() { - let (_dir, out) = generate_all_for_docs(); - let header = std::fs::read_to_string(out.join("generated/c/weaveffi.h")).unwrap(); - - assert!( - header.contains("weaveffi_contacts_create_contact("), - "function naming convention wrong: {header}" - ); - assert!( - header.contains("weaveffi_contacts_list_contacts("), - "list_contacts missing: {header}" - ); - assert!( - header.contains("weaveffi_contacts_count_contacts("), - "count_contacts missing: {header}" - ); -} - -#[test] -fn doc_node_dts_interface_and_enum() { - let (_dir, out) = generate_all_for_docs(); - let dts = std::fs::read_to_string(out.join("generated/node/types.d.ts")).unwrap(); - - assert!( - dts.contains("export interface Contact {"), - "TS interface missing: {dts}" - ); - assert!(dts.contains(" name: string;"), "name field missing: {dts}"); - assert!( - dts.contains(" email: string | null;"), - "optional field missing: {dts}" - ); - assert!( - dts.contains("export enum ContactType {"), - "TS enum missing: {dts}" - ); - assert!( - dts.contains(" Personal = 0,"), - "enum variant missing: {dts}" - ); -} - -#[test] -fn doc_node_dts_optional_and_list_return() { - let (_dir, out) = generate_all_for_docs(); - let dts = std::fs::read_to_string(out.join("generated/node/types.d.ts")).unwrap(); - - assert!( - dts.contains("Contact | null"), - "optional return missing: {dts}" - ); - assert!(dts.contains("Contact[]"), "list return missing: {dts}"); -} - -#[test] -fn doc_kotlin_wrapper() { - let (_dir, out) = generate_all_for_docs(); - let kt = std::fs::read_to_string( - out.join("generated/kotlin/src/main/kotlin/com/weaveffi/WeaveFFI.kt"), - ) - .unwrap(); - - assert!( - kt.contains("System.loadLibrary(\"weaveffi\")"), - "loadLibrary missing: {kt}" - ); - assert!( - kt.contains("@JvmStatic external fun"), - "external fun missing: {kt}" - ); - assert!( - kt.contains("enum class ContactType(val value: Int) {"), - "enum class missing: {kt}" - ); - assert!( - kt.contains("data class Contact(val name: String, val email: String?, val age: Int)"), - "record data class missing: {kt}" - ); -} - -#[test] -fn doc_kotlin_jni_shim() { - let (_dir, out) = generate_all_for_docs(); - let jni = - std::fs::read_to_string(out.join("generated/kotlin/src/main/cpp/weaveffi_jni.c")).unwrap(); - - assert!( - jni.contains("#include \"weaveffi.h\""), - "missing weaveffi.h include: {jni}" - ); - assert!( - jni.contains("JNIEXPORT"), - "missing JNIEXPORT declarations: {jni}" - ); - assert!( - jni.contains("weaveffi_error err = {0, NULL, NULL, 0};"), - "missing four-field error init: {jni}" - ); -} - -#[test] -fn doc_kotlin_cmake_exists() { - let (_dir, out) = generate_all_for_docs(); - let cmake = - std::fs::read_to_string(out.join("generated/kotlin/src/main/cpp/CMakeLists.txt")).unwrap(); - - assert!(cmake.contains("add_library(weaveffi SHARED weaveffi_jni.c)")); - assert!(cmake.contains("target_include_directories(weaveffi PRIVATE ../../../../c)")); -} - -#[test] -fn doc_wasm_loader_generated() { - let (_dir, out) = generate_all_for_docs(); - let js = std::fs::read_to_string(out.join("generated/wasm/weaveffi_wasm.js")).unwrap(); - - assert!( - js.contains("export async function loadWeaveffiWasm(source)"), - "loader function missing: {js}" - ); - assert!( - js.contains("WebAssembly.instantiate"), - "instantiate call missing: {js}" - ); -} - -#[test] -fn doc_wasm_readme_type_conventions() { - let (_dir, out) = generate_all_for_docs(); - let readme = std::fs::read_to_string(out.join("generated/wasm/README.md")).unwrap(); - - assert!(readme.contains("### Structs"), "structs section missing"); - assert!(readme.contains("### Enums"), "enums section missing"); - assert!( - readme.contains("### Optionals"), - "optionals section missing" - ); - assert!(readme.contains("### Lists"), "lists section missing"); -} - -#[test] -fn summary_md_all_links_resolve() { - let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap(); - let docs_src = workspace_root.join("docs/src"); - let summary = std::fs::read_to_string(docs_src.join("SUMMARY.md")) - .expect("docs/src/SUMMARY.md must exist"); - - let mut missing = Vec::new(); - for line in summary.lines() { - // Extract path from markdown links like [Title](path.md) - if let Some(start) = line.find("](") { - let rest = &line[start + 2..]; - if let Some(end) = rest.find(')') { - let target = &rest[..end]; - if target.is_empty() || !target.ends_with(".md") { - continue; - } - let full = docs_src.join(target); - if !full.exists() { - missing.push(target.to_string()); - } - } - } - } - - assert!( - missing.is_empty(), - "SUMMARY.md references missing files: {missing:?}" - ); -} - -/// 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.9.0" -modules: - - name: kv - errors: - name: KvError - codes: - - { name: KeyNotFound, code: 1001, message: "key not found" } - - { name: StoreFull, code: 1003, message: "store has reached capacity" } - interfaces: - - name: 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 - params: [] - return: i64 -"#; - -#[test] -fn readme_quickstart_yaml_parses_and_validates() { - let api = weaveffi_ir::parse::parse_api_str(README_QUICKSTART_YAML, "yaml") - .expect("README quickstart YAML should parse"); - let api = weaveffi_core::validate::validate_api(api, None) - .expect("README quickstart YAML should validate"); - - assert_eq!(api.modules.len(), 1); - let m = &api.modules[0]; - assert_eq!(m.name, "kv"); - assert_eq!(m.interfaces.len(), 1); - let store = &m.interfaces[0]; - assert_eq!(store.name, "Store"); - assert_eq!(store.constructors.len(), 1); - assert_eq!(store.constructors[0].name, "open"); - assert!(store.constructors[0].throws); - assert_eq!(store.methods.len(), 2); - assert!(store.methods[0].throws); - assert!(!store.methods[1].throws); - let domain = m.errors.as_ref().expect("KvError domain"); - assert_eq!(domain.name, "KvError"); - assert_eq!(domain.codes.len(), 2); -} - -/// The README quickstart YAML must match the snippet in `README.md` verbatim -/// so the docs cannot drift from what this suite verifies. -#[test] -fn readme_quickstart_yaml_matches_readme_snippet() { - let readme = read_workspace_file("README.md"); - assert!( - readme.contains(README_QUICKSTART_YAML.trim()), - "README.md quickstart YAML differs from README_QUICKSTART_YAML in this test" - ); -} - -#[test] -fn readme_quickstart_generates_c_header() { - let out_dir = tempfile::tempdir().expect("failed to create temp dir"); - let out_path = out_dir.path(); - let yml_path = out_path.join("kvstore.yml"); - std::fs::write(&yml_path, README_QUICKSTART_YAML).unwrap(); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - yml_path.to_str().unwrap(), - "-o", - out_path.join("generated").to_str().unwrap(), - ]) - .assert() - .success(); - - let header = std::fs::read_to_string(out_path.join("generated/c/weaveffi.h")).unwrap(); - - assert!( - header.contains("typedef struct weaveffi_kv_Store weaveffi_kv_Store;"), - "opaque interface typedef missing: {header}" - ); - assert!( - header.contains("weaveffi_kv_KvError_KeyNotFound = 1001"), - "error domain enum missing: {header}" - ); - assert!( - header.contains("weaveffi_kv_Store_open("), - "Store_open constructor missing: {header}" - ); - assert!( - header.contains("weaveffi_kv_Store_put(const weaveffi_kv_Store* self,"), - "Store_put method with self receiver missing: {header}" - ); - assert!( - header.contains("weaveffi_kv_Store_count("), - "Store_count missing: {header}" - ); - assert!( - header.contains("void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self);"), - "Store_destroy missing: {header}" - ); -} - -const GETTING_STARTED_YAML: &str = r#" -version: "0.9.0" -modules: - - name: math - structs: - - name: Point - fields: - - { name: x, type: f64 } - - { name: y, type: f64 } - functions: - - name: add - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 -"#; - -#[test] -fn getting_started_yaml_parses_and_validates() { - let api = weaveffi_ir::parse::parse_api_str(GETTING_STARTED_YAML, "yaml") - .expect("getting-started YAML should parse"); - let api = weaveffi_core::validate::validate_api(api, None) - .expect("getting-started YAML should validate"); - - assert_eq!(api.modules.len(), 1); - assert_eq!(api.modules[0].name, "math"); - assert_eq!(api.modules[0].structs.len(), 1); - assert_eq!(api.modules[0].structs[0].name, "Point"); - assert_eq!(api.modules[0].structs[0].fields.len(), 2); - assert_eq!(api.modules[0].functions.len(), 1); - assert_eq!(api.modules[0].functions[0].name, "add"); -} - -#[test] -fn getting_started_yaml_generates_all_targets() { - let out_dir = tempfile::tempdir().expect("failed to create temp dir"); - let out_path = out_dir.path(); - - let yml_path = out_path.join("weaveffi.yml"); - std::fs::write(&yml_path, GETTING_STARTED_YAML).unwrap(); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - yml_path.to_str().unwrap(), - "-o", - out_path.join("generated").to_str().unwrap(), - ]) - .assert() - .success(); - - let gen = out_path.join("generated"); - assert!(gen.join("c/weaveffi.h").exists(), "missing c/weaveffi.h"); - assert!( - gen.join("swift/Package.swift").exists(), - "missing swift/Package.swift" - ); - assert!( - gen.join("node/types.d.ts").exists(), - "missing node/types.d.ts" - ); - - let header = std::fs::read_to_string(gen.join("c/weaveffi.h")).unwrap(); - assert!( - header.contains("weaveffi_math_add"), - "C header should contain weaveffi_math_add" - ); - // Records are value types: the header carries no per-record C surface. - assert!( - !header.contains("weaveffi_math_Point"), - "C header should not declare a record surface" - ); - - let dts = std::fs::read_to_string(gen.join("node/types.d.ts")).unwrap(); - assert!( - dts.contains("interface Point"), - "types.d.ts should contain Point interface" - ); - assert!( - dts.contains("function add(a: number, b: number): number"), - "types.d.ts should contain the prefix-stripped add function" - ); -} - -#[test] -fn getting_started_yaml_validates_via_cli() { - let out_dir = tempfile::tempdir().expect("failed to create temp dir"); - let yml_path = out_dir.path().join("weaveffi.yml"); - std::fs::write(&yml_path, GETTING_STARTED_YAML).unwrap(); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["validate", yml_path.to_str().unwrap()]) - .assert() - .success() - .stdout(predicates::str::contains("Validation passed")); -} - -fn workspace_root() -> std::path::PathBuf { - std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf() -} - -fn read_workspace_file(rel: &str) -> String { - let path = workspace_root().join(rel); - let contents = std::fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); - // Windows checkouts may carry CRLF while Rust normalizes multiline string - // literals to LF; normalize so verbatim-snippet comparisons match. - contents.replace("\r\n", "\n") -} - -#[test] -fn readme_states_value_proposition() { - let readme = read_workspace_file("README.md"); - assert!( - readme.contains( - "WeaveFFI generates type-safe bindings for 11 languages for any native library" - ), - "README missing the canonical value proposition" - ); - assert!( - readme.contains("no hand-written JNI"), - "README missing 'no hand-written JNI' phrase" - ); -} - -#[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("version: \"0.9.0\""), - "README should reference schema version 0.9.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(&format!("version: \"{old}\"")), - "README should not reference older schema version {old}" - ); - } -} - -#[test] -fn readme_lists_all_eleven_targets_in_table() { - let readme = read_workspace_file("README.md"); - for target in [ - "**C**", - "**C++**", - "**Swift**", - "**Kotlin**", - "**Node.js**", - "**Wasm**", - "**Python**", - "**.NET**", - "**Dart**", - "**Go**", - "**Ruby**", - ] { - assert!( - readme.contains(target), - "README supported-targets table missing {target}" - ); - } -} - -#[test] -fn readme_cli_reference_lists_every_subcommand() { - let readme = read_workspace_file("README.md"); - for cmd in [ - "weaveffi generate", - "weaveffi package", - "weaveffi validate", - "weaveffi diff", - "weaveffi extract", - "weaveffi schema ", - "weaveffi schema-version", - "weaveffi completions", - ] { - assert!( - readme.contains(cmd), - "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] -fn readme_has_required_top_level_sections() { - let readme = read_workspace_file("README.md"); - for section in [ - "## Quickstart", - "## Why WeaveFFI?", - "## How does it compare?", - "## Supported targets", - "## Install", - "## CLI reference", - "## Documentation", - "## Status", - "## Contributing", - "## License", - ] { - assert!( - readme.contains(section), - "README missing top-level section header `{section}`" - ); - } -} - -#[test] -fn readme_status_describes_pre_1_0_development() { - let readme = read_workspace_file("README.md"); - assert!( - readme.contains("0.x"), - "README status section should describe the project as in active 0.x development" - ); - assert!( - !readme.contains("release candidate"), - "README status section should not claim to be a release candidate" - ); -} - -#[test] -fn readme_install_section_documents_cargo_and_releases() { - let readme = read_workspace_file("README.md"); - assert!( - readme.contains("cargo install weaveffi-cli"), - "README install section missing cargo install command" - ); - assert!( - readme.contains("github.com/weavefoundry/weaveffi/releases"), - "README install section missing pre-built binaries / releases link" - ); -} - -#[test] -fn readme_comparison_link_points_to_docs() { - let readme = read_workspace_file("README.md"); - assert!( - readme.contains("docs/src/comparison.md"), - "README should link to docs/src/comparison.md from the comparison section" - ); -} - -#[test] -fn readme_quickstart_command_emits_all_advertised_targets() { - let out_dir = tempfile::tempdir().expect("failed to create temp dir"); - let out_path = out_dir.path(); - let yml_path = out_path.join("kvstore.yml"); - std::fs::write(&yml_path, README_QUICKSTART_YAML).unwrap(); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - yml_path.to_str().unwrap(), - "-o", - out_path.join("generated").to_str().unwrap(), - "--target", - "c,swift,python,node,dart", - ]) - .assert() - .success(); - - let gen = out_path.join("generated"); - assert!(gen.join("c/weaveffi.h").exists(), "missing c/weaveffi.h"); - assert!( - gen.join("swift/Sources/WeaveFFI/WeaveFFI.swift").exists(), - "missing swift/Sources/WeaveFFI/WeaveFFI.swift" - ); - assert!( - gen.join("python/kvstore/weaveffi.pyi").exists(), - "missing python/kvstore/weaveffi.pyi" - ); - assert!( - gen.join("node/types.d.ts").exists(), - "missing node/types.d.ts" - ); - assert!( - gen.join("dart/lib/weaveffi.dart").exists(), - "missing dart/lib/weaveffi.dart" - ); -} - -#[test] -fn readme_quickstart_snippets_match_actual_output() { - let readme = read_workspace_file("README.md"); - let out_dir = tempfile::tempdir().expect("failed to create temp dir"); - let out_path = out_dir.path(); - let yml_path = out_path.join("kvstore.yml"); - std::fs::write(&yml_path, README_QUICKSTART_YAML).unwrap(); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - yml_path.to_str().unwrap(), - "-o", - out_path.join("generated").to_str().unwrap(), - "--target", - "c,swift,python", - ]) - .assert() - .success(); - - let gen = out_path.join("generated"); - - // Each line below appears in BOTH the README snippet and the freshly - // generated output, proving the README is in sync with the CLI. - let c_header = std::fs::read_to_string(gen.join("c/weaveffi.h")).unwrap(); - for needle in [ - "typedef struct weaveffi_kv_Store weaveffi_kv_Store;", - "weaveffi_kv_KvError_KeyNotFound = 1001", - "void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self);", - ] { - assert!( - readme.contains(needle), - "README C snippet missing line: {needle}" - ); - assert!( - c_header.contains(needle), - "Generated C header missing line: {needle}" - ); - } - - let swift = std::fs::read_to_string(gen.join("swift/Sources/WeaveFFI/WeaveFFI.swift")).unwrap(); - for needle in [ - "public enum KvError: Error, LocalizedError {", - "public final class Store {", - "public static func open(path: String) throws -> Store {", - ] { - assert!( - readme.contains(needle), - "README Swift snippet missing line: {needle}" - ); - assert!( - swift.contains(needle), - "Generated Swift file missing line: {needle}" - ); - } - - let pyi = std::fs::read_to_string(gen.join("python/kvstore/weaveffi.pyi")).unwrap(); - for needle in ["class KeyNotFound(KvError):", "class Store:"] { - assert!( - readme.contains(needle), - "README Python snippet missing line: {needle}" - ); - assert!( - pyi.contains(needle), - "Generated Python .pyi missing line: {needle}" - ); - } -} - -#[test] -fn comparison_doc_has_required_structure() { - let comparison = read_workspace_file("docs/src/comparison.md"); - - for competitor in ["UniFFI", "cbindgen", "diplomat", "SWIG", "autocxx"] { - assert!( - comparison.contains(competitor), - "comparison.md missing competitor reference: {competitor}" - ); - } - - assert!( - comparison.contains("## When to choose WeaveFFI"), - "comparison.md missing 'When to choose WeaveFFI' section" - ); - assert!( - comparison.contains("## Where competitors are stronger"), - "comparison.md should be honest about competitor strengths" - ); -} - -#[test] -fn faq_doc_has_top_ten_questions() { - let faq = read_workspace_file("docs/src/faq.md"); - for question in [ - "Why not UniFFI?", - "Can I use it with C++ codebases?", - "Does it support generics?", - "What's the runtime overhead?", - "How are errors propagated?", - "Can I customize the generated code?", - "Does it work with Flutter?", - "Is it Windows-friendly?", - "How do I distribute the cdylib?", - "What's the licensing?", - ] { - assert!( - faq.contains(question), - "faq.md missing question: {question}" - ); - } -} - -#[test] -fn summary_md_includes_comparison_and_faq() { - let summary = read_workspace_file("docs/src/SUMMARY.md"); - assert!( - summary.contains("(comparison.md)"), - "SUMMARY.md should link to comparison.md" - ); - assert!( - summary.contains("(faq.md)"), - "SUMMARY.md should link to faq.md" - ); -} - -#[test] -fn intro_doc_matches_new_value_proposition() { - let intro = read_workspace_file("docs/src/intro.md"); - assert!( - intro.contains( - "WeaveFFI generates type-safe bindings for 11 languages for any native library" - ), - "intro.md should lead with the canonical value proposition" - ); - assert!( - intro.contains("[Comparison](comparison.md)"), - "intro.md should link to the comparison page" - ); - assert!( - intro.contains("[FAQ](faq.md)"), - "intro.md should link to the FAQ page" - ); -} diff --git a/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml b/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml index b5728218..a770ead1 100644 --- a/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml +++ b/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml @@ -1,4 +1,4 @@ -version: "0.9.0" +version: "0.10.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 index 30d0d9a8..0e7f717b 100644 --- a/crates/weaveffi-cli/tests/fixtures/edge_cases.yml +++ b/crates/weaveffi-cli/tests/fixtures/edge_cases.yml @@ -1,4 +1,4 @@ -version: "0.9.0" +version: "0.10.0" # Shapes the mainstream fixtures do not reach: identifiers that are reserved # words in one or more target languages, deeply nested composites, optional # parameters, interfaces in every legal position (including inside records, diff --git a/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml b/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml index 458285f6..32dcb996 100644 --- a/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml +++ b/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml @@ -1,4 +1,4 @@ -version: "0.9.0" +version: "0.10.0" modules: - name: shared structs: diff --git a/crates/weaveffi-cli/tests/fixtures/nested_modules.yml b/crates/weaveffi-cli/tests/fixtures/nested_modules.yml index 3b4934ef..012ed7a6 100644 --- a/crates/weaveffi-cli/tests/fixtures/nested_modules.yml +++ b/crates/weaveffi-cli/tests/fixtures/nested_modules.yml @@ -1,4 +1,4 @@ -version: "0.9.0" +version: "0.10.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/shapes.yml b/crates/weaveffi-cli/tests/fixtures/shapes.yml index 7d4bccf2..3fdef3a8 100644 --- a/crates/weaveffi-cli/tests/fixtures/shapes.yml +++ b/crates/weaveffi-cli/tests/fixtures/shapes.yml @@ -1,4 +1,4 @@ -version: "0.9.0" +version: "0.10.0" modules: - name: shapes doc: Rich (algebraic) enums and the expanded numeric primitives. diff --git a/crates/weaveffi-cli/tests/publish_script.rs b/crates/weaveffi-cli/tests/publish_script.rs deleted file mode 100644 index 3f1429cb..00000000 --- a/crates/weaveffi-cli/tests/publish_script.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::path::Path; - -fn parse_crates_from_script(script: &str) -> Vec { - let start = script.find("CRATES=(").expect("CRATES=( not found"); - let after = &script[start..]; - let end = after.find(')').expect("closing ) not found"); - let block = &after[..end]; - - block - .lines() - .filter_map(|line| { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.contains("CRATES") { - None - } else { - Some(trimmed.to_string()) - } - }) - .collect() -} - -#[test] -fn publish_script_contains_all_publishable_crates() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let script = std::fs::read_to_string(repo_root.join("scripts/publish-crates.sh")) - .expect("failed to read publish-crates.sh"); - - let crates = parse_crates_from_script(&script); - - let expected = [ - "weaveffi-ir", - "weaveffi-abi", - "weaveffi-core", - "weaveffi-bridge", - "weaveffi-macros", - "weaveffi", - "weaveffi-gen-c", - "weaveffi-gen-swift", - "weaveffi-gen-kotlin", - "weaveffi-gen-node", - "weaveffi-gen-wasm", - "weaveffi-gen-python", - "weaveffi-gen-dotnet", - "weaveffi-gen-cpp", - "weaveffi-gen-dart", - "weaveffi-gen-go", - "weaveffi-gen-ruby", - "weaveffi-cli", - ]; - - assert_eq!( - crates, expected, - "CRATES array does not match expected order" - ); -} - -#[test] -fn publish_script_cli_is_last() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let script = std::fs::read_to_string(repo_root.join("scripts/publish-crates.sh")) - .expect("failed to read publish-crates.sh"); - - let crates = parse_crates_from_script(&script); - assert_eq!( - crates.last().map(|s| s.as_str()), - Some("weaveffi-cli"), - "weaveffi-cli must be published last" - ); -} - -#[test] -fn publish_script_core_before_generators() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let script = std::fs::read_to_string(repo_root.join("scripts/publish-crates.sh")) - .expect("failed to read publish-crates.sh"); - - let crates = parse_crates_from_script(&script); - let core_pos = crates.iter().position(|c| c == "weaveffi-core"); - let gen_positions: Vec<_> = crates - .iter() - .enumerate() - .filter(|(_, c)| c.starts_with("weaveffi-gen-")) - .map(|(i, _)| i) - .collect(); - - let core_pos = core_pos.expect("weaveffi-core not found"); - for gen_pos in &gen_positions { - assert!( - core_pos < *gen_pos, - "weaveffi-core (pos {core_pos}) must come before generators (pos {gen_pos})" - ); - } -} diff --git a/crates/weaveffi-cli/tests/safety_audit.rs b/crates/weaveffi-cli/tests/safety_audit.rs deleted file mode 100644 index f0d74379..00000000 --- a/crates/weaveffi-cli/tests/safety_audit.rs +++ /dev/null @@ -1,450 +0,0 @@ -fn audit_api_yaml() -> &'static str { - r#"version: "0.9.0" -modules: - - name: inventory - structs: - - name: Widget - fields: - - name: id - type: i64 - - name: name - type: string - - name: label - type: "string?" - interfaces: - - name: Store - constructors: - - name: open - params: - - name: path - type: string - methods: - - name: get_widget - params: - - name: id - type: i64 - return: Widget - - name: name - params: [] - return: string - functions: - - name: create_store - params: - - name: name - type: string - return: Store - - name: store_name - params: - - name: store - type: Store - return: string - - name: list_widgets - params: [] - return: "[Widget]" - - name: count_widgets - params: [] - return: i32 -"# -} - -fn generate_target(target: &str) -> tempfile::TempDir { - let src = tempfile::tempdir().expect("create input dir"); - std::fs::write(src.path().join("api.yml"), audit_api_yaml()).unwrap(); - - let dst = tempfile::tempdir().expect("create output dir"); - let api_path = src.path().join("api.yml"); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - api_path.to_str().unwrap(), - "-o", - dst.path().to_str().unwrap(), - "--target", - target, - ]) - .assert() - .success(); - dst -} - -fn generate_all_targets() -> tempfile::TempDir { - let src = tempfile::tempdir().expect("create input dir"); - std::fs::write(src.path().join("api.yml"), audit_api_yaml()).unwrap(); - - let dst = tempfile::tempdir().expect("create output dir"); - let api_path = src.path().join("api.yml"); - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - api_path.to_str().unwrap(), - "-o", - dst.path().to_str().unwrap(), - ]) - .assert() - .success(); - dst -} - -fn read_generated(dir: &tempfile::TempDir, rel: &str) -> String { - std::fs::read_to_string(dir.path().join(rel)) - .unwrap_or_else(|e| panic!("failed to read {rel}: {e}")) -} - -#[test] -fn audit_c_string_ownership() { - let out = generate_target("c"); - let h = read_generated(&out, "c/weaveffi.h"); - - assert!( - h.contains("void weaveffi_free_string("), - "C header must declare weaveffi_free_string for owned string cleanup" - ); - assert!( - h.contains("void weaveffi_free_bytes("), - "C header must declare weaveffi_free_bytes for owned buffer cleanup" - ); - - let const_char_fns: Vec<&str> = h - .lines() - .filter(|l| { - // Every exported prototype is tagged with the `WEAVEFFI_API` - // visibility macro, with the `const char*` return type right after. - l.trim().strip_prefix("WEAVEFFI_API ").is_some_and(|t| { - t.starts_with("const char*") && t.contains('(') && t.ends_with(';') - }) - }) - .collect(); - assert!( - !const_char_fns.is_empty(), - "API should produce at least one function returning const char*" - ); - - // The interface is the one type with a lifecycle: it must have a destroy - // hook. Records are value types and must not. - assert!( - h.contains("void weaveffi_inventory_Store_destroy("), - "Store interface must have a _destroy declaration" - ); - assert!( - !h.contains("weaveffi_inventory_Widget_destroy("), - "Widget is a value record and must not have a _destroy declaration" - ); - - // A returned record is an owned value buffer the caller must release via - // weaveffi_free_bytes; the prototype hands its length back via out_len. - assert!( - h.contains("const uint8_t* weaveffi_inventory_Store_get_widget(") - && h.contains("size_t* out_len"), - "record return must be an owned value buffer with out_len" - ); - - for line in &const_char_fns { - assert!( - h.contains("weaveffi_free_string"), - "weaveffi_free_string must be declared for callers of: {line}" - ); - } -} - -#[test] -fn audit_swift_deinit_calls_destroy() { - let out = generate_target("swift"); - let swift = read_generated(&out, "swift/Sources/WeaveFFI/WeaveFFI.swift"); - - assert!( - swift.contains("public final class Store {"), - "Swift must generate the Store interface class" - ); - - assert!( - swift.contains("deinit {"), - "Store class must have a deinit block" - ); - assert!( - swift.contains("weaveffi_inventory_Store_destroy(ptr)"), - "Store deinit must call weaveffi_inventory_Store_destroy(ptr)" - ); - - let class_count = swift.matches("public final class ").count(); - let deinit_count = swift.matches("deinit {").count(); - assert_eq!( - class_count, deinit_count, - "every interface class must have exactly one deinit \ - (classes={class_count}, deinits={deinit_count})" - ); - - assert!( - swift.contains("defer { weaveffi_free_string("), - "string returns must use defer to free owned strings" - ); -} - -#[test] -fn audit_kotlin_closeable() { - let out = generate_target("kotlin"); - let kt = read_generated(&out, "kotlin/src/main/kotlin/com/weaveffi/WeaveFFI.kt"); - - assert!( - kt.contains("class Store private constructor(handle: Long) : AutoCloseable"), - "Store must implement AutoCloseable" - ); - assert!( - kt.contains("override fun close()"), - "Store must override close()" - ); - assert!( - kt.contains("WeaveNativeRef(handle) { h -> nativeDestroy(h) }"), - "the native reference must release through nativeDestroy exactly once" - ); - assert!( - kt.contains("handle.getAndSet(0L)") && kt.contains("if (h != 0L) release(h)"), - "release must atomically zero the handle so close() and the cleaner can't double-free" - ); - assert!( - kt.contains("weaveCleaner.register(this, ref)"), - "Store must register with a java.lang.ref.Cleaner as the GC safety net" - ); - assert!( - kt.contains("internal fun cloneHandle(): Long = nativeClone(ref.get())"), - "Store must expose cloneHandle() so a wrapper can hand a fresh strong reference to the producer" - ); -} - -#[test] -fn audit_python_context_manager() { - let out = generate_target("python"); - let py = read_generated(&out, "python/api/weaveffi.py"); - - assert!( - py.contains("class _PointerGuard"), - "missing _PointerGuard context manager utility" - ); - assert!( - py.contains("__exit__"), - "_PointerGuard must implement __exit__ for resource cleanup" - ); - - assert!(py.contains("class Store:"), "missing Store class"); - assert!( - py.contains("def __del__(self)"), - "Store must have __del__ destructor" - ); - assert!( - py.contains("weaveffi_inventory_Store_destroy"), - "Store __del__ must call weaveffi_inventory_Store_destroy" - ); - assert!( - py.contains("self._ptr = None"), - "Store must null _ptr after destroy to prevent double-free" - ); -} - -#[test] -fn audit_dotnet_idisposable() { - let out = generate_target("dotnet"); - let cs = read_generated(&out, "dotnet/WeaveFFI.cs"); - - assert!( - cs.contains("public class Store : IDisposable"), - "Store must implement IDisposable" - ); - assert!( - cs.contains("public void Dispose()"), - "Store must have Dispose() method" - ); - assert!( - cs.contains("weaveffi_inventory_Store_destroy("), - "Dispose must call weaveffi_inventory_Store_destroy" - ); - assert!( - cs.contains("Interlocked.Exchange(ref _released, 1)"), - "Store must release at most once, even when Dispose races the finalizer" - ); - assert!( - cs.contains("throw new ObjectDisposedException(nameof(Store))"), - "Store must refuse use after Dispose instead of passing a dangling pointer" - ); - assert!( - cs.contains("GC.SuppressFinalize(this)"), - "Dispose must call GC.SuppressFinalize" - ); - assert!( - cs.contains("~Store()"), - "Store must have a finalizer as GC safety net" - ); -} - -#[test] -fn audit_cpp_raii() { - let out = generate_target("cpp"); - let hpp = read_generated(&out, "cpp/weaveffi.hpp"); - - assert!(hpp.contains("class Store {"), "missing Store class"); - assert!(hpp.contains("~Store()"), "Store must have destructor"); - assert!( - hpp.contains("weaveffi_inventory_Store_destroy("), - "Store destructor must call _destroy" - ); - - // ABI 2 objects are reference counted, so copies are legal: each copy - // takes its own strong reference via `_clone` and releases it in its own - // destructor, which is what makes value semantics safe. - assert!( - hpp.contains( - "Store(const Store& other) : handle_(weaveffi_inventory_Store_clone(other.handle_)) {}" - ), - "Store copy constructor must take a fresh strong reference via _clone" - ); - assert!( - hpp.contains("Store& operator=(const Store& other) {"), - "Store must have copy assignment" - ); - assert!( - hpp.contains( - "weaveffi_inventory_Store* h = weaveffi_inventory_Store_clone(other.handle_);" - ), - "copy assignment must clone before destroying the old reference (self-assignment safe)" - ); - - assert!( - hpp.contains("Store(Store&& other) noexcept"), - "Store must have noexcept move constructor" - ); - assert!( - hpp.contains("Store& operator=(Store&& other) noexcept"), - "Store must have noexcept move assignment" - ); - assert!( - hpp.contains("other.handle_ = nullptr"), - "move must null source handle to prevent double-free" - ); - assert!( - hpp.contains("if (this != &other)"), - "move assignment must check self-assignment" - ); -} - -#[test] -fn audit_no_raw_pointer_leaks() { - let out = generate_all_targets(); - - // C: free_string/free_bytes declared, interface destroy declared, error - // out-params present on fallible prototypes. - { - let h = read_generated(&out, "c/weaveffi.h"); - assert!( - h.contains("weaveffi_free_string("), - "C: missing weaveffi_free_string declaration" - ); - assert!( - h.contains("weaveffi_free_bytes("), - "C: missing weaveffi_free_bytes declaration" - ); - assert!( - h.contains("weaveffi_inventory_Store_destroy("), - "C: missing Store_destroy declaration" - ); - assert!( - h.contains("weaveffi_inventory_Store_clone("), - "C: missing Store_clone declaration" - ); - // `_clone` and `_destroy` are the reference-count primitives: they - // never fail, so they are the only object symbols without `out_err`. - let fn_lines: Vec<&str> = h - .lines() - .filter(|l| l.contains("weaveffi_inventory_") && l.contains('(') && l.ends_with(';')) - .filter(|l| !l.contains("_destroy(") && !l.contains("_clone(")) - .filter(|l| !l.contains("typedef")) - .collect(); - for line in &fn_lines { - assert!( - line.contains("weaveffi_error* out_err"), - "C: function must have error out-param for error path safety: {line}" - ); - } - } - - // Swift: owned strings freed via defer, interface cleaned up in deinit - { - let swift = read_generated(&out, "swift/Sources/WeaveFFI/WeaveFFI.swift"); - assert!( - swift.contains("weaveffi_free_string("), - "Swift: missing weaveffi_free_string call" - ); - assert!( - swift.contains("weaveffi_inventory_Store_destroy(ptr)"), - "Swift: missing Store_destroy in deinit" - ); - assert!( - swift.contains("defer {"), - "Swift: must use defer for resource cleanup" - ); - } - - // Kotlin JNI: owned strings freed after JNI copy, interface destroy in - // nativeDestroy - { - let jni = read_generated(&out, "kotlin/src/main/cpp/weaveffi_jni.c"); - assert!( - jni.contains("weaveffi_free_string("), - "Kotlin JNI: missing weaveffi_free_string after the jstring copy" - ); - assert!( - jni.contains("_destroy("), - "Kotlin JNI: missing _destroy in nativeDestroy" - ); - } - - // Python: free_string bound in preamble, interface __del__ calls destroy - { - let py = read_generated(&out, "python/api/weaveffi.py"); - assert!( - py.contains("weaveffi_free_string"), - "Python: missing weaveffi_free_string binding" - ); - assert!( - py.contains("weaveffi_inventory_Store_destroy"), - "Python: missing Store_destroy call" - ); - assert!( - py.contains("self._ptr = None"), - "Python: must null pointer after destroy" - ); - } - - // .NET: strings freed after Marshal copy, interface Dispose calls destroy - { - let cs = read_generated(&out, "dotnet/WeaveFFI.cs"); - assert!( - cs.contains("weaveffi_free_string("), - ".NET: missing weaveffi_free_string call after string copy" - ); - assert!( - cs.contains("weaveffi_inventory_Store_destroy("), - ".NET: missing Store_destroy in Dispose" - ); - assert!( - cs.contains("GC.SuppressFinalize"), - ".NET: must suppress finalize after Dispose" - ); - } - - // C++: strings freed after std::string copy, RAII destructor calls destroy - { - let hpp = read_generated(&out, "cpp/weaveffi.hpp"); - assert!( - hpp.contains("weaveffi_free_string("), - "C++: missing weaveffi_free_string call after string copy" - ); - assert!( - hpp.contains("weaveffi_inventory_Store_destroy("), - "C++: missing Store_destroy in destructor" - ); - assert!( - hpp.contains("other.handle_ = nullptr"), - "C++: move must null source handle" - ); - } -} diff --git a/crates/weaveffi-cli/tests/snapshots.rs b/crates/weaveffi-cli/tests/snapshots.rs index 32ab48fb..47c21256 100644 --- a/crates/weaveffi-cli/tests/snapshots.rs +++ b/crates/weaveffi-cli/tests/snapshots.rs @@ -20,24 +20,24 @@ //! conformance harness's job. use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use camino::Utf8Path; -use weaveffi_core::codegen::{ConfiguredBackend, Target}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::validate::validate_api; -use weaveffi_gen_c::{CConfig, CGenerator}; -use weaveffi_gen_cpp::{CppConfig, CppGenerator}; -use weaveffi_gen_dart::{DartConfig, DartGenerator}; -use weaveffi_gen_dotnet::{DotnetConfig, DotnetGenerator}; -use weaveffi_gen_go::{GoConfig, GoGenerator}; -use weaveffi_gen_kotlin::{KotlinConfig, KotlinGenerator}; -use weaveffi_gen_node::{NodeConfig, NodeGenerator}; -use weaveffi_gen_python::{PythonConfig, PythonGenerator}; -use weaveffi_gen_ruby::{RubyConfig, RubyGenerator}; -use weaveffi_gen_swift::{SwiftConfig, SwiftGenerator}; -use weaveffi_gen_wasm::{WasmConfig, WasmGenerator}; -use weaveffi_ir::parse::parse_api_str; +use weaveffi_gen::codegen::{ConfiguredBackend, Target}; +use weaveffi_gen::targets::c::{CConfig, CGenerator}; +use weaveffi_gen::targets::cpp::{CppConfig, CppGenerator}; +use weaveffi_gen::targets::dart::{DartConfig, DartGenerator}; +use weaveffi_gen::targets::dotnet::{DotnetConfig, DotnetGenerator}; +use weaveffi_gen::targets::go::{GoConfig, GoGenerator}; +use weaveffi_gen::targets::kotlin::{KotlinConfig, KotlinGenerator}; +use weaveffi_gen::targets::node::{NodeConfig, NodeGenerator}; +use weaveffi_gen::targets::python::{PythonConfig, PythonGenerator}; +use weaveffi_gen::targets::ruby::{RubyConfig, RubyGenerator}; +use weaveffi_gen::targets::swift::{SwiftConfig, SwiftGenerator}; +use weaveffi_gen::targets::wasm::{WasmConfig, WasmGenerator}; +use weaveffi_model::parse::parse_api_str; +use weaveffi_model::resolved::ResolvedApi; +use weaveffi_model::validate::validate_api; const FIXTURES: [&str; 5] = [ "kitchen_sink", @@ -55,27 +55,9 @@ fn load_api(stem: &str) -> ResolvedApi { .unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display())); let api = parse_api_str(&contents, "yaml") .unwrap_or_else(|e| panic!("parse fixture {}: {e}", path.display())); - validate_api(api, None).unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) -} - -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 + validate_api(api, None) + .unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) + .with_identity(weaveffi_model::pkg::Identity::named(stem)) } fn sanitize(rel: &Path) -> String { @@ -115,19 +97,17 @@ fn assert_prelude_present(contents: &str, file: &Path) { } fn run_snapshots(target: &dyn Target) { + let out_dir = Utf8Path::new("out"); 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()); + let mut files = target.render(&api, out_dir); + files.sort_by(|a, b| a.path.cmp(&b.path)); assert!( !files.is_empty(), "generator {} produced no files for fixture {stem}", target.name(), ); + let root = out_dir.join(target.name()); insta::with_settings!({ snapshot_path => "snapshots", @@ -136,13 +116,12 @@ fn run_snapshots(target: &dyn Target) { }, { for file in files { let rel = file - .strip_prefix(root.as_std_path()) + .path + .strip_prefix(&root) .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); + let name = format!("{}_{stem}__{}", target.name(), sanitize(rel.as_std_path())); + insta::assert_snapshot!(name, redact_version(&file.contents)); + assert_prelude_present(&file.contents, file.path.as_std_path()); } }); } diff --git a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_buffer_h.snap new file mode 100644 index 00000000..2ee73664 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_buffer_h.snap @@ -0,0 +1,539 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef DOCS_EVERYWHERE_BUFFER_H +#define DOCS_EVERYWHERE_BUFFER_H + +#include "docs_everywhere.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Value buffer runtime. + * + * The wire format is little-endian and packed: fixed-width scalars at their + * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length + * followed by the raw bytes (no NUL terminator), optionals as a presence byte + * followed by the value, lists as a `uint32_t` count followed by the + * elements, maps as a `uint32_t` count followed by alternating keys and + * values, and objects as a `uint64_t` token holding one strong reference. + * ------------------------------------------------------------------------- */ + +/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. + A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be + passed to C string functions when the text has no interior NUL. */ +typedef struct docs_everywhere_str { + const char* ptr; + size_t len; +} docs_everywhere_str; + +/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ +typedef struct docs_everywhere_bytes { + const uint8_t* ptr; + size_t len; +} docs_everywhere_bytes; + +/* Borrow a NUL-terminated C string (NULL is the empty string). */ +static inline docs_everywhere_str docs_everywhere_str_of(const char* s) { + docs_everywhere_str v; + v.ptr = s; + v.len = s ? strlen(s) : 0; + return v; +} + +/* Borrow `len` bytes at `ptr`. */ +static inline docs_everywhere_bytes docs_everywhere_bytes_of(const uint8_t* ptr, size_t len) { + docs_everywhere_bytes v; + v.ptr = ptr; + v.len = len; + return v; +} + +/* Release a decoded string. Never call this on a borrowed view. */ +static inline void docs_everywhere_str_free(docs_everywhere_str* s) { + free((void*)s->ptr); + s->ptr = NULL; + s->len = 0; +} + +/* Release decoded bytes. Never call this on a borrowed view. */ +static inline void docs_everywhere_bytes_free(docs_everywhere_bytes* b) { + free((void*)b->ptr); + b->ptr = NULL; + b->len = 0; +} + +/* A growable output buffer. Zero-initialize it, write values, pass + `ptr`/`len` to the call, then release it with docs_everywhere_writer_free. An + allocation failure sets `failed` and turns later writes into no-ops. */ +typedef struct docs_everywhere_writer { + uint8_t* ptr; + size_t len; + size_t cap; + bool failed; +} docs_everywhere_writer; + +static inline void docs_everywhere_writer_free(docs_everywhere_writer* w) { + free(w->ptr); + w->ptr = NULL; + w->len = 0; + w->cap = 0; + w->failed = false; +} + +static inline void docs_everywhere_writer_put_raw(docs_everywhere_writer* w, const void* data, size_t n) { + if (w->failed || n == 0) { + return; + } + if (n > w->cap - w->len) { + size_t cap = w->cap ? w->cap : 64; + while (cap - w->len < n) { + if (cap > SIZE_MAX / 2) { + w->failed = true; + return; + } + cap *= 2; + } + uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); + if (grown == NULL) { + w->failed = true; + return; + } + w->ptr = grown; + w->cap = cap; + } + memcpy(w->ptr + w->len, data, n); + w->len += n; +} + +static inline void docs_everywhere_writer_put_u64(docs_everywhere_writer* w, uint64_t v) { + uint8_t b[8]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)(v >> (8 * i)); + } + docs_everywhere_writer_put_raw(w, b, 8); +} + +static inline void docs_everywhere_writer_put_u32(docs_everywhere_writer* w, uint32_t v) { + uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; + docs_everywhere_writer_put_raw(w, b, 4); +} + +static inline void docs_everywhere_writer_put_u16(docs_everywhere_writer* w, uint16_t v) { + uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; + docs_everywhere_writer_put_raw(w, b, 2); +} + +static inline void docs_everywhere_writer_put_u8(docs_everywhere_writer* w, uint8_t v) { + docs_everywhere_writer_put_raw(w, &v, 1); +} + +static inline void docs_everywhere_writer_put_i8(docs_everywhere_writer* w, int8_t v) { + docs_everywhere_writer_put_u8(w, (uint8_t)v); +} + +static inline void docs_everywhere_writer_put_i16(docs_everywhere_writer* w, int16_t v) { + docs_everywhere_writer_put_u16(w, (uint16_t)v); +} + +static inline void docs_everywhere_writer_put_i32(docs_everywhere_writer* w, int32_t v) { + docs_everywhere_writer_put_u32(w, (uint32_t)v); +} + +static inline void docs_everywhere_writer_put_i64(docs_everywhere_writer* w, int64_t v) { + docs_everywhere_writer_put_u64(w, (uint64_t)v); +} + +static inline void docs_everywhere_writer_put_bool(docs_everywhere_writer* w, bool v) { + docs_everywhere_writer_put_u8(w, v ? 1 : 0); +} + +static inline void docs_everywhere_writer_put_f32(docs_everywhere_writer* w, float v) { + uint32_t bits; + memcpy(&bits, &v, 4); + docs_everywhere_writer_put_u32(w, bits); +} + +static inline void docs_everywhere_writer_put_f64(docs_everywhere_writer* w, double v) { + uint64_t bits; + memcpy(&bits, &v, 8); + docs_everywhere_writer_put_u64(w, bits); +} + +static inline void docs_everywhere_writer_put_len(docs_everywhere_writer* w, size_t n) { + if (n > UINT32_MAX) { + w->failed = true; + return; + } + docs_everywhere_writer_put_u32(w, (uint32_t)n); +} + +static inline void docs_everywhere_writer_put_string(docs_everywhere_writer* w, docs_everywhere_str v) { + docs_everywhere_writer_put_len(w, v.len); + docs_everywhere_writer_put_raw(w, v.ptr, v.len); +} + +static inline void docs_everywhere_writer_put_bytes(docs_everywhere_writer* w, docs_everywhere_bytes v) { + docs_everywhere_writer_put_len(w, v.len); + docs_everywhere_writer_put_raw(w, v.ptr, v.len); +} + +/* Write an object token. The token carries one strong reference, so pass + a reference the buffer may own (the generated codecs pass a fresh + `_clone`). */ +static inline void docs_everywhere_writer_put_object(docs_everywhere_writer* w, const void* owned) { + docs_everywhere_writer_put_u64(w, (uint64_t)(uintptr_t)owned); +} + +/* A cursor over an input buffer. A malformed buffer sets `failed`; every + later read then returns a zero value, so a decoder can always finish and + release what it built. */ +typedef struct docs_everywhere_reader { + const uint8_t* ptr; + size_t len; + size_t pos; + bool failed; +} docs_everywhere_reader; + +static inline docs_everywhere_reader docs_everywhere_reader_of(const uint8_t* ptr, size_t len) { + docs_everywhere_reader r; + r.ptr = ptr; + r.len = ptr ? len : 0; + r.pos = 0; + r.failed = false; + return r; +} + +/* `true` when every byte was consumed and nothing was malformed. */ +static inline bool docs_everywhere_reader_finish(const docs_everywhere_reader* r) { + return !r->failed && r->pos == r->len; +} + +static inline const uint8_t* docs_everywhere_reader_take(docs_everywhere_reader* r, size_t n) { + if (r->failed || n > r->len - r->pos) { + r->failed = true; + return NULL; + } + const uint8_t* at = r->ptr + r->pos; + r->pos += n; + return at; +} + +/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is + zero). `min_wire` is the smallest encoding of one element; a count the + remaining bytes can't hold fails the reader before anything is + allocated. */ +static inline void* docs_everywhere_reader_alloc(docs_everywhere_reader* r, size_t n, size_t size, size_t min_wire) { + if (r->failed || n == 0) { + return NULL; + } + if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { + r->failed = true; + return NULL; + } + void* p = calloc(n, size); + if (p == NULL) { + r->failed = true; + } + return p; +} + +static inline uint64_t docs_everywhere_reader_get_u64(docs_everywhere_reader* r) { + const uint8_t* b = docs_everywhere_reader_take(r, 8); + uint64_t v = 0; + if (b != NULL) { + for (int i = 0; i < 8; i++) { + v |= (uint64_t)b[i] << (8 * i); + } + } + return v; +} + +static inline uint32_t docs_everywhere_reader_get_u32(docs_everywhere_reader* r) { + const uint8_t* b = docs_everywhere_reader_take(r, 4); + if (b == NULL) { + return 0; + } + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static inline uint16_t docs_everywhere_reader_get_u16(docs_everywhere_reader* r) { + const uint8_t* b = docs_everywhere_reader_take(r, 2); + return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; +} + +static inline uint8_t docs_everywhere_reader_get_u8(docs_everywhere_reader* r) { + const uint8_t* b = docs_everywhere_reader_take(r, 1); + return b ? b[0] : (uint8_t)0; +} + +static inline int8_t docs_everywhere_reader_get_i8(docs_everywhere_reader* r) { + return (int8_t)docs_everywhere_reader_get_u8(r); +} + +static inline int16_t docs_everywhere_reader_get_i16(docs_everywhere_reader* r) { + return (int16_t)docs_everywhere_reader_get_u16(r); +} + +static inline int32_t docs_everywhere_reader_get_i32(docs_everywhere_reader* r) { + return (int32_t)docs_everywhere_reader_get_u32(r); +} + +static inline int64_t docs_everywhere_reader_get_i64(docs_everywhere_reader* r) { + return (int64_t)docs_everywhere_reader_get_u64(r); +} + +static inline bool docs_everywhere_reader_get_bool(docs_everywhere_reader* r) { + uint8_t v = docs_everywhere_reader_get_u8(r); + if (v > 1) { + r->failed = true; + return false; + } + return v == 1; +} + +static inline float docs_everywhere_reader_get_f32(docs_everywhere_reader* r) { + uint32_t bits = docs_everywhere_reader_get_u32(r); + float v; + memcpy(&v, &bits, 4); + return v; +} + +static inline double docs_everywhere_reader_get_f64(docs_everywhere_reader* r) { + uint64_t bits = docs_everywhere_reader_get_u64(r); + double v; + memcpy(&v, &bits, 8); + return v; +} + +/* A `uint32_t` length followed by that many bytes, copied into a fresh + NUL-terminated allocation. */ +static inline uint8_t* docs_everywhere_reader_get_run(docs_everywhere_reader* r, size_t* out_len) { + size_t n = docs_everywhere_reader_get_u32(r); + const uint8_t* at = docs_everywhere_reader_take(r, n); + *out_len = 0; + if (at == NULL) { + return NULL; + } + uint8_t* copy = (uint8_t*)malloc(n + 1); + if (copy == NULL) { + r->failed = true; + return NULL; + } + memcpy(copy, at, n); + copy[n] = 0; + *out_len = n; + return copy; +} + +static inline docs_everywhere_str docs_everywhere_reader_get_string(docs_everywhere_reader* r) { + docs_everywhere_str v; + v.ptr = (const char*)docs_everywhere_reader_get_run(r, &v.len); + return v; +} + +static inline docs_everywhere_bytes docs_everywhere_reader_get_bytes(docs_everywhere_reader* r) { + docs_everywhere_bytes v; + v.ptr = docs_everywhere_reader_get_run(r, &v.len); + return v; +} + +/* Read an object token and adopt the strong reference it carries. A zero + token in a required position fails the reader. */ +static inline void* docs_everywhere_reader_get_object(docs_everywhere_reader* r) { + uint64_t token = docs_everywhere_reader_get_u64(r); + if (token == 0) { + r->failed = true; + } + return (void*)(uintptr_t)token; +} + +/* --------------------------------------------------------------------------- + * Value types. A record is a struct of its fields; a rich enum is a `tag` + * plus a union `as` with one struct per payload-carrying variant; a list is + * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an + * optional is a pointer that is NULL when absent; an object is its pointer; + * a string or byte string is a view (`ptr` + `len`). + * ------------------------------------------------------------------------- */ + +typedef struct docs_everywhere_docs_Document docs_everywhere_docs_Document; +typedef struct docs_everywhere_list_string docs_everywhere_list_string; + +/** `[string]` */ +struct docs_everywhere_list_string { + docs_everywhere_str* items; + size_t len; +}; + +/** + * Represents a single document tracked by the system. + * + * Documents are persisted to disk and exposed via the public API. + */ +struct docs_everywhere_docs_Document { + /** Stable opaque identifier */ + int64_t id; + /** Human-readable title shown in lists */ + docs_everywhere_str title; + /** Current lifecycle status */ + docs_everywhere_docs_Status status; + /** Free-form tags for grouping */ + docs_everywhere_list_string tags; +}; + +/* --------------------------------------------------------------------------- + * Codecs. Each value type T above, and each optional shape, has four + * functions: + * + * T_write(w, v) appends the value at `v` to writer `w` + * T_read(r, out) decodes one value from reader `r` + * T_decode(ptr, len, out) decodes a whole buffer; on a malformed + * buffer it frees what it built and + * returns false + * T_free(v) releases a decoded value + * + * An optional's T_write takes the nullable pointer itself. Writing an + * object stores a fresh `_clone` reference for the reader to adopt; + * decoding adopts each object reference, and T_free destroys it. T_free + * releases only what T_read or T_decode allocated; never call it on a + * value that borrows caller memory. + * ------------------------------------------------------------------------- */ + +static inline void docs_everywhere_docs_Document_write(docs_everywhere_writer* w, const docs_everywhere_docs_Document* v); +static inline void docs_everywhere_docs_Document_read(docs_everywhere_reader* r, docs_everywhere_docs_Document* out); +static inline bool docs_everywhere_docs_Document_decode(const uint8_t* ptr, size_t len, docs_everywhere_docs_Document* out); +static inline void docs_everywhere_docs_Document_free(docs_everywhere_docs_Document* v); +static inline void docs_everywhere_list_string_write(docs_everywhere_writer* w, const docs_everywhere_list_string* v); +static inline void docs_everywhere_list_string_read(docs_everywhere_reader* r, docs_everywhere_list_string* out); +static inline bool docs_everywhere_list_string_decode(const uint8_t* ptr, size_t len, docs_everywhere_list_string* out); +static inline void docs_everywhere_list_string_free(docs_everywhere_list_string* v); +static inline void docs_everywhere_opt_docs_Document_write(docs_everywhere_writer* w, const docs_everywhere_docs_Document* v); +static inline void docs_everywhere_opt_docs_Document_read(docs_everywhere_reader* r, docs_everywhere_docs_Document** out); +static inline bool docs_everywhere_opt_docs_Document_decode(const uint8_t* ptr, size_t len, docs_everywhere_docs_Document** out); +static inline void docs_everywhere_opt_docs_Document_free(docs_everywhere_docs_Document** v); + +static inline void docs_everywhere_docs_Document_write(docs_everywhere_writer* w, const docs_everywhere_docs_Document* v) { + docs_everywhere_writer_put_i64(w, v->id); + docs_everywhere_writer_put_string(w, v->title); + docs_everywhere_writer_put_i32(w, (int32_t)v->status); + docs_everywhere_list_string_write(w, &v->tags); +} + +static inline void docs_everywhere_docs_Document_read(docs_everywhere_reader* r, docs_everywhere_docs_Document* out) { + memset(out, 0, sizeof *out); + out->id = docs_everywhere_reader_get_i64(r); + out->title = docs_everywhere_reader_get_string(r); + out->status = (docs_everywhere_docs_Status)docs_everywhere_reader_get_i32(r); + docs_everywhere_list_string_read(r, &out->tags); +} + +static inline bool docs_everywhere_docs_Document_decode(const uint8_t* ptr, size_t len, docs_everywhere_docs_Document* out) { + docs_everywhere_reader r = docs_everywhere_reader_of(ptr, len); + docs_everywhere_docs_Document_read(&r, out); + if (!docs_everywhere_reader_finish(&r)) { + docs_everywhere_docs_Document_free(out); + return false; + } + return true; +} + +static inline void docs_everywhere_docs_Document_free(docs_everywhere_docs_Document* v) { + docs_everywhere_str_free(&v->title); + docs_everywhere_list_string_free(&v->tags); + memset(v, 0, sizeof *v); +} + +static inline void docs_everywhere_list_string_write(docs_everywhere_writer* w, const docs_everywhere_list_string* v) { + docs_everywhere_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + docs_everywhere_writer_put_string(w, v->items[i]); + } +} + +static inline void docs_everywhere_list_string_read(docs_everywhere_reader* r, docs_everywhere_list_string* out) { + memset(out, 0, sizeof *out); + size_t n = docs_everywhere_reader_get_u32(r); + out->items = (docs_everywhere_str*)docs_everywhere_reader_alloc(r, n, sizeof(docs_everywhere_str), 4); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->items[i] = docs_everywhere_reader_get_string(r); + } +} + +static inline bool docs_everywhere_list_string_decode(const uint8_t* ptr, size_t len, docs_everywhere_list_string* out) { + docs_everywhere_reader r = docs_everywhere_reader_of(ptr, len); + docs_everywhere_list_string_read(&r, out); + if (!docs_everywhere_reader_finish(&r)) { + docs_everywhere_list_string_free(out); + return false; + } + return true; +} + +static inline void docs_everywhere_list_string_free(docs_everywhere_list_string* v) { + for (size_t i = 0; i < v->len; i++) { + docs_everywhere_str_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void docs_everywhere_opt_docs_Document_write(docs_everywhere_writer* w, const docs_everywhere_docs_Document* v) { + docs_everywhere_writer_put_bool(w, v != NULL); + if (v != NULL) { + docs_everywhere_docs_Document_write(w, &(*v)); + } +} + +static inline void docs_everywhere_opt_docs_Document_read(docs_everywhere_reader* r, docs_everywhere_docs_Document** out) { + *out = NULL; + if (!docs_everywhere_reader_get_bool(r)) { + return; + } + docs_everywhere_docs_Document* value = (docs_everywhere_docs_Document*)calloc(1, sizeof(docs_everywhere_docs_Document)); + if (value == NULL) { + r->failed = true; + return; + } + docs_everywhere_docs_Document_read(r, &(*value)); + *out = value; +} + +static inline bool docs_everywhere_opt_docs_Document_decode(const uint8_t* ptr, size_t len, docs_everywhere_docs_Document** out) { + docs_everywhere_reader r = docs_everywhere_reader_of(ptr, len); + docs_everywhere_opt_docs_Document_read(&r, out); + if (!docs_everywhere_reader_finish(&r)) { + docs_everywhere_opt_docs_Document_free(out); + return false; + } + return true; +} + +static inline void docs_everywhere_opt_docs_Document_free(docs_everywhere_docs_Document** v) { + if (*v == NULL) { + return; + } + docs_everywhere_docs_Document_free(&(**v)); + free(*v); + *v = NULL; +} + +#ifdef __cplusplus +} +#endif + +#endif // DOCS_EVERYWHERE_BUFFER_H + +// END docs_everywhere_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_h.snap b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_h.snap new file mode 100644 index 00000000..31ab85e2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__docs_everywhere_h.snap @@ -0,0 +1,196 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef DOCS_EVERYWHERE_H +#define DOCS_EVERYWHERE_H + +#include +#include +#include + +#ifndef DOCS_EVERYWHERE_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef DOCS_EVERYWHERE_BUILD +# define DOCS_EVERYWHERE_API __declspec(dllexport) +# else +# define DOCS_EVERYWHERE_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define DOCS_EVERYWHERE_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define DOCS_EVERYWHERE_API __attribute__((visibility("default"))) +# else +# define DOCS_EVERYWHERE_API +# endif +#endif + +#ifndef DOCS_EVERYWHERE_DEPRECATED +# if defined(_MSC_VER) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define DOCS_EVERYWHERE_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports docs_everywhere_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define DOCS_EVERYWHERE_ABI_VERSION 3u +DOCS_EVERYWHERE_API uint32_t docs_everywhere_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by docs_everywhere_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct docs_everywhere_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} docs_everywhere_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_set(docs_everywhere_error* err, int32_t code, const char* message); +DOCS_EVERYWHERE_API void docs_everywhere_error_clear(docs_everywhere_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + docs_everywhere_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_free(docs_everywhere_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +DOCS_EVERYWHERE_API void docs_everywhere_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct docs_everywhere_cancel_token docs_everywhere_cancel_token; +DOCS_EVERYWHERE_API docs_everywhere_cancel_token* docs_everywhere_cancel_token_create(void); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_cancel(docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API bool docs_everywhere_cancel_token_is_cancelled(const docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_destroy(docs_everywhere_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +DOCS_EVERYWHERE_API uint8_t* docs_everywhere_alloc(uint32_t size); +DOCS_EVERYWHERE_API void docs_everywhere_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +DOCS_EVERYWHERE_API uint64_t docs_everywhere_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define DOCS_EVERYWHERE_DOCS_CHECKSUM 0x7950eec1e9cfc819ull +DOCS_EVERYWHERE_API uint64_t docs_everywhere_docs_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with docs_everywhere_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * docs_everywhere_buffer.h declares a C struct and codec for each. + */ + +/** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ +typedef enum { + /** Document is being edited */ + docs_everywhere_docs_Status_Draft = 0, + /** Document is publicly visible */ + docs_everywhere_docs_Status_Published = 1, + /** Document is hidden from listings */ + docs_everywhere_docs_Status_Archived = 2 +} docs_everywhere_docs_Status; +/** Error codes reported by throwing functions in the `docs` module tree. */ +typedef enum { + /** Returned when no document matches the requested id */ + docs_everywhere_docs_DocsErrors_not_found = 1, + /** Raised when attempting to move into a forbidden state */ + docs_everywhere_docs_DocsErrors_invalid_status = 2 +} docs_everywhere_docs_DocsErrors; +typedef struct docs_everywhere_docs_Notebook docs_everywhere_docs_Notebook; +/** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + * + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct docs_everywhere_docs_SavedListener_vtable { + /** Fires after a document has been persisted successfully */ + void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); + void (*free)(void* ctx); +} docs_everywhere_docs_SavedListener_vtable; +typedef void (*docs_everywhere_docs_publish_async_callback)(void* context, docs_everywhere_error* err, bool result); + +// Module: docs +/** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ +/** Create an empty notebook */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_new(docs_everywhere_error* out_err); +/** The largest number of documents one notebook will hold */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_max_documents(docs_everywhere_error* out_err); +/** Add a document, returning its position in the notebook */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_insert(const docs_everywhere_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, docs_everywhere_error* out_err); +/** Look up a document by identifier, failing when none matches */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_Notebook_find(const docs_everywhere_docs_Notebook* self, int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** The number of documents currently held */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_size(const docs_everywhere_docs_Notebook* self, docs_everywhere_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_clone(const docs_everywhere_docs_Notebook* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_Notebook_destroy(docs_everywhere_docs_Notebook* self); + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_create_document(const uint8_t* title_ptr, size_t title_len, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, docs_everywhere_error* out_err); +/** Look up a document by identifier */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_get_document(int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** Publish a document in the background */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_publish_async(int64_t id, docs_everywhere_docs_publish_async_callback callback, void* context); +/** Register a listener for saved notifications */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_watch(void* listener_ctx, const docs_everywhere_docs_SavedListener_vtable* listener_vtable, docs_everywhere_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // DOCS_EVERYWHERE_H + +// END docs_everywhere.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__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_docs_everywhere__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap deleted file mode 100644 index 69dd7ae3..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap +++ /dev/null @@ -1,182 +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 - -/* 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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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. - */ - -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -typedef enum { - /** Document is being edited */ - weaveffi_docs_Status_Draft = 0, - /** Document is publicly visible */ - weaveffi_docs_Status_Published = 1, - /** Document is hidden from listings */ - weaveffi_docs_Status_Archived = 2 -} weaveffi_docs_Status; -/** Error codes reported by throwing functions in the `docs` module tree. */ -typedef enum { - /** Returned when no document matches the requested id */ - weaveffi_docs_DocsErrors_not_found = 1, - /** Raised when attempting to move into a forbidden state */ - weaveffi_docs_DocsErrors_invalid_status = 2 -} weaveffi_docs_DocsErrors; -typedef struct weaveffi_docs_Notebook weaveffi_docs_Notebook; -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - * - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. - */ -typedef struct weaveffi_docs_SavedListener_vtable { - /** Fires after a document has been persisted successfully */ - void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_docs_SavedListener_vtable; -typedef void (*weaveffi_docs_publish_async_callback)(void* context, weaveffi_error* err, bool result); - -// Module: docs -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ -/** Create an empty notebook */ -WEAVEFFI_API weaveffi_docs_Notebook* weaveffi_docs_Notebook_new(weaveffi_error* out_err); -/** The largest number of documents one notebook will hold */ -WEAVEFFI_API int32_t weaveffi_docs_Notebook_max_documents(weaveffi_error* out_err); -/** Add a document, returning its position in the notebook */ -WEAVEFFI_API int32_t weaveffi_docs_Notebook_insert(const weaveffi_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, weaveffi_error* out_err); -/** Look up a document by identifier, failing when none matches */ -WEAVEFFI_API const uint8_t* weaveffi_docs_Notebook_find(const weaveffi_docs_Notebook* self, int64_t id, size_t* out_len, weaveffi_error* out_err); -/** The number of documents currently held */ -WEAVEFFI_API int32_t weaveffi_docs_Notebook_size(const weaveffi_docs_Notebook* self, weaveffi_error* out_err); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_docs_Notebook* weaveffi_docs_Notebook_clone(const weaveffi_docs_Notebook* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_docs_Notebook_destroy(weaveffi_docs_Notebook* self); - -/** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - */ -WEAVEFFI_API const uint8_t* weaveffi_docs_create_document(const char* title, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, weaveffi_error* out_err); -/** Look up a document by identifier */ -WEAVEFFI_API const uint8_t* weaveffi_docs_get_document(int64_t id, size_t* out_len, weaveffi_error* out_err); -/** Publish a document in the background */ -WEAVEFFI_API void weaveffi_docs_publish_async_async(int64_t id, weaveffi_docs_publish_async_callback callback, void* context); -/** Register a listener for saved notifications */ -WEAVEFFI_API void weaveffi_docs_watch(void* listener_ctx, const weaveffi_docs_SavedListener_vtable* listener_vtable, weaveffi_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_buffer_h.snap new file mode 100644 index 00000000..45cc3993 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_buffer_h.snap @@ -0,0 +1,1907 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef EDGE_CASES_BUFFER_H +#define EDGE_CASES_BUFFER_H + +#include "edge_cases.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Value buffer runtime. + * + * The wire format is little-endian and packed: fixed-width scalars at their + * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length + * followed by the raw bytes (no NUL terminator), optionals as a presence byte + * followed by the value, lists as a `uint32_t` count followed by the + * elements, maps as a `uint32_t` count followed by alternating keys and + * values, and objects as a `uint64_t` token holding one strong reference. + * ------------------------------------------------------------------------- */ + +/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. + A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be + passed to C string functions when the text has no interior NUL. */ +typedef struct edge_cases_str { + const char* ptr; + size_t len; +} edge_cases_str; + +/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ +typedef struct edge_cases_bytes { + const uint8_t* ptr; + size_t len; +} edge_cases_bytes; + +/* Borrow a NUL-terminated C string (NULL is the empty string). */ +static inline edge_cases_str edge_cases_str_of(const char* s) { + edge_cases_str v; + v.ptr = s; + v.len = s ? strlen(s) : 0; + return v; +} + +/* Borrow `len` bytes at `ptr`. */ +static inline edge_cases_bytes edge_cases_bytes_of(const uint8_t* ptr, size_t len) { + edge_cases_bytes v; + v.ptr = ptr; + v.len = len; + return v; +} + +/* Release a decoded string. Never call this on a borrowed view. */ +static inline void edge_cases_str_free(edge_cases_str* s) { + free((void*)s->ptr); + s->ptr = NULL; + s->len = 0; +} + +/* Release decoded bytes. Never call this on a borrowed view. */ +static inline void edge_cases_bytes_free(edge_cases_bytes* b) { + free((void*)b->ptr); + b->ptr = NULL; + b->len = 0; +} + +/* A growable output buffer. Zero-initialize it, write values, pass + `ptr`/`len` to the call, then release it with edge_cases_writer_free. An + allocation failure sets `failed` and turns later writes into no-ops. */ +typedef struct edge_cases_writer { + uint8_t* ptr; + size_t len; + size_t cap; + bool failed; +} edge_cases_writer; + +static inline void edge_cases_writer_free(edge_cases_writer* w) { + free(w->ptr); + w->ptr = NULL; + w->len = 0; + w->cap = 0; + w->failed = false; +} + +static inline void edge_cases_writer_put_raw(edge_cases_writer* w, const void* data, size_t n) { + if (w->failed || n == 0) { + return; + } + if (n > w->cap - w->len) { + size_t cap = w->cap ? w->cap : 64; + while (cap - w->len < n) { + if (cap > SIZE_MAX / 2) { + w->failed = true; + return; + } + cap *= 2; + } + uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); + if (grown == NULL) { + w->failed = true; + return; + } + w->ptr = grown; + w->cap = cap; + } + memcpy(w->ptr + w->len, data, n); + w->len += n; +} + +static inline void edge_cases_writer_put_u64(edge_cases_writer* w, uint64_t v) { + uint8_t b[8]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)(v >> (8 * i)); + } + edge_cases_writer_put_raw(w, b, 8); +} + +static inline void edge_cases_writer_put_u32(edge_cases_writer* w, uint32_t v) { + uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; + edge_cases_writer_put_raw(w, b, 4); +} + +static inline void edge_cases_writer_put_u16(edge_cases_writer* w, uint16_t v) { + uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; + edge_cases_writer_put_raw(w, b, 2); +} + +static inline void edge_cases_writer_put_u8(edge_cases_writer* w, uint8_t v) { + edge_cases_writer_put_raw(w, &v, 1); +} + +static inline void edge_cases_writer_put_i8(edge_cases_writer* w, int8_t v) { + edge_cases_writer_put_u8(w, (uint8_t)v); +} + +static inline void edge_cases_writer_put_i16(edge_cases_writer* w, int16_t v) { + edge_cases_writer_put_u16(w, (uint16_t)v); +} + +static inline void edge_cases_writer_put_i32(edge_cases_writer* w, int32_t v) { + edge_cases_writer_put_u32(w, (uint32_t)v); +} + +static inline void edge_cases_writer_put_i64(edge_cases_writer* w, int64_t v) { + edge_cases_writer_put_u64(w, (uint64_t)v); +} + +static inline void edge_cases_writer_put_bool(edge_cases_writer* w, bool v) { + edge_cases_writer_put_u8(w, v ? 1 : 0); +} + +static inline void edge_cases_writer_put_f32(edge_cases_writer* w, float v) { + uint32_t bits; + memcpy(&bits, &v, 4); + edge_cases_writer_put_u32(w, bits); +} + +static inline void edge_cases_writer_put_f64(edge_cases_writer* w, double v) { + uint64_t bits; + memcpy(&bits, &v, 8); + edge_cases_writer_put_u64(w, bits); +} + +static inline void edge_cases_writer_put_len(edge_cases_writer* w, size_t n) { + if (n > UINT32_MAX) { + w->failed = true; + return; + } + edge_cases_writer_put_u32(w, (uint32_t)n); +} + +static inline void edge_cases_writer_put_string(edge_cases_writer* w, edge_cases_str v) { + edge_cases_writer_put_len(w, v.len); + edge_cases_writer_put_raw(w, v.ptr, v.len); +} + +static inline void edge_cases_writer_put_bytes(edge_cases_writer* w, edge_cases_bytes v) { + edge_cases_writer_put_len(w, v.len); + edge_cases_writer_put_raw(w, v.ptr, v.len); +} + +/* Write an object token. The token carries one strong reference, so pass + a reference the buffer may own (the generated codecs pass a fresh + `_clone`). */ +static inline void edge_cases_writer_put_object(edge_cases_writer* w, const void* owned) { + edge_cases_writer_put_u64(w, (uint64_t)(uintptr_t)owned); +} + +/* A cursor over an input buffer. A malformed buffer sets `failed`; every + later read then returns a zero value, so a decoder can always finish and + release what it built. */ +typedef struct edge_cases_reader { + const uint8_t* ptr; + size_t len; + size_t pos; + bool failed; +} edge_cases_reader; + +static inline edge_cases_reader edge_cases_reader_of(const uint8_t* ptr, size_t len) { + edge_cases_reader r; + r.ptr = ptr; + r.len = ptr ? len : 0; + r.pos = 0; + r.failed = false; + return r; +} + +/* `true` when every byte was consumed and nothing was malformed. */ +static inline bool edge_cases_reader_finish(const edge_cases_reader* r) { + return !r->failed && r->pos == r->len; +} + +static inline const uint8_t* edge_cases_reader_take(edge_cases_reader* r, size_t n) { + if (r->failed || n > r->len - r->pos) { + r->failed = true; + return NULL; + } + const uint8_t* at = r->ptr + r->pos; + r->pos += n; + return at; +} + +/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is + zero). `min_wire` is the smallest encoding of one element; a count the + remaining bytes can't hold fails the reader before anything is + allocated. */ +static inline void* edge_cases_reader_alloc(edge_cases_reader* r, size_t n, size_t size, size_t min_wire) { + if (r->failed || n == 0) { + return NULL; + } + if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { + r->failed = true; + return NULL; + } + void* p = calloc(n, size); + if (p == NULL) { + r->failed = true; + } + return p; +} + +static inline uint64_t edge_cases_reader_get_u64(edge_cases_reader* r) { + const uint8_t* b = edge_cases_reader_take(r, 8); + uint64_t v = 0; + if (b != NULL) { + for (int i = 0; i < 8; i++) { + v |= (uint64_t)b[i] << (8 * i); + } + } + return v; +} + +static inline uint32_t edge_cases_reader_get_u32(edge_cases_reader* r) { + const uint8_t* b = edge_cases_reader_take(r, 4); + if (b == NULL) { + return 0; + } + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static inline uint16_t edge_cases_reader_get_u16(edge_cases_reader* r) { + const uint8_t* b = edge_cases_reader_take(r, 2); + return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; +} + +static inline uint8_t edge_cases_reader_get_u8(edge_cases_reader* r) { + const uint8_t* b = edge_cases_reader_take(r, 1); + return b ? b[0] : (uint8_t)0; +} + +static inline int8_t edge_cases_reader_get_i8(edge_cases_reader* r) { + return (int8_t)edge_cases_reader_get_u8(r); +} + +static inline int16_t edge_cases_reader_get_i16(edge_cases_reader* r) { + return (int16_t)edge_cases_reader_get_u16(r); +} + +static inline int32_t edge_cases_reader_get_i32(edge_cases_reader* r) { + return (int32_t)edge_cases_reader_get_u32(r); +} + +static inline int64_t edge_cases_reader_get_i64(edge_cases_reader* r) { + return (int64_t)edge_cases_reader_get_u64(r); +} + +static inline bool edge_cases_reader_get_bool(edge_cases_reader* r) { + uint8_t v = edge_cases_reader_get_u8(r); + if (v > 1) { + r->failed = true; + return false; + } + return v == 1; +} + +static inline float edge_cases_reader_get_f32(edge_cases_reader* r) { + uint32_t bits = edge_cases_reader_get_u32(r); + float v; + memcpy(&v, &bits, 4); + return v; +} + +static inline double edge_cases_reader_get_f64(edge_cases_reader* r) { + uint64_t bits = edge_cases_reader_get_u64(r); + double v; + memcpy(&v, &bits, 8); + return v; +} + +/* A `uint32_t` length followed by that many bytes, copied into a fresh + NUL-terminated allocation. */ +static inline uint8_t* edge_cases_reader_get_run(edge_cases_reader* r, size_t* out_len) { + size_t n = edge_cases_reader_get_u32(r); + const uint8_t* at = edge_cases_reader_take(r, n); + *out_len = 0; + if (at == NULL) { + return NULL; + } + uint8_t* copy = (uint8_t*)malloc(n + 1); + if (copy == NULL) { + r->failed = true; + return NULL; + } + memcpy(copy, at, n); + copy[n] = 0; + *out_len = n; + return copy; +} + +static inline edge_cases_str edge_cases_reader_get_string(edge_cases_reader* r) { + edge_cases_str v; + v.ptr = (const char*)edge_cases_reader_get_run(r, &v.len); + return v; +} + +static inline edge_cases_bytes edge_cases_reader_get_bytes(edge_cases_reader* r) { + edge_cases_bytes v; + v.ptr = edge_cases_reader_get_run(r, &v.len); + return v; +} + +/* Read an object token and adopt the strong reference it carries. A zero + token in a required position fails the reader. */ +static inline void* edge_cases_reader_get_object(edge_cases_reader* r) { + uint64_t token = edge_cases_reader_get_u64(r); + if (token == 0) { + r->failed = true; + } + return (void*)(uintptr_t)token; +} + +/* --------------------------------------------------------------------------- + * Value types. A record is a struct of its fields; a rich enum is a `tag` + * plus a union `as` with one struct per payload-carrying variant; a list is + * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an + * optional is a pointer that is NULL when absent; an object is its pointer; + * a string or byte string is a view (`ptr` + `len`). + * ------------------------------------------------------------------------- */ + +typedef struct edge_cases_edge_cases_Leaf edge_cases_edge_cases_Leaf; +typedef struct edge_cases_edge_cases_Inner edge_cases_edge_cases_Inner; +typedef struct edge_cases_edge_cases_Outer edge_cases_edge_cases_Outer; +typedef struct edge_cases_edge_cases_Keywords edge_cases_edge_cases_Keywords; +typedef struct edge_cases_edge_cases_Retired edge_cases_edge_cases_Retired; +typedef struct edge_cases_edge_cases_WidgetHolder edge_cases_edge_cases_WidgetHolder; +typedef struct edge_cases_list_edge_cases_Leaf edge_cases_list_edge_cases_Leaf; +typedef struct edge_cases_map_string_edge_cases_Inner edge_cases_map_string_edge_cases_Inner; +typedef struct edge_cases_list_edge_cases_Mode edge_cases_list_edge_cases_Mode; +typedef struct edge_cases_list_i32 edge_cases_list_i32; +typedef struct edge_cases_list_list_i32 edge_cases_list_list_i32; +typedef struct edge_cases_list_opt_i64 edge_cases_list_opt_i64; +typedef struct edge_cases_map_i64_string edge_cases_map_i64_string; +typedef struct edge_cases_list_edge_cases_Widget edge_cases_list_edge_cases_Widget; +typedef struct edge_cases_map_string_edge_cases_Widget edge_cases_map_string_edge_cases_Widget; +typedef struct edge_cases_list_string edge_cases_list_string; +typedef struct edge_cases_list_opt_edge_cases_Outer edge_cases_list_opt_edge_cases_Outer; +typedef struct edge_cases_list_opt_edge_cases_Widget edge_cases_list_opt_edge_cases_Widget; +typedef struct edge_cases_list_edge_cases_Outer edge_cases_list_edge_cases_Outer; + +/** `[edge_cases.Leaf]` */ +struct edge_cases_list_edge_cases_Leaf { + edge_cases_edge_cases_Leaf* items; + size_t len; +}; + +/** `{string:edge_cases.Inner}` */ +struct edge_cases_map_string_edge_cases_Inner { + edge_cases_str* keys; + edge_cases_edge_cases_Inner* values; + size_t len; +}; + +/** `[edge_cases.Mode]` */ +struct edge_cases_list_edge_cases_Mode { + edge_cases_edge_cases_Mode* items; + size_t len; +}; + +/** `[i32]` */ +struct edge_cases_list_i32 { + int32_t* items; + size_t len; +}; + +/** `[[i32]]` */ +struct edge_cases_list_list_i32 { + edge_cases_list_i32* items; + size_t len; +}; + +/** `[i64?]` */ +struct edge_cases_list_opt_i64 { + int64_t** items; + size_t len; +}; + +/** `{i64:string}` */ +struct edge_cases_map_i64_string { + int64_t* keys; + edge_cases_str* values; + size_t len; +}; + +/** `[edge_cases.Widget]` */ +struct edge_cases_list_edge_cases_Widget { + edge_cases_edge_cases_Widget** items; + size_t len; +}; + +/** `{string:edge_cases.Widget}` */ +struct edge_cases_map_string_edge_cases_Widget { + edge_cases_str* keys; + edge_cases_edge_cases_Widget** values; + size_t len; +}; + +/** `[string]` */ +struct edge_cases_list_string { + edge_cases_str* items; + size_t len; +}; + +/** `[edge_cases.Outer?]` */ +struct edge_cases_list_opt_edge_cases_Outer { + edge_cases_edge_cases_Outer** items; + size_t len; +}; + +/** `[edge_cases.Widget?]` */ +struct edge_cases_list_opt_edge_cases_Widget { + edge_cases_edge_cases_Widget** items; + size_t len; +}; + +/** `[edge_cases.Outer]` */ +struct edge_cases_list_edge_cases_Outer { + edge_cases_edge_cases_Outer* items; + size_t len; +}; + +struct edge_cases_edge_cases_Leaf { + float weight; +}; + +/** Middle layer of a three-deep struct nesting */ +struct edge_cases_edge_cases_Inner { + edge_cases_edge_cases_Leaf leaf; + edge_cases_list_edge_cases_Leaf leaves; +}; + +struct edge_cases_edge_cases_Outer { + edge_cases_edge_cases_Inner inner; + edge_cases_edge_cases_Inner* maybe_inner; + edge_cases_map_string_edge_cases_Inner by_name; + edge_cases_list_edge_cases_Mode modes; + edge_cases_list_list_i32 matrix; + edge_cases_list_opt_i64 sparse; + edge_cases_bytes* blob; + edge_cases_str* note; + edge_cases_map_i64_string by_id; + bool _hidden; + uint8_t v2; +}; + +/** Every field is a reserved word somewhere */ +struct edge_cases_edge_cases_Keywords { + int32_t class_; + int32_t def; + int32_t import; + int32_t from; + int32_t func; + int32_t range; + int32_t register_; + int32_t volatile_; + int32_t new_; + int32_t default_; + int32_t end; + int32_t in; + int32_t is; + int32_t int_; + int32_t var; + int32_t val; + int32_t self; + int32_t this_; + int32_t object; + int32_t string; + int32_t case_; + int32_t select; + int32_t switch_; + int32_t void_; + int32_t union_; + int32_t namespace_; + int32_t operator_; + int32_t template_; + int32_t package; + int32_t protocol; + int32_t guard; + int32_t defer; + int32_t inout; + int32_t internal; + int32_t when; + int32_t fun; + int32_t data; + int32_t late; + int32_t part; + int32_t out; + int32_t ref; + int32_t event; + int32_t base; + int32_t lambda; + int32_t yield; + int32_t nil; + int32_t null; + int32_t begin; + int32_t rescue; + int32_t module; + int32_t chan; + int32_t go; + int32_t let; + int32_t export_; + int32_t delete_; + int32_t typedef_; + int32_t sizeof_; + int32_t static_; + int32_t super; +}; + +/** + * Deprecated struct + * + * Deprecated: Use Outer instead + */ +struct edge_cases_edge_cases_Retired { + int64_t id; +}; + +/** Record carrying objects in every buffered position */ +struct edge_cases_edge_cases_WidgetHolder { + edge_cases_edge_cases_Widget* primary; + edge_cases_edge_cases_Widget* fallback; + edge_cases_list_edge_cases_Widget extras; + edge_cases_map_string_edge_cases_Widget by_name; +}; + +/* --------------------------------------------------------------------------- + * Codecs. Each value type T above, and each optional shape, has four + * functions: + * + * T_write(w, v) appends the value at `v` to writer `w` + * T_read(r, out) decodes one value from reader `r` + * T_decode(ptr, len, out) decodes a whole buffer; on a malformed + * buffer it frees what it built and + * returns false + * T_free(v) releases a decoded value + * + * An optional's T_write takes the nullable pointer itself. Writing an + * object stores a fresh `_clone` reference for the reader to adopt; + * decoding adopts each object reference, and T_free destroys it. T_free + * releases only what T_read or T_decode allocated; never call it on a + * value that borrows caller memory. + * ------------------------------------------------------------------------- */ + +static inline void edge_cases_edge_cases_Leaf_write(edge_cases_writer* w, const edge_cases_edge_cases_Leaf* v); +static inline void edge_cases_edge_cases_Leaf_read(edge_cases_reader* r, edge_cases_edge_cases_Leaf* out); +static inline bool edge_cases_edge_cases_Leaf_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Leaf* out); +static inline void edge_cases_edge_cases_Leaf_free(edge_cases_edge_cases_Leaf* v); +static inline void edge_cases_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_edge_cases_Inner* v); +static inline void edge_cases_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_edge_cases_Inner* out); +static inline bool edge_cases_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Inner* out); +static inline void edge_cases_edge_cases_Inner_free(edge_cases_edge_cases_Inner* v); +static inline void edge_cases_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_edge_cases_Outer* v); +static inline void edge_cases_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_edge_cases_Outer* out); +static inline bool edge_cases_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Outer* out); +static inline void edge_cases_edge_cases_Outer_free(edge_cases_edge_cases_Outer* v); +static inline void edge_cases_edge_cases_Keywords_write(edge_cases_writer* w, const edge_cases_edge_cases_Keywords* v); +static inline void edge_cases_edge_cases_Keywords_read(edge_cases_reader* r, edge_cases_edge_cases_Keywords* out); +static inline bool edge_cases_edge_cases_Keywords_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Keywords* out); +static inline void edge_cases_edge_cases_Keywords_free(edge_cases_edge_cases_Keywords* v); +static inline void edge_cases_edge_cases_Retired_write(edge_cases_writer* w, const edge_cases_edge_cases_Retired* v); +static inline void edge_cases_edge_cases_Retired_read(edge_cases_reader* r, edge_cases_edge_cases_Retired* out); +static inline bool edge_cases_edge_cases_Retired_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Retired* out); +static inline void edge_cases_edge_cases_Retired_free(edge_cases_edge_cases_Retired* v); +static inline void edge_cases_edge_cases_WidgetHolder_write(edge_cases_writer* w, const edge_cases_edge_cases_WidgetHolder* v); +static inline void edge_cases_edge_cases_WidgetHolder_read(edge_cases_reader* r, edge_cases_edge_cases_WidgetHolder* out); +static inline bool edge_cases_edge_cases_WidgetHolder_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_WidgetHolder* out); +static inline void edge_cases_edge_cases_WidgetHolder_free(edge_cases_edge_cases_WidgetHolder* v); +static inline void edge_cases_list_edge_cases_Leaf_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Leaf* v); +static inline void edge_cases_list_edge_cases_Leaf_read(edge_cases_reader* r, edge_cases_list_edge_cases_Leaf* out); +static inline bool edge_cases_list_edge_cases_Leaf_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Leaf* out); +static inline void edge_cases_list_edge_cases_Leaf_free(edge_cases_list_edge_cases_Leaf* v); +static inline void edge_cases_opt_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_edge_cases_Inner* v); +static inline void edge_cases_opt_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_edge_cases_Inner** out); +static inline bool edge_cases_opt_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Inner** out); +static inline void edge_cases_opt_edge_cases_Inner_free(edge_cases_edge_cases_Inner** v); +static inline void edge_cases_map_string_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_map_string_edge_cases_Inner* v); +static inline void edge_cases_map_string_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_map_string_edge_cases_Inner* out); +static inline bool edge_cases_map_string_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_map_string_edge_cases_Inner* out); +static inline void edge_cases_map_string_edge_cases_Inner_free(edge_cases_map_string_edge_cases_Inner* v); +static inline void edge_cases_list_edge_cases_Mode_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Mode* v); +static inline void edge_cases_list_edge_cases_Mode_read(edge_cases_reader* r, edge_cases_list_edge_cases_Mode* out); +static inline bool edge_cases_list_edge_cases_Mode_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Mode* out); +static inline void edge_cases_list_edge_cases_Mode_free(edge_cases_list_edge_cases_Mode* v); +static inline void edge_cases_list_i32_write(edge_cases_writer* w, const edge_cases_list_i32* v); +static inline void edge_cases_list_i32_read(edge_cases_reader* r, edge_cases_list_i32* out); +static inline bool edge_cases_list_i32_decode(const uint8_t* ptr, size_t len, edge_cases_list_i32* out); +static inline void edge_cases_list_i32_free(edge_cases_list_i32* v); +static inline void edge_cases_list_list_i32_write(edge_cases_writer* w, const edge_cases_list_list_i32* v); +static inline void edge_cases_list_list_i32_read(edge_cases_reader* r, edge_cases_list_list_i32* out); +static inline bool edge_cases_list_list_i32_decode(const uint8_t* ptr, size_t len, edge_cases_list_list_i32* out); +static inline void edge_cases_list_list_i32_free(edge_cases_list_list_i32* v); +static inline void edge_cases_opt_i64_write(edge_cases_writer* w, const int64_t* v); +static inline void edge_cases_opt_i64_read(edge_cases_reader* r, int64_t** out); +static inline bool edge_cases_opt_i64_decode(const uint8_t* ptr, size_t len, int64_t** out); +static inline void edge_cases_opt_i64_free(int64_t** v); +static inline void edge_cases_list_opt_i64_write(edge_cases_writer* w, const edge_cases_list_opt_i64* v); +static inline void edge_cases_list_opt_i64_read(edge_cases_reader* r, edge_cases_list_opt_i64* out); +static inline bool edge_cases_list_opt_i64_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_i64* out); +static inline void edge_cases_list_opt_i64_free(edge_cases_list_opt_i64* v); +static inline void edge_cases_opt_bytes_write(edge_cases_writer* w, const edge_cases_bytes* v); +static inline void edge_cases_opt_bytes_read(edge_cases_reader* r, edge_cases_bytes** out); +static inline bool edge_cases_opt_bytes_decode(const uint8_t* ptr, size_t len, edge_cases_bytes** out); +static inline void edge_cases_opt_bytes_free(edge_cases_bytes** v); +static inline void edge_cases_opt_string_write(edge_cases_writer* w, const edge_cases_str* v); +static inline void edge_cases_opt_string_read(edge_cases_reader* r, edge_cases_str** out); +static inline bool edge_cases_opt_string_decode(const uint8_t* ptr, size_t len, edge_cases_str** out); +static inline void edge_cases_opt_string_free(edge_cases_str** v); +static inline void edge_cases_map_i64_string_write(edge_cases_writer* w, const edge_cases_map_i64_string* v); +static inline void edge_cases_map_i64_string_read(edge_cases_reader* r, edge_cases_map_i64_string* out); +static inline bool edge_cases_map_i64_string_decode(const uint8_t* ptr, size_t len, edge_cases_map_i64_string* out); +static inline void edge_cases_map_i64_string_free(edge_cases_map_i64_string* v); +static inline void edge_cases_opt_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_edge_cases_Widget* v); +static inline void edge_cases_opt_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_edge_cases_Widget** out); +static inline bool edge_cases_opt_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Widget** out); +static inline void edge_cases_opt_edge_cases_Widget_free(edge_cases_edge_cases_Widget** v); +static inline void edge_cases_list_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Widget* v); +static inline void edge_cases_list_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_list_edge_cases_Widget* out); +static inline bool edge_cases_list_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Widget* out); +static inline void edge_cases_list_edge_cases_Widget_free(edge_cases_list_edge_cases_Widget* v); +static inline void edge_cases_map_string_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_map_string_edge_cases_Widget* v); +static inline void edge_cases_map_string_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_map_string_edge_cases_Widget* out); +static inline bool edge_cases_map_string_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_map_string_edge_cases_Widget* out); +static inline void edge_cases_map_string_edge_cases_Widget_free(edge_cases_map_string_edge_cases_Widget* v); +static inline void edge_cases_list_string_write(edge_cases_writer* w, const edge_cases_list_string* v); +static inline void edge_cases_list_string_read(edge_cases_reader* r, edge_cases_list_string* out); +static inline bool edge_cases_list_string_decode(const uint8_t* ptr, size_t len, edge_cases_list_string* out); +static inline void edge_cases_list_string_free(edge_cases_list_string* v); +static inline void edge_cases_opt_i32_write(edge_cases_writer* w, const int32_t* v); +static inline void edge_cases_opt_i32_read(edge_cases_reader* r, int32_t** out); +static inline bool edge_cases_opt_i32_decode(const uint8_t* ptr, size_t len, int32_t** out); +static inline void edge_cases_opt_i32_free(int32_t** v); +static inline void edge_cases_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_edge_cases_Outer* v); +static inline void edge_cases_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_edge_cases_Outer** out); +static inline bool edge_cases_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Outer** out); +static inline void edge_cases_opt_edge_cases_Outer_free(edge_cases_edge_cases_Outer** v); +static inline void edge_cases_list_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Outer* v); +static inline void edge_cases_list_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Outer* out); +static inline bool edge_cases_list_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Outer* out); +static inline void edge_cases_list_opt_edge_cases_Outer_free(edge_cases_list_opt_edge_cases_Outer* v); +static inline void edge_cases_opt_list_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Outer* v); +static inline void edge_cases_opt_list_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Outer** out); +static inline bool edge_cases_opt_list_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Outer** out); +static inline void edge_cases_opt_list_opt_edge_cases_Outer_free(edge_cases_list_opt_edge_cases_Outer** v); +static inline void edge_cases_opt_list_string_write(edge_cases_writer* w, const edge_cases_list_string* v); +static inline void edge_cases_opt_list_string_read(edge_cases_reader* r, edge_cases_list_string** out); +static inline bool edge_cases_opt_list_string_decode(const uint8_t* ptr, size_t len, edge_cases_list_string** out); +static inline void edge_cases_opt_list_string_free(edge_cases_list_string** v); +static inline void edge_cases_opt_edge_cases_Mode_write(edge_cases_writer* w, const edge_cases_edge_cases_Mode* v); +static inline void edge_cases_opt_edge_cases_Mode_read(edge_cases_reader* r, edge_cases_edge_cases_Mode** out); +static inline bool edge_cases_opt_edge_cases_Mode_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Mode** out); +static inline void edge_cases_opt_edge_cases_Mode_free(edge_cases_edge_cases_Mode** v); +static inline void edge_cases_list_opt_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Widget* v); +static inline void edge_cases_list_opt_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Widget* out); +static inline bool edge_cases_list_opt_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Widget* out); +static inline void edge_cases_list_opt_edge_cases_Widget_free(edge_cases_list_opt_edge_cases_Widget* v); +static inline void edge_cases_list_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Outer* v); +static inline void edge_cases_list_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_edge_cases_Outer* out); +static inline bool edge_cases_list_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Outer* out); +static inline void edge_cases_list_edge_cases_Outer_free(edge_cases_list_edge_cases_Outer* v); + +static inline void edge_cases_edge_cases_Leaf_write(edge_cases_writer* w, const edge_cases_edge_cases_Leaf* v) { + edge_cases_writer_put_f32(w, v->weight); +} + +static inline void edge_cases_edge_cases_Leaf_read(edge_cases_reader* r, edge_cases_edge_cases_Leaf* out) { + memset(out, 0, sizeof *out); + out->weight = edge_cases_reader_get_f32(r); +} + +static inline bool edge_cases_edge_cases_Leaf_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Leaf* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_edge_cases_Leaf_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_edge_cases_Leaf_free(out); + return false; + } + return true; +} + +static inline void edge_cases_edge_cases_Leaf_free(edge_cases_edge_cases_Leaf* v) { + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_edge_cases_Inner* v) { + edge_cases_edge_cases_Leaf_write(w, &v->leaf); + edge_cases_list_edge_cases_Leaf_write(w, &v->leaves); +} + +static inline void edge_cases_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_edge_cases_Inner* out) { + memset(out, 0, sizeof *out); + edge_cases_edge_cases_Leaf_read(r, &out->leaf); + edge_cases_list_edge_cases_Leaf_read(r, &out->leaves); +} + +static inline bool edge_cases_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Inner* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_edge_cases_Inner_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_edge_cases_Inner_free(out); + return false; + } + return true; +} + +static inline void edge_cases_edge_cases_Inner_free(edge_cases_edge_cases_Inner* v) { + edge_cases_list_edge_cases_Leaf_free(&v->leaves); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_edge_cases_Outer* v) { + edge_cases_edge_cases_Inner_write(w, &v->inner); + edge_cases_opt_edge_cases_Inner_write(w, v->maybe_inner); + edge_cases_map_string_edge_cases_Inner_write(w, &v->by_name); + edge_cases_list_edge_cases_Mode_write(w, &v->modes); + edge_cases_list_list_i32_write(w, &v->matrix); + edge_cases_list_opt_i64_write(w, &v->sparse); + edge_cases_opt_bytes_write(w, v->blob); + edge_cases_opt_string_write(w, v->note); + edge_cases_map_i64_string_write(w, &v->by_id); + edge_cases_writer_put_bool(w, v->_hidden); + edge_cases_writer_put_u8(w, v->v2); +} + +static inline void edge_cases_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_edge_cases_Outer* out) { + memset(out, 0, sizeof *out); + edge_cases_edge_cases_Inner_read(r, &out->inner); + edge_cases_opt_edge_cases_Inner_read(r, &out->maybe_inner); + edge_cases_map_string_edge_cases_Inner_read(r, &out->by_name); + edge_cases_list_edge_cases_Mode_read(r, &out->modes); + edge_cases_list_list_i32_read(r, &out->matrix); + edge_cases_list_opt_i64_read(r, &out->sparse); + edge_cases_opt_bytes_read(r, &out->blob); + edge_cases_opt_string_read(r, &out->note); + edge_cases_map_i64_string_read(r, &out->by_id); + out->_hidden = edge_cases_reader_get_bool(r); + out->v2 = edge_cases_reader_get_u8(r); +} + +static inline bool edge_cases_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Outer* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_edge_cases_Outer_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_edge_cases_Outer_free(out); + return false; + } + return true; +} + +static inline void edge_cases_edge_cases_Outer_free(edge_cases_edge_cases_Outer* v) { + edge_cases_edge_cases_Inner_free(&v->inner); + edge_cases_opt_edge_cases_Inner_free(&v->maybe_inner); + edge_cases_map_string_edge_cases_Inner_free(&v->by_name); + edge_cases_list_edge_cases_Mode_free(&v->modes); + edge_cases_list_list_i32_free(&v->matrix); + edge_cases_list_opt_i64_free(&v->sparse); + edge_cases_opt_bytes_free(&v->blob); + edge_cases_opt_string_free(&v->note); + edge_cases_map_i64_string_free(&v->by_id); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_edge_cases_Keywords_write(edge_cases_writer* w, const edge_cases_edge_cases_Keywords* v) { + edge_cases_writer_put_i32(w, v->class_); + edge_cases_writer_put_i32(w, v->def); + edge_cases_writer_put_i32(w, v->import); + edge_cases_writer_put_i32(w, v->from); + edge_cases_writer_put_i32(w, v->func); + edge_cases_writer_put_i32(w, v->range); + edge_cases_writer_put_i32(w, v->register_); + edge_cases_writer_put_i32(w, v->volatile_); + edge_cases_writer_put_i32(w, v->new_); + edge_cases_writer_put_i32(w, v->default_); + edge_cases_writer_put_i32(w, v->end); + edge_cases_writer_put_i32(w, v->in); + edge_cases_writer_put_i32(w, v->is); + edge_cases_writer_put_i32(w, v->int_); + edge_cases_writer_put_i32(w, v->var); + edge_cases_writer_put_i32(w, v->val); + edge_cases_writer_put_i32(w, v->self); + edge_cases_writer_put_i32(w, v->this_); + edge_cases_writer_put_i32(w, v->object); + edge_cases_writer_put_i32(w, v->string); + edge_cases_writer_put_i32(w, v->case_); + edge_cases_writer_put_i32(w, v->select); + edge_cases_writer_put_i32(w, v->switch_); + edge_cases_writer_put_i32(w, v->void_); + edge_cases_writer_put_i32(w, v->union_); + edge_cases_writer_put_i32(w, v->namespace_); + edge_cases_writer_put_i32(w, v->operator_); + edge_cases_writer_put_i32(w, v->template_); + edge_cases_writer_put_i32(w, v->package); + edge_cases_writer_put_i32(w, v->protocol); + edge_cases_writer_put_i32(w, v->guard); + edge_cases_writer_put_i32(w, v->defer); + edge_cases_writer_put_i32(w, v->inout); + edge_cases_writer_put_i32(w, v->internal); + edge_cases_writer_put_i32(w, v->when); + edge_cases_writer_put_i32(w, v->fun); + edge_cases_writer_put_i32(w, v->data); + edge_cases_writer_put_i32(w, v->late); + edge_cases_writer_put_i32(w, v->part); + edge_cases_writer_put_i32(w, v->out); + edge_cases_writer_put_i32(w, v->ref); + edge_cases_writer_put_i32(w, v->event); + edge_cases_writer_put_i32(w, v->base); + edge_cases_writer_put_i32(w, v->lambda); + edge_cases_writer_put_i32(w, v->yield); + edge_cases_writer_put_i32(w, v->nil); + edge_cases_writer_put_i32(w, v->null); + edge_cases_writer_put_i32(w, v->begin); + edge_cases_writer_put_i32(w, v->rescue); + edge_cases_writer_put_i32(w, v->module); + edge_cases_writer_put_i32(w, v->chan); + edge_cases_writer_put_i32(w, v->go); + edge_cases_writer_put_i32(w, v->let); + edge_cases_writer_put_i32(w, v->export_); + edge_cases_writer_put_i32(w, v->delete_); + edge_cases_writer_put_i32(w, v->typedef_); + edge_cases_writer_put_i32(w, v->sizeof_); + edge_cases_writer_put_i32(w, v->static_); + edge_cases_writer_put_i32(w, v->super); +} + +static inline void edge_cases_edge_cases_Keywords_read(edge_cases_reader* r, edge_cases_edge_cases_Keywords* out) { + memset(out, 0, sizeof *out); + out->class_ = edge_cases_reader_get_i32(r); + out->def = edge_cases_reader_get_i32(r); + out->import = edge_cases_reader_get_i32(r); + out->from = edge_cases_reader_get_i32(r); + out->func = edge_cases_reader_get_i32(r); + out->range = edge_cases_reader_get_i32(r); + out->register_ = edge_cases_reader_get_i32(r); + out->volatile_ = edge_cases_reader_get_i32(r); + out->new_ = edge_cases_reader_get_i32(r); + out->default_ = edge_cases_reader_get_i32(r); + out->end = edge_cases_reader_get_i32(r); + out->in = edge_cases_reader_get_i32(r); + out->is = edge_cases_reader_get_i32(r); + out->int_ = edge_cases_reader_get_i32(r); + out->var = edge_cases_reader_get_i32(r); + out->val = edge_cases_reader_get_i32(r); + out->self = edge_cases_reader_get_i32(r); + out->this_ = edge_cases_reader_get_i32(r); + out->object = edge_cases_reader_get_i32(r); + out->string = edge_cases_reader_get_i32(r); + out->case_ = edge_cases_reader_get_i32(r); + out->select = edge_cases_reader_get_i32(r); + out->switch_ = edge_cases_reader_get_i32(r); + out->void_ = edge_cases_reader_get_i32(r); + out->union_ = edge_cases_reader_get_i32(r); + out->namespace_ = edge_cases_reader_get_i32(r); + out->operator_ = edge_cases_reader_get_i32(r); + out->template_ = edge_cases_reader_get_i32(r); + out->package = edge_cases_reader_get_i32(r); + out->protocol = edge_cases_reader_get_i32(r); + out->guard = edge_cases_reader_get_i32(r); + out->defer = edge_cases_reader_get_i32(r); + out->inout = edge_cases_reader_get_i32(r); + out->internal = edge_cases_reader_get_i32(r); + out->when = edge_cases_reader_get_i32(r); + out->fun = edge_cases_reader_get_i32(r); + out->data = edge_cases_reader_get_i32(r); + out->late = edge_cases_reader_get_i32(r); + out->part = edge_cases_reader_get_i32(r); + out->out = edge_cases_reader_get_i32(r); + out->ref = edge_cases_reader_get_i32(r); + out->event = edge_cases_reader_get_i32(r); + out->base = edge_cases_reader_get_i32(r); + out->lambda = edge_cases_reader_get_i32(r); + out->yield = edge_cases_reader_get_i32(r); + out->nil = edge_cases_reader_get_i32(r); + out->null = edge_cases_reader_get_i32(r); + out->begin = edge_cases_reader_get_i32(r); + out->rescue = edge_cases_reader_get_i32(r); + out->module = edge_cases_reader_get_i32(r); + out->chan = edge_cases_reader_get_i32(r); + out->go = edge_cases_reader_get_i32(r); + out->let = edge_cases_reader_get_i32(r); + out->export_ = edge_cases_reader_get_i32(r); + out->delete_ = edge_cases_reader_get_i32(r); + out->typedef_ = edge_cases_reader_get_i32(r); + out->sizeof_ = edge_cases_reader_get_i32(r); + out->static_ = edge_cases_reader_get_i32(r); + out->super = edge_cases_reader_get_i32(r); +} + +static inline bool edge_cases_edge_cases_Keywords_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Keywords* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_edge_cases_Keywords_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_edge_cases_Keywords_free(out); + return false; + } + return true; +} + +static inline void edge_cases_edge_cases_Keywords_free(edge_cases_edge_cases_Keywords* v) { + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_edge_cases_Retired_write(edge_cases_writer* w, const edge_cases_edge_cases_Retired* v) { + edge_cases_writer_put_i64(w, v->id); +} + +static inline void edge_cases_edge_cases_Retired_read(edge_cases_reader* r, edge_cases_edge_cases_Retired* out) { + memset(out, 0, sizeof *out); + out->id = edge_cases_reader_get_i64(r); +} + +static inline bool edge_cases_edge_cases_Retired_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Retired* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_edge_cases_Retired_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_edge_cases_Retired_free(out); + return false; + } + return true; +} + +static inline void edge_cases_edge_cases_Retired_free(edge_cases_edge_cases_Retired* v) { + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_edge_cases_WidgetHolder_write(edge_cases_writer* w, const edge_cases_edge_cases_WidgetHolder* v) { + edge_cases_writer_put_object(w, edge_cases_edge_cases_Widget_clone(v->primary)); + edge_cases_opt_edge_cases_Widget_write(w, v->fallback); + edge_cases_list_edge_cases_Widget_write(w, &v->extras); + edge_cases_map_string_edge_cases_Widget_write(w, &v->by_name); +} + +static inline void edge_cases_edge_cases_WidgetHolder_read(edge_cases_reader* r, edge_cases_edge_cases_WidgetHolder* out) { + memset(out, 0, sizeof *out); + out->primary = (edge_cases_edge_cases_Widget*)edge_cases_reader_get_object(r); + edge_cases_opt_edge_cases_Widget_read(r, &out->fallback); + edge_cases_list_edge_cases_Widget_read(r, &out->extras); + edge_cases_map_string_edge_cases_Widget_read(r, &out->by_name); +} + +static inline bool edge_cases_edge_cases_WidgetHolder_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_WidgetHolder* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_edge_cases_WidgetHolder_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_edge_cases_WidgetHolder_free(out); + return false; + } + return true; +} + +static inline void edge_cases_edge_cases_WidgetHolder_free(edge_cases_edge_cases_WidgetHolder* v) { + edge_cases_edge_cases_Widget_destroy(v->primary); + edge_cases_opt_edge_cases_Widget_free(&v->fallback); + edge_cases_list_edge_cases_Widget_free(&v->extras); + edge_cases_map_string_edge_cases_Widget_free(&v->by_name); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_list_edge_cases_Leaf_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Leaf* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_edge_cases_Leaf_write(w, &v->items[i]); + } +} + +static inline void edge_cases_list_edge_cases_Leaf_read(edge_cases_reader* r, edge_cases_list_edge_cases_Leaf* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (edge_cases_edge_cases_Leaf*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Leaf), 4); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + edge_cases_edge_cases_Leaf_read(r, &out->items[i]); + } +} + +static inline bool edge_cases_list_edge_cases_Leaf_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Leaf* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_edge_cases_Leaf_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_edge_cases_Leaf_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_edge_cases_Leaf_free(edge_cases_list_edge_cases_Leaf* v) { + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_opt_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_edge_cases_Inner* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_edge_cases_Inner_write(w, &(*v)); + } +} + +static inline void edge_cases_opt_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_edge_cases_Inner** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + edge_cases_edge_cases_Inner* value = (edge_cases_edge_cases_Inner*)calloc(1, sizeof(edge_cases_edge_cases_Inner)); + if (value == NULL) { + r->failed = true; + return; + } + edge_cases_edge_cases_Inner_read(r, &(*value)); + *out = value; +} + +static inline bool edge_cases_opt_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Inner** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_edge_cases_Inner_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_edge_cases_Inner_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_edge_cases_Inner_free(edge_cases_edge_cases_Inner** v) { + if (*v == NULL) { + return; + } + edge_cases_edge_cases_Inner_free(&(**v)); + free(*v); + *v = NULL; +} + +static inline void edge_cases_map_string_edge_cases_Inner_write(edge_cases_writer* w, const edge_cases_map_string_edge_cases_Inner* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_writer_put_string(w, v->keys[i]); + edge_cases_edge_cases_Inner_write(w, &v->values[i]); + } +} + +static inline void edge_cases_map_string_edge_cases_Inner_read(edge_cases_reader* r, edge_cases_map_string_edge_cases_Inner* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->keys = (edge_cases_str*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_str), 12); + out->values = (edge_cases_edge_cases_Inner*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Inner), 0); + if (out->keys == NULL || out->values == NULL) { + free(out->keys); + free(out->values); + out->keys = NULL; + out->values = NULL; + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->keys[i] = edge_cases_reader_get_string(r); + edge_cases_edge_cases_Inner_read(r, &out->values[i]); + } +} + +static inline bool edge_cases_map_string_edge_cases_Inner_decode(const uint8_t* ptr, size_t len, edge_cases_map_string_edge_cases_Inner* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_map_string_edge_cases_Inner_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_map_string_edge_cases_Inner_free(out); + return false; + } + return true; +} + +static inline void edge_cases_map_string_edge_cases_Inner_free(edge_cases_map_string_edge_cases_Inner* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_str_free(&v->keys[i]); + edge_cases_edge_cases_Inner_free(&v->values[i]); + } + free(v->keys); + free(v->values); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_list_edge_cases_Mode_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Mode* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_writer_put_i32(w, (int32_t)v->items[i]); + } +} + +static inline void edge_cases_list_edge_cases_Mode_read(edge_cases_reader* r, edge_cases_list_edge_cases_Mode* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (edge_cases_edge_cases_Mode*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Mode), 4); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->items[i] = (edge_cases_edge_cases_Mode)edge_cases_reader_get_i32(r); + } +} + +static inline bool edge_cases_list_edge_cases_Mode_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Mode* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_edge_cases_Mode_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_edge_cases_Mode_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_edge_cases_Mode_free(edge_cases_list_edge_cases_Mode* v) { + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_list_i32_write(edge_cases_writer* w, const edge_cases_list_i32* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_writer_put_i32(w, v->items[i]); + } +} + +static inline void edge_cases_list_i32_read(edge_cases_reader* r, edge_cases_list_i32* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (int32_t*)edge_cases_reader_alloc(r, n, sizeof(int32_t), 4); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->items[i] = edge_cases_reader_get_i32(r); + } +} + +static inline bool edge_cases_list_i32_decode(const uint8_t* ptr, size_t len, edge_cases_list_i32* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_i32_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_i32_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_i32_free(edge_cases_list_i32* v) { + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_list_list_i32_write(edge_cases_writer* w, const edge_cases_list_list_i32* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_list_i32_write(w, &v->items[i]); + } +} + +static inline void edge_cases_list_list_i32_read(edge_cases_reader* r, edge_cases_list_list_i32* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (edge_cases_list_i32*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_list_i32), 4); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + edge_cases_list_i32_read(r, &out->items[i]); + } +} + +static inline bool edge_cases_list_list_i32_decode(const uint8_t* ptr, size_t len, edge_cases_list_list_i32* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_list_i32_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_list_i32_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_list_i32_free(edge_cases_list_list_i32* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_list_i32_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_opt_i64_write(edge_cases_writer* w, const int64_t* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_writer_put_i64(w, (*v)); + } +} + +static inline void edge_cases_opt_i64_read(edge_cases_reader* r, int64_t** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + int64_t* value = (int64_t*)calloc(1, sizeof(int64_t)); + if (value == NULL) { + r->failed = true; + return; + } + (*value) = edge_cases_reader_get_i64(r); + *out = value; +} + +static inline bool edge_cases_opt_i64_decode(const uint8_t* ptr, size_t len, int64_t** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_i64_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_i64_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_i64_free(int64_t** v) { + if (*v == NULL) { + return; + } + free(*v); + *v = NULL; +} + +static inline void edge_cases_list_opt_i64_write(edge_cases_writer* w, const edge_cases_list_opt_i64* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_opt_i64_write(w, v->items[i]); + } +} + +static inline void edge_cases_list_opt_i64_read(edge_cases_reader* r, edge_cases_list_opt_i64* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (int64_t**)edge_cases_reader_alloc(r, n, sizeof(int64_t*), 1); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + edge_cases_opt_i64_read(r, &out->items[i]); + } +} + +static inline bool edge_cases_list_opt_i64_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_i64* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_opt_i64_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_opt_i64_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_opt_i64_free(edge_cases_list_opt_i64* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_opt_i64_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_opt_bytes_write(edge_cases_writer* w, const edge_cases_bytes* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_writer_put_bytes(w, (*v)); + } +} + +static inline void edge_cases_opt_bytes_read(edge_cases_reader* r, edge_cases_bytes** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + edge_cases_bytes* value = (edge_cases_bytes*)calloc(1, sizeof(edge_cases_bytes)); + if (value == NULL) { + r->failed = true; + return; + } + (*value) = edge_cases_reader_get_bytes(r); + *out = value; +} + +static inline bool edge_cases_opt_bytes_decode(const uint8_t* ptr, size_t len, edge_cases_bytes** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_bytes_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_bytes_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_bytes_free(edge_cases_bytes** v) { + if (*v == NULL) { + return; + } + edge_cases_bytes_free(&(**v)); + free(*v); + *v = NULL; +} + +static inline void edge_cases_opt_string_write(edge_cases_writer* w, const edge_cases_str* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_writer_put_string(w, (*v)); + } +} + +static inline void edge_cases_opt_string_read(edge_cases_reader* r, edge_cases_str** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + edge_cases_str* value = (edge_cases_str*)calloc(1, sizeof(edge_cases_str)); + if (value == NULL) { + r->failed = true; + return; + } + (*value) = edge_cases_reader_get_string(r); + *out = value; +} + +static inline bool edge_cases_opt_string_decode(const uint8_t* ptr, size_t len, edge_cases_str** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_string_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_string_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_string_free(edge_cases_str** v) { + if (*v == NULL) { + return; + } + edge_cases_str_free(&(**v)); + free(*v); + *v = NULL; +} + +static inline void edge_cases_map_i64_string_write(edge_cases_writer* w, const edge_cases_map_i64_string* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_writer_put_i64(w, v->keys[i]); + edge_cases_writer_put_string(w, v->values[i]); + } +} + +static inline void edge_cases_map_i64_string_read(edge_cases_reader* r, edge_cases_map_i64_string* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->keys = (int64_t*)edge_cases_reader_alloc(r, n, sizeof(int64_t), 12); + out->values = (edge_cases_str*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_str), 0); + if (out->keys == NULL || out->values == NULL) { + free(out->keys); + free(out->values); + out->keys = NULL; + out->values = NULL; + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->keys[i] = edge_cases_reader_get_i64(r); + out->values[i] = edge_cases_reader_get_string(r); + } +} + +static inline bool edge_cases_map_i64_string_decode(const uint8_t* ptr, size_t len, edge_cases_map_i64_string* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_map_i64_string_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_map_i64_string_free(out); + return false; + } + return true; +} + +static inline void edge_cases_map_i64_string_free(edge_cases_map_i64_string* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_str_free(&v->values[i]); + } + free(v->keys); + free(v->values); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_opt_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_edge_cases_Widget* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_writer_put_object(w, edge_cases_edge_cases_Widget_clone(v)); + } +} + +static inline void edge_cases_opt_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_edge_cases_Widget** out) { + *out = NULL; + if (edge_cases_reader_get_bool(r)) { + *out = (edge_cases_edge_cases_Widget*)edge_cases_reader_get_object(r); + } +} + +static inline bool edge_cases_opt_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Widget** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_edge_cases_Widget_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_edge_cases_Widget_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_edge_cases_Widget_free(edge_cases_edge_cases_Widget** v) { + edge_cases_edge_cases_Widget_destroy(*v); + *v = NULL; +} + +static inline void edge_cases_list_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Widget* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_writer_put_object(w, edge_cases_edge_cases_Widget_clone(v->items[i])); + } +} + +static inline void edge_cases_list_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_list_edge_cases_Widget* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (edge_cases_edge_cases_Widget**)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Widget*), 8); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->items[i] = (edge_cases_edge_cases_Widget*)edge_cases_reader_get_object(r); + } +} + +static inline bool edge_cases_list_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Widget* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_edge_cases_Widget_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_edge_cases_Widget_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_edge_cases_Widget_free(edge_cases_list_edge_cases_Widget* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_edge_cases_Widget_destroy(v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_map_string_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_map_string_edge_cases_Widget* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_writer_put_string(w, v->keys[i]); + edge_cases_writer_put_object(w, edge_cases_edge_cases_Widget_clone(v->values[i])); + } +} + +static inline void edge_cases_map_string_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_map_string_edge_cases_Widget* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->keys = (edge_cases_str*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_str), 12); + out->values = (edge_cases_edge_cases_Widget**)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Widget*), 0); + if (out->keys == NULL || out->values == NULL) { + free(out->keys); + free(out->values); + out->keys = NULL; + out->values = NULL; + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->keys[i] = edge_cases_reader_get_string(r); + out->values[i] = (edge_cases_edge_cases_Widget*)edge_cases_reader_get_object(r); + } +} + +static inline bool edge_cases_map_string_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_map_string_edge_cases_Widget* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_map_string_edge_cases_Widget_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_map_string_edge_cases_Widget_free(out); + return false; + } + return true; +} + +static inline void edge_cases_map_string_edge_cases_Widget_free(edge_cases_map_string_edge_cases_Widget* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_str_free(&v->keys[i]); + edge_cases_edge_cases_Widget_destroy(v->values[i]); + } + free(v->keys); + free(v->values); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_list_string_write(edge_cases_writer* w, const edge_cases_list_string* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_writer_put_string(w, v->items[i]); + } +} + +static inline void edge_cases_list_string_read(edge_cases_reader* r, edge_cases_list_string* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (edge_cases_str*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_str), 4); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->items[i] = edge_cases_reader_get_string(r); + } +} + +static inline bool edge_cases_list_string_decode(const uint8_t* ptr, size_t len, edge_cases_list_string* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_string_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_string_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_string_free(edge_cases_list_string* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_str_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_opt_i32_write(edge_cases_writer* w, const int32_t* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_writer_put_i32(w, (*v)); + } +} + +static inline void edge_cases_opt_i32_read(edge_cases_reader* r, int32_t** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + int32_t* value = (int32_t*)calloc(1, sizeof(int32_t)); + if (value == NULL) { + r->failed = true; + return; + } + (*value) = edge_cases_reader_get_i32(r); + *out = value; +} + +static inline bool edge_cases_opt_i32_decode(const uint8_t* ptr, size_t len, int32_t** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_i32_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_i32_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_i32_free(int32_t** v) { + if (*v == NULL) { + return; + } + free(*v); + *v = NULL; +} + +static inline void edge_cases_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_edge_cases_Outer* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_edge_cases_Outer_write(w, &(*v)); + } +} + +static inline void edge_cases_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_edge_cases_Outer** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + edge_cases_edge_cases_Outer* value = (edge_cases_edge_cases_Outer*)calloc(1, sizeof(edge_cases_edge_cases_Outer)); + if (value == NULL) { + r->failed = true; + return; + } + edge_cases_edge_cases_Outer_read(r, &(*value)); + *out = value; +} + +static inline bool edge_cases_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Outer** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_edge_cases_Outer_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_edge_cases_Outer_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_edge_cases_Outer_free(edge_cases_edge_cases_Outer** v) { + if (*v == NULL) { + return; + } + edge_cases_edge_cases_Outer_free(&(**v)); + free(*v); + *v = NULL; +} + +static inline void edge_cases_list_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Outer* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_opt_edge_cases_Outer_write(w, v->items[i]); + } +} + +static inline void edge_cases_list_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Outer* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (edge_cases_edge_cases_Outer**)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Outer*), 1); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + edge_cases_opt_edge_cases_Outer_read(r, &out->items[i]); + } +} + +static inline bool edge_cases_list_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Outer* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_opt_edge_cases_Outer_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_opt_edge_cases_Outer_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_opt_edge_cases_Outer_free(edge_cases_list_opt_edge_cases_Outer* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_opt_edge_cases_Outer_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_opt_list_opt_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Outer* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_list_opt_edge_cases_Outer_write(w, &(*v)); + } +} + +static inline void edge_cases_opt_list_opt_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Outer** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + edge_cases_list_opt_edge_cases_Outer* value = (edge_cases_list_opt_edge_cases_Outer*)calloc(1, sizeof(edge_cases_list_opt_edge_cases_Outer)); + if (value == NULL) { + r->failed = true; + return; + } + edge_cases_list_opt_edge_cases_Outer_read(r, &(*value)); + *out = value; +} + +static inline bool edge_cases_opt_list_opt_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Outer** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_list_opt_edge_cases_Outer_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_list_opt_edge_cases_Outer_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_list_opt_edge_cases_Outer_free(edge_cases_list_opt_edge_cases_Outer** v) { + if (*v == NULL) { + return; + } + edge_cases_list_opt_edge_cases_Outer_free(&(**v)); + free(*v); + *v = NULL; +} + +static inline void edge_cases_opt_list_string_write(edge_cases_writer* w, const edge_cases_list_string* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_list_string_write(w, &(*v)); + } +} + +static inline void edge_cases_opt_list_string_read(edge_cases_reader* r, edge_cases_list_string** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + edge_cases_list_string* value = (edge_cases_list_string*)calloc(1, sizeof(edge_cases_list_string)); + if (value == NULL) { + r->failed = true; + return; + } + edge_cases_list_string_read(r, &(*value)); + *out = value; +} + +static inline bool edge_cases_opt_list_string_decode(const uint8_t* ptr, size_t len, edge_cases_list_string** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_list_string_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_list_string_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_list_string_free(edge_cases_list_string** v) { + if (*v == NULL) { + return; + } + edge_cases_list_string_free(&(**v)); + free(*v); + *v = NULL; +} + +static inline void edge_cases_opt_edge_cases_Mode_write(edge_cases_writer* w, const edge_cases_edge_cases_Mode* v) { + edge_cases_writer_put_bool(w, v != NULL); + if (v != NULL) { + edge_cases_writer_put_i32(w, (int32_t)(*v)); + } +} + +static inline void edge_cases_opt_edge_cases_Mode_read(edge_cases_reader* r, edge_cases_edge_cases_Mode** out) { + *out = NULL; + if (!edge_cases_reader_get_bool(r)) { + return; + } + edge_cases_edge_cases_Mode* value = (edge_cases_edge_cases_Mode*)calloc(1, sizeof(edge_cases_edge_cases_Mode)); + if (value == NULL) { + r->failed = true; + return; + } + (*value) = (edge_cases_edge_cases_Mode)edge_cases_reader_get_i32(r); + *out = value; +} + +static inline bool edge_cases_opt_edge_cases_Mode_decode(const uint8_t* ptr, size_t len, edge_cases_edge_cases_Mode** out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_opt_edge_cases_Mode_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_opt_edge_cases_Mode_free(out); + return false; + } + return true; +} + +static inline void edge_cases_opt_edge_cases_Mode_free(edge_cases_edge_cases_Mode** v) { + if (*v == NULL) { + return; + } + free(*v); + *v = NULL; +} + +static inline void edge_cases_list_opt_edge_cases_Widget_write(edge_cases_writer* w, const edge_cases_list_opt_edge_cases_Widget* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_opt_edge_cases_Widget_write(w, v->items[i]); + } +} + +static inline void edge_cases_list_opt_edge_cases_Widget_read(edge_cases_reader* r, edge_cases_list_opt_edge_cases_Widget* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (edge_cases_edge_cases_Widget**)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Widget*), 1); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + edge_cases_opt_edge_cases_Widget_read(r, &out->items[i]); + } +} + +static inline bool edge_cases_list_opt_edge_cases_Widget_decode(const uint8_t* ptr, size_t len, edge_cases_list_opt_edge_cases_Widget* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_opt_edge_cases_Widget_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_opt_edge_cases_Widget_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_opt_edge_cases_Widget_free(edge_cases_list_opt_edge_cases_Widget* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_opt_edge_cases_Widget_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void edge_cases_list_edge_cases_Outer_write(edge_cases_writer* w, const edge_cases_list_edge_cases_Outer* v) { + edge_cases_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + edge_cases_edge_cases_Outer_write(w, &v->items[i]); + } +} + +static inline void edge_cases_list_edge_cases_Outer_read(edge_cases_reader* r, edge_cases_list_edge_cases_Outer* out) { + memset(out, 0, sizeof *out); + size_t n = edge_cases_reader_get_u32(r); + out->items = (edge_cases_edge_cases_Outer*)edge_cases_reader_alloc(r, n, sizeof(edge_cases_edge_cases_Outer), 33); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + edge_cases_edge_cases_Outer_read(r, &out->items[i]); + } +} + +static inline bool edge_cases_list_edge_cases_Outer_decode(const uint8_t* ptr, size_t len, edge_cases_list_edge_cases_Outer* out) { + edge_cases_reader r = edge_cases_reader_of(ptr, len); + edge_cases_list_edge_cases_Outer_read(&r, out); + if (!edge_cases_reader_finish(&r)) { + edge_cases_list_edge_cases_Outer_free(out); + return false; + } + return true; +} + +static inline void edge_cases_list_edge_cases_Outer_free(edge_cases_list_edge_cases_Outer* v) { + for (size_t i = 0; i < v->len; i++) { + edge_cases_edge_cases_Outer_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +#ifdef __cplusplus +} +#endif + +#endif // EDGE_CASES_BUFFER_H + +// END edge_cases_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_h.snap b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_h.snap new file mode 100644 index 00000000..94864086 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__edge_cases_h.snap @@ -0,0 +1,245 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef EDGE_CASES_H +#define EDGE_CASES_H + +#include +#include +#include + +#ifndef EDGE_CASES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef EDGE_CASES_BUILD +# define EDGE_CASES_API __declspec(dllexport) +# else +# define EDGE_CASES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define EDGE_CASES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define EDGE_CASES_API __attribute__((visibility("default"))) +# else +# define EDGE_CASES_API +# endif +#endif + +#ifndef EDGE_CASES_DEPRECATED +# if defined(_MSC_VER) +# define EDGE_CASES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define EDGE_CASES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define EDGE_CASES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports edge_cases_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define EDGE_CASES_ABI_VERSION 3u +EDGE_CASES_API uint32_t edge_cases_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by edge_cases_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct edge_cases_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} edge_cases_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +EDGE_CASES_API void edge_cases_error_set(edge_cases_error* err, int32_t code, const char* message); +EDGE_CASES_API void edge_cases_error_clear(edge_cases_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + edge_cases_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +EDGE_CASES_API void edge_cases_error_free(edge_cases_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +EDGE_CASES_API void edge_cases_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct edge_cases_cancel_token edge_cases_cancel_token; +EDGE_CASES_API edge_cases_cancel_token* edge_cases_cancel_token_create(void); +EDGE_CASES_API void edge_cases_cancel_token_cancel(edge_cases_cancel_token* token); +EDGE_CASES_API bool edge_cases_cancel_token_is_cancelled(const edge_cases_cancel_token* token); +EDGE_CASES_API void edge_cases_cancel_token_destroy(edge_cases_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +EDGE_CASES_API uint8_t* edge_cases_alloc(uint32_t size); +EDGE_CASES_API void edge_cases_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +EDGE_CASES_API uint64_t edge_cases_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define EDGE_CASES_EDGE_CASES_CHECKSUM 0x1567f441073bd4aaull +EDGE_CASES_API uint64_t edge_cases_edge_cases_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with edge_cases_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * edge_cases_buffer.h declares a C struct and codec for each. + */ + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +typedef enum { edge_cases_edge_cases_Legacy_None = 0, edge_cases_edge_cases_Legacy_Default = 5, edge_cases_edge_cases_Legacy_Class = 10 } edge_cases_edge_cases_Legacy; +typedef enum { edge_cases_edge_cases_Mode_Fast = 1, edge_cases_edge_cases_Mode_Safe = 2 } edge_cases_edge_cases_Mode; +/** Error codes reported by throwing functions in the `edge_cases` module tree. */ +typedef enum { edge_cases_edge_cases_EdgeErrors_Busy = 7 } edge_cases_edge_cases_EdgeErrors; +typedef struct edge_cases_edge_cases_Widget edge_cases_edge_cases_Widget; +typedef struct edge_cases_edge_cases_Vault edge_cases_edge_cases_Vault; +typedef struct edge_cases_edge_cases_WidgetStreamIterator edge_cases_edge_cases_WidgetStreamIterator; +typedef struct edge_cases_edge_cases_StringStreamIterator edge_cases_edge_cases_StringStreamIterator; +typedef struct edge_cases_edge_cases_ScalarStreamIterator edge_cases_edge_cases_ScalarStreamIterator; +typedef struct edge_cases_edge_cases_ModeStreamIterator edge_cases_edge_cases_ModeStreamIterator; +/** + * Callback interface carrying composite payloads and objects + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_BatchListener_vtable { + /** Composite payloads cross as value buffers */ + void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); + /** Objects cross as strong references the consumer must release */ + void (*on_widget)(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); + void (*on_tick)(void* ctx, edge_cases_error* out_err); + /** Scalar return travels back through the vtable */ + bool (*should_continue)(void* ctx, int32_t class_, edge_cases_error* out_err); + /** Enum return travels back as its integer discriminant */ + edge_cases_edge_cases_Mode (*pick_mode)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_BatchListener_vtable; +/** + * Callback interface whose name and methods are keywords + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_Keyword_vtable { + void (*delete_)(void* ctx, int32_t self, edge_cases_error* out_err); + int32_t (*import)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_Keyword_vtable; +typedef void (*edge_cases_edge_cases_async_widget_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result); +typedef void (*edge_cases_edge_cases_async_void_callback)(void* context, edge_cases_error* err); +typedef void (*edge_cases_edge_cases_async_bytes_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_struct_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_list_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_optional_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_enum_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result); +typedef void (*edge_cases_edge_cases_async_keywords_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_Widget_fetch_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: edge_cases +/** Interface with no constructor; instances come from make_widget */ +/** Void method that can fail */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_refresh(const edge_cases_edge_cases_Widget* self, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_rename(const edge_cases_edge_cases_Widget* self, const uint8_t* class_ptr, size_t class_len, const uint8_t* import_ptr, size_t import_len, size_t* out_len, edge_cases_error* out_err); +/** Async method with a struct result */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_fetch(const edge_cases_edge_cases_Widget* self, int64_t id, edge_cases_edge_cases_Widget_fetch_callback callback, void* context); +EDGE_CASES_DEPRECATED("Use rename instead") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_old_name(const edge_cases_edge_cases_Widget* self, size_t* out_len, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Widget_clone(const edge_cases_edge_cases_Widget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_destroy(edge_cases_edge_cases_Widget* self); + +/** Deprecated interface with a throwing constructor */ +/** Fails when the vault is busy */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_open(const uint8_t* path_ptr, size_t path_len, edge_cases_error* out_err); +/** Throwing static */ +EDGE_CASES_API bool edge_cases_edge_cases_Vault_probe(edge_cases_error* out_err); +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Vault_peek(const edge_cases_edge_cases_Vault* self, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_clone(const edge_cases_edge_cases_Vault* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Vault_destroy(edge_cases_edge_cases_Vault* self); + +/** No parameters and no result */ +EDGE_CASES_API void edge_cases_edge_cases_noop(edge_cases_error* out_err); +/** Function named after a keyword whose parameters are all keywords */ +EDGE_CASES_API int32_t edge_cases_edge_cases_select(int32_t class_, const uint8_t* from_ptr, size_t from_len, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, edge_cases_error* out_err); +/** Optional list of optional structs, the deepest legal nesting */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_optional_params(const uint8_t* text_ptr, size_t text_len, const uint8_t* count_ptr, size_t count_len, const uint8_t* outer_ptr, size_t outer_len, const uint8_t* tags_ptr, size_t tags_len, const uint8_t* blob_ptr, size_t blob_len, const uint8_t* mode_ptr, size_t mode_len, size_t* out_len, edge_cases_error* out_err); +/** Two callback interface parameters on one function */ +EDGE_CASES_API void edge_cases_edge_cases_register_batch(void* listener_ctx, const edge_cases_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const edge_cases_edge_cases_Keyword_vtable* keyword_vtable, edge_cases_error* out_err); +/** Objects in every buffered position */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, edge_cases_error* out_err); +/** Iterator of objects */ +EDGE_CASES_API edge_cases_edge_cases_WidgetStreamIterator* edge_cases_edge_cases_widget_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_WidgetStreamIterator_next(edge_cases_edge_cases_WidgetStreamIterator* iter, edge_cases_edge_cases_Widget** out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_WidgetStreamIterator_destroy(edge_cases_edge_cases_WidgetStreamIterator* iter); +/** Async returning an object */ +EDGE_CASES_API void edge_cases_edge_cases_async_widget(edge_cases_edge_cases_async_widget_callback callback, void* context); +/** Factory for an interface with no constructor */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_make_widget(const uint8_t* label_ptr, size_t label_len, edge_cases_error* out_err); +/** Optional interface return */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_find_widget(int64_t id, edge_cases_error* out_err); +/** Interface parameter, including an optional one */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_inspect_widget(const edge_cases_edge_cases_Widget* widget, const edge_cases_edge_cases_Widget* fallback, size_t* out_len, edge_cases_error* out_err); +/** Iterator of strings */ +EDGE_CASES_API edge_cases_edge_cases_StringStreamIterator* edge_cases_edge_cases_string_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_StringStreamIterator_next(edge_cases_edge_cases_StringStreamIterator* iter, const uint8_t** out_item, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_StringStreamIterator_destroy(edge_cases_edge_cases_StringStreamIterator* iter); +/** Iterator of scalars */ +EDGE_CASES_API edge_cases_edge_cases_ScalarStreamIterator* edge_cases_edge_cases_scalar_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ScalarStreamIterator_next(edge_cases_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ScalarStreamIterator_destroy(edge_cases_edge_cases_ScalarStreamIterator* iter); +/** Iterator of enums */ +EDGE_CASES_API edge_cases_edge_cases_ModeStreamIterator* edge_cases_edge_cases_mode_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ModeStreamIterator_next(edge_cases_edge_cases_ModeStreamIterator* iter, edge_cases_edge_cases_Mode* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ModeStreamIterator_destroy(edge_cases_edge_cases_ModeStreamIterator* iter); +/** Async with no result */ +EDGE_CASES_API void edge_cases_edge_cases_async_void(edge_cases_edge_cases_async_void_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_bytes(uint32_t size, edge_cases_edge_cases_async_bytes_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_struct(edge_cases_edge_cases_async_struct_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_list(edge_cases_cancel_token* cancel_token, edge_cases_edge_cases_async_list_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_optional(edge_cases_edge_cases_async_optional_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_enum(edge_cases_edge_cases_async_enum_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_keywords(const uint8_t* class_ptr, size_t class_len, int32_t import, edge_cases_edge_cases_async_keywords_callback callback, void* context); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_DEPRECATED("Use Mode-based APIs") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy value, size_t* out_len, edge_cases_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // EDGE_CASES_H + +// END edge_cases.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__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_edge_cases__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_h.snap deleted file mode 100644 index 4b4e647b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_h.snap +++ /dev/null @@ -1,231 +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 - -/* 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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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_WidgetStreamIterator weaveffi_edge_cases_WidgetStreamIterator; -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; -/** - * Callback interface carrying composite payloads and objects - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. - */ -typedef struct weaveffi_edge_cases_BatchListener_vtable { - /** Composite payloads cross as value buffers */ - void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, weaveffi_edge_cases_Mode mode, weaveffi_error* out_err); - /** Objects cross as strong references the consumer must release */ - void (*on_widget)(void* ctx, weaveffi_edge_cases_Widget* widget, weaveffi_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, weaveffi_error* out_err); - void (*on_tick)(void* ctx, weaveffi_error* out_err); - /** Scalar return travels back through the vtable */ - bool (*should_continue)(void* ctx, int32_t class_, weaveffi_error* out_err); - /** Enum return travels back as its integer discriminant */ - weaveffi_edge_cases_Mode (*pick_mode)(void* ctx, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_edge_cases_BatchListener_vtable; -/** - * Callback interface whose name and methods are keywords - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. - */ -typedef struct weaveffi_edge_cases_Keyword_vtable { - void (*delete_)(void* ctx, int32_t self, weaveffi_error* out_err); - int32_t (*import)(void* ctx, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_edge_cases_Keyword_vtable; -typedef void (*weaveffi_edge_cases_async_widget_callback)(void* context, weaveffi_error* err, weaveffi_edge_cases_Widget* result); -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); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_edge_cases_Widget* weaveffi_edge_cases_Widget_clone(const weaveffi_edge_cases_Widget* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -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); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_edge_cases_Vault* weaveffi_edge_cases_Vault_clone(const weaveffi_edge_cases_Vault* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_edge_cases_Vault_destroy(weaveffi_edge_cases_Vault* self); - -/** 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); -/** Two callback interface parameters on one function */ -WEAVEFFI_API void weaveffi_edge_cases_register_batch(void* listener_ctx, const weaveffi_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const weaveffi_edge_cases_Keyword_vtable* keyword_vtable, weaveffi_error* out_err); -/** Objects in every buffered position */ -WEAVEFFI_API const uint8_t* weaveffi_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, weaveffi_error* out_err); -/** Iterator of objects */ -WEAVEFFI_API weaveffi_edge_cases_WidgetStreamIterator* weaveffi_edge_cases_widget_stream(weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_edge_cases_WidgetStreamIterator_next(weaveffi_edge_cases_WidgetStreamIterator* iter, weaveffi_edge_cases_Widget** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_edge_cases_WidgetStreamIterator_destroy(weaveffi_edge_cases_WidgetStreamIterator* iter); -/** Async returning an object */ -WEAVEFFI_API void weaveffi_edge_cases_async_widget_async(weaveffi_edge_cases_async_widget_callback callback, void* context); -/** 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); -/** 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_kitchen_sink__kitchen_sink_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_buffer_h.snap new file mode 100644 index 00000000..0737cc41 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_buffer_h.snap @@ -0,0 +1,898 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef KITCHEN_SINK_BUFFER_H +#define KITCHEN_SINK_BUFFER_H + +#include "kitchen_sink.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Value buffer runtime. + * + * The wire format is little-endian and packed: fixed-width scalars at their + * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length + * followed by the raw bytes (no NUL terminator), optionals as a presence byte + * followed by the value, lists as a `uint32_t` count followed by the + * elements, maps as a `uint32_t` count followed by alternating keys and + * values, and objects as a `uint64_t` token holding one strong reference. + * ------------------------------------------------------------------------- */ + +/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. + A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be + passed to C string functions when the text has no interior NUL. */ +typedef struct kitchen_sink_str { + const char* ptr; + size_t len; +} kitchen_sink_str; + +/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ +typedef struct kitchen_sink_bytes { + const uint8_t* ptr; + size_t len; +} kitchen_sink_bytes; + +/* Borrow a NUL-terminated C string (NULL is the empty string). */ +static inline kitchen_sink_str kitchen_sink_str_of(const char* s) { + kitchen_sink_str v; + v.ptr = s; + v.len = s ? strlen(s) : 0; + return v; +} + +/* Borrow `len` bytes at `ptr`. */ +static inline kitchen_sink_bytes kitchen_sink_bytes_of(const uint8_t* ptr, size_t len) { + kitchen_sink_bytes v; + v.ptr = ptr; + v.len = len; + return v; +} + +/* Release a decoded string. Never call this on a borrowed view. */ +static inline void kitchen_sink_str_free(kitchen_sink_str* s) { + free((void*)s->ptr); + s->ptr = NULL; + s->len = 0; +} + +/* Release decoded bytes. Never call this on a borrowed view. */ +static inline void kitchen_sink_bytes_free(kitchen_sink_bytes* b) { + free((void*)b->ptr); + b->ptr = NULL; + b->len = 0; +} + +/* A growable output buffer. Zero-initialize it, write values, pass + `ptr`/`len` to the call, then release it with kitchen_sink_writer_free. An + allocation failure sets `failed` and turns later writes into no-ops. */ +typedef struct kitchen_sink_writer { + uint8_t* ptr; + size_t len; + size_t cap; + bool failed; +} kitchen_sink_writer; + +static inline void kitchen_sink_writer_free(kitchen_sink_writer* w) { + free(w->ptr); + w->ptr = NULL; + w->len = 0; + w->cap = 0; + w->failed = false; +} + +static inline void kitchen_sink_writer_put_raw(kitchen_sink_writer* w, const void* data, size_t n) { + if (w->failed || n == 0) { + return; + } + if (n > w->cap - w->len) { + size_t cap = w->cap ? w->cap : 64; + while (cap - w->len < n) { + if (cap > SIZE_MAX / 2) { + w->failed = true; + return; + } + cap *= 2; + } + uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); + if (grown == NULL) { + w->failed = true; + return; + } + w->ptr = grown; + w->cap = cap; + } + memcpy(w->ptr + w->len, data, n); + w->len += n; +} + +static inline void kitchen_sink_writer_put_u64(kitchen_sink_writer* w, uint64_t v) { + uint8_t b[8]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)(v >> (8 * i)); + } + kitchen_sink_writer_put_raw(w, b, 8); +} + +static inline void kitchen_sink_writer_put_u32(kitchen_sink_writer* w, uint32_t v) { + uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; + kitchen_sink_writer_put_raw(w, b, 4); +} + +static inline void kitchen_sink_writer_put_u16(kitchen_sink_writer* w, uint16_t v) { + uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; + kitchen_sink_writer_put_raw(w, b, 2); +} + +static inline void kitchen_sink_writer_put_u8(kitchen_sink_writer* w, uint8_t v) { + kitchen_sink_writer_put_raw(w, &v, 1); +} + +static inline void kitchen_sink_writer_put_i8(kitchen_sink_writer* w, int8_t v) { + kitchen_sink_writer_put_u8(w, (uint8_t)v); +} + +static inline void kitchen_sink_writer_put_i16(kitchen_sink_writer* w, int16_t v) { + kitchen_sink_writer_put_u16(w, (uint16_t)v); +} + +static inline void kitchen_sink_writer_put_i32(kitchen_sink_writer* w, int32_t v) { + kitchen_sink_writer_put_u32(w, (uint32_t)v); +} + +static inline void kitchen_sink_writer_put_i64(kitchen_sink_writer* w, int64_t v) { + kitchen_sink_writer_put_u64(w, (uint64_t)v); +} + +static inline void kitchen_sink_writer_put_bool(kitchen_sink_writer* w, bool v) { + kitchen_sink_writer_put_u8(w, v ? 1 : 0); +} + +static inline void kitchen_sink_writer_put_f32(kitchen_sink_writer* w, float v) { + uint32_t bits; + memcpy(&bits, &v, 4); + kitchen_sink_writer_put_u32(w, bits); +} + +static inline void kitchen_sink_writer_put_f64(kitchen_sink_writer* w, double v) { + uint64_t bits; + memcpy(&bits, &v, 8); + kitchen_sink_writer_put_u64(w, bits); +} + +static inline void kitchen_sink_writer_put_len(kitchen_sink_writer* w, size_t n) { + if (n > UINT32_MAX) { + w->failed = true; + return; + } + kitchen_sink_writer_put_u32(w, (uint32_t)n); +} + +static inline void kitchen_sink_writer_put_string(kitchen_sink_writer* w, kitchen_sink_str v) { + kitchen_sink_writer_put_len(w, v.len); + kitchen_sink_writer_put_raw(w, v.ptr, v.len); +} + +static inline void kitchen_sink_writer_put_bytes(kitchen_sink_writer* w, kitchen_sink_bytes v) { + kitchen_sink_writer_put_len(w, v.len); + kitchen_sink_writer_put_raw(w, v.ptr, v.len); +} + +/* Write an object token. The token carries one strong reference, so pass + a reference the buffer may own (the generated codecs pass a fresh + `_clone`). */ +static inline void kitchen_sink_writer_put_object(kitchen_sink_writer* w, const void* owned) { + kitchen_sink_writer_put_u64(w, (uint64_t)(uintptr_t)owned); +} + +/* A cursor over an input buffer. A malformed buffer sets `failed`; every + later read then returns a zero value, so a decoder can always finish and + release what it built. */ +typedef struct kitchen_sink_reader { + const uint8_t* ptr; + size_t len; + size_t pos; + bool failed; +} kitchen_sink_reader; + +static inline kitchen_sink_reader kitchen_sink_reader_of(const uint8_t* ptr, size_t len) { + kitchen_sink_reader r; + r.ptr = ptr; + r.len = ptr ? len : 0; + r.pos = 0; + r.failed = false; + return r; +} + +/* `true` when every byte was consumed and nothing was malformed. */ +static inline bool kitchen_sink_reader_finish(const kitchen_sink_reader* r) { + return !r->failed && r->pos == r->len; +} + +static inline const uint8_t* kitchen_sink_reader_take(kitchen_sink_reader* r, size_t n) { + if (r->failed || n > r->len - r->pos) { + r->failed = true; + return NULL; + } + const uint8_t* at = r->ptr + r->pos; + r->pos += n; + return at; +} + +/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is + zero). `min_wire` is the smallest encoding of one element; a count the + remaining bytes can't hold fails the reader before anything is + allocated. */ +static inline void* kitchen_sink_reader_alloc(kitchen_sink_reader* r, size_t n, size_t size, size_t min_wire) { + if (r->failed || n == 0) { + return NULL; + } + if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { + r->failed = true; + return NULL; + } + void* p = calloc(n, size); + if (p == NULL) { + r->failed = true; + } + return p; +} + +static inline uint64_t kitchen_sink_reader_get_u64(kitchen_sink_reader* r) { + const uint8_t* b = kitchen_sink_reader_take(r, 8); + uint64_t v = 0; + if (b != NULL) { + for (int i = 0; i < 8; i++) { + v |= (uint64_t)b[i] << (8 * i); + } + } + return v; +} + +static inline uint32_t kitchen_sink_reader_get_u32(kitchen_sink_reader* r) { + const uint8_t* b = kitchen_sink_reader_take(r, 4); + if (b == NULL) { + return 0; + } + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static inline uint16_t kitchen_sink_reader_get_u16(kitchen_sink_reader* r) { + const uint8_t* b = kitchen_sink_reader_take(r, 2); + return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; +} + +static inline uint8_t kitchen_sink_reader_get_u8(kitchen_sink_reader* r) { + const uint8_t* b = kitchen_sink_reader_take(r, 1); + return b ? b[0] : (uint8_t)0; +} + +static inline int8_t kitchen_sink_reader_get_i8(kitchen_sink_reader* r) { + return (int8_t)kitchen_sink_reader_get_u8(r); +} + +static inline int16_t kitchen_sink_reader_get_i16(kitchen_sink_reader* r) { + return (int16_t)kitchen_sink_reader_get_u16(r); +} + +static inline int32_t kitchen_sink_reader_get_i32(kitchen_sink_reader* r) { + return (int32_t)kitchen_sink_reader_get_u32(r); +} + +static inline int64_t kitchen_sink_reader_get_i64(kitchen_sink_reader* r) { + return (int64_t)kitchen_sink_reader_get_u64(r); +} + +static inline bool kitchen_sink_reader_get_bool(kitchen_sink_reader* r) { + uint8_t v = kitchen_sink_reader_get_u8(r); + if (v > 1) { + r->failed = true; + return false; + } + return v == 1; +} + +static inline float kitchen_sink_reader_get_f32(kitchen_sink_reader* r) { + uint32_t bits = kitchen_sink_reader_get_u32(r); + float v; + memcpy(&v, &bits, 4); + return v; +} + +static inline double kitchen_sink_reader_get_f64(kitchen_sink_reader* r) { + uint64_t bits = kitchen_sink_reader_get_u64(r); + double v; + memcpy(&v, &bits, 8); + return v; +} + +/* A `uint32_t` length followed by that many bytes, copied into a fresh + NUL-terminated allocation. */ +static inline uint8_t* kitchen_sink_reader_get_run(kitchen_sink_reader* r, size_t* out_len) { + size_t n = kitchen_sink_reader_get_u32(r); + const uint8_t* at = kitchen_sink_reader_take(r, n); + *out_len = 0; + if (at == NULL) { + return NULL; + } + uint8_t* copy = (uint8_t*)malloc(n + 1); + if (copy == NULL) { + r->failed = true; + return NULL; + } + memcpy(copy, at, n); + copy[n] = 0; + *out_len = n; + return copy; +} + +static inline kitchen_sink_str kitchen_sink_reader_get_string(kitchen_sink_reader* r) { + kitchen_sink_str v; + v.ptr = (const char*)kitchen_sink_reader_get_run(r, &v.len); + return v; +} + +static inline kitchen_sink_bytes kitchen_sink_reader_get_bytes(kitchen_sink_reader* r) { + kitchen_sink_bytes v; + v.ptr = kitchen_sink_reader_get_run(r, &v.len); + return v; +} + +/* Read an object token and adopt the strong reference it carries. A zero + token in a required position fails the reader. */ +static inline void* kitchen_sink_reader_get_object(kitchen_sink_reader* r) { + uint64_t token = kitchen_sink_reader_get_u64(r); + if (token == 0) { + r->failed = true; + } + return (void*)(uintptr_t)token; +} + +/* --------------------------------------------------------------------------- + * Value types. A record is a struct of its fields; a rich enum is a `tag` + * plus a union `as` with one struct per payload-carrying variant; a list is + * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an + * optional is a pointer that is NULL when absent; an object is its pointer; + * a string or byte string is a view (`ptr` + `len`). + * ------------------------------------------------------------------------- */ + +typedef struct kitchen_sink_shared_Token kitchen_sink_shared_Token; +typedef struct kitchen_sink_kitchen_Item kitchen_sink_kitchen_Item; +typedef struct kitchen_sink_list_string kitchen_sink_list_string; +typedef struct kitchen_sink_map_string_string kitchen_sink_map_string_string; +typedef struct kitchen_sink_list_kitchen_Gadget kitchen_sink_list_kitchen_Gadget; +typedef struct kitchen_sink_list_kitchen_Item kitchen_sink_list_kitchen_Item; +typedef struct kitchen_sink_map_string_i32 kitchen_sink_map_string_i32; + +/** `[string]` */ +struct kitchen_sink_list_string { + kitchen_sink_str* items; + size_t len; +}; + +/** `{string:string}` */ +struct kitchen_sink_map_string_string { + kitchen_sink_str* keys; + kitchen_sink_str* values; + size_t len; +}; + +/** `[kitchen.Gadget]` */ +struct kitchen_sink_list_kitchen_Gadget { + kitchen_sink_kitchen_Gadget** items; + size_t len; +}; + +/** `[kitchen.Item]` */ +struct kitchen_sink_list_kitchen_Item { + kitchen_sink_kitchen_Item* items; + size_t len; +}; + +/** `{string:i32}` */ +struct kitchen_sink_map_string_i32 { + kitchen_sink_str* keys; + int32_t* values; + size_t len; +}; + +/** A reusable token referenced across modules */ +struct kitchen_sink_shared_Token { + int64_t id; + kitchen_sink_str label; +}; + +/** Kitchen sink struct exercising every field feature */ +struct kitchen_sink_kitchen_Item { + /** Stable identifier */ + int64_t id; + /** Display name */ + kitchen_sink_str name; + /** Initial count */ + int32_t count; + /** Whether the item is enabled */ + bool enabled; + /** Scaling ratio */ + double ratio; + uint32_t nick; + kitchen_sink_bytes payload; + kitchen_sink_list_string tags; + kitchen_sink_map_string_string attrs; + int64_t* parent; + /** Cross-module struct reference */ + kitchen_sink_shared_Token token; + kitchen_sink_kitchen_Priority priority; + /** An optional object inside a record */ + kitchen_sink_kitchen_Gadget* gadget; +}; + +/* --------------------------------------------------------------------------- + * Codecs. Each value type T above, and each optional shape, has four + * functions: + * + * T_write(w, v) appends the value at `v` to writer `w` + * T_read(r, out) decodes one value from reader `r` + * T_decode(ptr, len, out) decodes a whole buffer; on a malformed + * buffer it frees what it built and + * returns false + * T_free(v) releases a decoded value + * + * An optional's T_write takes the nullable pointer itself. Writing an + * object stores a fresh `_clone` reference for the reader to adopt; + * decoding adopts each object reference, and T_free destroys it. T_free + * releases only what T_read or T_decode allocated; never call it on a + * value that borrows caller memory. + * ------------------------------------------------------------------------- */ + +static inline void kitchen_sink_shared_Token_write(kitchen_sink_writer* w, const kitchen_sink_shared_Token* v); +static inline void kitchen_sink_shared_Token_read(kitchen_sink_reader* r, kitchen_sink_shared_Token* out); +static inline bool kitchen_sink_shared_Token_decode(const uint8_t* ptr, size_t len, kitchen_sink_shared_Token* out); +static inline void kitchen_sink_shared_Token_free(kitchen_sink_shared_Token* v); +static inline void kitchen_sink_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v); +static inline void kitchen_sink_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item* out); +static inline bool kitchen_sink_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item* out); +static inline void kitchen_sink_kitchen_Item_free(kitchen_sink_kitchen_Item* v); +static inline void kitchen_sink_list_string_write(kitchen_sink_writer* w, const kitchen_sink_list_string* v); +static inline void kitchen_sink_list_string_read(kitchen_sink_reader* r, kitchen_sink_list_string* out); +static inline bool kitchen_sink_list_string_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_string* out); +static inline void kitchen_sink_list_string_free(kitchen_sink_list_string* v); +static inline void kitchen_sink_map_string_string_write(kitchen_sink_writer* w, const kitchen_sink_map_string_string* v); +static inline void kitchen_sink_map_string_string_read(kitchen_sink_reader* r, kitchen_sink_map_string_string* out); +static inline bool kitchen_sink_map_string_string_decode(const uint8_t* ptr, size_t len, kitchen_sink_map_string_string* out); +static inline void kitchen_sink_map_string_string_free(kitchen_sink_map_string_string* v); +static inline void kitchen_sink_opt_i64_write(kitchen_sink_writer* w, const int64_t* v); +static inline void kitchen_sink_opt_i64_read(kitchen_sink_reader* r, int64_t** out); +static inline bool kitchen_sink_opt_i64_decode(const uint8_t* ptr, size_t len, int64_t** out); +static inline void kitchen_sink_opt_i64_free(int64_t** v); +static inline void kitchen_sink_opt_kitchen_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Gadget* v); +static inline void kitchen_sink_opt_kitchen_Gadget_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Gadget** out); +static inline bool kitchen_sink_opt_kitchen_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Gadget** out); +static inline void kitchen_sink_opt_kitchen_Gadget_free(kitchen_sink_kitchen_Gadget** v); +static inline void kitchen_sink_list_kitchen_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_list_kitchen_Gadget* v); +static inline void kitchen_sink_list_kitchen_Gadget_read(kitchen_sink_reader* r, kitchen_sink_list_kitchen_Gadget* out); +static inline bool kitchen_sink_list_kitchen_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_kitchen_Gadget* out); +static inline void kitchen_sink_list_kitchen_Gadget_free(kitchen_sink_list_kitchen_Gadget* v); +static inline void kitchen_sink_opt_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v); +static inline void kitchen_sink_opt_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item** out); +static inline bool kitchen_sink_opt_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item** out); +static inline void kitchen_sink_opt_kitchen_Item_free(kitchen_sink_kitchen_Item** v); +static inline void kitchen_sink_list_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_list_kitchen_Item* v); +static inline void kitchen_sink_list_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_list_kitchen_Item* out); +static inline bool kitchen_sink_list_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_kitchen_Item* out); +static inline void kitchen_sink_list_kitchen_Item_free(kitchen_sink_list_kitchen_Item* v); +static inline void kitchen_sink_map_string_i32_write(kitchen_sink_writer* w, const kitchen_sink_map_string_i32* v); +static inline void kitchen_sink_map_string_i32_read(kitchen_sink_reader* r, kitchen_sink_map_string_i32* out); +static inline bool kitchen_sink_map_string_i32_decode(const uint8_t* ptr, size_t len, kitchen_sink_map_string_i32* out); +static inline void kitchen_sink_map_string_i32_free(kitchen_sink_map_string_i32* v); + +static inline void kitchen_sink_shared_Token_write(kitchen_sink_writer* w, const kitchen_sink_shared_Token* v) { + kitchen_sink_writer_put_i64(w, v->id); + kitchen_sink_writer_put_string(w, v->label); +} + +static inline void kitchen_sink_shared_Token_read(kitchen_sink_reader* r, kitchen_sink_shared_Token* out) { + memset(out, 0, sizeof *out); + out->id = kitchen_sink_reader_get_i64(r); + out->label = kitchen_sink_reader_get_string(r); +} + +static inline bool kitchen_sink_shared_Token_decode(const uint8_t* ptr, size_t len, kitchen_sink_shared_Token* out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_shared_Token_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_shared_Token_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_shared_Token_free(kitchen_sink_shared_Token* v) { + kitchen_sink_str_free(&v->label); + memset(v, 0, sizeof *v); +} + +static inline void kitchen_sink_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v) { + kitchen_sink_writer_put_i64(w, v->id); + kitchen_sink_writer_put_string(w, v->name); + kitchen_sink_writer_put_i32(w, v->count); + kitchen_sink_writer_put_bool(w, v->enabled); + kitchen_sink_writer_put_f64(w, v->ratio); + kitchen_sink_writer_put_u32(w, v->nick); + kitchen_sink_writer_put_bytes(w, v->payload); + kitchen_sink_list_string_write(w, &v->tags); + kitchen_sink_map_string_string_write(w, &v->attrs); + kitchen_sink_opt_i64_write(w, v->parent); + kitchen_sink_shared_Token_write(w, &v->token); + kitchen_sink_writer_put_i32(w, (int32_t)v->priority); + kitchen_sink_opt_kitchen_Gadget_write(w, v->gadget); +} + +static inline void kitchen_sink_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item* out) { + memset(out, 0, sizeof *out); + out->id = kitchen_sink_reader_get_i64(r); + out->name = kitchen_sink_reader_get_string(r); + out->count = kitchen_sink_reader_get_i32(r); + out->enabled = kitchen_sink_reader_get_bool(r); + out->ratio = kitchen_sink_reader_get_f64(r); + out->nick = kitchen_sink_reader_get_u32(r); + out->payload = kitchen_sink_reader_get_bytes(r); + kitchen_sink_list_string_read(r, &out->tags); + kitchen_sink_map_string_string_read(r, &out->attrs); + kitchen_sink_opt_i64_read(r, &out->parent); + kitchen_sink_shared_Token_read(r, &out->token); + out->priority = (kitchen_sink_kitchen_Priority)kitchen_sink_reader_get_i32(r); + kitchen_sink_opt_kitchen_Gadget_read(r, &out->gadget); +} + +static inline bool kitchen_sink_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item* out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_kitchen_Item_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_kitchen_Item_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_kitchen_Item_free(kitchen_sink_kitchen_Item* v) { + kitchen_sink_str_free(&v->name); + kitchen_sink_bytes_free(&v->payload); + kitchen_sink_list_string_free(&v->tags); + kitchen_sink_map_string_string_free(&v->attrs); + kitchen_sink_opt_i64_free(&v->parent); + kitchen_sink_shared_Token_free(&v->token); + kitchen_sink_opt_kitchen_Gadget_free(&v->gadget); + memset(v, 0, sizeof *v); +} + +static inline void kitchen_sink_list_string_write(kitchen_sink_writer* w, const kitchen_sink_list_string* v) { + kitchen_sink_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_writer_put_string(w, v->items[i]); + } +} + +static inline void kitchen_sink_list_string_read(kitchen_sink_reader* r, kitchen_sink_list_string* out) { + memset(out, 0, sizeof *out); + size_t n = kitchen_sink_reader_get_u32(r); + out->items = (kitchen_sink_str*)kitchen_sink_reader_alloc(r, n, sizeof(kitchen_sink_str), 4); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->items[i] = kitchen_sink_reader_get_string(r); + } +} + +static inline bool kitchen_sink_list_string_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_string* out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_list_string_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_list_string_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_list_string_free(kitchen_sink_list_string* v) { + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_str_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void kitchen_sink_map_string_string_write(kitchen_sink_writer* w, const kitchen_sink_map_string_string* v) { + kitchen_sink_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_writer_put_string(w, v->keys[i]); + kitchen_sink_writer_put_string(w, v->values[i]); + } +} + +static inline void kitchen_sink_map_string_string_read(kitchen_sink_reader* r, kitchen_sink_map_string_string* out) { + memset(out, 0, sizeof *out); + size_t n = kitchen_sink_reader_get_u32(r); + out->keys = (kitchen_sink_str*)kitchen_sink_reader_alloc(r, n, sizeof(kitchen_sink_str), 8); + out->values = (kitchen_sink_str*)kitchen_sink_reader_alloc(r, n, sizeof(kitchen_sink_str), 0); + if (out->keys == NULL || out->values == NULL) { + free(out->keys); + free(out->values); + out->keys = NULL; + out->values = NULL; + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->keys[i] = kitchen_sink_reader_get_string(r); + out->values[i] = kitchen_sink_reader_get_string(r); + } +} + +static inline bool kitchen_sink_map_string_string_decode(const uint8_t* ptr, size_t len, kitchen_sink_map_string_string* out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_map_string_string_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_map_string_string_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_map_string_string_free(kitchen_sink_map_string_string* v) { + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_str_free(&v->keys[i]); + kitchen_sink_str_free(&v->values[i]); + } + free(v->keys); + free(v->values); + memset(v, 0, sizeof *v); +} + +static inline void kitchen_sink_opt_i64_write(kitchen_sink_writer* w, const int64_t* v) { + kitchen_sink_writer_put_bool(w, v != NULL); + if (v != NULL) { + kitchen_sink_writer_put_i64(w, (*v)); + } +} + +static inline void kitchen_sink_opt_i64_read(kitchen_sink_reader* r, int64_t** out) { + *out = NULL; + if (!kitchen_sink_reader_get_bool(r)) { + return; + } + int64_t* value = (int64_t*)calloc(1, sizeof(int64_t)); + if (value == NULL) { + r->failed = true; + return; + } + (*value) = kitchen_sink_reader_get_i64(r); + *out = value; +} + +static inline bool kitchen_sink_opt_i64_decode(const uint8_t* ptr, size_t len, int64_t** out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_opt_i64_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_opt_i64_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_opt_i64_free(int64_t** v) { + if (*v == NULL) { + return; + } + free(*v); + *v = NULL; +} + +static inline void kitchen_sink_opt_kitchen_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Gadget* v) { + kitchen_sink_writer_put_bool(w, v != NULL); + if (v != NULL) { + kitchen_sink_writer_put_object(w, kitchen_sink_kitchen_Gadget_clone(v)); + } +} + +static inline void kitchen_sink_opt_kitchen_Gadget_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Gadget** out) { + *out = NULL; + if (kitchen_sink_reader_get_bool(r)) { + *out = (kitchen_sink_kitchen_Gadget*)kitchen_sink_reader_get_object(r); + } +} + +static inline bool kitchen_sink_opt_kitchen_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Gadget** out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_opt_kitchen_Gadget_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_opt_kitchen_Gadget_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_opt_kitchen_Gadget_free(kitchen_sink_kitchen_Gadget** v) { + kitchen_sink_kitchen_Gadget_destroy(*v); + *v = NULL; +} + +static inline void kitchen_sink_list_kitchen_Gadget_write(kitchen_sink_writer* w, const kitchen_sink_list_kitchen_Gadget* v) { + kitchen_sink_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_writer_put_object(w, kitchen_sink_kitchen_Gadget_clone(v->items[i])); + } +} + +static inline void kitchen_sink_list_kitchen_Gadget_read(kitchen_sink_reader* r, kitchen_sink_list_kitchen_Gadget* out) { + memset(out, 0, sizeof *out); + size_t n = kitchen_sink_reader_get_u32(r); + out->items = (kitchen_sink_kitchen_Gadget**)kitchen_sink_reader_alloc(r, n, sizeof(kitchen_sink_kitchen_Gadget*), 8); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->items[i] = (kitchen_sink_kitchen_Gadget*)kitchen_sink_reader_get_object(r); + } +} + +static inline bool kitchen_sink_list_kitchen_Gadget_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_kitchen_Gadget* out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_list_kitchen_Gadget_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_list_kitchen_Gadget_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_list_kitchen_Gadget_free(kitchen_sink_list_kitchen_Gadget* v) { + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_kitchen_Gadget_destroy(v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void kitchen_sink_opt_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v) { + kitchen_sink_writer_put_bool(w, v != NULL); + if (v != NULL) { + kitchen_sink_kitchen_Item_write(w, &(*v)); + } +} + +static inline void kitchen_sink_opt_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item** out) { + *out = NULL; + if (!kitchen_sink_reader_get_bool(r)) { + return; + } + kitchen_sink_kitchen_Item* value = (kitchen_sink_kitchen_Item*)calloc(1, sizeof(kitchen_sink_kitchen_Item)); + if (value == NULL) { + r->failed = true; + return; + } + kitchen_sink_kitchen_Item_read(r, &(*value)); + *out = value; +} + +static inline bool kitchen_sink_opt_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item** out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_opt_kitchen_Item_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_opt_kitchen_Item_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_opt_kitchen_Item_free(kitchen_sink_kitchen_Item** v) { + if (*v == NULL) { + return; + } + kitchen_sink_kitchen_Item_free(&(**v)); + free(*v); + *v = NULL; +} + +static inline void kitchen_sink_list_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_list_kitchen_Item* v) { + kitchen_sink_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_kitchen_Item_write(w, &v->items[i]); + } +} + +static inline void kitchen_sink_list_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_list_kitchen_Item* out) { + memset(out, 0, sizeof *out); + size_t n = kitchen_sink_reader_get_u32(r); + out->items = (kitchen_sink_kitchen_Item*)kitchen_sink_reader_alloc(r, n, sizeof(kitchen_sink_kitchen_Item), 59); + if (out->items == NULL) { + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + kitchen_sink_kitchen_Item_read(r, &out->items[i]); + } +} + +static inline bool kitchen_sink_list_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_list_kitchen_Item* out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_list_kitchen_Item_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_list_kitchen_Item_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_list_kitchen_Item_free(kitchen_sink_list_kitchen_Item* v) { + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_kitchen_Item_free(&v->items[i]); + } + free(v->items); + memset(v, 0, sizeof *v); +} + +static inline void kitchen_sink_map_string_i32_write(kitchen_sink_writer* w, const kitchen_sink_map_string_i32* v) { + kitchen_sink_writer_put_len(w, v->len); + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_writer_put_string(w, v->keys[i]); + kitchen_sink_writer_put_i32(w, v->values[i]); + } +} + +static inline void kitchen_sink_map_string_i32_read(kitchen_sink_reader* r, kitchen_sink_map_string_i32* out) { + memset(out, 0, sizeof *out); + size_t n = kitchen_sink_reader_get_u32(r); + out->keys = (kitchen_sink_str*)kitchen_sink_reader_alloc(r, n, sizeof(kitchen_sink_str), 8); + out->values = (int32_t*)kitchen_sink_reader_alloc(r, n, sizeof(int32_t), 0); + if (out->keys == NULL || out->values == NULL) { + free(out->keys); + free(out->values); + out->keys = NULL; + out->values = NULL; + return; + } + out->len = n; + for (size_t i = 0; i < n; i++) { + out->keys[i] = kitchen_sink_reader_get_string(r); + out->values[i] = kitchen_sink_reader_get_i32(r); + } +} + +static inline bool kitchen_sink_map_string_i32_decode(const uint8_t* ptr, size_t len, kitchen_sink_map_string_i32* out) { + kitchen_sink_reader r = kitchen_sink_reader_of(ptr, len); + kitchen_sink_map_string_i32_read(&r, out); + if (!kitchen_sink_reader_finish(&r)) { + kitchen_sink_map_string_i32_free(out); + return false; + } + return true; +} + +static inline void kitchen_sink_map_string_i32_free(kitchen_sink_map_string_i32* v) { + for (size_t i = 0; i < v->len; i++) { + kitchen_sink_str_free(&v->keys[i]); + } + free(v->keys); + free(v->values); + memset(v, 0, sizeof *v); +} + +#ifdef __cplusplus +} +#endif + +#endif // KITCHEN_SINK_BUFFER_H + +// END kitchen_sink_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_h.snap new file mode 100644 index 00000000..14faeffb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__kitchen_sink_h.snap @@ -0,0 +1,219 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef KITCHEN_SINK_H +#define KITCHEN_SINK_H + +#include +#include +#include + +#ifndef KITCHEN_SINK_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef KITCHEN_SINK_BUILD +# define KITCHEN_SINK_API __declspec(dllexport) +# else +# define KITCHEN_SINK_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define KITCHEN_SINK_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define KITCHEN_SINK_API __attribute__((visibility("default"))) +# else +# define KITCHEN_SINK_API +# endif +#endif + +#ifndef KITCHEN_SINK_DEPRECATED +# if defined(_MSC_VER) +# define KITCHEN_SINK_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define KITCHEN_SINK_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define KITCHEN_SINK_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports kitchen_sink_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define KITCHEN_SINK_ABI_VERSION 3u +KITCHEN_SINK_API uint32_t kitchen_sink_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by kitchen_sink_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct kitchen_sink_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} kitchen_sink_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +KITCHEN_SINK_API void kitchen_sink_error_set(kitchen_sink_error* err, int32_t code, const char* message); +KITCHEN_SINK_API void kitchen_sink_error_clear(kitchen_sink_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + kitchen_sink_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_error_free(kitchen_sink_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +KITCHEN_SINK_API void kitchen_sink_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct kitchen_sink_cancel_token kitchen_sink_cancel_token; +KITCHEN_SINK_API kitchen_sink_cancel_token* kitchen_sink_cancel_token_create(void); +KITCHEN_SINK_API void kitchen_sink_cancel_token_cancel(kitchen_sink_cancel_token* token); +KITCHEN_SINK_API bool kitchen_sink_cancel_token_is_cancelled(const kitchen_sink_cancel_token* token); +KITCHEN_SINK_API void kitchen_sink_cancel_token_destroy(kitchen_sink_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +KITCHEN_SINK_API uint8_t* kitchen_sink_alloc(uint32_t size); +KITCHEN_SINK_API void kitchen_sink_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +KITCHEN_SINK_API uint64_t kitchen_sink_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define KITCHEN_SINK_SHARED_CHECKSUM 0x42c4ce2c8d0af052ull +KITCHEN_SINK_API uint64_t kitchen_sink_shared_checksum(void); +#define KITCHEN_SINK_KITCHEN_CHECKSUM 0xd83da75b24b545ffull +KITCHEN_SINK_API uint64_t kitchen_sink_kitchen_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with kitchen_sink_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * kitchen_sink_buffer.h declares a C struct and codec for each. + */ + +/** Task priority levels */ +typedef enum { + /** Low priority */ + kitchen_sink_kitchen_Priority_Low = 0, + /** Normal priority */ + kitchen_sink_kitchen_Priority_Normal = 1, + /** High priority */ + kitchen_sink_kitchen_Priority_High = 2 +} kitchen_sink_kitchen_Priority; +/** Error codes reported by throwing functions in the `kitchen` module tree. */ +typedef enum { kitchen_sink_kitchen_KitchenErrors_NotFound = 1, kitchen_sink_kitchen_KitchenErrors_InvalidInput = 2 } kitchen_sink_kitchen_KitchenErrors; +typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget; +typedef struct kitchen_sink_kitchen_StreamGadgetsIterator kitchen_sink_kitchen_StreamGadgetsIterator; +typedef struct kitchen_sink_kitchen_StreamItemsIterator kitchen_sink_kitchen_StreamItemsIterator; +/** + * Consumer-implemented observer of ready items + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct kitchen_sink_kitchen_ReadyListener_vtable { + /** Fires when an item is ready */ + void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); + /** Receives the item itself and says whether to keep listening */ + bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); + void (*free)(void* ctx); +} kitchen_sink_kitchen_ReadyListener_vtable; +typedef void (*kitchen_sink_kitchen_do_async_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*kitchen_sink_kitchen_do_cancellable_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: shared +/** Trivial cross-module helper */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_shared_ping(size_t* out_len, kitchen_sink_error* out_err); + +// Module: kitchen +/** A pocket-sized interface exercising the object surface */ +/** Create a gadget with the given id */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(int64_t id, kitchen_sink_error* out_err); +/** The gadget subsystem version string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_version(size_t* out_len, kitchen_sink_error* out_err); +/** Render the gadget as a human-readable string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err); +/** Poke the gadget a number of times, failing on a negative count */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_Gadget_poke(const kitchen_sink_kitchen_Gadget* self, int32_t times, kitchen_sink_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_kitchen_Gadget_destroy(kitchen_sink_kitchen_Gadget* self); + +/** Identity for bool */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_bool_id(bool x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_i32_id(int32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API uint32_t kitchen_sink_kitchen_u32_id(uint32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int64_t kitchen_sink_kitchen_i64_id(int64_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API double kitchen_sink_kitchen_f64_id(double x, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_string(const uint8_t* s_ptr, size_t s_len, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_bytes(const uint8_t* b_ptr, size_t b_len, size_t* out_len, kitchen_sink_error* out_err); +/** Register a listener, returning how many are attached */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err); +/** Optional object in both directions */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_maybe_gadget(const kitchen_sink_kitchen_Gadget* fallback, kitchen_sink_error* out_err); +/** A list of objects */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_gadgets(size_t* out_len, kitchen_sink_error* out_err); +/** An iterator over objects */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamGadgetsIterator* kitchen_sink_kitchen_stream_gadgets(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamGadgetsIterator_next(kitchen_sink_kitchen_StreamGadgetsIterator* iter, kitchen_sink_kitchen_Gadget** out_item, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamGadgetsIterator_destroy(kitchen_sink_kitchen_StreamGadgetsIterator* iter); +/** Optional struct return */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_maybe_item(int64_t id, size_t* out_len, kitchen_sink_error* out_err); +/** List of structs */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_list_items(size_t* out_len, kitchen_sink_error* out_err); +/** Map return type */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_get_attrs(size_t* out_len, kitchen_sink_error* out_err); +/** Iterator return type */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamItemsIterator* kitchen_sink_kitchen_stream_items(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamItemsIterator_next(kitchen_sink_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamItemsIterator_destroy(kitchen_sink_kitchen_StreamItemsIterator* iter); +/** Returns the shared Token type from another module */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_cross_module_token(size_t* out_len, kitchen_sink_error* out_err); +/** Async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_async(const uint8_t* input_ptr, size_t input_len, kitchen_sink_kitchen_do_async_callback callback, void* context); +/** Cancellable async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_ptr, size_t input_len, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_do_cancellable_callback callback, void* context); +/** Legacy operation kept for compatibility */ +KITCHEN_SINK_DEPRECATED("Use new_op instead") +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_legacy_op(kitchen_sink_error* out_err); +/** Replacement for legacy_op */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_new_op(kitchen_sink_error* out_err); + +// Module: kitchen_nested +/** Trivial nested-module function */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_nested_hello(size_t* out_len, kitchen_sink_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // KITCHEN_SINK_H + +// END kitchen_sink.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__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_kitchen_sink__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap deleted file mode 100644 index 7490435b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap +++ /dev/null @@ -1,203 +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 - -/* 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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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. - */ - -/** Task priority levels */ -typedef enum { - /** Low priority */ - weaveffi_kitchen_Priority_Low = 0, - /** Normal priority */ - weaveffi_kitchen_Priority_Normal = 1, - /** High priority */ - weaveffi_kitchen_Priority_High = 2 -} weaveffi_kitchen_Priority; -/** Error codes reported by throwing functions in the `kitchen` module tree. */ -typedef enum { weaveffi_kitchen_KitchenErrors_NotFound = 1, weaveffi_kitchen_KitchenErrors_InvalidInput = 2 } weaveffi_kitchen_KitchenErrors; -typedef struct weaveffi_kitchen_Gadget weaveffi_kitchen_Gadget; -typedef struct weaveffi_kitchen_StreamGadgetsIterator weaveffi_kitchen_StreamGadgetsIterator; -typedef struct weaveffi_kitchen_StreamItemsIterator weaveffi_kitchen_StreamItemsIterator; -/** - * Consumer-implemented observer of ready items - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. - */ -typedef struct weaveffi_kitchen_ReadyListener_vtable { - /** Fires when an item is ready */ - void (*on_ready)(void* ctx, int32_t code, const char* msg, weaveffi_error* out_err); - /** Receives the item itself and says whether to keep listening */ - bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, weaveffi_kitchen_Gadget* gadget, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_kitchen_ReadyListener_vtable; -typedef void (*weaveffi_kitchen_do_async_callback)(void* context, weaveffi_error* err, const char* result); -typedef void (*weaveffi_kitchen_do_cancellable_callback)(void* context, weaveffi_error* err, const char* result); - -// Module: shared -/** Trivial cross-module helper */ -WEAVEFFI_API const char* weaveffi_shared_ping(weaveffi_error* out_err); - -// Module: kitchen -/** A pocket-sized interface exercising the object surface */ -/** Create a gadget with the given id */ -WEAVEFFI_API weaveffi_kitchen_Gadget* weaveffi_kitchen_Gadget_new(int64_t id, weaveffi_error* out_err); -/** The gadget subsystem version string */ -WEAVEFFI_API const char* weaveffi_kitchen_Gadget_version(weaveffi_error* out_err); -/** Render the gadget as a human-readable string */ -WEAVEFFI_API const char* weaveffi_kitchen_Gadget_describe(const weaveffi_kitchen_Gadget* self, weaveffi_error* out_err); -/** Poke the gadget a number of times, failing on a negative count */ -WEAVEFFI_API int32_t weaveffi_kitchen_Gadget_poke(const weaveffi_kitchen_Gadget* self, int32_t times, weaveffi_error* out_err); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_kitchen_Gadget* weaveffi_kitchen_Gadget_clone(const weaveffi_kitchen_Gadget* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_kitchen_Gadget_destroy(weaveffi_kitchen_Gadget* self); - -/** Identity for bool */ -WEAVEFFI_API bool weaveffi_kitchen_bool_id(bool x, weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_kitchen_i32_id(int32_t x, weaveffi_error* out_err); -WEAVEFFI_API uint32_t weaveffi_kitchen_u32_id(uint32_t x, weaveffi_error* out_err); -WEAVEFFI_API int64_t weaveffi_kitchen_i64_id(int64_t x, weaveffi_error* out_err); -WEAVEFFI_API double weaveffi_kitchen_f64_id(double x, weaveffi_error* out_err); -WEAVEFFI_API const char* weaveffi_kitchen_echo_string(const char* s, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_kitchen_echo_bytes(const uint8_t* b_ptr, size_t b_len, size_t* out_len, weaveffi_error* out_err); -/** Register a listener, returning how many are attached */ -WEAVEFFI_API int32_t weaveffi_kitchen_subscribe(void* listener_ctx, const weaveffi_kitchen_ReadyListener_vtable* listener_vtable, weaveffi_error* out_err); -/** Optional object in both directions */ -WEAVEFFI_API weaveffi_kitchen_Gadget* weaveffi_kitchen_maybe_gadget(const weaveffi_kitchen_Gadget* fallback, weaveffi_error* out_err); -/** A list of objects */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_gadgets(size_t* out_len, weaveffi_error* out_err); -/** An iterator over objects */ -WEAVEFFI_API weaveffi_kitchen_StreamGadgetsIterator* weaveffi_kitchen_stream_gadgets(weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_kitchen_StreamGadgetsIterator_next(weaveffi_kitchen_StreamGadgetsIterator* iter, weaveffi_kitchen_Gadget** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_kitchen_StreamGadgetsIterator_destroy(weaveffi_kitchen_StreamGadgetsIterator* iter); -/** Optional struct return */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_maybe_item(int64_t id, size_t* out_len, weaveffi_error* out_err); -/** List of structs */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_list_items(size_t* out_len, weaveffi_error* out_err); -/** Map return type */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_get_attrs(size_t* out_len, weaveffi_error* out_err); -/** Iterator return type */ -WEAVEFFI_API weaveffi_kitchen_StreamItemsIterator* weaveffi_kitchen_stream_items(weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_kitchen_StreamItemsIterator_next(weaveffi_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_kitchen_StreamItemsIterator_destroy(weaveffi_kitchen_StreamItemsIterator* iter); -/** Returns the shared Token type from another module */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_cross_module_token(size_t* out_len, weaveffi_error* out_err); -/** Async operation */ -WEAVEFFI_API void weaveffi_kitchen_do_async_async(const char* input, weaveffi_kitchen_do_async_callback callback, void* context); -/** Cancellable async operation */ -WEAVEFFI_API void weaveffi_kitchen_do_cancellable_async(const char* input, weaveffi_cancel_token* cancel_token, weaveffi_kitchen_do_cancellable_callback callback, void* context); -/** Legacy operation kept for compatibility */ -WEAVEFFI_DEPRECATED("Use new_op instead") -WEAVEFFI_API int32_t weaveffi_kitchen_legacy_op(weaveffi_error* out_err); -/** Replacement for legacy_op */ -WEAVEFFI_API int32_t weaveffi_kitchen_new_op(weaveffi_error* out_err); - -// Module: kitchen_nested -/** Trivial nested-module function */ -WEAVEFFI_API const char* weaveffi_kitchen_nested_hello(weaveffi_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_buffer_h.snap new file mode 100644 index 00000000..f541207a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_buffer_h.snap @@ -0,0 +1,505 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NESTED_MODULES_BUFFER_H +#define NESTED_MODULES_BUFFER_H + +#include "nested_modules.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Value buffer runtime. + * + * The wire format is little-endian and packed: fixed-width scalars at their + * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length + * followed by the raw bytes (no NUL terminator), optionals as a presence byte + * followed by the value, lists as a `uint32_t` count followed by the + * elements, maps as a `uint32_t` count followed by alternating keys and + * values, and objects as a `uint64_t` token holding one strong reference. + * ------------------------------------------------------------------------- */ + +/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. + A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be + passed to C string functions when the text has no interior NUL. */ +typedef struct nested_modules_str { + const char* ptr; + size_t len; +} nested_modules_str; + +/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ +typedef struct nested_modules_bytes { + const uint8_t* ptr; + size_t len; +} nested_modules_bytes; + +/* Borrow a NUL-terminated C string (NULL is the empty string). */ +static inline nested_modules_str nested_modules_str_of(const char* s) { + nested_modules_str v; + v.ptr = s; + v.len = s ? strlen(s) : 0; + return v; +} + +/* Borrow `len` bytes at `ptr`. */ +static inline nested_modules_bytes nested_modules_bytes_of(const uint8_t* ptr, size_t len) { + nested_modules_bytes v; + v.ptr = ptr; + v.len = len; + return v; +} + +/* Release a decoded string. Never call this on a borrowed view. */ +static inline void nested_modules_str_free(nested_modules_str* s) { + free((void*)s->ptr); + s->ptr = NULL; + s->len = 0; +} + +/* Release decoded bytes. Never call this on a borrowed view. */ +static inline void nested_modules_bytes_free(nested_modules_bytes* b) { + free((void*)b->ptr); + b->ptr = NULL; + b->len = 0; +} + +/* A growable output buffer. Zero-initialize it, write values, pass + `ptr`/`len` to the call, then release it with nested_modules_writer_free. An + allocation failure sets `failed` and turns later writes into no-ops. */ +typedef struct nested_modules_writer { + uint8_t* ptr; + size_t len; + size_t cap; + bool failed; +} nested_modules_writer; + +static inline void nested_modules_writer_free(nested_modules_writer* w) { + free(w->ptr); + w->ptr = NULL; + w->len = 0; + w->cap = 0; + w->failed = false; +} + +static inline void nested_modules_writer_put_raw(nested_modules_writer* w, const void* data, size_t n) { + if (w->failed || n == 0) { + return; + } + if (n > w->cap - w->len) { + size_t cap = w->cap ? w->cap : 64; + while (cap - w->len < n) { + if (cap > SIZE_MAX / 2) { + w->failed = true; + return; + } + cap *= 2; + } + uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); + if (grown == NULL) { + w->failed = true; + return; + } + w->ptr = grown; + w->cap = cap; + } + memcpy(w->ptr + w->len, data, n); + w->len += n; +} + +static inline void nested_modules_writer_put_u64(nested_modules_writer* w, uint64_t v) { + uint8_t b[8]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)(v >> (8 * i)); + } + nested_modules_writer_put_raw(w, b, 8); +} + +static inline void nested_modules_writer_put_u32(nested_modules_writer* w, uint32_t v) { + uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; + nested_modules_writer_put_raw(w, b, 4); +} + +static inline void nested_modules_writer_put_u16(nested_modules_writer* w, uint16_t v) { + uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; + nested_modules_writer_put_raw(w, b, 2); +} + +static inline void nested_modules_writer_put_u8(nested_modules_writer* w, uint8_t v) { + nested_modules_writer_put_raw(w, &v, 1); +} + +static inline void nested_modules_writer_put_i8(nested_modules_writer* w, int8_t v) { + nested_modules_writer_put_u8(w, (uint8_t)v); +} + +static inline void nested_modules_writer_put_i16(nested_modules_writer* w, int16_t v) { + nested_modules_writer_put_u16(w, (uint16_t)v); +} + +static inline void nested_modules_writer_put_i32(nested_modules_writer* w, int32_t v) { + nested_modules_writer_put_u32(w, (uint32_t)v); +} + +static inline void nested_modules_writer_put_i64(nested_modules_writer* w, int64_t v) { + nested_modules_writer_put_u64(w, (uint64_t)v); +} + +static inline void nested_modules_writer_put_bool(nested_modules_writer* w, bool v) { + nested_modules_writer_put_u8(w, v ? 1 : 0); +} + +static inline void nested_modules_writer_put_f32(nested_modules_writer* w, float v) { + uint32_t bits; + memcpy(&bits, &v, 4); + nested_modules_writer_put_u32(w, bits); +} + +static inline void nested_modules_writer_put_f64(nested_modules_writer* w, double v) { + uint64_t bits; + memcpy(&bits, &v, 8); + nested_modules_writer_put_u64(w, bits); +} + +static inline void nested_modules_writer_put_len(nested_modules_writer* w, size_t n) { + if (n > UINT32_MAX) { + w->failed = true; + return; + } + nested_modules_writer_put_u32(w, (uint32_t)n); +} + +static inline void nested_modules_writer_put_string(nested_modules_writer* w, nested_modules_str v) { + nested_modules_writer_put_len(w, v.len); + nested_modules_writer_put_raw(w, v.ptr, v.len); +} + +static inline void nested_modules_writer_put_bytes(nested_modules_writer* w, nested_modules_bytes v) { + nested_modules_writer_put_len(w, v.len); + nested_modules_writer_put_raw(w, v.ptr, v.len); +} + +/* Write an object token. The token carries one strong reference, so pass + a reference the buffer may own (the generated codecs pass a fresh + `_clone`). */ +static inline void nested_modules_writer_put_object(nested_modules_writer* w, const void* owned) { + nested_modules_writer_put_u64(w, (uint64_t)(uintptr_t)owned); +} + +/* A cursor over an input buffer. A malformed buffer sets `failed`; every + later read then returns a zero value, so a decoder can always finish and + release what it built. */ +typedef struct nested_modules_reader { + const uint8_t* ptr; + size_t len; + size_t pos; + bool failed; +} nested_modules_reader; + +static inline nested_modules_reader nested_modules_reader_of(const uint8_t* ptr, size_t len) { + nested_modules_reader r; + r.ptr = ptr; + r.len = ptr ? len : 0; + r.pos = 0; + r.failed = false; + return r; +} + +/* `true` when every byte was consumed and nothing was malformed. */ +static inline bool nested_modules_reader_finish(const nested_modules_reader* r) { + return !r->failed && r->pos == r->len; +} + +static inline const uint8_t* nested_modules_reader_take(nested_modules_reader* r, size_t n) { + if (r->failed || n > r->len - r->pos) { + r->failed = true; + return NULL; + } + const uint8_t* at = r->ptr + r->pos; + r->pos += n; + return at; +} + +/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is + zero). `min_wire` is the smallest encoding of one element; a count the + remaining bytes can't hold fails the reader before anything is + allocated. */ +static inline void* nested_modules_reader_alloc(nested_modules_reader* r, size_t n, size_t size, size_t min_wire) { + if (r->failed || n == 0) { + return NULL; + } + if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { + r->failed = true; + return NULL; + } + void* p = calloc(n, size); + if (p == NULL) { + r->failed = true; + } + return p; +} + +static inline uint64_t nested_modules_reader_get_u64(nested_modules_reader* r) { + const uint8_t* b = nested_modules_reader_take(r, 8); + uint64_t v = 0; + if (b != NULL) { + for (int i = 0; i < 8; i++) { + v |= (uint64_t)b[i] << (8 * i); + } + } + return v; +} + +static inline uint32_t nested_modules_reader_get_u32(nested_modules_reader* r) { + const uint8_t* b = nested_modules_reader_take(r, 4); + if (b == NULL) { + return 0; + } + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static inline uint16_t nested_modules_reader_get_u16(nested_modules_reader* r) { + const uint8_t* b = nested_modules_reader_take(r, 2); + return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; +} + +static inline uint8_t nested_modules_reader_get_u8(nested_modules_reader* r) { + const uint8_t* b = nested_modules_reader_take(r, 1); + return b ? b[0] : (uint8_t)0; +} + +static inline int8_t nested_modules_reader_get_i8(nested_modules_reader* r) { + return (int8_t)nested_modules_reader_get_u8(r); +} + +static inline int16_t nested_modules_reader_get_i16(nested_modules_reader* r) { + return (int16_t)nested_modules_reader_get_u16(r); +} + +static inline int32_t nested_modules_reader_get_i32(nested_modules_reader* r) { + return (int32_t)nested_modules_reader_get_u32(r); +} + +static inline int64_t nested_modules_reader_get_i64(nested_modules_reader* r) { + return (int64_t)nested_modules_reader_get_u64(r); +} + +static inline bool nested_modules_reader_get_bool(nested_modules_reader* r) { + uint8_t v = nested_modules_reader_get_u8(r); + if (v > 1) { + r->failed = true; + return false; + } + return v == 1; +} + +static inline float nested_modules_reader_get_f32(nested_modules_reader* r) { + uint32_t bits = nested_modules_reader_get_u32(r); + float v; + memcpy(&v, &bits, 4); + return v; +} + +static inline double nested_modules_reader_get_f64(nested_modules_reader* r) { + uint64_t bits = nested_modules_reader_get_u64(r); + double v; + memcpy(&v, &bits, 8); + return v; +} + +/* A `uint32_t` length followed by that many bytes, copied into a fresh + NUL-terminated allocation. */ +static inline uint8_t* nested_modules_reader_get_run(nested_modules_reader* r, size_t* out_len) { + size_t n = nested_modules_reader_get_u32(r); + const uint8_t* at = nested_modules_reader_take(r, n); + *out_len = 0; + if (at == NULL) { + return NULL; + } + uint8_t* copy = (uint8_t*)malloc(n + 1); + if (copy == NULL) { + r->failed = true; + return NULL; + } + memcpy(copy, at, n); + copy[n] = 0; + *out_len = n; + return copy; +} + +static inline nested_modules_str nested_modules_reader_get_string(nested_modules_reader* r) { + nested_modules_str v; + v.ptr = (const char*)nested_modules_reader_get_run(r, &v.len); + return v; +} + +static inline nested_modules_bytes nested_modules_reader_get_bytes(nested_modules_reader* r) { + nested_modules_bytes v; + v.ptr = nested_modules_reader_get_run(r, &v.len); + return v; +} + +/* Read an object token and adopt the strong reference it carries. A zero + token in a required position fails the reader. */ +static inline void* nested_modules_reader_get_object(nested_modules_reader* r) { + uint64_t token = nested_modules_reader_get_u64(r); + if (token == 0) { + r->failed = true; + } + return (void*)(uintptr_t)token; +} + +/* --------------------------------------------------------------------------- + * Value types. A record is a struct of its fields; a rich enum is a `tag` + * plus a union `as` with one struct per payload-carrying variant; a list is + * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an + * optional is a pointer that is NULL when absent; an object is its pointer; + * a string or byte string is a view (`ptr` + `len`). + * ------------------------------------------------------------------------- */ + +typedef struct nested_modules_graphics_Canvas nested_modules_graphics_Canvas; +typedef struct nested_modules_graphics_shapes_Shape nested_modules_graphics_shapes_Shape; +typedef struct nested_modules_graphics_shapes_geometry_Detail nested_modules_graphics_shapes_geometry_Detail; + +/** A struct two levels deep. */ +struct nested_modules_graphics_shapes_geometry_Detail { + double area; +}; + +/** A struct one level deep that references both an ancestor enum and a deeper struct. */ +struct nested_modules_graphics_shapes_Shape { + int32_t sides; + /** Reference up to the parent module's enum */ + nested_modules_graphics_Unit unit; + /** Reference down into a deeper module */ + nested_modules_graphics_shapes_geometry_Detail detail; +}; + +/** Top-level struct that references a struct defined one level deeper. */ +struct nested_modules_graphics_Canvas { + int32_t width; + /** Reference down into a nested module */ + nested_modules_graphics_shapes_Shape root; +}; + +/* --------------------------------------------------------------------------- + * Codecs. Each value type T above, and each optional shape, has four + * functions: + * + * T_write(w, v) appends the value at `v` to writer `w` + * T_read(r, out) decodes one value from reader `r` + * T_decode(ptr, len, out) decodes a whole buffer; on a malformed + * buffer it frees what it built and + * returns false + * T_free(v) releases a decoded value + * + * An optional's T_write takes the nullable pointer itself. Writing an + * object stores a fresh `_clone` reference for the reader to adopt; + * decoding adopts each object reference, and T_free destroys it. T_free + * releases only what T_read or T_decode allocated; never call it on a + * value that borrows caller memory. + * ------------------------------------------------------------------------- */ + +static inline void nested_modules_graphics_Canvas_write(nested_modules_writer* w, const nested_modules_graphics_Canvas* v); +static inline void nested_modules_graphics_Canvas_read(nested_modules_reader* r, nested_modules_graphics_Canvas* out); +static inline bool nested_modules_graphics_Canvas_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_Canvas* out); +static inline void nested_modules_graphics_Canvas_free(nested_modules_graphics_Canvas* v); +static inline void nested_modules_graphics_shapes_Shape_write(nested_modules_writer* w, const nested_modules_graphics_shapes_Shape* v); +static inline void nested_modules_graphics_shapes_Shape_read(nested_modules_reader* r, nested_modules_graphics_shapes_Shape* out); +static inline bool nested_modules_graphics_shapes_Shape_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_shapes_Shape* out); +static inline void nested_modules_graphics_shapes_Shape_free(nested_modules_graphics_shapes_Shape* v); +static inline void nested_modules_graphics_shapes_geometry_Detail_write(nested_modules_writer* w, const nested_modules_graphics_shapes_geometry_Detail* v); +static inline void nested_modules_graphics_shapes_geometry_Detail_read(nested_modules_reader* r, nested_modules_graphics_shapes_geometry_Detail* out); +static inline bool nested_modules_graphics_shapes_geometry_Detail_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_shapes_geometry_Detail* out); +static inline void nested_modules_graphics_shapes_geometry_Detail_free(nested_modules_graphics_shapes_geometry_Detail* v); + +static inline void nested_modules_graphics_Canvas_write(nested_modules_writer* w, const nested_modules_graphics_Canvas* v) { + nested_modules_writer_put_i32(w, v->width); + nested_modules_graphics_shapes_Shape_write(w, &v->root); +} + +static inline void nested_modules_graphics_Canvas_read(nested_modules_reader* r, nested_modules_graphics_Canvas* out) { + memset(out, 0, sizeof *out); + out->width = nested_modules_reader_get_i32(r); + nested_modules_graphics_shapes_Shape_read(r, &out->root); +} + +static inline bool nested_modules_graphics_Canvas_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_Canvas* out) { + nested_modules_reader r = nested_modules_reader_of(ptr, len); + nested_modules_graphics_Canvas_read(&r, out); + if (!nested_modules_reader_finish(&r)) { + nested_modules_graphics_Canvas_free(out); + return false; + } + return true; +} + +static inline void nested_modules_graphics_Canvas_free(nested_modules_graphics_Canvas* v) { + memset(v, 0, sizeof *v); +} + +static inline void nested_modules_graphics_shapes_Shape_write(nested_modules_writer* w, const nested_modules_graphics_shapes_Shape* v) { + nested_modules_writer_put_i32(w, v->sides); + nested_modules_writer_put_i32(w, (int32_t)v->unit); + nested_modules_graphics_shapes_geometry_Detail_write(w, &v->detail); +} + +static inline void nested_modules_graphics_shapes_Shape_read(nested_modules_reader* r, nested_modules_graphics_shapes_Shape* out) { + memset(out, 0, sizeof *out); + out->sides = nested_modules_reader_get_i32(r); + out->unit = (nested_modules_graphics_Unit)nested_modules_reader_get_i32(r); + nested_modules_graphics_shapes_geometry_Detail_read(r, &out->detail); +} + +static inline bool nested_modules_graphics_shapes_Shape_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_shapes_Shape* out) { + nested_modules_reader r = nested_modules_reader_of(ptr, len); + nested_modules_graphics_shapes_Shape_read(&r, out); + if (!nested_modules_reader_finish(&r)) { + nested_modules_graphics_shapes_Shape_free(out); + return false; + } + return true; +} + +static inline void nested_modules_graphics_shapes_Shape_free(nested_modules_graphics_shapes_Shape* v) { + memset(v, 0, sizeof *v); +} + +static inline void nested_modules_graphics_shapes_geometry_Detail_write(nested_modules_writer* w, const nested_modules_graphics_shapes_geometry_Detail* v) { + nested_modules_writer_put_f64(w, v->area); +} + +static inline void nested_modules_graphics_shapes_geometry_Detail_read(nested_modules_reader* r, nested_modules_graphics_shapes_geometry_Detail* out) { + memset(out, 0, sizeof *out); + out->area = nested_modules_reader_get_f64(r); +} + +static inline bool nested_modules_graphics_shapes_geometry_Detail_decode(const uint8_t* ptr, size_t len, nested_modules_graphics_shapes_geometry_Detail* out) { + nested_modules_reader r = nested_modules_reader_of(ptr, len); + nested_modules_graphics_shapes_geometry_Detail_read(&r, out); + if (!nested_modules_reader_finish(&r)) { + nested_modules_graphics_shapes_geometry_Detail_free(out); + return false; + } + return true; +} + +static inline void nested_modules_graphics_shapes_geometry_Detail_free(nested_modules_graphics_shapes_geometry_Detail* v) { + memset(v, 0, sizeof *v); +} + +#ifdef __cplusplus +} +#endif + +#endif // NESTED_MODULES_BUFFER_H + +// END nested_modules_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_h.snap b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_h.snap new file mode 100644 index 00000000..27c97f2b --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__nested_modules_h.snap @@ -0,0 +1,144 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NESTED_MODULES_H +#define NESTED_MODULES_H + +#include +#include +#include + +#ifndef NESTED_MODULES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef NESTED_MODULES_BUILD +# define NESTED_MODULES_API __declspec(dllexport) +# else +# define NESTED_MODULES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define NESTED_MODULES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define NESTED_MODULES_API __attribute__((visibility("default"))) +# else +# define NESTED_MODULES_API +# endif +#endif + +#ifndef NESTED_MODULES_DEPRECATED +# if defined(_MSC_VER) +# define NESTED_MODULES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define NESTED_MODULES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define NESTED_MODULES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports nested_modules_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define NESTED_MODULES_ABI_VERSION 3u +NESTED_MODULES_API uint32_t nested_modules_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by nested_modules_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct nested_modules_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} nested_modules_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +NESTED_MODULES_API void nested_modules_error_set(nested_modules_error* err, int32_t code, const char* message); +NESTED_MODULES_API void nested_modules_error_clear(nested_modules_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + nested_modules_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +NESTED_MODULES_API void nested_modules_error_free(nested_modules_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +NESTED_MODULES_API void nested_modules_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct nested_modules_cancel_token nested_modules_cancel_token; +NESTED_MODULES_API nested_modules_cancel_token* nested_modules_cancel_token_create(void); +NESTED_MODULES_API void nested_modules_cancel_token_cancel(nested_modules_cancel_token* token); +NESTED_MODULES_API bool nested_modules_cancel_token_is_cancelled(const nested_modules_cancel_token* token); +NESTED_MODULES_API void nested_modules_cancel_token_destroy(nested_modules_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +NESTED_MODULES_API uint8_t* nested_modules_alloc(uint32_t size); +NESTED_MODULES_API void nested_modules_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +NESTED_MODULES_API uint64_t nested_modules_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define NESTED_MODULES_GRAPHICS_CHECKSUM 0x44d31b5b10fdb90dull +NESTED_MODULES_API uint64_t nested_modules_graphics_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with nested_modules_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * nested_modules_buffer.h declares a C struct and codec for each. + */ + +/** A measurement unit shared with nested submodules. */ +typedef enum { nested_modules_graphics_Unit_pixels = 0, nested_modules_graphics_Unit_points = 1 } nested_modules_graphics_Unit; +/** Error codes reported by throwing functions in the `graphics` module tree. */ +typedef enum { nested_modules_graphics_GraphicsError_OutOfBounds = 1 } nested_modules_graphics_GraphicsError; + +// Module: graphics +/** Construct a Canvas. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_new_canvas(int32_t width, size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes +/** Construct a Shape. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_make_shape(int32_t sides, size_t* out_len, nested_modules_error* out_err); +/** Nested function returning a struct owned by an ancestor module. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_describe(size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes_geometry +/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, nested_modules_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // NESTED_MODULES_H + +// END nested_modules.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__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_nested_modules__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap deleted file mode 100644 index 3f78f7e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap +++ /dev/null @@ -1,130 +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 - -/* 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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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. - */ - -/** A measurement unit shared with nested submodules. */ -typedef enum { weaveffi_graphics_Unit_pixels = 0, weaveffi_graphics_Unit_points = 1 } weaveffi_graphics_Unit; -/** Error codes reported by throwing functions in the `graphics` module tree. */ -typedef enum { weaveffi_graphics_GraphicsError_OutOfBounds = 1 } weaveffi_graphics_GraphicsError; - -// Module: graphics -/** Construct a Canvas. */ -WEAVEFFI_API const uint8_t* weaveffi_graphics_new_canvas(int32_t width, size_t* out_len, weaveffi_error* out_err); - -// Module: graphics_shapes -/** Construct a Shape. */ -WEAVEFFI_API const uint8_t* weaveffi_graphics_shapes_make_shape(int32_t sides, size_t* out_len, weaveffi_error* out_err); -/** Nested function returning a struct owned by an ancestor module. */ -WEAVEFFI_API const uint8_t* weaveffi_graphics_shapes_describe(size_t* out_len, weaveffi_error* out_err); - -// Module: graphics_shapes_geometry -/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ -WEAVEFFI_API const uint8_t* weaveffi_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, 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_shapes__shapes_buffer_h.snap b/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_buffer_h.snap new file mode 100644 index 00000000..25549e50 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_buffer_h.snap @@ -0,0 +1,568 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef SHAPES_BUFFER_H +#define SHAPES_BUFFER_H + +#include "shapes.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Value buffer runtime. + * + * The wire format is little-endian and packed: fixed-width scalars at their + * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length + * followed by the raw bytes (no NUL terminator), optionals as a presence byte + * followed by the value, lists as a `uint32_t` count followed by the + * elements, maps as a `uint32_t` count followed by alternating keys and + * values, and objects as a `uint64_t` token holding one strong reference. + * ------------------------------------------------------------------------- */ + +/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. + A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be + passed to C string functions when the text has no interior NUL. */ +typedef struct shapes_str { + const char* ptr; + size_t len; +} shapes_str; + +/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ +typedef struct shapes_bytes { + const uint8_t* ptr; + size_t len; +} shapes_bytes; + +/* Borrow a NUL-terminated C string (NULL is the empty string). */ +static inline shapes_str shapes_str_of(const char* s) { + shapes_str v; + v.ptr = s; + v.len = s ? strlen(s) : 0; + return v; +} + +/* Borrow `len` bytes at `ptr`. */ +static inline shapes_bytes shapes_bytes_of(const uint8_t* ptr, size_t len) { + shapes_bytes v; + v.ptr = ptr; + v.len = len; + return v; +} + +/* Release a decoded string. Never call this on a borrowed view. */ +static inline void shapes_str_free(shapes_str* s) { + free((void*)s->ptr); + s->ptr = NULL; + s->len = 0; +} + +/* Release decoded bytes. Never call this on a borrowed view. */ +static inline void shapes_bytes_free(shapes_bytes* b) { + free((void*)b->ptr); + b->ptr = NULL; + b->len = 0; +} + +/* A growable output buffer. Zero-initialize it, write values, pass + `ptr`/`len` to the call, then release it with shapes_writer_free. An + allocation failure sets `failed` and turns later writes into no-ops. */ +typedef struct shapes_writer { + uint8_t* ptr; + size_t len; + size_t cap; + bool failed; +} shapes_writer; + +static inline void shapes_writer_free(shapes_writer* w) { + free(w->ptr); + w->ptr = NULL; + w->len = 0; + w->cap = 0; + w->failed = false; +} + +static inline void shapes_writer_put_raw(shapes_writer* w, const void* data, size_t n) { + if (w->failed || n == 0) { + return; + } + if (n > w->cap - w->len) { + size_t cap = w->cap ? w->cap : 64; + while (cap - w->len < n) { + if (cap > SIZE_MAX / 2) { + w->failed = true; + return; + } + cap *= 2; + } + uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); + if (grown == NULL) { + w->failed = true; + return; + } + w->ptr = grown; + w->cap = cap; + } + memcpy(w->ptr + w->len, data, n); + w->len += n; +} + +static inline void shapes_writer_put_u64(shapes_writer* w, uint64_t v) { + uint8_t b[8]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)(v >> (8 * i)); + } + shapes_writer_put_raw(w, b, 8); +} + +static inline void shapes_writer_put_u32(shapes_writer* w, uint32_t v) { + uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; + shapes_writer_put_raw(w, b, 4); +} + +static inline void shapes_writer_put_u16(shapes_writer* w, uint16_t v) { + uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; + shapes_writer_put_raw(w, b, 2); +} + +static inline void shapes_writer_put_u8(shapes_writer* w, uint8_t v) { + shapes_writer_put_raw(w, &v, 1); +} + +static inline void shapes_writer_put_i8(shapes_writer* w, int8_t v) { + shapes_writer_put_u8(w, (uint8_t)v); +} + +static inline void shapes_writer_put_i16(shapes_writer* w, int16_t v) { + shapes_writer_put_u16(w, (uint16_t)v); +} + +static inline void shapes_writer_put_i32(shapes_writer* w, int32_t v) { + shapes_writer_put_u32(w, (uint32_t)v); +} + +static inline void shapes_writer_put_i64(shapes_writer* w, int64_t v) { + shapes_writer_put_u64(w, (uint64_t)v); +} + +static inline void shapes_writer_put_bool(shapes_writer* w, bool v) { + shapes_writer_put_u8(w, v ? 1 : 0); +} + +static inline void shapes_writer_put_f32(shapes_writer* w, float v) { + uint32_t bits; + memcpy(&bits, &v, 4); + shapes_writer_put_u32(w, bits); +} + +static inline void shapes_writer_put_f64(shapes_writer* w, double v) { + uint64_t bits; + memcpy(&bits, &v, 8); + shapes_writer_put_u64(w, bits); +} + +static inline void shapes_writer_put_len(shapes_writer* w, size_t n) { + if (n > UINT32_MAX) { + w->failed = true; + return; + } + shapes_writer_put_u32(w, (uint32_t)n); +} + +static inline void shapes_writer_put_string(shapes_writer* w, shapes_str v) { + shapes_writer_put_len(w, v.len); + shapes_writer_put_raw(w, v.ptr, v.len); +} + +static inline void shapes_writer_put_bytes(shapes_writer* w, shapes_bytes v) { + shapes_writer_put_len(w, v.len); + shapes_writer_put_raw(w, v.ptr, v.len); +} + +/* Write an object token. The token carries one strong reference, so pass + a reference the buffer may own (the generated codecs pass a fresh + `_clone`). */ +static inline void shapes_writer_put_object(shapes_writer* w, const void* owned) { + shapes_writer_put_u64(w, (uint64_t)(uintptr_t)owned); +} + +/* A cursor over an input buffer. A malformed buffer sets `failed`; every + later read then returns a zero value, so a decoder can always finish and + release what it built. */ +typedef struct shapes_reader { + const uint8_t* ptr; + size_t len; + size_t pos; + bool failed; +} shapes_reader; + +static inline shapes_reader shapes_reader_of(const uint8_t* ptr, size_t len) { + shapes_reader r; + r.ptr = ptr; + r.len = ptr ? len : 0; + r.pos = 0; + r.failed = false; + return r; +} + +/* `true` when every byte was consumed and nothing was malformed. */ +static inline bool shapes_reader_finish(const shapes_reader* r) { + return !r->failed && r->pos == r->len; +} + +static inline const uint8_t* shapes_reader_take(shapes_reader* r, size_t n) { + if (r->failed || n > r->len - r->pos) { + r->failed = true; + return NULL; + } + const uint8_t* at = r->ptr + r->pos; + r->pos += n; + return at; +} + +/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is + zero). `min_wire` is the smallest encoding of one element; a count the + remaining bytes can't hold fails the reader before anything is + allocated. */ +static inline void* shapes_reader_alloc(shapes_reader* r, size_t n, size_t size, size_t min_wire) { + if (r->failed || n == 0) { + return NULL; + } + if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { + r->failed = true; + return NULL; + } + void* p = calloc(n, size); + if (p == NULL) { + r->failed = true; + } + return p; +} + +static inline uint64_t shapes_reader_get_u64(shapes_reader* r) { + const uint8_t* b = shapes_reader_take(r, 8); + uint64_t v = 0; + if (b != NULL) { + for (int i = 0; i < 8; i++) { + v |= (uint64_t)b[i] << (8 * i); + } + } + return v; +} + +static inline uint32_t shapes_reader_get_u32(shapes_reader* r) { + const uint8_t* b = shapes_reader_take(r, 4); + if (b == NULL) { + return 0; + } + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static inline uint16_t shapes_reader_get_u16(shapes_reader* r) { + const uint8_t* b = shapes_reader_take(r, 2); + return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; +} + +static inline uint8_t shapes_reader_get_u8(shapes_reader* r) { + const uint8_t* b = shapes_reader_take(r, 1); + return b ? b[0] : (uint8_t)0; +} + +static inline int8_t shapes_reader_get_i8(shapes_reader* r) { + return (int8_t)shapes_reader_get_u8(r); +} + +static inline int16_t shapes_reader_get_i16(shapes_reader* r) { + return (int16_t)shapes_reader_get_u16(r); +} + +static inline int32_t shapes_reader_get_i32(shapes_reader* r) { + return (int32_t)shapes_reader_get_u32(r); +} + +static inline int64_t shapes_reader_get_i64(shapes_reader* r) { + return (int64_t)shapes_reader_get_u64(r); +} + +static inline bool shapes_reader_get_bool(shapes_reader* r) { + uint8_t v = shapes_reader_get_u8(r); + if (v > 1) { + r->failed = true; + return false; + } + return v == 1; +} + +static inline float shapes_reader_get_f32(shapes_reader* r) { + uint32_t bits = shapes_reader_get_u32(r); + float v; + memcpy(&v, &bits, 4); + return v; +} + +static inline double shapes_reader_get_f64(shapes_reader* r) { + uint64_t bits = shapes_reader_get_u64(r); + double v; + memcpy(&v, &bits, 8); + return v; +} + +/* A `uint32_t` length followed by that many bytes, copied into a fresh + NUL-terminated allocation. */ +static inline uint8_t* shapes_reader_get_run(shapes_reader* r, size_t* out_len) { + size_t n = shapes_reader_get_u32(r); + const uint8_t* at = shapes_reader_take(r, n); + *out_len = 0; + if (at == NULL) { + return NULL; + } + uint8_t* copy = (uint8_t*)malloc(n + 1); + if (copy == NULL) { + r->failed = true; + return NULL; + } + memcpy(copy, at, n); + copy[n] = 0; + *out_len = n; + return copy; +} + +static inline shapes_str shapes_reader_get_string(shapes_reader* r) { + shapes_str v; + v.ptr = (const char*)shapes_reader_get_run(r, &v.len); + return v; +} + +static inline shapes_bytes shapes_reader_get_bytes(shapes_reader* r) { + shapes_bytes v; + v.ptr = shapes_reader_get_run(r, &v.len); + return v; +} + +/* Read an object token and adopt the strong reference it carries. A zero + token in a required position fails the reader. */ +static inline void* shapes_reader_get_object(shapes_reader* r) { + uint64_t token = shapes_reader_get_u64(r); + if (token == 0) { + r->failed = true; + } + return (void*)(uintptr_t)token; +} + +/* --------------------------------------------------------------------------- + * Value types. A record is a struct of its fields; a rich enum is a `tag` + * plus a union `as` with one struct per payload-carrying variant; a list is + * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an + * optional is a pointer that is NULL when absent; an object is its pointer; + * a string or byte string is a view (`ptr` + `len`). + * ------------------------------------------------------------------------- */ + +typedef struct shapes_shapes_Numerics shapes_shapes_Numerics; +typedef struct shapes_shapes_Shape shapes_shapes_Shape; + +/** One field per supported numeric primitive */ +struct shapes_shapes_Numerics { + int8_t a; + int16_t b; + int32_t c; + int64_t d; + uint8_t e; + uint16_t f; + uint32_t g; + uint64_t h; + float i; + double j; +}; + +/** An algebraic shape (sum type with associated data) */ +struct shapes_shapes_Shape { + shapes_shapes_Shape_Tag tag; + union { + /** A circle with a radius */ + struct { + /** Radius in points */ + double radius; + } Circle; + /** An axis-aligned rectangle */ + struct { + float width; + float height; + } Rectangle; + /** An integer-bounded shape (exercises narrow + wide ints) */ + struct { + int16_t min; + int16_t max; + uint64_t weight; + } Bounded; + /** A labeled shape with a small count */ + struct { + shapes_str label; + uint8_t count; + } Labeled; + } as; +}; + +/* --------------------------------------------------------------------------- + * Codecs. Each value type T above, and each optional shape, has four + * functions: + * + * T_write(w, v) appends the value at `v` to writer `w` + * T_read(r, out) decodes one value from reader `r` + * T_decode(ptr, len, out) decodes a whole buffer; on a malformed + * buffer it frees what it built and + * returns false + * T_free(v) releases a decoded value + * + * An optional's T_write takes the nullable pointer itself. Writing an + * object stores a fresh `_clone` reference for the reader to adopt; + * decoding adopts each object reference, and T_free destroys it. T_free + * releases only what T_read or T_decode allocated; never call it on a + * value that borrows caller memory. + * ------------------------------------------------------------------------- */ + +static inline void shapes_shapes_Numerics_write(shapes_writer* w, const shapes_shapes_Numerics* v); +static inline void shapes_shapes_Numerics_read(shapes_reader* r, shapes_shapes_Numerics* out); +static inline bool shapes_shapes_Numerics_decode(const uint8_t* ptr, size_t len, shapes_shapes_Numerics* out); +static inline void shapes_shapes_Numerics_free(shapes_shapes_Numerics* v); +static inline void shapes_shapes_Shape_write(shapes_writer* w, const shapes_shapes_Shape* v); +static inline void shapes_shapes_Shape_read(shapes_reader* r, shapes_shapes_Shape* out); +static inline bool shapes_shapes_Shape_decode(const uint8_t* ptr, size_t len, shapes_shapes_Shape* out); +static inline void shapes_shapes_Shape_free(shapes_shapes_Shape* v); + +static inline void shapes_shapes_Numerics_write(shapes_writer* w, const shapes_shapes_Numerics* v) { + shapes_writer_put_i8(w, v->a); + shapes_writer_put_i16(w, v->b); + shapes_writer_put_i32(w, v->c); + shapes_writer_put_i64(w, v->d); + shapes_writer_put_u8(w, v->e); + shapes_writer_put_u16(w, v->f); + shapes_writer_put_u32(w, v->g); + shapes_writer_put_u64(w, v->h); + shapes_writer_put_f32(w, v->i); + shapes_writer_put_f64(w, v->j); +} + +static inline void shapes_shapes_Numerics_read(shapes_reader* r, shapes_shapes_Numerics* out) { + memset(out, 0, sizeof *out); + out->a = shapes_reader_get_i8(r); + out->b = shapes_reader_get_i16(r); + out->c = shapes_reader_get_i32(r); + out->d = shapes_reader_get_i64(r); + out->e = shapes_reader_get_u8(r); + out->f = shapes_reader_get_u16(r); + out->g = shapes_reader_get_u32(r); + out->h = shapes_reader_get_u64(r); + out->i = shapes_reader_get_f32(r); + out->j = shapes_reader_get_f64(r); +} + +static inline bool shapes_shapes_Numerics_decode(const uint8_t* ptr, size_t len, shapes_shapes_Numerics* out) { + shapes_reader r = shapes_reader_of(ptr, len); + shapes_shapes_Numerics_read(&r, out); + if (!shapes_reader_finish(&r)) { + shapes_shapes_Numerics_free(out); + return false; + } + return true; +} + +static inline void shapes_shapes_Numerics_free(shapes_shapes_Numerics* v) { + memset(v, 0, sizeof *v); +} + +static inline void shapes_shapes_Shape_write(shapes_writer* w, const shapes_shapes_Shape* v) { + shapes_writer_put_i32(w, (int32_t)v->tag); + switch (v->tag) { + case shapes_shapes_Shape_Empty: + break; + case shapes_shapes_Shape_Circle: + shapes_writer_put_f64(w, v->as.Circle.radius); + break; + case shapes_shapes_Shape_Rectangle: + shapes_writer_put_f32(w, v->as.Rectangle.width); + shapes_writer_put_f32(w, v->as.Rectangle.height); + break; + case shapes_shapes_Shape_Bounded: + shapes_writer_put_i16(w, v->as.Bounded.min); + shapes_writer_put_i16(w, v->as.Bounded.max); + shapes_writer_put_u64(w, v->as.Bounded.weight); + break; + case shapes_shapes_Shape_Labeled: + shapes_writer_put_string(w, v->as.Labeled.label); + shapes_writer_put_u8(w, v->as.Labeled.count); + break; + default: + w->failed = true; + break; + } +} + +static inline void shapes_shapes_Shape_read(shapes_reader* r, shapes_shapes_Shape* out) { + memset(out, 0, sizeof *out); + int32_t tag = shapes_reader_get_i32(r); + switch (tag) { + case shapes_shapes_Shape_Empty: + out->tag = shapes_shapes_Shape_Empty; + break; + case shapes_shapes_Shape_Circle: + out->tag = shapes_shapes_Shape_Circle; + out->as.Circle.radius = shapes_reader_get_f64(r); + break; + case shapes_shapes_Shape_Rectangle: + out->tag = shapes_shapes_Shape_Rectangle; + out->as.Rectangle.width = shapes_reader_get_f32(r); + out->as.Rectangle.height = shapes_reader_get_f32(r); + break; + case shapes_shapes_Shape_Bounded: + out->tag = shapes_shapes_Shape_Bounded; + out->as.Bounded.min = shapes_reader_get_i16(r); + out->as.Bounded.max = shapes_reader_get_i16(r); + out->as.Bounded.weight = shapes_reader_get_u64(r); + break; + case shapes_shapes_Shape_Labeled: + out->tag = shapes_shapes_Shape_Labeled; + out->as.Labeled.label = shapes_reader_get_string(r); + out->as.Labeled.count = shapes_reader_get_u8(r); + break; + default: + r->failed = true; + break; + } +} + +static inline bool shapes_shapes_Shape_decode(const uint8_t* ptr, size_t len, shapes_shapes_Shape* out) { + shapes_reader r = shapes_reader_of(ptr, len); + shapes_shapes_Shape_read(&r, out); + if (!shapes_reader_finish(&r)) { + shapes_shapes_Shape_free(out); + return false; + } + return true; +} + +static inline void shapes_shapes_Shape_free(shapes_shapes_Shape* v) { + switch (v->tag) { + case shapes_shapes_Shape_Labeled: + shapes_str_free(&v->as.Labeled.label); + break; + default: + break; + } + memset(v, 0, sizeof *v); +} + +#ifdef __cplusplus +} +#endif + +#endif // SHAPES_BUFFER_H + +// END shapes_buffer.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_h.snap b/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_h.snap new file mode 100644 index 00000000..df20c3f5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_shapes__shapes_h.snap @@ -0,0 +1,151 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef SHAPES_H +#define SHAPES_H + +#include +#include +#include + +#ifndef SHAPES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef SHAPES_BUILD +# define SHAPES_API __declspec(dllexport) +# else +# define SHAPES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define SHAPES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define SHAPES_API __attribute__((visibility("default"))) +# else +# define SHAPES_API +# endif +#endif + +#ifndef SHAPES_DEPRECATED +# if defined(_MSC_VER) +# define SHAPES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define SHAPES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define SHAPES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports shapes_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define SHAPES_ABI_VERSION 3u +SHAPES_API uint32_t shapes_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by shapes_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct shapes_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} shapes_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +SHAPES_API void shapes_error_set(shapes_error* err, int32_t code, const char* message); +SHAPES_API void shapes_error_clear(shapes_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + shapes_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +SHAPES_API void shapes_error_free(shapes_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +SHAPES_API void shapes_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct shapes_cancel_token shapes_cancel_token; +SHAPES_API shapes_cancel_token* shapes_cancel_token_create(void); +SHAPES_API void shapes_cancel_token_cancel(shapes_cancel_token* token); +SHAPES_API bool shapes_cancel_token_is_cancelled(const shapes_cancel_token* token); +SHAPES_API void shapes_cancel_token_destroy(shapes_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +SHAPES_API uint8_t* shapes_alloc(uint32_t size); +SHAPES_API void shapes_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +SHAPES_API uint64_t shapes_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define SHAPES_SHAPES_CHECKSUM 0x06acf433ffa2a0f0ull +SHAPES_API uint64_t shapes_shapes_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with shapes_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * shapes_buffer.h declares a C struct and codec for each. + */ + +/** An algebraic shape (sum type with associated data) */ +typedef enum { + /** The empty shape (a unit variant) */ + shapes_shapes_Shape_Empty = 0, + /** A circle with a radius */ + shapes_shapes_Shape_Circle = 1, + /** An axis-aligned rectangle */ + shapes_shapes_Shape_Rectangle = 2, + /** An integer-bounded shape (exercises narrow + wide ints) */ + shapes_shapes_Shape_Bounded = 3, + /** A labeled shape with a small count */ + shapes_shapes_Shape_Labeled = 4 +} shapes_shapes_Shape_Tag; +/** A plain C-style enum (no payloads) */ +typedef enum { shapes_shapes_Channel_Red = 0, shapes_shapes_Channel_Green = 1, shapes_shapes_Channel_Blue = 2 } shapes_shapes_Channel; + +// Module: shapes +/** Render a shape to a string */ +SHAPES_API const uint8_t* shapes_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, shapes_error* out_err); +/** Scale a shape by a factor, returning a new shape */ +SHAPES_API const uint8_t* shapes_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, shapes_error* out_err); +/** Sum a list of bytes into a wide integer (numerics smoke) */ +SHAPES_API uint64_t shapes_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, shapes_error* out_err); +/** Build a Numerics struct from every numeric primitive */ +SHAPES_API const uint8_t* shapes_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, shapes_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // SHAPES_H + +// END shapes.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_shapes__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_shapes__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap deleted file mode 100644 index 3d9b9736..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap +++ /dev/null @@ -1,137 +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 - -/* 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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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. - */ - -/** An algebraic shape (sum type with associated data) */ -typedef enum { - /** The empty shape (a unit variant) */ - weaveffi_shapes_Shape_Empty = 0, - /** A circle with a radius */ - weaveffi_shapes_Shape_Circle = 1, - /** An axis-aligned rectangle */ - weaveffi_shapes_Shape_Rectangle = 2, - /** An integer-bounded shape (exercises narrow + wide ints) */ - weaveffi_shapes_Shape_Bounded = 3, - /** A labeled shape with a small count */ - weaveffi_shapes_Shape_Labeled = 4 -} weaveffi_shapes_Shape_Tag; -/** A plain C-style enum (no payloads) */ -typedef enum { weaveffi_shapes_Channel_Red = 0, weaveffi_shapes_Channel_Green = 1, weaveffi_shapes_Channel_Blue = 2 } weaveffi_shapes_Channel; - -// Module: shapes -/** Render a shape to a string */ -WEAVEFFI_API const char* weaveffi_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, weaveffi_error* out_err); -/** Scale a shape by a factor, returning a new shape */ -WEAVEFFI_API const uint8_t* weaveffi_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, weaveffi_error* out_err); -/** Sum a list of bytes into a wide integer (numerics smoke) */ -WEAVEFFI_API uint64_t weaveffi_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, weaveffi_error* out_err); -/** Build a Numerics struct from every numeric primitive */ -WEAVEFFI_API const uint8_t* weaveffi_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, weaveffi_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__CMakeLists_txt.snap index f169a251..b817eb9d 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__CMakeLists_txt.snap @@ -1,15 +1,26 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.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) +project(docs_everywhere_cpp VERSION 0.1.0 LANGUAGES CXX) + +# The producer library to link: a full path from the DOCS_EVERYWHERE_LIBRARY cache or +# environment variable, or else the library name `docs_everywhere` (a CMake target +# of that name, or libdocs_everywhere on the linker search path). +set(DOCS_EVERYWHERE_LIBRARY "$ENV{DOCS_EVERYWHERE_LIBRARY}" CACHE FILEPATH "Path to the docs_everywhere native library") + +add_library(docs_everywhere_cpp INTERFACE) +add_library(docs_everywhere::cpp ALIAS docs_everywhere_cpp) +target_include_directories(docs_everywhere_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_features(docs_everywhere_cpp INTERFACE cxx_std_17) +if(DOCS_EVERYWHERE_LIBRARY) + target_link_libraries(docs_everywhere_cpp INTERFACE "${DOCS_EVERYWHERE_LIBRARY}") +else() + target_link_libraries(docs_everywhere_cpp INTERFACE docs_everywhere) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__README_md.snap index da3ea6c6..e1d8ad02 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__README_md.snap @@ -1,39 +1,43 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI C++ Bindings +# docs_everywhere C++ bindings -## Prerequisites +A header-only C++17 wrapper over the `docs_everywhere` C ABI. `docs_everywhere.hpp` +includes the C header beside it; every declaration lives in +`namespace docs_everywhere`. -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate +## Prerequisites -## Usage with CMake +- CMake 3.14 or later +- A C++17 compiler +- The `docs_everywhere` native library (`libdocs_everywhere.so`, `libdocs_everywhere.dylib`, or + `docs_everywhere.dll`) -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: +## Use with CMake ```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) +add_subdirectory(path/to/cpp) +add_executable(app main.cpp) +target_link_libraries(app PRIVATE docs_everywhere::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: +`docs_everywhere::cpp` adds this directory to the include path, requires +C++17, and links the native library: the file named by the `DOCS_EVERYWHERE_LIBRARY` +CMake cache variable or environment variable when set, or else +`docs_everywhere` by name. ```cpp -#include "weaveffi.hpp" +#include "docs_everywhere.hpp" ``` +The first free function, constructor, or static member you call checks +that the linked library matches these headers and throws +`docs_everywhere::LoadError` if it doesn't; call `docs_everywhere::check_library()` +at startup to check eagerly. + diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_h.snap b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_h.snap new file mode 100644 index 00000000..31ab85e2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_h.snap @@ -0,0 +1,196 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef DOCS_EVERYWHERE_H +#define DOCS_EVERYWHERE_H + +#include +#include +#include + +#ifndef DOCS_EVERYWHERE_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef DOCS_EVERYWHERE_BUILD +# define DOCS_EVERYWHERE_API __declspec(dllexport) +# else +# define DOCS_EVERYWHERE_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define DOCS_EVERYWHERE_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define DOCS_EVERYWHERE_API __attribute__((visibility("default"))) +# else +# define DOCS_EVERYWHERE_API +# endif +#endif + +#ifndef DOCS_EVERYWHERE_DEPRECATED +# if defined(_MSC_VER) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define DOCS_EVERYWHERE_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports docs_everywhere_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define DOCS_EVERYWHERE_ABI_VERSION 3u +DOCS_EVERYWHERE_API uint32_t docs_everywhere_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by docs_everywhere_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct docs_everywhere_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} docs_everywhere_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_set(docs_everywhere_error* err, int32_t code, const char* message); +DOCS_EVERYWHERE_API void docs_everywhere_error_clear(docs_everywhere_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + docs_everywhere_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_free(docs_everywhere_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +DOCS_EVERYWHERE_API void docs_everywhere_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct docs_everywhere_cancel_token docs_everywhere_cancel_token; +DOCS_EVERYWHERE_API docs_everywhere_cancel_token* docs_everywhere_cancel_token_create(void); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_cancel(docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API bool docs_everywhere_cancel_token_is_cancelled(const docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_destroy(docs_everywhere_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +DOCS_EVERYWHERE_API uint8_t* docs_everywhere_alloc(uint32_t size); +DOCS_EVERYWHERE_API void docs_everywhere_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +DOCS_EVERYWHERE_API uint64_t docs_everywhere_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define DOCS_EVERYWHERE_DOCS_CHECKSUM 0x7950eec1e9cfc819ull +DOCS_EVERYWHERE_API uint64_t docs_everywhere_docs_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with docs_everywhere_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * docs_everywhere_buffer.h declares a C struct and codec for each. + */ + +/** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ +typedef enum { + /** Document is being edited */ + docs_everywhere_docs_Status_Draft = 0, + /** Document is publicly visible */ + docs_everywhere_docs_Status_Published = 1, + /** Document is hidden from listings */ + docs_everywhere_docs_Status_Archived = 2 +} docs_everywhere_docs_Status; +/** Error codes reported by throwing functions in the `docs` module tree. */ +typedef enum { + /** Returned when no document matches the requested id */ + docs_everywhere_docs_DocsErrors_not_found = 1, + /** Raised when attempting to move into a forbidden state */ + docs_everywhere_docs_DocsErrors_invalid_status = 2 +} docs_everywhere_docs_DocsErrors; +typedef struct docs_everywhere_docs_Notebook docs_everywhere_docs_Notebook; +/** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + * + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct docs_everywhere_docs_SavedListener_vtable { + /** Fires after a document has been persisted successfully */ + void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); + void (*free)(void* ctx); +} docs_everywhere_docs_SavedListener_vtable; +typedef void (*docs_everywhere_docs_publish_async_callback)(void* context, docs_everywhere_error* err, bool result); + +// Module: docs +/** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ +/** Create an empty notebook */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_new(docs_everywhere_error* out_err); +/** The largest number of documents one notebook will hold */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_max_documents(docs_everywhere_error* out_err); +/** Add a document, returning its position in the notebook */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_insert(const docs_everywhere_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, docs_everywhere_error* out_err); +/** Look up a document by identifier, failing when none matches */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_Notebook_find(const docs_everywhere_docs_Notebook* self, int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** The number of documents currently held */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_size(const docs_everywhere_docs_Notebook* self, docs_everywhere_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_clone(const docs_everywhere_docs_Notebook* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_Notebook_destroy(docs_everywhere_docs_Notebook* self); + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_create_document(const uint8_t* title_ptr, size_t title_len, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, docs_everywhere_error* out_err); +/** Look up a document by identifier */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_get_document(int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** Publish a document in the background */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_publish_async(int64_t id, docs_everywhere_docs_publish_async_callback callback, void* context); +/** Register a listener for saved notifications */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_watch(void* listener_ctx, const docs_everywhere_docs_SavedListener_vtable* listener_vtable, docs_everywhere_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // DOCS_EVERYWHERE_H + +// END docs_everywhere.h diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_hpp.snap new file mode 100644 index 00000000..30368ff1 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__docs_everywhere_hpp.snap @@ -0,0 +1,764 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#pragma once + +#include "docs_everywhere.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace docs_everywhere { + +/** + * Base exception for every error reported through the C ABI. + * + * Positive codes are the declared error codes (see the typed domain + * exceptions); negative codes are runtime traps: -1 a generic producer + * error, -2 a producer panic, -3 a marshalling failure, -4 a + * callback-interface implementation that threw, and -5 a cancelled call + * (thrown as Cancelled). + */ +class Error : public std::runtime_error { + int32_t code_; + +public: + /** Builds an error carrying `code` and `message`. */ + Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} + + /** The numeric error code. */ + int32_t code() const noexcept { return code_; } +}; + +/** Thrown by an async call whose CancelToken was cancelled (code -5). */ +class Cancelled : public Error { +public: + /** Builds a cancellation error carrying the producer's message. */ + explicit Cancelled(const std::string& message) : Error(-5, message) {} +}; + +/** + * Thrown on first use when the linked docs_everywhere library was built for a + * different C ABI revision or a different API contract than this header. + */ +class LoadError : public Error { +public: + /** Builds a load error describing the mismatch. */ + explicit LoadError(const std::string& message) : Error(-1, message) {} +}; + +/** + * Tag selecting the constructor that adopts a raw C pointer (one strong + * reference the wrapper then owns), in the style of std::adopt_lock. + */ +struct adopt_t { + explicit adopt_t() = default; +}; + +/** The adopt_t tag value: `Gadget g(adopt, raw)`. */ +inline constexpr adopt_t adopt{}; + +/** + * A cancellation token for cancellable async functions. + * + * Create one, pass it to any number of cancellable calls, and call cancel() + * from any thread. A cancelled call settles its future with Cancelled. Each + * call takes its own reference to the native token, so the token can be + * destroyed at any time; the destructor releases this wrapper's reference. + */ +class CancelToken { + docs_everywhere_cancel_token* handle_; + + struct none_t {}; + explicit CancelToken(none_t) noexcept : handle_(nullptr) {} + +public: + /** Creates a new, uncancelled token. */ + CancelToken() : handle_(docs_everywhere_cancel_token_create()) { + if (handle_ == nullptr) throw std::bad_alloc(); + } + + /** Releases this wrapper's reference to the native token. */ + ~CancelToken() { + if (handle_ != nullptr) docs_everywhere_cancel_token_destroy(handle_); + } + + CancelToken(const CancelToken&) = delete; + CancelToken& operator=(const CancelToken&) = delete; + + /** Transfers the native token; `other` becomes empty. */ + CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + + /** Releases the current token and takes over `other`'s. */ + CancelToken& operator=(CancelToken&& other) noexcept { + if (this != &other) { + if (handle_ != nullptr) docs_everywhere_cancel_token_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /** The empty token a cancellable call uses when none is passed. */ + static const CancelToken& none() noexcept { + static const CancelToken token{none_t{}}; + return token; + } + + /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ + void cancel() const noexcept { + if (handle_ != nullptr) docs_everywhere_cancel_token_cancel(handle_); + } + + /** Whether cancel() has been called. */ + bool is_cancelled() const noexcept { + return handle_ != nullptr && docs_everywhere_cancel_token_is_cancelled(handle_); + } + + /** The native token, borrowed (null for an empty token). */ + docs_everywhere_cancel_token* handle() const noexcept { return handle_; } +}; + +namespace detail { + +/** The exception for a runtime (non-domain) error code. */ +inline std::exception_ptr make_error(int32_t code, const std::string& message) { + if (code == -5) return std::make_exception_ptr(Cancelled(message)); + return std::make_exception_ptr(Error(code, message)); +} + +/** The message of a nonzero error slot. */ +inline std::string error_message(const docs_everywhere_error& err) { + return std::string(err.message != nullptr ? err.message : "unknown error"); +} + +/** Throws the generic Error (or Cancelled) if `err` carries a nonzero code. */ +inline void check(docs_everywhere_error& err) { + if (err.code == 0) return; + std::exception_ptr ex = make_error(err.code, error_message(err)); + docs_everywhere_error_clear(&err); + std::rethrow_exception(ex); +} + +/** Copies a producer-returned UTF-8 run into a string, then releases it. */ +inline std::string take_string(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::string(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { docs_everywhere_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::string(reinterpret_cast(ptr), len); +} + +/** Copies a producer-returned byte run into a vector, then releases it. */ +inline std::vector take_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::vector(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { docs_everywhere_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::vector(ptr, ptr + len); +} + +/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ +inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { + if (ptr == nullptr || len == 0) return std::string_view(); + return std::string_view(reinterpret_cast(ptr), len); +} + +/** Views a borrowed byte run (null when empty) as an owned vector. */ +inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr || len == 0) return std::vector(); + return std::vector(ptr, ptr + len); +} + +/** The data pointer of a string argument, as the C ABI expects it. */ +inline const uint8_t* utf8(std::string_view s) noexcept { + return reinterpret_cast(s.data()); +} + +} // namespace detail + +/** + * Verifies that the linked docs_everywhere library implements the C ABI revision + * and the module contracts this header was generated against, throwing + * LoadError naming the first mismatch. The check runs once; every free + * function, constructor, and static member calls this before its first + * native call, so calling it at startup is optional. + */ +inline void check_library() { + static const std::string failure = [] { + uint32_t abi = docs_everywhere_abi_version(); + if (abi != DOCS_EVERYWHERE_ABI_VERSION) { + return "docs_everywhere: C ABI revision mismatch (header " + std::to_string(DOCS_EVERYWHERE_ABI_VERSION) + + ", library " + std::to_string(abi) + ")"; + } + if (docs_everywhere_docs_checksum() != 0x7950eec1e9cfc819ull) { + return std::string("docs_everywhere: module 'docs' does not match the linked library (contract checksum mismatch); regenerate the bindings for this build"); + } + return std::string(); + }(); + if (!failure.empty()) throw LoadError(failure); +} + +namespace detail { + +/** + * Serializes values into the value-buffer wire format: little-endian, packed + * with no alignment, lengths and element counts as u32. + */ +class BufferWriter { + std::vector buf_; + + template + void append_le(T v) { + for (size_t i = 0; i < sizeof(T); ++i) { + buf_.push_back(static_cast(v >> (8 * i))); + } + } + +public: + /** Pointer to the encoded bytes. */ + const uint8_t* data() const noexcept { return buf_.data(); } + + /** Number of encoded bytes. */ + size_t size() const noexcept { return buf_.size(); } + + void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } + void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } + void write_u8(uint8_t v) { buf_.push_back(v); } + void write_i16(int16_t v) { append_le(static_cast(v)); } + void write_u16(uint16_t v) { append_le(v); } + void write_i32(int32_t v) { append_le(static_cast(v)); } + void write_u32(uint32_t v) { append_le(v); } + void write_i64(int64_t v) { append_le(static_cast(v)); } + void write_u64(uint64_t v) { append_le(v); } + + void write_f32(float v) { + uint32_t bits = 0; + std::memcpy(&bits, &v, sizeof(bits)); + append_le(bits); + } + + void write_f64(double v) { + uint64_t bits = 0; + std::memcpy(&bits, &v, sizeof(bits)); + append_le(bits); + } + + /** Writes a byte length or an element count as a u32. */ + void write_len(size_t n) { + if (static_cast(n) > UINT32_MAX) throw Error(-3, "value buffer length exceeds u32"); + append_le(static_cast(n)); + } + + void write_string(std::string_view v) { + write_len(v.size()); + buf_.insert(buf_.end(), v.begin(), v.end()); + } + + void write_bytes(const std::vector& v) { + write_len(v.size()); + buf_.insert(buf_.end(), v.begin(), v.end()); + } + + /** Writes an optional's presence flag: 0 absent, 1 present. */ + void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } +}; + +/** + * Decodes values from the value-buffer wire format. A malformed buffer is a + * producer/consumer contract violation (both sides are generated from one + * API), so every decode failure throws the generic Error with the + * marshalling code -3. Object tokens adopted before a failure live in RAII + * wrappers that release them on unwind. + */ +class BufferReader { + const uint8_t* data_; + size_t len_; + size_t pos_; + + [[noreturn]] static void fail(const char* what) { + throw Error(-3, std::string("malformed 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) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} + + /** Bytes not yet consumed. */ + size_t remaining() const noexcept { return len_ - pos_; } + + bool read_bool() { + uint8_t b = read_le("bool"); + if (b > 1) fail("bool byte out of range"); + return b != 0; + } + + int8_t read_i8() { return read_le("i8"); } + uint8_t read_u8() { return read_le("u8"); } + int16_t read_i16() { return read_le("i16"); } + uint16_t read_u16() { return read_le("u16"); } + int32_t read_i32() { return read_le("i32"); } + uint32_t read_u32() { return read_le("u32"); } + int64_t read_i64() { return read_le("i64"); } + uint64_t read_u64() { return read_le("u64"); } + + float read_f32() { + uint32_t bits = read_le("f32"); + float v = 0; + std::memcpy(&v, &bits, sizeof(v)); + return v; + } + + double read_f64() { + uint64_t bits = read_le("f64"); + double v = 0; + std::memcpy(&v, &bits, sizeof(v)); + return v; + } + + /** Reads a byte length, rejecting one larger than the bytes remaining. */ + size_t read_len() { + uint32_t n = read_le("length"); + if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); + return static_cast(n); + } + + /** + * Reads an element count. Elements may encode to zero bytes, so a count + * larger than the bytes remaining is legal; callers cap preallocation + * with reserve_hint(). + */ + size_t read_count() { return static_cast(read_le("count")); } + + /** A preallocation size for `n` elements that a malformed count can't inflate. */ + size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } + + std::string read_string() { + size_t n = read_len(); + std::string v(reinterpret_cast(data_) + pos_, n); + pos_ += n; + return v; + } + + std::vector read_bytes() { + size_t n = read_len(); + std::vector v(data_ + pos_, data_ + pos_ + n); + pos_ += n; + return v; + } + + bool read_option_flag() { + uint8_t b = read_le("option flag"); + if (b > 1) fail("option flag byte out of range"); + return b != 0; + } + + /** Rejects unconsumed bytes after decoding a complete value. */ + void expect_end() const { + if (pos_ != len_) fail("trailing bytes after value"); + } +}; + +/** Releases a producer-allocated buffer with docs_everywhere_free_bytes on scope exit. */ +struct BufferGuard { + /** The producer-allocated buffer, or null when the call reported an error. */ + const uint8_t* ptr; + /** The buffer length in bytes. */ + size_t len; + + BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} + BufferGuard(const BufferGuard&) = delete; + BufferGuard& operator=(const BufferGuard&) = delete; + + ~BufferGuard() { + if (ptr != nullptr) docs_everywhere_free_bytes(const_cast(ptr), len); + } +}; + +} // namespace detail + +/** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ +enum class Status : int32_t { + /** Document is being edited */ + Draft = 0, + /** Document is publicly visible */ + Published = 1, + /** Document is hidden from listings */ + Archived = 2 +}; + +struct Document; +class Notebook; +class SavedListener; + +/** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ +class Notebook { + docs_everywhere_docs_Notebook* handle_; + +public: + /** The C type this class wraps. */ + using raw_type = docs_everywhere_docs_Notebook; + + /** Adopts one strong reference to a producer object: `Notebook(adopt, raw)`. */ + explicit Notebook(adopt_t, docs_everywhere_docs_Notebook* h) noexcept : handle_(h) {} + + /** Releases this wrapper's reference; the object is dropped with its last one. */ + ~Notebook() { + if (handle_) docs_everywhere_docs_Notebook_destroy(handle_); + } + + /** Copies share the object: the copy takes a new strong reference. */ + Notebook(const Notebook& other) : handle_(docs_everywhere_docs_Notebook_clone(other.handle_)) {} + + Notebook& operator=(const Notebook& other) { + if (this != &other) { + docs_everywhere_docs_Notebook* h = docs_everywhere_docs_Notebook_clone(other.handle_); + if (handle_) docs_everywhere_docs_Notebook_destroy(handle_); + handle_ = h; + } + return *this; + } + + Notebook(Notebook&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + + Notebook& operator=(Notebook&& other) noexcept { + if (this != &other) { + if (handle_) docs_everywhere_docs_Notebook_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /** The wrapped pointer, borrowed: this wrapper keeps its reference. */ + const docs_everywhere_docs_Notebook* handle() const { return handle_; } + + /** A new strong reference the caller owns (for example to write into a value buffer). */ + docs_everywhere_docs_Notebook* clone_handle() const { return docs_everywhere_docs_Notebook_clone(handle_); } + + /** Create an empty notebook */ + explicit Notebook(); + + /** Add a document, returning its position in the notebook */ + int32_t insert(const Document& document) const; + + /** Look up a document by identifier, failing when none matches */ + Document find(int64_t id) const; + + /** The number of documents currently held */ + int32_t size() const; + + /** The largest number of documents one notebook will hold */ + static int32_t max_documents(); + +}; + +/** + * Represents a single document tracked by the system. + * + * Documents are persisted to disk and exposed via the public API. + */ +struct Document { + /** Stable opaque identifier */ + int64_t id; + /** Human-readable title shown in lists */ + std::string title; + /** Current lifecycle status */ + Status status; + /** Free-form tags for grouping */ + std::vector tags; +}; + +namespace detail { + +/** Encodes a `Document` in the value-buffer wire format. */ +inline void write_Document(BufferWriter& w, const Document& v) { + w.write_i64(v.id); + w.write_string(v.title); + w.write_i32(static_cast(v.status)); + w.write_len(v.tags.size()); + for (const auto& item0 : v.tags) { + w.write_string(item0); + } +} + +/** Decodes a `Document` from the value-buffer wire format. */ +inline Document read_Document(BufferReader& r) { + int64_t f_id = r.read_i64(); + std::string f_title = r.read_string(); + Status f_status = static_cast(r.read_i32()); + std::vector f_tags{}; + { + size_t f_tags_n = r.read_count(); + f_tags.reserve(r.reserve_hint(f_tags_n)); + for (size_t f_tags_i = 0; f_tags_i < f_tags_n; ++f_tags_i) { + std::string f_tags_item = r.read_string(); + f_tags.push_back(std::move(f_tags_item)); + } + } + return Document{std::move(f_id), std::move(f_title), std::move(f_status), std::move(f_tags)}; +} + +} // namespace detail + +/** Typed errors reported by the `docs` module's throwing functions. */ +class DocsErrorsError : public Error { +public: + DocsErrorsError(int32_t code, const std::string& msg) : Error(code, msg) {} +}; + +/** Returned when no document matches the requested id */ +class NotFoundError : public DocsErrorsError { +public: + explicit NotFoundError(const std::string& msg) : DocsErrorsError(1, msg) {} +}; + +/** Raised when attempting to move into a forbidden state */ +class InvalidStatusError : public DocsErrorsError { +public: + explicit InvalidStatusError(const std::string& msg) : DocsErrorsError(2, msg) {} +}; + +namespace detail { + +/** Map a `docs` error code and payload to its typed exception (the runtime exception for negative codes, DocsErrorsError for unknown codes). */ +inline std::exception_ptr make_docs_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { + if (code < 0) return make_error(code, msg); + (void)payload_ptr; + (void)payload_len; + switch (code) { + case 1: return std::make_exception_ptr(NotFoundError(msg)); + case 2: return std::make_exception_ptr(InvalidStatusError(msg)); + default: return std::make_exception_ptr(DocsErrorsError(code, msg)); + } +} + +/** Throw the typed `docs` exception if `err` carries a nonzero code. */ +inline void check_docs(docs_everywhere_error& err) { + if (err.code == 0) return; + std::string msg = error_message(err); + std::exception_ptr ex = make_docs_error(err.code, msg, err.payload_ptr, err.payload_len); + docs_everywhere_error_clear(&err); + std::rethrow_exception(ex); +} + +} // namespace detail + +/** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + * + * + * Implement this interface and pass a `std::shared_ptr` to any function that accepts one. The producer may call the methods from any thread until it releases its last reference to the implementation. An exception thrown by a method is reported to the producer as a foreign error (code -4) and aborts the call that triggered it. + */ +class SavedListener { +public: + virtual ~SavedListener() = default; + + /** Fires after a document has been persisted successfully */ + virtual void on_saved(int64_t doc_id, int32_t revision) = 0; +}; + +namespace detail { + +/** Vtable trampolines adapting a `SavedListener` implementation to `docs_everywhere_docs_SavedListener_vtable`. */ +struct SavedListener_trampolines { + static void on_saved(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err) { + try { + SavedListener& impl = **static_cast*>(ctx); + impl.on_saved(doc_id, revision); + return; + } catch (const std::exception& e) { + docs_everywhere_error_set(out_err, -4, e.what()); + } catch (...) { + docs_everywhere_error_set(out_err, -4, "SavedListener::on_saved threw a non-standard exception"); + } + } + + /** Releases the implementation box once the producer drops its last reference. */ + static void free_ctx(void* ctx) { + delete static_cast*>(ctx); + } +}; + +/** The process-wide vtable every `SavedListener` implementation is passed with. */ +inline const docs_everywhere_docs_SavedListener_vtable& SavedListener_vtable() { + static const docs_everywhere_docs_SavedListener_vtable vtable = { + &SavedListener_trampolines::on_saved, + &SavedListener_trampolines::free_ctx, + }; + return vtable; +} + +} // namespace detail + +inline Notebook::Notebook() : handle_(nullptr) { + check_library(); + docs_everywhere_error err{}; + auto result = docs_everywhere_docs_Notebook_new(&err); + detail::check(err); + handle_ = result; +} + +inline int32_t Notebook::insert(const Document& document) const { + detail::BufferWriter document_buf; + detail::write_Document(document_buf, document); + docs_everywhere_error err{}; + auto result = docs_everywhere_docs_Notebook_insert(handle_, document_buf.data(), document_buf.size(), &err); + detail::check(err); + return result; +} + +inline Document Notebook::find(int64_t id) const { + size_t out_len = 0; + docs_everywhere_error err{}; + auto result = docs_everywhere_docs_Notebook_find(handle_, id, &out_len, &err); + detail::check_docs(err); + detail::BufferGuard result_guard(result, out_len); + detail::BufferReader result_r(result, out_len); + Document ret = detail::read_Document(result_r); + result_r.expect_end(); + return ret; +} + +inline int32_t Notebook::size() const { + docs_everywhere_error err{}; + auto result = docs_everywhere_docs_Notebook_size(handle_, &err); + detail::check(err); + return result; +} + +inline int32_t Notebook::max_documents() { + check_library(); + docs_everywhere_error err{}; + auto result = docs_everywhere_docs_Notebook_max_documents(&err); + detail::check(err); + return result; +} + +namespace docs { + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +inline Document create_document(std::string_view title, const std::vector& tags) { + check_library(); + detail::BufferWriter tags_buf; + tags_buf.write_len(tags.size()); + for (const auto& item0 : tags) { + tags_buf.write_string(item0); + } + size_t out_len = 0; + docs_everywhere_error err{}; + auto result = docs_everywhere_docs_create_document(detail::utf8(title), title.size(), tags_buf.data(), tags_buf.size(), &out_len, &err); + detail::check(err); + detail::BufferGuard result_guard(result, out_len); + detail::BufferReader result_r(result, out_len); + Document ret = detail::read_Document(result_r); + result_r.expect_end(); + return ret; +} + +/** Look up a document by identifier */ +inline std::optional get_document(int64_t id) { + check_library(); + size_t out_len = 0; + docs_everywhere_error err{}; + auto result = docs_everywhere_docs_get_document(id, &out_len, &err); + detail::check(err); + detail::BufferGuard result_guard(result, out_len); + detail::BufferReader result_r(result, out_len); + std::optional ret{}; + if (result_r.read_option_flag()) { + Document ret_v = detail::read_Document(result_r); + ret = std::move(ret_v); + } + result_r.expect_end(); + return ret; +} + +/** Publish a document in the background */ +inline std::future publish_async(int64_t id) { + check_library(); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + docs_everywhere_docs_publish_async(id, [](void* context, docs_everywhere_error* err, bool result) { + std::unique_ptr> p(static_cast*>(context)); + try { + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); + p->set_exception(detail::make_error(err->code, msg)); + } else { + p->set_value(result); + } + } catch (...) { + p->set_exception(std::current_exception()); + } + docs_everywhere_error_free(err); + }, static_cast(promise.release())); + return future; +} + +/** Register a listener for saved notifications */ +inline void watch(std::shared_ptr listener) { + check_library(); + if (!listener) throw std::invalid_argument("listener: null callback interface"); + auto listener_ctx = std::make_unique>(std::move(listener)); + docs_everywhere_error err{}; + docs_everywhere_docs_watch(static_cast(listener_ctx.release()), &detail::SavedListener_vtable(), &err); + detail::check(err); +} + +} // namespace docs + +} // namespace docs_everywhere + +// END docs_everywhere.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap deleted file mode 100644 index 2aa4e27f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap +++ /dev/null @@ -1,733 +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" { - -/* 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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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); - -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -typedef enum { - /** Document is being edited */ - weaveffi_docs_Status_Draft = 0, - /** Document is publicly visible */ - weaveffi_docs_Status_Published = 1, - /** Document is hidden from listings */ - weaveffi_docs_Status_Archived = 2 -} weaveffi_docs_Status; -/** Error codes reported by throwing functions in the `docs` module tree. */ -typedef enum { - /** Returned when no document matches the requested id */ - weaveffi_docs_DocsErrors_not_found = 1, - /** Raised when attempting to move into a forbidden state */ - weaveffi_docs_DocsErrors_invalid_status = 2 -} weaveffi_docs_DocsErrors; -typedef struct weaveffi_docs_Notebook weaveffi_docs_Notebook; -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - * - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. - */ -typedef struct weaveffi_docs_SavedListener_vtable { - /** Fires after a document has been persisted successfully */ - void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_docs_SavedListener_vtable; -typedef void (*weaveffi_docs_publish_async_callback)(void* context, weaveffi_error* err, bool result); - -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ -/** Create an empty notebook */ -WEAVEFFI_API weaveffi_docs_Notebook* weaveffi_docs_Notebook_new(weaveffi_error* out_err); -/** The largest number of documents one notebook will hold */ -WEAVEFFI_API int32_t weaveffi_docs_Notebook_max_documents(weaveffi_error* out_err); -/** Add a document, returning its position in the notebook */ -WEAVEFFI_API int32_t weaveffi_docs_Notebook_insert(const weaveffi_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, weaveffi_error* out_err); -/** Look up a document by identifier, failing when none matches */ -WEAVEFFI_API const uint8_t* weaveffi_docs_Notebook_find(const weaveffi_docs_Notebook* self, int64_t id, size_t* out_len, weaveffi_error* out_err); -/** The number of documents currently held */ -WEAVEFFI_API int32_t weaveffi_docs_Notebook_size(const weaveffi_docs_Notebook* self, weaveffi_error* out_err); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_docs_Notebook* weaveffi_docs_Notebook_clone(const weaveffi_docs_Notebook* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_docs_Notebook_destroy(weaveffi_docs_Notebook* self); - -/** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - */ -WEAVEFFI_API const uint8_t* weaveffi_docs_create_document(const char* title, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, weaveffi_error* out_err); -/** Look up a document by identifier */ -WEAVEFFI_API const uint8_t* weaveffi_docs_get_document(int64_t id, size_t* out_len, weaveffi_error* out_err); -/** Publish a document in the background */ -WEAVEFFI_API void weaveffi_docs_publish_async_async(int64_t id, weaveffi_docs_publish_async_callback callback, void* context); -/** Register a listener for saved notifications */ -WEAVEFFI_API void weaveffi_docs_watch(void* listener_ctx, const weaveffi_docs_SavedListener_vtable* listener_vtable, weaveffi_error* out_err); - -} // extern "C" - -namespace weaveffi { - -/** - * Base exception for every error reported through the C ABI. - * - * Positive codes are the module's declared error codes (see the typed - * domain exceptions); negative codes are runtime traps: -1 a generic - * producer error, -2 a producer panic, -3 a marshalling failure, -4 a - * callback-interface implementation that threw. - */ -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_; } -}; - -/** - * Throws WeaveFFIError when the loaded producer library was built for a - * different WeaveFFI ABI revision than this header. Call once at startup. - */ -inline void check_abi_version() { - uint32_t actual = weaveffi_abi_version(); - if (actual != WEAVEFFI_ABI_VERSION) { - throw WeaveFFIError(-1, "WeaveFFI ABI revision mismatch: header " + std::to_string(WEAVEFFI_ABI_VERSION) + ", library " + std::to_string(actual)); - } -} - -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. Object tokens adopted before a - * failure live in RAII wrappers that release them on unwind. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - [[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 - -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -enum class Status : int32_t { - /** Document is being edited */ - Draft = 0, - /** Document is publicly visible */ - Published = 1, - /** Document is hidden from listings */ - Archived = 2 -}; - -struct Document; -class Notebook; -class SavedListener; - -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ -class Notebook { - weaveffi_docs_Notebook* handle_; - -public: - /** Adopts one strong reference to a producer object. */ - explicit Notebook(weaveffi_docs_Notebook* h) : handle_(h) {} - - /** Releases this wrapper's reference; the object is dropped with its last one. */ - ~Notebook() { - if (handle_) weaveffi_docs_Notebook_destroy(handle_); - } - - /** Copies share the object: the copy takes a new strong reference. */ - Notebook(const Notebook& other) : handle_(weaveffi_docs_Notebook_clone(other.handle_)) {} - - Notebook& operator=(const Notebook& other) { - if (this != &other) { - weaveffi_docs_Notebook* h = weaveffi_docs_Notebook_clone(other.handle_); - if (handle_) weaveffi_docs_Notebook_destroy(handle_); - handle_ = h; - } - return *this; - } - - Notebook(Notebook&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - - Notebook& operator=(Notebook&& other) noexcept { - if (this != &other) { - if (handle_) weaveffi_docs_Notebook_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** The wrapped pointer, borrowed: this wrapper keeps its reference. */ - const weaveffi_docs_Notebook* handle() const { return handle_; } - - /** A new strong reference the caller owns (for example to write into a value buffer). */ - weaveffi_docs_Notebook* clone_handle() const { return weaveffi_docs_Notebook_clone(handle_); } - - /** Create an empty notebook */ - Notebook(); - - /** Add a document, returning its position in the notebook */ - int32_t insert(const Document& document) const; - - /** Look up a document by identifier, failing when none matches */ - Document find(int64_t id) const; - - /** The number of documents currently held */ - int32_t size() const; - - /** The largest number of documents one notebook will hold */ - static int32_t max_documents(); - -}; - -/** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ -struct Document { - /** Stable opaque identifier */ - int64_t id; - /** Human-readable title shown in lists */ - std::string title; - /** Current lifecycle status */ - Status status; - /** Free-form tags for grouping */ - std::vector tags; -}; - -namespace detail { - -/** Encodes a `Document` in the WeaveFFI value-buffer format. */ -inline void write_Document(BufferWriter& w, const Document& v) { - w.write_i64(v.id); - w.write_string(v.title); - w.write_i32(static_cast(v.status)); - w.write_len(v.tags.size()); - for (const auto& item0 : v.tags) { - w.write_string(item0); - } -} - -/** Decodes a `Document` from the WeaveFFI value-buffer format. */ -inline Document read_Document(BufferReader& r) { - int64_t f_id = r.read_i64(); - std::string f_title = r.read_string(); - Status f_status = static_cast(r.read_i32()); - std::vector f_tags{}; - { - size_t f_tags_n = r.read_len(); - f_tags.reserve(f_tags_n); - for (size_t f_tags_i = 0; f_tags_i < f_tags_n; ++f_tags_i) { - std::string f_tags_item = r.read_string(); - f_tags.push_back(std::move(f_tags_item)); - } - } - return Document{std::move(f_id), std::move(f_title), std::move(f_status), std::move(f_tags)}; -} - -} // namespace detail - -/** Typed errors reported by the `docs` module's throwing functions. */ -class DocsErrorsError : public WeaveFFIError { -public: - DocsErrorsError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; - -/** Returned when no document matches the requested id */ -class NotFoundError : public DocsErrorsError { -public: - NotFoundError(const std::string& msg) : DocsErrorsError(1, msg) {} -}; - -/** Raised when attempting to move into a forbidden state */ -class InvalidStatusError : public DocsErrorsError { -public: - InvalidStatusError(const std::string& msg) : DocsErrorsError(2, msg) {} -}; - -namespace detail { - -/** Map a `docs` error code and payload to its typed exception (WeaveFFIError for runtime codes, DocsErrorsError for unknown codes). */ -inline std::exception_ptr make_docs_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)); - (void)payload_ptr; - (void)payload_len; - switch (code) { - case 1: return std::make_exception_ptr(NotFoundError(msg)); - case 2: return std::make_exception_ptr(InvalidStatusError(msg)); - default: return std::make_exception_ptr(DocsErrorsError(code, msg)); - } -} - -/** Throw the typed `docs` exception if `err` carries a nonzero code. */ -inline void check_docs(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - std::exception_ptr ex = make_docs_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - * - * - * Implement this interface and pass a `std::shared_ptr` to any function that accepts one. The producer may call the methods from any thread until it releases its last reference to the implementation. An exception thrown by a method is reported to the producer as a foreign error (code -4) and aborts the call that triggered it. - */ -class SavedListener { -public: - virtual ~SavedListener() = default; - - /** Fires after a document has been persisted successfully */ - virtual void on_saved(int64_t doc_id, int32_t revision) = 0; -}; - -namespace detail { - -/** Vtable trampolines adapting a `SavedListener` implementation to `weaveffi_docs_SavedListener_vtable`. */ -struct SavedListener_trampolines { - static void on_saved(void* ctx, int64_t doc_id, int32_t revision, weaveffi_error* out_err) { - try { - SavedListener& impl = **static_cast*>(ctx); - impl.on_saved(doc_id, revision); - return; - } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); - } catch (...) { - weaveffi_error_set(out_err, -4, "SavedListener::on_saved threw a non-standard exception"); - } - } - - /** Releases the implementation box once the producer drops its last reference. */ - static void free_ctx(void* ctx) { - delete static_cast*>(ctx); - } -}; - -/** The process-wide vtable every `SavedListener` implementation is passed with. */ -inline const weaveffi_docs_SavedListener_vtable& SavedListener_vtable() { - static const weaveffi_docs_SavedListener_vtable vtable = { - &SavedListener_trampolines::on_saved, - &SavedListener_trampolines::free_ctx, - }; - return vtable; -} - -} // namespace detail - -inline Notebook::Notebook() : handle_(nullptr) { - weaveffi_error err{}; - auto result = weaveffi_docs_Notebook_new(&err); - detail::check(err); - handle_ = result; -} - -inline int32_t Notebook::insert(const Document& document) const { - detail::BufferWriter document_buf; - detail::write_Document(document_buf, document); - weaveffi_error err{}; - auto result = weaveffi_docs_Notebook_insert(handle_, document_buf.data(), document_buf.size(), &err); - detail::check(err); - return result; -} - -inline Document Notebook::find(int64_t id) const { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_docs_Notebook_find(handle_, id, &out_len, &err); - detail::check_docs(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Document ret = detail::read_Document(result_r); - result_r.expect_end(); - return ret; -} - -inline int32_t Notebook::size() const { - weaveffi_error err{}; - auto result = weaveffi_docs_Notebook_size(handle_, &err); - detail::check(err); - return result; -} - -inline int32_t Notebook::max_documents() { - weaveffi_error err{}; - auto result = weaveffi_docs_Notebook_max_documents(&err); - detail::check(err); - return result; -} - -namespace docs { - -/** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - */ -inline Document create_document(const std::string& title, const std::vector& tags) { - detail::BufferWriter tags_buf; - tags_buf.write_len(tags.size()); - for (const auto& item0 : tags) { - tags_buf.write_string(item0); - } - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_docs_create_document(title.c_str(), tags_buf.data(), tags_buf.size(), &out_len, &err); - detail::check(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Document ret = detail::read_Document(result_r); - result_r.expect_end(); - return ret; -} - -/** Look up a document by identifier */ -inline std::optional get_document(int64_t id) { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_docs_get_document(id, &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()) { - Document ret_v = detail::read_Document(result_r); - ret = std::move(ret_v); - } - result_r.expect_end(); - return ret; -} - -/** Publish a document in the background */ -inline std::future publish_async(int64_t id) { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_docs_publish_async_async(id, [](void* context, weaveffi_error* err, bool result) { - auto* p = static_cast*>(context); - try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); - p->set_exception(detail::make_error(err->code, msg)); - } else { - p->set_value(result); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); - return future; -} - -/** Register a listener for saved notifications */ -inline void watch(std::shared_ptr listener) { - if (!listener) throw std::invalid_argument("listener: null callback interface"); - auto* listener_ctx = new std::shared_ptr(std::move(listener)); - weaveffi_error err{}; - weaveffi_docs_watch(static_cast(listener_ctx), &detail::SavedListener_vtable(), &err); - detail::check(err); -} - -} // namespace docs - -} // namespace weaveffi - -// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__CMakeLists_txt.snap index f169a251..84261aa3 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__CMakeLists_txt.snap @@ -1,15 +1,26 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.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) +project(edge_cases_cpp VERSION 0.1.0 LANGUAGES CXX) + +# The producer library to link: a full path from the EDGE_CASES_LIBRARY cache or +# environment variable, or else the library name `edge_cases` (a CMake target +# of that name, or libedge_cases on the linker search path). +set(EDGE_CASES_LIBRARY "$ENV{EDGE_CASES_LIBRARY}" CACHE FILEPATH "Path to the edge_cases native library") + +add_library(edge_cases_cpp INTERFACE) +add_library(edge_cases::cpp ALIAS edge_cases_cpp) +target_include_directories(edge_cases_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_features(edge_cases_cpp INTERFACE cxx_std_17) +if(EDGE_CASES_LIBRARY) + target_link_libraries(edge_cases_cpp INTERFACE "${EDGE_CASES_LIBRARY}") +else() + target_link_libraries(edge_cases_cpp INTERFACE edge_cases) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__README_md.snap index da3ea6c6..1c7b58b7 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__README_md.snap @@ -1,39 +1,43 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI C++ Bindings +# edge_cases C++ bindings -## Prerequisites +A header-only C++17 wrapper over the `edge_cases` C ABI. `edge_cases.hpp` +includes the C header beside it; every declaration lives in +`namespace edge_cases`. -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate +## Prerequisites -## Usage with CMake +- CMake 3.14 or later +- A C++17 compiler +- The `edge_cases` native library (`libedge_cases.so`, `libedge_cases.dylib`, or + `edge_cases.dll`) -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: +## Use with CMake ```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) +add_subdirectory(path/to/cpp) +add_executable(app main.cpp) +target_link_libraries(app PRIVATE edge_cases::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: +`edge_cases::cpp` adds this directory to the include path, requires +C++17, and links the native library: the file named by the `EDGE_CASES_LIBRARY` +CMake cache variable or environment variable when set, or else +`edge_cases` by name. ```cpp -#include "weaveffi.hpp" +#include "edge_cases.hpp" ``` +The first free function, constructor, or static member you call checks +that the linked library matches these headers and throws +`edge_cases::LoadError` if it doesn't; call `edge_cases::check_library()` +at startup to check eagerly. + diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_h.snap b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_h.snap new file mode 100644 index 00000000..94864086 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_h.snap @@ -0,0 +1,245 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef EDGE_CASES_H +#define EDGE_CASES_H + +#include +#include +#include + +#ifndef EDGE_CASES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef EDGE_CASES_BUILD +# define EDGE_CASES_API __declspec(dllexport) +# else +# define EDGE_CASES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define EDGE_CASES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define EDGE_CASES_API __attribute__((visibility("default"))) +# else +# define EDGE_CASES_API +# endif +#endif + +#ifndef EDGE_CASES_DEPRECATED +# if defined(_MSC_VER) +# define EDGE_CASES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define EDGE_CASES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define EDGE_CASES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports edge_cases_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define EDGE_CASES_ABI_VERSION 3u +EDGE_CASES_API uint32_t edge_cases_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by edge_cases_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct edge_cases_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} edge_cases_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +EDGE_CASES_API void edge_cases_error_set(edge_cases_error* err, int32_t code, const char* message); +EDGE_CASES_API void edge_cases_error_clear(edge_cases_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + edge_cases_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +EDGE_CASES_API void edge_cases_error_free(edge_cases_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +EDGE_CASES_API void edge_cases_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct edge_cases_cancel_token edge_cases_cancel_token; +EDGE_CASES_API edge_cases_cancel_token* edge_cases_cancel_token_create(void); +EDGE_CASES_API void edge_cases_cancel_token_cancel(edge_cases_cancel_token* token); +EDGE_CASES_API bool edge_cases_cancel_token_is_cancelled(const edge_cases_cancel_token* token); +EDGE_CASES_API void edge_cases_cancel_token_destroy(edge_cases_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +EDGE_CASES_API uint8_t* edge_cases_alloc(uint32_t size); +EDGE_CASES_API void edge_cases_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +EDGE_CASES_API uint64_t edge_cases_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define EDGE_CASES_EDGE_CASES_CHECKSUM 0x1567f441073bd4aaull +EDGE_CASES_API uint64_t edge_cases_edge_cases_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with edge_cases_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * edge_cases_buffer.h declares a C struct and codec for each. + */ + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +typedef enum { edge_cases_edge_cases_Legacy_None = 0, edge_cases_edge_cases_Legacy_Default = 5, edge_cases_edge_cases_Legacy_Class = 10 } edge_cases_edge_cases_Legacy; +typedef enum { edge_cases_edge_cases_Mode_Fast = 1, edge_cases_edge_cases_Mode_Safe = 2 } edge_cases_edge_cases_Mode; +/** Error codes reported by throwing functions in the `edge_cases` module tree. */ +typedef enum { edge_cases_edge_cases_EdgeErrors_Busy = 7 } edge_cases_edge_cases_EdgeErrors; +typedef struct edge_cases_edge_cases_Widget edge_cases_edge_cases_Widget; +typedef struct edge_cases_edge_cases_Vault edge_cases_edge_cases_Vault; +typedef struct edge_cases_edge_cases_WidgetStreamIterator edge_cases_edge_cases_WidgetStreamIterator; +typedef struct edge_cases_edge_cases_StringStreamIterator edge_cases_edge_cases_StringStreamIterator; +typedef struct edge_cases_edge_cases_ScalarStreamIterator edge_cases_edge_cases_ScalarStreamIterator; +typedef struct edge_cases_edge_cases_ModeStreamIterator edge_cases_edge_cases_ModeStreamIterator; +/** + * Callback interface carrying composite payloads and objects + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_BatchListener_vtable { + /** Composite payloads cross as value buffers */ + void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); + /** Objects cross as strong references the consumer must release */ + void (*on_widget)(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); + void (*on_tick)(void* ctx, edge_cases_error* out_err); + /** Scalar return travels back through the vtable */ + bool (*should_continue)(void* ctx, int32_t class_, edge_cases_error* out_err); + /** Enum return travels back as its integer discriminant */ + edge_cases_edge_cases_Mode (*pick_mode)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_BatchListener_vtable; +/** + * Callback interface whose name and methods are keywords + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_Keyword_vtable { + void (*delete_)(void* ctx, int32_t self, edge_cases_error* out_err); + int32_t (*import)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_Keyword_vtable; +typedef void (*edge_cases_edge_cases_async_widget_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result); +typedef void (*edge_cases_edge_cases_async_void_callback)(void* context, edge_cases_error* err); +typedef void (*edge_cases_edge_cases_async_bytes_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_struct_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_list_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_optional_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_enum_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result); +typedef void (*edge_cases_edge_cases_async_keywords_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_Widget_fetch_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: edge_cases +/** Interface with no constructor; instances come from make_widget */ +/** Void method that can fail */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_refresh(const edge_cases_edge_cases_Widget* self, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_rename(const edge_cases_edge_cases_Widget* self, const uint8_t* class_ptr, size_t class_len, const uint8_t* import_ptr, size_t import_len, size_t* out_len, edge_cases_error* out_err); +/** Async method with a struct result */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_fetch(const edge_cases_edge_cases_Widget* self, int64_t id, edge_cases_edge_cases_Widget_fetch_callback callback, void* context); +EDGE_CASES_DEPRECATED("Use rename instead") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_old_name(const edge_cases_edge_cases_Widget* self, size_t* out_len, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Widget_clone(const edge_cases_edge_cases_Widget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_destroy(edge_cases_edge_cases_Widget* self); + +/** Deprecated interface with a throwing constructor */ +/** Fails when the vault is busy */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_open(const uint8_t* path_ptr, size_t path_len, edge_cases_error* out_err); +/** Throwing static */ +EDGE_CASES_API bool edge_cases_edge_cases_Vault_probe(edge_cases_error* out_err); +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Vault_peek(const edge_cases_edge_cases_Vault* self, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_clone(const edge_cases_edge_cases_Vault* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Vault_destroy(edge_cases_edge_cases_Vault* self); + +/** No parameters and no result */ +EDGE_CASES_API void edge_cases_edge_cases_noop(edge_cases_error* out_err); +/** Function named after a keyword whose parameters are all keywords */ +EDGE_CASES_API int32_t edge_cases_edge_cases_select(int32_t class_, const uint8_t* from_ptr, size_t from_len, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, edge_cases_error* out_err); +/** Optional list of optional structs, the deepest legal nesting */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_optional_params(const uint8_t* text_ptr, size_t text_len, const uint8_t* count_ptr, size_t count_len, const uint8_t* outer_ptr, size_t outer_len, const uint8_t* tags_ptr, size_t tags_len, const uint8_t* blob_ptr, size_t blob_len, const uint8_t* mode_ptr, size_t mode_len, size_t* out_len, edge_cases_error* out_err); +/** Two callback interface parameters on one function */ +EDGE_CASES_API void edge_cases_edge_cases_register_batch(void* listener_ctx, const edge_cases_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const edge_cases_edge_cases_Keyword_vtable* keyword_vtable, edge_cases_error* out_err); +/** Objects in every buffered position */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, edge_cases_error* out_err); +/** Iterator of objects */ +EDGE_CASES_API edge_cases_edge_cases_WidgetStreamIterator* edge_cases_edge_cases_widget_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_WidgetStreamIterator_next(edge_cases_edge_cases_WidgetStreamIterator* iter, edge_cases_edge_cases_Widget** out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_WidgetStreamIterator_destroy(edge_cases_edge_cases_WidgetStreamIterator* iter); +/** Async returning an object */ +EDGE_CASES_API void edge_cases_edge_cases_async_widget(edge_cases_edge_cases_async_widget_callback callback, void* context); +/** Factory for an interface with no constructor */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_make_widget(const uint8_t* label_ptr, size_t label_len, edge_cases_error* out_err); +/** Optional interface return */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_find_widget(int64_t id, edge_cases_error* out_err); +/** Interface parameter, including an optional one */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_inspect_widget(const edge_cases_edge_cases_Widget* widget, const edge_cases_edge_cases_Widget* fallback, size_t* out_len, edge_cases_error* out_err); +/** Iterator of strings */ +EDGE_CASES_API edge_cases_edge_cases_StringStreamIterator* edge_cases_edge_cases_string_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_StringStreamIterator_next(edge_cases_edge_cases_StringStreamIterator* iter, const uint8_t** out_item, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_StringStreamIterator_destroy(edge_cases_edge_cases_StringStreamIterator* iter); +/** Iterator of scalars */ +EDGE_CASES_API edge_cases_edge_cases_ScalarStreamIterator* edge_cases_edge_cases_scalar_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ScalarStreamIterator_next(edge_cases_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ScalarStreamIterator_destroy(edge_cases_edge_cases_ScalarStreamIterator* iter); +/** Iterator of enums */ +EDGE_CASES_API edge_cases_edge_cases_ModeStreamIterator* edge_cases_edge_cases_mode_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ModeStreamIterator_next(edge_cases_edge_cases_ModeStreamIterator* iter, edge_cases_edge_cases_Mode* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ModeStreamIterator_destroy(edge_cases_edge_cases_ModeStreamIterator* iter); +/** Async with no result */ +EDGE_CASES_API void edge_cases_edge_cases_async_void(edge_cases_edge_cases_async_void_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_bytes(uint32_t size, edge_cases_edge_cases_async_bytes_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_struct(edge_cases_edge_cases_async_struct_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_list(edge_cases_cancel_token* cancel_token, edge_cases_edge_cases_async_list_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_optional(edge_cases_edge_cases_async_optional_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_enum(edge_cases_edge_cases_async_enum_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_keywords(const uint8_t* class_ptr, size_t class_len, int32_t import, edge_cases_edge_cases_async_keywords_callback callback, void* context); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_DEPRECATED("Use Mode-based APIs") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy value, size_t* out_len, edge_cases_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // EDGE_CASES_H + +// END edge_cases.h diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_hpp.snap similarity index 57% rename from crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__weaveffi_hpp.snap rename to crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_hpp.snap index 7b7a2295..154c3b48 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__edge_cases_hpp.snap @@ -1,274 +1,230 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o #pragma once +#include "edge_cases.h" + +#include #include -#include -#include -#include -#include -#include -#include -#include -#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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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); +namespace edge_cases { -/** 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_WidgetStreamIterator weaveffi_edge_cases_WidgetStreamIterator; -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; -/** - * Callback interface carrying composite payloads and objects - * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. - */ -typedef struct weaveffi_edge_cases_BatchListener_vtable { - /** Composite payloads cross as value buffers */ - void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, weaveffi_edge_cases_Mode mode, weaveffi_error* out_err); - /** Objects cross as strong references the consumer must release */ - void (*on_widget)(void* ctx, weaveffi_edge_cases_Widget* widget, weaveffi_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, weaveffi_error* out_err); - void (*on_tick)(void* ctx, weaveffi_error* out_err); - /** Scalar return travels back through the vtable */ - bool (*should_continue)(void* ctx, int32_t class_, weaveffi_error* out_err); - /** Enum return travels back as its integer discriminant */ - weaveffi_edge_cases_Mode (*pick_mode)(void* ctx, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_edge_cases_BatchListener_vtable; /** - * Callback interface whose name and methods are keywords + * Base exception for every error reported through the C ABI. * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + * Positive codes are the declared error codes (see the typed domain + * exceptions); negative codes are runtime traps: -1 a generic producer + * error, -2 a producer panic, -3 a marshalling failure, -4 a + * callback-interface implementation that threw, and -5 a cancelled call + * (thrown as Cancelled). */ -typedef struct weaveffi_edge_cases_Keyword_vtable { - void (*delete_)(void* ctx, int32_t self, weaveffi_error* out_err); - int32_t (*import)(void* ctx, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_edge_cases_Keyword_vtable; -typedef void (*weaveffi_edge_cases_async_widget_callback)(void* context, weaveffi_error* err, weaveffi_edge_cases_Widget* result); -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); +class Error : public std::runtime_error { + int32_t code_; -/** 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); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_edge_cases_Widget* weaveffi_edge_cases_Widget_clone(const weaveffi_edge_cases_Widget* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_edge_cases_Widget_destroy(weaveffi_edge_cases_Widget* self); +public: + /** Builds an error carrying `code` and `message`. */ + Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} -/** 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); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_edge_cases_Vault* weaveffi_edge_cases_Vault_clone(const weaveffi_edge_cases_Vault* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_edge_cases_Vault_destroy(weaveffi_edge_cases_Vault* self); + /** The numeric error code. */ + int32_t code() const noexcept { return code_; } +}; -/** 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); -/** Two callback interface parameters on one function */ -WEAVEFFI_API void weaveffi_edge_cases_register_batch(void* listener_ctx, const weaveffi_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const weaveffi_edge_cases_Keyword_vtable* keyword_vtable, weaveffi_error* out_err); -/** Objects in every buffered position */ -WEAVEFFI_API const uint8_t* weaveffi_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, weaveffi_error* out_err); -/** Iterator of objects */ -WEAVEFFI_API weaveffi_edge_cases_WidgetStreamIterator* weaveffi_edge_cases_widget_stream(weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_edge_cases_WidgetStreamIterator_next(weaveffi_edge_cases_WidgetStreamIterator* iter, weaveffi_edge_cases_Widget** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_edge_cases_WidgetStreamIterator_destroy(weaveffi_edge_cases_WidgetStreamIterator* iter); -/** Async returning an object */ -WEAVEFFI_API void weaveffi_edge_cases_async_widget_async(weaveffi_edge_cases_async_widget_callback callback, void* context); -/** 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); -/** 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); +/** Thrown by an async call whose CancelToken was cancelled (code -5). */ +class Cancelled : public Error { +public: + /** Builds a cancellation error carrying the producer's message. */ + explicit Cancelled(const std::string& message) : Error(-5, message) {} +}; + +/** + * Thrown on first use when the linked edge_cases library was built for a + * different C ABI revision or a different API contract than this header. + */ +class LoadError : public Error { +public: + /** Builds a load error describing the mismatch. */ + explicit LoadError(const std::string& message) : Error(-1, message) {} +}; -} // extern "C" +/** + * Tag selecting the constructor that adopts a raw C pointer (one strong + * reference the wrapper then owns), in the style of std::adopt_lock. + */ +struct adopt_t { + explicit adopt_t() = default; +}; -namespace weaveffi { +/** The adopt_t tag value: `Gadget g(adopt, raw)`. */ +inline constexpr adopt_t adopt{}; /** - * Base exception for every error reported through the C ABI. + * A cancellation token for cancellable async functions. * - * Positive codes are the module's declared error codes (see the typed - * domain exceptions); negative codes are runtime traps: -1 a generic - * producer error, -2 a producer panic, -3 a marshalling failure, -4 a - * callback-interface implementation that threw. + * Create one, pass it to any number of cancellable calls, and call cancel() + * from any thread. A cancelled call settles its future with Cancelled. Each + * call takes its own reference to the native token, so the token can be + * destroyed at any time; the destructor releases this wrapper's reference. */ -class WeaveFFIError : public std::runtime_error { - int32_t code_; +class CancelToken { + edge_cases_cancel_token* handle_; + + struct none_t {}; + explicit CancelToken(none_t) noexcept : handle_(nullptr) {} public: - WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} - int32_t code() const { return code_; } -}; + /** Creates a new, uncancelled token. */ + CancelToken() : handle_(edge_cases_cancel_token_create()) { + if (handle_ == nullptr) throw std::bad_alloc(); + } -/** - * Throws WeaveFFIError when the loaded producer library was built for a - * different WeaveFFI ABI revision than this header. Call once at startup. - */ -inline void check_abi_version() { - uint32_t actual = weaveffi_abi_version(); - if (actual != WEAVEFFI_ABI_VERSION) { - throw WeaveFFIError(-1, "WeaveFFI ABI revision mismatch: header " + std::to_string(WEAVEFFI_ABI_VERSION) + ", library " + std::to_string(actual)); + /** Releases this wrapper's reference to the native token. */ + ~CancelToken() { + if (handle_ != nullptr) edge_cases_cancel_token_destroy(handle_); + } + + CancelToken(const CancelToken&) = delete; + CancelToken& operator=(const CancelToken&) = delete; + + /** Transfers the native token; `other` becomes empty. */ + CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + + /** Releases the current token and takes over `other`'s. */ + CancelToken& operator=(CancelToken&& other) noexcept { + if (this != &other) { + if (handle_ != nullptr) edge_cases_cancel_token_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; } -} + + /** The empty token a cancellable call uses when none is passed. */ + static const CancelToken& none() noexcept { + static const CancelToken token{none_t{}}; + return token; + } + + /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ + void cancel() const noexcept { + if (handle_ != nullptr) edge_cases_cancel_token_cancel(handle_); + } + + /** Whether cancel() has been called. */ + bool is_cancelled() const noexcept { + return handle_ != nullptr && edge_cases_cancel_token_is_cancelled(handle_); + } + + /** The native token, borrowed (null for an empty token). */ + edge_cases_cancel_token* handle() const noexcept { return handle_; } +}; namespace detail { -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { +/** The exception for a runtime (non-domain) error code. */ +inline std::exception_ptr make_error(int32_t code, const std::string& message) { + if (code == -5) return std::make_exception_ptr(Cancelled(message)); + return std::make_exception_ptr(Error(code, message)); +} + +/** The message of a nonzero error slot. */ +inline std::string error_message(const edge_cases_error& err) { + return std::string(err.message != nullptr ? err.message : "unknown error"); +} + +/** Throws the generic Error (or Cancelled) if `err` carries a nonzero code. */ +inline void check(edge_cases_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); + std::exception_ptr ex = make_error(err.code, error_message(err)); + edge_cases_error_clear(&err); + std::rethrow_exception(ex); } -/** 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)); +/** Copies a producer-returned UTF-8 run into a string, then releases it. */ +inline std::string take_string(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::string(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { edge_cases_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::string(reinterpret_cast(ptr), len); +} + +/** Copies a producer-returned byte run into a vector, then releases it. */ +inline std::vector take_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::vector(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { edge_cases_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::vector(ptr, ptr + len); +} + +/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ +inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { + if (ptr == nullptr || len == 0) return std::string_view(); + return std::string_view(reinterpret_cast(ptr), len); +} + +/** Views a borrowed byte run (null when empty) as an owned vector. */ +inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr || len == 0) return std::vector(); + return std::vector(ptr, ptr + len); +} + +/** The data pointer of a string argument, as the C ABI expects it. */ +inline const uint8_t* utf8(std::string_view s) noexcept { + return reinterpret_cast(s.data()); } } // namespace detail +/** + * Verifies that the linked edge_cases library implements the C ABI revision + * and the module contracts this header was generated against, throwing + * LoadError naming the first mismatch. The check runs once; every free + * function, constructor, and static member calls this before its first + * native call, so calling it at startup is optional. + */ +inline void check_library() { + static const std::string failure = [] { + uint32_t abi = edge_cases_abi_version(); + if (abi != EDGE_CASES_ABI_VERSION) { + return "edge_cases: C ABI revision mismatch (header " + std::to_string(EDGE_CASES_ABI_VERSION) + + ", library " + std::to_string(abi) + ")"; + } + if (edge_cases_edge_cases_checksum() != 0x1567f441073bd4aaull) { + return std::string("edge_cases: module 'edge_cases' does not match the linked library (contract checksum mismatch); regenerate the bindings for this build"); + } + return std::string(); + }(); + if (!failure.empty()) throw LoadError(failure); +} + namespace detail { /** - * Serializes values into the WeaveFFI value-buffer wire format: little-endian, - * packed with no alignment, lengths and element counts as u32. + * Serializes values into the value-buffer wire format: little-endian, packed + * with no alignment, lengths and element counts as u32. */ class BufferWriter { std::vector buf_; @@ -282,10 +238,10 @@ class BufferWriter { public: /** Pointer to the encoded bytes. */ - const uint8_t* data() const { return buf_.data(); } + const uint8_t* data() const noexcept { return buf_.data(); } /** Number of encoded bytes. */ - size_t size() const { return buf_.size(); } + size_t size() const noexcept { return buf_.size(); } void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } @@ -309,10 +265,13 @@ public: 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)); } + /** Writes a byte length or an element count as a u32. */ + void write_len(size_t n) { + if (static_cast(n) > UINT32_MAX) throw Error(-3, "value buffer length exceeds u32"); + append_le(static_cast(n)); + } - void write_string(const std::string& v) { + void write_string(std::string_view v) { write_len(v.size()); buf_.insert(buf_.end(), v.begin(), v.end()); } @@ -327,11 +286,11 @@ public: }; /** - * 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. Object tokens adopted before a - * failure live in RAII wrappers that release them on unwind. + * Decodes values from the value-buffer wire format. A malformed buffer is a + * producer/consumer contract violation (both sides are generated from one + * API), so every decode failure throws the generic Error with the + * marshalling code -3. Object tokens adopted before a failure live in RAII + * wrappers that release them on unwind. */ class BufferReader { const uint8_t* data_; @@ -339,7 +298,7 @@ class BufferReader { size_t pos_; [[noreturn]] static void fail(const char* what) { - throw WeaveFFIError(-2, std::string("malformed WeaveFFI value buffer: ") + what); + throw Error(-3, std::string("malformed value buffer: ") + what); } void require(size_t n, const char* what) const { @@ -358,10 +317,10 @@ class BufferReader { } public: - BufferReader(const uint8_t* data, size_t len) : data_(data), len_(len), pos_(0) {} + BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} /** Bytes not yet consumed. */ - size_t remaining() const { return len_ - pos_; } + size_t remaining() const noexcept { return len_ - pos_; } bool read_bool() { uint8_t b = read_le("bool"); @@ -392,13 +351,23 @@ public: return v; } - /** Reads a length prefix, rejecting one larger than the bytes remaining. */ + /** Reads a byte length, rejecting one larger than the bytes remaining. */ size_t read_len() { uint32_t n = read_le("length"); if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); return static_cast(n); } + /** + * Reads an element count. Elements may encode to zero bytes, so a count + * larger than the bytes remaining is legal; callers cap preallocation + * with reserve_hint(). + */ + size_t read_count() { return static_cast(read_le("count")); } + + /** A preallocation size for `n` elements that a malformed count can't inflate. */ + size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } + std::string read_string() { size_t n = read_len(); std::string v(reinterpret_cast(data_) + pos_, n); @@ -425,15 +394,19 @@ public: } }; -/** Releases a producer-allocated buffer with weaveffi_free_bytes on scope exit. */ +/** Releases a producer-allocated buffer with edge_cases_free_bytes on scope exit. */ struct BufferGuard { /** The producer-allocated buffer, or null when the call reported an error. */ const uint8_t* ptr; /** The buffer length in bytes. */ size_t len; + BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} + BufferGuard(const BufferGuard&) = delete; + BufferGuard& operator=(const BufferGuard&) = delete; + ~BufferGuard() { - if (ptr != nullptr) weaveffi_free_bytes(const_cast(ptr), len); + if (ptr != nullptr) edge_cases_free_bytes(const_cast(ptr), len); } }; @@ -464,24 +437,27 @@ class Keyword; /** Interface with no constructor; instances come from make_widget */ class Widget { - weaveffi_edge_cases_Widget* handle_; + edge_cases_edge_cases_Widget* handle_; public: - /** Adopts one strong reference to a producer object. */ - explicit Widget(weaveffi_edge_cases_Widget* h) : handle_(h) {} + /** The C type this class wraps. */ + using raw_type = edge_cases_edge_cases_Widget; + + /** Adopts one strong reference to a producer object: `Widget(adopt, raw)`. */ + explicit Widget(adopt_t, edge_cases_edge_cases_Widget* h) noexcept : handle_(h) {} /** Releases this wrapper's reference; the object is dropped with its last one. */ ~Widget() { - if (handle_) weaveffi_edge_cases_Widget_destroy(handle_); + if (handle_) edge_cases_edge_cases_Widget_destroy(handle_); } /** Copies share the object: the copy takes a new strong reference. */ - Widget(const Widget& other) : handle_(weaveffi_edge_cases_Widget_clone(other.handle_)) {} + Widget(const Widget& other) : handle_(edge_cases_edge_cases_Widget_clone(other.handle_)) {} Widget& operator=(const Widget& other) { if (this != &other) { - weaveffi_edge_cases_Widget* h = weaveffi_edge_cases_Widget_clone(other.handle_); - if (handle_) weaveffi_edge_cases_Widget_destroy(handle_); + edge_cases_edge_cases_Widget* h = edge_cases_edge_cases_Widget_clone(other.handle_); + if (handle_) edge_cases_edge_cases_Widget_destroy(handle_); handle_ = h; } return *this; @@ -493,7 +469,7 @@ public: Widget& operator=(Widget&& other) noexcept { if (this != &other) { - if (handle_) weaveffi_edge_cases_Widget_destroy(handle_); + if (handle_) edge_cases_edge_cases_Widget_destroy(handle_); handle_ = other.handle_; other.handle_ = nullptr; } @@ -501,15 +477,15 @@ public: } /** The wrapped pointer, borrowed: this wrapper keeps its reference. */ - const weaveffi_edge_cases_Widget* handle() const { return handle_; } + const edge_cases_edge_cases_Widget* handle() const { return handle_; } /** A new strong reference the caller owns (for example to write into a value buffer). */ - weaveffi_edge_cases_Widget* clone_handle() const { return weaveffi_edge_cases_Widget_clone(handle_); } + edge_cases_edge_cases_Widget* clone_handle() const { return edge_cases_edge_cases_Widget_clone(handle_); } /** Void method that can fail */ void refresh() const; - std::string rename(const std::string& class_, const std::optional& import) const; + std::string rename(std::string_view class_, const std::optional& import) const; /** Async method with a struct result */ std::future fetch(int64_t id) const; @@ -521,24 +497,27 @@ public: /** Deprecated interface with a throwing constructor */ class Vault { - weaveffi_edge_cases_Vault* handle_; + edge_cases_edge_cases_Vault* handle_; public: - /** Adopts one strong reference to a producer object. */ - explicit Vault(weaveffi_edge_cases_Vault* h) : handle_(h) {} + /** The C type this class wraps. */ + using raw_type = edge_cases_edge_cases_Vault; + + /** Adopts one strong reference to a producer object: `Vault(adopt, raw)`. */ + explicit Vault(adopt_t, edge_cases_edge_cases_Vault* h) noexcept : handle_(h) {} /** Releases this wrapper's reference; the object is dropped with its last one. */ ~Vault() { - if (handle_) weaveffi_edge_cases_Vault_destroy(handle_); + if (handle_) edge_cases_edge_cases_Vault_destroy(handle_); } /** Copies share the object: the copy takes a new strong reference. */ - Vault(const Vault& other) : handle_(weaveffi_edge_cases_Vault_clone(other.handle_)) {} + Vault(const Vault& other) : handle_(edge_cases_edge_cases_Vault_clone(other.handle_)) {} Vault& operator=(const Vault& other) { if (this != &other) { - weaveffi_edge_cases_Vault* h = weaveffi_edge_cases_Vault_clone(other.handle_); - if (handle_) weaveffi_edge_cases_Vault_destroy(handle_); + edge_cases_edge_cases_Vault* h = edge_cases_edge_cases_Vault_clone(other.handle_); + if (handle_) edge_cases_edge_cases_Vault_destroy(handle_); handle_ = h; } return *this; @@ -550,7 +529,7 @@ public: Vault& operator=(Vault&& other) noexcept { if (this != &other) { - if (handle_) weaveffi_edge_cases_Vault_destroy(handle_); + if (handle_) edge_cases_edge_cases_Vault_destroy(handle_); handle_ = other.handle_; other.handle_ = nullptr; } @@ -558,13 +537,13 @@ public: } /** The wrapped pointer, borrowed: this wrapper keeps its reference. */ - const weaveffi_edge_cases_Vault* handle() const { return handle_; } + const edge_cases_edge_cases_Vault* handle() const { return handle_; } /** A new strong reference the caller owns (for example to write into a value buffer). */ - weaveffi_edge_cases_Vault* clone_handle() const { return weaveffi_edge_cases_Vault_clone(handle_); } + edge_cases_edge_cases_Vault* clone_handle() const { return edge_cases_edge_cases_Vault_clone(handle_); } /** Fails when the vault is busy */ - static Vault open(const std::string& path); + static Vault open(std::string_view path); std::optional peek() const; @@ -675,18 +654,18 @@ struct WidgetHolder { namespace detail { -/** Encodes a `Leaf` in the WeaveFFI value-buffer format. */ +/** Encodes a `Leaf` in the value-buffer wire format. */ inline void write_Leaf(BufferWriter& w, const Leaf& v) { w.write_f32(v.weight); } -/** Decodes a `Leaf` from the WeaveFFI value-buffer format. */ +/** Decodes a `Leaf` from the value-buffer wire format. */ inline Leaf read_Leaf(BufferReader& r) { float f_weight = r.read_f32(); return Leaf{std::move(f_weight)}; } -/** Encodes a `Inner` in the WeaveFFI value-buffer format. */ +/** Encodes a `Inner` in the value-buffer wire format. */ inline void write_Inner(BufferWriter& w, const Inner& v) { detail::write_Leaf(w, v.leaf); w.write_len(v.leaves.size()); @@ -695,13 +674,13 @@ inline void write_Inner(BufferWriter& w, const Inner& v) { } } -/** Decodes a `Inner` from the WeaveFFI value-buffer format. */ +/** Decodes a `Inner` from the value-buffer wire format. */ inline Inner read_Inner(BufferReader& r) { Leaf f_leaf = detail::read_Leaf(r); std::vector f_leaves{}; { - size_t f_leaves_n = r.read_len(); - f_leaves.reserve(f_leaves_n); + size_t f_leaves_n = r.read_count(); + f_leaves.reserve(r.reserve_hint(f_leaves_n)); for (size_t f_leaves_i = 0; f_leaves_i < f_leaves_n; ++f_leaves_i) { Leaf f_leaves_item = detail::read_Leaf(r); f_leaves.push_back(std::move(f_leaves_item)); @@ -710,7 +689,7 @@ inline Inner read_Inner(BufferReader& r) { return Inner{std::move(f_leaf), std::move(f_leaves)}; } -/** Encodes a `Outer` in the WeaveFFI value-buffer format. */ +/** Encodes a `Outer` in the value-buffer wire 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()); @@ -757,7 +736,7 @@ inline void write_Outer(BufferWriter& w, const Outer& v) { w.write_u8(v.v2); } -/** Decodes a `Outer` from the WeaveFFI value-buffer format. */ +/** Decodes a `Outer` from the value-buffer wire format. */ inline Outer read_Outer(BufferReader& r) { Inner f_inner = detail::read_Inner(r); std::optional f_maybe_inner{}; @@ -767,7 +746,7 @@ inline Outer read_Outer(BufferReader& r) { } std::unordered_map f_by_name{}; { - size_t f_by_name_n = r.read_len(); + size_t f_by_name_n = r.read_count(); for (size_t f_by_name_i = 0; f_by_name_i < f_by_name_n; ++f_by_name_i) { std::string f_by_name_key = r.read_string(); Inner f_by_name_val = detail::read_Inner(r); @@ -776,8 +755,8 @@ inline Outer read_Outer(BufferReader& r) { } std::vector f_modes{}; { - size_t f_modes_n = r.read_len(); - f_modes.reserve(f_modes_n); + size_t f_modes_n = r.read_count(); + f_modes.reserve(r.reserve_hint(f_modes_n)); for (size_t f_modes_i = 0; f_modes_i < f_modes_n; ++f_modes_i) { Mode f_modes_item = static_cast(r.read_i32()); f_modes.push_back(std::move(f_modes_item)); @@ -785,13 +764,13 @@ inline Outer read_Outer(BufferReader& r) { } std::vector> f_matrix{}; { - size_t f_matrix_n = r.read_len(); - f_matrix.reserve(f_matrix_n); + size_t f_matrix_n = r.read_count(); + f_matrix.reserve(r.reserve_hint(f_matrix_n)); for (size_t f_matrix_i = 0; f_matrix_i < f_matrix_n; ++f_matrix_i) { std::vector f_matrix_item{}; { - size_t f_matrix_item_n = r.read_len(); - f_matrix_item.reserve(f_matrix_item_n); + size_t f_matrix_item_n = r.read_count(); + f_matrix_item.reserve(r.reserve_hint(f_matrix_item_n)); for (size_t f_matrix_item_i = 0; f_matrix_item_i < f_matrix_item_n; ++f_matrix_item_i) { int32_t f_matrix_item_item = r.read_i32(); f_matrix_item.push_back(std::move(f_matrix_item_item)); @@ -802,8 +781,8 @@ inline Outer read_Outer(BufferReader& r) { } std::vector> f_sparse{}; { - size_t f_sparse_n = r.read_len(); - f_sparse.reserve(f_sparse_n); + size_t f_sparse_n = r.read_count(); + f_sparse.reserve(r.reserve_hint(f_sparse_n)); for (size_t f_sparse_i = 0; f_sparse_i < f_sparse_n; ++f_sparse_i) { std::optional f_sparse_item{}; if (r.read_option_flag()) { @@ -825,7 +804,7 @@ inline Outer read_Outer(BufferReader& r) { } std::unordered_map f_by_id{}; { - size_t f_by_id_n = r.read_len(); + size_t f_by_id_n = r.read_count(); for (size_t f_by_id_i = 0; f_by_id_i < f_by_id_n; ++f_by_id_i) { int64_t f_by_id_key = r.read_i64(); std::string f_by_id_val = r.read_string(); @@ -837,7 +816,7 @@ inline Outer read_Outer(BufferReader& r) { return Outer{std::move(f_inner), std::move(f_maybe_inner), std::move(f_by_name), std::move(f_modes), std::move(f_matrix), std::move(f_sparse), std::move(f_blob), std::move(f_note), std::move(f_by_id), std::move(f__hidden), std::move(f_v2)}; } -/** Encodes a `Keywords` in the WeaveFFI value-buffer format. */ +/** Encodes a `Keywords` in the value-buffer wire format. */ inline void write_Keywords(BufferWriter& w, const Keywords& v) { w.write_i32(v.class_); w.write_i32(v.def); @@ -900,7 +879,7 @@ inline void write_Keywords(BufferWriter& w, const Keywords& v) { w.write_i32(v.super); } -/** Decodes a `Keywords` from the WeaveFFI value-buffer format. */ +/** Decodes a `Keywords` from the value-buffer wire format. */ inline Keywords read_Keywords(BufferReader& r) { int32_t f_class = r.read_i32(); int32_t f_def = r.read_i32(); @@ -964,18 +943,18 @@ inline Keywords read_Keywords(BufferReader& r) { return Keywords{std::move(f_class), std::move(f_def), std::move(f_import), std::move(f_from), std::move(f_func), std::move(f_range), std::move(f_register), std::move(f_volatile), std::move(f_new), std::move(f_default), std::move(f_end), std::move(f_in), std::move(f_is), std::move(f_int), std::move(f_var), std::move(f_val), std::move(f_self), std::move(f_this), std::move(f_object), std::move(f_string), std::move(f_case), std::move(f_select), std::move(f_switch), std::move(f_void), std::move(f_union), std::move(f_namespace), std::move(f_operator), std::move(f_template), std::move(f_package), std::move(f_protocol), std::move(f_guard), std::move(f_defer), std::move(f_inout), std::move(f_internal), std::move(f_when), std::move(f_fun), std::move(f_data), std::move(f_late), std::move(f_part), std::move(f_out), std::move(f_ref), std::move(f_event), std::move(f_base), std::move(f_lambda), std::move(f_yield), std::move(f_nil), std::move(f_null), std::move(f_begin), std::move(f_rescue), std::move(f_module), std::move(f_chan), std::move(f_go), std::move(f_let), std::move(f_export), std::move(f_delete), std::move(f_typedef), std::move(f_sizeof), std::move(f_static), std::move(f_super)}; } -/** Encodes a `Retired` in the WeaveFFI value-buffer format. */ +/** Encodes a `Retired` in the value-buffer wire format. */ inline void write_Retired(BufferWriter& w, const Retired& v) { w.write_i64(v.id); } -/** Decodes a `Retired` from the WeaveFFI value-buffer format. */ +/** Decodes a `Retired` from the value-buffer wire format. */ inline Retired read_Retired(BufferReader& r) { int64_t f_id = r.read_i64(); return Retired{std::move(f_id)}; } -/** Encodes a `WidgetHolder` in the WeaveFFI value-buffer format. */ +/** Encodes a `WidgetHolder` in the value-buffer wire format. */ inline void write_WidgetHolder(BufferWriter& w, const WidgetHolder& v) { w.write_u64(static_cast(reinterpret_cast(v.primary.clone_handle()))); w.write_option_flag(v.fallback.has_value()); @@ -993,29 +972,29 @@ inline void write_WidgetHolder(BufferWriter& w, const WidgetHolder& v) { } } -/** Decodes a `WidgetHolder` from the WeaveFFI value-buffer format. */ +/** Decodes a `WidgetHolder` from the value-buffer wire format. */ inline WidgetHolder read_WidgetHolder(BufferReader& r) { - Widget f_primary = Widget(reinterpret_cast(static_cast(r.read_u64()))); + Widget f_primary = Widget(adopt, reinterpret_cast(static_cast(r.read_u64()))); std::optional f_fallback{}; if (r.read_option_flag()) { - Widget f_fallback_v = Widget(reinterpret_cast(static_cast(r.read_u64()))); + Widget f_fallback_v = Widget(adopt, reinterpret_cast(static_cast(r.read_u64()))); f_fallback = std::move(f_fallback_v); } std::vector f_extras{}; { - size_t f_extras_n = r.read_len(); - f_extras.reserve(f_extras_n); + size_t f_extras_n = r.read_count(); + f_extras.reserve(r.reserve_hint(f_extras_n)); for (size_t f_extras_i = 0; f_extras_i < f_extras_n; ++f_extras_i) { - Widget f_extras_item = Widget(reinterpret_cast(static_cast(r.read_u64()))); + Widget f_extras_item = Widget(adopt, reinterpret_cast(static_cast(r.read_u64()))); f_extras.push_back(std::move(f_extras_item)); } } std::unordered_map f_by_name{}; { - size_t f_by_name_n = r.read_len(); + size_t f_by_name_n = r.read_count(); for (size_t f_by_name_i = 0; f_by_name_i < f_by_name_n; ++f_by_name_i) { std::string f_by_name_key = r.read_string(); - Widget f_by_name_val = Widget(reinterpret_cast(static_cast(r.read_u64()))); + Widget f_by_name_val = Widget(adopt, reinterpret_cast(static_cast(r.read_u64()))); f_by_name.emplace(std::move(f_by_name_key), std::move(f_by_name_val)); } } @@ -1025,22 +1004,22 @@ inline WidgetHolder read_WidgetHolder(BufferReader& r) { } // namespace detail /** Typed errors reported by the `edge_cases` module's throwing functions. */ -class EdgeErrorsError : public WeaveFFIError { +class EdgeErrorsError : public Error { public: - EdgeErrorsError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} + EdgeErrorsError(int32_t code, const std::string& msg) : Error(code, msg) {} }; /** Resource is busy */ class BusyError : public EdgeErrorsError { public: - BusyError(const std::string& msg) : EdgeErrorsError(7, msg) {} + explicit 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). */ +/** Map a `edge_cases` error code and payload to its typed exception (the runtime exception for negative 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)); + if (code < 0) return make_error(code, msg); (void)payload_ptr; (void)payload_len; switch (code) { @@ -1050,11 +1029,11 @@ inline std::exception_ptr make_edge_cases_error(int32_t code, const std::string& } /** Throw the typed `edge_cases` exception if `err` carries a nonzero code. */ -inline void check_edge_cases(weaveffi_error& err) { +inline void check_edge_cases(edge_cases_error& err) { if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); + std::string msg = error_message(err); std::exception_ptr ex = make_edge_cases_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); + edge_cases_error_clear(&err); std::rethrow_exception(ex); } @@ -1082,23 +1061,23 @@ public: namespace detail { -/** Vtable trampolines adapting a `BatchListener` implementation to `weaveffi_edge_cases_BatchListener_vtable`. */ +/** Vtable trampolines adapting a `BatchListener` implementation to `edge_cases_edge_cases_BatchListener_vtable`. */ struct BatchListener_trampolines { - static void on_batch(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, weaveffi_edge_cases_Mode mode, weaveffi_error* out_err) { + static void on_batch(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err) { try { BatchListener& impl = **static_cast*>(ctx); detail::BufferReader items_r(items_ptr, items_len); std::vector items_val{}; { - size_t items_val_n = items_r.read_len(); - items_val.reserve(items_val_n); + size_t items_val_n = items_r.read_count(); + items_val.reserve(items_r.reserve_hint(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::vector raw_val(raw_ptr, raw_ptr + raw_len); + std::vector raw_val = detail::borrow_bytes(raw_ptr, raw_len); detail::BufferReader first_r(first_ptr, first_len); std::optional first_val{}; if (first_r.read_option_flag()) { @@ -1109,25 +1088,25 @@ struct BatchListener_trampolines { impl.on_batch(items_val, raw_val, first_val, static_cast(static_cast(mode))); return; } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + edge_cases_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "BatchListener::on_batch threw a non-standard exception"); + edge_cases_error_set(out_err, -4, "BatchListener::on_batch threw a non-standard exception"); } } - static void on_widget(void* ctx, weaveffi_edge_cases_Widget* widget, weaveffi_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, weaveffi_error* out_err) { - Widget widget_val(widget); + static void on_widget(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err) { + Widget widget_val(adopt, widget); std::optional fallback_val; - if (fallback) fallback_val.emplace(fallback); + if (fallback) fallback_val.emplace(adopt, fallback); try { BatchListener& impl = **static_cast*>(ctx); detail::BufferReader children_r(children_ptr, children_len); std::vector children_val{}; { - size_t children_val_n = children_r.read_len(); - children_val.reserve(children_val_n); + size_t children_val_n = children_r.read_count(); + children_val.reserve(children_r.reserve_hint(children_val_n)); for (size_t children_val_i = 0; children_val_i < children_val_n; ++children_val_i) { - Widget children_val_item = Widget(reinterpret_cast(static_cast(children_r.read_u64()))); + Widget children_val_item = Widget(adopt, reinterpret_cast(static_cast(children_r.read_u64()))); children_val.push_back(std::move(children_val_item)); } } @@ -1135,46 +1114,46 @@ struct BatchListener_trampolines { impl.on_widget(std::move(widget_val), std::move(fallback_val), children_val); return; } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + edge_cases_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "BatchListener::on_widget threw a non-standard exception"); + edge_cases_error_set(out_err, -4, "BatchListener::on_widget threw a non-standard exception"); } } - static void on_tick(void* ctx, weaveffi_error* out_err) { + static void on_tick(void* ctx, edge_cases_error* out_err) { try { BatchListener& impl = **static_cast*>(ctx); impl.on_tick(); return; } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + edge_cases_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "BatchListener::on_tick threw a non-standard exception"); + edge_cases_error_set(out_err, -4, "BatchListener::on_tick threw a non-standard exception"); } } - static bool should_continue(void* ctx, int32_t class_, weaveffi_error* out_err) { + static bool should_continue(void* ctx, int32_t class_, edge_cases_error* out_err) { try { BatchListener& impl = **static_cast*>(ctx); return impl.should_continue(class_); } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + edge_cases_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "BatchListener::should_continue threw a non-standard exception"); + edge_cases_error_set(out_err, -4, "BatchListener::should_continue threw a non-standard exception"); } return bool{}; } - static weaveffi_edge_cases_Mode pick_mode(void* ctx, weaveffi_error* out_err) { + static edge_cases_edge_cases_Mode pick_mode(void* ctx, edge_cases_error* out_err) { try { BatchListener& impl = **static_cast*>(ctx); - return static_cast(static_cast(impl.pick_mode())); + return static_cast(static_cast(impl.pick_mode())); } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + edge_cases_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "BatchListener::pick_mode threw a non-standard exception"); + edge_cases_error_set(out_err, -4, "BatchListener::pick_mode threw a non-standard exception"); } - return weaveffi_edge_cases_Mode{}; + return edge_cases_edge_cases_Mode{}; } /** Releases the implementation box once the producer drops its last reference. */ @@ -1184,8 +1163,8 @@ struct BatchListener_trampolines { }; /** The process-wide vtable every `BatchListener` implementation is passed with. */ -inline const weaveffi_edge_cases_BatchListener_vtable& BatchListener_vtable() { - static const weaveffi_edge_cases_BatchListener_vtable vtable = { +inline const edge_cases_edge_cases_BatchListener_vtable& BatchListener_vtable() { + static const edge_cases_edge_cases_BatchListener_vtable vtable = { &BatchListener_trampolines::on_batch, &BatchListener_trampolines::on_widget, &BatchListener_trampolines::on_tick, @@ -1213,28 +1192,28 @@ public: namespace detail { -/** Vtable trampolines adapting a `Keyword` implementation to `weaveffi_edge_cases_Keyword_vtable`. */ +/** Vtable trampolines adapting a `Keyword` implementation to `edge_cases_edge_cases_Keyword_vtable`. */ struct Keyword_trampolines { - static void delete_(void* ctx, int32_t self, weaveffi_error* out_err) { + static void delete_(void* ctx, int32_t self, edge_cases_error* out_err) { try { Keyword& impl = **static_cast*>(ctx); impl.delete_(self); return; } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + edge_cases_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "Keyword::delete_ threw a non-standard exception"); + edge_cases_error_set(out_err, -4, "Keyword::delete_ threw a non-standard exception"); } } - static int32_t import(void* ctx, weaveffi_error* out_err) { + static int32_t import(void* ctx, edge_cases_error* out_err) { try { Keyword& impl = **static_cast*>(ctx); return impl.import(); } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + edge_cases_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "Keyword::import threw a non-standard exception"); + edge_cases_error_set(out_err, -4, "Keyword::import threw a non-standard exception"); } return int32_t{}; } @@ -1246,8 +1225,8 @@ struct Keyword_trampolines { }; /** The process-wide vtable every `Keyword` implementation is passed with. */ -inline const weaveffi_edge_cases_Keyword_vtable& Keyword_vtable() { - static const weaveffi_edge_cases_Keyword_vtable vtable = { +inline const edge_cases_edge_cases_Keyword_vtable& Keyword_vtable() { + static const edge_cases_edge_cases_Keyword_vtable vtable = { &Keyword_trampolines::delete_, &Keyword_trampolines::import, &Keyword_trampolines::free_ctx, @@ -1258,36 +1237,35 @@ inline const weaveffi_edge_cases_Keyword_vtable& Keyword_vtable() { } // namespace detail inline void Widget::refresh() const { - weaveffi_error err{}; - weaveffi_edge_cases_Widget_refresh(handle_, &err); + edge_cases_error err{}; + edge_cases_edge_cases_Widget_refresh(handle_, &err); detail::check_edge_cases(err); } -inline std::string Widget::rename(const std::string& class_, const std::optional& import) const { +inline std::string Widget::rename(std::string_view 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(handle_, class_.c_str(), import_buf.data(), import_buf.size(), &err); + size_t out_len = 0; + edge_cases_error err{}; + auto result = edge_cases_edge_cases_Widget_rename(handle_, detail::utf8(class_), class_.size(), import_buf.data(), import_buf.size(), &out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } inline std::future Widget::fetch(int64_t id) const { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_edge_cases_Widget_fetch_async(handle_, id, [](void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { - auto* p = static_cast*>(context); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + edge_cases_edge_cases_Widget_fetch(handle_, id, [](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + std::unique_ptr> p(static_cast*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_edge_cases_error(err->code, msg, err->payload_ptr, err->payload_len)); } else { - detail::BufferGuard result_guard{result_ptr, result_len}; + detail::BufferGuard result_guard(result_ptr, result_len); detail::BufferReader result_r(result_ptr, result_len); Outer value = detail::read_Outer(result_r); result_r.expect_end(); @@ -1296,39 +1274,39 @@ inline std::future Widget::fetch(int64_t id) const { } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); return future; } inline std::string Widget::old_name() const { - weaveffi_error err{}; - auto result = weaveffi_edge_cases_Widget_old_name(handle_, &err); + size_t out_len = 0; + edge_cases_error err{}; + auto result = edge_cases_edge_cases_Widget_old_name(handle_, &out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } -inline Vault Vault::open(const std::string& path) { - weaveffi_error err{}; - auto result = weaveffi_edge_cases_Vault_open(path.c_str(), &err); +inline Vault Vault::open(std::string_view path) { + check_library(); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_Vault_open(detail::utf8(path), path.size(), &err); detail::check_edge_cases(err); - return Vault(result); + return Vault(adopt, result); } inline std::optional Vault::peek() const { - weaveffi_error err{}; - auto result = weaveffi_edge_cases_Vault_peek(handle_, &err); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_Vault_peek(handle_, &err); detail::check(err); if (!result) return std::nullopt; - return Widget(result); + return Widget(adopt, result); } inline bool Vault::probe() { - weaveffi_error err{}; - auto result = weaveffi_edge_cases_Vault_probe(&err); + check_library(); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_Vault_probe(&err); detail::check_edge_cases(err); return result; } @@ -1337,13 +1315,15 @@ namespace edge_cases { /** No parameters and no result */ inline void noop() { - weaveffi_error err{}; - weaveffi_edge_cases_noop(&err); + check_library(); + edge_cases_error err{}; + edge_cases_edge_cases_noop(&err); detail::check(err); } /** Function named after a keyword whose parameters are all keywords */ -inline int32_t select(int32_t class_, const std::string& from, const std::vector& func, const std::optional& range, bool type_) { +inline int32_t select(int32_t class_, std::string_view from, const std::vector& func, const std::optional& range, bool type_) { + check_library(); detail::BufferWriter func_buf; func_buf.write_len(func.size()); for (const auto& item0 : func) { @@ -1354,14 +1334,15 @@ inline int32_t select(int32_t class_, const std::string& from, const std::vector 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); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_select(class_, detail::utf8(from), from.size(), func_buf.data(), func_buf.size(), range_buf.data(), range_buf.size(), type_, &err); detail::check(err); return result; } /** Optional list of optional structs, the deepest legal nesting */ inline std::optional>> nested_optional(const std::optional>>& items) { + check_library(); detail::BufferWriter items_buf; items_buf.write_option_flag(items.has_value()); if (items.has_value()) { @@ -1374,17 +1355,17 @@ inline std::optional>> nested_optional(const st } } size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_edge_cases_nested_optional(items_buf.data(), items_buf.size(), &out_len, &err); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_nested_optional(items_buf.data(), items_buf.size(), &out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + 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); + size_t ret_v_n = result_r.read_count(); + ret_v.reserve(result_r.reserve_hint(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()) { @@ -1401,6 +1382,7 @@ inline std::optional>> nested_optional(const st } inline std::optional optional_params(const std::optional& text, const std::optional& count, const std::optional& outer, const std::optional>& tags, const std::optional>& blob, const std::optional& mode) { + check_library(); detail::BufferWriter text_buf; text_buf.write_option_flag(text.has_value()); if (text.has_value()) { @@ -1435,10 +1417,10 @@ inline std::optional optional_params(const std::optional& tex 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); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_optional_params(text_buf.data(), text_buf.size(), count_buf.data(), count_buf.size(), outer_buf.data(), outer_buf.size(), tags_buf.data(), tags_buf.size(), blob_buf.data(), blob_buf.size(), mode_buf.data(), mode_buf.size(), &out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + detail::BufferGuard result_guard(result, out_len); detail::BufferReader result_r(result, out_len); std::optional ret{}; if (result_r.read_option_flag()) { @@ -1451,17 +1433,19 @@ inline std::optional optional_params(const std::optional& tex /** Two callback interface parameters on one function */ inline void register_batch(std::shared_ptr listener, std::shared_ptr keyword) { + check_library(); if (!listener) throw std::invalid_argument("listener: null callback interface"); - auto* listener_ctx = new std::shared_ptr(std::move(listener)); + auto listener_ctx = std::make_unique>(std::move(listener)); if (!keyword) throw std::invalid_argument("keyword: null callback interface"); - auto* keyword_ctx = new std::shared_ptr(std::move(keyword)); - weaveffi_error err{}; - weaveffi_edge_cases_register_batch(static_cast(listener_ctx), &detail::BatchListener_vtable(), static_cast(keyword_ctx), &detail::Keyword_vtable(), &err); + auto keyword_ctx = std::make_unique>(std::move(keyword)); + edge_cases_error err{}; + edge_cases_edge_cases_register_batch(static_cast(listener_ctx.release()), &detail::BatchListener_vtable(), static_cast(keyword_ctx.release()), &detail::Keyword_vtable(), &err); detail::check(err); } /** Objects in every buffered position */ inline WidgetHolder widgets_everywhere(const std::vector& list, const std::unordered_map& by_name, const std::vector>& sparse, const WidgetHolder& holder) { + check_library(); detail::BufferWriter list_buf; list_buf.write_len(list.size()); for (const auto& item0 : list) { @@ -1484,10 +1468,10 @@ inline WidgetHolder widgets_everywhere(const std::vector& list, const st detail::BufferWriter holder_buf; detail::write_WidgetHolder(holder_buf, holder); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_edge_cases_widgets_everywhere(list_buf.data(), list_buf.size(), by_name_buf.data(), by_name_buf.size(), sparse_buf.data(), sparse_buf.size(), holder_buf.data(), holder_buf.size(), &out_len, &err); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_widgets_everywhere(list_buf.data(), list_buf.size(), by_name_buf.data(), by_name_buf.size(), sparse_buf.data(), sparse_buf.size(), holder_buf.data(), holder_buf.size(), &out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + detail::BufferGuard result_guard(result, out_len); detail::BufferReader result_r(result, out_len); WidgetHolder ret = detail::read_WidgetHolder(result_r); result_r.expect_end(); @@ -1500,14 +1484,14 @@ inline WidgetHolder widgets_everywhere(const std::vector& list, const st * 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 WidgetStreamIterator { - weaveffi_edge_cases_WidgetStreamIterator* handle_; + edge_cases_edge_cases_WidgetStreamIterator* handle_; public: - /** Adopts ownership of the raw producer iterator. */ - explicit WidgetStreamIterator(weaveffi_edge_cases_WidgetStreamIterator* h) : handle_(h) {} + /** Adopts ownership of a raw producer iterator. */ + explicit WidgetStreamIterator(adopt_t, edge_cases_edge_cases_WidgetStreamIterator* h) noexcept : handle_(h) {} ~WidgetStreamIterator() { - if (handle_) weaveffi_edge_cases_WidgetStreamIterator_destroy(handle_); + if (handle_) edge_cases_edge_cases_WidgetStreamIterator_destroy(handle_); } WidgetStreamIterator(const WidgetStreamIterator&) = delete; @@ -1519,30 +1503,30 @@ public: WidgetStreamIterator& operator=(WidgetStreamIterator&& other) noexcept { if (this != &other) { - if (handle_) weaveffi_edge_cases_WidgetStreamIterator_destroy(handle_); + if (handle_) edge_cases_edge_cases_WidgetStreamIterator_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. */ + /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic Error after releasing the iterator. */ std::optional next() { if (!handle_) return std::nullopt; - weaveffi_error err{}; - weaveffi_edge_cases_Widget* item{}; - int32_t has_item = weaveffi_edge_cases_WidgetStreamIterator_next(handle_, &item, &err); + edge_cases_error err{}; + edge_cases_edge_cases_Widget* item{}; + int32_t has_item = edge_cases_edge_cases_WidgetStreamIterator_next(handle_, &item, &err); if (err.code != 0) { - weaveffi_edge_cases_WidgetStreamIterator_destroy(handle_); + edge_cases_edge_cases_WidgetStreamIterator_destroy(handle_); handle_ = nullptr; detail::check(err); } if (has_item == 0) { - weaveffi_edge_cases_WidgetStreamIterator_destroy(handle_); + edge_cases_edge_cases_WidgetStreamIterator_destroy(handle_); handle_ = nullptr; return std::nullopt; } - return Widget(item); + return Widget(adopt, item); } /** Sentinel type marking the end of the range. */ @@ -1583,59 +1567,62 @@ public: * @return A lazy `WidgetStreamIterator` range that streams one element per iteration step and releases the producer iterator when exhausted or destroyed. */ inline WidgetStreamIterator widget_stream() { - weaveffi_error err{}; - weaveffi_edge_cases_WidgetStreamIterator* iter = weaveffi_edge_cases_widget_stream(&err); + check_library(); + edge_cases_error err{}; + edge_cases_edge_cases_WidgetStreamIterator* iter = edge_cases_edge_cases_widget_stream(&err); detail::check(err); - return WidgetStreamIterator(iter); + return WidgetStreamIterator(adopt, iter); } /** Async returning an object */ inline std::future async_widget() { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_edge_cases_async_widget_async([](void* context, weaveffi_error* err, weaveffi_edge_cases_Widget* result) { - auto* p = static_cast*>(context); + check_library(); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + edge_cases_edge_cases_async_widget([](void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result) { + std::unique_ptr> p(static_cast*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_edge_cases_error(err->code, msg, err->payload_ptr, err->payload_len)); } else { - p->set_value(Widget(result)); + p->set_value(Widget(adopt, result)); } } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); return future; } /** 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); +inline Widget make_widget(std::string_view label) { + check_library(); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_make_widget(detail::utf8(label), label.size(), &err); detail::check(err); - return Widget(result); + return Widget(adopt, result); } /** Optional interface return */ inline std::optional find_widget(int64_t id) { - weaveffi_error err{}; - auto result = weaveffi_edge_cases_find_widget(id, &err); + check_library(); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_find_widget(id, &err); detail::check_edge_cases(err); if (!result) return std::nullopt; - return Widget(result); + return Widget(adopt, 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(widget.handle(), fallback.has_value() ? fallback->handle() : nullptr, &err); + check_library(); + size_t out_len = 0; + edge_cases_error err{}; + auto result = edge_cases_edge_cases_inspect_widget(widget.handle(), fallback.has_value() ? fallback->handle() : nullptr, &out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } /** @@ -1644,14 +1631,14 @@ inline std::string inspect_widget(const Widget& widget, const 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); + edge_cases_error err{}; + const uint8_t* item{}; + size_t item_len = 0; + int32_t has_item = edge_cases_edge_cases_StringStreamIterator_next(handle_, &item, &item_len, &err); if (err.code != 0) { - weaveffi_edge_cases_StringStreamIterator_destroy(handle_); + edge_cases_edge_cases_StringStreamIterator_destroy(handle_); handle_ = nullptr; detail::check(err); } if (has_item == 0) { - weaveffi_edge_cases_StringStreamIterator_destroy(handle_); + edge_cases_edge_cases_StringStreamIterator_destroy(handle_); handle_ = nullptr; return std::nullopt; } - std::string value(item); - weaveffi_free_string(item); - return value; + return detail::take_string(item, item_len); } /** Sentinel type marking the end of the range. */ @@ -1729,10 +1715,11 @@ public: * @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); + check_library(); + edge_cases_error err{}; + edge_cases_edge_cases_StringStreamIterator* iter = edge_cases_edge_cases_string_stream(&err); detail::check(err); - return StringStreamIterator(iter); + return StringStreamIterator(adopt, iter); } /** @@ -1741,14 +1728,14 @@ inline StringStreamIterator 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 ScalarStreamIterator { - weaveffi_edge_cases_ScalarStreamIterator* handle_; + edge_cases_edge_cases_ScalarStreamIterator* handle_; public: - /** Adopts ownership of the raw producer iterator. */ - explicit ScalarStreamIterator(weaveffi_edge_cases_ScalarStreamIterator* h) : handle_(h) {} + /** Adopts ownership of a raw producer iterator. */ + explicit ScalarStreamIterator(adopt_t, edge_cases_edge_cases_ScalarStreamIterator* h) noexcept : handle_(h) {} ~ScalarStreamIterator() { - if (handle_) weaveffi_edge_cases_ScalarStreamIterator_destroy(handle_); + if (handle_) edge_cases_edge_cases_ScalarStreamIterator_destroy(handle_); } ScalarStreamIterator(const ScalarStreamIterator&) = delete; @@ -1760,26 +1747,26 @@ public: ScalarStreamIterator& operator=(ScalarStreamIterator&& other) noexcept { if (this != &other) { - if (handle_) weaveffi_edge_cases_ScalarStreamIterator_destroy(handle_); + if (handle_) edge_cases_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. */ + /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic Error after releasing the iterator. */ std::optional next() { if (!handle_) return std::nullopt; - weaveffi_error err{}; + edge_cases_error err{}; int32_t item{}; - int32_t has_item = weaveffi_edge_cases_ScalarStreamIterator_next(handle_, &item, &err); + int32_t has_item = edge_cases_edge_cases_ScalarStreamIterator_next(handle_, &item, &err); if (err.code != 0) { - weaveffi_edge_cases_ScalarStreamIterator_destroy(handle_); + edge_cases_edge_cases_ScalarStreamIterator_destroy(handle_); handle_ = nullptr; detail::check(err); } if (has_item == 0) { - weaveffi_edge_cases_ScalarStreamIterator_destroy(handle_); + edge_cases_edge_cases_ScalarStreamIterator_destroy(handle_); handle_ = nullptr; return std::nullopt; } @@ -1824,10 +1811,11 @@ public: * @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); + check_library(); + edge_cases_error err{}; + edge_cases_edge_cases_ScalarStreamIterator* iter = edge_cases_edge_cases_scalar_stream(&err); detail::check(err); - return ScalarStreamIterator(iter); + return ScalarStreamIterator(adopt, iter); } /** @@ -1836,14 +1824,14 @@ inline ScalarStreamIterator 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 ModeStreamIterator { - weaveffi_edge_cases_ModeStreamIterator* handle_; + edge_cases_edge_cases_ModeStreamIterator* handle_; public: - /** Adopts ownership of the raw producer iterator. */ - explicit ModeStreamIterator(weaveffi_edge_cases_ModeStreamIterator* h) : handle_(h) {} + /** Adopts ownership of a raw producer iterator. */ + explicit ModeStreamIterator(adopt_t, edge_cases_edge_cases_ModeStreamIterator* h) noexcept : handle_(h) {} ~ModeStreamIterator() { - if (handle_) weaveffi_edge_cases_ModeStreamIterator_destroy(handle_); + if (handle_) edge_cases_edge_cases_ModeStreamIterator_destroy(handle_); } ModeStreamIterator(const ModeStreamIterator&) = delete; @@ -1855,26 +1843,26 @@ public: ModeStreamIterator& operator=(ModeStreamIterator&& other) noexcept { if (this != &other) { - if (handle_) weaveffi_edge_cases_ModeStreamIterator_destroy(handle_); + if (handle_) edge_cases_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. */ + /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic Error 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); + edge_cases_error err{}; + edge_cases_edge_cases_Mode item{}; + int32_t has_item = edge_cases_edge_cases_ModeStreamIterator_next(handle_, &item, &err); if (err.code != 0) { - weaveffi_edge_cases_ModeStreamIterator_destroy(handle_); + edge_cases_edge_cases_ModeStreamIterator_destroy(handle_); handle_ = nullptr; detail::check(err); } if (has_item == 0) { - weaveffi_edge_cases_ModeStreamIterator_destroy(handle_); + edge_cases_edge_cases_ModeStreamIterator_destroy(handle_); handle_ = nullptr; return std::nullopt; } @@ -1919,21 +1907,23 @@ public: * @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); + check_library(); + edge_cases_error err{}; + edge_cases_edge_cases_ModeStreamIterator* iter = edge_cases_edge_cases_mode_stream(&err); detail::check(err); - return ModeStreamIterator(iter); + return ModeStreamIterator(adopt, 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); + check_library(); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + edge_cases_edge_cases_async_void([](void* context, edge_cases_error* err) { + std::unique_ptr> p(static_cast*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_error(err->code, msg)); } else { p->set_value(); @@ -1941,46 +1931,44 @@ inline std::future async_void() { } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); 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); + check_library(); + auto promise = std::make_unique>>(); + auto future = promise->get_future(); + edge_cases_edge_cases_async_bytes(size, [](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + std::unique_ptr>> p(static_cast>*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_error(err->code, msg)); } else { - std::vector value(result, result + result_len); - weaveffi_free_bytes(const_cast(result), result_len); - p->set_value(std::move(value)); + p->set_value(detail::take_bytes(result_ptr, result_len)); } } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); 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); + check_library(); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + edge_cases_edge_cases_async_struct([](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + std::unique_ptr> p(static_cast*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_edge_cases_error(err->code, msg, err->payload_ptr, err->payload_len)); } else { - detail::BufferGuard result_guard{result_ptr, result_len}; + detail::BufferGuard result_guard(result_ptr, result_len); detail::BufferReader result_r(result_ptr, result_len); Outer value = detail::read_Outer(result_r); result_r.expect_end(); @@ -1989,28 +1977,28 @@ inline std::future async_struct() { } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); return future; } -inline std::future> async_list(weaveffi_cancel_token* cancel_token) { - 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); +inline std::future> async_list(const CancelToken& cancel_token = CancelToken::none()) { + check_library(); + auto promise = std::make_unique>>(); + auto future = promise->get_future(); + edge_cases_edge_cases_async_list(cancel_token.handle(), [](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + std::unique_ptr>> p(static_cast>*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_edge_cases_error(err->code, msg, err->payload_ptr, err->payload_len)); } else { - detail::BufferGuard result_guard{result_ptr, result_len}; + detail::BufferGuard result_guard(result_ptr, result_len); detail::BufferReader result_r(result_ptr, result_len); std::vector value{}; { - size_t value_n = result_r.read_len(); - value.reserve(value_n); + size_t value_n = result_r.read_count(); + value.reserve(result_r.reserve_hint(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)); @@ -2022,23 +2010,23 @@ inline std::future> async_list(weaveffi_cancel_token* cancel_ } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); 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); + check_library(); + auto promise = std::make_unique>>(); + auto future = promise->get_future(); + edge_cases_edge_cases_async_optional([](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + std::unique_ptr>> p(static_cast>*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_error(err->code, msg)); } else { - detail::BufferGuard result_guard{result_ptr, result_len}; + detail::BufferGuard result_guard(result_ptr, result_len); detail::BufferReader result_r(result_ptr, result_len); std::optional value{}; if (result_r.read_option_flag()) { @@ -2051,20 +2039,20 @@ inline std::future> async_optional() { } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); 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); + check_library(); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + edge_cases_edge_cases_async_enum([](void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result) { + std::unique_ptr> p(static_cast*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_error(err->code, msg)); } else { p->set_value(static_cast(result)); @@ -2072,43 +2060,41 @@ inline std::future async_enum() { } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); 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); +inline std::future async_keywords(std::string_view class_, int32_t import) { + check_library(); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + edge_cases_edge_cases_async_keywords(detail::utf8(class_), class_.size(), import, [](void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + std::unique_ptr> p(static_cast*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_error(err->code, msg)); } else { - std::string value(result ? result : ""); - weaveffi_free_string(result); - p->set_value(std::move(value)); + p->set_value(detail::take_string(result_ptr, result_len)); } } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + edge_cases_error_free(err); + }, static_cast(promise.release())); return future; } inline Keywords keywords_roundtrip(const Keywords& value) { + check_library(); detail::BufferWriter value_buf; detail::write_Keywords(value_buf, value); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_edge_cases_keywords_roundtrip(value_buf.data(), value_buf.size(), &out_len, &err); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_keywords_roundtrip(value_buf.data(), value_buf.size(), &out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + 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(); @@ -2117,11 +2103,12 @@ inline Keywords keywords_roundtrip(const Keywords& value) { [[deprecated("Use Mode-based APIs")]] inline Retired legacy_value(Legacy value) { + check_library(); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_edge_cases_legacy_value(static_cast(static_cast(value)), &out_len, &err); + edge_cases_error err{}; + auto result = edge_cases_edge_cases_legacy_value(static_cast(static_cast(value)), &out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + 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(); @@ -2130,6 +2117,6 @@ inline Retired legacy_value(Legacy value) { } // namespace edge_cases -} // namespace weaveffi +} // namespace edge_cases -// END weaveffi.hpp +// END edge_cases.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__CMakeLists_txt.snap index f169a251..6eee449a 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__CMakeLists_txt.snap @@ -1,15 +1,26 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.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) +project(kitchen_sink_cpp VERSION 0.1.0 LANGUAGES CXX) + +# The producer library to link: a full path from the KITCHEN_SINK_LIBRARY cache or +# environment variable, or else the library name `kitchen_sink` (a CMake target +# of that name, or libkitchen_sink on the linker search path). +set(KITCHEN_SINK_LIBRARY "$ENV{KITCHEN_SINK_LIBRARY}" CACHE FILEPATH "Path to the kitchen_sink native library") + +add_library(kitchen_sink_cpp INTERFACE) +add_library(kitchen_sink::cpp ALIAS kitchen_sink_cpp) +target_include_directories(kitchen_sink_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_features(kitchen_sink_cpp INTERFACE cxx_std_17) +if(KITCHEN_SINK_LIBRARY) + target_link_libraries(kitchen_sink_cpp INTERFACE "${KITCHEN_SINK_LIBRARY}") +else() + target_link_libraries(kitchen_sink_cpp INTERFACE kitchen_sink) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__README_md.snap index da3ea6c6..e9bf90b7 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__README_md.snap @@ -1,39 +1,43 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI C++ Bindings +# kitchen_sink C++ bindings -## Prerequisites +A header-only C++17 wrapper over the `kitchen_sink` C ABI. `kitchen_sink.hpp` +includes the C header beside it; every declaration lives in +`namespace kitchen_sink`. -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate +## Prerequisites -## Usage with CMake +- CMake 3.14 or later +- A C++17 compiler +- The `kitchen_sink` native library (`libkitchen_sink.so`, `libkitchen_sink.dylib`, or + `kitchen_sink.dll`) -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: +## Use with CMake ```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) +add_subdirectory(path/to/cpp) +add_executable(app main.cpp) +target_link_libraries(app PRIVATE kitchen_sink::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: +`kitchen_sink::cpp` adds this directory to the include path, requires +C++17, and links the native library: the file named by the `KITCHEN_SINK_LIBRARY` +CMake cache variable or environment variable when set, or else +`kitchen_sink` by name. ```cpp -#include "weaveffi.hpp" +#include "kitchen_sink.hpp" ``` +The first free function, constructor, or static member you call checks +that the linked library matches these headers and throws +`kitchen_sink::LoadError` if it doesn't; call `kitchen_sink::check_library()` +at startup to check eagerly. + diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_h.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_h.snap new file mode 100644 index 00000000..14faeffb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_h.snap @@ -0,0 +1,219 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef KITCHEN_SINK_H +#define KITCHEN_SINK_H + +#include +#include +#include + +#ifndef KITCHEN_SINK_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef KITCHEN_SINK_BUILD +# define KITCHEN_SINK_API __declspec(dllexport) +# else +# define KITCHEN_SINK_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define KITCHEN_SINK_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define KITCHEN_SINK_API __attribute__((visibility("default"))) +# else +# define KITCHEN_SINK_API +# endif +#endif + +#ifndef KITCHEN_SINK_DEPRECATED +# if defined(_MSC_VER) +# define KITCHEN_SINK_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define KITCHEN_SINK_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define KITCHEN_SINK_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports kitchen_sink_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define KITCHEN_SINK_ABI_VERSION 3u +KITCHEN_SINK_API uint32_t kitchen_sink_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by kitchen_sink_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct kitchen_sink_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} kitchen_sink_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +KITCHEN_SINK_API void kitchen_sink_error_set(kitchen_sink_error* err, int32_t code, const char* message); +KITCHEN_SINK_API void kitchen_sink_error_clear(kitchen_sink_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + kitchen_sink_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_error_free(kitchen_sink_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +KITCHEN_SINK_API void kitchen_sink_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct kitchen_sink_cancel_token kitchen_sink_cancel_token; +KITCHEN_SINK_API kitchen_sink_cancel_token* kitchen_sink_cancel_token_create(void); +KITCHEN_SINK_API void kitchen_sink_cancel_token_cancel(kitchen_sink_cancel_token* token); +KITCHEN_SINK_API bool kitchen_sink_cancel_token_is_cancelled(const kitchen_sink_cancel_token* token); +KITCHEN_SINK_API void kitchen_sink_cancel_token_destroy(kitchen_sink_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +KITCHEN_SINK_API uint8_t* kitchen_sink_alloc(uint32_t size); +KITCHEN_SINK_API void kitchen_sink_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +KITCHEN_SINK_API uint64_t kitchen_sink_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define KITCHEN_SINK_SHARED_CHECKSUM 0x42c4ce2c8d0af052ull +KITCHEN_SINK_API uint64_t kitchen_sink_shared_checksum(void); +#define KITCHEN_SINK_KITCHEN_CHECKSUM 0xd83da75b24b545ffull +KITCHEN_SINK_API uint64_t kitchen_sink_kitchen_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with kitchen_sink_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * kitchen_sink_buffer.h declares a C struct and codec for each. + */ + +/** Task priority levels */ +typedef enum { + /** Low priority */ + kitchen_sink_kitchen_Priority_Low = 0, + /** Normal priority */ + kitchen_sink_kitchen_Priority_Normal = 1, + /** High priority */ + kitchen_sink_kitchen_Priority_High = 2 +} kitchen_sink_kitchen_Priority; +/** Error codes reported by throwing functions in the `kitchen` module tree. */ +typedef enum { kitchen_sink_kitchen_KitchenErrors_NotFound = 1, kitchen_sink_kitchen_KitchenErrors_InvalidInput = 2 } kitchen_sink_kitchen_KitchenErrors; +typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget; +typedef struct kitchen_sink_kitchen_StreamGadgetsIterator kitchen_sink_kitchen_StreamGadgetsIterator; +typedef struct kitchen_sink_kitchen_StreamItemsIterator kitchen_sink_kitchen_StreamItemsIterator; +/** + * Consumer-implemented observer of ready items + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct kitchen_sink_kitchen_ReadyListener_vtable { + /** Fires when an item is ready */ + void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); + /** Receives the item itself and says whether to keep listening */ + bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); + void (*free)(void* ctx); +} kitchen_sink_kitchen_ReadyListener_vtable; +typedef void (*kitchen_sink_kitchen_do_async_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*kitchen_sink_kitchen_do_cancellable_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: shared +/** Trivial cross-module helper */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_shared_ping(size_t* out_len, kitchen_sink_error* out_err); + +// Module: kitchen +/** A pocket-sized interface exercising the object surface */ +/** Create a gadget with the given id */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(int64_t id, kitchen_sink_error* out_err); +/** The gadget subsystem version string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_version(size_t* out_len, kitchen_sink_error* out_err); +/** Render the gadget as a human-readable string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err); +/** Poke the gadget a number of times, failing on a negative count */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_Gadget_poke(const kitchen_sink_kitchen_Gadget* self, int32_t times, kitchen_sink_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_kitchen_Gadget_destroy(kitchen_sink_kitchen_Gadget* self); + +/** Identity for bool */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_bool_id(bool x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_i32_id(int32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API uint32_t kitchen_sink_kitchen_u32_id(uint32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int64_t kitchen_sink_kitchen_i64_id(int64_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API double kitchen_sink_kitchen_f64_id(double x, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_string(const uint8_t* s_ptr, size_t s_len, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_bytes(const uint8_t* b_ptr, size_t b_len, size_t* out_len, kitchen_sink_error* out_err); +/** Register a listener, returning how many are attached */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err); +/** Optional object in both directions */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_maybe_gadget(const kitchen_sink_kitchen_Gadget* fallback, kitchen_sink_error* out_err); +/** A list of objects */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_gadgets(size_t* out_len, kitchen_sink_error* out_err); +/** An iterator over objects */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamGadgetsIterator* kitchen_sink_kitchen_stream_gadgets(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamGadgetsIterator_next(kitchen_sink_kitchen_StreamGadgetsIterator* iter, kitchen_sink_kitchen_Gadget** out_item, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamGadgetsIterator_destroy(kitchen_sink_kitchen_StreamGadgetsIterator* iter); +/** Optional struct return */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_maybe_item(int64_t id, size_t* out_len, kitchen_sink_error* out_err); +/** List of structs */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_list_items(size_t* out_len, kitchen_sink_error* out_err); +/** Map return type */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_get_attrs(size_t* out_len, kitchen_sink_error* out_err); +/** Iterator return type */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamItemsIterator* kitchen_sink_kitchen_stream_items(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamItemsIterator_next(kitchen_sink_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamItemsIterator_destroy(kitchen_sink_kitchen_StreamItemsIterator* iter); +/** Returns the shared Token type from another module */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_cross_module_token(size_t* out_len, kitchen_sink_error* out_err); +/** Async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_async(const uint8_t* input_ptr, size_t input_len, kitchen_sink_kitchen_do_async_callback callback, void* context); +/** Cancellable async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_ptr, size_t input_len, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_do_cancellable_callback callback, void* context); +/** Legacy operation kept for compatibility */ +KITCHEN_SINK_DEPRECATED("Use new_op instead") +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_legacy_op(kitchen_sink_error* out_err); +/** Replacement for legacy_op */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_new_op(kitchen_sink_error* out_err); + +// Module: kitchen_nested +/** Trivial nested-module function */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_nested_hello(size_t* out_len, kitchen_sink_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // KITCHEN_SINK_H + +// END kitchen_sink.h diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_hpp.snap similarity index 52% rename from crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap rename to crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_hpp.snap index 35bf650a..3ba874d8 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__kitchen_sink_hpp.snap @@ -1,244 +1,233 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o #pragma once +#include "kitchen_sink.h" + +#include #include -#include -#include -#include -#include -#include -#include -#include -#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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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); +namespace kitchen_sink { -/** Task priority levels */ -typedef enum { - /** Low priority */ - weaveffi_kitchen_Priority_Low = 0, - /** Normal priority */ - weaveffi_kitchen_Priority_Normal = 1, - /** High priority */ - weaveffi_kitchen_Priority_High = 2 -} weaveffi_kitchen_Priority; -/** Error codes reported by throwing functions in the `kitchen` module tree. */ -typedef enum { weaveffi_kitchen_KitchenErrors_NotFound = 1, weaveffi_kitchen_KitchenErrors_InvalidInput = 2 } weaveffi_kitchen_KitchenErrors; -typedef struct weaveffi_kitchen_Gadget weaveffi_kitchen_Gadget; -typedef struct weaveffi_kitchen_StreamGadgetsIterator weaveffi_kitchen_StreamGadgetsIterator; -typedef struct weaveffi_kitchen_StreamItemsIterator weaveffi_kitchen_StreamItemsIterator; /** - * Consumer-implemented observer of ready items + * Base exception for every error reported through the C ABI. * - * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + * Positive codes are the declared error codes (see the typed domain + * exceptions); negative codes are runtime traps: -1 a generic producer + * error, -2 a producer panic, -3 a marshalling failure, -4 a + * callback-interface implementation that threw, and -5 a cancelled call + * (thrown as Cancelled). */ -typedef struct weaveffi_kitchen_ReadyListener_vtable { - /** Fires when an item is ready */ - void (*on_ready)(void* ctx, int32_t code, const char* msg, weaveffi_error* out_err); - /** Receives the item itself and says whether to keep listening */ - bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, weaveffi_kitchen_Gadget* gadget, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_kitchen_ReadyListener_vtable; -typedef void (*weaveffi_kitchen_do_async_callback)(void* context, weaveffi_error* err, const char* result); -typedef void (*weaveffi_kitchen_do_cancellable_callback)(void* context, weaveffi_error* err, const char* result); +class Error : public std::runtime_error { + int32_t code_; -/** Trivial cross-module helper */ -WEAVEFFI_API const char* weaveffi_shared_ping(weaveffi_error* out_err); +public: + /** Builds an error carrying `code` and `message`. */ + Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} -/** A pocket-sized interface exercising the object surface */ -/** Create a gadget with the given id */ -WEAVEFFI_API weaveffi_kitchen_Gadget* weaveffi_kitchen_Gadget_new(int64_t id, weaveffi_error* out_err); -/** The gadget subsystem version string */ -WEAVEFFI_API const char* weaveffi_kitchen_Gadget_version(weaveffi_error* out_err); -/** Render the gadget as a human-readable string */ -WEAVEFFI_API const char* weaveffi_kitchen_Gadget_describe(const weaveffi_kitchen_Gadget* self, weaveffi_error* out_err); -/** Poke the gadget a number of times, failing on a negative count */ -WEAVEFFI_API int32_t weaveffi_kitchen_Gadget_poke(const weaveffi_kitchen_Gadget* self, int32_t times, weaveffi_error* out_err); -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_kitchen_Gadget* weaveffi_kitchen_Gadget_clone(const weaveffi_kitchen_Gadget* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_kitchen_Gadget_destroy(weaveffi_kitchen_Gadget* self); + /** The numeric error code. */ + int32_t code() const noexcept { return code_; } +}; -/** Identity for bool */ -WEAVEFFI_API bool weaveffi_kitchen_bool_id(bool x, weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_kitchen_i32_id(int32_t x, weaveffi_error* out_err); -WEAVEFFI_API uint32_t weaveffi_kitchen_u32_id(uint32_t x, weaveffi_error* out_err); -WEAVEFFI_API int64_t weaveffi_kitchen_i64_id(int64_t x, weaveffi_error* out_err); -WEAVEFFI_API double weaveffi_kitchen_f64_id(double x, weaveffi_error* out_err); -WEAVEFFI_API const char* weaveffi_kitchen_echo_string(const char* s, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_kitchen_echo_bytes(const uint8_t* b_ptr, size_t b_len, size_t* out_len, weaveffi_error* out_err); -/** Register a listener, returning how many are attached */ -WEAVEFFI_API int32_t weaveffi_kitchen_subscribe(void* listener_ctx, const weaveffi_kitchen_ReadyListener_vtable* listener_vtable, weaveffi_error* out_err); -/** Optional object in both directions */ -WEAVEFFI_API weaveffi_kitchen_Gadget* weaveffi_kitchen_maybe_gadget(const weaveffi_kitchen_Gadget* fallback, weaveffi_error* out_err); -/** A list of objects */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_gadgets(size_t* out_len, weaveffi_error* out_err); -/** An iterator over objects */ -WEAVEFFI_API weaveffi_kitchen_StreamGadgetsIterator* weaveffi_kitchen_stream_gadgets(weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_kitchen_StreamGadgetsIterator_next(weaveffi_kitchen_StreamGadgetsIterator* iter, weaveffi_kitchen_Gadget** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_kitchen_StreamGadgetsIterator_destroy(weaveffi_kitchen_StreamGadgetsIterator* iter); -/** Optional struct return */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_maybe_item(int64_t id, size_t* out_len, weaveffi_error* out_err); -/** List of structs */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_list_items(size_t* out_len, weaveffi_error* out_err); -/** Map return type */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_get_attrs(size_t* out_len, weaveffi_error* out_err); -/** Iterator return type */ -WEAVEFFI_API weaveffi_kitchen_StreamItemsIterator* weaveffi_kitchen_stream_items(weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_kitchen_StreamItemsIterator_next(weaveffi_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_kitchen_StreamItemsIterator_destroy(weaveffi_kitchen_StreamItemsIterator* iter); -/** Returns the shared Token type from another module */ -WEAVEFFI_API const uint8_t* weaveffi_kitchen_cross_module_token(size_t* out_len, weaveffi_error* out_err); -/** Async operation */ -WEAVEFFI_API void weaveffi_kitchen_do_async_async(const char* input, weaveffi_kitchen_do_async_callback callback, void* context); -/** Cancellable async operation */ -WEAVEFFI_API void weaveffi_kitchen_do_cancellable_async(const char* input, weaveffi_cancel_token* cancel_token, weaveffi_kitchen_do_cancellable_callback callback, void* context); -/** Legacy operation kept for compatibility */ -WEAVEFFI_DEPRECATED("Use new_op instead") -WEAVEFFI_API int32_t weaveffi_kitchen_legacy_op(weaveffi_error* out_err); -/** Replacement for legacy_op */ -WEAVEFFI_API int32_t weaveffi_kitchen_new_op(weaveffi_error* out_err); +/** Thrown by an async call whose CancelToken was cancelled (code -5). */ +class Cancelled : public Error { +public: + /** Builds a cancellation error carrying the producer's message. */ + explicit Cancelled(const std::string& message) : Error(-5, message) {} +}; -/** Trivial nested-module function */ -WEAVEFFI_API const char* weaveffi_kitchen_nested_hello(weaveffi_error* out_err); +/** + * Thrown on first use when the linked kitchen_sink library was built for a + * different C ABI revision or a different API contract than this header. + */ +class LoadError : public Error { +public: + /** Builds a load error describing the mismatch. */ + explicit LoadError(const std::string& message) : Error(-1, message) {} +}; -} // extern "C" +/** + * Tag selecting the constructor that adopts a raw C pointer (one strong + * reference the wrapper then owns), in the style of std::adopt_lock. + */ +struct adopt_t { + explicit adopt_t() = default; +}; -namespace weaveffi { +/** The adopt_t tag value: `Gadget g(adopt, raw)`. */ +inline constexpr adopt_t adopt{}; /** - * Base exception for every error reported through the C ABI. + * A cancellation token for cancellable async functions. * - * Positive codes are the module's declared error codes (see the typed - * domain exceptions); negative codes are runtime traps: -1 a generic - * producer error, -2 a producer panic, -3 a marshalling failure, -4 a - * callback-interface implementation that threw. + * Create one, pass it to any number of cancellable calls, and call cancel() + * from any thread. A cancelled call settles its future with Cancelled. Each + * call takes its own reference to the native token, so the token can be + * destroyed at any time; the destructor releases this wrapper's reference. */ -class WeaveFFIError : public std::runtime_error { - int32_t code_; +class CancelToken { + kitchen_sink_cancel_token* handle_; + + struct none_t {}; + explicit CancelToken(none_t) noexcept : handle_(nullptr) {} public: - WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} - int32_t code() const { return code_; } -}; + /** Creates a new, uncancelled token. */ + CancelToken() : handle_(kitchen_sink_cancel_token_create()) { + if (handle_ == nullptr) throw std::bad_alloc(); + } -/** - * Throws WeaveFFIError when the loaded producer library was built for a - * different WeaveFFI ABI revision than this header. Call once at startup. - */ -inline void check_abi_version() { - uint32_t actual = weaveffi_abi_version(); - if (actual != WEAVEFFI_ABI_VERSION) { - throw WeaveFFIError(-1, "WeaveFFI ABI revision mismatch: header " + std::to_string(WEAVEFFI_ABI_VERSION) + ", library " + std::to_string(actual)); + /** Releases this wrapper's reference to the native token. */ + ~CancelToken() { + if (handle_ != nullptr) kitchen_sink_cancel_token_destroy(handle_); + } + + CancelToken(const CancelToken&) = delete; + CancelToken& operator=(const CancelToken&) = delete; + + /** Transfers the native token; `other` becomes empty. */ + CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + + /** Releases the current token and takes over `other`'s. */ + CancelToken& operator=(CancelToken&& other) noexcept { + if (this != &other) { + if (handle_ != nullptr) kitchen_sink_cancel_token_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /** The empty token a cancellable call uses when none is passed. */ + static const CancelToken& none() noexcept { + static const CancelToken token{none_t{}}; + return token; } -} + + /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ + void cancel() const noexcept { + if (handle_ != nullptr) kitchen_sink_cancel_token_cancel(handle_); + } + + /** Whether cancel() has been called. */ + bool is_cancelled() const noexcept { + return handle_ != nullptr && kitchen_sink_cancel_token_is_cancelled(handle_); + } + + /** The native token, borrowed (null for an empty token). */ + kitchen_sink_cancel_token* handle() const noexcept { return handle_; } +}; namespace detail { -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { +/** The exception for a runtime (non-domain) error code. */ +inline std::exception_ptr make_error(int32_t code, const std::string& message) { + if (code == -5) return std::make_exception_ptr(Cancelled(message)); + return std::make_exception_ptr(Error(code, message)); +} + +/** The message of a nonzero error slot. */ +inline std::string error_message(const kitchen_sink_error& err) { + return std::string(err.message != nullptr ? err.message : "unknown error"); +} + +/** Throws the generic Error (or Cancelled) if `err` carries a nonzero code. */ +inline void check(kitchen_sink_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); + std::exception_ptr ex = make_error(err.code, error_message(err)); + kitchen_sink_error_clear(&err); + std::rethrow_exception(ex); } -/** 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)); +/** Copies a producer-returned UTF-8 run into a string, then releases it. */ +inline std::string take_string(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::string(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { kitchen_sink_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::string(reinterpret_cast(ptr), len); +} + +/** Copies a producer-returned byte run into a vector, then releases it. */ +inline std::vector take_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::vector(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { kitchen_sink_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::vector(ptr, ptr + len); +} + +/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ +inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { + if (ptr == nullptr || len == 0) return std::string_view(); + return std::string_view(reinterpret_cast(ptr), len); +} + +/** Views a borrowed byte run (null when empty) as an owned vector. */ +inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr || len == 0) return std::vector(); + return std::vector(ptr, ptr + len); +} + +/** The data pointer of a string argument, as the C ABI expects it. */ +inline const uint8_t* utf8(std::string_view s) noexcept { + return reinterpret_cast(s.data()); } } // namespace detail +/** + * Verifies that the linked kitchen_sink library implements the C ABI revision + * and the module contracts this header was generated against, throwing + * LoadError naming the first mismatch. The check runs once; every free + * function, constructor, and static member calls this before its first + * native call, so calling it at startup is optional. + */ +inline void check_library() { + static const std::string failure = [] { + uint32_t abi = kitchen_sink_abi_version(); + if (abi != KITCHEN_SINK_ABI_VERSION) { + return "kitchen_sink: C ABI revision mismatch (header " + std::to_string(KITCHEN_SINK_ABI_VERSION) + + ", library " + std::to_string(abi) + ")"; + } + if (kitchen_sink_shared_checksum() != 0x42c4ce2c8d0af052ull) { + return std::string("kitchen_sink: module 'shared' does not match the linked library (contract checksum mismatch); regenerate the bindings for this build"); + } + if (kitchen_sink_kitchen_checksum() != 0xd83da75b24b545ffull) { + return std::string("kitchen_sink: module 'kitchen' does not match the linked library (contract checksum mismatch); regenerate the bindings for this build"); + } + return std::string(); + }(); + if (!failure.empty()) throw LoadError(failure); +} + namespace detail { /** - * Serializes values into the WeaveFFI value-buffer wire format: little-endian, - * packed with no alignment, lengths and element counts as u32. + * Serializes values into the value-buffer wire format: little-endian, packed + * with no alignment, lengths and element counts as u32. */ class BufferWriter { std::vector buf_; @@ -252,10 +241,10 @@ class BufferWriter { public: /** Pointer to the encoded bytes. */ - const uint8_t* data() const { return buf_.data(); } + const uint8_t* data() const noexcept { return buf_.data(); } /** Number of encoded bytes. */ - size_t size() const { return buf_.size(); } + size_t size() const noexcept { return buf_.size(); } void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } @@ -279,10 +268,13 @@ public: 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)); } + /** Writes a byte length or an element count as a u32. */ + void write_len(size_t n) { + if (static_cast(n) > UINT32_MAX) throw Error(-3, "value buffer length exceeds u32"); + append_le(static_cast(n)); + } - void write_string(const std::string& v) { + void write_string(std::string_view v) { write_len(v.size()); buf_.insert(buf_.end(), v.begin(), v.end()); } @@ -297,11 +289,11 @@ public: }; /** - * 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. Object tokens adopted before a - * failure live in RAII wrappers that release them on unwind. + * Decodes values from the value-buffer wire format. A malformed buffer is a + * producer/consumer contract violation (both sides are generated from one + * API), so every decode failure throws the generic Error with the + * marshalling code -3. Object tokens adopted before a failure live in RAII + * wrappers that release them on unwind. */ class BufferReader { const uint8_t* data_; @@ -309,7 +301,7 @@ class BufferReader { size_t pos_; [[noreturn]] static void fail(const char* what) { - throw WeaveFFIError(-2, std::string("malformed WeaveFFI value buffer: ") + what); + throw Error(-3, std::string("malformed value buffer: ") + what); } void require(size_t n, const char* what) const { @@ -328,10 +320,10 @@ class BufferReader { } public: - BufferReader(const uint8_t* data, size_t len) : data_(data), len_(len), pos_(0) {} + BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} /** Bytes not yet consumed. */ - size_t remaining() const { return len_ - pos_; } + size_t remaining() const noexcept { return len_ - pos_; } bool read_bool() { uint8_t b = read_le("bool"); @@ -362,13 +354,23 @@ public: return v; } - /** Reads a length prefix, rejecting one larger than the bytes remaining. */ + /** Reads a byte length, rejecting one larger than the bytes remaining. */ size_t read_len() { uint32_t n = read_le("length"); if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); return static_cast(n); } + /** + * Reads an element count. Elements may encode to zero bytes, so a count + * larger than the bytes remaining is legal; callers cap preallocation + * with reserve_hint(). + */ + size_t read_count() { return static_cast(read_le("count")); } + + /** A preallocation size for `n` elements that a malformed count can't inflate. */ + size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } + std::string read_string() { size_t n = read_len(); std::string v(reinterpret_cast(data_) + pos_, n); @@ -395,15 +397,19 @@ public: } }; -/** Releases a producer-allocated buffer with weaveffi_free_bytes on scope exit. */ +/** Releases a producer-allocated buffer with kitchen_sink_free_bytes on scope exit. */ struct BufferGuard { /** The producer-allocated buffer, or null when the call reported an error. */ const uint8_t* ptr; /** The buffer length in bytes. */ size_t len; + BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} + BufferGuard(const BufferGuard&) = delete; + BufferGuard& operator=(const BufferGuard&) = delete; + ~BufferGuard() { - if (ptr != nullptr) weaveffi_free_bytes(const_cast(ptr), len); + if (ptr != nullptr) kitchen_sink_free_bytes(const_cast(ptr), len); } }; @@ -426,24 +432,27 @@ class ReadyListener; /** A pocket-sized interface exercising the object surface */ class Gadget { - weaveffi_kitchen_Gadget* handle_; + kitchen_sink_kitchen_Gadget* handle_; public: - /** Adopts one strong reference to a producer object. */ - explicit Gadget(weaveffi_kitchen_Gadget* h) : handle_(h) {} + /** The C type this class wraps. */ + using raw_type = kitchen_sink_kitchen_Gadget; + + /** Adopts one strong reference to a producer object: `Gadget(adopt, raw)`. */ + explicit Gadget(adopt_t, kitchen_sink_kitchen_Gadget* h) noexcept : handle_(h) {} /** Releases this wrapper's reference; the object is dropped with its last one. */ ~Gadget() { - if (handle_) weaveffi_kitchen_Gadget_destroy(handle_); + if (handle_) kitchen_sink_kitchen_Gadget_destroy(handle_); } /** Copies share the object: the copy takes a new strong reference. */ - Gadget(const Gadget& other) : handle_(weaveffi_kitchen_Gadget_clone(other.handle_)) {} + Gadget(const Gadget& other) : handle_(kitchen_sink_kitchen_Gadget_clone(other.handle_)) {} Gadget& operator=(const Gadget& other) { if (this != &other) { - weaveffi_kitchen_Gadget* h = weaveffi_kitchen_Gadget_clone(other.handle_); - if (handle_) weaveffi_kitchen_Gadget_destroy(handle_); + kitchen_sink_kitchen_Gadget* h = kitchen_sink_kitchen_Gadget_clone(other.handle_); + if (handle_) kitchen_sink_kitchen_Gadget_destroy(handle_); handle_ = h; } return *this; @@ -455,7 +464,7 @@ public: Gadget& operator=(Gadget&& other) noexcept { if (this != &other) { - if (handle_) weaveffi_kitchen_Gadget_destroy(handle_); + if (handle_) kitchen_sink_kitchen_Gadget_destroy(handle_); handle_ = other.handle_; other.handle_ = nullptr; } @@ -463,13 +472,13 @@ public: } /** The wrapped pointer, borrowed: this wrapper keeps its reference. */ - const weaveffi_kitchen_Gadget* handle() const { return handle_; } + const kitchen_sink_kitchen_Gadget* handle() const { return handle_; } /** A new strong reference the caller owns (for example to write into a value buffer). */ - weaveffi_kitchen_Gadget* clone_handle() const { return weaveffi_kitchen_Gadget_clone(handle_); } + kitchen_sink_kitchen_Gadget* clone_handle() const { return kitchen_sink_kitchen_Gadget_clone(handle_); } /** Create a gadget with the given id */ - Gadget(int64_t id); + explicit Gadget(int64_t id); /** Render the gadget as a human-readable string */ std::string describe() const; @@ -514,20 +523,20 @@ struct Item { namespace detail { -/** Encodes a `Token` in the WeaveFFI value-buffer format. */ +/** Encodes a `Token` in the value-buffer wire format. */ inline void write_Token(BufferWriter& w, const Token& v) { w.write_i64(v.id); w.write_string(v.label); } -/** Decodes a `Token` from the WeaveFFI value-buffer format. */ +/** Decodes a `Token` from the value-buffer wire format. */ inline Token read_Token(BufferReader& r) { int64_t f_id = r.read_i64(); std::string f_label = r.read_string(); return Token{std::move(f_id), std::move(f_label)}; } -/** Encodes a `Item` in the WeaveFFI value-buffer format. */ +/** Encodes a `Item` in the value-buffer wire format. */ inline void write_Item(BufferWriter& w, const Item& v) { w.write_i64(v.id); w.write_string(v.name); @@ -557,7 +566,7 @@ inline void write_Item(BufferWriter& w, const Item& v) { } } -/** Decodes a `Item` from the WeaveFFI value-buffer format. */ +/** Decodes a `Item` from the value-buffer wire format. */ inline Item read_Item(BufferReader& r) { int64_t f_id = r.read_i64(); std::string f_name = r.read_string(); @@ -568,8 +577,8 @@ inline Item read_Item(BufferReader& r) { std::vector f_payload = r.read_bytes(); std::vector f_tags{}; { - size_t f_tags_n = r.read_len(); - f_tags.reserve(f_tags_n); + size_t f_tags_n = r.read_count(); + f_tags.reserve(r.reserve_hint(f_tags_n)); for (size_t f_tags_i = 0; f_tags_i < f_tags_n; ++f_tags_i) { std::string f_tags_item = r.read_string(); f_tags.push_back(std::move(f_tags_item)); @@ -577,7 +586,7 @@ inline Item read_Item(BufferReader& r) { } std::unordered_map f_attrs{}; { - size_t f_attrs_n = r.read_len(); + size_t f_attrs_n = r.read_count(); for (size_t f_attrs_i = 0; f_attrs_i < f_attrs_n; ++f_attrs_i) { std::string f_attrs_key = r.read_string(); std::string f_attrs_val = r.read_string(); @@ -593,7 +602,7 @@ inline Item read_Item(BufferReader& r) { Priority f_priority = static_cast(r.read_i32()); std::optional f_gadget{}; if (r.read_option_flag()) { - Gadget f_gadget_v = Gadget(reinterpret_cast(static_cast(r.read_u64()))); + Gadget f_gadget_v = Gadget(adopt, reinterpret_cast(static_cast(r.read_u64()))); f_gadget = std::move(f_gadget_v); } return Item{std::move(f_id), std::move(f_name), std::move(f_count), std::move(f_enabled), std::move(f_ratio), std::move(f_nick), std::move(f_payload), std::move(f_tags), std::move(f_attrs), std::move(f_parent), std::move(f_token), std::move(f_priority), std::move(f_gadget)}; @@ -602,28 +611,28 @@ inline Item read_Item(BufferReader& r) { } // namespace detail /** Typed errors reported by the `kitchen` module's throwing functions. */ -class KitchenErrorsError : public WeaveFFIError { +class KitchenErrorsError : public Error { public: - KitchenErrorsError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} + KitchenErrorsError(int32_t code, const std::string& msg) : Error(code, msg) {} }; /** Item not found */ class NotFoundError : public KitchenErrorsError { public: - NotFoundError(const std::string& msg) : KitchenErrorsError(1, msg) {} + explicit NotFoundError(const std::string& msg) : KitchenErrorsError(1, msg) {} }; /** Invalid input */ class InvalidInputError : public KitchenErrorsError { public: - InvalidInputError(const std::string& msg) : KitchenErrorsError(2, msg) {} + explicit InvalidInputError(const std::string& msg) : KitchenErrorsError(2, msg) {} }; namespace detail { -/** Map a `kitchen` error code and payload to its typed exception (WeaveFFIError for runtime codes, KitchenErrorsError for unknown codes). */ +/** Map a `kitchen` error code and payload to its typed exception (the runtime exception for negative codes, KitchenErrorsError for unknown codes). */ inline std::exception_ptr make_kitchen_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)); + if (code < 0) return make_error(code, msg); (void)payload_ptr; (void)payload_len; switch (code) { @@ -634,11 +643,11 @@ inline std::exception_ptr make_kitchen_error(int32_t code, const std::string& ms } /** Throw the typed `kitchen` exception if `err` carries a nonzero code. */ -inline void check_kitchen(weaveffi_error& err) { +inline void check_kitchen(kitchen_sink_error& err) { if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); + std::string msg = error_message(err); std::exception_ptr ex = make_kitchen_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); + kitchen_sink_error_clear(&err); std::rethrow_exception(ex); } @@ -654,30 +663,30 @@ public: virtual ~ReadyListener() = default; /** Fires when an item is ready */ - virtual void on_ready(int32_t code, const std::string& msg) = 0; + virtual void on_ready(int32_t code, std::string_view msg) = 0; /** Receives the item itself and says whether to keep listening */ virtual bool on_item(const Item& item, Gadget gadget) = 0; }; namespace detail { -/** Vtable trampolines adapting a `ReadyListener` implementation to `weaveffi_kitchen_ReadyListener_vtable`. */ +/** Vtable trampolines adapting a `ReadyListener` implementation to `kitchen_sink_kitchen_ReadyListener_vtable`. */ struct ReadyListener_trampolines { - static void on_ready(void* ctx, int32_t code, const char* msg, weaveffi_error* out_err) { + static void on_ready(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err) { try { ReadyListener& impl = **static_cast*>(ctx); - std::string msg_val(msg ? msg : ""); + std::string_view msg_val = detail::borrow_string(msg_ptr, msg_len); impl.on_ready(code, msg_val); return; } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + kitchen_sink_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "ReadyListener::on_ready threw a non-standard exception"); + kitchen_sink_error_set(out_err, -4, "ReadyListener::on_ready threw a non-standard exception"); } } - static bool on_item(void* ctx, const uint8_t* item_ptr, size_t item_len, weaveffi_kitchen_Gadget* gadget, weaveffi_error* out_err) { - Gadget gadget_val(gadget); + static bool on_item(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err) { + Gadget gadget_val(adopt, gadget); try { ReadyListener& impl = **static_cast*>(ctx); detail::BufferReader item_r(item_ptr, item_len); @@ -685,9 +694,9 @@ struct ReadyListener_trampolines { item_r.expect_end(); return impl.on_item(item_val, std::move(gadget_val)); } catch (const std::exception& e) { - weaveffi_error_set(out_err, -4, e.what()); + kitchen_sink_error_set(out_err, -4, e.what()); } catch (...) { - weaveffi_error_set(out_err, -4, "ReadyListener::on_item threw a non-standard exception"); + kitchen_sink_error_set(out_err, -4, "ReadyListener::on_item threw a non-standard exception"); } return bool{}; } @@ -699,8 +708,8 @@ struct ReadyListener_trampolines { }; /** The process-wide vtable every `ReadyListener` implementation is passed with. */ -inline const weaveffi_kitchen_ReadyListener_vtable& ReadyListener_vtable() { - static const weaveffi_kitchen_ReadyListener_vtable vtable = { +inline const kitchen_sink_kitchen_ReadyListener_vtable& ReadyListener_vtable() { + static const kitchen_sink_kitchen_ReadyListener_vtable vtable = { &ReadyListener_trampolines::on_ready, &ReadyListener_trampolines::on_item, &ReadyListener_trampolines::free_ctx, @@ -711,47 +720,47 @@ inline const weaveffi_kitchen_ReadyListener_vtable& ReadyListener_vtable() { } // namespace detail inline Gadget::Gadget(int64_t id) : handle_(nullptr) { - weaveffi_error err{}; - auto result = weaveffi_kitchen_Gadget_new(id, &err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_Gadget_new(id, &err); detail::check(err); handle_ = result; } inline std::string Gadget::describe() const { - weaveffi_error err{}; - auto result = weaveffi_kitchen_Gadget_describe(handle_, &err); + size_t out_len = 0; + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_Gadget_describe(handle_, &out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } inline int32_t Gadget::poke(int32_t times) const { - weaveffi_error err{}; - auto result = weaveffi_kitchen_Gadget_poke(handle_, times, &err); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_Gadget_poke(handle_, times, &err); detail::check_kitchen(err); return result; } inline std::string Gadget::version() { - weaveffi_error err{}; - auto result = weaveffi_kitchen_Gadget_version(&err); + check_library(); + size_t out_len = 0; + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_Gadget_version(&out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } namespace shared { /** Trivial cross-module helper */ inline std::string ping() { - weaveffi_error err{}; - auto result = weaveffi_shared_ping(&err); + check_library(); + size_t out_len = 0; + kitchen_sink_error err{}; + auto result = kitchen_sink_shared_ping(&out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } } // namespace shared @@ -760,92 +769,99 @@ namespace kitchen { /** Identity for bool */ inline bool bool_id(bool x) { - weaveffi_error err{}; - auto result = weaveffi_kitchen_bool_id(x, &err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_bool_id(x, &err); detail::check(err); return result; } inline int32_t i32_id(int32_t x) { - weaveffi_error err{}; - auto result = weaveffi_kitchen_i32_id(x, &err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_i32_id(x, &err); detail::check(err); return result; } inline uint32_t u32_id(uint32_t x) { - weaveffi_error err{}; - auto result = weaveffi_kitchen_u32_id(x, &err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_u32_id(x, &err); detail::check(err); return result; } inline int64_t i64_id(int64_t x) { - weaveffi_error err{}; - auto result = weaveffi_kitchen_i64_id(x, &err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_i64_id(x, &err); detail::check(err); return result; } inline double f64_id(double x) { - weaveffi_error err{}; - auto result = weaveffi_kitchen_f64_id(x, &err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_f64_id(x, &err); detail::check(err); return result; } -inline std::string echo_string(const std::string& s) { - weaveffi_error err{}; - auto result = weaveffi_kitchen_echo_string(s.c_str(), &err); +inline std::string echo_string(std::string_view s) { + check_library(); + size_t out_len = 0; + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_echo_string(detail::utf8(s), s.size(), &out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } inline std::vector echo_bytes(const std::vector& b) { + check_library(); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_kitchen_echo_bytes(b.data(), b.size(), &out_len, &err); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_echo_bytes(b.data(), b.size(), &out_len, &err); detail::check(err); - std::vector ret(result, result + out_len); - weaveffi_free_bytes(const_cast(result), out_len); - return ret; + return detail::take_bytes(result, out_len); } /** Register a listener, returning how many are attached */ inline int32_t subscribe(std::shared_ptr listener) { + check_library(); if (!listener) throw std::invalid_argument("listener: null callback interface"); - auto* listener_ctx = new std::shared_ptr(std::move(listener)); - weaveffi_error err{}; - auto result = weaveffi_kitchen_subscribe(static_cast(listener_ctx), &detail::ReadyListener_vtable(), &err); + auto listener_ctx = std::make_unique>(std::move(listener)); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_subscribe(static_cast(listener_ctx.release()), &detail::ReadyListener_vtable(), &err); detail::check(err); return result; } /** Optional object in both directions */ inline std::optional maybe_gadget(const std::optional& fallback) { - weaveffi_error err{}; - auto result = weaveffi_kitchen_maybe_gadget(fallback.has_value() ? fallback->handle() : nullptr, &err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_maybe_gadget(fallback.has_value() ? fallback->handle() : nullptr, &err); detail::check(err); if (!result) return std::nullopt; - return Gadget(result); + return Gadget(adopt, result); } /** A list of objects */ inline std::vector gadgets() { + check_library(); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_kitchen_gadgets(&out_len, &err); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_gadgets(&out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + 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); + size_t ret_n = result_r.read_count(); + ret.reserve(result_r.reserve_hint(ret_n)); for (size_t ret_i = 0; ret_i < ret_n; ++ret_i) { - Gadget ret_item = Gadget(reinterpret_cast(static_cast(result_r.read_u64()))); + Gadget ret_item = Gadget(adopt, reinterpret_cast(static_cast(result_r.read_u64()))); ret.push_back(std::move(ret_item)); } } @@ -859,14 +875,14 @@ inline std::vector gadgets() { * 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 StreamGadgetsIterator { - weaveffi_kitchen_StreamGadgetsIterator* handle_; + kitchen_sink_kitchen_StreamGadgetsIterator* handle_; public: - /** Adopts ownership of the raw producer iterator. */ - explicit StreamGadgetsIterator(weaveffi_kitchen_StreamGadgetsIterator* h) : handle_(h) {} + /** Adopts ownership of a raw producer iterator. */ + explicit StreamGadgetsIterator(adopt_t, kitchen_sink_kitchen_StreamGadgetsIterator* h) noexcept : handle_(h) {} ~StreamGadgetsIterator() { - if (handle_) weaveffi_kitchen_StreamGadgetsIterator_destroy(handle_); + if (handle_) kitchen_sink_kitchen_StreamGadgetsIterator_destroy(handle_); } StreamGadgetsIterator(const StreamGadgetsIterator&) = delete; @@ -878,30 +894,30 @@ public: StreamGadgetsIterator& operator=(StreamGadgetsIterator&& other) noexcept { if (this != &other) { - if (handle_) weaveffi_kitchen_StreamGadgetsIterator_destroy(handle_); + if (handle_) kitchen_sink_kitchen_StreamGadgetsIterator_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. */ + /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic Error after releasing the iterator. */ std::optional next() { if (!handle_) return std::nullopt; - weaveffi_error err{}; - weaveffi_kitchen_Gadget* item{}; - int32_t has_item = weaveffi_kitchen_StreamGadgetsIterator_next(handle_, &item, &err); + kitchen_sink_error err{}; + kitchen_sink_kitchen_Gadget* item{}; + int32_t has_item = kitchen_sink_kitchen_StreamGadgetsIterator_next(handle_, &item, &err); if (err.code != 0) { - weaveffi_kitchen_StreamGadgetsIterator_destroy(handle_); + kitchen_sink_kitchen_StreamGadgetsIterator_destroy(handle_); handle_ = nullptr; detail::check(err); } if (has_item == 0) { - weaveffi_kitchen_StreamGadgetsIterator_destroy(handle_); + kitchen_sink_kitchen_StreamGadgetsIterator_destroy(handle_); handle_ = nullptr; return std::nullopt; } - return Gadget(item); + return Gadget(adopt, item); } /** Sentinel type marking the end of the range. */ @@ -942,19 +958,21 @@ public: * @return A lazy `StreamGadgetsIterator` range that streams one element per iteration step and releases the producer iterator when exhausted or destroyed. */ inline StreamGadgetsIterator stream_gadgets() { - weaveffi_error err{}; - weaveffi_kitchen_StreamGadgetsIterator* iter = weaveffi_kitchen_stream_gadgets(&err); + check_library(); + kitchen_sink_error err{}; + kitchen_sink_kitchen_StreamGadgetsIterator* iter = kitchen_sink_kitchen_stream_gadgets(&err); detail::check(err); - return StreamGadgetsIterator(iter); + return StreamGadgetsIterator(adopt, iter); } /** Optional struct return */ inline std::optional maybe_item(int64_t id) { + check_library(); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_kitchen_maybe_item(id, &out_len, &err); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_maybe_item(id, &out_len, &err); detail::check_kitchen(err); - detail::BufferGuard result_guard{result, out_len}; + detail::BufferGuard result_guard(result, out_len); detail::BufferReader result_r(result, out_len); std::optional ret{}; if (result_r.read_option_flag()) { @@ -967,16 +985,17 @@ inline std::optional maybe_item(int64_t id) { /** List of structs */ inline std::vector list_items() { + check_library(); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_kitchen_list_items(&out_len, &err); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_list_items(&out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + 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); + size_t ret_n = result_r.read_count(); + ret.reserve(result_r.reserve_hint(ret_n)); for (size_t ret_i = 0; ret_i < ret_n; ++ret_i) { Item ret_item = detail::read_Item(result_r); ret.push_back(std::move(ret_item)); @@ -988,15 +1007,16 @@ inline std::vector list_items() { /** Map return type */ inline std::unordered_map get_attrs() { + check_library(); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_kitchen_get_attrs(&out_len, &err); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_get_attrs(&out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + detail::BufferGuard result_guard(result, out_len); detail::BufferReader result_r(result, out_len); std::unordered_map ret{}; { - size_t ret_n = result_r.read_len(); + size_t ret_n = result_r.read_count(); for (size_t ret_i = 0; ret_i < ret_n; ++ret_i) { std::string ret_key = result_r.read_string(); int32_t ret_val = result_r.read_i32(); @@ -1013,14 +1033,14 @@ inline std::unordered_map get_attrs() { * 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 StreamItemsIterator { - weaveffi_kitchen_StreamItemsIterator* handle_; + kitchen_sink_kitchen_StreamItemsIterator* handle_; public: - /** Adopts ownership of the raw producer iterator. */ - explicit StreamItemsIterator(weaveffi_kitchen_StreamItemsIterator* h) : handle_(h) {} + /** Adopts ownership of a raw producer iterator. */ + explicit StreamItemsIterator(adopt_t, kitchen_sink_kitchen_StreamItemsIterator* h) noexcept : handle_(h) {} ~StreamItemsIterator() { - if (handle_) weaveffi_kitchen_StreamItemsIterator_destroy(handle_); + if (handle_) kitchen_sink_kitchen_StreamItemsIterator_destroy(handle_); } StreamItemsIterator(const StreamItemsIterator&) = delete; @@ -1032,31 +1052,31 @@ public: StreamItemsIterator& operator=(StreamItemsIterator&& other) noexcept { if (this != &other) { - if (handle_) weaveffi_kitchen_StreamItemsIterator_destroy(handle_); + if (handle_) kitchen_sink_kitchen_StreamItemsIterator_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. */ + /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic Error after releasing the iterator. */ std::optional next() { if (!handle_) return std::nullopt; - weaveffi_error err{}; + kitchen_sink_error err{}; const uint8_t* item{}; size_t item_len = 0; - int32_t has_item = weaveffi_kitchen_StreamItemsIterator_next(handle_, &item, &item_len, &err); + int32_t has_item = kitchen_sink_kitchen_StreamItemsIterator_next(handle_, &item, &item_len, &err); if (err.code != 0) { - weaveffi_kitchen_StreamItemsIterator_destroy(handle_); + kitchen_sink_kitchen_StreamItemsIterator_destroy(handle_); handle_ = nullptr; detail::check(err); } if (has_item == 0) { - weaveffi_kitchen_StreamItemsIterator_destroy(handle_); + kitchen_sink_kitchen_StreamItemsIterator_destroy(handle_); handle_ = nullptr; return std::nullopt; } - detail::BufferGuard item_guard{item, item_len}; + detail::BufferGuard item_guard(item, item_len); detail::BufferReader item_r(item, item_len); Item value = detail::read_Item(item_r); item_r.expect_end(); @@ -1101,19 +1121,21 @@ public: * @return A lazy `StreamItemsIterator` range that streams one element per iteration step and releases the producer iterator when exhausted or destroyed. */ inline StreamItemsIterator stream_items() { - weaveffi_error err{}; - weaveffi_kitchen_StreamItemsIterator* iter = weaveffi_kitchen_stream_items(&err); + check_library(); + kitchen_sink_error err{}; + kitchen_sink_kitchen_StreamItemsIterator* iter = kitchen_sink_kitchen_stream_items(&err); detail::check(err); - return StreamItemsIterator(iter); + return StreamItemsIterator(adopt, iter); } /** Returns the shared Token type from another module */ inline Token cross_module_token() { + check_library(); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_kitchen_cross_module_token(&out_len, &err); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_cross_module_token(&out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + detail::BufferGuard result_guard(result, out_len); detail::BufferReader result_r(result, out_len); Token ret = detail::read_Token(result_r); result_r.expect_end(); @@ -1121,66 +1143,64 @@ inline Token cross_module_token() { } /** Async operation */ -inline std::future do_async(const std::string& input) { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_kitchen_do_async_async(input.c_str(), [](void* context, weaveffi_error* err, const char* result) { - auto* p = static_cast*>(context); +inline std::future do_async(std::string_view input) { + check_library(); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + kitchen_sink_kitchen_do_async(detail::utf8(input), input.size(), [](void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { + std::unique_ptr> p(static_cast*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_error(err->code, msg)); } else { - std::string value(result ? result : ""); - weaveffi_free_string(result); - p->set_value(std::move(value)); + p->set_value(detail::take_string(result_ptr, result_len)); } } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + kitchen_sink_error_free(err); + }, static_cast(promise.release())); return future; } /** Cancellable async operation */ -inline std::future do_cancellable(const std::string& input, weaveffi_cancel_token* cancel_token) { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_kitchen_do_cancellable_async(input.c_str(), cancel_token, [](void* context, weaveffi_error* err, const char* result) { - auto* p = static_cast*>(context); +inline std::future do_cancellable(std::string_view input, const CancelToken& cancel_token = CancelToken::none()) { + check_library(); + auto promise = std::make_unique>(); + auto future = promise->get_future(); + kitchen_sink_kitchen_do_cancellable(detail::utf8(input), input.size(), cancel_token.handle(), [](void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { + std::unique_ptr> p(static_cast*>(context)); try { - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); + if (err != nullptr && err->code != 0) { + std::string msg = detail::error_message(*err); p->set_exception(detail::make_error(err->code, msg)); } else { - std::string value(result ? result : ""); - weaveffi_free_string(result); - p->set_value(std::move(value)); + p->set_value(detail::take_string(result_ptr, result_len)); } } catch (...) { p->set_exception(std::current_exception()); } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); + kitchen_sink_error_free(err); + }, static_cast(promise.release())); return future; } /** Legacy operation kept for compatibility */ [[deprecated("Use new_op instead")]] inline int32_t legacy_op() { - weaveffi_error err{}; - auto result = weaveffi_kitchen_legacy_op(&err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_legacy_op(&err); detail::check(err); return result; } /** Replacement for legacy_op */ inline int32_t new_op() { - weaveffi_error err{}; - auto result = weaveffi_kitchen_new_op(&err); + check_library(); + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_new_op(&err); detail::check(err); return result; } @@ -1191,16 +1211,16 @@ namespace kitchen::nested { /** Trivial nested-module function */ inline std::string hello() { - weaveffi_error err{}; - auto result = weaveffi_kitchen_nested_hello(&err); + check_library(); + size_t out_len = 0; + kitchen_sink_error err{}; + auto result = kitchen_sink_kitchen_nested_hello(&out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } } // namespace kitchen::nested -} // namespace weaveffi +} // namespace kitchen_sink -// END weaveffi.hpp +// END kitchen_sink.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__CMakeLists_txt.snap index f169a251..c1ee2d9a 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__CMakeLists_txt.snap @@ -1,15 +1,26 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.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) +project(nested_modules_cpp VERSION 0.1.0 LANGUAGES CXX) + +# The producer library to link: a full path from the NESTED_MODULES_LIBRARY cache or +# environment variable, or else the library name `nested_modules` (a CMake target +# of that name, or libnested_modules on the linker search path). +set(NESTED_MODULES_LIBRARY "$ENV{NESTED_MODULES_LIBRARY}" CACHE FILEPATH "Path to the nested_modules native library") + +add_library(nested_modules_cpp INTERFACE) +add_library(nested_modules::cpp ALIAS nested_modules_cpp) +target_include_directories(nested_modules_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_features(nested_modules_cpp INTERFACE cxx_std_17) +if(NESTED_MODULES_LIBRARY) + target_link_libraries(nested_modules_cpp INTERFACE "${NESTED_MODULES_LIBRARY}") +else() + target_link_libraries(nested_modules_cpp INTERFACE nested_modules) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__README_md.snap index da3ea6c6..2e417184 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__README_md.snap @@ -1,39 +1,43 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI C++ Bindings +# nested_modules C++ bindings -## Prerequisites +A header-only C++17 wrapper over the `nested_modules` C ABI. `nested_modules.hpp` +includes the C header beside it; every declaration lives in +`namespace nested_modules`. -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate +## Prerequisites -## Usage with CMake +- CMake 3.14 or later +- A C++17 compiler +- The `nested_modules` native library (`libnested_modules.so`, `libnested_modules.dylib`, or + `nested_modules.dll`) -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: +## Use with CMake ```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) +add_subdirectory(path/to/cpp) +add_executable(app main.cpp) +target_link_libraries(app PRIVATE nested_modules::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: +`nested_modules::cpp` adds this directory to the include path, requires +C++17, and links the native library: the file named by the `NESTED_MODULES_LIBRARY` +CMake cache variable or environment variable when set, or else +`nested_modules` by name. ```cpp -#include "weaveffi.hpp" +#include "nested_modules.hpp" ``` +The first free function, constructor, or static member you call checks +that the linked library matches these headers and throws +`nested_modules::LoadError` if it doesn't; call `nested_modules::check_library()` +at startup to check eagerly. + diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_h.snap b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_h.snap new file mode 100644 index 00000000..27c97f2b --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_h.snap @@ -0,0 +1,144 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NESTED_MODULES_H +#define NESTED_MODULES_H + +#include +#include +#include + +#ifndef NESTED_MODULES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef NESTED_MODULES_BUILD +# define NESTED_MODULES_API __declspec(dllexport) +# else +# define NESTED_MODULES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define NESTED_MODULES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define NESTED_MODULES_API __attribute__((visibility("default"))) +# else +# define NESTED_MODULES_API +# endif +#endif + +#ifndef NESTED_MODULES_DEPRECATED +# if defined(_MSC_VER) +# define NESTED_MODULES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define NESTED_MODULES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define NESTED_MODULES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports nested_modules_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define NESTED_MODULES_ABI_VERSION 3u +NESTED_MODULES_API uint32_t nested_modules_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by nested_modules_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct nested_modules_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} nested_modules_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +NESTED_MODULES_API void nested_modules_error_set(nested_modules_error* err, int32_t code, const char* message); +NESTED_MODULES_API void nested_modules_error_clear(nested_modules_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + nested_modules_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +NESTED_MODULES_API void nested_modules_error_free(nested_modules_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +NESTED_MODULES_API void nested_modules_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct nested_modules_cancel_token nested_modules_cancel_token; +NESTED_MODULES_API nested_modules_cancel_token* nested_modules_cancel_token_create(void); +NESTED_MODULES_API void nested_modules_cancel_token_cancel(nested_modules_cancel_token* token); +NESTED_MODULES_API bool nested_modules_cancel_token_is_cancelled(const nested_modules_cancel_token* token); +NESTED_MODULES_API void nested_modules_cancel_token_destroy(nested_modules_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +NESTED_MODULES_API uint8_t* nested_modules_alloc(uint32_t size); +NESTED_MODULES_API void nested_modules_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +NESTED_MODULES_API uint64_t nested_modules_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define NESTED_MODULES_GRAPHICS_CHECKSUM 0x44d31b5b10fdb90dull +NESTED_MODULES_API uint64_t nested_modules_graphics_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with nested_modules_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * nested_modules_buffer.h declares a C struct and codec for each. + */ + +/** A measurement unit shared with nested submodules. */ +typedef enum { nested_modules_graphics_Unit_pixels = 0, nested_modules_graphics_Unit_points = 1 } nested_modules_graphics_Unit; +/** Error codes reported by throwing functions in the `graphics` module tree. */ +typedef enum { nested_modules_graphics_GraphicsError_OutOfBounds = 1 } nested_modules_graphics_GraphicsError; + +// Module: graphics +/** Construct a Canvas. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_new_canvas(int32_t width, size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes +/** Construct a Shape. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_make_shape(int32_t sides, size_t* out_len, nested_modules_error* out_err); +/** Nested function returning a struct owned by an ancestor module. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_describe(size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes_geometry +/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, nested_modules_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // NESTED_MODULES_H + +// END nested_modules.h diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_hpp.snap new file mode 100644 index 00000000..1cd1bbd7 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__nested_modules_hpp.snap @@ -0,0 +1,595 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#pragma once + +#include "nested_modules.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nested_modules { + +/** + * Base exception for every error reported through the C ABI. + * + * Positive codes are the declared error codes (see the typed domain + * exceptions); negative codes are runtime traps: -1 a generic producer + * error, -2 a producer panic, -3 a marshalling failure, -4 a + * callback-interface implementation that threw, and -5 a cancelled call + * (thrown as Cancelled). + */ +class Error : public std::runtime_error { + int32_t code_; + +public: + /** Builds an error carrying `code` and `message`. */ + Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} + + /** The numeric error code. */ + int32_t code() const noexcept { return code_; } +}; + +/** Thrown by an async call whose CancelToken was cancelled (code -5). */ +class Cancelled : public Error { +public: + /** Builds a cancellation error carrying the producer's message. */ + explicit Cancelled(const std::string& message) : Error(-5, message) {} +}; + +/** + * Thrown on first use when the linked nested_modules library was built for a + * different C ABI revision or a different API contract than this header. + */ +class LoadError : public Error { +public: + /** Builds a load error describing the mismatch. */ + explicit LoadError(const std::string& message) : Error(-1, message) {} +}; + +/** + * Tag selecting the constructor that adopts a raw C pointer (one strong + * reference the wrapper then owns), in the style of std::adopt_lock. + */ +struct adopt_t { + explicit adopt_t() = default; +}; + +/** The adopt_t tag value: `Gadget g(adopt, raw)`. */ +inline constexpr adopt_t adopt{}; + +/** + * A cancellation token for cancellable async functions. + * + * Create one, pass it to any number of cancellable calls, and call cancel() + * from any thread. A cancelled call settles its future with Cancelled. Each + * call takes its own reference to the native token, so the token can be + * destroyed at any time; the destructor releases this wrapper's reference. + */ +class CancelToken { + nested_modules_cancel_token* handle_; + + struct none_t {}; + explicit CancelToken(none_t) noexcept : handle_(nullptr) {} + +public: + /** Creates a new, uncancelled token. */ + CancelToken() : handle_(nested_modules_cancel_token_create()) { + if (handle_ == nullptr) throw std::bad_alloc(); + } + + /** Releases this wrapper's reference to the native token. */ + ~CancelToken() { + if (handle_ != nullptr) nested_modules_cancel_token_destroy(handle_); + } + + CancelToken(const CancelToken&) = delete; + CancelToken& operator=(const CancelToken&) = delete; + + /** Transfers the native token; `other` becomes empty. */ + CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + + /** Releases the current token and takes over `other`'s. */ + CancelToken& operator=(CancelToken&& other) noexcept { + if (this != &other) { + if (handle_ != nullptr) nested_modules_cancel_token_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /** The empty token a cancellable call uses when none is passed. */ + static const CancelToken& none() noexcept { + static const CancelToken token{none_t{}}; + return token; + } + + /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ + void cancel() const noexcept { + if (handle_ != nullptr) nested_modules_cancel_token_cancel(handle_); + } + + /** Whether cancel() has been called. */ + bool is_cancelled() const noexcept { + return handle_ != nullptr && nested_modules_cancel_token_is_cancelled(handle_); + } + + /** The native token, borrowed (null for an empty token). */ + nested_modules_cancel_token* handle() const noexcept { return handle_; } +}; + +namespace detail { + +/** The exception for a runtime (non-domain) error code. */ +inline std::exception_ptr make_error(int32_t code, const std::string& message) { + if (code == -5) return std::make_exception_ptr(Cancelled(message)); + return std::make_exception_ptr(Error(code, message)); +} + +/** The message of a nonzero error slot. */ +inline std::string error_message(const nested_modules_error& err) { + return std::string(err.message != nullptr ? err.message : "unknown error"); +} + +/** Throws the generic Error (or Cancelled) if `err` carries a nonzero code. */ +inline void check(nested_modules_error& err) { + if (err.code == 0) return; + std::exception_ptr ex = make_error(err.code, error_message(err)); + nested_modules_error_clear(&err); + std::rethrow_exception(ex); +} + +/** Copies a producer-returned UTF-8 run into a string, then releases it. */ +inline std::string take_string(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::string(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { nested_modules_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::string(reinterpret_cast(ptr), len); +} + +/** Copies a producer-returned byte run into a vector, then releases it. */ +inline std::vector take_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::vector(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { nested_modules_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::vector(ptr, ptr + len); +} + +/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ +inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { + if (ptr == nullptr || len == 0) return std::string_view(); + return std::string_view(reinterpret_cast(ptr), len); +} + +/** Views a borrowed byte run (null when empty) as an owned vector. */ +inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr || len == 0) return std::vector(); + return std::vector(ptr, ptr + len); +} + +/** The data pointer of a string argument, as the C ABI expects it. */ +inline const uint8_t* utf8(std::string_view s) noexcept { + return reinterpret_cast(s.data()); +} + +} // namespace detail + +/** + * Verifies that the linked nested_modules library implements the C ABI revision + * and the module contracts this header was generated against, throwing + * LoadError naming the first mismatch. The check runs once; every free + * function, constructor, and static member calls this before its first + * native call, so calling it at startup is optional. + */ +inline void check_library() { + static const std::string failure = [] { + uint32_t abi = nested_modules_abi_version(); + if (abi != NESTED_MODULES_ABI_VERSION) { + return "nested_modules: C ABI revision mismatch (header " + std::to_string(NESTED_MODULES_ABI_VERSION) + + ", library " + std::to_string(abi) + ")"; + } + if (nested_modules_graphics_checksum() != 0x44d31b5b10fdb90dull) { + return std::string("nested_modules: module 'graphics' does not match the linked library (contract checksum mismatch); regenerate the bindings for this build"); + } + return std::string(); + }(); + if (!failure.empty()) throw LoadError(failure); +} + +namespace detail { + +/** + * Serializes values into the value-buffer wire format: little-endian, packed + * with no alignment, lengths and element counts as u32. + */ +class BufferWriter { + std::vector buf_; + + template + void append_le(T v) { + for (size_t i = 0; i < sizeof(T); ++i) { + buf_.push_back(static_cast(v >> (8 * i))); + } + } + +public: + /** Pointer to the encoded bytes. */ + const uint8_t* data() const noexcept { return buf_.data(); } + + /** Number of encoded bytes. */ + size_t size() const noexcept { return buf_.size(); } + + void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } + void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } + void write_u8(uint8_t v) { buf_.push_back(v); } + void write_i16(int16_t v) { append_le(static_cast(v)); } + void write_u16(uint16_t v) { append_le(v); } + void write_i32(int32_t v) { append_le(static_cast(v)); } + void write_u32(uint32_t v) { append_le(v); } + void write_i64(int64_t v) { append_le(static_cast(v)); } + void write_u64(uint64_t v) { append_le(v); } + + void write_f32(float v) { + uint32_t bits = 0; + std::memcpy(&bits, &v, sizeof(bits)); + append_le(bits); + } + + void write_f64(double v) { + uint64_t bits = 0; + std::memcpy(&bits, &v, sizeof(bits)); + append_le(bits); + } + + /** Writes a byte length or an element count as a u32. */ + void write_len(size_t n) { + if (static_cast(n) > UINT32_MAX) throw Error(-3, "value buffer length exceeds u32"); + append_le(static_cast(n)); + } + + void write_string(std::string_view v) { + write_len(v.size()); + buf_.insert(buf_.end(), v.begin(), v.end()); + } + + void write_bytes(const std::vector& v) { + write_len(v.size()); + buf_.insert(buf_.end(), v.begin(), v.end()); + } + + /** Writes an optional's presence flag: 0 absent, 1 present. */ + void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } +}; + +/** + * Decodes values from the value-buffer wire format. A malformed buffer is a + * producer/consumer contract violation (both sides are generated from one + * API), so every decode failure throws the generic Error with the + * marshalling code -3. Object tokens adopted before a failure live in RAII + * wrappers that release them on unwind. + */ +class BufferReader { + const uint8_t* data_; + size_t len_; + size_t pos_; + + [[noreturn]] static void fail(const char* what) { + throw Error(-3, std::string("malformed 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) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} + + /** Bytes not yet consumed. */ + size_t remaining() const noexcept { return len_ - pos_; } + + bool read_bool() { + uint8_t b = read_le("bool"); + if (b > 1) fail("bool byte out of range"); + return b != 0; + } + + int8_t read_i8() { return read_le("i8"); } + uint8_t read_u8() { return read_le("u8"); } + int16_t read_i16() { return read_le("i16"); } + uint16_t read_u16() { return read_le("u16"); } + int32_t read_i32() { return read_le("i32"); } + uint32_t read_u32() { return read_le("u32"); } + int64_t read_i64() { return read_le("i64"); } + uint64_t read_u64() { return read_le("u64"); } + + float read_f32() { + uint32_t bits = read_le("f32"); + float v = 0; + std::memcpy(&v, &bits, sizeof(v)); + return v; + } + + double read_f64() { + uint64_t bits = read_le("f64"); + double v = 0; + std::memcpy(&v, &bits, sizeof(v)); + return v; + } + + /** Reads a byte length, rejecting one larger than the bytes remaining. */ + size_t read_len() { + uint32_t n = read_le("length"); + if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); + return static_cast(n); + } + + /** + * Reads an element count. Elements may encode to zero bytes, so a count + * larger than the bytes remaining is legal; callers cap preallocation + * with reserve_hint(). + */ + size_t read_count() { return static_cast(read_le("count")); } + + /** A preallocation size for `n` elements that a malformed count can't inflate. */ + size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } + + std::string read_string() { + size_t n = read_len(); + std::string v(reinterpret_cast(data_) + pos_, n); + pos_ += n; + return v; + } + + std::vector read_bytes() { + size_t n = read_len(); + std::vector v(data_ + pos_, data_ + pos_ + n); + pos_ += n; + return v; + } + + bool read_option_flag() { + uint8_t b = read_le("option flag"); + if (b > 1) fail("option flag byte out of range"); + return b != 0; + } + + /** Rejects unconsumed bytes after decoding a complete value. */ + void expect_end() const { + if (pos_ != len_) fail("trailing bytes after value"); + } +}; + +/** Releases a producer-allocated buffer with nested_modules_free_bytes on scope exit. */ +struct BufferGuard { + /** The producer-allocated buffer, or null when the call reported an error. */ + const uint8_t* ptr; + /** The buffer length in bytes. */ + size_t len; + + BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} + BufferGuard(const BufferGuard&) = delete; + BufferGuard& operator=(const BufferGuard&) = delete; + + ~BufferGuard() { + if (ptr != nullptr) nested_modules_free_bytes(const_cast(ptr), len); + } +}; + +} // namespace detail + +/** A measurement unit shared with nested submodules. */ +enum class Unit : int32_t { + pixels = 0, + points = 1 +}; + +struct Canvas; +struct Shape; +struct Detail; + +/** A struct two levels deep. */ +struct Detail { + double area; +}; + +/** A struct one level deep that references both an ancestor enum and a deeper struct. */ +struct Shape { + int32_t sides; + /** Reference up to the parent module's enum */ + Unit unit; + /** Reference down into a deeper module */ + Detail detail; +}; + +/** Top-level struct that references a struct defined one level deeper. */ +struct Canvas { + int32_t width; + /** Reference down into a nested module */ + Shape root; +}; + +namespace detail { + +/** Encodes a `Detail` in the value-buffer wire format. */ +inline void write_Detail(BufferWriter& w, const Detail& v) { + w.write_f64(v.area); +} + +/** Decodes a `Detail` from the value-buffer wire format. */ +inline Detail read_Detail(BufferReader& r) { + double f_area = r.read_f64(); + return Detail{std::move(f_area)}; +} + +/** Encodes a `Shape` in the value-buffer wire format. */ +inline void write_Shape(BufferWriter& w, const Shape& v) { + w.write_i32(v.sides); + w.write_i32(static_cast(v.unit)); + detail::write_Detail(w, v.detail); +} + +/** Decodes a `Shape` from the value-buffer wire format. */ +inline Shape read_Shape(BufferReader& r) { + int32_t f_sides = r.read_i32(); + Unit f_unit = static_cast(r.read_i32()); + Detail f_detail = detail::read_Detail(r); + return Shape{std::move(f_sides), std::move(f_unit), std::move(f_detail)}; +} + +/** Encodes a `Canvas` in the value-buffer wire format. */ +inline void write_Canvas(BufferWriter& w, const Canvas& v) { + w.write_i32(v.width); + detail::write_Shape(w, v.root); +} + +/** Decodes a `Canvas` from the value-buffer wire format. */ +inline Canvas read_Canvas(BufferReader& r) { + int32_t f_width = r.read_i32(); + Shape f_root = detail::read_Shape(r); + return Canvas{std::move(f_width), std::move(f_root)}; +} + +} // namespace detail + +/** Typed errors reported by the `graphics` module's throwing functions. */ +class GraphicsError : public Error { +public: + GraphicsError(int32_t code, const std::string& msg) : Error(code, msg) {} +}; + +/** coordinate out of bounds */ +class OutOfBoundsError : public GraphicsError { +public: + explicit OutOfBoundsError(const std::string& msg) : GraphicsError(1, msg) {} +}; + +namespace detail { + +/** Map a `graphics` error code and payload to its typed exception (the runtime exception for negative codes, GraphicsError for unknown codes). */ +inline std::exception_ptr make_graphics_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { + if (code < 0) return make_error(code, msg); + (void)payload_ptr; + (void)payload_len; + switch (code) { + case 1: return std::make_exception_ptr(OutOfBoundsError(msg)); + default: return std::make_exception_ptr(GraphicsError(code, msg)); + } +} + +/** Throw the typed `graphics` exception if `err` carries a nonzero code. */ +inline void check_graphics(nested_modules_error& err) { + if (err.code == 0) return; + std::string msg = error_message(err); + std::exception_ptr ex = make_graphics_error(err.code, msg, err.payload_ptr, err.payload_len); + nested_modules_error_clear(&err); + std::rethrow_exception(ex); +} + +} // namespace detail + +namespace graphics { + +/** Construct a Canvas. */ +inline Canvas new_canvas(int32_t width) { + check_library(); + size_t out_len = 0; + nested_modules_error err{}; + auto result = nested_modules_graphics_new_canvas(width, &out_len, &err); + detail::check(err); + detail::BufferGuard result_guard(result, out_len); + detail::BufferReader result_r(result, out_len); + Canvas ret = detail::read_Canvas(result_r); + result_r.expect_end(); + return ret; +} + +} // namespace graphics + +namespace graphics::shapes { + +/** Construct a Shape. */ +inline Shape make_shape(int32_t sides) { + check_library(); + size_t out_len = 0; + nested_modules_error err{}; + auto result = nested_modules_graphics_shapes_make_shape(sides, &out_len, &err); + detail::check(err); + detail::BufferGuard result_guard(result, out_len); + detail::BufferReader result_r(result, out_len); + Shape ret = detail::read_Shape(result_r); + result_r.expect_end(); + return ret; +} + +/** Nested function returning a struct owned by an ancestor module. */ +inline Canvas describe() { + check_library(); + size_t out_len = 0; + nested_modules_error err{}; + auto result = nested_modules_graphics_shapes_describe(&out_len, &err); + detail::check(err); + detail::BufferGuard result_guard(result, out_len); + detail::BufferReader result_r(result, out_len); + Canvas ret = detail::read_Canvas(result_r); + result_r.expect_end(); + return ret; +} + +} // namespace graphics::shapes + +namespace graphics::shapes::geometry { + +/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ +inline Detail compute(const Shape& shape) { + check_library(); + detail::BufferWriter shape_buf; + detail::write_Shape(shape_buf, shape); + size_t out_len = 0; + nested_modules_error err{}; + auto result = nested_modules_graphics_shapes_geometry_compute(shape_buf.data(), shape_buf.size(), &out_len, &err); + detail::check_graphics(err); + detail::BufferGuard result_guard(result, out_len); + detail::BufferReader result_r(result, out_len); + Detail ret = detail::read_Detail(result_r); + result_r.expect_end(); + return ret; +} + +} // namespace graphics::shapes::geometry + +} // namespace nested_modules + +// END nested_modules.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 deleted file mode 100644 index 595b1c70..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap +++ /dev/null @@ -1,514 +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" { - -/* 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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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); - -/** A measurement unit shared with nested submodules. */ -typedef enum { weaveffi_graphics_Unit_pixels = 0, weaveffi_graphics_Unit_points = 1 } weaveffi_graphics_Unit; -/** Error codes reported by throwing functions in the `graphics` module tree. */ -typedef enum { weaveffi_graphics_GraphicsError_OutOfBounds = 1 } weaveffi_graphics_GraphicsError; - -/** Construct a Canvas. */ -WEAVEFFI_API const uint8_t* weaveffi_graphics_new_canvas(int32_t width, size_t* out_len, weaveffi_error* out_err); - -/** Construct a Shape. */ -WEAVEFFI_API const uint8_t* weaveffi_graphics_shapes_make_shape(int32_t sides, size_t* out_len, weaveffi_error* out_err); -/** Nested function returning a struct owned by an ancestor module. */ -WEAVEFFI_API const uint8_t* weaveffi_graphics_shapes_describe(size_t* out_len, weaveffi_error* out_err); - -/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ -WEAVEFFI_API const uint8_t* weaveffi_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, weaveffi_error* out_err); - -} // extern "C" - -namespace weaveffi { - -/** - * Base exception for every error reported through the C ABI. - * - * Positive codes are the module's declared error codes (see the typed - * domain exceptions); negative codes are runtime traps: -1 a generic - * producer error, -2 a producer panic, -3 a marshalling failure, -4 a - * callback-interface implementation that threw. - */ -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_; } -}; - -/** - * Throws WeaveFFIError when the loaded producer library was built for a - * different WeaveFFI ABI revision than this header. Call once at startup. - */ -inline void check_abi_version() { - uint32_t actual = weaveffi_abi_version(); - if (actual != WEAVEFFI_ABI_VERSION) { - throw WeaveFFIError(-1, "WeaveFFI ABI revision mismatch: header " + std::to_string(WEAVEFFI_ABI_VERSION) + ", library " + std::to_string(actual)); - } -} - -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. Object tokens adopted before a - * failure live in RAII wrappers that release them on unwind. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - [[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 - -/** A measurement unit shared with nested submodules. */ -enum class Unit : int32_t { - pixels = 0, - points = 1 -}; - -struct Canvas; -struct Shape; -struct Detail; - -/** A struct two levels deep. */ -struct Detail { - double area; -}; - -/** A struct one level deep that references both an ancestor enum and a deeper struct. */ -struct Shape { - int32_t sides; - /** Reference up to the parent module's enum */ - Unit unit; - /** Reference down into a deeper module */ - Detail detail; -}; - -/** Top-level struct that references a struct defined one level deeper. */ -struct Canvas { - int32_t width; - /** Reference down into a nested module */ - Shape root; -}; - -namespace detail { - -/** Encodes a `Detail` in the WeaveFFI value-buffer format. */ -inline void write_Detail(BufferWriter& w, const Detail& v) { - w.write_f64(v.area); -} - -/** Decodes a `Detail` from the WeaveFFI value-buffer format. */ -inline Detail read_Detail(BufferReader& r) { - double f_area = r.read_f64(); - return Detail{std::move(f_area)}; -} - -/** Encodes a `Shape` in the WeaveFFI value-buffer format. */ -inline void write_Shape(BufferWriter& w, const Shape& v) { - w.write_i32(v.sides); - w.write_i32(static_cast(v.unit)); - detail::write_Detail(w, v.detail); -} - -/** Decodes a `Shape` from the WeaveFFI value-buffer format. */ -inline Shape read_Shape(BufferReader& r) { - int32_t f_sides = r.read_i32(); - Unit f_unit = static_cast(r.read_i32()); - Detail f_detail = detail::read_Detail(r); - return Shape{std::move(f_sides), std::move(f_unit), std::move(f_detail)}; -} - -/** Encodes a `Canvas` in the WeaveFFI value-buffer format. */ -inline void write_Canvas(BufferWriter& w, const Canvas& v) { - w.write_i32(v.width); - detail::write_Shape(w, v.root); -} - -/** Decodes a `Canvas` from the WeaveFFI value-buffer format. */ -inline Canvas read_Canvas(BufferReader& r) { - int32_t f_width = r.read_i32(); - Shape f_root = detail::read_Shape(r); - return Canvas{std::move(f_width), std::move(f_root)}; -} - -} // namespace detail - -/** Typed errors reported by the `graphics` module's throwing functions. */ -class GraphicsError : public WeaveFFIError { -public: - GraphicsError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; - -/** coordinate out of bounds */ -class OutOfBoundsError : public GraphicsError { -public: - OutOfBoundsError(const std::string& msg) : GraphicsError(1, msg) {} -}; - -namespace detail { - -/** Map a `graphics` error code and payload to its typed exception (WeaveFFIError for runtime codes, GraphicsError for unknown codes). */ -inline std::exception_ptr make_graphics_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)); - (void)payload_ptr; - (void)payload_len; - switch (code) { - case 1: return std::make_exception_ptr(OutOfBoundsError(msg)); - default: return std::make_exception_ptr(GraphicsError(code, msg)); - } -} - -/** Throw the typed `graphics` exception if `err` carries a nonzero code. */ -inline void check_graphics(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - std::exception_ptr ex = make_graphics_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -namespace graphics { - -/** Construct a Canvas. */ -inline Canvas new_canvas(int32_t width) { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_graphics_new_canvas(width, &out_len, &err); - detail::check(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Canvas ret = detail::read_Canvas(result_r); - result_r.expect_end(); - return ret; -} - -} // namespace graphics - -namespace graphics::shapes { - -/** Construct a Shape. */ -inline Shape make_shape(int32_t sides) { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_graphics_shapes_make_shape(sides, &out_len, &err); - detail::check(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Shape ret = detail::read_Shape(result_r); - result_r.expect_end(); - return ret; -} - -/** Nested function returning a struct owned by an ancestor module. */ -inline Canvas describe() { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_graphics_shapes_describe(&out_len, &err); - detail::check(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Canvas ret = detail::read_Canvas(result_r); - result_r.expect_end(); - return ret; -} - -} // namespace graphics::shapes - -namespace graphics::shapes::geometry { - -/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ -inline Detail compute(const Shape& shape) { - detail::BufferWriter shape_buf; - detail::write_Shape(shape_buf, shape); - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_graphics_shapes_geometry_compute(shape_buf.data(), shape_buf.size(), &out_len, &err); - detail::check_graphics(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Detail ret = detail::read_Detail(result_r); - result_r.expect_end(); - return ret; -} - -} // namespace graphics::shapes::geometry - -} // namespace weaveffi - -// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__CMakeLists_txt.snap index f169a251..544a78cd 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__CMakeLists_txt.snap @@ -1,15 +1,26 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.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) +project(shapes_cpp VERSION 0.1.0 LANGUAGES CXX) + +# The producer library to link: a full path from the SHAPES_LIBRARY cache or +# environment variable, or else the library name `shapes` (a CMake target +# of that name, or libshapes on the linker search path). +set(SHAPES_LIBRARY "$ENV{SHAPES_LIBRARY}" CACHE FILEPATH "Path to the shapes native library") + +add_library(shapes_cpp INTERFACE) +add_library(shapes::cpp ALIAS shapes_cpp) +target_include_directories(shapes_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_features(shapes_cpp INTERFACE cxx_std_17) +if(SHAPES_LIBRARY) + target_link_libraries(shapes_cpp INTERFACE "${SHAPES_LIBRARY}") +else() + target_link_libraries(shapes_cpp INTERFACE shapes) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__README_md.snap index da3ea6c6..c36d04f3 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__README_md.snap @@ -1,39 +1,43 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI C++ Bindings +# shapes C++ bindings -## Prerequisites +A header-only C++17 wrapper over the `shapes` C ABI. `shapes.hpp` +includes the C header beside it; every declaration lives in +`namespace shapes`. -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate +## Prerequisites -## Usage with CMake +- CMake 3.14 or later +- A C++17 compiler +- The `shapes` native library (`libshapes.so`, `libshapes.dylib`, or + `shapes.dll`) -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: +## Use with CMake ```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) +add_subdirectory(path/to/cpp) +add_executable(app main.cpp) +target_link_libraries(app PRIVATE shapes::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: +`shapes::cpp` adds this directory to the include path, requires +C++17, and links the native library: the file named by the `SHAPES_LIBRARY` +CMake cache variable or environment variable when set, or else +`shapes` by name. ```cpp -#include "weaveffi.hpp" +#include "shapes.hpp" ``` +The first free function, constructor, or static member you call checks +that the linked library matches these headers and throws +`shapes::LoadError` if it doesn't; call `shapes::check_library()` +at startup to check eagerly. + diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_h.snap b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_h.snap new file mode 100644 index 00000000..df20c3f5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_h.snap @@ -0,0 +1,151 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef SHAPES_H +#define SHAPES_H + +#include +#include +#include + +#ifndef SHAPES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef SHAPES_BUILD +# define SHAPES_API __declspec(dllexport) +# else +# define SHAPES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define SHAPES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define SHAPES_API __attribute__((visibility("default"))) +# else +# define SHAPES_API +# endif +#endif + +#ifndef SHAPES_DEPRECATED +# if defined(_MSC_VER) +# define SHAPES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define SHAPES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define SHAPES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports shapes_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define SHAPES_ABI_VERSION 3u +SHAPES_API uint32_t shapes_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by shapes_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct shapes_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} shapes_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +SHAPES_API void shapes_error_set(shapes_error* err, int32_t code, const char* message); +SHAPES_API void shapes_error_clear(shapes_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + shapes_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +SHAPES_API void shapes_error_free(shapes_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +SHAPES_API void shapes_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct shapes_cancel_token shapes_cancel_token; +SHAPES_API shapes_cancel_token* shapes_cancel_token_create(void); +SHAPES_API void shapes_cancel_token_cancel(shapes_cancel_token* token); +SHAPES_API bool shapes_cancel_token_is_cancelled(const shapes_cancel_token* token); +SHAPES_API void shapes_cancel_token_destroy(shapes_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +SHAPES_API uint8_t* shapes_alloc(uint32_t size); +SHAPES_API void shapes_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +SHAPES_API uint64_t shapes_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define SHAPES_SHAPES_CHECKSUM 0x06acf433ffa2a0f0ull +SHAPES_API uint64_t shapes_shapes_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with shapes_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * shapes_buffer.h declares a C struct and codec for each. + */ + +/** An algebraic shape (sum type with associated data) */ +typedef enum { + /** The empty shape (a unit variant) */ + shapes_shapes_Shape_Empty = 0, + /** A circle with a radius */ + shapes_shapes_Shape_Circle = 1, + /** An axis-aligned rectangle */ + shapes_shapes_Shape_Rectangle = 2, + /** An integer-bounded shape (exercises narrow + wide ints) */ + shapes_shapes_Shape_Bounded = 3, + /** A labeled shape with a small count */ + shapes_shapes_Shape_Labeled = 4 +} shapes_shapes_Shape_Tag; +/** A plain C-style enum (no payloads) */ +typedef enum { shapes_shapes_Channel_Red = 0, shapes_shapes_Channel_Green = 1, shapes_shapes_Channel_Blue = 2 } shapes_shapes_Channel; + +// Module: shapes +/** Render a shape to a string */ +SHAPES_API const uint8_t* shapes_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, shapes_error* out_err); +/** Scale a shape by a factor, returning a new shape */ +SHAPES_API const uint8_t* shapes_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, shapes_error* out_err); +/** Sum a list of bytes into a wide integer (numerics smoke) */ +SHAPES_API uint64_t shapes_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, shapes_error* out_err); +/** Build a Numerics struct from every numeric primitive */ +SHAPES_API const uint8_t* shapes_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, shapes_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // SHAPES_H + +// END shapes.h diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_hpp.snap similarity index 50% rename from crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap rename to crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_hpp.snap index b2eda08a..0dc0cdfb 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__shapes_hpp.snap @@ -1,178 +1,229 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o #pragma once +#include "shapes.h" + +#include #include -#include -#include -#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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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); - -/** An algebraic shape (sum type with associated data) */ -typedef enum { - /** The empty shape (a unit variant) */ - weaveffi_shapes_Shape_Empty = 0, - /** A circle with a radius */ - weaveffi_shapes_Shape_Circle = 1, - /** An axis-aligned rectangle */ - weaveffi_shapes_Shape_Rectangle = 2, - /** An integer-bounded shape (exercises narrow + wide ints) */ - weaveffi_shapes_Shape_Bounded = 3, - /** A labeled shape with a small count */ - weaveffi_shapes_Shape_Labeled = 4 -} weaveffi_shapes_Shape_Tag; -/** A plain C-style enum (no payloads) */ -typedef enum { weaveffi_shapes_Channel_Red = 0, weaveffi_shapes_Channel_Green = 1, weaveffi_shapes_Channel_Blue = 2 } weaveffi_shapes_Channel; - -/** Render a shape to a string */ -WEAVEFFI_API const char* weaveffi_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, weaveffi_error* out_err); -/** Scale a shape by a factor, returning a new shape */ -WEAVEFFI_API const uint8_t* weaveffi_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, weaveffi_error* out_err); -/** Sum a list of bytes into a wide integer (numerics smoke) */ -WEAVEFFI_API uint64_t weaveffi_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, weaveffi_error* out_err); -/** Build a Numerics struct from every numeric primitive */ -WEAVEFFI_API const uint8_t* weaveffi_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, weaveffi_error* out_err); - -} // extern "C" +#include -namespace weaveffi { +namespace shapes { /** * Base exception for every error reported through the C ABI. * - * Positive codes are the module's declared error codes (see the typed - * domain exceptions); negative codes are runtime traps: -1 a generic - * producer error, -2 a producer panic, -3 a marshalling failure, -4 a - * callback-interface implementation that threw. + * Positive codes are the declared error codes (see the typed domain + * exceptions); negative codes are runtime traps: -1 a generic producer + * error, -2 a producer panic, -3 a marshalling failure, -4 a + * callback-interface implementation that threw, and -5 a cancelled call + * (thrown as Cancelled). */ -class WeaveFFIError : public std::runtime_error { +class Error : 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_; } + /** Builds an error carrying `code` and `message`. */ + Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} + + /** The numeric error code. */ + int32_t code() const noexcept { return code_; } +}; + +/** Thrown by an async call whose CancelToken was cancelled (code -5). */ +class Cancelled : public Error { +public: + /** Builds a cancellation error carrying the producer's message. */ + explicit Cancelled(const std::string& message) : Error(-5, message) {} }; /** - * Throws WeaveFFIError when the loaded producer library was built for a - * different WeaveFFI ABI revision than this header. Call once at startup. + * Thrown on first use when the linked shapes library was built for a + * different C ABI revision or a different API contract than this header. + */ +class LoadError : public Error { +public: + /** Builds a load error describing the mismatch. */ + explicit LoadError(const std::string& message) : Error(-1, message) {} +}; + +/** + * Tag selecting the constructor that adopts a raw C pointer (one strong + * reference the wrapper then owns), in the style of std::adopt_lock. + */ +struct adopt_t { + explicit adopt_t() = default; +}; + +/** The adopt_t tag value: `Gadget g(adopt, raw)`. */ +inline constexpr adopt_t adopt{}; + +/** + * A cancellation token for cancellable async functions. + * + * Create one, pass it to any number of cancellable calls, and call cancel() + * from any thread. A cancelled call settles its future with Cancelled. Each + * call takes its own reference to the native token, so the token can be + * destroyed at any time; the destructor releases this wrapper's reference. */ -inline void check_abi_version() { - uint32_t actual = weaveffi_abi_version(); - if (actual != WEAVEFFI_ABI_VERSION) { - throw WeaveFFIError(-1, "WeaveFFI ABI revision mismatch: header " + std::to_string(WEAVEFFI_ABI_VERSION) + ", library " + std::to_string(actual)); +class CancelToken { + shapes_cancel_token* handle_; + + struct none_t {}; + explicit CancelToken(none_t) noexcept : handle_(nullptr) {} + +public: + /** Creates a new, uncancelled token. */ + CancelToken() : handle_(shapes_cancel_token_create()) { + if (handle_ == nullptr) throw std::bad_alloc(); + } + + /** Releases this wrapper's reference to the native token. */ + ~CancelToken() { + if (handle_ != nullptr) shapes_cancel_token_destroy(handle_); + } + + CancelToken(const CancelToken&) = delete; + CancelToken& operator=(const CancelToken&) = delete; + + /** Transfers the native token; `other` becomes empty. */ + CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + + /** Releases the current token and takes over `other`'s. */ + CancelToken& operator=(CancelToken&& other) noexcept { + if (this != &other) { + if (handle_ != nullptr) shapes_cancel_token_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; } -} + + /** The empty token a cancellable call uses when none is passed. */ + static const CancelToken& none() noexcept { + static const CancelToken token{none_t{}}; + return token; + } + + /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ + void cancel() const noexcept { + if (handle_ != nullptr) shapes_cancel_token_cancel(handle_); + } + + /** Whether cancel() has been called. */ + bool is_cancelled() const noexcept { + return handle_ != nullptr && shapes_cancel_token_is_cancelled(handle_); + } + + /** The native token, borrowed (null for an empty token). */ + shapes_cancel_token* handle() const noexcept { return handle_; } +}; namespace detail { -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { +/** The exception for a runtime (non-domain) error code. */ +inline std::exception_ptr make_error(int32_t code, const std::string& message) { + if (code == -5) return std::make_exception_ptr(Cancelled(message)); + return std::make_exception_ptr(Error(code, message)); +} + +/** The message of a nonzero error slot. */ +inline std::string error_message(const shapes_error& err) { + return std::string(err.message != nullptr ? err.message : "unknown error"); +} + +/** Throws the generic Error (or Cancelled) if `err` carries a nonzero code. */ +inline void check(shapes_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); + std::exception_ptr ex = make_error(err.code, error_message(err)); + shapes_error_clear(&err); + std::rethrow_exception(ex); +} + +/** Copies a producer-returned UTF-8 run into a string, then releases it. */ +inline std::string take_string(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::string(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { shapes_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::string(reinterpret_cast(ptr), len); } -/** 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)); +/** Copies a producer-returned byte run into a vector, then releases it. */ +inline std::vector take_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::vector(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { shapes_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::vector(ptr, ptr + len); +} + +/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ +inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { + if (ptr == nullptr || len == 0) return std::string_view(); + return std::string_view(reinterpret_cast(ptr), len); +} + +/** Views a borrowed byte run (null when empty) as an owned vector. */ +inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr || len == 0) return std::vector(); + return std::vector(ptr, ptr + len); +} + +/** The data pointer of a string argument, as the C ABI expects it. */ +inline const uint8_t* utf8(std::string_view s) noexcept { + return reinterpret_cast(s.data()); } } // namespace detail +/** + * Verifies that the linked shapes library implements the C ABI revision + * and the module contracts this header was generated against, throwing + * LoadError naming the first mismatch. The check runs once; every free + * function, constructor, and static member calls this before its first + * native call, so calling it at startup is optional. + */ +inline void check_library() { + static const std::string failure = [] { + uint32_t abi = shapes_abi_version(); + if (abi != SHAPES_ABI_VERSION) { + return "shapes: C ABI revision mismatch (header " + std::to_string(SHAPES_ABI_VERSION) + + ", library " + std::to_string(abi) + ")"; + } + if (shapes_shapes_checksum() != 0x06acf433ffa2a0f0ull) { + return std::string("shapes: module 'shapes' does not match the linked library (contract checksum mismatch); regenerate the bindings for this build"); + } + return std::string(); + }(); + if (!failure.empty()) throw LoadError(failure); +} + namespace detail { /** - * Serializes values into the WeaveFFI value-buffer wire format: little-endian, - * packed with no alignment, lengths and element counts as u32. + * Serializes values into the value-buffer wire format: little-endian, packed + * with no alignment, lengths and element counts as u32. */ class BufferWriter { std::vector buf_; @@ -186,10 +237,10 @@ class BufferWriter { public: /** Pointer to the encoded bytes. */ - const uint8_t* data() const { return buf_.data(); } + const uint8_t* data() const noexcept { return buf_.data(); } /** Number of encoded bytes. */ - size_t size() const { return buf_.size(); } + size_t size() const noexcept { return buf_.size(); } void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } @@ -213,10 +264,13 @@ public: 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)); } + /** Writes a byte length or an element count as a u32. */ + void write_len(size_t n) { + if (static_cast(n) > UINT32_MAX) throw Error(-3, "value buffer length exceeds u32"); + append_le(static_cast(n)); + } - void write_string(const std::string& v) { + void write_string(std::string_view v) { write_len(v.size()); buf_.insert(buf_.end(), v.begin(), v.end()); } @@ -231,11 +285,11 @@ public: }; /** - * 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. Object tokens adopted before a - * failure live in RAII wrappers that release them on unwind. + * Decodes values from the value-buffer wire format. A malformed buffer is a + * producer/consumer contract violation (both sides are generated from one + * API), so every decode failure throws the generic Error with the + * marshalling code -3. Object tokens adopted before a failure live in RAII + * wrappers that release them on unwind. */ class BufferReader { const uint8_t* data_; @@ -243,7 +297,7 @@ class BufferReader { size_t pos_; [[noreturn]] static void fail(const char* what) { - throw WeaveFFIError(-2, std::string("malformed WeaveFFI value buffer: ") + what); + throw Error(-3, std::string("malformed value buffer: ") + what); } void require(size_t n, const char* what) const { @@ -262,10 +316,10 @@ class BufferReader { } public: - BufferReader(const uint8_t* data, size_t len) : data_(data), len_(len), pos_(0) {} + BufferReader(const uint8_t* data, size_t len) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} /** Bytes not yet consumed. */ - size_t remaining() const { return len_ - pos_; } + size_t remaining() const noexcept { return len_ - pos_; } bool read_bool() { uint8_t b = read_le("bool"); @@ -296,13 +350,23 @@ public: return v; } - /** Reads a length prefix, rejecting one larger than the bytes remaining. */ + /** Reads a byte length, rejecting one larger than the bytes remaining. */ size_t read_len() { uint32_t n = read_le("length"); if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); return static_cast(n); } + /** + * Reads an element count. Elements may encode to zero bytes, so a count + * larger than the bytes remaining is legal; callers cap preallocation + * with reserve_hint(). + */ + size_t read_count() { return static_cast(read_le("count")); } + + /** A preallocation size for `n` elements that a malformed count can't inflate. */ + size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } + std::string read_string() { size_t n = read_len(); std::string v(reinterpret_cast(data_) + pos_, n); @@ -329,15 +393,19 @@ public: } }; -/** Releases a producer-allocated buffer with weaveffi_free_bytes on scope exit. */ +/** Releases a producer-allocated buffer with shapes_free_bytes on scope exit. */ struct BufferGuard { /** The producer-allocated buffer, or null when the call reported an error. */ const uint8_t* ptr; /** The buffer length in bytes. */ size_t len; + BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} + BufferGuard(const BufferGuard&) = delete; + BufferGuard& operator=(const BufferGuard&) = delete; + ~BufferGuard() { - if (ptr != nullptr) weaveffi_free_bytes(const_cast(ptr), len); + if (ptr != nullptr) shapes_free_bytes(const_cast(ptr), len); } }; @@ -425,7 +493,7 @@ struct Shape { namespace detail { -/** Encodes a `Numerics` in the WeaveFFI value-buffer format. */ +/** Encodes a `Numerics` in the value-buffer wire format. */ inline void write_Numerics(BufferWriter& w, const Numerics& v) { w.write_i8(v.a); w.write_i16(v.b); @@ -439,7 +507,7 @@ inline void write_Numerics(BufferWriter& w, const Numerics& v) { w.write_f64(v.j); } -/** Decodes a `Numerics` from the WeaveFFI value-buffer format. */ +/** Decodes a `Numerics` from the value-buffer wire format. */ inline Numerics read_Numerics(BufferReader& r) { int8_t f_a = r.read_i8(); int16_t f_b = r.read_i16(); @@ -454,7 +522,7 @@ inline Numerics read_Numerics(BufferReader& r) { return Numerics{std::move(f_a), std::move(f_b), std::move(f_c), std::move(f_d), std::move(f_e), std::move(f_f), std::move(f_g), std::move(f_h), std::move(f_i), std::move(f_j)}; } -/** Encodes a `Shape` in the WeaveFFI value-buffer format. */ +/** Encodes a `Shape` in the value-buffer wire format. */ inline void write_Shape(BufferWriter& w, const Shape& v) { switch (v.value.index()) { case 0: { @@ -492,7 +560,7 @@ inline void write_Shape(BufferWriter& w, const Shape& v) { } } -/** Decodes a `Shape` from the WeaveFFI value-buffer format. */ +/** Decodes a `Shape` from the value-buffer wire format. */ inline Shape read_Shape(BufferReader& r) { int32_t tag = r.read_i32(); switch (tag) { @@ -522,7 +590,7 @@ inline Shape read_Shape(BufferReader& r) { default: break; } - throw WeaveFFIError(-2, "malformed WeaveFFI value buffer: unknown Shape tag"); + throw Error(-3, "malformed value buffer: unknown Shape tag"); } } // namespace detail @@ -531,25 +599,26 @@ namespace shapes { /** Render a shape to a string */ inline std::string describe(const Shape& shape) { + check_library(); detail::BufferWriter shape_buf; detail::write_Shape(shape_buf, shape); - weaveffi_error err{}; - auto result = weaveffi_shapes_describe(shape_buf.data(), shape_buf.size(), &err); + size_t out_len = 0; + shapes_error err{}; + auto result = shapes_shapes_describe(shape_buf.data(), shape_buf.size(), &out_len, &err); detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; + return detail::take_string(result, out_len); } /** Scale a shape by a factor, returning a new shape */ inline Shape scale(const Shape& shape, double factor) { + check_library(); detail::BufferWriter shape_buf; detail::write_Shape(shape_buf, shape); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_shapes_scale(shape_buf.data(), shape_buf.size(), factor, &out_len, &err); + shapes_error err{}; + auto result = shapes_shapes_scale(shape_buf.data(), shape_buf.size(), factor, &out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + detail::BufferGuard result_guard(result, out_len); detail::BufferReader result_r(result, out_len); Shape ret = detail::read_Shape(result_r); result_r.expect_end(); @@ -558,19 +627,21 @@ inline Shape scale(const Shape& shape, double factor) { /** Sum a list of bytes into a wide integer (numerics smoke) */ inline uint64_t sum_bytes(const std::vector& values) { - weaveffi_error err{}; - auto result = weaveffi_shapes_sum_bytes(values.data(), values.size(), &err); + check_library(); + shapes_error err{}; + auto result = shapes_shapes_sum_bytes(values.data(), values.size(), &err); detail::check(err); return result; } /** Build a Numerics struct from every numeric primitive */ inline Numerics make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j) { + check_library(); size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &out_len, &err); + shapes_error err{}; + auto result = shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &out_len, &err); detail::check(err); - detail::BufferGuard result_guard{result, out_len}; + detail::BufferGuard result_guard(result, out_len); detail::BufferReader result_r(result, out_len); Numerics ret = detail::read_Numerics(result_r); result_r.expect_end(); @@ -579,6 +650,6 @@ inline Numerics make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t } // namespace shapes -} // namespace weaveffi +} // namespace shapes -// END weaveffi.hpp +// END shapes.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__README_md.snap index 732894ed..794b90af 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__README_md.snap @@ -1,46 +1,39 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Dart) +# docs_everywhere (Dart) -Auto-generated Dart bindings using `dart:ffi`. +`dart:ffi` bindings for the `docs_everywhere` native library. ## 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: +Add this package as a dependency (a `path:` dependency works) and import it: ```dart -import 'package:weaveffi/weaveffi.dart'; +import 'package:docs_everywhere/docs_everywhere.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`. +The library loads `libdocs_everywhere.dylib` (macOS), `libdocs_everywhere.so` (Linux and Android), or +`docs_everywhere.dll` (Windows) from the platform's search path on first use. Set +`DOCS_EVERYWHERE_LIBRARY` to a full path to load a specific build. On iOS, and as a macOS +fallback, the bindings look the symbols up in the running executable. ## Objects and callbacks -Interface objects are reference counted by the native library. Each Dart -wrapper holds one reference: call `dispose()` when you are done with it, or let -the garbage collector's finalizer release it. Objects nested inside records and -collections follow the same rule. +Each interface wrapper holds one native reference: call `dispose()` when +you're done, or let the garbage collector's finalizer release it. -Callback interfaces are abstract classes: implement one and pass an instance to -any function that takes it. Methods are bound with `NativeCallable.isolateLocal`, -so the native library may only invoke them on the isolate's own thread (which -holds when it calls them synchronously during a call from Dart). An exception -thrown by an implementation aborts the native call and surfaces to the original -Dart caller as a `WeaveFFIException` with `WeaveFFIException.foreignCode`. +Callback-interface methods that return a value run synchronously on the +isolate's thread; void methods may be called from any thread and are +delivered on the event loop. ## Requirements -- Dart SDK >= 3.1.0 (for `NativeCallable`) -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. +- Dart SDK `>=3.12.0 <4.0.0` +- The `ffi` package diff --git a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_docs_everywhere_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_docs_everywhere_dart.snap new file mode 100644 index 00000000..759e4eb5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_docs_everywhere_dart.snap @@ -0,0 +1,1046 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io' show Platform; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +// ── Library loading ── +// The native library opens on first use and is verified before any other +// lookup: its C ABI revision and every module's contract checksum must match +// the values these bindings were generated against. + +/// The native library every binding in this file calls. +final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); + +DynamicLibrary _openLibrary() { + // An explicit path wins, so a caller can point at any build artifact. + final override = Platform.environment['DOCS_EVERYWHERE_LIBRARY']; + if (override != null && override.isNotEmpty) { + return DynamicLibrary.open(override); + } + // iOS links native code into the app binary. + if (Platform.isIOS) return DynamicLibrary.process(); + final candidates = Platform.isMacOS + ? const ['libdocs_everywhere.dylib'] + : Platform.isWindows + ? const ['docs_everywhere.dll'] + : const ['libdocs_everywhere.so']; + ArgumentError? failure; + for (final candidate in candidates) { + try { + return DynamicLibrary.open(candidate); + } on ArgumentError catch (e) { + failure ??= e; + } + } + // A macOS app may link the library into its executable instead. + if (Platform.isMacOS) return DynamicLibrary.process(); + throw failure!; +} + +/// The C ABI revision these bindings were generated against. +const int _abiVersion = 3; + +/// Each top-level module's name, checksum symbol, and expected checksum. +const List<(String, String, int)> _contracts = <(String, String, int)>[ + ('docs', 'docs_everywhere_docs_checksum', 0x7950eec1e9cfc819), +]; + +DynamicLibrary _verifyLibrary(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction( + 'docs_everywhere_abi_version'); + } on ArgumentError { + throw StateError( + "the native library 'docs_everywhere' isn't loaded; set DOCS_EVERYWHERE_LIBRARY to " + 'its path'); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError('docs_everywhere: these bindings expect C ABI revision ' + '$_abiVersion, but the loaded library reports revision $found'); + } + for (final (module, symbol, expected) in _contracts) { + final int actual; + try { + actual = lib.lookupFunction(symbol)(); + } on ArgumentError { + throw StateError( + "docs_everywhere: the loaded library doesn't export module '$module'"); + } + if (actual != expected) { + throw StateError("docs_everywhere: module '$module' doesn't match the loaded " + 'library (contract checksum ${_hex(actual)}, expected ' + '${_hex(expected)}); regenerate the bindings'); + } + } + return lib; +} + +String _hex(int v) => '0x${v.toUnsigned(64).toRadixString(16).padLeft(16, '0')}'; + +// ── Errors and memory ── + +/// The C `docs_everywhere_error` slot every fallible call writes. A non-zero +/// `code` carries a producer-owned `message` and, for a domain error with +/// fields, a value-buffer `payload`; `docs_everywhere_error_clear` releases both. +final class _Error extends Struct { + @Int32() + external int code; + external Pointer message; + external Pointer payloadPtr; + @Size() + external int payloadLen; +} + +final _errorClear = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + 'docs_everywhere_error_clear', + isLeaf: true); + +final _freeBytes = _lib.lookupFunction, Size), + void Function(Pointer, int)>('docs_everywhere_free_bytes', isLeaf: true); + +// Synchronous calls on this isolate share one error slot and one length +// slot: each call reads (and clears) them right after it returns, before any +// other call can reuse them, including a nested call from a callback. +final Pointer<_Error> _err = calloc<_Error>(); +final Pointer _outLen = calloc(); + +/// A failure the native library reported. Domain errors extend this class +/// with a positive [code]; the negative runtime codes are the constants +/// below. +class NativeException implements Exception { + /// Creates an exception carrying [code] and [message]. + NativeException(this.code, this.message); + + /// The producer reported an untyped error. + static const int genericCode = -1; + + /// The producer panicked; [message] carries the panic text. + static const int panicCode = -2; + + /// An argument couldn't be lifted by the producer. + static const int marshalCode = -3; + + /// A callback implementation threw; [message] carries its text. + static const int foreignCode = -4; + + /// The call was cancelled (see [CancelledException]). + static const int cancelledCode = -5; + + /// The error code: positive for a domain error, negative for a runtime + /// failure. + final int code; + + /// The producer's message. + final String message; + + @override + String toString() => '$runtimeType($code): $message'; +} + +/// A cancellable call completed because its token was cancelled. +class CancelledException extends NativeException { + /// Creates a cancellation with the producer's [message]. + CancelledException([String message = 'cancelled']) + : super(NativeException.cancelledCode, message); +} + +typedef _ErrorMapper = NativeException Function( + int code, String message, Uint8List payload); + +/// Maps a runtime failure (or an undeclared code) onto its exception. +NativeException _runtimeError(int code, String message, Uint8List payload) => + code == NativeException.cancelledCode + ? CancelledException(message) + : NativeException(code, message); + +/// Builds the exception [err] reports, using [map] for domain codes. +NativeException _readError(Pointer<_Error> err, _ErrorMapper map) { + final e = err.ref; + final message = e.message == nullptr ? '' : e.message.toDartString(); + return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); +} + +/// Throws the error a synchronous call left in [err], clearing the slot. +void _check(Pointer<_Error> err, [_ErrorMapper map = _runtimeError]) { + if (err.ref.code == 0) return; + final error = _readError(err, map); + _errorClear(err); + throw error; +} + +/// Copies a borrowed native byte run into Dart memory. +Uint8List _copyBytes(Pointer ptr, int len) => + ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); + +/// Copies an owned native byte run and releases it. +Uint8List _takeBytes(Pointer ptr, int len) { + if (ptr == nullptr) return Uint8List(0); + final bytes = Uint8List.fromList(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return bytes; +} + +/// Decodes an owned UTF-8 run and releases it. +String _takeString(Pointer ptr, int len) { + if (ptr == nullptr) return ''; + final text = utf8.decode(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return text; +} + +/// Decodes a borrowed UTF-8 run. +String _readString(Pointer ptr, int len) => + ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); + +/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. +Pointer _stage(Arena arena, List bytes) { + if (bytes.isEmpty) return nullptr; + final ptr = arena(bytes.length); + ptr.asTypedList(bytes.length).setAll(0, bytes); + return ptr; +} + +// ── Objects ── + +/// The lifetime machinery every interface wrapper shares. A wrapper owns one +/// strong reference to its native object and releases it exactly once: in +/// [dispose], or through a GC finalizer when it's collected undisposed. +/// +/// Every call borrows the pointer through [_enter] and [_leave]. A [dispose] +/// that arrives while a call is in flight (from a callback the call made) +/// only marks the wrapper; the reference is released when the last call +/// leaves, so the native object never disappears under a running call. +abstract base class _NativeObject implements Finalizable { + _NativeObject(this._ptr) { + _finalizer.attach(this, _ptr, detach: this); + } + + final Pointer _ptr; + int _calls = 0; + bool _disposed = false; + + NativeFinalizer get _finalizer; + void _destroy(Pointer ptr); + Pointer _clone(Pointer ptr); + + /// Releases this wrapper's native reference. Safe to call more than once; + /// the native object is dropped when its last reference (this one, another + /// wrapper's, or the producer's) goes away. Using the wrapper afterwards + /// throws a [StateError]. + void dispose() { + if (_disposed) return; + _disposed = true; + if (_calls == 0) _release(); + } + + void _release() { + _finalizer.detach(this); + _destroy(_ptr); + } + + Pointer _enter() { + if (_disposed) throw StateError('$runtimeType used after dispose()'); + _calls++; + return _ptr; + } + + void _leave() { + if (--_calls == 0 && _disposed) _release(); + } + + /// A second strong reference, for an object token in a value buffer. + Pointer _cloneRef() { + final ptr = _enter(); + try { + return _clone(ptr); + } finally { + _leave(); + } + } +} + +/// Borrows [object]'s pointer until [arena] is released. +Pointer _borrow(Arena arena, _NativeObject object) { + final ptr = object._enter(); + arena.onReleaseAll(object._leave); + return ptr; +} + +// ── Value buffers ── +// Records, rich enums, optionals, lists, maps, and error payloads cross the +// C ABI serialized in this little-endian, packed format. A malformed buffer +// is a contract violation between producer and bindings, never a domain +// error. + +Never _bufferError(String context) => + throw StateError('malformed value buffer: $context'); + +final class _BufferWriter { + Uint8List _buf = Uint8List(64); + late ByteData _data = ByteData.sublistView(_buf); + int _len = 0; + + void _reserve(int extra) { + if (_len + extra <= _buf.length) return; + var cap = _buf.length * 2; + while (cap < _len + extra) { + cap *= 2; + } + _buf = Uint8List(cap)..setRange(0, _len, _buf); + _data = ByteData.sublistView(_buf); + } + + Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); + + void writeBool(bool v) => writeU8(v ? 1 : 0); + + void writeFlag(bool present) => writeU8(present ? 1 : 0); + + void writeI8(int v) { + _reserve(1); + _data.setInt8(_len, v); + _len += 1; + } + + void writeU8(int v) { + _reserve(1); + _data.setUint8(_len, v); + _len += 1; + } + + void writeI16(int v) { + _reserve(2); + _data.setInt16(_len, v, Endian.little); + _len += 2; + } + + void writeU16(int v) { + _reserve(2); + _data.setUint16(_len, v, Endian.little); + _len += 2; + } + + void writeI32(int v) { + _reserve(4); + _data.setInt32(_len, v, Endian.little); + _len += 4; + } + + void writeU32(int v) { + _reserve(4); + _data.setUint32(_len, v, Endian.little); + _len += 4; + } + + void writeI64(int v) { + _reserve(8); + _data.setInt64(_len, v, Endian.little); + _len += 8; + } + + // Dart ints are signed 64-bit; a u64 travels as its bit pattern. + void writeU64(int v) => writeI64(v); + + void writeF32(double v) { + _reserve(4); + _data.setFloat32(_len, v, Endian.little); + _len += 4; + } + + void writeF64(double v) { + _reserve(8); + _data.setFloat64(_len, v, Endian.little); + _len += 8; + } + + void writeLength(int v) => writeU32(v); + + void writeString(String v) => writeBytes(utf8.encode(v)); + + void writeBytes(List v) { + writeLength(v.length); + _reserve(v.length); + _buf.setRange(_len, _len + v.length, v); + _len += v.length; + } +} + +final class _BufferReader { + _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); + + final Uint8List _buf; + final ByteData _data; + int _pos = 0; + + int _take(int n, String context) { + if (_buf.length - _pos < n) _bufferError('truncated $context'); + final at = _pos; + _pos += n; + return at; + } + + bool readBool() { + final b = _buf[_take(1, 'bool')]; + if (b > 1) _bufferError('bool byte $b'); + return b == 1; + } + + bool readFlag() { + final b = _buf[_take(1, 'option flag')]; + if (b > 1) _bufferError('option flag $b'); + return b == 1; + } + + int readI8() => _data.getInt8(_take(1, 'i8')); + + int readU8() => _buf[_take(1, 'u8')]; + + int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); + + int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); + + int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); + + int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); + + int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); + + // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. + int readU64() => readI64(); + + double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); + + double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); + + int readLength() => readU32(); + + String readString() { + final n = readLength(); + final at = _take(n, 'string'); + return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); + } + + Uint8List readBytes() { + final n = readLength(); + final at = _take(n, 'bytes'); + return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); + } + + void expectEnd() { + if (_pos != _buf.length) _bufferError('trailing bytes'); + } +} + +/// Encodes one value with [write] into a fresh buffer. +Uint8List _encode(T value, void Function(_BufferWriter, T) write) { + final writer = _BufferWriter(); + write(writer, value); + return writer.takeBytes(); +} + +/// Decodes exactly one value with [read] from [bytes]. +T _decode(Uint8List bytes, T Function(_BufferReader) read) { + final reader = _BufferReader(bytes); + final value = read(reader); + reader.expectEnd(); + return value; +} + +// ── Async completion ── +// An async call completes through a `NativeCallable.listener` the producer +// invokes exactly once, from any thread. Everything the completion carries is +// owned by these bindings, so it's decoded later on the event loop and then +// released. + +final _errorFree = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + 'docs_everywhere_error_free'); + +/// Builds the exception a completion reports and frees its boxed error. +NativeException _takeAsyncError(Pointer<_Error> err, _ErrorMapper map) { + final error = _readError(err, map); + _errorFree(err); + return error; +} + +// ── Callback interfaces ── +// An implementation passed to the producer is parked in [_callbacks] under +// an integer key. The `ctx` the producer receives points at a small native +// [_CallbackContext] holding that key, so the producer never holds a Dart +// object and the GC never sees a raw pointer. The entry lives until the +// producer calls the vtable's `free`. +// +// Methods that return a value are `NativeCallable.isolateLocal` trampolines: +// they run synchronously, so the producer must call them on this isolate's +// thread while a call from Dart is in progress. Void methods and `free` are +// `NativeCallable.isolateGroupBound` forwarders, which may run on any thread: +// they copy their arguments into a message for this isolate (borrowed bytes +// are copied before the forwarder returns) and the implementation runs later +// on the event loop, in the zone that passed the callback. + +/// Where forwarders send messages: the allocator they build a message with +/// and the isolate port that receives it. Forwarders run outside the +/// isolate and can't read Dart globals, so everything they need is native. +final class _CallbackPost extends Struct { + external Pointer Function(Size)>> alloc; + external Pointer)>> release; + external Pointer)>> post; + @Int64() + external int port; +} + +/// The native `ctx` of one registered implementation. +final class _CallbackContext extends Struct { + @Int64() + external int key; + external Pointer<_CallbackPost> post; +} + +// The `Dart_CObject` layout `Dart_PostCObject` reads. +final class _CArray extends Struct { + @IntPtr() + external int length; + external Pointer> values; +} + +final class _CTypedData extends Struct { + @Int32() + external int type; + @IntPtr() + external int length; + external Pointer values; +} + +final class _CValue extends Union { + @Bool() + external bool asBool; + @Int64() + external int asInt64; + @Double() + external double asDouble; + external _CArray asArray; + external _CTypedData asTypedData; +} + +final class _CObject extends Struct { + @Int32() + external int type; + external _CValue value; +} + +typedef _CallbackDispatch = void Function( + Object impl, int method, List message); + +final class _CallbackEntry { + _CallbackEntry(this.impl, this.dispatch, this.context) : zone = Zone.current; + + final Object impl; + final _CallbackDispatch dispatch; + final Pointer<_CallbackContext> context; + final Zone zone; +} + +final Map _callbacks = {}; +int _nextCallbackKey = 1; + +/// Registers [impl] and returns the `ctx` the producer receives. +Pointer _registerCallback(Object impl, _CallbackDispatch dispatch) { + final key = _nextCallbackKey++; + final context = calloc<_CallbackContext>(); + context.ref + ..key = key + ..post = _callbackPost; + _callbacks[key] = _CallbackEntry(impl, dispatch, context); + return context.cast(); +} + +/// The implementation registered under [ctx]. +Object _callbackTarget(Pointer ctx) { + final key = ctx.cast<_CallbackContext>().ref.key; + final entry = _callbacks[key]; + if (entry == null) { + throw StateError('callback context $key is not registered'); + } + return entry.impl; +} + +final Pointer<_CallbackPost> _callbackPost = () { + final port = RawReceivePort(_receiveCallbackMessage, 'callback messages'); + port.keepIsolateAlive = false; + _callbackPort = port; + final allocator = Platform.isWindows + ? DynamicLibrary.open('ole32.dll') + : DynamicLibrary.process(); + final post = calloc<_CallbackPost>(); + post.ref + ..alloc = allocator.lookup(Platform.isWindows ? 'CoTaskMemAlloc' : 'malloc') + ..release = allocator.lookup(Platform.isWindows ? 'CoTaskMemFree' : 'free') + ..post = NativeApi.postCObject.cast() + ..port = port.sendPort.nativePort; + return post; +}(); + +RawReceivePort? _callbackPort; + +void _receiveCallbackMessage(Object? message) { + final fields = message! as List; + final key = fields[0]! as int; + final method = fields[1]! as int; + if (method == _CallbackMessage.free) { + final entry = _callbacks.remove(key); + if (entry != null) calloc.free(entry.context); + return; + } + final entry = _callbacks[key]; + if (entry == null) return; + entry.zone.runGuarded(() => entry.dispatch(entry.impl, method, fields)); +} + +/// One message from a forwarder: `[key, method, arguments...]`, built in +/// native memory and posted with `Dart_PostCObject`, which copies it. Runs +/// outside the isolate, so it touches no Dart globals. +final class _CallbackMessage { + _CallbackMessage(Pointer ctx, int method, int arguments) + : _context = ctx.cast<_CallbackContext>().ref, + _capacity = arguments + 2 { + final post = _context.post.ref; + final alloc = post.alloc.asFunction Function(int)>(); + _objects = alloc(sizeOf<_CObject>() * (_capacity + 1)).cast(); + _slots = alloc(sizeOf>() * _capacity).cast(); + int64(_context.key); + int64(method); + } + + static const int free = -1; + static const int _kBool = 1; + static const int _kInt64 = 3; + static const int _kDouble = 4; + static const int _kArray = 6; + static const int _kTypedData = 7; + static const int _kUint8 = 2; + + final _CallbackContext _context; + final int _capacity; + late final Pointer<_CObject> _objects; + late final Pointer> _slots; + int _count = 0; + + _CObject _next(int type) { + final object = _objects + (_count + 1); + _slots[_count++] = object; + return object.ref..type = type; + } + + void boolean(bool v) => _next(_kBool).value.asBool = v; + + void int64(int v) => _next(_kInt64).value.asInt64 = v; + + void float64(double v) => _next(_kDouble).value.asDouble = v; + + void pointer(Pointer v) => int64(v.address); + + /// A borrowed byte run; posting copies it. + void bytes(Pointer ptr, int len) { + final data = _next(_kTypedData).value.asTypedData; + data + ..type = _kUint8 + ..length = ptr == nullptr ? 0 : len + ..values = ptr; + } + + void send() { + final post = _context.post.ref; + _objects.ref.type = _kArray; + _objects.ref.value.asArray + ..length = _count + ..values = _slots; + post.post.asFunction)>()( + post.port, _objects); + final release = post.release.asFunction)>(); + release(_slots.cast()); + release(_objects.cast()); + } +} + +// The vtable `free` entry every callback interface shares. +void _forwardCallbackFree(Pointer ctx) => + _CallbackMessage(ctx, _CallbackMessage.free, 0).send(); + +final Pointer)>> _callbackFree = + _pin(NativeCallable)>.isolateGroupBound( + _forwardCallbackFree)); + +// Vtable entries live for the process: they're never closed, so they're +// anchored here, and they don't keep an idle isolate alive. +final List> _pinned = >[]; + +Pointer> _pin(NativeCallable callable) { + callable.keepIsolateAlive = false; + _pinned.add(callable); + return callable.nativeFunction; +} + +final _errorSet = _lib.lookupFunction< + Void Function(Pointer<_Error>, Int32, Pointer), + void Function(Pointer<_Error>, int, Pointer)>('docs_everywhere_error_set'); + +/// Reports an implementation's exception to the producer through +/// `docs_everywhere_error_set` with the foreign code (-4). Never throws: it runs +/// in the catch path of a trampoline the exception must not unwind through. +void _foreignError(Pointer<_Error> outErr, Object error) { + if (outErr == nullptr) return; + Pointer message = nullptr; + try { + message = error.toString().toNativeUtf8(allocator: calloc); + _errorSet(outErr, NativeException.foreignCode, message); + } catch (_) { + _errorSet(outErr, NativeException.foreignCode, nullptr); + } finally { + if (message != nullptr) calloc.free(message); + } +} + +/// The `DocsErrors` error domain of module `docs`. +class DocsErrorsException extends NativeException { + /// Creates a domain error carrying [code] and [message]. + DocsErrorsException(super.code, super.message); +} + +/// Returned when no document matches the requested id +class NotFoundException extends DocsErrorsException { + NotFoundException([String message = 'Document not found']) : super(1, message); +} + +/// Raised when attempting to move into a forbidden state +class InvalidStatusException extends DocsErrorsException { + InvalidStatusException([String message = 'Invalid status transition']) : super(2, message); +} + +NativeException _mapDocsErrorsException(int code, String message, Uint8List payload) { + switch (code) { + case 1: + return NotFoundException(message); + case 2: + return InvalidStatusException(message); + default: + return _runtimeError(code, message, payload); + } +} + +/// Lifecycle status of a document. +/// +/// Used to drive UI badges and filters. +enum Status { + /// Document is being edited + draft(0), + /// Document is publicly visible + published(1), + /// Document is hidden from listings + archived(2); + + const Status(this.value); + + /// The C discriminant. + final int value; + + /// The variant whose discriminant is [value]. + static Status fromValue(int value) => values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError.value(value, 'value', 'not a Status discriminant')); +} + +/// Represents a single document tracked by the system. +/// +/// Documents are persisted to disk and exposed via the public API. +class Document { + /// Creates a [Document]. + Document({required this.id, required this.title, required this.status, required this.tags}); + + /// Stable opaque identifier + final int id; + /// Human-readable title shown in lists + final String title; + /// Current lifecycle status + final Status status; + /// Free-form tags for grouping + final List tags; +} + +void _packDocument(_BufferWriter w, Document v) { + w.writeI64(v.id); + w.writeString(v.title); + w.writeI32(v.status.value); + final _t0 = v.tags; + w.writeLength(_t0.length); + for (final _t1 in _t0) { + w.writeString(_t1); + } +} + +Document _unpackDocument(_BufferReader r) => Document( + id: r.readI64(), + title: r.readString(), + status: Status.fromValue(r.readI32()), + tags: [for (var i = r.readLength(); i > 0; i--) r.readString()], +); + +/// Consumer-implemented observer of document-saved notifications. +/// +/// Implementations must be safe to call from any thread. +/// +/// A callback interface: implement it and pass an instance to any +/// function taking a [SavedListener]. The library keeps the instance alive +/// until the producer releases it. +/// +/// Methods that return a value run synchronously and must be called by +/// the producer on this isolate's thread during a call from Dart; a +/// thrown exception fails that call with [NativeException.foreignCode]. +/// Void methods may be called from any thread: they're delivered +/// asynchronously on this isolate's event loop, in the zone that passed +/// the instance, where a thrown exception is an uncaught error of that +/// zone. Object arguments are owned by the implementation. +abstract class SavedListener { + /// Fires after a document has been persisted successfully + void onSaved(int docId, int revision); +} + +typedef _DocsEverywhereDocsSavedListenerOnSaved = Void Function(Pointer, Int64, Int32, Pointer<_Error>); + +// The C `docs_everywhere_docs_SavedListener_vtable` layout. +final class _SavedListenerVtable extends Struct { + external Pointer> onSaved; + external Pointer)>> free; +} + +// Vtable entry `docs_everywhere_docs_SavedListener_vtable.on_saved`: forwarded to the isolate. +void _savedListenerOnSaved(Pointer ctx, int docId, int revision, Pointer<_Error> outErr) { + _CallbackMessage(ctx, 0, 2) + ..int64(docId) + ..int64(revision) + ..send(); +} + +void _savedListenerDispatch(Object impl, int method, List message) { + final target = impl as SavedListener; + switch (method) { + case 0: { + final _a2 = message[2]! as int; + final _a3 = message[3]! as int; + target.onSaved(_a2, _a3); + } + } +} + +final Pointer<_SavedListenerVtable> _savedListenerVtable = () { + final vtable = calloc<_SavedListenerVtable>(); + vtable.ref + ..onSaved = _pin(NativeCallable<_DocsEverywhereDocsSavedListenerOnSaved>.isolateGroupBound(_savedListenerOnSaved)) + ..free = _callbackFree; + return vtable; +}(); + +final _docsEverywhereDocsNotebookClone = _lib.lookupFunction< + Pointer Function(Pointer), + Pointer Function(Pointer)>('docs_everywhere_docs_Notebook_clone'); + +final _docsEverywhereDocsNotebookDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('docs_everywhere_docs_Notebook_destroy'); +final _docsEverywhereDocsNotebookDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('docs_everywhere_docs_Notebook_destroy')); + +final _docsEverywhereDocsNotebookNew = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('docs_everywhere_docs_Notebook_new'); + +final _docsEverywhereDocsNotebookInsert = _lib.lookupFunction< + Int32 Function(Pointer, Pointer, Size, Pointer<_Error>), + int Function(Pointer, Pointer, int, Pointer<_Error>)>('docs_everywhere_docs_Notebook_insert'); + +final _docsEverywhereDocsNotebookFind = _lib.lookupFunction< + Pointer Function(Pointer, Int64, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('docs_everywhere_docs_Notebook_find'); + +final _docsEverywhereDocsNotebookSize = _lib.lookupFunction< + Int32 Function(Pointer, Pointer<_Error>), + int Function(Pointer, Pointer<_Error>)>('docs_everywhere_docs_Notebook_size'); + +final _docsEverywhereDocsNotebookMaxDocuments = _lib.lookupFunction< + Int32 Function(Pointer<_Error>), + int Function(Pointer<_Error>)>('docs_everywhere_docs_Notebook_max_documents'); + +/// An ordered collection of documents. +/// +/// Notebooks own their documents and release them when destroyed. +/// +/// A reference-counted native object. Each instance holds one strong +/// reference, released by [dispose] or, when an undisposed instance is +/// collected, by a finalizer. +final class Notebook extends _NativeObject { + Notebook._(super._ptr); + + @override + NativeFinalizer get _finalizer => _docsEverywhereDocsNotebookDestroyFinalizer; + + @override + void _destroy(Pointer ptr) => _docsEverywhereDocsNotebookDestroy(ptr); + + @override + Pointer _clone(Pointer ptr) => _docsEverywhereDocsNotebookClone(ptr); + + /// Create an empty notebook + /// + /// The returned object is owned by the caller: call its `dispose()` + /// when you're done with it. + factory Notebook() { + final _result = _docsEverywhereDocsNotebookNew(_err); + _check(_err); + return Notebook._(_result); + } + + /// Add a document, returning its position in the notebook + int insert(Document document) { + final _self = _enter(); + final _arena = Arena(); + try { + final _documentWriter = _BufferWriter(); + _packDocument(_documentWriter, document); + final _documentBytes = _documentWriter.takeBytes(); + final _result = _docsEverywhereDocsNotebookInsert(_self, _stage(_arena, _documentBytes), _documentBytes.length, _err); + _check(_err); + return _result; + } finally { + _arena.releaseAll(); + _leave(); + } + } + + /// Look up a document by identifier, failing when none matches + /// + /// Throws [DocsErrorsException] on domain errors. + Document find(int id) { + final _self = _enter(); + try { + final _result = _docsEverywhereDocsNotebookFind(_self, id, _outLen, _err); + _check(_err, _mapDocsErrorsException); + return _decode(_takeBytes(_result, _outLen.value), _unpackDocument); + } finally { + _leave(); + } + } + + /// The number of documents currently held + int size() { + final _self = _enter(); + try { + final _result = _docsEverywhereDocsNotebookSize(_self, _err); + _check(_err); + return _result; + } finally { + _leave(); + } + } + + /// The largest number of documents one notebook will hold + static int maxDocuments() { + final _result = _docsEverywhereDocsNotebookMaxDocuments(_err); + _check(_err); + return _result; + } +} + +final _docsEverywhereDocsCreateDocument = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('docs_everywhere_docs_create_document'); + +/// Create a brand new document. +/// +/// The document starts in `Draft` status; callers must publish +/// it explicitly to make it visible. +Document createDocument(String title, List tags) { + final _arena = Arena(); + try { + final _titleBytes = utf8.encode(title); + final _tagsWriter = _BufferWriter(); + final _t0 = tags; + _tagsWriter.writeLength(_t0.length); + for (final _t1 in _t0) { + _tagsWriter.writeString(_t1); + } + final _tagsBytes = _tagsWriter.takeBytes(); + final _result = _docsEverywhereDocsCreateDocument(_stage(_arena, _titleBytes), _titleBytes.length, _stage(_arena, _tagsBytes), _tagsBytes.length, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackDocument); + } finally { + _arena.releaseAll(); + } +} + +final _docsEverywhereDocsGetDocument = _lib.lookupFunction< + Pointer Function(Int64, Pointer, Pointer<_Error>), + Pointer Function(int, Pointer, Pointer<_Error>)>('docs_everywhere_docs_get_document'); + +/// Look up a document by identifier +Document? getDocument(int id) { + final _result = _docsEverywhereDocsGetDocument(id, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), (r) => (r.readFlag() ? _unpackDocument(r) : null)); +} + +typedef _DocsEverywhereDocsPublishAsyncCallback = Void Function(Pointer, Pointer<_Error>, Bool); + +final _docsEverywhereDocsPublishAsync = _lib.lookupFunction< + Void Function(Int64, Pointer>, Pointer), + void Function(int, Pointer>, Pointer)>('docs_everywhere_docs_publish_async'); + +/// Publish a document in the background +Future publishAsync(int id) { + final _completer = Completer(); + late final NativeCallable<_DocsEverywhereDocsPublishAsyncCallback> _callback; + _callback = NativeCallable<_DocsEverywhereDocsPublishAsyncCallback>.listener(( + Pointer _, Pointer<_Error> _error, bool _value) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _runtimeError); + _completer.complete(_value); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _docsEverywhereDocsPublishAsync(id, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } + return _completer.future; +} + +final _docsEverywhereDocsWatch = _lib.lookupFunction< + Void Function(Pointer, Pointer, Pointer<_Error>), + void Function(Pointer, Pointer, Pointer<_Error>)>('docs_everywhere_docs_watch'); + +/// Register a listener for saved notifications +void watch(SavedListener listener) { + final _listenerCtx = _registerCallback(listener, _savedListenerDispatch); + _docsEverywhereDocsWatch(_listenerCtx, _savedListenerVtable.cast(), _err); + _check(_err); +} + +// END docs_everywhere.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap deleted file mode 100644 index 5a63c7c3..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap +++ /dev/null @@ -1,833 +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}'); -} - -// The ABI revision these bindings were generated against. -const int _abiVersion = 2; - -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 _NativeWeaveffiErrorSet = Void Function(Pointer<_WeaveFFIError>, Int32, Pointer); -typedef _DartWeaveffiErrorSet = void Function(Pointer<_WeaveFFIError>, int, Pointer); -final _weaveffiErrorSet = _lib.lookupFunction< - _NativeWeaveffiErrorSet, _DartWeaveffiErrorSet>('weaveffi_error_set'); - -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: a producer panic ([panicCode]), a marshalling -/// error ([marshalCode]), a callback-interface implementation that threw -/// ([foreignCode], carrying the Dart exception's text), or an unknown code. -class WeaveFFIException implements Exception { - /// The producer reported an untyped error. - static const int genericCode = -1; - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - /// An argument could not be lifted by the producer. - static const int marshalCode = -3; - /// A Dart callback-interface implementation threw; [message] carries the - /// exception's text. - static const int foreignCode = -4; - 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'); - } -} - -// ── WeaveFFI callback-interface runtime ── -// A Dart implementation passed to the producer is stored here under an -// integer key; the key (widened to a pointer) is what crosses as `ctx`, so the -// producer never holds a Dart object and the GC never sees a raw pointer. The -// entry lives until the producer calls the vtable's `free(ctx)`. -final Map _callbackTable = {}; -int _nextCallbackKey = 1; - -Pointer _registerCallback(Object impl) { - final key = _nextCallbackKey++; - _callbackTable[key] = impl; - return Pointer.fromAddress(key); -} - -Object _callbackFor(Pointer ctx) { - final impl = _callbackTable[ctx.address]; - if (impl == null) { - throw StateError('callback interface context ${ctx.address} is not registered'); - } - return impl; -} - -// Vtable trampolines are process-wide: their NativeCallables are never closed, -// so they are anchored here to keep the native thunks alive. `keepIsolateAlive` -// is cleared on each so an idle isolate can still exit. -final List> _callbackCallables = []; - -Pointer> _pinCallable( - NativeCallable callable) { - callable.keepIsolateAlive = false; - _callbackCallables.add(callable); - return callable.nativeFunction; -} - -// Reports a Dart exception thrown by a callback-interface implementation -// through `weaveffi_error_set` with the reserved foreign error code (-4). -// The producer copies the message, so the temporary is freed right away. This -// must never throw: it runs in the catch path of a trampoline whose C frame -// an exception must not unwind through. -void _foreignError(Pointer<_WeaveFFIError> outErr, Object error) { - if (outErr == nullptr) return; - Pointer message = nullptr; - try { - message = error.toString().toNativeUtf8(); - _weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, message); - } catch (_) { - _weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, nullptr); - } finally { - if (message != nullptr) calloc.free(message); - } -} - -/// Typed error domain `DocsErrors` declared by module `docs`. -class DocsErrorsException extends WeaveFFIException { - DocsErrorsException(super.code, super.message); -} - -/// Returned when no document matches the requested id -class NotFoundException extends DocsErrorsException { - NotFoundException([String message = 'Document not found']) : super(1, message); -} - -/// Raised when attempting to move into a forbidden state -class InvalidStatusException extends DocsErrorsException { - InvalidStatusException([String message = 'Invalid status transition']) : super(2, message); -} - -WeaveFFIException _mapDocsErrorsException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return NotFoundException(message); - case 2: - return InvalidStatusException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkDocsErrorsException(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 _mapDocsErrorsException(code, msg, payload); - } -} - -/// Lifecycle status of a document. -/// -/// Used to drive UI badges and filters. -enum Status { - /// Document is being edited - draft(0), - /// Document is publicly visible - published(1), - /// Document is hidden from listings - archived(2), - ; - const Status(this.value); - final int value; - - static Status fromValue(int value) => - Status.values.firstWhere((e) => e.value == value); -} - -/// Represents a single document tracked by the system. -/// -/// Documents are persisted to disk and exposed via the public API. -class Document { - /// Stable opaque identifier - final int id; - /// Human-readable title shown in lists - final String title; - /// Current lifecycle status - final Status status; - /// Free-form tags for grouping - final List tags; - - Document({required this.id, required this.title, required this.status, required this.tags}); -} - -void _packDocument(_BufferWriter w, Document v) { - w.writeInt64(v.id); - w.writeString(v.title); - w.writeInt32(v.status.value); - final t0 = v.tags; - w.writeLength(t0.length); - for (final t1 in t0) { - w.writeString(t1); - } -} - -Document _unpackDocument(_BufferReader r) { - return Document( - id: r.readInt64(), - title: r.readString(), - status: Status.fromValue(r.readInt32()), - tags: List.generate(r.readLength(), (_) => r.readString()), - ); -} - -/// Consumer-implemented observer of document-saved notifications. -/// -/// Implementations must be safe to call from any thread. -/// -/// A callback interface: implement this class and pass an instance to any -/// function taking a [SavedListener]. The producer keeps the instance alive -/// (through a private handle table) until it releases the callback, so no -/// other reference needs to be held. -/// -/// Object arguments are owned by the implementation: call `dispose()` on -/// them (or let the finalizer run) when done. A thrown exception is reported -/// to the producer, which aborts the call it was making; the original Dart -/// caller then observes a [WeaveFFIException] with [WeaveFFIException.foreignCode] -/// carrying the exception's text. -/// -/// Dart limitation: methods are bound with `NativeCallable.isolateLocal`, so -/// the producer may only invoke them on the thread of the isolate that -/// passed the callback. That holds when the producer calls them -/// synchronously during a call from Dart. A producer that invokes a method -/// from another thread (an async task, a background worker) is unsupported: -/// a `NativeCallable.listener` cannot return a value or read borrowed -/// arguments after the native frame returns. -abstract class SavedListener { - /// Fires after a document has been persisted successfully - void onSaved(int docId, int revision); -} - -typedef _NativeSavedListenerVtOnSaved = Void Function(Pointer, Int64, Int32, Pointer<_WeaveFFIError>); -typedef _NativeSavedListenerVtFree = Void Function(Pointer); - -// The C `weaveffi_docs_SavedListener_vtable` layout. -final class _SavedListenerVtableStruct extends Struct { - external Pointer> onSaved; - external Pointer> free; -} - -// Vtable entry `weaveffi_docs_SavedListener_vtable.on_saved`. -void _savedListenerVtOnSaved(Pointer ctx, int docId, int revision, Pointer<_WeaveFFIError> outErr) { - try { - final impl = _callbackFor(ctx) as SavedListener; - impl.onSaved(docId, revision); - } catch (e) { - _foreignError(outErr, e); - } -} - -void _savedListenerVtFree(Pointer ctx) { - _callbackTable.remove(ctx.address); -} - -// The process-wide static `weaveffi_docs_SavedListener_vtable` every SavedListener passed to the producer -// shares; the per-instance state travels in `ctx`. -final Pointer<_SavedListenerVtableStruct> _SavedListenerVtable = () { - final vt = calloc<_SavedListenerVtableStruct>(); - vt.ref.onSaved = _pinCallable(NativeCallable<_NativeSavedListenerVtOnSaved>.isolateLocal( - _savedListenerVtOnSaved)); - vt.ref.free = _pinCallable(NativeCallable<_NativeSavedListenerVtFree>.listener( - _savedListenerVtFree)); - return vt; -}(); - -typedef _NativeWeaveffiDocsNotebookClone = Pointer Function(Pointer); -typedef _DartWeaveffiDocsNotebookClone = Pointer Function(Pointer); -final _weaveffiDocsNotebookClone = _lib.lookupFunction< - _NativeWeaveffiDocsNotebookClone, _DartWeaveffiDocsNotebookClone>('weaveffi_docs_Notebook_clone'); - -typedef _NativeWeaveffiDocsNotebookDestroy = Void Function(Pointer); -typedef _DartWeaveffiDocsNotebookDestroy = void Function(Pointer); -final _weaveffiDocsNotebookDestroy = _lib.lookupFunction< - _NativeWeaveffiDocsNotebookDestroy, _DartWeaveffiDocsNotebookDestroy>('weaveffi_docs_Notebook_destroy'); -final _weaveffiDocsNotebookDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('weaveffi_docs_Notebook_destroy')); - -typedef _NativeWeaveffiDocsNotebookNew = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiDocsNotebookNew = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiDocsNotebookNew = _lib.lookupFunction< - _NativeWeaveffiDocsNotebookNew, _DartWeaveffiDocsNotebookNew>('weaveffi_docs_Notebook_new'); - -typedef _NativeWeaveffiDocsNotebookInsert = Int32 Function(Pointer, Pointer, Size, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiDocsNotebookInsert = int Function(Pointer, Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiDocsNotebookInsert = _lib.lookupFunction< - _NativeWeaveffiDocsNotebookInsert, _DartWeaveffiDocsNotebookInsert>('weaveffi_docs_Notebook_insert'); - -typedef _NativeWeaveffiDocsNotebookFind = Pointer Function(Pointer, Int64, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiDocsNotebookFind = Pointer Function(Pointer, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiDocsNotebookFind = _lib.lookupFunction< - _NativeWeaveffiDocsNotebookFind, _DartWeaveffiDocsNotebookFind>('weaveffi_docs_Notebook_find'); - -typedef _NativeWeaveffiDocsNotebookSize = Int32 Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiDocsNotebookSize = int Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiDocsNotebookSize = _lib.lookupFunction< - _NativeWeaveffiDocsNotebookSize, _DartWeaveffiDocsNotebookSize>('weaveffi_docs_Notebook_size'); - -typedef _NativeWeaveffiDocsNotebookMaxDocuments = Int32 Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiDocsNotebookMaxDocuments = int Function(Pointer<_WeaveFFIError>); -final _weaveffiDocsNotebookMaxDocuments = _lib.lookupFunction< - _NativeWeaveffiDocsNotebookMaxDocuments, _DartWeaveffiDocsNotebookMaxDocuments>('weaveffi_docs_Notebook_max_documents'); - -/// An ordered collection of documents. -/// -/// Notebooks own their documents and release them when destroyed. -/// -/// A reference-counted native object. Each instance holds one strong -/// reference, released by [dispose] or, if the instance is collected -/// without being disposed, by a GC finalizer. Instances stay valid for as -/// long as the Dart object is reachable; the producer keeps its own -/// references to any object it retains. -class Notebook implements Finalizable { - final Pointer _ptr; - bool _disposed = false; - - /// Adopts one strong reference to the native object. - Notebook._(this._ptr) { - _weaveffiDocsNotebookDestroyFinalizer.attach(this, _ptr, detach: this); - } - - /// The borrowed native pointer for the duration of a call. - Pointer get _handle { - if (_disposed) throw StateError('Notebook used after dispose()'); - return _ptr; - } - - /// Mints a second strong reference (the interface's `_clone` symbol) for - /// an object token written into a value buffer. - Pointer _cloneRef() => _weaveffiDocsNotebookClone(_handle); - - /// Releases this instance's native reference. Safe to call more than - /// once; the native object is dropped when its last reference (this - /// one, any other wrapper's, or the producer's) is released. - void dispose() { - if (_disposed) return; - _disposed = true; - _weaveffiDocsNotebookDestroyFinalizer.detach(this); - _weaveffiDocsNotebookDestroy(_ptr); - } - - /// Create an empty notebook - /// - /// The returned object is owned by the caller: call its `dispose()` when - /// you are done with it. - factory Notebook() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiDocsNotebookNew(err); - _checkError(err); - return Notebook._(result); - } finally { - calloc.free(err); - } - } - - /// Add a document, returning its position in the notebook - int insert(Document document) { - final documentWriter = _BufferWriter(); - _packDocument(documentWriter, document); - final documentBuf = documentWriter.takeBytes(); - final documentPtr = _stageBytes(documentBuf); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiDocsNotebookInsert(_handle, documentPtr, documentBuf.length, err); - _checkError(err); - return result; - } finally { - calloc.free(documentPtr); - calloc.free(err); - } - } - - /// Look up a document by identifier, failing when none matches - /// - /// Throws [DocsErrorsException] on domain errors. - Document find(int id) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiDocsNotebookFind(_handle, id, outLen, err); - _checkDocsErrorsException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final decoded = _unpackDocument(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } - } - - /// The number of documents currently held - int size() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiDocsNotebookSize(_handle, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } - } - - /// The largest number of documents one notebook will hold - static int maxDocuments() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiDocsNotebookMaxDocuments(err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } - } -} - -typedef _NativeWeaveffiDocsCreateDocument = Pointer Function(Pointer, Pointer, Size, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiDocsCreateDocument = Pointer Function(Pointer, Pointer, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiDocsCreateDocument = _lib.lookupFunction< - _NativeWeaveffiDocsCreateDocument, _DartWeaveffiDocsCreateDocument>('weaveffi_docs_create_document'); - -/// Create a brand new document. -/// -/// The document starts in `Draft` status; callers must publish -/// it explicitly to make it visible. -Document createDocument(String title, List tags) { - final titlePtr = title.toNativeUtf8(); - final tagsWriter = _BufferWriter(); - final t0 = tags; - tagsWriter.writeLength(t0.length); - for (final t1 in t0) { - tagsWriter.writeString(t1); - } - final tagsBuf = tagsWriter.takeBytes(); - final tagsPtr = _stageBytes(tagsBuf); - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiDocsCreateDocument(titlePtr, tagsPtr, tagsBuf.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 = _unpackDocument(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(titlePtr); - calloc.free(tagsPtr); - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiDocsGetDocument = Pointer Function(Int64, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiDocsGetDocument = Pointer Function(int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiDocsGetDocument = _lib.lookupFunction< - _NativeWeaveffiDocsGetDocument, _DartWeaveffiDocsGetDocument>('weaveffi_docs_get_document'); - -/// Look up a document by identifier -Document? getDocument(int id) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiDocsGetDocument(id, 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() ? _unpackDocument(reader) : null); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeAsyncCb_weaveffi_docs_publish_async = Void Function(Pointer, Pointer<_WeaveFFIError>, Bool); - -typedef _NativeWeaveffiDocsPublishAsyncAsync = Void Function(Int64, Pointer>, Pointer); -typedef _DartWeaveffiDocsPublishAsyncAsync = void Function(int, Pointer>, Pointer); -final _weaveffiDocsPublishAsyncAsync = _lib.lookupFunction< - _NativeWeaveffiDocsPublishAsyncAsync, _DartWeaveffiDocsPublishAsyncAsync>('weaveffi_docs_publish_async_async'); - -/// Publish a document in the background -Future publishAsync(int id) { - final completer = Completer(); - late NativeCallable<_NativeAsyncCb_weaveffi_docs_publish_async> callable; - callable = NativeCallable<_NativeAsyncCb_weaveffi_docs_publish_async>.listener((Pointer context, Pointer<_WeaveFFIError> err, bool 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 { - _weaveffiDocsPublishAsyncAsync(id, callable.nativeFunction, nullptr); - } catch (e) { - callable.close(); - rethrow; - } - return completer.future; -} - -typedef _NativeWeaveffiDocsWatch = Void Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiDocsWatch = void Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiDocsWatch = _lib.lookupFunction< - _NativeWeaveffiDocsWatch, _DartWeaveffiDocsWatch>('weaveffi_docs_watch'); - -/// Register a listener for saved notifications -void watch(SavedListener listener) { - final err = calloc<_WeaveFFIError>(); - try { - _weaveffiDocsWatch(_registerCallback(listener), _SavedListenerVtable.cast(), err); - _checkError(err); - } finally { - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__pubspec_yaml.snap index 020ab447..973243cc 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__pubspec_yaml.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__pubspec_yaml.snap @@ -1,16 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -name: weaveffi +name: docs_everywhere version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI +description: 'Native bindings for docs_everywhere' + environment: - sdk: '>=3.1.0 <4.0.0' + sdk: '>=3.12.0 <4.0.0' + dependencies: - ffi: ^2.0.0 + ffi: ^2.1.0 # END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__README_md.snap index 732894ed..20d5449a 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__README_md.snap @@ -1,46 +1,39 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Dart) +# edge_cases (Dart) -Auto-generated Dart bindings using `dart:ffi`. +`dart:ffi` bindings for the `edge_cases` native library. ## 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: +Add this package as a dependency (a `path:` dependency works) and import it: ```dart -import 'package:weaveffi/weaveffi.dart'; +import 'package:edge_cases/edge_cases.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`. +The library loads `libedge_cases.dylib` (macOS), `libedge_cases.so` (Linux and Android), or +`edge_cases.dll` (Windows) from the platform's search path on first use. Set +`EDGE_CASES_LIBRARY` to a full path to load a specific build. On iOS, and as a macOS +fallback, the bindings look the symbols up in the running executable. ## Objects and callbacks -Interface objects are reference counted by the native library. Each Dart -wrapper holds one reference: call `dispose()` when you are done with it, or let -the garbage collector's finalizer release it. Objects nested inside records and -collections follow the same rule. +Each interface wrapper holds one native reference: call `dispose()` when +you're done, or let the garbage collector's finalizer release it. -Callback interfaces are abstract classes: implement one and pass an instance to -any function that takes it. Methods are bound with `NativeCallable.isolateLocal`, -so the native library may only invoke them on the isolate's own thread (which -holds when it calls them synchronously during a call from Dart). An exception -thrown by an implementation aborts the native call and surfaces to the original -Dart caller as a `WeaveFFIException` with `WeaveFFIException.foreignCode`. +Callback-interface methods that return a value run synchronously on the +isolate's thread; void methods may be called from any thread and are +delivered on the event loop. ## Requirements -- Dart SDK >= 3.1.0 (for `NativeCallable`) -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. +- Dart SDK `>=3.12.0 <4.0.0` +- The `ffi` package diff --git a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_edge_cases_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_edge_cases_dart.snap new file mode 100644 index 00000000..0ba27b38 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_edge_cases_dart.snap @@ -0,0 +1,2285 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io' show Platform; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +// ── Library loading ── +// The native library opens on first use and is verified before any other +// lookup: its C ABI revision and every module's contract checksum must match +// the values these bindings were generated against. + +/// The native library every binding in this file calls. +final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); + +DynamicLibrary _openLibrary() { + // An explicit path wins, so a caller can point at any build artifact. + final override = Platform.environment['EDGE_CASES_LIBRARY']; + if (override != null && override.isNotEmpty) { + return DynamicLibrary.open(override); + } + // iOS links native code into the app binary. + if (Platform.isIOS) return DynamicLibrary.process(); + final candidates = Platform.isMacOS + ? const ['libedge_cases.dylib'] + : Platform.isWindows + ? const ['edge_cases.dll'] + : const ['libedge_cases.so']; + ArgumentError? failure; + for (final candidate in candidates) { + try { + return DynamicLibrary.open(candidate); + } on ArgumentError catch (e) { + failure ??= e; + } + } + // A macOS app may link the library into its executable instead. + if (Platform.isMacOS) return DynamicLibrary.process(); + throw failure!; +} + +/// The C ABI revision these bindings were generated against. +const int _abiVersion = 3; + +/// Each top-level module's name, checksum symbol, and expected checksum. +const List<(String, String, int)> _contracts = <(String, String, int)>[ + ('edge_cases', 'edge_cases_edge_cases_checksum', 0x1567f441073bd4aa), +]; + +DynamicLibrary _verifyLibrary(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction( + 'edge_cases_abi_version'); + } on ArgumentError { + throw StateError( + "the native library 'edge_cases' isn't loaded; set EDGE_CASES_LIBRARY to " + 'its path'); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError('edge_cases: these bindings expect C ABI revision ' + '$_abiVersion, but the loaded library reports revision $found'); + } + for (final (module, symbol, expected) in _contracts) { + final int actual; + try { + actual = lib.lookupFunction(symbol)(); + } on ArgumentError { + throw StateError( + "edge_cases: the loaded library doesn't export module '$module'"); + } + if (actual != expected) { + throw StateError("edge_cases: module '$module' doesn't match the loaded " + 'library (contract checksum ${_hex(actual)}, expected ' + '${_hex(expected)}); regenerate the bindings'); + } + } + return lib; +} + +String _hex(int v) => '0x${v.toUnsigned(64).toRadixString(16).padLeft(16, '0')}'; + +// ── Errors and memory ── + +/// The C `edge_cases_error` slot every fallible call writes. A non-zero +/// `code` carries a producer-owned `message` and, for a domain error with +/// fields, a value-buffer `payload`; `edge_cases_error_clear` releases both. +final class _Error extends Struct { + @Int32() + external int code; + external Pointer message; + external Pointer payloadPtr; + @Size() + external int payloadLen; +} + +final _errorClear = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + 'edge_cases_error_clear', + isLeaf: true); + +final _freeBytes = _lib.lookupFunction, Size), + void Function(Pointer, int)>('edge_cases_free_bytes', isLeaf: true); + +// Synchronous calls on this isolate share one error slot and one length +// slot: each call reads (and clears) them right after it returns, before any +// other call can reuse them, including a nested call from a callback. +final Pointer<_Error> _err = calloc<_Error>(); +final Pointer _outLen = calloc(); + +/// A failure the native library reported. Domain errors extend this class +/// with a positive [code]; the negative runtime codes are the constants +/// below. +class NativeException implements Exception { + /// Creates an exception carrying [code] and [message]. + NativeException(this.code, this.message); + + /// The producer reported an untyped error. + static const int genericCode = -1; + + /// The producer panicked; [message] carries the panic text. + static const int panicCode = -2; + + /// An argument couldn't be lifted by the producer. + static const int marshalCode = -3; + + /// A callback implementation threw; [message] carries its text. + static const int foreignCode = -4; + + /// The call was cancelled (see [CancelledException]). + static const int cancelledCode = -5; + + /// The error code: positive for a domain error, negative for a runtime + /// failure. + final int code; + + /// The producer's message. + final String message; + + @override + String toString() => '$runtimeType($code): $message'; +} + +/// A cancellable call completed because its token was cancelled. +class CancelledException extends NativeException { + /// Creates a cancellation with the producer's [message]. + CancelledException([String message = 'cancelled']) + : super(NativeException.cancelledCode, message); +} + +typedef _ErrorMapper = NativeException Function( + int code, String message, Uint8List payload); + +/// Maps a runtime failure (or an undeclared code) onto its exception. +NativeException _runtimeError(int code, String message, Uint8List payload) => + code == NativeException.cancelledCode + ? CancelledException(message) + : NativeException(code, message); + +/// Builds the exception [err] reports, using [map] for domain codes. +NativeException _readError(Pointer<_Error> err, _ErrorMapper map) { + final e = err.ref; + final message = e.message == nullptr ? '' : e.message.toDartString(); + return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); +} + +/// Throws the error a synchronous call left in [err], clearing the slot. +void _check(Pointer<_Error> err, [_ErrorMapper map = _runtimeError]) { + if (err.ref.code == 0) return; + final error = _readError(err, map); + _errorClear(err); + throw error; +} + +/// Copies a borrowed native byte run into Dart memory. +Uint8List _copyBytes(Pointer ptr, int len) => + ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); + +/// Copies an owned native byte run and releases it. +Uint8List _takeBytes(Pointer ptr, int len) { + if (ptr == nullptr) return Uint8List(0); + final bytes = Uint8List.fromList(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return bytes; +} + +/// Decodes an owned UTF-8 run and releases it. +String _takeString(Pointer ptr, int len) { + if (ptr == nullptr) return ''; + final text = utf8.decode(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return text; +} + +/// Decodes a borrowed UTF-8 run. +String _readString(Pointer ptr, int len) => + ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); + +/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. +Pointer _stage(Arena arena, List bytes) { + if (bytes.isEmpty) return nullptr; + final ptr = arena(bytes.length); + ptr.asTypedList(bytes.length).setAll(0, bytes); + return ptr; +} + +// ── Objects ── + +/// The lifetime machinery every interface wrapper shares. A wrapper owns one +/// strong reference to its native object and releases it exactly once: in +/// [dispose], or through a GC finalizer when it's collected undisposed. +/// +/// Every call borrows the pointer through [_enter] and [_leave]. A [dispose] +/// that arrives while a call is in flight (from a callback the call made) +/// only marks the wrapper; the reference is released when the last call +/// leaves, so the native object never disappears under a running call. +abstract base class _NativeObject implements Finalizable { + _NativeObject(this._ptr) { + _finalizer.attach(this, _ptr, detach: this); + } + + final Pointer _ptr; + int _calls = 0; + bool _disposed = false; + + NativeFinalizer get _finalizer; + void _destroy(Pointer ptr); + Pointer _clone(Pointer ptr); + + /// Releases this wrapper's native reference. Safe to call more than once; + /// the native object is dropped when its last reference (this one, another + /// wrapper's, or the producer's) goes away. Using the wrapper afterwards + /// throws a [StateError]. + void dispose() { + if (_disposed) return; + _disposed = true; + if (_calls == 0) _release(); + } + + void _release() { + _finalizer.detach(this); + _destroy(_ptr); + } + + Pointer _enter() { + if (_disposed) throw StateError('$runtimeType used after dispose()'); + _calls++; + return _ptr; + } + + void _leave() { + if (--_calls == 0 && _disposed) _release(); + } + + /// A second strong reference, for an object token in a value buffer. + Pointer _cloneRef() { + final ptr = _enter(); + try { + return _clone(ptr); + } finally { + _leave(); + } + } +} + +/// Borrows [object]'s pointer until [arena] is released. +Pointer _borrow(Arena arena, _NativeObject object) { + final ptr = object._enter(); + arena.onReleaseAll(object._leave); + return ptr; +} + +// ── Value buffers ── +// Records, rich enums, optionals, lists, maps, and error payloads cross the +// C ABI serialized in this little-endian, packed format. A malformed buffer +// is a contract violation between producer and bindings, never a domain +// error. + +Never _bufferError(String context) => + throw StateError('malformed value buffer: $context'); + +final class _BufferWriter { + Uint8List _buf = Uint8List(64); + late ByteData _data = ByteData.sublistView(_buf); + int _len = 0; + + void _reserve(int extra) { + if (_len + extra <= _buf.length) return; + var cap = _buf.length * 2; + while (cap < _len + extra) { + cap *= 2; + } + _buf = Uint8List(cap)..setRange(0, _len, _buf); + _data = ByteData.sublistView(_buf); + } + + Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); + + void writeBool(bool v) => writeU8(v ? 1 : 0); + + void writeFlag(bool present) => writeU8(present ? 1 : 0); + + void writeI8(int v) { + _reserve(1); + _data.setInt8(_len, v); + _len += 1; + } + + void writeU8(int v) { + _reserve(1); + _data.setUint8(_len, v); + _len += 1; + } + + void writeI16(int v) { + _reserve(2); + _data.setInt16(_len, v, Endian.little); + _len += 2; + } + + void writeU16(int v) { + _reserve(2); + _data.setUint16(_len, v, Endian.little); + _len += 2; + } + + void writeI32(int v) { + _reserve(4); + _data.setInt32(_len, v, Endian.little); + _len += 4; + } + + void writeU32(int v) { + _reserve(4); + _data.setUint32(_len, v, Endian.little); + _len += 4; + } + + void writeI64(int v) { + _reserve(8); + _data.setInt64(_len, v, Endian.little); + _len += 8; + } + + // Dart ints are signed 64-bit; a u64 travels as its bit pattern. + void writeU64(int v) => writeI64(v); + + void writeF32(double v) { + _reserve(4); + _data.setFloat32(_len, v, Endian.little); + _len += 4; + } + + void writeF64(double v) { + _reserve(8); + _data.setFloat64(_len, v, Endian.little); + _len += 8; + } + + void writeLength(int v) => writeU32(v); + + void writeString(String v) => writeBytes(utf8.encode(v)); + + void writeBytes(List v) { + writeLength(v.length); + _reserve(v.length); + _buf.setRange(_len, _len + v.length, v); + _len += v.length; + } +} + +final class _BufferReader { + _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); + + final Uint8List _buf; + final ByteData _data; + int _pos = 0; + + int _take(int n, String context) { + if (_buf.length - _pos < n) _bufferError('truncated $context'); + final at = _pos; + _pos += n; + return at; + } + + bool readBool() { + final b = _buf[_take(1, 'bool')]; + if (b > 1) _bufferError('bool byte $b'); + return b == 1; + } + + bool readFlag() { + final b = _buf[_take(1, 'option flag')]; + if (b > 1) _bufferError('option flag $b'); + return b == 1; + } + + int readI8() => _data.getInt8(_take(1, 'i8')); + + int readU8() => _buf[_take(1, 'u8')]; + + int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); + + int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); + + int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); + + int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); + + int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); + + // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. + int readU64() => readI64(); + + double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); + + double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); + + int readLength() => readU32(); + + String readString() { + final n = readLength(); + final at = _take(n, 'string'); + return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); + } + + Uint8List readBytes() { + final n = readLength(); + final at = _take(n, 'bytes'); + return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); + } + + void expectEnd() { + if (_pos != _buf.length) _bufferError('trailing bytes'); + } +} + +/// Encodes one value with [write] into a fresh buffer. +Uint8List _encode(T value, void Function(_BufferWriter, T) write) { + final writer = _BufferWriter(); + write(writer, value); + return writer.takeBytes(); +} + +/// Decodes exactly one value with [read] from [bytes]. +T _decode(Uint8List bytes, T Function(_BufferReader) read) { + final reader = _BufferReader(bytes); + final value = read(reader); + reader.expectEnd(); + return value; +} + +// ── Async completion ── +// An async call completes through a `NativeCallable.listener` the producer +// invokes exactly once, from any thread. Everything the completion carries is +// owned by these bindings, so it's decoded later on the event loop and then +// released. + +final _errorFree = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + 'edge_cases_error_free'); + +/// Builds the exception a completion reports and frees its boxed error. +NativeException _takeAsyncError(Pointer<_Error> err, _ErrorMapper map) { + final error = _readError(err, map); + _errorFree(err); + return error; +} + +// ── Cancellation ── + +/// Cancels the cancellable calls it's passed to. One token can serve any +/// number of calls; once cancelled it stays cancelled, and a call that +/// receives a cancelled token completes with [CancelledException] without +/// waiting for its work. +final class CancelToken { + bool _cancelled = false; + final List _listeners = []; + + /// Whether [cancel] has been called. + bool get isCancelled => _cancelled; + + /// Requests cancellation of every call this token was passed to. Calls + /// that already completed are unaffected. Idempotent. + void cancel() { + if (_cancelled) return; + _cancelled = true; + final listeners = List.of(_listeners); + _listeners.clear(); + for (final listener in listeners) { + listener(); + } + } +} + +final _cancelTokenCreate = + _lib.lookupFunction Function(), Pointer Function()>( + 'edge_cases_cancel_token_create'); +final _cancelTokenCancel = _lib.lookupFunction), + void Function(Pointer)>('edge_cases_cancel_token_cancel'); +final _cancelTokenDestroy = _lib.lookupFunction), + void Function(Pointer)>('edge_cases_cancel_token_destroy'); + +/// The native token behind one launch: created for the call, cancelled when +/// the [CancelToken] is, and destroyed once the call completes. The producer +/// holds its own reference, so destroying ours never races the call. +final class _NativeCancel { + _NativeCancel._(this._token) : pointer = _cancelTokenCreate() { + if (_token.isCancelled) { + _cancelTokenCancel(pointer); + } else { + _token._listeners.add(_cancel); + } + } + + /// Binds a native token to [token], or returns null for no token. + static _NativeCancel? bind(CancelToken? token) => + token == null ? null : _NativeCancel._(token); + + final CancelToken _token; + + /// The `edge_cases_cancel_token*` passed to the launcher. + final Pointer pointer; + + void _cancel() => _cancelTokenCancel(pointer); + + /// Detaches from the [CancelToken] and drops the native reference. + void release() { + _token._listeners.remove(_cancel); + _cancelTokenDestroy(pointer); + } +} + +// ── Callback interfaces ── +// An implementation passed to the producer is parked in [_callbacks] under +// an integer key. The `ctx` the producer receives points at a small native +// [_CallbackContext] holding that key, so the producer never holds a Dart +// object and the GC never sees a raw pointer. The entry lives until the +// producer calls the vtable's `free`. +// +// Methods that return a value are `NativeCallable.isolateLocal` trampolines: +// they run synchronously, so the producer must call them on this isolate's +// thread while a call from Dart is in progress. Void methods and `free` are +// `NativeCallable.isolateGroupBound` forwarders, which may run on any thread: +// they copy their arguments into a message for this isolate (borrowed bytes +// are copied before the forwarder returns) and the implementation runs later +// on the event loop, in the zone that passed the callback. + +/// Where forwarders send messages: the allocator they build a message with +/// and the isolate port that receives it. Forwarders run outside the +/// isolate and can't read Dart globals, so everything they need is native. +final class _CallbackPost extends Struct { + external Pointer Function(Size)>> alloc; + external Pointer)>> release; + external Pointer)>> post; + @Int64() + external int port; +} + +/// The native `ctx` of one registered implementation. +final class _CallbackContext extends Struct { + @Int64() + external int key; + external Pointer<_CallbackPost> post; +} + +// The `Dart_CObject` layout `Dart_PostCObject` reads. +final class _CArray extends Struct { + @IntPtr() + external int length; + external Pointer> values; +} + +final class _CTypedData extends Struct { + @Int32() + external int type; + @IntPtr() + external int length; + external Pointer values; +} + +final class _CValue extends Union { + @Bool() + external bool asBool; + @Int64() + external int asInt64; + @Double() + external double asDouble; + external _CArray asArray; + external _CTypedData asTypedData; +} + +final class _CObject extends Struct { + @Int32() + external int type; + external _CValue value; +} + +typedef _CallbackDispatch = void Function( + Object impl, int method, List message); + +final class _CallbackEntry { + _CallbackEntry(this.impl, this.dispatch, this.context) : zone = Zone.current; + + final Object impl; + final _CallbackDispatch dispatch; + final Pointer<_CallbackContext> context; + final Zone zone; +} + +final Map _callbacks = {}; +int _nextCallbackKey = 1; + +/// Registers [impl] and returns the `ctx` the producer receives. +Pointer _registerCallback(Object impl, _CallbackDispatch dispatch) { + final key = _nextCallbackKey++; + final context = calloc<_CallbackContext>(); + context.ref + ..key = key + ..post = _callbackPost; + _callbacks[key] = _CallbackEntry(impl, dispatch, context); + return context.cast(); +} + +/// The implementation registered under [ctx]. +Object _callbackTarget(Pointer ctx) { + final key = ctx.cast<_CallbackContext>().ref.key; + final entry = _callbacks[key]; + if (entry == null) { + throw StateError('callback context $key is not registered'); + } + return entry.impl; +} + +final Pointer<_CallbackPost> _callbackPost = () { + final port = RawReceivePort(_receiveCallbackMessage, 'callback messages'); + port.keepIsolateAlive = false; + _callbackPort = port; + final allocator = Platform.isWindows + ? DynamicLibrary.open('ole32.dll') + : DynamicLibrary.process(); + final post = calloc<_CallbackPost>(); + post.ref + ..alloc = allocator.lookup(Platform.isWindows ? 'CoTaskMemAlloc' : 'malloc') + ..release = allocator.lookup(Platform.isWindows ? 'CoTaskMemFree' : 'free') + ..post = NativeApi.postCObject.cast() + ..port = port.sendPort.nativePort; + return post; +}(); + +RawReceivePort? _callbackPort; + +void _receiveCallbackMessage(Object? message) { + final fields = message! as List; + final key = fields[0]! as int; + final method = fields[1]! as int; + if (method == _CallbackMessage.free) { + final entry = _callbacks.remove(key); + if (entry != null) calloc.free(entry.context); + return; + } + final entry = _callbacks[key]; + if (entry == null) return; + entry.zone.runGuarded(() => entry.dispatch(entry.impl, method, fields)); +} + +/// One message from a forwarder: `[key, method, arguments...]`, built in +/// native memory and posted with `Dart_PostCObject`, which copies it. Runs +/// outside the isolate, so it touches no Dart globals. +final class _CallbackMessage { + _CallbackMessage(Pointer ctx, int method, int arguments) + : _context = ctx.cast<_CallbackContext>().ref, + _capacity = arguments + 2 { + final post = _context.post.ref; + final alloc = post.alloc.asFunction Function(int)>(); + _objects = alloc(sizeOf<_CObject>() * (_capacity + 1)).cast(); + _slots = alloc(sizeOf>() * _capacity).cast(); + int64(_context.key); + int64(method); + } + + static const int free = -1; + static const int _kBool = 1; + static const int _kInt64 = 3; + static const int _kDouble = 4; + static const int _kArray = 6; + static const int _kTypedData = 7; + static const int _kUint8 = 2; + + final _CallbackContext _context; + final int _capacity; + late final Pointer<_CObject> _objects; + late final Pointer> _slots; + int _count = 0; + + _CObject _next(int type) { + final object = _objects + (_count + 1); + _slots[_count++] = object; + return object.ref..type = type; + } + + void boolean(bool v) => _next(_kBool).value.asBool = v; + + void int64(int v) => _next(_kInt64).value.asInt64 = v; + + void float64(double v) => _next(_kDouble).value.asDouble = v; + + void pointer(Pointer v) => int64(v.address); + + /// A borrowed byte run; posting copies it. + void bytes(Pointer ptr, int len) { + final data = _next(_kTypedData).value.asTypedData; + data + ..type = _kUint8 + ..length = ptr == nullptr ? 0 : len + ..values = ptr; + } + + void send() { + final post = _context.post.ref; + _objects.ref.type = _kArray; + _objects.ref.value.asArray + ..length = _count + ..values = _slots; + post.post.asFunction)>()( + post.port, _objects); + final release = post.release.asFunction)>(); + release(_slots.cast()); + release(_objects.cast()); + } +} + +// The vtable `free` entry every callback interface shares. +void _forwardCallbackFree(Pointer ctx) => + _CallbackMessage(ctx, _CallbackMessage.free, 0).send(); + +final Pointer)>> _callbackFree = + _pin(NativeCallable)>.isolateGroupBound( + _forwardCallbackFree)); + +// Vtable entries live for the process: they're never closed, so they're +// anchored here, and they don't keep an idle isolate alive. +final List> _pinned = >[]; + +Pointer> _pin(NativeCallable callable) { + callable.keepIsolateAlive = false; + _pinned.add(callable); + return callable.nativeFunction; +} + +final _errorSet = _lib.lookupFunction< + Void Function(Pointer<_Error>, Int32, Pointer), + void Function(Pointer<_Error>, int, Pointer)>('edge_cases_error_set'); + +/// Reports an implementation's exception to the producer through +/// `edge_cases_error_set` with the foreign code (-4). Never throws: it runs +/// in the catch path of a trampoline the exception must not unwind through. +void _foreignError(Pointer<_Error> outErr, Object error) { + if (outErr == nullptr) return; + Pointer message = nullptr; + try { + message = error.toString().toNativeUtf8(allocator: calloc); + _errorSet(outErr, NativeException.foreignCode, message); + } catch (_) { + _errorSet(outErr, NativeException.foreignCode, nullptr); + } finally { + if (message != nullptr) calloc.free(message); + } +} + +// ── Iterators ── + +/// Anchors one live native iterator for its GC-finalizer backstop. A +/// suspended `sync*` body keeps its anchor reachable; abandoning the +/// iteration drops the body, and the finalizer destroys the native iterator. +/// An iteration that runs to completion (or fails) detaches and destroys it +/// eagerly instead, so the iterator is destroyed exactly once either way. +final class _IteratorAnchor implements Finalizable {} + +/// The `out_item` slot iterator steps write, wide enough for any element. +final Pointer _outItem = calloc(); + +/// The `EdgeErrors` error domain of module `edge_cases`. +class EdgeErrorsException extends NativeException { + /// Creates a domain error carrying [code] and [message]. + EdgeErrorsException(super.code, super.message); +} + +/// Resource is busy +class BusyException extends EdgeErrorsException { + BusyException([String message = 'Resource is busy']) : super(7, message); +} + +NativeException _mapEdgeErrorsException(int code, String message, Uint8List payload) { + switch (code) { + case 7: + return BusyException(message); + default: + return _runtimeError(code, message, payload); + } +} + +/// Deprecated enum with non-contiguous values and keyword-like variants +@Deprecated('Use Mode instead') +enum Legacy { + none(0), + default_(5), + class_(10); + + const Legacy(this.value); + + /// The C discriminant. + final int value; + + /// The variant whose discriminant is [value]. + static Legacy fromValue(int value) => values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError.value(value, 'value', 'not a Legacy discriminant')); +} + +enum Mode { + fast(1), + safe(2); + + const Mode(this.value); + + /// The C discriminant. + final int value; + + /// The variant whose discriminant is [value]. + static Mode fromValue(int value) => values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError.value(value, 'value', 'not a Mode discriminant')); +} + +class Leaf { + /// Creates a [Leaf]. + Leaf({required this.weight}); + + final double weight; +} + +void _packLeaf(_BufferWriter w, Leaf v) { + w.writeF32(v.weight); +} + +Leaf _unpackLeaf(_BufferReader r) => Leaf( + weight: r.readF32(), +); + +/// Middle layer of a three-deep struct nesting +class Inner { + /// Creates an [Inner]. + Inner({required this.leaf, required this.leaves}); + + final Leaf leaf; + final List 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) => Inner( + leaf: _unpackLeaf(r), + leaves: [for (var i = r.readLength(); i > 0; i--) _unpackLeaf(r)], +); + +class Outer { + /// Creates an [Outer]. + Outer({required this.inner, this.maybeInner, required this.byName, required this.modes, required this.matrix, required this.sparse, this.blob, this.note, required this.byId, required this.hidden, required this.v2}); + + final Inner inner; + final Inner? maybeInner; + final Map byName; + final List modes; + final List> matrix; + final List sparse; + final List? blob; + final String? note; + final Map byId; + final bool hidden; + final int v2; +} + +void _packOuter(_BufferWriter w, Outer v) { + _packInner(w, v.inner); + final _t0 = v.maybeInner; + w.writeFlag(_t0 != null); + if (_t0 != null) { + _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.writeI32(_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.writeI32(_t8); + } + } + final _t9 = v.sparse; + w.writeLength(_t9.length); + for (final _t10 in _t9) { + final _t11 = _t10; + w.writeFlag(_t11 != null); + if (_t11 != null) { + w.writeI64(_t11); + } + } + final _t12 = v.blob; + w.writeFlag(_t12 != null); + if (_t12 != null) { + w.writeBytes(_t12); + } + final _t13 = v.note; + w.writeFlag(_t13 != null); + if (_t13 != null) { + w.writeString(_t13); + } + final _t14 = v.byId; + w.writeLength(_t14.length); + for (final _t15 in _t14.entries) { + w.writeI64(_t15.key); + w.writeString(_t15.value); + } + w.writeBool(v.hidden); + w.writeU8(v.v2); +} + +Outer _unpackOuter(_BufferReader r) => Outer( + inner: _unpackInner(r), + maybeInner: (r.readFlag() ? _unpackInner(r) : null), + byName: {for (var i = r.readLength(); i > 0; i--) r.readString(): _unpackInner(r)}, + modes: [for (var i = r.readLength(); i > 0; i--) Mode.fromValue(r.readI32())], + matrix: >[for (var i = r.readLength(); i > 0; i--) [for (var i = r.readLength(); i > 0; i--) r.readI32()]], + sparse: [for (var i = r.readLength(); i > 0; i--) (r.readFlag() ? r.readI64() : null)], + blob: (r.readFlag() ? r.readBytes() : null), + note: (r.readFlag() ? r.readString() : null), + byId: {for (var i = r.readLength(); i > 0; i--) r.readI64(): r.readString()}, + hidden: r.readBool(), + v2: r.readU8(), +); + +/// Every field is a reserved word somewhere +class Keywords { + /// Creates a [Keywords]. + Keywords({required this.class_, required this.def, required this.import, required this.from, required this.func, required this.range, required this.register, required this.volatile, required this.new_, required this.default_, required this.end, required this.in_, required this.is_, required this.int_, required this.var_, required this.val, required this.self, required this.this_, required this.object, required this.string, required this.case_, required this.select, required this.switch_, required this.void_, required this.union, required this.namespace, required this.operator, required this.template, required this.package, required this.protocol, required this.guard, required this.defer, required this.inout, required this.internal, required this.when, required this.fun, required this.data, required this.late, required this.part, required this.out, required this.ref, required this.event, required this.base, required this.lambda, required this.yield, required this.nil, required this.null_, required this.begin, required this.rescue, required this.module, required this.chan, required this.go, required this.let, required this.export, required this.delete, required this.typedef, required this.sizeof, required this.static, required this.super_}); + + final int class_; + final int def; + final int import; + final int from; + final int func; + final int range; + final int register; + final int volatile; + final int new_; + final int default_; + final int end; + final int in_; + final int is_; + final int int_; + final int var_; + final int val; + final int self; + final int this_; + final int object; + final int string; + final int case_; + final int select; + final int switch_; + final int void_; + final int union; + final int namespace; + final int operator; + final int template; + final int package; + final int protocol; + final int guard; + final int defer; + final int inout; + final int internal; + final int when; + final int fun; + final int data; + final int late; + final int part; + final int out; + final int ref; + final int event; + final int base; + final int lambda; + final int yield; + final int nil; + final int null_; + final int begin; + final int rescue; + final int module; + final int chan; + final int go; + final int let; + final int export; + final int delete; + final int typedef; + final int sizeof; + final int static; + final int super_; +} + +void _packKeywords(_BufferWriter w, Keywords v) { + w.writeI32(v.class_); + w.writeI32(v.def); + w.writeI32(v.import); + w.writeI32(v.from); + w.writeI32(v.func); + w.writeI32(v.range); + w.writeI32(v.register); + w.writeI32(v.volatile); + w.writeI32(v.new_); + w.writeI32(v.default_); + w.writeI32(v.end); + w.writeI32(v.in_); + w.writeI32(v.is_); + w.writeI32(v.int_); + w.writeI32(v.var_); + w.writeI32(v.val); + w.writeI32(v.self); + w.writeI32(v.this_); + w.writeI32(v.object); + w.writeI32(v.string); + w.writeI32(v.case_); + w.writeI32(v.select); + w.writeI32(v.switch_); + w.writeI32(v.void_); + w.writeI32(v.union); + w.writeI32(v.namespace); + w.writeI32(v.operator); + w.writeI32(v.template); + w.writeI32(v.package); + w.writeI32(v.protocol); + w.writeI32(v.guard); + w.writeI32(v.defer); + w.writeI32(v.inout); + w.writeI32(v.internal); + w.writeI32(v.when); + w.writeI32(v.fun); + w.writeI32(v.data); + w.writeI32(v.late); + w.writeI32(v.part); + w.writeI32(v.out); + w.writeI32(v.ref); + w.writeI32(v.event); + w.writeI32(v.base); + w.writeI32(v.lambda); + w.writeI32(v.yield); + w.writeI32(v.nil); + w.writeI32(v.null_); + w.writeI32(v.begin); + w.writeI32(v.rescue); + w.writeI32(v.module); + w.writeI32(v.chan); + w.writeI32(v.go); + w.writeI32(v.let); + w.writeI32(v.export); + w.writeI32(v.delete); + w.writeI32(v.typedef); + w.writeI32(v.sizeof); + w.writeI32(v.static); + w.writeI32(v.super_); +} + +Keywords _unpackKeywords(_BufferReader r) => Keywords( + class_: r.readI32(), + def: r.readI32(), + import: r.readI32(), + from: r.readI32(), + func: r.readI32(), + range: r.readI32(), + register: r.readI32(), + volatile: r.readI32(), + new_: r.readI32(), + default_: r.readI32(), + end: r.readI32(), + in_: r.readI32(), + is_: r.readI32(), + int_: r.readI32(), + var_: r.readI32(), + val: r.readI32(), + self: r.readI32(), + this_: r.readI32(), + object: r.readI32(), + string: r.readI32(), + case_: r.readI32(), + select: r.readI32(), + switch_: r.readI32(), + void_: r.readI32(), + union: r.readI32(), + namespace: r.readI32(), + operator: r.readI32(), + template: r.readI32(), + package: r.readI32(), + protocol: r.readI32(), + guard: r.readI32(), + defer: r.readI32(), + inout: r.readI32(), + internal: r.readI32(), + when: r.readI32(), + fun: r.readI32(), + data: r.readI32(), + late: r.readI32(), + part: r.readI32(), + out: r.readI32(), + ref: r.readI32(), + event: r.readI32(), + base: r.readI32(), + lambda: r.readI32(), + yield: r.readI32(), + nil: r.readI32(), + null_: r.readI32(), + begin: r.readI32(), + rescue: r.readI32(), + module: r.readI32(), + chan: r.readI32(), + go: r.readI32(), + let: r.readI32(), + export: r.readI32(), + delete: r.readI32(), + typedef: r.readI32(), + sizeof: r.readI32(), + static: r.readI32(), + super_: r.readI32(), +); + +/// Deprecated struct +@Deprecated('Use Outer instead') +class Retired { + /// Creates a [Retired]. + Retired({required this.id}); + + final int id; +} + +void _packRetired(_BufferWriter w, Retired v) { + w.writeI64(v.id); +} + +Retired _unpackRetired(_BufferReader r) => Retired( + id: r.readI64(), +); + +/// Record carrying objects in every buffered position +class WidgetHolder { + /// Creates a [WidgetHolder]. + WidgetHolder({required this.primary, this.fallback, required this.extras, required this.byName}); + + final Widget primary; + final Widget? fallback; + final List extras; + final Map byName; +} + +void _packWidgetHolder(_BufferWriter w, WidgetHolder v) { + w.writeU64(v.primary._cloneRef().address); + final _t0 = v.fallback; + w.writeFlag(_t0 != null); + if (_t0 != null) { + w.writeU64(_t0._cloneRef().address); + } + final _t1 = v.extras; + w.writeLength(_t1.length); + for (final _t2 in _t1) { + w.writeU64(_t2._cloneRef().address); + } + final _t3 = v.byName; + w.writeLength(_t3.length); + for (final _t4 in _t3.entries) { + w.writeString(_t4.key); + w.writeU64(_t4.value._cloneRef().address); + } +} + +WidgetHolder _unpackWidgetHolder(_BufferReader r) => WidgetHolder( + primary: Widget._(Pointer.fromAddress(r.readU64())), + fallback: (r.readFlag() ? Widget._(Pointer.fromAddress(r.readU64())) : null), + extras: [for (var i = r.readLength(); i > 0; i--) Widget._(Pointer.fromAddress(r.readU64()))], + byName: {for (var i = r.readLength(); i > 0; i--) r.readString(): Widget._(Pointer.fromAddress(r.readU64()))}, +); + +/// Callback interface carrying composite payloads and objects +/// +/// A callback interface: implement it and pass an instance to any +/// function taking a [BatchListener]. The library keeps the instance alive +/// until the producer releases it. +/// +/// Methods that return a value run synchronously and must be called by +/// the producer on this isolate's thread during a call from Dart; a +/// thrown exception fails that call with [NativeException.foreignCode]. +/// Void methods may be called from any thread: they're delivered +/// asynchronously on this isolate's event loop, in the zone that passed +/// the instance, where a thrown exception is an uncaught error of that +/// zone. Object arguments are owned by the implementation. +abstract class BatchListener { + /// Composite payloads cross as value buffers + void onBatch(List items, List raw, Outer? first, Mode mode); + + /// Objects cross as strong references the consumer must release + void onWidget(Widget widget, Widget? fallback, List children); + + void onTick(); + + /// Scalar return travels back through the vtable + bool shouldContinue(int class_); + + /// Enum return travels back as its integer discriminant + Mode pickMode(); +} + +typedef _EdgeCasesEdgeCasesBatchListenerOnBatch = Void Function(Pointer, Pointer, Size, Pointer, Size, Pointer, Size, Int32, Pointer<_Error>); +typedef _EdgeCasesEdgeCasesBatchListenerOnWidget = Void Function(Pointer, Pointer, Pointer, Pointer, Size, Pointer<_Error>); +typedef _EdgeCasesEdgeCasesBatchListenerOnTick = Void Function(Pointer, Pointer<_Error>); +typedef _EdgeCasesEdgeCasesBatchListenerShouldContinue = Bool Function(Pointer, Int32, Pointer<_Error>); +typedef _EdgeCasesEdgeCasesBatchListenerPickMode = Int32 Function(Pointer, Pointer<_Error>); + +// The C `edge_cases_edge_cases_BatchListener_vtable` layout. +final class _BatchListenerVtable extends Struct { + external Pointer> onBatch; + external Pointer> onWidget; + external Pointer> onTick; + external Pointer> shouldContinue; + external Pointer> pickMode; + external Pointer)>> free; +} + +// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.on_batch`: forwarded to the isolate. +void _batchListenerOnBatch(Pointer ctx, Pointer itemsPtr, int itemsLen, Pointer rawPtr, int rawLen, Pointer firstPtr, int firstLen, int mode, Pointer<_Error> outErr) { + _CallbackMessage(ctx, 0, 4) + ..bytes(itemsPtr, itemsLen) + ..bytes(rawPtr, rawLen) + ..bytes(firstPtr, firstLen) + ..int64(mode) + ..send(); +} + +// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.on_widget`: forwarded to the isolate. +void _batchListenerOnWidget(Pointer ctx, Pointer widget, Pointer fallback, Pointer childrenPtr, int childrenLen, Pointer<_Error> outErr) { + _CallbackMessage(ctx, 1, 3) + ..pointer(widget) + ..pointer(fallback) + ..bytes(childrenPtr, childrenLen) + ..send(); +} + +// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.on_tick`: forwarded to the isolate. +void _batchListenerOnTick(Pointer ctx, Pointer<_Error> outErr) { + _CallbackMessage(ctx, 2, 0) + ..send(); +} + +// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.should_continue`. +bool _batchListenerShouldContinue(Pointer ctx, int class_, Pointer<_Error> outErr) { + try { + return (_callbackTarget(ctx) as BatchListener).shouldContinue(class_); + } catch (e) { + _foreignError(outErr, e); + return false; + } +} + +// Vtable entry `edge_cases_edge_cases_BatchListener_vtable.pick_mode`. +int _batchListenerPickMode(Pointer ctx, Pointer<_Error> outErr) { + try { + return (_callbackTarget(ctx) as BatchListener).pickMode().value; + } catch (e) { + _foreignError(outErr, e); + return 0; + } +} + +void _batchListenerDispatch(Object impl, int method, List message) { + final target = impl as BatchListener; + switch (method) { + case 0: { + final _a2 = _decode(message[2]! as Uint8List, (r) => [for (var i = r.readLength(); i > 0; i--) _unpackOuter(r)]); + final _a3 = message[3]! as Uint8List; + final _a4 = _decode(message[4]! as Uint8List, (r) => (r.readFlag() ? _unpackOuter(r) : null)); + final _a5 = Mode.fromValue(message[5]! as int); + target.onBatch(_a2, _a3, _a4, _a5); + } + case 1: { + final _a2Address = message[2]! as int; + final _a2 = Widget._(Pointer.fromAddress(_a2Address)); + final _a3Address = message[3]! as int; + final _a3 = _a3Address == 0 ? null : Widget._(Pointer.fromAddress(_a3Address)); + final _a4 = _decode(message[4]! as Uint8List, (r) => [for (var i = r.readLength(); i > 0; i--) Widget._(Pointer.fromAddress(r.readU64()))]); + target.onWidget(_a2, _a3, _a4); + } + case 2: { + target.onTick(); + } + } +} + +final Pointer<_BatchListenerVtable> _batchListenerVtable = () { + final vtable = calloc<_BatchListenerVtable>(); + vtable.ref + ..onBatch = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerOnBatch>.isolateGroupBound(_batchListenerOnBatch)) + ..onWidget = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerOnWidget>.isolateGroupBound(_batchListenerOnWidget)) + ..onTick = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerOnTick>.isolateGroupBound(_batchListenerOnTick)) + ..shouldContinue = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerShouldContinue>.isolateLocal(_batchListenerShouldContinue, exceptionalReturn: false)) + ..pickMode = _pin(NativeCallable<_EdgeCasesEdgeCasesBatchListenerPickMode>.isolateLocal(_batchListenerPickMode, exceptionalReturn: 0)) + ..free = _callbackFree; + return vtable; +}(); + +/// Callback interface whose name and methods are keywords +/// +/// A callback interface: implement it and pass an instance to any +/// function taking a [Keyword]. The library keeps the instance alive +/// until the producer releases it. +/// +/// Methods that return a value run synchronously and must be called by +/// the producer on this isolate's thread during a call from Dart; a +/// thrown exception fails that call with [NativeException.foreignCode]. +/// Void methods may be called from any thread: they're delivered +/// asynchronously on this isolate's event loop, in the zone that passed +/// the instance, where a thrown exception is an uncaught error of that +/// zone. Object arguments are owned by the implementation. +abstract class Keyword { + void delete(int self); + + int import(); +} + +typedef _EdgeCasesEdgeCasesKeywordDelete = Void Function(Pointer, Int32, Pointer<_Error>); +typedef _EdgeCasesEdgeCasesKeywordImport = Int32 Function(Pointer, Pointer<_Error>); + +// The C `edge_cases_edge_cases_Keyword_vtable` layout. +final class _KeywordVtable extends Struct { + external Pointer> delete; + external Pointer> import; + external Pointer)>> free; +} + +// Vtable entry `edge_cases_edge_cases_Keyword_vtable.delete`: forwarded to the isolate. +void _keywordDelete(Pointer ctx, int self, Pointer<_Error> outErr) { + _CallbackMessage(ctx, 0, 1) + ..int64(self) + ..send(); +} + +// Vtable entry `edge_cases_edge_cases_Keyword_vtable.import`. +int _keywordImport(Pointer ctx, Pointer<_Error> outErr) { + try { + return (_callbackTarget(ctx) as Keyword).import(); + } catch (e) { + _foreignError(outErr, e); + return 0; + } +} + +void _keywordDispatch(Object impl, int method, List message) { + final target = impl as Keyword; + switch (method) { + case 0: { + final _a2 = message[2]! as int; + target.delete(_a2); + } + } +} + +final Pointer<_KeywordVtable> _keywordVtable = () { + final vtable = calloc<_KeywordVtable>(); + vtable.ref + ..delete = _pin(NativeCallable<_EdgeCasesEdgeCasesKeywordDelete>.isolateGroupBound(_keywordDelete)) + ..import = _pin(NativeCallable<_EdgeCasesEdgeCasesKeywordImport>.isolateLocal(_keywordImport, exceptionalReturn: 0)) + ..free = _callbackFree; + return vtable; +}(); + +final _edgeCasesEdgeCasesWidgetClone = _lib.lookupFunction< + Pointer Function(Pointer), + Pointer Function(Pointer)>('edge_cases_edge_cases_Widget_clone'); + +final _edgeCasesEdgeCasesWidgetDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('edge_cases_edge_cases_Widget_destroy'); +final _edgeCasesEdgeCasesWidgetDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('edge_cases_edge_cases_Widget_destroy')); + +final _edgeCasesEdgeCasesWidgetRefresh = _lib.lookupFunction< + Void Function(Pointer, Pointer<_Error>), + void Function(Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Widget_refresh'); + +final _edgeCasesEdgeCasesWidgetRename = _lib.lookupFunction< + Pointer Function(Pointer, Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Widget_rename'); + +typedef _EdgeCasesEdgeCasesWidgetFetchCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _edgeCasesEdgeCasesWidgetFetch = _lib.lookupFunction< + Void Function(Pointer, Int64, Pointer>, Pointer), + void Function(Pointer, int, Pointer>, Pointer)>('edge_cases_edge_cases_Widget_fetch'); + +final _edgeCasesEdgeCasesWidgetOldName = _lib.lookupFunction< + Pointer Function(Pointer, Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Widget_old_name'); + +/// Interface with no constructor; instances come from make_widget +/// +/// A reference-counted native object. Each instance holds one strong +/// reference, released by [dispose] or, when an undisposed instance is +/// collected, by a finalizer. +final class Widget extends _NativeObject { + Widget._(super._ptr); + + @override + NativeFinalizer get _finalizer => _edgeCasesEdgeCasesWidgetDestroyFinalizer; + + @override + void _destroy(Pointer ptr) => _edgeCasesEdgeCasesWidgetDestroy(ptr); + + @override + Pointer _clone(Pointer ptr) => _edgeCasesEdgeCasesWidgetClone(ptr); + + /// Void method that can fail + /// + /// Throws [EdgeErrorsException] on domain errors. + void refresh() { + final _self = _enter(); + try { + _edgeCasesEdgeCasesWidgetRefresh(_self, _err); + _check(_err, _mapEdgeErrorsException); + } finally { + _leave(); + } + } + + String rename(String class_, String? import) { + final _self = _enter(); + final _arena = Arena(); + try { + final _class_Bytes = utf8.encode(class_); + final _importWriter = _BufferWriter(); + final _t0 = import; + _importWriter.writeFlag(_t0 != null); + if (_t0 != null) { + _importWriter.writeString(_t0); + } + final _importBytes = _importWriter.takeBytes(); + final _result = _edgeCasesEdgeCasesWidgetRename(_self, _stage(_arena, _class_Bytes), _class_Bytes.length, _stage(_arena, _importBytes), _importBytes.length, _outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); + } finally { + _arena.releaseAll(); + _leave(); + } + } + + /// Async method with a struct result + /// + /// Throws [EdgeErrorsException] on domain errors. + Future fetch(int id) { + final _self = _enter(); + final _completer = Completer(); + late final NativeCallable<_EdgeCasesEdgeCasesWidgetFetchCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesWidgetFetchCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value, int _valueLen) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _mapEdgeErrorsException); + _completer.complete(_decode(_takeBytes(_value, _valueLen), _unpackOuter)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _edgeCasesEdgeCasesWidgetFetch(_self, id, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } finally { + _leave(); + } + return _completer.future; + } + + @Deprecated('Use rename instead') + String oldName() { + final _self = _enter(); + try { + final _result = _edgeCasesEdgeCasesWidgetOldName(_self, _outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); + } finally { + _leave(); + } + } +} + +final _edgeCasesEdgeCasesVaultClone = _lib.lookupFunction< + Pointer Function(Pointer), + Pointer Function(Pointer)>('edge_cases_edge_cases_Vault_clone'); + +final _edgeCasesEdgeCasesVaultDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('edge_cases_edge_cases_Vault_destroy'); +final _edgeCasesEdgeCasesVaultDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('edge_cases_edge_cases_Vault_destroy')); + +final _edgeCasesEdgeCasesVaultOpen = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer<_Error>)>('edge_cases_edge_cases_Vault_open'); + +final _edgeCasesEdgeCasesVaultPeek = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('edge_cases_edge_cases_Vault_peek'); + +final _edgeCasesEdgeCasesVaultProbe = _lib.lookupFunction< + Bool Function(Pointer<_Error>), + bool Function(Pointer<_Error>)>('edge_cases_edge_cases_Vault_probe'); + +/// Deprecated interface with a throwing constructor +/// +/// A reference-counted native object. Each instance holds one strong +/// reference, released by [dispose] or, when an undisposed instance is +/// collected, by a finalizer. +@Deprecated('Use Widget instead') +final class Vault extends _NativeObject { + Vault._(super._ptr); + + @override + NativeFinalizer get _finalizer => _edgeCasesEdgeCasesVaultDestroyFinalizer; + + @override + void _destroy(Pointer ptr) => _edgeCasesEdgeCasesVaultDestroy(ptr); + + @override + Pointer _clone(Pointer ptr) => _edgeCasesEdgeCasesVaultClone(ptr); + + /// Fails when the vault is busy + /// + /// The returned object is owned by the caller: call its `dispose()` + /// when you're done with it. + /// + /// Throws [EdgeErrorsException] on domain errors. + factory Vault.open(String path) { + final _arena = Arena(); + try { + final _pathBytes = utf8.encode(path); + final _result = _edgeCasesEdgeCasesVaultOpen(_stage(_arena, _pathBytes), _pathBytes.length, _err); + _check(_err, _mapEdgeErrorsException); + return Vault._(_result); + } finally { + _arena.releaseAll(); + } + } + + /// Returns null when the producer reports no object. A returned object + /// is owned by the caller: call its `dispose()` when you're done with it. + Widget? peek() { + final _self = _enter(); + try { + final _result = _edgeCasesEdgeCasesVaultPeek(_self, _err); + _check(_err); + return _result == nullptr ? null : Widget._(_result); + } finally { + _leave(); + } + } + + /// Throwing static + /// + /// Throws [EdgeErrorsException] on domain errors. + static bool probe() { + final _result = _edgeCasesEdgeCasesVaultProbe(_err); + _check(_err, _mapEdgeErrorsException); + return _result; + } +} + +final _edgeCasesEdgeCasesNoop = _lib.lookupFunction< + Void Function(Pointer<_Error>), + void Function(Pointer<_Error>)>('edge_cases_edge_cases_noop'); + +/// No parameters and no result +void noop() { + _edgeCasesEdgeCasesNoop(_err); + _check(_err); +} + +final _edgeCasesEdgeCasesSelect = _lib.lookupFunction< + Int32 Function(Int32, Pointer, Size, Pointer, Size, Pointer, Size, Bool, Pointer<_Error>), + int Function(int, Pointer, int, Pointer, int, Pointer, int, bool, Pointer<_Error>)>('edge_cases_edge_cases_select'); + +/// Function named after a keyword whose parameters are all keywords +int select(int class_, String from, List func, int? range, bool type) { + final _arena = Arena(); + try { + final _fromBytes = utf8.encode(from); + final _funcWriter = _BufferWriter(); + final _t0 = func; + _funcWriter.writeLength(_t0.length); + for (final _t1 in _t0) { + _funcWriter.writeString(_t1); + } + final _funcBytes = _funcWriter.takeBytes(); + final _rangeWriter = _BufferWriter(); + final _t2 = range; + _rangeWriter.writeFlag(_t2 != null); + if (_t2 != null) { + _rangeWriter.writeI32(_t2); + } + final _rangeBytes = _rangeWriter.takeBytes(); + final _result = _edgeCasesEdgeCasesSelect(class_, _stage(_arena, _fromBytes), _fromBytes.length, _stage(_arena, _funcBytes), _funcBytes.length, _stage(_arena, _rangeBytes), _rangeBytes.length, type, _err); + _check(_err); + return _result; + } finally { + _arena.releaseAll(); + } +} + +final _edgeCasesEdgeCasesNestedOptional = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_nested_optional'); + +/// Optional list of optional structs, the deepest legal nesting +List? nestedOptional(List? items) { + final _arena = Arena(); + try { + final _itemsWriter = _BufferWriter(); + final _t0 = items; + _itemsWriter.writeFlag(_t0 != null); + if (_t0 != null) { + final _t1 = _t0; + _itemsWriter.writeLength(_t1.length); + for (final _t2 in _t1) { + final _t3 = _t2; + _itemsWriter.writeFlag(_t3 != null); + if (_t3 != null) { + _packOuter(_itemsWriter, _t3); + } + } + } + final _itemsBytes = _itemsWriter.takeBytes(); + final _result = _edgeCasesEdgeCasesNestedOptional(_stage(_arena, _itemsBytes), _itemsBytes.length, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), (r) => (r.readFlag() ? [for (var i = r.readLength(); i > 0; i--) (r.readFlag() ? _unpackOuter(r) : null)] : null)); + } finally { + _arena.releaseAll(); + } +} + +final _edgeCasesEdgeCasesOptionalParams = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_optional_params'); + +Mode? optionalParams(String? text, int? count, Outer? outer, List? tags, List? blob, Mode? mode) { + final _arena = Arena(); + try { + final _textWriter = _BufferWriter(); + final _t0 = text; + _textWriter.writeFlag(_t0 != null); + if (_t0 != null) { + _textWriter.writeString(_t0); + } + final _textBytes = _textWriter.takeBytes(); + final _countWriter = _BufferWriter(); + final _t1 = count; + _countWriter.writeFlag(_t1 != null); + if (_t1 != null) { + _countWriter.writeI32(_t1); + } + final _countBytes = _countWriter.takeBytes(); + final _outerWriter = _BufferWriter(); + final _t2 = outer; + _outerWriter.writeFlag(_t2 != null); + if (_t2 != null) { + _packOuter(_outerWriter, _t2); + } + final _outerBytes = _outerWriter.takeBytes(); + final _tagsWriter = _BufferWriter(); + final _t3 = tags; + _tagsWriter.writeFlag(_t3 != null); + if (_t3 != null) { + final _t4 = _t3; + _tagsWriter.writeLength(_t4.length); + for (final _t5 in _t4) { + _tagsWriter.writeString(_t5); + } + } + final _tagsBytes = _tagsWriter.takeBytes(); + final _blobWriter = _BufferWriter(); + final _t6 = blob; + _blobWriter.writeFlag(_t6 != null); + if (_t6 != null) { + _blobWriter.writeBytes(_t6); + } + final _blobBytes = _blobWriter.takeBytes(); + final _modeWriter = _BufferWriter(); + final _t7 = mode; + _modeWriter.writeFlag(_t7 != null); + if (_t7 != null) { + _modeWriter.writeI32(_t7.value); + } + final _modeBytes = _modeWriter.takeBytes(); + final _result = _edgeCasesEdgeCasesOptionalParams(_stage(_arena, _textBytes), _textBytes.length, _stage(_arena, _countBytes), _countBytes.length, _stage(_arena, _outerBytes), _outerBytes.length, _stage(_arena, _tagsBytes), _tagsBytes.length, _stage(_arena, _blobBytes), _blobBytes.length, _stage(_arena, _modeBytes), _modeBytes.length, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), (r) => (r.readFlag() ? Mode.fromValue(r.readI32()) : null)); + } finally { + _arena.releaseAll(); + } +} + +final _edgeCasesEdgeCasesRegisterBatch = _lib.lookupFunction< + Void Function(Pointer, Pointer, Pointer, Pointer, Pointer<_Error>), + void Function(Pointer, Pointer, Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_register_batch'); + +/// Two callback interface parameters on one function +void registerBatch(BatchListener listener, Keyword keyword) { + final _listenerCtx = _registerCallback(listener, _batchListenerDispatch); + final _keywordCtx = _registerCallback(keyword, _keywordDispatch); + _edgeCasesEdgeCasesRegisterBatch(_listenerCtx, _batchListenerVtable.cast(), _keywordCtx, _keywordVtable.cast(), _err); + _check(_err); +} + +final _edgeCasesEdgeCasesWidgetsEverywhere = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_widgets_everywhere'); + +/// Objects in every buffered position +WidgetHolder widgetsEverywhere(List list, Map byName, List sparse, WidgetHolder holder) { + final _arena = Arena(); + try { + final _listWriter = _BufferWriter(); + final _t0 = list; + _listWriter.writeLength(_t0.length); + for (final _t1 in _t0) { + _listWriter.writeU64(_t1._cloneRef().address); + } + final _listBytes = _listWriter.takeBytes(); + final _byNameWriter = _BufferWriter(); + final _t2 = byName; + _byNameWriter.writeLength(_t2.length); + for (final _t3 in _t2.entries) { + _byNameWriter.writeString(_t3.key); + _byNameWriter.writeU64(_t3.value._cloneRef().address); + } + final _byNameBytes = _byNameWriter.takeBytes(); + final _sparseWriter = _BufferWriter(); + final _t4 = sparse; + _sparseWriter.writeLength(_t4.length); + for (final _t5 in _t4) { + final _t6 = _t5; + _sparseWriter.writeFlag(_t6 != null); + if (_t6 != null) { + _sparseWriter.writeU64(_t6._cloneRef().address); + } + } + final _sparseBytes = _sparseWriter.takeBytes(); + final _holderWriter = _BufferWriter(); + _packWidgetHolder(_holderWriter, holder); + final _holderBytes = _holderWriter.takeBytes(); + final _result = _edgeCasesEdgeCasesWidgetsEverywhere(_stage(_arena, _listBytes), _listBytes.length, _stage(_arena, _byNameBytes), _byNameBytes.length, _stage(_arena, _sparseBytes), _sparseBytes.length, _stage(_arena, _holderBytes), _holderBytes.length, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackWidgetHolder); + } finally { + _arena.releaseAll(); + } +} + +final _edgeCasesEdgeCasesWidgetStream = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('edge_cases_edge_cases_widget_stream'); + +final _edgeCasesEdgeCasesWidgetStreamIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer>, Pointer<_Error>), + int Function(Pointer, Pointer>, Pointer<_Error>)>('edge_cases_edge_cases_WidgetStreamIterator_next'); + +final _edgeCasesEdgeCasesWidgetStreamIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('edge_cases_edge_cases_WidgetStreamIterator_destroy'); +final _edgeCasesEdgeCasesWidgetStreamIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('edge_cases_edge_cases_WidgetStreamIterator_destroy')); + +/// Iterator of objects +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +/// +/// Each element is owned by the caller: call its `dispose()` when +/// you're done with it. +Iterable widgetStream() sync* { + final Pointer _iter; + _iter = _edgeCasesEdgeCasesWidgetStream(_err); + _check(_err); + final _anchor = _IteratorAnchor(); + _edgeCasesEdgeCasesWidgetStreamIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _more = _edgeCasesEdgeCasesWidgetStreamIteratorNext(_iter, _outItem.cast(), _err); + _check(_err); + if (_more == 0) break; + yield Widget._(_outItem.cast>().value); + } + } finally { + _edgeCasesEdgeCasesWidgetStreamIteratorDestroyFinalizer.detach(_anchor); + _edgeCasesEdgeCasesWidgetStreamIteratorDestroy(_iter); + } +} + +typedef _EdgeCasesEdgeCasesAsyncWidgetCallback = Void Function(Pointer, Pointer<_Error>, Pointer); + +final _edgeCasesEdgeCasesAsyncWidget = _lib.lookupFunction< + Void Function(Pointer>, Pointer), + void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_widget'); + +/// Async returning an object +/// +/// The returned object is owned by the caller: call its `dispose()` +/// when you're done with it. +/// +/// Throws [EdgeErrorsException] on domain errors. +Future asyncWidget() { + final _completer = Completer(); + late final NativeCallable<_EdgeCasesEdgeCasesAsyncWidgetCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncWidgetCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _mapEdgeErrorsException); + _completer.complete(Widget._(_value)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _edgeCasesEdgeCasesAsyncWidget(_callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } + return _completer.future; +} + +final _edgeCasesEdgeCasesMakeWidget = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer<_Error>)>('edge_cases_edge_cases_make_widget'); + +/// Factory for an interface with no constructor +/// +/// The returned object is owned by the caller: call its `dispose()` +/// when you're done with it. +Widget makeWidget(String label) { + final _arena = Arena(); + try { + final _labelBytes = utf8.encode(label); + final _result = _edgeCasesEdgeCasesMakeWidget(_stage(_arena, _labelBytes), _labelBytes.length, _err); + _check(_err); + return Widget._(_result); + } finally { + _arena.releaseAll(); + } +} + +final _edgeCasesEdgeCasesFindWidget = _lib.lookupFunction< + Pointer Function(Int64, Pointer<_Error>), + Pointer Function(int, Pointer<_Error>)>('edge_cases_edge_cases_find_widget'); + +/// Optional interface return +/// +/// Returns null when the producer reports no object. A returned object +/// is owned by the caller: call its `dispose()` when you're done with it. +/// +/// Throws [EdgeErrorsException] on domain errors. +Widget? findWidget(int id) { + final _result = _edgeCasesEdgeCasesFindWidget(id, _err); + _check(_err, _mapEdgeErrorsException); + return _result == nullptr ? null : Widget._(_result); +} + +final _edgeCasesEdgeCasesInspectWidget = _lib.lookupFunction< + Pointer Function(Pointer, Pointer, Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_inspect_widget'); + +/// Interface parameter, including an optional one +String inspectWidget(Widget widget, Widget? fallback) { + final _arena = Arena(); + try { + final _widgetPtr = _borrow(_arena, widget); + final _fallbackPtr = fallback == null ? nullptr : _borrow(_arena, fallback); + final _result = _edgeCasesEdgeCasesInspectWidget(_widgetPtr, _fallbackPtr, _outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); + } finally { + _arena.releaseAll(); + } +} + +final _edgeCasesEdgeCasesStringStream = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('edge_cases_edge_cases_string_stream'); + +final _edgeCasesEdgeCasesStringStreamIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer>, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer>, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_StringStreamIterator_next'); + +final _edgeCasesEdgeCasesStringStreamIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('edge_cases_edge_cases_StringStreamIterator_destroy'); +final _edgeCasesEdgeCasesStringStreamIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('edge_cases_edge_cases_StringStreamIterator_destroy')); + +/// Iterator of strings +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +Iterable stringStream() sync* { + final Pointer _iter; + _iter = _edgeCasesEdgeCasesStringStream(_err); + _check(_err); + final _anchor = _IteratorAnchor(); + _edgeCasesEdgeCasesStringStreamIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _more = _edgeCasesEdgeCasesStringStreamIteratorNext(_iter, _outItem.cast(), _outLen, _err); + _check(_err); + if (_more == 0) break; + yield _takeString(_outItem.cast>().value, _outLen.value); + } + } finally { + _edgeCasesEdgeCasesStringStreamIteratorDestroyFinalizer.detach(_anchor); + _edgeCasesEdgeCasesStringStreamIteratorDestroy(_iter); + } +} + +final _edgeCasesEdgeCasesScalarStream = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('edge_cases_edge_cases_scalar_stream'); + +final _edgeCasesEdgeCasesScalarStreamIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_ScalarStreamIterator_next'); + +final _edgeCasesEdgeCasesScalarStreamIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('edge_cases_edge_cases_ScalarStreamIterator_destroy'); +final _edgeCasesEdgeCasesScalarStreamIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('edge_cases_edge_cases_ScalarStreamIterator_destroy')); + +/// Iterator of scalars +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +Iterable scalarStream() sync* { + final Pointer _iter; + _iter = _edgeCasesEdgeCasesScalarStream(_err); + _check(_err); + final _anchor = _IteratorAnchor(); + _edgeCasesEdgeCasesScalarStreamIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _more = _edgeCasesEdgeCasesScalarStreamIteratorNext(_iter, _outItem.cast(), _err); + _check(_err); + if (_more == 0) break; + yield _outItem.cast().value; + } + } finally { + _edgeCasesEdgeCasesScalarStreamIteratorDestroyFinalizer.detach(_anchor); + _edgeCasesEdgeCasesScalarStreamIteratorDestroy(_iter); + } +} + +final _edgeCasesEdgeCasesModeStream = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('edge_cases_edge_cases_mode_stream'); + +final _edgeCasesEdgeCasesModeStreamIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_ModeStreamIterator_next'); + +final _edgeCasesEdgeCasesModeStreamIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('edge_cases_edge_cases_ModeStreamIterator_destroy'); +final _edgeCasesEdgeCasesModeStreamIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('edge_cases_edge_cases_ModeStreamIterator_destroy')); + +/// Iterator of enums +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +Iterable modeStream() sync* { + final Pointer _iter; + _iter = _edgeCasesEdgeCasesModeStream(_err); + _check(_err); + final _anchor = _IteratorAnchor(); + _edgeCasesEdgeCasesModeStreamIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _more = _edgeCasesEdgeCasesModeStreamIteratorNext(_iter, _outItem.cast(), _err); + _check(_err); + if (_more == 0) break; + yield Mode.fromValue(_outItem.cast().value); + } + } finally { + _edgeCasesEdgeCasesModeStreamIteratorDestroyFinalizer.detach(_anchor); + _edgeCasesEdgeCasesModeStreamIteratorDestroy(_iter); + } +} + +typedef _EdgeCasesEdgeCasesAsyncVoidCallback = Void Function(Pointer, Pointer<_Error>); + +final _edgeCasesEdgeCasesAsyncVoid = _lib.lookupFunction< + Void Function(Pointer>, Pointer), + void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_void'); + +/// Async with no result +Future asyncVoid() { + final _completer = Completer(); + late final NativeCallable<_EdgeCasesEdgeCasesAsyncVoidCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncVoidCallback>.listener(( + Pointer _, Pointer<_Error> _error) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _runtimeError); + _completer.complete(); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _edgeCasesEdgeCasesAsyncVoid(_callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } + return _completer.future; +} + +typedef _EdgeCasesEdgeCasesAsyncBytesCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _edgeCasesEdgeCasesAsyncBytes = _lib.lookupFunction< + Void Function(Uint32, Pointer>, Pointer), + void Function(int, Pointer>, Pointer)>('edge_cases_edge_cases_async_bytes'); + +Future> asyncBytes(int size) { + final _completer = Completer>(); + late final NativeCallable<_EdgeCasesEdgeCasesAsyncBytesCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncBytesCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value, int _valueLen) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _runtimeError); + _completer.complete(_takeBytes(_value, _valueLen)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _edgeCasesEdgeCasesAsyncBytes(size, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } + return _completer.future; +} + +typedef _EdgeCasesEdgeCasesAsyncStructCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _edgeCasesEdgeCasesAsyncStruct = _lib.lookupFunction< + Void Function(Pointer>, Pointer), + void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_struct'); + +/// Throws [EdgeErrorsException] on domain errors. +Future asyncStruct() { + final _completer = Completer(); + late final NativeCallable<_EdgeCasesEdgeCasesAsyncStructCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncStructCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value, int _valueLen) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _mapEdgeErrorsException); + _completer.complete(_decode(_takeBytes(_value, _valueLen), _unpackOuter)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _edgeCasesEdgeCasesAsyncStruct(_callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } + return _completer.future; +} + +typedef _EdgeCasesEdgeCasesAsyncListCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _edgeCasesEdgeCasesAsyncList = _lib.lookupFunction< + Void Function(Pointer, Pointer>, Pointer), + void Function(Pointer, Pointer>, Pointer)>('edge_cases_edge_cases_async_list'); + +/// Cancelling [cancelToken] completes the returned future with a +/// [CancelledException]. +/// +/// Throws [EdgeErrorsException] on domain errors. +Future> asyncList({CancelToken? cancelToken}) { + final _completer = Completer>(); + final _cancel = _NativeCancel.bind(cancelToken); + late final NativeCallable<_EdgeCasesEdgeCasesAsyncListCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncListCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value, int _valueLen) { + _callback.close(); + _cancel?.release(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _mapEdgeErrorsException); + _completer.complete(_decode(_takeBytes(_value, _valueLen), (r) => [for (var i = r.readLength(); i > 0; i--) _unpackOuter(r)])); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _edgeCasesEdgeCasesAsyncList(_cancel?.pointer ?? nullptr, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + _cancel?.release(); + rethrow; + } + return _completer.future; +} + +typedef _EdgeCasesEdgeCasesAsyncOptionalCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _edgeCasesEdgeCasesAsyncOptional = _lib.lookupFunction< + Void Function(Pointer>, Pointer), + void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_optional'); + +Future asyncOptional() { + final _completer = Completer(); + late final NativeCallable<_EdgeCasesEdgeCasesAsyncOptionalCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncOptionalCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value, int _valueLen) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _runtimeError); + _completer.complete(_decode(_takeBytes(_value, _valueLen), (r) => (r.readFlag() ? r.readI64() : null))); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _edgeCasesEdgeCasesAsyncOptional(_callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } + return _completer.future; +} + +typedef _EdgeCasesEdgeCasesAsyncEnumCallback = Void Function(Pointer, Pointer<_Error>, Int32); + +final _edgeCasesEdgeCasesAsyncEnum = _lib.lookupFunction< + Void Function(Pointer>, Pointer), + void Function(Pointer>, Pointer)>('edge_cases_edge_cases_async_enum'); + +Future asyncEnum() { + final _completer = Completer(); + late final NativeCallable<_EdgeCasesEdgeCasesAsyncEnumCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncEnumCallback>.listener(( + Pointer _, Pointer<_Error> _error, int _value) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _runtimeError); + _completer.complete(Mode.fromValue(_value)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + _edgeCasesEdgeCasesAsyncEnum(_callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } + return _completer.future; +} + +typedef _EdgeCasesEdgeCasesAsyncKeywordsCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _edgeCasesEdgeCasesAsyncKeywords = _lib.lookupFunction< + Void Function(Pointer, Size, Int32, Pointer>, Pointer), + void Function(Pointer, int, int, Pointer>, Pointer)>('edge_cases_edge_cases_async_keywords'); + +Future asyncKeywords(String class_, int import) { + final _arena = Arena(); + final _completer = Completer(); + late final NativeCallable<_EdgeCasesEdgeCasesAsyncKeywordsCallback> _callback; + _callback = NativeCallable<_EdgeCasesEdgeCasesAsyncKeywordsCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value, int _valueLen) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _runtimeError); + _completer.complete(_takeString(_value, _valueLen)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + final _class_Bytes = utf8.encode(class_); + _edgeCasesEdgeCasesAsyncKeywords(_stage(_arena, _class_Bytes), _class_Bytes.length, import, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } finally { + _arena.releaseAll(); + } + return _completer.future; +} + +final _edgeCasesEdgeCasesKeywordsRoundtrip = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_keywords_roundtrip'); + +Keywords keywordsRoundtrip(Keywords value) { + final _arena = Arena(); + try { + final _valueWriter = _BufferWriter(); + _packKeywords(_valueWriter, value); + final _valueBytes = _valueWriter.takeBytes(); + final _result = _edgeCasesEdgeCasesKeywordsRoundtrip(_stage(_arena, _valueBytes), _valueBytes.length, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackKeywords); + } finally { + _arena.releaseAll(); + } +} + +final _edgeCasesEdgeCasesLegacyValue = _lib.lookupFunction< + Pointer Function(Int32, Pointer, Pointer<_Error>), + Pointer Function(int, Pointer, Pointer<_Error>)>('edge_cases_edge_cases_legacy_value'); + +@Deprecated('Use Mode-based APIs') +Retired legacyValue(Legacy value) { + final _result = _edgeCasesEdgeCasesLegacyValue(value.value, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackRetired); +} + +// END edge_cases.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_weaveffi_dart.snap deleted file mode 100644 index 7992dbaf..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_weaveffi_dart.snap +++ /dev/null @@ -1,2259 +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}'); -} - -// The ABI revision these bindings were generated against. -const int _abiVersion = 2; - -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 _NativeWeaveffiErrorSet = Void Function(Pointer<_WeaveFFIError>, Int32, Pointer); -typedef _DartWeaveffiErrorSet = void Function(Pointer<_WeaveFFIError>, int, Pointer); -final _weaveffiErrorSet = _lib.lookupFunction< - _NativeWeaveffiErrorSet, _DartWeaveffiErrorSet>('weaveffi_error_set'); - -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: a producer panic ([panicCode]), a marshalling -/// error ([marshalCode]), a callback-interface implementation that threw -/// ([foreignCode], carrying the Dart exception's text), or an unknown code. -class WeaveFFIException implements Exception { - /// The producer reported an untyped error. - static const int genericCode = -1; - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - /// An argument could not be lifted by the producer. - static const int marshalCode = -3; - /// A Dart callback-interface implementation threw; [message] carries the - /// exception's text. - static const int foreignCode = -4; - 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'); - } -} - -// ── WeaveFFI callback-interface runtime ── -// A Dart implementation passed to the producer is stored here under an -// integer key; the key (widened to a pointer) is what crosses as `ctx`, so the -// producer never holds a Dart object and the GC never sees a raw pointer. The -// entry lives until the producer calls the vtable's `free(ctx)`. -final Map _callbackTable = {}; -int _nextCallbackKey = 1; - -Pointer _registerCallback(Object impl) { - final key = _nextCallbackKey++; - _callbackTable[key] = impl; - return Pointer.fromAddress(key); -} - -Object _callbackFor(Pointer ctx) { - final impl = _callbackTable[ctx.address]; - if (impl == null) { - throw StateError('callback interface context ${ctx.address} is not registered'); - } - return impl; -} - -// Vtable trampolines are process-wide: their NativeCallables are never closed, -// so they are anchored here to keep the native thunks alive. `keepIsolateAlive` -// is cleared on each so an idle isolate can still exit. -final List> _callbackCallables = []; - -Pointer> _pinCallable( - NativeCallable callable) { - callable.keepIsolateAlive = false; - _callbackCallables.add(callable); - return callable.nativeFunction; -} - -// Reports a Dart exception thrown by a callback-interface implementation -// through `weaveffi_error_set` with the reserved foreign error code (-4). -// The producer copies the message, so the temporary is freed right away. This -// must never throw: it runs in the catch path of a trampoline whose C frame -// an exception must not unwind through. -void _foreignError(Pointer<_WeaveFFIError> outErr, Object error) { - if (outErr == nullptr) return; - Pointer message = nullptr; - try { - message = error.toString().toNativeUtf8(); - _weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, message); - } catch (_) { - _weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, nullptr); - } finally { - if (message != nullptr) calloc.free(message); - } -} - -// 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(), - ); -} - -/// Record carrying objects in every buffered position -class WidgetHolder { - final Widget primary; - final Widget? fallback; - final List extras; - final Map byName; - - WidgetHolder({required this.primary, this.fallback, required this.extras, required this.byName}); -} - -void _packWidgetHolder(_BufferWriter w, WidgetHolder v) { - w.writeUint64(v.primary._cloneRef().address); - final t0 = v.fallback; - if (t0 == null) { - w.writeOptionFlag(false); - } else { - w.writeOptionFlag(true); - w.writeUint64(t0._cloneRef().address); - } - final t1 = v.extras; - w.writeLength(t1.length); - for (final t2 in t1) { - w.writeUint64(t2._cloneRef().address); - } - final t3 = v.byName; - w.writeLength(t3.length); - for (final t4 in t3.entries) { - w.writeString(t4.key); - w.writeUint64(t4.value._cloneRef().address); - } -} - -WidgetHolder _unpackWidgetHolder(_BufferReader r) { - return WidgetHolder( - primary: Widget._(Pointer.fromAddress(r.readUint64())), - fallback: (r.readOptionFlag() ? Widget._(Pointer.fromAddress(r.readUint64())) : null), - extras: List.generate(r.readLength(), (_) => Widget._(Pointer.fromAddress(r.readUint64()))), - byName: { for (var i = r.readLength(); i > 0; i--) r.readString(): Widget._(Pointer.fromAddress(r.readUint64())) }, - ); -} - -/// Callback interface carrying composite payloads and objects -/// -/// A callback interface: implement this class and pass an instance to any -/// function taking a [BatchListener]. The producer keeps the instance alive -/// (through a private handle table) until it releases the callback, so no -/// other reference needs to be held. -/// -/// Object arguments are owned by the implementation: call `dispose()` on -/// them (or let the finalizer run) when done. A thrown exception is reported -/// to the producer, which aborts the call it was making; the original Dart -/// caller then observes a [WeaveFFIException] with [WeaveFFIException.foreignCode] -/// carrying the exception's text. -/// -/// Dart limitation: methods are bound with `NativeCallable.isolateLocal`, so -/// the producer may only invoke them on the thread of the isolate that -/// passed the callback. That holds when the producer calls them -/// synchronously during a call from Dart. A producer that invokes a method -/// from another thread (an async task, a background worker) is unsupported: -/// a `NativeCallable.listener` cannot return a value or read borrowed -/// arguments after the native frame returns. -abstract class BatchListener { - /// Composite payloads cross as value buffers - void onBatch(List items, List raw, Outer? first, Mode mode); - /// Objects cross as strong references the consumer must release - void onWidget(Widget widget, Widget? fallback, List children); - void onTick(); - /// Scalar return travels back through the vtable - bool shouldContinue(int class_); - /// Enum return travels back as its integer discriminant - Mode pickMode(); -} - -typedef _NativeBatchListenerVtOnBatch = Void Function(Pointer, Pointer, Size, Pointer, Size, Pointer, Size, Int32, Pointer<_WeaveFFIError>); -typedef _NativeBatchListenerVtOnWidget = Void Function(Pointer, Pointer, Pointer, Pointer, Size, Pointer<_WeaveFFIError>); -typedef _NativeBatchListenerVtOnTick = Void Function(Pointer, Pointer<_WeaveFFIError>); -typedef _NativeBatchListenerVtShouldContinue = Bool Function(Pointer, Int32, Pointer<_WeaveFFIError>); -typedef _NativeBatchListenerVtPickMode = Int32 Function(Pointer, Pointer<_WeaveFFIError>); -typedef _NativeBatchListenerVtFree = Void Function(Pointer); - -// The C `weaveffi_edge_cases_BatchListener_vtable` layout. -final class _BatchListenerVtableStruct extends Struct { - external Pointer> onBatch; - external Pointer> onWidget; - external Pointer> onTick; - external Pointer> shouldContinue; - external Pointer> pickMode; - external Pointer> free; -} - -// Vtable entry `weaveffi_edge_cases_BatchListener_vtable.on_batch`. -void _batchListenerVtOnBatch(Pointer ctx, Pointer itemsPtr, int itemsLen, Pointer rawPtr, int rawLen, Pointer firstPtr, int firstLen, int mode, Pointer<_WeaveFFIError> outErr) { - try { - 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(); - final impl = _callbackFor(ctx) as BatchListener; - impl.onBatch(itemsValue, rawPtr == nullptr ? [] : rawPtr.asTypedList(rawLen).toList(), firstValue, Mode.fromValue(mode)); - } catch (e) { - _foreignError(outErr, e); - } -} - -// Vtable entry `weaveffi_edge_cases_BatchListener_vtable.on_widget`. -void _batchListenerVtOnWidget(Pointer ctx, Pointer widget, Pointer fallback, Pointer childrenPtr, int childrenLen, Pointer<_WeaveFFIError> outErr) { - try { - final widgetValue = Widget._(widget); - final fallbackValue = fallback == nullptr ? null : Widget._(fallback); - final childrenData = _copyNativeBytes(childrenPtr, childrenLen); - final childrenReader = _BufferReader(childrenData); - final childrenValue = List.generate(childrenReader.readLength(), (_) => Widget._(Pointer.fromAddress(childrenReader.readUint64()))); - childrenReader.expectEnd(); - final impl = _callbackFor(ctx) as BatchListener; - impl.onWidget(widgetValue, fallbackValue, childrenValue); - } catch (e) { - _foreignError(outErr, e); - } -} - -// Vtable entry `weaveffi_edge_cases_BatchListener_vtable.on_tick`. -void _batchListenerVtOnTick(Pointer ctx, Pointer<_WeaveFFIError> outErr) { - try { - final impl = _callbackFor(ctx) as BatchListener; - impl.onTick(); - } catch (e) { - _foreignError(outErr, e); - } -} - -// Vtable entry `weaveffi_edge_cases_BatchListener_vtable.should_continue`. -bool _batchListenerVtShouldContinue(Pointer ctx, int class_, Pointer<_WeaveFFIError> outErr) { - try { - final impl = _callbackFor(ctx) as BatchListener; - return impl.shouldContinue(class_); - } catch (e) { - _foreignError(outErr, e); - return false; - } -} - -// Vtable entry `weaveffi_edge_cases_BatchListener_vtable.pick_mode`. -int _batchListenerVtPickMode(Pointer ctx, Pointer<_WeaveFFIError> outErr) { - try { - final impl = _callbackFor(ctx) as BatchListener; - return impl.pickMode().value; - } catch (e) { - _foreignError(outErr, e); - return 0; - } -} - -void _batchListenerVtFree(Pointer ctx) { - _callbackTable.remove(ctx.address); -} - -// The process-wide static `weaveffi_edge_cases_BatchListener_vtable` every BatchListener passed to the producer -// shares; the per-instance state travels in `ctx`. -final Pointer<_BatchListenerVtableStruct> _BatchListenerVtable = () { - final vt = calloc<_BatchListenerVtableStruct>(); - vt.ref.onBatch = _pinCallable(NativeCallable<_NativeBatchListenerVtOnBatch>.isolateLocal( - _batchListenerVtOnBatch)); - vt.ref.onWidget = _pinCallable(NativeCallable<_NativeBatchListenerVtOnWidget>.isolateLocal( - _batchListenerVtOnWidget)); - vt.ref.onTick = _pinCallable(NativeCallable<_NativeBatchListenerVtOnTick>.isolateLocal( - _batchListenerVtOnTick)); - vt.ref.shouldContinue = _pinCallable(NativeCallable<_NativeBatchListenerVtShouldContinue>.isolateLocal( - _batchListenerVtShouldContinue, exceptionalReturn: false)); - vt.ref.pickMode = _pinCallable(NativeCallable<_NativeBatchListenerVtPickMode>.isolateLocal( - _batchListenerVtPickMode, exceptionalReturn: 0)); - vt.ref.free = _pinCallable(NativeCallable<_NativeBatchListenerVtFree>.listener( - _batchListenerVtFree)); - return vt; -}(); - -/// Callback interface whose name and methods are keywords -/// -/// A callback interface: implement this class and pass an instance to any -/// function taking a [Keyword]. The producer keeps the instance alive -/// (through a private handle table) until it releases the callback, so no -/// other reference needs to be held. -/// -/// Object arguments are owned by the implementation: call `dispose()` on -/// them (or let the finalizer run) when done. A thrown exception is reported -/// to the producer, which aborts the call it was making; the original Dart -/// caller then observes a [WeaveFFIException] with [WeaveFFIException.foreignCode] -/// carrying the exception's text. -/// -/// Dart limitation: methods are bound with `NativeCallable.isolateLocal`, so -/// the producer may only invoke them on the thread of the isolate that -/// passed the callback. That holds when the producer calls them -/// synchronously during a call from Dart. A producer that invokes a method -/// from another thread (an async task, a background worker) is unsupported: -/// a `NativeCallable.listener` cannot return a value or read borrowed -/// arguments after the native frame returns. -abstract class Keyword { - void delete(int self); - int import(); -} - -typedef _NativeKeywordVtDelete = Void Function(Pointer, Int32, Pointer<_WeaveFFIError>); -typedef _NativeKeywordVtImport = Int32 Function(Pointer, Pointer<_WeaveFFIError>); -typedef _NativeKeywordVtFree = Void Function(Pointer); - -// The C `weaveffi_edge_cases_Keyword_vtable` layout. -final class _KeywordVtableStruct extends Struct { - external Pointer> delete; - external Pointer> import; - external Pointer> free; -} - -// Vtable entry `weaveffi_edge_cases_Keyword_vtable.delete`. -void _keywordVtDelete(Pointer ctx, int self, Pointer<_WeaveFFIError> outErr) { - try { - final impl = _callbackFor(ctx) as Keyword; - impl.delete(self); - } catch (e) { - _foreignError(outErr, e); - } -} - -// Vtable entry `weaveffi_edge_cases_Keyword_vtable.import`. -int _keywordVtImport(Pointer ctx, Pointer<_WeaveFFIError> outErr) { - try { - final impl = _callbackFor(ctx) as Keyword; - return impl.import(); - } catch (e) { - _foreignError(outErr, e); - return 0; - } -} - -void _keywordVtFree(Pointer ctx) { - _callbackTable.remove(ctx.address); -} - -// The process-wide static `weaveffi_edge_cases_Keyword_vtable` every Keyword passed to the producer -// shares; the per-instance state travels in `ctx`. -final Pointer<_KeywordVtableStruct> _KeywordVtable = () { - final vt = calloc<_KeywordVtableStruct>(); - vt.ref.delete = _pinCallable(NativeCallable<_NativeKeywordVtDelete>.isolateLocal( - _keywordVtDelete)); - vt.ref.import = _pinCallable(NativeCallable<_NativeKeywordVtImport>.isolateLocal( - _keywordVtImport, exceptionalReturn: 0)); - vt.ref.free = _pinCallable(NativeCallable<_NativeKeywordVtFree>.listener( - _keywordVtFree)); - return vt; -}(); - -typedef _NativeWeaveffiEdgeCasesWidgetClone = Pointer Function(Pointer); -typedef _DartWeaveffiEdgeCasesWidgetClone = Pointer Function(Pointer); -final _weaveffiEdgeCasesWidgetClone = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesWidgetClone, _DartWeaveffiEdgeCasesWidgetClone>('weaveffi_edge_cases_Widget_clone'); - -typedef _NativeWeaveffiEdgeCasesWidgetDestroy = Void Function(Pointer); -typedef _DartWeaveffiEdgeCasesWidgetDestroy = void Function(Pointer); -final _weaveffiEdgeCasesWidgetDestroy = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesWidgetDestroy, _DartWeaveffiEdgeCasesWidgetDestroy>('weaveffi_edge_cases_Widget_destroy'); -final _weaveffiEdgeCasesWidgetDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('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 -/// -/// A reference-counted native object. Each instance holds one strong -/// reference, released by [dispose] or, if the instance is collected -/// without being disposed, by a GC finalizer. Instances stay valid for as -/// long as the Dart object is reachable; the producer keeps its own -/// references to any object it retains. -class Widget implements Finalizable { - final Pointer _ptr; - bool _disposed = false; - - /// Adopts one strong reference to the native object. - Widget._(this._ptr) { - _weaveffiEdgeCasesWidgetDestroyFinalizer.attach(this, _ptr, detach: this); - } - - /// The borrowed native pointer for the duration of a call. - Pointer get _handle { - if (_disposed) throw StateError('Widget used after dispose()'); - return _ptr; - } - - /// Mints a second strong reference (the interface's `_clone` symbol) for - /// an object token written into a value buffer. - Pointer _cloneRef() => _weaveffiEdgeCasesWidgetClone(_handle); - - /// Releases this instance's native reference. Safe to call more than - /// once; the native object is dropped when its last reference (this - /// one, any other wrapper's, or the producer's) is released. - void dispose() { - if (_disposed) return; - _disposed = true; - _weaveffiEdgeCasesWidgetDestroyFinalizer.detach(this); - _weaveffiEdgeCasesWidgetDestroy(_ptr); - } - - /// 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 _NativeWeaveffiEdgeCasesVaultClone = Pointer Function(Pointer); -typedef _DartWeaveffiEdgeCasesVaultClone = Pointer Function(Pointer); -final _weaveffiEdgeCasesVaultClone = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesVaultClone, _DartWeaveffiEdgeCasesVaultClone>('weaveffi_edge_cases_Vault_clone'); - -typedef _NativeWeaveffiEdgeCasesVaultDestroy = Void Function(Pointer); -typedef _DartWeaveffiEdgeCasesVaultDestroy = void Function(Pointer); -final _weaveffiEdgeCasesVaultDestroy = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesVaultDestroy, _DartWeaveffiEdgeCasesVaultDestroy>('weaveffi_edge_cases_Vault_destroy'); -final _weaveffiEdgeCasesVaultDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('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 -/// -/// A reference-counted native object. Each instance holds one strong -/// reference, released by [dispose] or, if the instance is collected -/// without being disposed, by a GC finalizer. Instances stay valid for as -/// long as the Dart object is reachable; the producer keeps its own -/// references to any object it retains. -@Deprecated('Use Widget instead') -class Vault implements Finalizable { - final Pointer _ptr; - bool _disposed = false; - - /// Adopts one strong reference to the native object. - Vault._(this._ptr) { - _weaveffiEdgeCasesVaultDestroyFinalizer.attach(this, _ptr, detach: this); - } - - /// The borrowed native pointer for the duration of a call. - Pointer get _handle { - if (_disposed) throw StateError('Vault used after dispose()'); - return _ptr; - } - - /// Mints a second strong reference (the interface's `_clone` symbol) for - /// an object token written into a value buffer. - Pointer _cloneRef() => _weaveffiEdgeCasesVaultClone(_handle); - - /// Releases this instance's native reference. Safe to call more than - /// once; the native object is dropped when its last reference (this - /// one, any other wrapper's, or the producer's) is released. - void dispose() { - if (_disposed) return; - _disposed = true; - _weaveffiEdgeCasesVaultDestroyFinalizer.detach(this); - _weaveffiEdgeCasesVaultDestroy(_ptr); - } - - /// Fails when the vault is busy - /// - /// The returned object is owned by the caller: call its `dispose()` when - /// you are done with it. - /// - /// 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); - } - } - - /// Returns `null` when the producer reports no object. A non-null result - /// is owned by the caller: call its `dispose()` when you are done with it. - Widget? peek() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiEdgeCasesVaultPeek(_handle, err); - _checkError(err); - return result == nullptr ? null : 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 _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 _NativeWeaveffiEdgeCasesRegisterBatch = Void Function(Pointer, Pointer, Pointer, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiEdgeCasesRegisterBatch = void Function(Pointer, Pointer, Pointer, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiEdgeCasesRegisterBatch = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesRegisterBatch, _DartWeaveffiEdgeCasesRegisterBatch>('weaveffi_edge_cases_register_batch'); - -/// Two callback interface parameters on one function -void registerBatch(BatchListener listener, Keyword keyword) { - final err = calloc<_WeaveFFIError>(); - try { - _weaveffiEdgeCasesRegisterBatch(_registerCallback(listener), _BatchListenerVtable.cast(), _registerCallback(keyword), _KeywordVtable.cast(), err); - _checkError(err); - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiEdgeCasesWidgetsEverywhere = Pointer Function(Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiEdgeCasesWidgetsEverywhere = Pointer Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiEdgeCasesWidgetsEverywhere = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesWidgetsEverywhere, _DartWeaveffiEdgeCasesWidgetsEverywhere>('weaveffi_edge_cases_widgets_everywhere'); - -/// Objects in every buffered position -WidgetHolder widgetsEverywhere(List list, Map byName, List sparse, WidgetHolder holder) { - final listWriter = _BufferWriter(); - final t0 = list; - listWriter.writeLength(t0.length); - for (final t1 in t0) { - listWriter.writeUint64(t1._cloneRef().address); - } - final listBuf = listWriter.takeBytes(); - final listPtr = _stageBytes(listBuf); - final byNameWriter = _BufferWriter(); - final t2 = byName; - byNameWriter.writeLength(t2.length); - for (final t3 in t2.entries) { - byNameWriter.writeString(t3.key); - byNameWriter.writeUint64(t3.value._cloneRef().address); - } - final byNameBuf = byNameWriter.takeBytes(); - final byNamePtr = _stageBytes(byNameBuf); - final sparseWriter = _BufferWriter(); - final t4 = sparse; - sparseWriter.writeLength(t4.length); - for (final t5 in t4) { - final t6 = t5; - if (t6 == null) { - sparseWriter.writeOptionFlag(false); - } else { - sparseWriter.writeOptionFlag(true); - sparseWriter.writeUint64(t6._cloneRef().address); - } - } - final sparseBuf = sparseWriter.takeBytes(); - final sparsePtr = _stageBytes(sparseBuf); - final holderWriter = _BufferWriter(); - _packWidgetHolder(holderWriter, holder); - final holderBuf = holderWriter.takeBytes(); - final holderPtr = _stageBytes(holderBuf); - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiEdgeCasesWidgetsEverywhere(listPtr, listBuf.length, byNamePtr, byNameBuf.length, sparsePtr, sparseBuf.length, holderPtr, holderBuf.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 = _unpackWidgetHolder(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(listPtr); - calloc.free(byNamePtr); - calloc.free(sparsePtr); - calloc.free(holderPtr); - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiEdgeCasesWidgetStream = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiEdgeCasesWidgetStream = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiEdgeCasesWidgetStream = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesWidgetStream, _DartWeaveffiEdgeCasesWidgetStream>('weaveffi_edge_cases_widget_stream'); - -typedef _NativeWeaveffiEdgeCasesWidgetStreamIteratorNext = Int32 Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiEdgeCasesWidgetStreamIteratorNext = int Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); -final _weaveffiEdgeCasesWidgetStreamIteratorNext = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesWidgetStreamIteratorNext, _DartWeaveffiEdgeCasesWidgetStreamIteratorNext>('weaveffi_edge_cases_WidgetStreamIterator_next'); - -typedef _NativeWeaveffiEdgeCasesWidgetStreamIteratorDestroy = Void Function(Pointer); -typedef _DartWeaveffiEdgeCasesWidgetStreamIteratorDestroy = void Function(Pointer); -final _weaveffiEdgeCasesWidgetStreamIteratorDestroy = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesWidgetStreamIteratorDestroy, _DartWeaveffiEdgeCasesWidgetStreamIteratorDestroy>('weaveffi_edge_cases_WidgetStreamIterator_destroy'); -final _weaveffiEdgeCasesWidgetStreamIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('weaveffi_edge_cases_WidgetStreamIterator_destroy')); - -/// Iterator of objects -/// -/// 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. -/// -/// Each yielded element is owned by the caller: call its `dispose()` -/// when you are done with it. -Iterable widgetStream() sync* { - final err = calloc<_WeaveFFIError>(); - final outItem = calloc>(); - Pointer iter = nullptr; - final anchor = _IteratorLifetime(); - try { - iter = _weaveffiEdgeCasesWidgetStream(err); - _checkError(err); - _weaveffiEdgeCasesWidgetStreamIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor); - while (_weaveffiEdgeCasesWidgetStreamIteratorNext(iter, outItem, err) != 0) { - _checkError(err); - final itemPtr = outItem.value; - yield Widget._(itemPtr); - } - _checkError(err); - } finally { - if (iter != nullptr) { - _weaveffiEdgeCasesWidgetStreamIteratorDestroyFinalizer.detach(anchor); - _weaveffiEdgeCasesWidgetStreamIteratorDestroy(iter); - iter = nullptr; - } - calloc.free(outItem); - calloc.free(err); - } -} - -typedef _NativeAsyncCb_weaveffi_edge_cases_async_widget = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer); - -typedef _NativeWeaveffiEdgeCasesAsyncWidgetAsync = Void Function(Pointer>, Pointer); -typedef _DartWeaveffiEdgeCasesAsyncWidgetAsync = void Function(Pointer>, Pointer); -final _weaveffiEdgeCasesAsyncWidgetAsync = _lib.lookupFunction< - _NativeWeaveffiEdgeCasesAsyncWidgetAsync, _DartWeaveffiEdgeCasesAsyncWidgetAsync>('weaveffi_edge_cases_async_widget_async'); - -/// Async returning an object -/// -/// The returned object is owned by the caller: call its `dispose()` when -/// you are done with it. -/// -/// Throws [EdgeErrorsException] on domain errors. -Future asyncWidget() { - final completer = Completer(); - late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_widget> callable; - callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_widget>.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(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorFree(err); - completer.completeError(_mapEdgeErrorsException(code, msg, payload)); - return; - } - completer.complete(Widget._(result)); - } catch (e) { - completer.completeError(e); - } finally { - callable.close(); - } - }); - try { - _weaveffiEdgeCasesAsyncWidgetAsync(callable.nativeFunction, nullptr); - } catch (e) { - callable.close(); - rethrow; - } - return completer.future; -} - -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 -/// -/// The returned object is owned by the caller: call its `dispose()` when -/// you are done with it. -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 -/// -/// Returns `null` when the producer reports no object. A non-null result -/// is owned by the caller: call its `dispose()` when you are done with it. -/// -/// Throws [EdgeErrorsException] on domain errors. -Widget? findWidget(int id) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiEdgeCasesFindWidget(id, err); - _checkEdgeErrorsException(err); - return result == nullptr ? null : 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 _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_edge_cases__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__pubspec_yaml.snap index 020ab447..40ccfab8 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__pubspec_yaml.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__pubspec_yaml.snap @@ -1,16 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -name: weaveffi +name: edge_cases version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI +description: 'Native bindings for edge_cases' + environment: - sdk: '>=3.1.0 <4.0.0' + sdk: '>=3.12.0 <4.0.0' + dependencies: - ffi: ^2.0.0 + ffi: ^2.1.0 # END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__README_md.snap index 732894ed..79832093 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__README_md.snap @@ -1,46 +1,39 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Dart) +# kitchen_sink (Dart) -Auto-generated Dart bindings using `dart:ffi`. +`dart:ffi` bindings for the `kitchen_sink` native library. ## 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: +Add this package as a dependency (a `path:` dependency works) and import it: ```dart -import 'package:weaveffi/weaveffi.dart'; +import 'package:kitchen_sink/kitchen_sink.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`. +The library loads `libkitchen_sink.dylib` (macOS), `libkitchen_sink.so` (Linux and Android), or +`kitchen_sink.dll` (Windows) from the platform's search path on first use. Set +`KITCHEN_SINK_LIBRARY` to a full path to load a specific build. On iOS, and as a macOS +fallback, the bindings look the symbols up in the running executable. ## Objects and callbacks -Interface objects are reference counted by the native library. Each Dart -wrapper holds one reference: call `dispose()` when you are done with it, or let -the garbage collector's finalizer release it. Objects nested inside records and -collections follow the same rule. +Each interface wrapper holds one native reference: call `dispose()` when +you're done, or let the garbage collector's finalizer release it. -Callback interfaces are abstract classes: implement one and pass an instance to -any function that takes it. Methods are bound with `NativeCallable.isolateLocal`, -so the native library may only invoke them on the isolate's own thread (which -holds when it calls them synchronously during a call from Dart). An exception -thrown by an implementation aborts the native call and surfaces to the original -Dart caller as a `WeaveFFIException` with `WeaveFFIException.foreignCode`. +Callback-interface methods that return a value run synchronously on the +isolate's thread; void methods may be called from any thread and are +delivered on the event loop. ## Requirements -- Dart SDK >= 3.1.0 (for `NativeCallable`) -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. +- Dart SDK `>=3.12.0 <4.0.0` +- The `ffi` package diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_kitchen_sink_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_kitchen_sink_dart.snap new file mode 100644 index 00000000..b5eb029d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_kitchen_sink_dart.snap @@ -0,0 +1,1464 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io' show Platform; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +// ── Library loading ── +// The native library opens on first use and is verified before any other +// lookup: its C ABI revision and every module's contract checksum must match +// the values these bindings were generated against. + +/// The native library every binding in this file calls. +final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); + +DynamicLibrary _openLibrary() { + // An explicit path wins, so a caller can point at any build artifact. + final override = Platform.environment['KITCHEN_SINK_LIBRARY']; + if (override != null && override.isNotEmpty) { + return DynamicLibrary.open(override); + } + // iOS links native code into the app binary. + if (Platform.isIOS) return DynamicLibrary.process(); + final candidates = Platform.isMacOS + ? const ['libkitchen_sink.dylib'] + : Platform.isWindows + ? const ['kitchen_sink.dll'] + : const ['libkitchen_sink.so']; + ArgumentError? failure; + for (final candidate in candidates) { + try { + return DynamicLibrary.open(candidate); + } on ArgumentError catch (e) { + failure ??= e; + } + } + // A macOS app may link the library into its executable instead. + if (Platform.isMacOS) return DynamicLibrary.process(); + throw failure!; +} + +/// The C ABI revision these bindings were generated against. +const int _abiVersion = 3; + +/// Each top-level module's name, checksum symbol, and expected checksum. +const List<(String, String, int)> _contracts = <(String, String, int)>[ + ('shared', 'kitchen_sink_shared_checksum', 0x42c4ce2c8d0af052), + ('kitchen', 'kitchen_sink_kitchen_checksum', 0xd83da75b24b545ff), +]; + +DynamicLibrary _verifyLibrary(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction( + 'kitchen_sink_abi_version'); + } on ArgumentError { + throw StateError( + "the native library 'kitchen_sink' isn't loaded; set KITCHEN_SINK_LIBRARY to " + 'its path'); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError('kitchen_sink: these bindings expect C ABI revision ' + '$_abiVersion, but the loaded library reports revision $found'); + } + for (final (module, symbol, expected) in _contracts) { + final int actual; + try { + actual = lib.lookupFunction(symbol)(); + } on ArgumentError { + throw StateError( + "kitchen_sink: the loaded library doesn't export module '$module'"); + } + if (actual != expected) { + throw StateError("kitchen_sink: module '$module' doesn't match the loaded " + 'library (contract checksum ${_hex(actual)}, expected ' + '${_hex(expected)}); regenerate the bindings'); + } + } + return lib; +} + +String _hex(int v) => '0x${v.toUnsigned(64).toRadixString(16).padLeft(16, '0')}'; + +// ── Errors and memory ── + +/// The C `kitchen_sink_error` slot every fallible call writes. A non-zero +/// `code` carries a producer-owned `message` and, for a domain error with +/// fields, a value-buffer `payload`; `kitchen_sink_error_clear` releases both. +final class _Error extends Struct { + @Int32() + external int code; + external Pointer message; + external Pointer payloadPtr; + @Size() + external int payloadLen; +} + +final _errorClear = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + 'kitchen_sink_error_clear', + isLeaf: true); + +final _freeBytes = _lib.lookupFunction, Size), + void Function(Pointer, int)>('kitchen_sink_free_bytes', isLeaf: true); + +// Synchronous calls on this isolate share one error slot and one length +// slot: each call reads (and clears) them right after it returns, before any +// other call can reuse them, including a nested call from a callback. +final Pointer<_Error> _err = calloc<_Error>(); +final Pointer _outLen = calloc(); + +/// A failure the native library reported. Domain errors extend this class +/// with a positive [code]; the negative runtime codes are the constants +/// below. +class NativeException implements Exception { + /// Creates an exception carrying [code] and [message]. + NativeException(this.code, this.message); + + /// The producer reported an untyped error. + static const int genericCode = -1; + + /// The producer panicked; [message] carries the panic text. + static const int panicCode = -2; + + /// An argument couldn't be lifted by the producer. + static const int marshalCode = -3; + + /// A callback implementation threw; [message] carries its text. + static const int foreignCode = -4; + + /// The call was cancelled (see [CancelledException]). + static const int cancelledCode = -5; + + /// The error code: positive for a domain error, negative for a runtime + /// failure. + final int code; + + /// The producer's message. + final String message; + + @override + String toString() => '$runtimeType($code): $message'; +} + +/// A cancellable call completed because its token was cancelled. +class CancelledException extends NativeException { + /// Creates a cancellation with the producer's [message]. + CancelledException([String message = 'cancelled']) + : super(NativeException.cancelledCode, message); +} + +typedef _ErrorMapper = NativeException Function( + int code, String message, Uint8List payload); + +/// Maps a runtime failure (or an undeclared code) onto its exception. +NativeException _runtimeError(int code, String message, Uint8List payload) => + code == NativeException.cancelledCode + ? CancelledException(message) + : NativeException(code, message); + +/// Builds the exception [err] reports, using [map] for domain codes. +NativeException _readError(Pointer<_Error> err, _ErrorMapper map) { + final e = err.ref; + final message = e.message == nullptr ? '' : e.message.toDartString(); + return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); +} + +/// Throws the error a synchronous call left in [err], clearing the slot. +void _check(Pointer<_Error> err, [_ErrorMapper map = _runtimeError]) { + if (err.ref.code == 0) return; + final error = _readError(err, map); + _errorClear(err); + throw error; +} + +/// Copies a borrowed native byte run into Dart memory. +Uint8List _copyBytes(Pointer ptr, int len) => + ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); + +/// Copies an owned native byte run and releases it. +Uint8List _takeBytes(Pointer ptr, int len) { + if (ptr == nullptr) return Uint8List(0); + final bytes = Uint8List.fromList(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return bytes; +} + +/// Decodes an owned UTF-8 run and releases it. +String _takeString(Pointer ptr, int len) { + if (ptr == nullptr) return ''; + final text = utf8.decode(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return text; +} + +/// Decodes a borrowed UTF-8 run. +String _readString(Pointer ptr, int len) => + ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); + +/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. +Pointer _stage(Arena arena, List bytes) { + if (bytes.isEmpty) return nullptr; + final ptr = arena(bytes.length); + ptr.asTypedList(bytes.length).setAll(0, bytes); + return ptr; +} + +// ── Objects ── + +/// The lifetime machinery every interface wrapper shares. A wrapper owns one +/// strong reference to its native object and releases it exactly once: in +/// [dispose], or through a GC finalizer when it's collected undisposed. +/// +/// Every call borrows the pointer through [_enter] and [_leave]. A [dispose] +/// that arrives while a call is in flight (from a callback the call made) +/// only marks the wrapper; the reference is released when the last call +/// leaves, so the native object never disappears under a running call. +abstract base class _NativeObject implements Finalizable { + _NativeObject(this._ptr) { + _finalizer.attach(this, _ptr, detach: this); + } + + final Pointer _ptr; + int _calls = 0; + bool _disposed = false; + + NativeFinalizer get _finalizer; + void _destroy(Pointer ptr); + Pointer _clone(Pointer ptr); + + /// Releases this wrapper's native reference. Safe to call more than once; + /// the native object is dropped when its last reference (this one, another + /// wrapper's, or the producer's) goes away. Using the wrapper afterwards + /// throws a [StateError]. + void dispose() { + if (_disposed) return; + _disposed = true; + if (_calls == 0) _release(); + } + + void _release() { + _finalizer.detach(this); + _destroy(_ptr); + } + + Pointer _enter() { + if (_disposed) throw StateError('$runtimeType used after dispose()'); + _calls++; + return _ptr; + } + + void _leave() { + if (--_calls == 0 && _disposed) _release(); + } + + /// A second strong reference, for an object token in a value buffer. + Pointer _cloneRef() { + final ptr = _enter(); + try { + return _clone(ptr); + } finally { + _leave(); + } + } +} + +/// Borrows [object]'s pointer until [arena] is released. +Pointer _borrow(Arena arena, _NativeObject object) { + final ptr = object._enter(); + arena.onReleaseAll(object._leave); + return ptr; +} + +// ── Value buffers ── +// Records, rich enums, optionals, lists, maps, and error payloads cross the +// C ABI serialized in this little-endian, packed format. A malformed buffer +// is a contract violation between producer and bindings, never a domain +// error. + +Never _bufferError(String context) => + throw StateError('malformed value buffer: $context'); + +final class _BufferWriter { + Uint8List _buf = Uint8List(64); + late ByteData _data = ByteData.sublistView(_buf); + int _len = 0; + + void _reserve(int extra) { + if (_len + extra <= _buf.length) return; + var cap = _buf.length * 2; + while (cap < _len + extra) { + cap *= 2; + } + _buf = Uint8List(cap)..setRange(0, _len, _buf); + _data = ByteData.sublistView(_buf); + } + + Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); + + void writeBool(bool v) => writeU8(v ? 1 : 0); + + void writeFlag(bool present) => writeU8(present ? 1 : 0); + + void writeI8(int v) { + _reserve(1); + _data.setInt8(_len, v); + _len += 1; + } + + void writeU8(int v) { + _reserve(1); + _data.setUint8(_len, v); + _len += 1; + } + + void writeI16(int v) { + _reserve(2); + _data.setInt16(_len, v, Endian.little); + _len += 2; + } + + void writeU16(int v) { + _reserve(2); + _data.setUint16(_len, v, Endian.little); + _len += 2; + } + + void writeI32(int v) { + _reserve(4); + _data.setInt32(_len, v, Endian.little); + _len += 4; + } + + void writeU32(int v) { + _reserve(4); + _data.setUint32(_len, v, Endian.little); + _len += 4; + } + + void writeI64(int v) { + _reserve(8); + _data.setInt64(_len, v, Endian.little); + _len += 8; + } + + // Dart ints are signed 64-bit; a u64 travels as its bit pattern. + void writeU64(int v) => writeI64(v); + + void writeF32(double v) { + _reserve(4); + _data.setFloat32(_len, v, Endian.little); + _len += 4; + } + + void writeF64(double v) { + _reserve(8); + _data.setFloat64(_len, v, Endian.little); + _len += 8; + } + + void writeLength(int v) => writeU32(v); + + void writeString(String v) => writeBytes(utf8.encode(v)); + + void writeBytes(List v) { + writeLength(v.length); + _reserve(v.length); + _buf.setRange(_len, _len + v.length, v); + _len += v.length; + } +} + +final class _BufferReader { + _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); + + final Uint8List _buf; + final ByteData _data; + int _pos = 0; + + int _take(int n, String context) { + if (_buf.length - _pos < n) _bufferError('truncated $context'); + final at = _pos; + _pos += n; + return at; + } + + bool readBool() { + final b = _buf[_take(1, 'bool')]; + if (b > 1) _bufferError('bool byte $b'); + return b == 1; + } + + bool readFlag() { + final b = _buf[_take(1, 'option flag')]; + if (b > 1) _bufferError('option flag $b'); + return b == 1; + } + + int readI8() => _data.getInt8(_take(1, 'i8')); + + int readU8() => _buf[_take(1, 'u8')]; + + int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); + + int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); + + int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); + + int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); + + int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); + + // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. + int readU64() => readI64(); + + double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); + + double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); + + int readLength() => readU32(); + + String readString() { + final n = readLength(); + final at = _take(n, 'string'); + return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); + } + + Uint8List readBytes() { + final n = readLength(); + final at = _take(n, 'bytes'); + return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); + } + + void expectEnd() { + if (_pos != _buf.length) _bufferError('trailing bytes'); + } +} + +/// Encodes one value with [write] into a fresh buffer. +Uint8List _encode(T value, void Function(_BufferWriter, T) write) { + final writer = _BufferWriter(); + write(writer, value); + return writer.takeBytes(); +} + +/// Decodes exactly one value with [read] from [bytes]. +T _decode(Uint8List bytes, T Function(_BufferReader) read) { + final reader = _BufferReader(bytes); + final value = read(reader); + reader.expectEnd(); + return value; +} + +// ── Async completion ── +// An async call completes through a `NativeCallable.listener` the producer +// invokes exactly once, from any thread. Everything the completion carries is +// owned by these bindings, so it's decoded later on the event loop and then +// released. + +final _errorFree = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + 'kitchen_sink_error_free'); + +/// Builds the exception a completion reports and frees its boxed error. +NativeException _takeAsyncError(Pointer<_Error> err, _ErrorMapper map) { + final error = _readError(err, map); + _errorFree(err); + return error; +} + +// ── Cancellation ── + +/// Cancels the cancellable calls it's passed to. One token can serve any +/// number of calls; once cancelled it stays cancelled, and a call that +/// receives a cancelled token completes with [CancelledException] without +/// waiting for its work. +final class CancelToken { + bool _cancelled = false; + final List _listeners = []; + + /// Whether [cancel] has been called. + bool get isCancelled => _cancelled; + + /// Requests cancellation of every call this token was passed to. Calls + /// that already completed are unaffected. Idempotent. + void cancel() { + if (_cancelled) return; + _cancelled = true; + final listeners = List.of(_listeners); + _listeners.clear(); + for (final listener in listeners) { + listener(); + } + } +} + +final _cancelTokenCreate = + _lib.lookupFunction Function(), Pointer Function()>( + 'kitchen_sink_cancel_token_create'); +final _cancelTokenCancel = _lib.lookupFunction), + void Function(Pointer)>('kitchen_sink_cancel_token_cancel'); +final _cancelTokenDestroy = _lib.lookupFunction), + void Function(Pointer)>('kitchen_sink_cancel_token_destroy'); + +/// The native token behind one launch: created for the call, cancelled when +/// the [CancelToken] is, and destroyed once the call completes. The producer +/// holds its own reference, so destroying ours never races the call. +final class _NativeCancel { + _NativeCancel._(this._token) : pointer = _cancelTokenCreate() { + if (_token.isCancelled) { + _cancelTokenCancel(pointer); + } else { + _token._listeners.add(_cancel); + } + } + + /// Binds a native token to [token], or returns null for no token. + static _NativeCancel? bind(CancelToken? token) => + token == null ? null : _NativeCancel._(token); + + final CancelToken _token; + + /// The `kitchen_sink_cancel_token*` passed to the launcher. + final Pointer pointer; + + void _cancel() => _cancelTokenCancel(pointer); + + /// Detaches from the [CancelToken] and drops the native reference. + void release() { + _token._listeners.remove(_cancel); + _cancelTokenDestroy(pointer); + } +} + +// ── Callback interfaces ── +// An implementation passed to the producer is parked in [_callbacks] under +// an integer key. The `ctx` the producer receives points at a small native +// [_CallbackContext] holding that key, so the producer never holds a Dart +// object and the GC never sees a raw pointer. The entry lives until the +// producer calls the vtable's `free`. +// +// Methods that return a value are `NativeCallable.isolateLocal` trampolines: +// they run synchronously, so the producer must call them on this isolate's +// thread while a call from Dart is in progress. Void methods and `free` are +// `NativeCallable.isolateGroupBound` forwarders, which may run on any thread: +// they copy their arguments into a message for this isolate (borrowed bytes +// are copied before the forwarder returns) and the implementation runs later +// on the event loop, in the zone that passed the callback. + +/// Where forwarders send messages: the allocator they build a message with +/// and the isolate port that receives it. Forwarders run outside the +/// isolate and can't read Dart globals, so everything they need is native. +final class _CallbackPost extends Struct { + external Pointer Function(Size)>> alloc; + external Pointer)>> release; + external Pointer)>> post; + @Int64() + external int port; +} + +/// The native `ctx` of one registered implementation. +final class _CallbackContext extends Struct { + @Int64() + external int key; + external Pointer<_CallbackPost> post; +} + +// The `Dart_CObject` layout `Dart_PostCObject` reads. +final class _CArray extends Struct { + @IntPtr() + external int length; + external Pointer> values; +} + +final class _CTypedData extends Struct { + @Int32() + external int type; + @IntPtr() + external int length; + external Pointer values; +} + +final class _CValue extends Union { + @Bool() + external bool asBool; + @Int64() + external int asInt64; + @Double() + external double asDouble; + external _CArray asArray; + external _CTypedData asTypedData; +} + +final class _CObject extends Struct { + @Int32() + external int type; + external _CValue value; +} + +typedef _CallbackDispatch = void Function( + Object impl, int method, List message); + +final class _CallbackEntry { + _CallbackEntry(this.impl, this.dispatch, this.context) : zone = Zone.current; + + final Object impl; + final _CallbackDispatch dispatch; + final Pointer<_CallbackContext> context; + final Zone zone; +} + +final Map _callbacks = {}; +int _nextCallbackKey = 1; + +/// Registers [impl] and returns the `ctx` the producer receives. +Pointer _registerCallback(Object impl, _CallbackDispatch dispatch) { + final key = _nextCallbackKey++; + final context = calloc<_CallbackContext>(); + context.ref + ..key = key + ..post = _callbackPost; + _callbacks[key] = _CallbackEntry(impl, dispatch, context); + return context.cast(); +} + +/// The implementation registered under [ctx]. +Object _callbackTarget(Pointer ctx) { + final key = ctx.cast<_CallbackContext>().ref.key; + final entry = _callbacks[key]; + if (entry == null) { + throw StateError('callback context $key is not registered'); + } + return entry.impl; +} + +final Pointer<_CallbackPost> _callbackPost = () { + final port = RawReceivePort(_receiveCallbackMessage, 'callback messages'); + port.keepIsolateAlive = false; + _callbackPort = port; + final allocator = Platform.isWindows + ? DynamicLibrary.open('ole32.dll') + : DynamicLibrary.process(); + final post = calloc<_CallbackPost>(); + post.ref + ..alloc = allocator.lookup(Platform.isWindows ? 'CoTaskMemAlloc' : 'malloc') + ..release = allocator.lookup(Platform.isWindows ? 'CoTaskMemFree' : 'free') + ..post = NativeApi.postCObject.cast() + ..port = port.sendPort.nativePort; + return post; +}(); + +RawReceivePort? _callbackPort; + +void _receiveCallbackMessage(Object? message) { + final fields = message! as List; + final key = fields[0]! as int; + final method = fields[1]! as int; + if (method == _CallbackMessage.free) { + final entry = _callbacks.remove(key); + if (entry != null) calloc.free(entry.context); + return; + } + final entry = _callbacks[key]; + if (entry == null) return; + entry.zone.runGuarded(() => entry.dispatch(entry.impl, method, fields)); +} + +/// One message from a forwarder: `[key, method, arguments...]`, built in +/// native memory and posted with `Dart_PostCObject`, which copies it. Runs +/// outside the isolate, so it touches no Dart globals. +final class _CallbackMessage { + _CallbackMessage(Pointer ctx, int method, int arguments) + : _context = ctx.cast<_CallbackContext>().ref, + _capacity = arguments + 2 { + final post = _context.post.ref; + final alloc = post.alloc.asFunction Function(int)>(); + _objects = alloc(sizeOf<_CObject>() * (_capacity + 1)).cast(); + _slots = alloc(sizeOf>() * _capacity).cast(); + int64(_context.key); + int64(method); + } + + static const int free = -1; + static const int _kBool = 1; + static const int _kInt64 = 3; + static const int _kDouble = 4; + static const int _kArray = 6; + static const int _kTypedData = 7; + static const int _kUint8 = 2; + + final _CallbackContext _context; + final int _capacity; + late final Pointer<_CObject> _objects; + late final Pointer> _slots; + int _count = 0; + + _CObject _next(int type) { + final object = _objects + (_count + 1); + _slots[_count++] = object; + return object.ref..type = type; + } + + void boolean(bool v) => _next(_kBool).value.asBool = v; + + void int64(int v) => _next(_kInt64).value.asInt64 = v; + + void float64(double v) => _next(_kDouble).value.asDouble = v; + + void pointer(Pointer v) => int64(v.address); + + /// A borrowed byte run; posting copies it. + void bytes(Pointer ptr, int len) { + final data = _next(_kTypedData).value.asTypedData; + data + ..type = _kUint8 + ..length = ptr == nullptr ? 0 : len + ..values = ptr; + } + + void send() { + final post = _context.post.ref; + _objects.ref.type = _kArray; + _objects.ref.value.asArray + ..length = _count + ..values = _slots; + post.post.asFunction)>()( + post.port, _objects); + final release = post.release.asFunction)>(); + release(_slots.cast()); + release(_objects.cast()); + } +} + +// The vtable `free` entry every callback interface shares. +void _forwardCallbackFree(Pointer ctx) => + _CallbackMessage(ctx, _CallbackMessage.free, 0).send(); + +final Pointer)>> _callbackFree = + _pin(NativeCallable)>.isolateGroupBound( + _forwardCallbackFree)); + +// Vtable entries live for the process: they're never closed, so they're +// anchored here, and they don't keep an idle isolate alive. +final List> _pinned = >[]; + +Pointer> _pin(NativeCallable callable) { + callable.keepIsolateAlive = false; + _pinned.add(callable); + return callable.nativeFunction; +} + +final _errorSet = _lib.lookupFunction< + Void Function(Pointer<_Error>, Int32, Pointer), + void Function(Pointer<_Error>, int, Pointer)>('kitchen_sink_error_set'); + +/// Reports an implementation's exception to the producer through +/// `kitchen_sink_error_set` with the foreign code (-4). Never throws: it runs +/// in the catch path of a trampoline the exception must not unwind through. +void _foreignError(Pointer<_Error> outErr, Object error) { + if (outErr == nullptr) return; + Pointer message = nullptr; + try { + message = error.toString().toNativeUtf8(allocator: calloc); + _errorSet(outErr, NativeException.foreignCode, message); + } catch (_) { + _errorSet(outErr, NativeException.foreignCode, nullptr); + } finally { + if (message != nullptr) calloc.free(message); + } +} + +// ── Iterators ── + +/// Anchors one live native iterator for its GC-finalizer backstop. A +/// suspended `sync*` body keeps its anchor reachable; abandoning the +/// iteration drops the body, and the finalizer destroys the native iterator. +/// An iteration that runs to completion (or fails) detaches and destroys it +/// eagerly instead, so the iterator is destroyed exactly once either way. +final class _IteratorAnchor implements Finalizable {} + +/// The `out_item` slot iterator steps write, wide enough for any element. +final Pointer _outItem = calloc(); + +/// A reusable token referenced across modules +class Token { + /// Creates a [Token]. + Token({required this.id, required this.label}); + + final int id; + final String label; +} + +void _packToken(_BufferWriter w, Token v) { + w.writeI64(v.id); + w.writeString(v.label); +} + +Token _unpackToken(_BufferReader r) => Token( + id: r.readI64(), + label: r.readString(), +); + +final _kitchenSinkSharedPing = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_shared_ping'); + +/// Trivial cross-module helper +String ping() { + final _result = _kitchenSinkSharedPing(_outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); +} + +/// The `KitchenErrors` error domain of module `kitchen`. +class KitchenErrorsException extends NativeException { + /// Creates a domain error carrying [code] and [message]. + KitchenErrorsException(super.code, super.message); +} + +/// Item not found +class NotFoundException extends KitchenErrorsException { + NotFoundException([String message = 'Item not found']) : super(1, message); +} + +/// Invalid input +class InvalidInputException extends KitchenErrorsException { + InvalidInputException([String message = 'Invalid input']) : super(2, message); +} + +NativeException _mapKitchenErrorsException(int code, String message, Uint8List payload) { + switch (code) { + case 1: + return NotFoundException(message); + case 2: + return InvalidInputException(message); + default: + return _runtimeError(code, message, payload); + } +} + +/// Task priority levels +enum Priority { + /// Low priority + low(0), + /// Normal priority + normal(1), + /// High priority + high(2); + + const Priority(this.value); + + /// The C discriminant. + final int value; + + /// The variant whose discriminant is [value]. + static Priority fromValue(int value) => values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError.value(value, 'value', 'not a Priority discriminant')); +} + +/// Kitchen sink struct exercising every field feature +class Item { + /// Creates an [Item]. + Item({required this.id, required this.name, required this.count, required this.enabled, required this.ratio, required this.nick, required this.payload, required this.tags, required this.attrs, this.parent, required this.token, required this.priority, this.gadget}); + + /// Stable identifier + final int id; + /// Display name + final String name; + /// Initial count + final int count; + /// Whether the item is enabled + final bool enabled; + /// Scaling ratio + final double ratio; + final int nick; + final List payload; + final List tags; + final Map attrs; + final int? parent; + /// Cross-module struct reference + final Token token; + final Priority priority; + /// An optional object inside a record + final Gadget? gadget; +} + +void _packItem(_BufferWriter w, Item v) { + w.writeI64(v.id); + w.writeString(v.name); + w.writeI32(v.count); + w.writeBool(v.enabled); + w.writeF64(v.ratio); + w.writeU32(v.nick); + w.writeBytes(v.payload); + final _t0 = v.tags; + w.writeLength(_t0.length); + for (final _t1 in _t0) { + w.writeString(_t1); + } + final _t2 = v.attrs; + w.writeLength(_t2.length); + for (final _t3 in _t2.entries) { + w.writeString(_t3.key); + w.writeString(_t3.value); + } + final _t4 = v.parent; + w.writeFlag(_t4 != null); + if (_t4 != null) { + w.writeI64(_t4); + } + _packToken(w, v.token); + w.writeI32(v.priority.value); + final _t5 = v.gadget; + w.writeFlag(_t5 != null); + if (_t5 != null) { + w.writeU64(_t5._cloneRef().address); + } +} + +Item _unpackItem(_BufferReader r) => Item( + id: r.readI64(), + name: r.readString(), + count: r.readI32(), + enabled: r.readBool(), + ratio: r.readF64(), + nick: r.readU32(), + payload: r.readBytes(), + tags: [for (var i = r.readLength(); i > 0; i--) r.readString()], + attrs: {for (var i = r.readLength(); i > 0; i--) r.readString(): r.readString()}, + parent: (r.readFlag() ? r.readI64() : null), + token: _unpackToken(r), + priority: Priority.fromValue(r.readI32()), + gadget: (r.readFlag() ? Gadget._(Pointer.fromAddress(r.readU64())) : null), +); + +/// Consumer-implemented observer of ready items +/// +/// A callback interface: implement it and pass an instance to any +/// function taking a [ReadyListener]. The library keeps the instance alive +/// until the producer releases it. +/// +/// Methods that return a value run synchronously and must be called by +/// the producer on this isolate's thread during a call from Dart; a +/// thrown exception fails that call with [NativeException.foreignCode]. +/// Void methods may be called from any thread: they're delivered +/// asynchronously on this isolate's event loop, in the zone that passed +/// the instance, where a thrown exception is an uncaught error of that +/// zone. Object arguments are owned by the implementation. +abstract class ReadyListener { + /// Fires when an item is ready + void onReady(int code, String msg); + + /// Receives the item itself and says whether to keep listening + bool onItem(Item item, Gadget gadget); +} + +typedef _KitchenSinkKitchenReadyListenerOnReady = Void Function(Pointer, Int32, Pointer, Size, Pointer<_Error>); +typedef _KitchenSinkKitchenReadyListenerOnItem = Bool Function(Pointer, Pointer, Size, Pointer, Pointer<_Error>); + +// The C `kitchen_sink_kitchen_ReadyListener_vtable` layout. +final class _ReadyListenerVtable extends Struct { + external Pointer> onReady; + external Pointer> onItem; + external Pointer)>> free; +} + +// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.on_ready`: forwarded to the isolate. +void _readyListenerOnReady(Pointer ctx, int code, Pointer msgPtr, int msgLen, Pointer<_Error> outErr) { + _CallbackMessage(ctx, 0, 2) + ..int64(code) + ..bytes(msgPtr, msgLen) + ..send(); +} + +// Vtable entry `kitchen_sink_kitchen_ReadyListener_vtable.on_item`. +bool _readyListenerOnItem(Pointer ctx, Pointer itemPtr, int itemLen, Pointer gadget, Pointer<_Error> outErr) { + try { + final _gadget = Gadget._(gadget); + return (_callbackTarget(ctx) as ReadyListener).onItem(_decode(_copyBytes(itemPtr, itemLen), _unpackItem), _gadget); + } catch (e) { + _foreignError(outErr, e); + return false; + } +} + +void _readyListenerDispatch(Object impl, int method, List message) { + final target = impl as ReadyListener; + switch (method) { + case 0: { + final _a2 = message[2]! as int; + final _a3 = utf8.decode(message[3]! as Uint8List); + target.onReady(_a2, _a3); + } + } +} + +final Pointer<_ReadyListenerVtable> _readyListenerVtable = () { + final vtable = calloc<_ReadyListenerVtable>(); + vtable.ref + ..onReady = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerOnReady>.isolateGroupBound(_readyListenerOnReady)) + ..onItem = _pin(NativeCallable<_KitchenSinkKitchenReadyListenerOnItem>.isolateLocal(_readyListenerOnItem, exceptionalReturn: false)) + ..free = _callbackFree; + return vtable; +}(); + +final _kitchenSinkKitchenGadgetClone = _lib.lookupFunction< + Pointer Function(Pointer), + Pointer Function(Pointer)>('kitchen_sink_kitchen_Gadget_clone'); + +final _kitchenSinkKitchenGadgetDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('kitchen_sink_kitchen_Gadget_destroy'); +final _kitchenSinkKitchenGadgetDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('kitchen_sink_kitchen_Gadget_destroy')); + +final _kitchenSinkKitchenGadgetNew = _lib.lookupFunction< + Pointer Function(Int64, Pointer<_Error>), + Pointer Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_new'); + +final _kitchenSinkKitchenGadgetDescribe = _lib.lookupFunction< + Pointer Function(Pointer, Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_describe'); + +final _kitchenSinkKitchenGadgetPoke = _lib.lookupFunction< + Int32 Function(Pointer, Int32, Pointer<_Error>), + int Function(Pointer, int, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_poke'); + +final _kitchenSinkKitchenGadgetVersion = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_Gadget_version'); + +/// A pocket-sized interface exercising the object surface +/// +/// A reference-counted native object. Each instance holds one strong +/// reference, released by [dispose] or, when an undisposed instance is +/// collected, by a finalizer. +final class Gadget extends _NativeObject { + Gadget._(super._ptr); + + @override + NativeFinalizer get _finalizer => _kitchenSinkKitchenGadgetDestroyFinalizer; + + @override + void _destroy(Pointer ptr) => _kitchenSinkKitchenGadgetDestroy(ptr); + + @override + Pointer _clone(Pointer ptr) => _kitchenSinkKitchenGadgetClone(ptr); + + /// Create a gadget with the given id + /// + /// The returned object is owned by the caller: call its `dispose()` + /// when you're done with it. + factory Gadget(int id) { + final _result = _kitchenSinkKitchenGadgetNew(id, _err); + _check(_err); + return Gadget._(_result); + } + + /// Render the gadget as a human-readable string + String describe() { + final _self = _enter(); + try { + final _result = _kitchenSinkKitchenGadgetDescribe(_self, _outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); + } finally { + _leave(); + } + } + + /// Poke the gadget a number of times, failing on a negative count + /// + /// Throws [KitchenErrorsException] on domain errors. + int poke(int times) { + final _self = _enter(); + try { + final _result = _kitchenSinkKitchenGadgetPoke(_self, times, _err); + _check(_err, _mapKitchenErrorsException); + return _result; + } finally { + _leave(); + } + } + + /// The gadget subsystem version string + static String version() { + final _result = _kitchenSinkKitchenGadgetVersion(_outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); + } +} + +final _kitchenSinkKitchenBoolId = _lib.lookupFunction< + Bool Function(Bool, Pointer<_Error>), + bool Function(bool, Pointer<_Error>)>('kitchen_sink_kitchen_bool_id'); + +/// Identity for bool +bool boolId(bool x) { + final _result = _kitchenSinkKitchenBoolId(x, _err); + _check(_err); + return _result; +} + +final _kitchenSinkKitchenI32Id = _lib.lookupFunction< + Int32 Function(Int32, Pointer<_Error>), + int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_i32_id'); + +int i32Id(int x) { + final _result = _kitchenSinkKitchenI32Id(x, _err); + _check(_err); + return _result; +} + +final _kitchenSinkKitchenU32Id = _lib.lookupFunction< + Uint32 Function(Uint32, Pointer<_Error>), + int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_u32_id'); + +int u32Id(int x) { + final _result = _kitchenSinkKitchenU32Id(x, _err); + _check(_err); + return _result; +} + +final _kitchenSinkKitchenI64Id = _lib.lookupFunction< + Int64 Function(Int64, Pointer<_Error>), + int Function(int, Pointer<_Error>)>('kitchen_sink_kitchen_i64_id'); + +int i64Id(int x) { + final _result = _kitchenSinkKitchenI64Id(x, _err); + _check(_err); + return _result; +} + +final _kitchenSinkKitchenF64Id = _lib.lookupFunction< + Double Function(Double, Pointer<_Error>), + double Function(double, Pointer<_Error>)>('kitchen_sink_kitchen_f64_id'); + +double f64Id(double x) { + final _result = _kitchenSinkKitchenF64Id(x, _err); + _check(_err); + return _result; +} + +final _kitchenSinkKitchenEchoString = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_echo_string'); + +String echoString(String s) { + final _arena = Arena(); + try { + final _sBytes = utf8.encode(s); + final _result = _kitchenSinkKitchenEchoString(_stage(_arena, _sBytes), _sBytes.length, _outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); + } finally { + _arena.releaseAll(); + } +} + +final _kitchenSinkKitchenEchoBytes = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_echo_bytes'); + +List echoBytes(List b) { + final _arena = Arena(); + try { + final _result = _kitchenSinkKitchenEchoBytes(_stage(_arena, b), b.length, _outLen, _err); + _check(_err); + return _takeBytes(_result, _outLen.value); + } finally { + _arena.releaseAll(); + } +} + +final _kitchenSinkKitchenSubscribe = _lib.lookupFunction< + Int32 Function(Pointer, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_subscribe'); + +/// Register a listener, returning how many are attached +int subscribe(ReadyListener listener) { + final _listenerCtx = _registerCallback(listener, _readyListenerDispatch); + final _result = _kitchenSinkKitchenSubscribe(_listenerCtx, _readyListenerVtable.cast(), _err); + _check(_err); + return _result; +} + +final _kitchenSinkKitchenMaybeGadget = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_maybe_gadget'); + +/// Optional object in both directions +/// +/// Returns null when the producer reports no object. A returned object +/// is owned by the caller: call its `dispose()` when you're done with it. +Gadget? maybeGadget(Gadget? fallback) { + final _arena = Arena(); + try { + final _fallbackPtr = fallback == null ? nullptr : _borrow(_arena, fallback); + final _result = _kitchenSinkKitchenMaybeGadget(_fallbackPtr, _err); + _check(_err); + return _result == nullptr ? null : Gadget._(_result); + } finally { + _arena.releaseAll(); + } +} + +final _kitchenSinkKitchenGadgets = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_gadgets'); + +/// A list of objects +List gadgets() { + final _result = _kitchenSinkKitchenGadgets(_outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), (r) => [for (var i = r.readLength(); i > 0; i--) Gadget._(Pointer.fromAddress(r.readU64()))]); +} + +final _kitchenSinkKitchenStreamGadgets = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('kitchen_sink_kitchen_stream_gadgets'); + +final _kitchenSinkKitchenStreamGadgetsIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer>, Pointer<_Error>), + int Function(Pointer, Pointer>, Pointer<_Error>)>('kitchen_sink_kitchen_StreamGadgetsIterator_next'); + +final _kitchenSinkKitchenStreamGadgetsIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('kitchen_sink_kitchen_StreamGadgetsIterator_destroy'); +final _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('kitchen_sink_kitchen_StreamGadgetsIterator_destroy')); + +/// An iterator over objects +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +/// +/// Each element is owned by the caller: call its `dispose()` when +/// you're done with it. +Iterable streamGadgets() sync* { + final Pointer _iter; + _iter = _kitchenSinkKitchenStreamGadgets(_err); + _check(_err); + final _anchor = _IteratorAnchor(); + _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _more = _kitchenSinkKitchenStreamGadgetsIteratorNext(_iter, _outItem.cast(), _err); + _check(_err); + if (_more == 0) break; + yield Gadget._(_outItem.cast>().value); + } + } finally { + _kitchenSinkKitchenStreamGadgetsIteratorDestroyFinalizer.detach(_anchor); + _kitchenSinkKitchenStreamGadgetsIteratorDestroy(_iter); + } +} + +final _kitchenSinkKitchenMaybeItem = _lib.lookupFunction< + Pointer Function(Int64, Pointer, Pointer<_Error>), + Pointer Function(int, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_maybe_item'); + +/// Optional struct return +/// +/// Throws [KitchenErrorsException] on domain errors. +Item? maybeItem(int id) { + final _result = _kitchenSinkKitchenMaybeItem(id, _outLen, _err); + _check(_err, _mapKitchenErrorsException); + return _decode(_takeBytes(_result, _outLen.value), (r) => (r.readFlag() ? _unpackItem(r) : null)); +} + +final _kitchenSinkKitchenListItems = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_list_items'); + +/// List of structs +List listItems() { + final _result = _kitchenSinkKitchenListItems(_outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), (r) => [for (var i = r.readLength(); i > 0; i--) _unpackItem(r)]); +} + +final _kitchenSinkKitchenGetAttrs = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_get_attrs'); + +/// Map return type +Map getAttrs() { + final _result = _kitchenSinkKitchenGetAttrs(_outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), (r) => {for (var i = r.readLength(); i > 0; i--) r.readString(): r.readI32()}); +} + +final _kitchenSinkKitchenStreamItems = _lib.lookupFunction< + Pointer Function(Pointer<_Error>), + Pointer Function(Pointer<_Error>)>('kitchen_sink_kitchen_stream_items'); + +final _kitchenSinkKitchenStreamItemsIteratorNext = _lib.lookupFunction< + Int32 Function(Pointer, Pointer>, Pointer, Pointer<_Error>), + int Function(Pointer, Pointer>, Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_StreamItemsIterator_next'); + +final _kitchenSinkKitchenStreamItemsIteratorDestroy = _lib.lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('kitchen_sink_kitchen_StreamItemsIterator_destroy'); +final _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('kitchen_sink_kitchen_StreamItemsIterator_destroy')); + +/// Iterator return type +/// +/// Returns a lazy [Iterable]: each step pulls one element from a native +/// iterator, and iterating again starts a new one. The native iterator +/// is released when the iteration ends, fails, or is abandoned and +/// collected. +Iterable streamItems() sync* { + final Pointer _iter; + _iter = _kitchenSinkKitchenStreamItems(_err); + _check(_err); + final _anchor = _IteratorAnchor(); + _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor); + try { + while (true) { + final _more = _kitchenSinkKitchenStreamItemsIteratorNext(_iter, _outItem.cast(), _outLen, _err); + _check(_err); + if (_more == 0) break; + yield _decode(_takeBytes(_outItem.cast>().value, _outLen.value), _unpackItem); + } + } finally { + _kitchenSinkKitchenStreamItemsIteratorDestroyFinalizer.detach(_anchor); + _kitchenSinkKitchenStreamItemsIteratorDestroy(_iter); + } +} + +final _kitchenSinkKitchenCrossModuleToken = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_cross_module_token'); + +/// Returns the shared Token type from another module +Token crossModuleToken() { + final _result = _kitchenSinkKitchenCrossModuleToken(_outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackToken); +} + +typedef _KitchenSinkKitchenDoAsyncCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _kitchenSinkKitchenDoAsync = _lib.lookupFunction< + Void Function(Pointer, Size, Pointer>, Pointer), + void Function(Pointer, int, Pointer>, Pointer)>('kitchen_sink_kitchen_do_async'); + +/// Async operation +Future doAsync(String input) { + final _arena = Arena(); + final _completer = Completer(); + late final NativeCallable<_KitchenSinkKitchenDoAsyncCallback> _callback; + _callback = NativeCallable<_KitchenSinkKitchenDoAsyncCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value, int _valueLen) { + _callback.close(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _runtimeError); + _completer.complete(_takeString(_value, _valueLen)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + final _inputBytes = utf8.encode(input); + _kitchenSinkKitchenDoAsync(_stage(_arena, _inputBytes), _inputBytes.length, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + rethrow; + } finally { + _arena.releaseAll(); + } + return _completer.future; +} + +typedef _KitchenSinkKitchenDoCancellableCallback = Void Function(Pointer, Pointer<_Error>, Pointer, Size); + +final _kitchenSinkKitchenDoCancellable = _lib.lookupFunction< + Void Function(Pointer, Size, Pointer, Pointer>, Pointer), + void Function(Pointer, int, Pointer, Pointer>, Pointer)>('kitchen_sink_kitchen_do_cancellable'); + +/// Cancellable async operation +/// +/// Cancelling [cancelToken] completes the returned future with a +/// [CancelledException]. +Future doCancellable(String input, {CancelToken? cancelToken}) { + final _arena = Arena(); + final _completer = Completer(); + final _cancel = _NativeCancel.bind(cancelToken); + late final NativeCallable<_KitchenSinkKitchenDoCancellableCallback> _callback; + _callback = NativeCallable<_KitchenSinkKitchenDoCancellableCallback>.listener(( + Pointer _, Pointer<_Error> _error, Pointer _value, int _valueLen) { + _callback.close(); + _cancel?.release(); + try { + if (_error != nullptr) throw _takeAsyncError(_error, _runtimeError); + _completer.complete(_takeString(_value, _valueLen)); + } catch (e, s) { + _completer.completeError(e, s); + } + }); + try { + final _inputBytes = utf8.encode(input); + _kitchenSinkKitchenDoCancellable(_stage(_arena, _inputBytes), _inputBytes.length, _cancel?.pointer ?? nullptr, _callback.nativeFunction, nullptr); + } catch (_) { + _callback.close(); + _cancel?.release(); + rethrow; + } finally { + _arena.releaseAll(); + } + return _completer.future; +} + +final _kitchenSinkKitchenLegacyOp = _lib.lookupFunction< + Int32 Function(Pointer<_Error>), + int Function(Pointer<_Error>)>('kitchen_sink_kitchen_legacy_op'); + +/// Legacy operation kept for compatibility +@Deprecated('Use new_op instead') +int legacyOp() { + final _result = _kitchenSinkKitchenLegacyOp(_err); + _check(_err); + return _result; +} + +final _kitchenSinkKitchenNewOp = _lib.lookupFunction< + Int32 Function(Pointer<_Error>), + int Function(Pointer<_Error>)>('kitchen_sink_kitchen_new_op'); + +/// Replacement for legacy_op +int newOp() { + final _result = _kitchenSinkKitchenNewOp(_err); + _check(_err); + return _result; +} + +final _kitchenSinkKitchenNestedHello = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('kitchen_sink_kitchen_nested_hello'); + +/// Trivial nested-module function +String hello() { + final _result = _kitchenSinkKitchenNestedHello(_outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); +} + +// END kitchen_sink.dart 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 deleted file mode 100644 index d7a8eefa..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap +++ /dev/null @@ -1,1341 +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}'); -} - -// The ABI revision these bindings were generated against. -const int _abiVersion = 2; - -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 _NativeWeaveffiErrorSet = Void Function(Pointer<_WeaveFFIError>, Int32, Pointer); -typedef _DartWeaveffiErrorSet = void Function(Pointer<_WeaveFFIError>, int, Pointer); -final _weaveffiErrorSet = _lib.lookupFunction< - _NativeWeaveffiErrorSet, _DartWeaveffiErrorSet>('weaveffi_error_set'); - -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: a producer panic ([panicCode]), a marshalling -/// error ([marshalCode]), a callback-interface implementation that threw -/// ([foreignCode], carrying the Dart exception's text), or an unknown code. -class WeaveFFIException implements Exception { - /// The producer reported an untyped error. - static const int genericCode = -1; - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - /// An argument could not be lifted by the producer. - static const int marshalCode = -3; - /// A Dart callback-interface implementation threw; [message] carries the - /// exception's text. - static const int foreignCode = -4; - 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'); - } -} - -// ── WeaveFFI callback-interface runtime ── -// A Dart implementation passed to the producer is stored here under an -// integer key; the key (widened to a pointer) is what crosses as `ctx`, so the -// producer never holds a Dart object and the GC never sees a raw pointer. The -// entry lives until the producer calls the vtable's `free(ctx)`. -final Map _callbackTable = {}; -int _nextCallbackKey = 1; - -Pointer _registerCallback(Object impl) { - final key = _nextCallbackKey++; - _callbackTable[key] = impl; - return Pointer.fromAddress(key); -} - -Object _callbackFor(Pointer ctx) { - final impl = _callbackTable[ctx.address]; - if (impl == null) { - throw StateError('callback interface context ${ctx.address} is not registered'); - } - return impl; -} - -// Vtable trampolines are process-wide: their NativeCallables are never closed, -// so they are anchored here to keep the native thunks alive. `keepIsolateAlive` -// is cleared on each so an idle isolate can still exit. -final List> _callbackCallables = []; - -Pointer> _pinCallable( - NativeCallable callable) { - callable.keepIsolateAlive = false; - _callbackCallables.add(callable); - return callable.nativeFunction; -} - -// Reports a Dart exception thrown by a callback-interface implementation -// through `weaveffi_error_set` with the reserved foreign error code (-4). -// The producer copies the message, so the temporary is freed right away. This -// must never throw: it runs in the catch path of a trampoline whose C frame -// an exception must not unwind through. -void _foreignError(Pointer<_WeaveFFIError> outErr, Object error) { - if (outErr == nullptr) return; - Pointer message = nullptr; - try { - message = error.toString().toNativeUtf8(); - _weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, message); - } catch (_) { - _weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, nullptr); - } finally { - if (message != nullptr) calloc.free(message); - } -} - -// 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 {} - -/// A reusable token referenced across modules -class Token { - final int id; - final String label; - - Token({required this.id, required this.label}); -} - -void _packToken(_BufferWriter w, Token v) { - w.writeInt64(v.id); - w.writeString(v.label); -} - -Token _unpackToken(_BufferReader r) { - return Token( - id: r.readInt64(), - label: r.readString(), - ); -} - -typedef _NativeWeaveffiSharedPing = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiSharedPing = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiSharedPing = _lib.lookupFunction< - _NativeWeaveffiSharedPing, _DartWeaveffiSharedPing>('weaveffi_shared_ping'); - -/// Trivial cross-module helper -String ping() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiSharedPing(err); - _checkError(err); - final decoded = result.toDartString(); - _weaveffiFreeString(result); - return decoded; - } finally { - calloc.free(err); - } -} - -/// Typed error domain `KitchenErrors` declared by module `kitchen`. -class KitchenErrorsException extends WeaveFFIException { - KitchenErrorsException(super.code, super.message); -} - -/// Item not found -class NotFoundException extends KitchenErrorsException { - NotFoundException([String message = 'Item not found']) : super(1, message); -} - -/// Invalid input -class InvalidInputException extends KitchenErrorsException { - InvalidInputException([String message = 'Invalid input']) : super(2, message); -} - -WeaveFFIException _mapKitchenErrorsException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return NotFoundException(message); - case 2: - return InvalidInputException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkKitchenErrorsException(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 _mapKitchenErrorsException(code, msg, payload); - } -} - -/// Task priority levels -enum Priority { - /// Low priority - low(0), - /// Normal priority - normal(1), - /// High priority - high(2), - ; - const Priority(this.value); - final int value; - - static Priority fromValue(int value) => - Priority.values.firstWhere((e) => e.value == value); -} - -/// Kitchen sink struct exercising every field feature -class Item { - /// Stable identifier - final int id; - /// Display name - final String name; - /// Initial count - final int count; - /// Whether the item is enabled - final bool enabled; - /// Scaling ratio - final double ratio; - final int nick; - final List payload; - final List tags; - final Map attrs; - final int? parent; - /// Cross-module struct reference - final Token token; - final Priority priority; - /// An optional object inside a record - final Gadget? gadget; - - Item({required this.id, required this.name, required this.count, required this.enabled, required this.ratio, required this.nick, required this.payload, required this.tags, required this.attrs, this.parent, required this.token, required this.priority, this.gadget}); -} - -void _packItem(_BufferWriter w, Item v) { - w.writeInt64(v.id); - w.writeString(v.name); - w.writeInt32(v.count); - w.writeBool(v.enabled); - w.writeFloat64(v.ratio); - w.writeUint32(v.nick); - w.writeBytes(v.payload); - final t0 = v.tags; - w.writeLength(t0.length); - for (final t1 in t0) { - w.writeString(t1); - } - final t2 = v.attrs; - w.writeLength(t2.length); - for (final t3 in t2.entries) { - w.writeString(t3.key); - w.writeString(t3.value); - } - final t4 = v.parent; - if (t4 == null) { - w.writeOptionFlag(false); - } else { - w.writeOptionFlag(true); - w.writeInt64(t4); - } - _packToken(w, v.token); - w.writeInt32(v.priority.value); - final t5 = v.gadget; - if (t5 == null) { - w.writeOptionFlag(false); - } else { - w.writeOptionFlag(true); - w.writeUint64(t5._cloneRef().address); - } -} - -Item _unpackItem(_BufferReader r) { - return Item( - id: r.readInt64(), - name: r.readString(), - count: r.readInt32(), - enabled: r.readBool(), - ratio: r.readFloat64(), - nick: r.readUint32(), - payload: r.readBytes(), - tags: List.generate(r.readLength(), (_) => r.readString()), - attrs: { for (var i = r.readLength(); i > 0; i--) r.readString(): r.readString() }, - parent: (r.readOptionFlag() ? r.readInt64() : null), - token: _unpackToken(r), - priority: Priority.fromValue(r.readInt32()), - gadget: (r.readOptionFlag() ? Gadget._(Pointer.fromAddress(r.readUint64())) : null), - ); -} - -/// Consumer-implemented observer of ready items -/// -/// A callback interface: implement this class and pass an instance to any -/// function taking a [ReadyListener]. The producer keeps the instance alive -/// (through a private handle table) until it releases the callback, so no -/// other reference needs to be held. -/// -/// Object arguments are owned by the implementation: call `dispose()` on -/// them (or let the finalizer run) when done. A thrown exception is reported -/// to the producer, which aborts the call it was making; the original Dart -/// caller then observes a [WeaveFFIException] with [WeaveFFIException.foreignCode] -/// carrying the exception's text. -/// -/// Dart limitation: methods are bound with `NativeCallable.isolateLocal`, so -/// the producer may only invoke them on the thread of the isolate that -/// passed the callback. That holds when the producer calls them -/// synchronously during a call from Dart. A producer that invokes a method -/// from another thread (an async task, a background worker) is unsupported: -/// a `NativeCallable.listener` cannot return a value or read borrowed -/// arguments after the native frame returns. -abstract class ReadyListener { - /// Fires when an item is ready - void onReady(int code, String msg); - /// Receives the item itself and says whether to keep listening - bool onItem(Item item, Gadget gadget); -} - -typedef _NativeReadyListenerVtOnReady = Void Function(Pointer, Int32, Pointer, Pointer<_WeaveFFIError>); -typedef _NativeReadyListenerVtOnItem = Bool Function(Pointer, Pointer, Size, Pointer, Pointer<_WeaveFFIError>); -typedef _NativeReadyListenerVtFree = Void Function(Pointer); - -// The C `weaveffi_kitchen_ReadyListener_vtable` layout. -final class _ReadyListenerVtableStruct extends Struct { - external Pointer> onReady; - external Pointer> onItem; - external Pointer> free; -} - -// Vtable entry `weaveffi_kitchen_ReadyListener_vtable.on_ready`. -void _readyListenerVtOnReady(Pointer ctx, int code, Pointer msg, Pointer<_WeaveFFIError> outErr) { - try { - final impl = _callbackFor(ctx) as ReadyListener; - impl.onReady(code, msg == nullptr ? '' : msg.toDartString()); - } catch (e) { - _foreignError(outErr, e); - } -} - -// Vtable entry `weaveffi_kitchen_ReadyListener_vtable.on_item`. -bool _readyListenerVtOnItem(Pointer ctx, Pointer itemPtr, int itemLen, Pointer gadget, Pointer<_WeaveFFIError> outErr) { - try { - final itemData = _copyNativeBytes(itemPtr, itemLen); - final itemReader = _BufferReader(itemData); - final itemValue = _unpackItem(itemReader); - itemReader.expectEnd(); - final gadgetValue = Gadget._(gadget); - final impl = _callbackFor(ctx) as ReadyListener; - return impl.onItem(itemValue, gadgetValue); - } catch (e) { - _foreignError(outErr, e); - return false; - } -} - -void _readyListenerVtFree(Pointer ctx) { - _callbackTable.remove(ctx.address); -} - -// The process-wide static `weaveffi_kitchen_ReadyListener_vtable` every ReadyListener passed to the producer -// shares; the per-instance state travels in `ctx`. -final Pointer<_ReadyListenerVtableStruct> _ReadyListenerVtable = () { - final vt = calloc<_ReadyListenerVtableStruct>(); - vt.ref.onReady = _pinCallable(NativeCallable<_NativeReadyListenerVtOnReady>.isolateLocal( - _readyListenerVtOnReady)); - vt.ref.onItem = _pinCallable(NativeCallable<_NativeReadyListenerVtOnItem>.isolateLocal( - _readyListenerVtOnItem, exceptionalReturn: false)); - vt.ref.free = _pinCallable(NativeCallable<_NativeReadyListenerVtFree>.listener( - _readyListenerVtFree)); - return vt; -}(); - -typedef _NativeWeaveffiKitchenGadgetClone = Pointer Function(Pointer); -typedef _DartWeaveffiKitchenGadgetClone = Pointer Function(Pointer); -final _weaveffiKitchenGadgetClone = _lib.lookupFunction< - _NativeWeaveffiKitchenGadgetClone, _DartWeaveffiKitchenGadgetClone>('weaveffi_kitchen_Gadget_clone'); - -typedef _NativeWeaveffiKitchenGadgetDestroy = Void Function(Pointer); -typedef _DartWeaveffiKitchenGadgetDestroy = void Function(Pointer); -final _weaveffiKitchenGadgetDestroy = _lib.lookupFunction< - _NativeWeaveffiKitchenGadgetDestroy, _DartWeaveffiKitchenGadgetDestroy>('weaveffi_kitchen_Gadget_destroy'); -final _weaveffiKitchenGadgetDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('weaveffi_kitchen_Gadget_destroy')); - -typedef _NativeWeaveffiKitchenGadgetNew = Pointer Function(Int64, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenGadgetNew = Pointer Function(int, Pointer<_WeaveFFIError>); -final _weaveffiKitchenGadgetNew = _lib.lookupFunction< - _NativeWeaveffiKitchenGadgetNew, _DartWeaveffiKitchenGadgetNew>('weaveffi_kitchen_Gadget_new'); - -typedef _NativeWeaveffiKitchenGadgetDescribe = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenGadgetDescribe = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenGadgetDescribe = _lib.lookupFunction< - _NativeWeaveffiKitchenGadgetDescribe, _DartWeaveffiKitchenGadgetDescribe>('weaveffi_kitchen_Gadget_describe'); - -typedef _NativeWeaveffiKitchenGadgetPoke = Int32 Function(Pointer, Int32, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenGadgetPoke = int Function(Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiKitchenGadgetPoke = _lib.lookupFunction< - _NativeWeaveffiKitchenGadgetPoke, _DartWeaveffiKitchenGadgetPoke>('weaveffi_kitchen_Gadget_poke'); - -typedef _NativeWeaveffiKitchenGadgetVersion = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenGadgetVersion = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiKitchenGadgetVersion = _lib.lookupFunction< - _NativeWeaveffiKitchenGadgetVersion, _DartWeaveffiKitchenGadgetVersion>('weaveffi_kitchen_Gadget_version'); - -/// A pocket-sized interface exercising the object surface -/// -/// A reference-counted native object. Each instance holds one strong -/// reference, released by [dispose] or, if the instance is collected -/// without being disposed, by a GC finalizer. Instances stay valid for as -/// long as the Dart object is reachable; the producer keeps its own -/// references to any object it retains. -class Gadget implements Finalizable { - final Pointer _ptr; - bool _disposed = false; - - /// Adopts one strong reference to the native object. - Gadget._(this._ptr) { - _weaveffiKitchenGadgetDestroyFinalizer.attach(this, _ptr, detach: this); - } - - /// The borrowed native pointer for the duration of a call. - Pointer get _handle { - if (_disposed) throw StateError('Gadget used after dispose()'); - return _ptr; - } - - /// Mints a second strong reference (the interface's `_clone` symbol) for - /// an object token written into a value buffer. - Pointer _cloneRef() => _weaveffiKitchenGadgetClone(_handle); - - /// Releases this instance's native reference. Safe to call more than - /// once; the native object is dropped when its last reference (this - /// one, any other wrapper's, or the producer's) is released. - void dispose() { - if (_disposed) return; - _disposed = true; - _weaveffiKitchenGadgetDestroyFinalizer.detach(this); - _weaveffiKitchenGadgetDestroy(_ptr); - } - - /// Create a gadget with the given id - /// - /// The returned object is owned by the caller: call its `dispose()` when - /// you are done with it. - factory Gadget(int id) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenGadgetNew(id, err); - _checkError(err); - return Gadget._(result); - } finally { - calloc.free(err); - } - } - - /// Render the gadget as a human-readable string - String describe() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenGadgetDescribe(_handle, err); - _checkError(err); - final decoded = result.toDartString(); - _weaveffiFreeString(result); - return decoded; - } finally { - calloc.free(err); - } - } - - /// Poke the gadget a number of times, failing on a negative count - /// - /// Throws [KitchenErrorsException] on domain errors. - int poke(int times) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenGadgetPoke(_handle, times, err); - _checkKitchenErrorsException(err); - return result; - } finally { - calloc.free(err); - } - } - - /// The gadget subsystem version string - static String version() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenGadgetVersion(err); - _checkError(err); - final decoded = result.toDartString(); - _weaveffiFreeString(result); - return decoded; - } finally { - calloc.free(err); - } - } -} - -typedef _NativeWeaveffiKitchenBoolId = Bool Function(Bool, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenBoolId = bool Function(bool, Pointer<_WeaveFFIError>); -final _weaveffiKitchenBoolId = _lib.lookupFunction< - _NativeWeaveffiKitchenBoolId, _DartWeaveffiKitchenBoolId>('weaveffi_kitchen_bool_id'); - -/// Identity for bool -bool boolId(bool x) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenBoolId(x, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenI32Id = Int32 Function(Int32, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenI32Id = int Function(int, Pointer<_WeaveFFIError>); -final _weaveffiKitchenI32Id = _lib.lookupFunction< - _NativeWeaveffiKitchenI32Id, _DartWeaveffiKitchenI32Id>('weaveffi_kitchen_i32_id'); - -int i32Id(int x) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenI32Id(x, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenU32Id = Uint32 Function(Uint32, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenU32Id = int Function(int, Pointer<_WeaveFFIError>); -final _weaveffiKitchenU32Id = _lib.lookupFunction< - _NativeWeaveffiKitchenU32Id, _DartWeaveffiKitchenU32Id>('weaveffi_kitchen_u32_id'); - -int u32Id(int x) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenU32Id(x, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenI64Id = Int64 Function(Int64, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenI64Id = int Function(int, Pointer<_WeaveFFIError>); -final _weaveffiKitchenI64Id = _lib.lookupFunction< - _NativeWeaveffiKitchenI64Id, _DartWeaveffiKitchenI64Id>('weaveffi_kitchen_i64_id'); - -int i64Id(int x) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenI64Id(x, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenF64Id = Double Function(Double, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenF64Id = double Function(double, Pointer<_WeaveFFIError>); -final _weaveffiKitchenF64Id = _lib.lookupFunction< - _NativeWeaveffiKitchenF64Id, _DartWeaveffiKitchenF64Id>('weaveffi_kitchen_f64_id'); - -double f64Id(double x) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenF64Id(x, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenEchoString = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenEchoString = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenEchoString = _lib.lookupFunction< - _NativeWeaveffiKitchenEchoString, _DartWeaveffiKitchenEchoString>('weaveffi_kitchen_echo_string'); - -String echoString(String s) { - final sPtr = s.toNativeUtf8(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenEchoString(sPtr, err); - _checkError(err); - final decoded = result.toDartString(); - _weaveffiFreeString(result); - return decoded; - } finally { - calloc.free(sPtr); - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenEchoBytes = Pointer Function(Pointer, Size, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenEchoBytes = Pointer Function(Pointer, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenEchoBytes = _lib.lookupFunction< - _NativeWeaveffiKitchenEchoBytes, _DartWeaveffiKitchenEchoBytes>('weaveffi_kitchen_echo_bytes'); - -List echoBytes(List b) { - final bPtr = b.isEmpty ? nullptr : calloc(b.length); - for (var i = 0; i < b.length; i++) { bPtr[i] = b[i]; } - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenEchoBytes(bPtr, b.length, outLen, err); - _checkError(err); - final n = outLen.value; - if (result == nullptr) return []; - final bytes = List.generate(n, (i) => result[i]); - _weaveffiFreeBytes(result, n); - return bytes; - } finally { - if (bPtr != nullptr) calloc.free(bPtr); - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenSubscribe = Int32 Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenSubscribe = int Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenSubscribe = _lib.lookupFunction< - _NativeWeaveffiKitchenSubscribe, _DartWeaveffiKitchenSubscribe>('weaveffi_kitchen_subscribe'); - -/// Register a listener, returning how many are attached -int subscribe(ReadyListener listener) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenSubscribe(_registerCallback(listener), _ReadyListenerVtable.cast(), err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenMaybeGadget = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenMaybeGadget = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenMaybeGadget = _lib.lookupFunction< - _NativeWeaveffiKitchenMaybeGadget, _DartWeaveffiKitchenMaybeGadget>('weaveffi_kitchen_maybe_gadget'); - -/// Optional object in both directions -/// -/// Returns `null` when the producer reports no object. A non-null result -/// is owned by the caller: call its `dispose()` when you are done with it. -Gadget? maybeGadget(Gadget? fallback) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenMaybeGadget(fallback?._handle ?? nullptr, err); - _checkError(err); - return result == nullptr ? null : Gadget._(result); - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenGadgets = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenGadgets = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenGadgets = _lib.lookupFunction< - _NativeWeaveffiKitchenGadgets, _DartWeaveffiKitchenGadgets>('weaveffi_kitchen_gadgets'); - -/// A list of objects -List gadgets() { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenGadgets(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 = List.generate(reader.readLength(), (_) => Gadget._(Pointer.fromAddress(reader.readUint64()))); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenStreamGadgets = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenStreamGadgets = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiKitchenStreamGadgets = _lib.lookupFunction< - _NativeWeaveffiKitchenStreamGadgets, _DartWeaveffiKitchenStreamGadgets>('weaveffi_kitchen_stream_gadgets'); - -typedef _NativeWeaveffiKitchenStreamGadgetsIteratorNext = Int32 Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenStreamGadgetsIteratorNext = int Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); -final _weaveffiKitchenStreamGadgetsIteratorNext = _lib.lookupFunction< - _NativeWeaveffiKitchenStreamGadgetsIteratorNext, _DartWeaveffiKitchenStreamGadgetsIteratorNext>('weaveffi_kitchen_StreamGadgetsIterator_next'); - -typedef _NativeWeaveffiKitchenStreamGadgetsIteratorDestroy = Void Function(Pointer); -typedef _DartWeaveffiKitchenStreamGadgetsIteratorDestroy = void Function(Pointer); -final _weaveffiKitchenStreamGadgetsIteratorDestroy = _lib.lookupFunction< - _NativeWeaveffiKitchenStreamGadgetsIteratorDestroy, _DartWeaveffiKitchenStreamGadgetsIteratorDestroy>('weaveffi_kitchen_StreamGadgetsIterator_destroy'); -final _weaveffiKitchenStreamGadgetsIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('weaveffi_kitchen_StreamGadgetsIterator_destroy')); - -/// An iterator over objects -/// -/// 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. -/// -/// Each yielded element is owned by the caller: call its `dispose()` -/// when you are done with it. -Iterable streamGadgets() sync* { - final err = calloc<_WeaveFFIError>(); - final outItem = calloc>(); - Pointer iter = nullptr; - final anchor = _IteratorLifetime(); - try { - iter = _weaveffiKitchenStreamGadgets(err); - _checkError(err); - _weaveffiKitchenStreamGadgetsIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor); - while (_weaveffiKitchenStreamGadgetsIteratorNext(iter, outItem, err) != 0) { - _checkError(err); - final itemPtr = outItem.value; - yield Gadget._(itemPtr); - } - _checkError(err); - } finally { - if (iter != nullptr) { - _weaveffiKitchenStreamGadgetsIteratorDestroyFinalizer.detach(anchor); - _weaveffiKitchenStreamGadgetsIteratorDestroy(iter); - iter = nullptr; - } - calloc.free(outItem); - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenMaybeItem = Pointer Function(Int64, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenMaybeItem = Pointer Function(int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenMaybeItem = _lib.lookupFunction< - _NativeWeaveffiKitchenMaybeItem, _DartWeaveffiKitchenMaybeItem>('weaveffi_kitchen_maybe_item'); - -/// Optional struct return -/// -/// Throws [KitchenErrorsException] on domain errors. -Item? maybeItem(int id) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenMaybeItem(id, outLen, err); - _checkKitchenErrorsException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final decoded = (reader.readOptionFlag() ? _unpackItem(reader) : null); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenListItems = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenListItems = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenListItems = _lib.lookupFunction< - _NativeWeaveffiKitchenListItems, _DartWeaveffiKitchenListItems>('weaveffi_kitchen_list_items'); - -/// List of structs -List listItems() { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenListItems(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 = List.generate(reader.readLength(), (_) => _unpackItem(reader)); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenGetAttrs = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenGetAttrs = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenGetAttrs = _lib.lookupFunction< - _NativeWeaveffiKitchenGetAttrs, _DartWeaveffiKitchenGetAttrs>('weaveffi_kitchen_get_attrs'); - -/// Map return type -Map getAttrs() { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenGetAttrs(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 = { for (var i = reader.readLength(); i > 0; i--) reader.readString(): reader.readInt32() }; - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenStreamItems = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenStreamItems = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiKitchenStreamItems = _lib.lookupFunction< - _NativeWeaveffiKitchenStreamItems, _DartWeaveffiKitchenStreamItems>('weaveffi_kitchen_stream_items'); - -typedef _NativeWeaveffiKitchenStreamItemsIteratorNext = Int32 Function(Pointer, Pointer>, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenStreamItemsIteratorNext = int Function(Pointer, Pointer>, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenStreamItemsIteratorNext = _lib.lookupFunction< - _NativeWeaveffiKitchenStreamItemsIteratorNext, _DartWeaveffiKitchenStreamItemsIteratorNext>('weaveffi_kitchen_StreamItemsIterator_next'); - -typedef _NativeWeaveffiKitchenStreamItemsIteratorDestroy = Void Function(Pointer); -typedef _DartWeaveffiKitchenStreamItemsIteratorDestroy = void Function(Pointer); -final _weaveffiKitchenStreamItemsIteratorDestroy = _lib.lookupFunction< - _NativeWeaveffiKitchenStreamItemsIteratorDestroy, _DartWeaveffiKitchenStreamItemsIteratorDestroy>('weaveffi_kitchen_StreamItemsIterator_destroy'); -final _weaveffiKitchenStreamItemsIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('weaveffi_kitchen_StreamItemsIterator_destroy')); - -/// Iterator return type -/// -/// 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 streamItems() sync* { - final err = calloc<_WeaveFFIError>(); - final outItem = calloc>(); - final outLen = calloc(); - Pointer iter = nullptr; - final anchor = _IteratorLifetime(); - try { - iter = _weaveffiKitchenStreamItems(err); - _checkError(err); - _weaveffiKitchenStreamItemsIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor); - while (_weaveffiKitchenStreamItemsIteratorNext(iter, outItem, outLen, err) != 0) { - _checkError(err); - final itemPtr = outItem.value; - final itemLen = outLen.value; - final itemData = _copyNativeBytes(itemPtr, itemLen); - if (itemPtr != nullptr) _weaveffiFreeBytes(itemPtr, itemLen); - final itemReader = _BufferReader(itemData); - final item = _unpackItem(itemReader); - itemReader.expectEnd(); - yield item; - } - _checkError(err); - } finally { - if (iter != nullptr) { - _weaveffiKitchenStreamItemsIteratorDestroyFinalizer.detach(anchor); - _weaveffiKitchenStreamItemsIteratorDestroy(iter); - iter = nullptr; - } - calloc.free(outLen); - calloc.free(outItem); - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenCrossModuleToken = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenCrossModuleToken = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKitchenCrossModuleToken = _lib.lookupFunction< - _NativeWeaveffiKitchenCrossModuleToken, _DartWeaveffiKitchenCrossModuleToken>('weaveffi_kitchen_cross_module_token'); - -/// Returns the shared Token type from another module -Token crossModuleToken() { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenCrossModuleToken(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 = _unpackToken(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeAsyncCb_weaveffi_kitchen_do_async = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer); - -typedef _NativeWeaveffiKitchenDoAsyncAsync = Void Function(Pointer, Pointer>, Pointer); -typedef _DartWeaveffiKitchenDoAsyncAsync = void Function(Pointer, Pointer>, Pointer); -final _weaveffiKitchenDoAsyncAsync = _lib.lookupFunction< - _NativeWeaveffiKitchenDoAsyncAsync, _DartWeaveffiKitchenDoAsyncAsync>('weaveffi_kitchen_do_async_async'); - -/// Async operation -Future doAsync(String input) { - final completer = Completer(); - final inputPtr = input.toNativeUtf8(); - late NativeCallable<_NativeAsyncCb_weaveffi_kitchen_do_async> callable; - callable = NativeCallable<_NativeAsyncCb_weaveffi_kitchen_do_async>.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 { - _weaveffiKitchenDoAsyncAsync(inputPtr, callable.nativeFunction, nullptr); - } catch (e) { - callable.close(); - calloc.free(inputPtr); - rethrow; - } - return completer.future.whenComplete(() { - calloc.free(inputPtr); - }); -} - -typedef _NativeAsyncCb_weaveffi_kitchen_do_cancellable = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer); - -typedef _NativeWeaveffiKitchenDoCancellableAsync = Void Function(Pointer, Pointer, Pointer>, Pointer); -typedef _DartWeaveffiKitchenDoCancellableAsync = void Function(Pointer, Pointer, Pointer>, Pointer); -final _weaveffiKitchenDoCancellableAsync = _lib.lookupFunction< - _NativeWeaveffiKitchenDoCancellableAsync, _DartWeaveffiKitchenDoCancellableAsync>('weaveffi_kitchen_do_cancellable_async'); - -/// Cancellable async operation -Future doCancellable(String input) { - final completer = Completer(); - final inputPtr = input.toNativeUtf8(); - late NativeCallable<_NativeAsyncCb_weaveffi_kitchen_do_cancellable> callable; - callable = NativeCallable<_NativeAsyncCb_weaveffi_kitchen_do_cancellable>.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 { - _weaveffiKitchenDoCancellableAsync(inputPtr, nullptr, callable.nativeFunction, nullptr); - } catch (e) { - callable.close(); - calloc.free(inputPtr); - rethrow; - } - return completer.future.whenComplete(() { - calloc.free(inputPtr); - }); -} - -typedef _NativeWeaveffiKitchenLegacyOp = Int32 Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenLegacyOp = int Function(Pointer<_WeaveFFIError>); -final _weaveffiKitchenLegacyOp = _lib.lookupFunction< - _NativeWeaveffiKitchenLegacyOp, _DartWeaveffiKitchenLegacyOp>('weaveffi_kitchen_legacy_op'); - -/// Legacy operation kept for compatibility -@Deprecated('Use new_op instead') -int legacyOp() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenLegacyOp(err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenNewOp = Int32 Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenNewOp = int Function(Pointer<_WeaveFFIError>); -final _weaveffiKitchenNewOp = _lib.lookupFunction< - _NativeWeaveffiKitchenNewOp, _DartWeaveffiKitchenNewOp>('weaveffi_kitchen_new_op'); - -/// Replacement for legacy_op -int newOp() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenNewOp(err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiKitchenNestedHello = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKitchenNestedHello = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiKitchenNestedHello = _lib.lookupFunction< - _NativeWeaveffiKitchenNestedHello, _DartWeaveffiKitchenNestedHello>('weaveffi_kitchen_nested_hello'); - -/// Trivial nested-module function -String hello() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKitchenNestedHello(err); - _checkError(err); - final decoded = result.toDartString(); - _weaveffiFreeString(result); - return decoded; - } finally { - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__pubspec_yaml.snap index 020ab447..6d542a54 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__pubspec_yaml.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__pubspec_yaml.snap @@ -1,16 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -name: weaveffi +name: kitchen_sink version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI +description: 'Native bindings for kitchen_sink' + environment: - sdk: '>=3.1.0 <4.0.0' + sdk: '>=3.12.0 <4.0.0' + dependencies: - ffi: ^2.0.0 + ffi: ^2.1.0 # END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__README_md.snap index 732894ed..bf98166d 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__README_md.snap @@ -1,46 +1,39 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Dart) +# nested_modules (Dart) -Auto-generated Dart bindings using `dart:ffi`. +`dart:ffi` bindings for the `nested_modules` native library. ## 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: +Add this package as a dependency (a `path:` dependency works) and import it: ```dart -import 'package:weaveffi/weaveffi.dart'; +import 'package:nested_modules/nested_modules.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`. +The library loads `libnested_modules.dylib` (macOS), `libnested_modules.so` (Linux and Android), or +`nested_modules.dll` (Windows) from the platform's search path on first use. Set +`NESTED_MODULES_LIBRARY` to a full path to load a specific build. On iOS, and as a macOS +fallback, the bindings look the symbols up in the running executable. ## Objects and callbacks -Interface objects are reference counted by the native library. Each Dart -wrapper holds one reference: call `dispose()` when you are done with it, or let -the garbage collector's finalizer release it. Objects nested inside records and -collections follow the same rule. +Each interface wrapper holds one native reference: call `dispose()` when +you're done, or let the garbage collector's finalizer release it. -Callback interfaces are abstract classes: implement one and pass an instance to -any function that takes it. Methods are bound with `NativeCallable.isolateLocal`, -so the native library may only invoke them on the isolate's own thread (which -holds when it calls them synchronously during a call from Dart). An exception -thrown by an implementation aborts the native call and surfaces to the original -Dart caller as a `WeaveFFIException` with `WeaveFFIException.foreignCode`. +Callback-interface methods that return a value run synchronously on the +isolate's thread; void methods may be called from any thread and are +delivered on the event loop. ## Requirements -- Dart SDK >= 3.1.0 (for `NativeCallable`) -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. +- Dart SDK `>=3.12.0 <4.0.0` +- The `ffi` package diff --git a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_nested_modules_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_nested_modules_dart.snap new file mode 100644 index 00000000..e076f8be --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_nested_modules_dart.snap @@ -0,0 +1,547 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element + +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io' show Platform; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +// ── Library loading ── +// The native library opens on first use and is verified before any other +// lookup: its C ABI revision and every module's contract checksum must match +// the values these bindings were generated against. + +/// The native library every binding in this file calls. +final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); + +DynamicLibrary _openLibrary() { + // An explicit path wins, so a caller can point at any build artifact. + final override = Platform.environment['NESTED_MODULES_LIBRARY']; + if (override != null && override.isNotEmpty) { + return DynamicLibrary.open(override); + } + // iOS links native code into the app binary. + if (Platform.isIOS) return DynamicLibrary.process(); + final candidates = Platform.isMacOS + ? const ['libnested_modules.dylib'] + : Platform.isWindows + ? const ['nested_modules.dll'] + : const ['libnested_modules.so']; + ArgumentError? failure; + for (final candidate in candidates) { + try { + return DynamicLibrary.open(candidate); + } on ArgumentError catch (e) { + failure ??= e; + } + } + // A macOS app may link the library into its executable instead. + if (Platform.isMacOS) return DynamicLibrary.process(); + throw failure!; +} + +/// The C ABI revision these bindings were generated against. +const int _abiVersion = 3; + +/// Each top-level module's name, checksum symbol, and expected checksum. +const List<(String, String, int)> _contracts = <(String, String, int)>[ + ('graphics', 'nested_modules_graphics_checksum', 0x44d31b5b10fdb90d), +]; + +DynamicLibrary _verifyLibrary(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction( + 'nested_modules_abi_version'); + } on ArgumentError { + throw StateError( + "the native library 'nested_modules' isn't loaded; set NESTED_MODULES_LIBRARY to " + 'its path'); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError('nested_modules: these bindings expect C ABI revision ' + '$_abiVersion, but the loaded library reports revision $found'); + } + for (final (module, symbol, expected) in _contracts) { + final int actual; + try { + actual = lib.lookupFunction(symbol)(); + } on ArgumentError { + throw StateError( + "nested_modules: the loaded library doesn't export module '$module'"); + } + if (actual != expected) { + throw StateError("nested_modules: module '$module' doesn't match the loaded " + 'library (contract checksum ${_hex(actual)}, expected ' + '${_hex(expected)}); regenerate the bindings'); + } + } + return lib; +} + +String _hex(int v) => '0x${v.toUnsigned(64).toRadixString(16).padLeft(16, '0')}'; + +// ── Errors and memory ── + +/// The C `nested_modules_error` slot every fallible call writes. A non-zero +/// `code` carries a producer-owned `message` and, for a domain error with +/// fields, a value-buffer `payload`; `nested_modules_error_clear` releases both. +final class _Error extends Struct { + @Int32() + external int code; + external Pointer message; + external Pointer payloadPtr; + @Size() + external int payloadLen; +} + +final _errorClear = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + 'nested_modules_error_clear', + isLeaf: true); + +final _freeBytes = _lib.lookupFunction, Size), + void Function(Pointer, int)>('nested_modules_free_bytes', isLeaf: true); + +// Synchronous calls on this isolate share one error slot and one length +// slot: each call reads (and clears) them right after it returns, before any +// other call can reuse them, including a nested call from a callback. +final Pointer<_Error> _err = calloc<_Error>(); +final Pointer _outLen = calloc(); + +/// A failure the native library reported. Domain errors extend this class +/// with a positive [code]; the negative runtime codes are the constants +/// below. +class NativeException implements Exception { + /// Creates an exception carrying [code] and [message]. + NativeException(this.code, this.message); + + /// The producer reported an untyped error. + static const int genericCode = -1; + + /// The producer panicked; [message] carries the panic text. + static const int panicCode = -2; + + /// An argument couldn't be lifted by the producer. + static const int marshalCode = -3; + + /// A callback implementation threw; [message] carries its text. + static const int foreignCode = -4; + + /// The call was cancelled (see [CancelledException]). + static const int cancelledCode = -5; + + /// The error code: positive for a domain error, negative for a runtime + /// failure. + final int code; + + /// The producer's message. + final String message; + + @override + String toString() => '$runtimeType($code): $message'; +} + +/// A cancellable call completed because its token was cancelled. +class CancelledException extends NativeException { + /// Creates a cancellation with the producer's [message]. + CancelledException([String message = 'cancelled']) + : super(NativeException.cancelledCode, message); +} + +typedef _ErrorMapper = NativeException Function( + int code, String message, Uint8List payload); + +/// Maps a runtime failure (or an undeclared code) onto its exception. +NativeException _runtimeError(int code, String message, Uint8List payload) => + code == NativeException.cancelledCode + ? CancelledException(message) + : NativeException(code, message); + +/// Builds the exception [err] reports, using [map] for domain codes. +NativeException _readError(Pointer<_Error> err, _ErrorMapper map) { + final e = err.ref; + final message = e.message == nullptr ? '' : e.message.toDartString(); + return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); +} + +/// Throws the error a synchronous call left in [err], clearing the slot. +void _check(Pointer<_Error> err, [_ErrorMapper map = _runtimeError]) { + if (err.ref.code == 0) return; + final error = _readError(err, map); + _errorClear(err); + throw error; +} + +/// Copies a borrowed native byte run into Dart memory. +Uint8List _copyBytes(Pointer ptr, int len) => + ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); + +/// Copies an owned native byte run and releases it. +Uint8List _takeBytes(Pointer ptr, int len) { + if (ptr == nullptr) return Uint8List(0); + final bytes = Uint8List.fromList(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return bytes; +} + +/// Decodes an owned UTF-8 run and releases it. +String _takeString(Pointer ptr, int len) { + if (ptr == nullptr) return ''; + final text = utf8.decode(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return text; +} + +/// Decodes a borrowed UTF-8 run. +String _readString(Pointer ptr, int len) => + ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); + +/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. +Pointer _stage(Arena arena, List bytes) { + if (bytes.isEmpty) return nullptr; + final ptr = arena(bytes.length); + ptr.asTypedList(bytes.length).setAll(0, bytes); + return ptr; +} + +// ── Value buffers ── +// Records, rich enums, optionals, lists, maps, and error payloads cross the +// C ABI serialized in this little-endian, packed format. A malformed buffer +// is a contract violation between producer and bindings, never a domain +// error. + +Never _bufferError(String context) => + throw StateError('malformed value buffer: $context'); + +final class _BufferWriter { + Uint8List _buf = Uint8List(64); + late ByteData _data = ByteData.sublistView(_buf); + int _len = 0; + + void _reserve(int extra) { + if (_len + extra <= _buf.length) return; + var cap = _buf.length * 2; + while (cap < _len + extra) { + cap *= 2; + } + _buf = Uint8List(cap)..setRange(0, _len, _buf); + _data = ByteData.sublistView(_buf); + } + + Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); + + void writeBool(bool v) => writeU8(v ? 1 : 0); + + void writeFlag(bool present) => writeU8(present ? 1 : 0); + + void writeI8(int v) { + _reserve(1); + _data.setInt8(_len, v); + _len += 1; + } + + void writeU8(int v) { + _reserve(1); + _data.setUint8(_len, v); + _len += 1; + } + + void writeI16(int v) { + _reserve(2); + _data.setInt16(_len, v, Endian.little); + _len += 2; + } + + void writeU16(int v) { + _reserve(2); + _data.setUint16(_len, v, Endian.little); + _len += 2; + } + + void writeI32(int v) { + _reserve(4); + _data.setInt32(_len, v, Endian.little); + _len += 4; + } + + void writeU32(int v) { + _reserve(4); + _data.setUint32(_len, v, Endian.little); + _len += 4; + } + + void writeI64(int v) { + _reserve(8); + _data.setInt64(_len, v, Endian.little); + _len += 8; + } + + // Dart ints are signed 64-bit; a u64 travels as its bit pattern. + void writeU64(int v) => writeI64(v); + + void writeF32(double v) { + _reserve(4); + _data.setFloat32(_len, v, Endian.little); + _len += 4; + } + + void writeF64(double v) { + _reserve(8); + _data.setFloat64(_len, v, Endian.little); + _len += 8; + } + + void writeLength(int v) => writeU32(v); + + void writeString(String v) => writeBytes(utf8.encode(v)); + + void writeBytes(List v) { + writeLength(v.length); + _reserve(v.length); + _buf.setRange(_len, _len + v.length, v); + _len += v.length; + } +} + +final class _BufferReader { + _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); + + final Uint8List _buf; + final ByteData _data; + int _pos = 0; + + int _take(int n, String context) { + if (_buf.length - _pos < n) _bufferError('truncated $context'); + final at = _pos; + _pos += n; + return at; + } + + bool readBool() { + final b = _buf[_take(1, 'bool')]; + if (b > 1) _bufferError('bool byte $b'); + return b == 1; + } + + bool readFlag() { + final b = _buf[_take(1, 'option flag')]; + if (b > 1) _bufferError('option flag $b'); + return b == 1; + } + + int readI8() => _data.getInt8(_take(1, 'i8')); + + int readU8() => _buf[_take(1, 'u8')]; + + int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); + + int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); + + int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); + + int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); + + int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); + + // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. + int readU64() => readI64(); + + double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); + + double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); + + int readLength() => readU32(); + + String readString() { + final n = readLength(); + final at = _take(n, 'string'); + return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); + } + + Uint8List readBytes() { + final n = readLength(); + final at = _take(n, 'bytes'); + return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); + } + + void expectEnd() { + if (_pos != _buf.length) _bufferError('trailing bytes'); + } +} + +/// Encodes one value with [write] into a fresh buffer. +Uint8List _encode(T value, void Function(_BufferWriter, T) write) { + final writer = _BufferWriter(); + write(writer, value); + return writer.takeBytes(); +} + +/// Decodes exactly one value with [read] from [bytes]. +T _decode(Uint8List bytes, T Function(_BufferReader) read) { + final reader = _BufferReader(bytes); + final value = read(reader); + reader.expectEnd(); + return value; +} + +/// The `GraphicsError` error domain of module `graphics`. +class GraphicsException extends NativeException { + /// Creates a domain error carrying [code] and [message]. + GraphicsException(super.code, super.message); +} + +/// coordinate out of bounds +class OutOfBoundsException extends GraphicsException { + OutOfBoundsException([String message = 'coordinate out of bounds']) : super(1, message); +} + +NativeException _mapGraphicsException(int code, String message, Uint8List payload) { + switch (code) { + case 1: + return OutOfBoundsException(message); + default: + return _runtimeError(code, message, payload); + } +} + +/// A measurement unit shared with nested submodules. +enum Unit { + pixels(0), + points(1); + + const Unit(this.value); + + /// The C discriminant. + final int value; + + /// The variant whose discriminant is [value]. + static Unit fromValue(int value) => values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError.value(value, 'value', 'not a Unit discriminant')); +} + +/// Top-level struct that references a struct defined one level deeper. +class Canvas { + /// Creates a [Canvas]. + Canvas({required this.width, required this.root}); + + final int width; + /// Reference down into a nested module + final Shape root; +} + +void _packCanvas(_BufferWriter w, Canvas v) { + w.writeI32(v.width); + _packShape(w, v.root); +} + +Canvas _unpackCanvas(_BufferReader r) => Canvas( + width: r.readI32(), + root: _unpackShape(r), +); + +final _nestedModulesGraphicsNewCanvas = _lib.lookupFunction< + Pointer Function(Int32, Pointer, Pointer<_Error>), + Pointer Function(int, Pointer, Pointer<_Error>)>('nested_modules_graphics_new_canvas', isLeaf: true); + +/// Construct a Canvas. +Canvas newCanvas(int width) { + final _result = _nestedModulesGraphicsNewCanvas(width, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackCanvas); +} + +/// A struct one level deep that references both an ancestor enum and a deeper struct. +class Shape { + /// Creates a [Shape]. + Shape({required this.sides, required this.unit, required this.detail}); + + final int sides; + /// Reference up to the parent module's enum + final Unit unit; + /// Reference down into a deeper module + final Detail detail; +} + +void _packShape(_BufferWriter w, Shape v) { + w.writeI32(v.sides); + w.writeI32(v.unit.value); + _packDetail(w, v.detail); +} + +Shape _unpackShape(_BufferReader r) => Shape( + sides: r.readI32(), + unit: Unit.fromValue(r.readI32()), + detail: _unpackDetail(r), +); + +final _nestedModulesGraphicsShapesMakeShape = _lib.lookupFunction< + Pointer Function(Int32, Pointer, Pointer<_Error>), + Pointer Function(int, Pointer, Pointer<_Error>)>('nested_modules_graphics_shapes_make_shape', isLeaf: true); + +/// Construct a Shape. +Shape makeShape(int sides) { + final _result = _nestedModulesGraphicsShapesMakeShape(sides, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackShape); +} + +final _nestedModulesGraphicsShapesDescribe = _lib.lookupFunction< + Pointer Function(Pointer, Pointer<_Error>), + Pointer Function(Pointer, Pointer<_Error>)>('nested_modules_graphics_shapes_describe', isLeaf: true); + +/// Nested function returning a struct owned by an ancestor module. +Canvas describe() { + final _result = _nestedModulesGraphicsShapesDescribe(_outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackCanvas); +} + +/// A struct two levels deep. +class Detail { + /// Creates a [Detail]. + Detail({required this.area}); + + final double area; +} + +void _packDetail(_BufferWriter w, Detail v) { + w.writeF64(v.area); +} + +Detail _unpackDetail(_BufferReader r) => Detail( + area: r.readF64(), +); + +final _nestedModulesGraphicsShapesGeometryCompute = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('nested_modules_graphics_shapes_geometry_compute', isLeaf: true); + +/// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. +/// +/// Throws [GraphicsException] on domain errors. +Detail compute(Shape shape) { + final _arena = Arena(); + try { + final _shapeWriter = _BufferWriter(); + _packShape(_shapeWriter, shape); + final _shapeBytes = _shapeWriter.takeBytes(); + final _result = _nestedModulesGraphicsShapesGeometryCompute(_stage(_arena, _shapeBytes), _shapeBytes.length, _outLen, _err); + _check(_err, _mapGraphicsException); + return _decode(_takeBytes(_result, _outLen.value), _unpackDetail); + } finally { + _arena.releaseAll(); + } +} + +// END nested_modules.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap deleted file mode 100644 index 2315e883..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap +++ /dev/null @@ -1,570 +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}'); -} - -// The ABI revision these bindings were generated against. -const int _abiVersion = 2; - -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 _NativeWeaveffiErrorSet = Void Function(Pointer<_WeaveFFIError>, Int32, Pointer); -typedef _DartWeaveffiErrorSet = void Function(Pointer<_WeaveFFIError>, int, Pointer); -final _weaveffiErrorSet = _lib.lookupFunction< - _NativeWeaveffiErrorSet, _DartWeaveffiErrorSet>('weaveffi_error_set'); - -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: a producer panic ([panicCode]), a marshalling -/// error ([marshalCode]), a callback-interface implementation that threw -/// ([foreignCode], carrying the Dart exception's text), or an unknown code. -class WeaveFFIException implements Exception { - /// The producer reported an untyped error. - static const int genericCode = -1; - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - /// An argument could not be lifted by the producer. - static const int marshalCode = -3; - /// A Dart callback-interface implementation threw; [message] carries the - /// exception's text. - static const int foreignCode = -4; - 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 `GraphicsError` declared by module `graphics`. -class GraphicsException extends WeaveFFIException { - GraphicsException(super.code, super.message); -} - -/// coordinate out of bounds -class OutOfBoundsException extends GraphicsException { - OutOfBoundsException([String message = 'coordinate out of bounds']) : super(1, message); -} - -WeaveFFIException _mapGraphicsException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return OutOfBoundsException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkGraphicsException(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 _mapGraphicsException(code, msg, payload); - } -} - -/// A measurement unit shared with nested submodules. -enum Unit { - pixels(0), - points(1), - ; - const Unit(this.value); - final int value; - - static Unit fromValue(int value) => - Unit.values.firstWhere((e) => e.value == value); -} - -/// Top-level struct that references a struct defined one level deeper. -class Canvas { - final int width; - /// Reference down into a nested module - final Shape root; - - Canvas({required this.width, required this.root}); -} - -void _packCanvas(_BufferWriter w, Canvas v) { - w.writeInt32(v.width); - _packShape(w, v.root); -} - -Canvas _unpackCanvas(_BufferReader r) { - return Canvas( - width: r.readInt32(), - root: _unpackShape(r), - ); -} - -typedef _NativeWeaveffiGraphicsNewCanvas = Pointer Function(Int32, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiGraphicsNewCanvas = Pointer Function(int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiGraphicsNewCanvas = _lib.lookupFunction< - _NativeWeaveffiGraphicsNewCanvas, _DartWeaveffiGraphicsNewCanvas>('weaveffi_graphics_new_canvas'); - -/// Construct a Canvas. -Canvas newCanvas(int width) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiGraphicsNewCanvas(width, 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 = _unpackCanvas(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -/// A struct one level deep that references both an ancestor enum and a deeper struct. -class Shape { - final int sides; - /// Reference up to the parent module's enum - final Unit unit; - /// Reference down into a deeper module - final Detail detail; - - Shape({required this.sides, required this.unit, required this.detail}); -} - -void _packShape(_BufferWriter w, Shape v) { - w.writeInt32(v.sides); - w.writeInt32(v.unit.value); - _packDetail(w, v.detail); -} - -Shape _unpackShape(_BufferReader r) { - return Shape( - sides: r.readInt32(), - unit: Unit.fromValue(r.readInt32()), - detail: _unpackDetail(r), - ); -} - -typedef _NativeWeaveffiGraphicsShapesMakeShape = Pointer Function(Int32, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiGraphicsShapesMakeShape = Pointer Function(int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiGraphicsShapesMakeShape = _lib.lookupFunction< - _NativeWeaveffiGraphicsShapesMakeShape, _DartWeaveffiGraphicsShapesMakeShape>('weaveffi_graphics_shapes_make_shape'); - -/// Construct a Shape. -Shape makeShape(int sides) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiGraphicsShapesMakeShape(sides, 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 = _unpackShape(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiGraphicsShapesDescribe = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiGraphicsShapesDescribe = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiGraphicsShapesDescribe = _lib.lookupFunction< - _NativeWeaveffiGraphicsShapesDescribe, _DartWeaveffiGraphicsShapesDescribe>('weaveffi_graphics_shapes_describe'); - -/// Nested function returning a struct owned by an ancestor module. -Canvas describe() { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiGraphicsShapesDescribe(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 = _unpackCanvas(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -/// A struct two levels deep. -class Detail { - final double area; - - Detail({required this.area}); -} - -void _packDetail(_BufferWriter w, Detail v) { - w.writeFloat64(v.area); -} - -Detail _unpackDetail(_BufferReader r) { - return Detail( - area: r.readFloat64(), - ); -} - -typedef _NativeWeaveffiGraphicsShapesGeometryCompute = Pointer Function(Pointer, Size, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiGraphicsShapesGeometryCompute = Pointer Function(Pointer, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiGraphicsShapesGeometryCompute = _lib.lookupFunction< - _NativeWeaveffiGraphicsShapesGeometryCompute, _DartWeaveffiGraphicsShapesGeometryCompute>('weaveffi_graphics_shapes_geometry_compute'); - -/// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. -/// -/// Throws [GraphicsException] on domain errors. -Detail compute(Shape shape) { - final shapeWriter = _BufferWriter(); - _packShape(shapeWriter, shape); - final shapeBuf = shapeWriter.takeBytes(); - final shapePtr = _stageBytes(shapeBuf); - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiGraphicsShapesGeometryCompute(shapePtr, shapeBuf.length, outLen, err); - _checkGraphicsException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final decoded = _unpackDetail(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(shapePtr); - calloc.free(outLen); - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__pubspec_yaml.snap index 020ab447..3c64734f 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__pubspec_yaml.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__pubspec_yaml.snap @@ -1,16 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -name: weaveffi +name: nested_modules version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI +description: 'Native bindings for nested_modules' + environment: - sdk: '>=3.1.0 <4.0.0' + sdk: '>=3.12.0 <4.0.0' + dependencies: - ffi: ^2.0.0 + ffi: ^2.1.0 # END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_shapes__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_shapes__README_md.snap index 732894ed..56579e29 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_shapes__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_shapes__README_md.snap @@ -1,46 +1,39 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Dart) +# shapes (Dart) -Auto-generated Dart bindings using `dart:ffi`. +`dart:ffi` bindings for the `shapes` native library. ## 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: +Add this package as a dependency (a `path:` dependency works) and import it: ```dart -import 'package:weaveffi/weaveffi.dart'; +import 'package:shapes/shapes.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`. +The library loads `libshapes.dylib` (macOS), `libshapes.so` (Linux and Android), or +`shapes.dll` (Windows) from the platform's search path on first use. Set +`SHAPES_LIBRARY` to a full path to load a specific build. On iOS, and as a macOS +fallback, the bindings look the symbols up in the running executable. ## Objects and callbacks -Interface objects are reference counted by the native library. Each Dart -wrapper holds one reference: call `dispose()` when you are done with it, or let -the garbage collector's finalizer release it. Objects nested inside records and -collections follow the same rule. +Each interface wrapper holds one native reference: call `dispose()` when +you're done, or let the garbage collector's finalizer release it. -Callback interfaces are abstract classes: implement one and pass an instance to -any function that takes it. Methods are bound with `NativeCallable.isolateLocal`, -so the native library may only invoke them on the isolate's own thread (which -holds when it calls them synchronously during a call from Dart). An exception -thrown by an implementation aborts the native call and surfaces to the original -Dart caller as a `WeaveFFIException` with `WeaveFFIException.foreignCode`. +Callback-interface methods that return a value run synchronously on the +isolate's thread; void methods may be called from any thread and are +delivered on the event loop. ## Requirements -- Dart SDK >= 3.1.0 (for `NativeCallable`) -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. +- Dart SDK `>=3.12.0 <4.0.0` +- The `ffi` package diff --git a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_shapes_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_shapes_dart.snap new file mode 100644 index 00000000..4009a59c --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_shapes_dart.snap @@ -0,0 +1,611 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element + +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io' show Platform; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +// ── Library loading ── +// The native library opens on first use and is verified before any other +// lookup: its C ABI revision and every module's contract checksum must match +// the values these bindings were generated against. + +/// The native library every binding in this file calls. +final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); + +DynamicLibrary _openLibrary() { + // An explicit path wins, so a caller can point at any build artifact. + final override = Platform.environment['SHAPES_LIBRARY']; + if (override != null && override.isNotEmpty) { + return DynamicLibrary.open(override); + } + // iOS links native code into the app binary. + if (Platform.isIOS) return DynamicLibrary.process(); + final candidates = Platform.isMacOS + ? const ['libshapes.dylib'] + : Platform.isWindows + ? const ['shapes.dll'] + : const ['libshapes.so']; + ArgumentError? failure; + for (final candidate in candidates) { + try { + return DynamicLibrary.open(candidate); + } on ArgumentError catch (e) { + failure ??= e; + } + } + // A macOS app may link the library into its executable instead. + if (Platform.isMacOS) return DynamicLibrary.process(); + throw failure!; +} + +/// The C ABI revision these bindings were generated against. +const int _abiVersion = 3; + +/// Each top-level module's name, checksum symbol, and expected checksum. +const List<(String, String, int)> _contracts = <(String, String, int)>[ + ('shapes', 'shapes_shapes_checksum', 0x06acf433ffa2a0f0), +]; + +DynamicLibrary _verifyLibrary(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction( + 'shapes_abi_version'); + } on ArgumentError { + throw StateError( + "the native library 'shapes' isn't loaded; set SHAPES_LIBRARY to " + 'its path'); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError('shapes: these bindings expect C ABI revision ' + '$_abiVersion, but the loaded library reports revision $found'); + } + for (final (module, symbol, expected) in _contracts) { + final int actual; + try { + actual = lib.lookupFunction(symbol)(); + } on ArgumentError { + throw StateError( + "shapes: the loaded library doesn't export module '$module'"); + } + if (actual != expected) { + throw StateError("shapes: module '$module' doesn't match the loaded " + 'library (contract checksum ${_hex(actual)}, expected ' + '${_hex(expected)}); regenerate the bindings'); + } + } + return lib; +} + +String _hex(int v) => '0x${v.toUnsigned(64).toRadixString(16).padLeft(16, '0')}'; + +// ── Errors and memory ── + +/// The C `shapes_error` slot every fallible call writes. A non-zero +/// `code` carries a producer-owned `message` and, for a domain error with +/// fields, a value-buffer `payload`; `shapes_error_clear` releases both. +final class _Error extends Struct { + @Int32() + external int code; + external Pointer message; + external Pointer payloadPtr; + @Size() + external int payloadLen; +} + +final _errorClear = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + 'shapes_error_clear', + isLeaf: true); + +final _freeBytes = _lib.lookupFunction, Size), + void Function(Pointer, int)>('shapes_free_bytes', isLeaf: true); + +// Synchronous calls on this isolate share one error slot and one length +// slot: each call reads (and clears) them right after it returns, before any +// other call can reuse them, including a nested call from a callback. +final Pointer<_Error> _err = calloc<_Error>(); +final Pointer _outLen = calloc(); + +/// A failure the native library reported. Domain errors extend this class +/// with a positive [code]; the negative runtime codes are the constants +/// below. +class NativeException implements Exception { + /// Creates an exception carrying [code] and [message]. + NativeException(this.code, this.message); + + /// The producer reported an untyped error. + static const int genericCode = -1; + + /// The producer panicked; [message] carries the panic text. + static const int panicCode = -2; + + /// An argument couldn't be lifted by the producer. + static const int marshalCode = -3; + + /// A callback implementation threw; [message] carries its text. + static const int foreignCode = -4; + + /// The call was cancelled (see [CancelledException]). + static const int cancelledCode = -5; + + /// The error code: positive for a domain error, negative for a runtime + /// failure. + final int code; + + /// The producer's message. + final String message; + + @override + String toString() => '$runtimeType($code): $message'; +} + +/// A cancellable call completed because its token was cancelled. +class CancelledException extends NativeException { + /// Creates a cancellation with the producer's [message]. + CancelledException([String message = 'cancelled']) + : super(NativeException.cancelledCode, message); +} + +typedef _ErrorMapper = NativeException Function( + int code, String message, Uint8List payload); + +/// Maps a runtime failure (or an undeclared code) onto its exception. +NativeException _runtimeError(int code, String message, Uint8List payload) => + code == NativeException.cancelledCode + ? CancelledException(message) + : NativeException(code, message); + +/// Builds the exception [err] reports, using [map] for domain codes. +NativeException _readError(Pointer<_Error> err, _ErrorMapper map) { + final e = err.ref; + final message = e.message == nullptr ? '' : e.message.toDartString(); + return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); +} + +/// Throws the error a synchronous call left in [err], clearing the slot. +void _check(Pointer<_Error> err, [_ErrorMapper map = _runtimeError]) { + if (err.ref.code == 0) return; + final error = _readError(err, map); + _errorClear(err); + throw error; +} + +/// Copies a borrowed native byte run into Dart memory. +Uint8List _copyBytes(Pointer ptr, int len) => + ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); + +/// Copies an owned native byte run and releases it. +Uint8List _takeBytes(Pointer ptr, int len) { + if (ptr == nullptr) return Uint8List(0); + final bytes = Uint8List.fromList(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return bytes; +} + +/// Decodes an owned UTF-8 run and releases it. +String _takeString(Pointer ptr, int len) { + if (ptr == nullptr) return ''; + final text = utf8.decode(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return text; +} + +/// Decodes a borrowed UTF-8 run. +String _readString(Pointer ptr, int len) => + ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); + +/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. +Pointer _stage(Arena arena, List bytes) { + if (bytes.isEmpty) return nullptr; + final ptr = arena(bytes.length); + ptr.asTypedList(bytes.length).setAll(0, bytes); + return ptr; +} + +// ── Value buffers ── +// Records, rich enums, optionals, lists, maps, and error payloads cross the +// C ABI serialized in this little-endian, packed format. A malformed buffer +// is a contract violation between producer and bindings, never a domain +// error. + +Never _bufferError(String context) => + throw StateError('malformed value buffer: $context'); + +final class _BufferWriter { + Uint8List _buf = Uint8List(64); + late ByteData _data = ByteData.sublistView(_buf); + int _len = 0; + + void _reserve(int extra) { + if (_len + extra <= _buf.length) return; + var cap = _buf.length * 2; + while (cap < _len + extra) { + cap *= 2; + } + _buf = Uint8List(cap)..setRange(0, _len, _buf); + _data = ByteData.sublistView(_buf); + } + + Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); + + void writeBool(bool v) => writeU8(v ? 1 : 0); + + void writeFlag(bool present) => writeU8(present ? 1 : 0); + + void writeI8(int v) { + _reserve(1); + _data.setInt8(_len, v); + _len += 1; + } + + void writeU8(int v) { + _reserve(1); + _data.setUint8(_len, v); + _len += 1; + } + + void writeI16(int v) { + _reserve(2); + _data.setInt16(_len, v, Endian.little); + _len += 2; + } + + void writeU16(int v) { + _reserve(2); + _data.setUint16(_len, v, Endian.little); + _len += 2; + } + + void writeI32(int v) { + _reserve(4); + _data.setInt32(_len, v, Endian.little); + _len += 4; + } + + void writeU32(int v) { + _reserve(4); + _data.setUint32(_len, v, Endian.little); + _len += 4; + } + + void writeI64(int v) { + _reserve(8); + _data.setInt64(_len, v, Endian.little); + _len += 8; + } + + // Dart ints are signed 64-bit; a u64 travels as its bit pattern. + void writeU64(int v) => writeI64(v); + + void writeF32(double v) { + _reserve(4); + _data.setFloat32(_len, v, Endian.little); + _len += 4; + } + + void writeF64(double v) { + _reserve(8); + _data.setFloat64(_len, v, Endian.little); + _len += 8; + } + + void writeLength(int v) => writeU32(v); + + void writeString(String v) => writeBytes(utf8.encode(v)); + + void writeBytes(List v) { + writeLength(v.length); + _reserve(v.length); + _buf.setRange(_len, _len + v.length, v); + _len += v.length; + } +} + +final class _BufferReader { + _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); + + final Uint8List _buf; + final ByteData _data; + int _pos = 0; + + int _take(int n, String context) { + if (_buf.length - _pos < n) _bufferError('truncated $context'); + final at = _pos; + _pos += n; + return at; + } + + bool readBool() { + final b = _buf[_take(1, 'bool')]; + if (b > 1) _bufferError('bool byte $b'); + return b == 1; + } + + bool readFlag() { + final b = _buf[_take(1, 'option flag')]; + if (b > 1) _bufferError('option flag $b'); + return b == 1; + } + + int readI8() => _data.getInt8(_take(1, 'i8')); + + int readU8() => _buf[_take(1, 'u8')]; + + int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); + + int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); + + int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); + + int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); + + int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); + + // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. + int readU64() => readI64(); + + double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); + + double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); + + int readLength() => readU32(); + + String readString() { + final n = readLength(); + final at = _take(n, 'string'); + return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); + } + + Uint8List readBytes() { + final n = readLength(); + final at = _take(n, 'bytes'); + return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); + } + + void expectEnd() { + if (_pos != _buf.length) _bufferError('trailing bytes'); + } +} + +/// Encodes one value with [write] into a fresh buffer. +Uint8List _encode(T value, void Function(_BufferWriter, T) write) { + final writer = _BufferWriter(); + write(writer, value); + return writer.takeBytes(); +} + +/// Decodes exactly one value with [read] from [bytes]. +T _decode(Uint8List bytes, T Function(_BufferReader) read) { + final reader = _BufferReader(bytes); + final value = read(reader); + reader.expectEnd(); + return value; +} + +/// An algebraic shape (sum type with associated data) +sealed class Shape { + const Shape(); +} + +/// The empty shape (a unit variant) +final class ShapeEmpty extends Shape { + /// Creates a [ShapeEmpty]. + const ShapeEmpty(); +} + +/// A circle with a radius +final class ShapeCircle extends Shape { + /// Creates a [ShapeCircle]. + ShapeCircle(this.radius); + + /// Radius in points + final double radius; +} + +/// An axis-aligned rectangle +final class ShapeRectangle extends Shape { + /// Creates a [ShapeRectangle]. + ShapeRectangle(this.width, this.height); + + final double width; + final double height; +} + +/// An integer-bounded shape (exercises narrow + wide ints) +final class ShapeBounded extends Shape { + /// Creates a [ShapeBounded]. + ShapeBounded(this.min, this.max, this.weight); + + final int min; + final int max; + final int weight; +} + +/// A labeled shape with a small count +final class ShapeLabeled extends Shape { + /// Creates a [ShapeLabeled]. + ShapeLabeled(this.label, this.count); + + final String label; + final int count; +} + +void _packShape(_BufferWriter w, Shape v) { + switch (v) { + case ShapeEmpty(): + w.writeI32(0); + case final ShapeCircle _t0: + w.writeI32(1); + w.writeF64(_t0.radius); + case final ShapeRectangle _t1: + w.writeI32(2); + w.writeF32(_t1.width); + w.writeF32(_t1.height); + case final ShapeBounded _t2: + w.writeI32(3); + w.writeI16(_t2.min); + w.writeI16(_t2.max); + w.writeU64(_t2.weight); + case final ShapeLabeled _t3: + w.writeI32(4); + w.writeString(_t3.label); + w.writeU8(_t3.count); + } +} + +Shape _unpackShape(_BufferReader r) { + final tag = r.readI32(); + switch (tag) { + case 0: + return const ShapeEmpty(); + case 1: + return ShapeCircle(r.readF64()); + case 2: + return ShapeRectangle(r.readF32(), r.readF32()); + case 3: + return ShapeBounded(r.readI16(), r.readI16(), r.readU64()); + case 4: + return ShapeLabeled(r.readString(), r.readU8()); + default: + _bufferError('unknown Shape tag $tag'); + } +} + +/// A plain C-style enum (no payloads) +enum Channel { + red(0), + green(1), + blue(2); + + const Channel(this.value); + + /// The C discriminant. + final int value; + + /// The variant whose discriminant is [value]. + static Channel fromValue(int value) => values.firstWhere( + (e) => e.value == value, + orElse: () => throw ArgumentError.value(value, 'value', 'not a Channel discriminant')); +} + +/// One field per supported numeric primitive +class Numerics { + /// Creates a [Numerics]. + Numerics({required this.a, required this.b, required this.c, required this.d, required this.e, required this.f, required this.g, required this.h, required this.i, required this.j}); + + final int a; + final int b; + final int c; + final int d; + final int e; + final int f; + final int g; + final int h; + final double i; + final double j; +} + +void _packNumerics(_BufferWriter w, Numerics v) { + w.writeI8(v.a); + w.writeI16(v.b); + w.writeI32(v.c); + w.writeI64(v.d); + w.writeU8(v.e); + w.writeU16(v.f); + w.writeU32(v.g); + w.writeU64(v.h); + w.writeF32(v.i); + w.writeF64(v.j); +} + +Numerics _unpackNumerics(_BufferReader r) => Numerics( + a: r.readI8(), + b: r.readI16(), + c: r.readI32(), + d: r.readI64(), + e: r.readU8(), + f: r.readU16(), + g: r.readU32(), + h: r.readU64(), + i: r.readF32(), + j: r.readF64(), +); + +final _shapesShapesDescribe = _lib.lookupFunction< + Pointer Function(Pointer, Size, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, Pointer, Pointer<_Error>)>('shapes_shapes_describe', isLeaf: true); + +/// Render a shape to a string +String describe(Shape shape) { + final _arena = Arena(); + try { + final _shapeWriter = _BufferWriter(); + _packShape(_shapeWriter, shape); + final _shapeBytes = _shapeWriter.takeBytes(); + final _result = _shapesShapesDescribe(_stage(_arena, _shapeBytes), _shapeBytes.length, _outLen, _err); + _check(_err); + return _takeString(_result, _outLen.value); + } finally { + _arena.releaseAll(); + } +} + +final _shapesShapesScale = _lib.lookupFunction< + Pointer Function(Pointer, Size, Double, Pointer, Pointer<_Error>), + Pointer Function(Pointer, int, double, Pointer, Pointer<_Error>)>('shapes_shapes_scale', isLeaf: true); + +/// Scale a shape by a factor, returning a new shape +Shape scale(Shape shape, double factor) { + final _arena = Arena(); + try { + final _shapeWriter = _BufferWriter(); + _packShape(_shapeWriter, shape); + final _shapeBytes = _shapeWriter.takeBytes(); + final _result = _shapesShapesScale(_stage(_arena, _shapeBytes), _shapeBytes.length, factor, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackShape); + } finally { + _arena.releaseAll(); + } +} + +final _shapesShapesSumBytes = _lib.lookupFunction< + Uint64 Function(Pointer, Size, Pointer<_Error>), + int Function(Pointer, int, Pointer<_Error>)>('shapes_shapes_sum_bytes', isLeaf: true); + +/// Sum a list of bytes into a wide integer (numerics smoke) +int sumBytes(List values) { + final _arena = Arena(); + try { + final _result = _shapesShapesSumBytes(_stage(_arena, values), values.length, _err); + _check(_err); + return _result; + } finally { + _arena.releaseAll(); + } +} + +final _shapesShapesMakeNumerics = _lib.lookupFunction< + Pointer Function(Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64, Float, Double, Pointer, Pointer<_Error>), + Pointer Function(int, int, int, int, int, int, int, int, double, double, Pointer, Pointer<_Error>)>('shapes_shapes_make_numerics', isLeaf: true); + +/// Build a Numerics struct from every numeric primitive +Numerics makeNumerics(int a, int b, int c, int d, int e, int f, int g, int h, double i, double j) { + final _result = _shapesShapesMakeNumerics(a, b, c, d, e, f, g, h, i, j, _outLen, _err); + _check(_err); + return _decode(_takeBytes(_result, _outLen.value), _unpackNumerics); +} + +// END shapes.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap deleted file mode 100644 index 9c9fd2e0..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap +++ /dev/null @@ -1,597 +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}'); -} - -// The ABI revision these bindings were generated against. -const int _abiVersion = 2; - -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 _NativeWeaveffiErrorSet = Void Function(Pointer<_WeaveFFIError>, Int32, Pointer); -typedef _DartWeaveffiErrorSet = void Function(Pointer<_WeaveFFIError>, int, Pointer); -final _weaveffiErrorSet = _lib.lookupFunction< - _NativeWeaveffiErrorSet, _DartWeaveffiErrorSet>('weaveffi_error_set'); - -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: a producer panic ([panicCode]), a marshalling -/// error ([marshalCode]), a callback-interface implementation that threw -/// ([foreignCode], carrying the Dart exception's text), or an unknown code. -class WeaveFFIException implements Exception { - /// The producer reported an untyped error. - static const int genericCode = -1; - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - /// An argument could not be lifted by the producer. - static const int marshalCode = -3; - /// A Dart callback-interface implementation threw; [message] carries the - /// exception's text. - static const int foreignCode = -4; - 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'); - } -} - -/// An algebraic shape (sum type with associated data) -sealed class Shape { - const Shape(); -} - -/// The empty shape (a unit variant) -class ShapeEmpty extends Shape {} - -/// A circle with a radius -class ShapeCircle extends Shape { - /// Radius in points - final double radius; - - ShapeCircle(this.radius); -} - -/// An axis-aligned rectangle -class ShapeRectangle extends Shape { - final double width; - final double height; - - ShapeRectangle(this.width, this.height); -} - -/// An integer-bounded shape (exercises narrow + wide ints) -class ShapeBounded extends Shape { - final int min; - final int max; - final int weight; - - ShapeBounded(this.min, this.max, this.weight); -} - -/// A labeled shape with a small count -class ShapeLabeled extends Shape { - final String label; - final int count; - - ShapeLabeled(this.label, this.count); -} - -void _packShape(_BufferWriter w, Shape v) { - switch (v) { - case ShapeEmpty(): - w.writeInt32(0); - case final ShapeCircle t0: - w.writeInt32(1); - w.writeFloat64(t0.radius); - case final ShapeRectangle t1: - w.writeInt32(2); - w.writeFloat32(t1.width); - w.writeFloat32(t1.height); - case final ShapeBounded t2: - w.writeInt32(3); - w.writeInt16(t2.min); - w.writeInt16(t2.max); - w.writeUint64(t2.weight); - case final ShapeLabeled t3: - w.writeInt32(4); - w.writeString(t3.label); - w.writeUint8(t3.count); - } -} - -Shape _unpackShape(_BufferReader r) { - final tag = r.readInt32(); - switch (tag) { - case 0: - return ShapeEmpty(); - case 1: - return ShapeCircle(r.readFloat64()); - case 2: - return ShapeRectangle(r.readFloat32(), r.readFloat32()); - case 3: - return ShapeBounded(r.readInt16(), r.readInt16(), r.readUint64()); - case 4: - return ShapeLabeled(r.readString(), r.readUint8()); - default: - _bufferError('unknown Shape tag $tag'); - } -} - -/// A plain C-style enum (no payloads) -enum Channel { - red(0), - green(1), - blue(2), - ; - const Channel(this.value); - final int value; - - static Channel fromValue(int value) => - Channel.values.firstWhere((e) => e.value == value); -} - -/// One field per supported numeric primitive -class Numerics { - final int a; - final int b; - final int c; - final int d; - final int e; - final int f; - final int g; - final int h; - final double i; - final double j; - - Numerics({required this.a, required this.b, required this.c, required this.d, required this.e, required this.f, required this.g, required this.h, required this.i, required this.j}); -} - -void _packNumerics(_BufferWriter w, Numerics v) { - w.writeInt8(v.a); - w.writeInt16(v.b); - w.writeInt32(v.c); - w.writeInt64(v.d); - w.writeUint8(v.e); - w.writeUint16(v.f); - w.writeUint32(v.g); - w.writeUint64(v.h); - w.writeFloat32(v.i); - w.writeFloat64(v.j); -} - -Numerics _unpackNumerics(_BufferReader r) { - return Numerics( - a: r.readInt8(), - b: r.readInt16(), - c: r.readInt32(), - d: r.readInt64(), - e: r.readUint8(), - f: r.readUint16(), - g: r.readUint32(), - h: r.readUint64(), - i: r.readFloat32(), - j: r.readFloat64(), - ); -} - -typedef _NativeWeaveffiShapesDescribe = Pointer Function(Pointer, Size, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiShapesDescribe = Pointer Function(Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiShapesDescribe = _lib.lookupFunction< - _NativeWeaveffiShapesDescribe, _DartWeaveffiShapesDescribe>('weaveffi_shapes_describe'); - -/// Render a shape to a string -String describe(Shape shape) { - final shapeWriter = _BufferWriter(); - _packShape(shapeWriter, shape); - final shapeBuf = shapeWriter.takeBytes(); - final shapePtr = _stageBytes(shapeBuf); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiShapesDescribe(shapePtr, shapeBuf.length, err); - _checkError(err); - final decoded = result.toDartString(); - _weaveffiFreeString(result); - return decoded; - } finally { - calloc.free(shapePtr); - calloc.free(err); - } -} - -typedef _NativeWeaveffiShapesScale = Pointer Function(Pointer, Size, Double, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiShapesScale = Pointer Function(Pointer, int, double, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiShapesScale = _lib.lookupFunction< - _NativeWeaveffiShapesScale, _DartWeaveffiShapesScale>('weaveffi_shapes_scale'); - -/// Scale a shape by a factor, returning a new shape -Shape scale(Shape shape, double factor) { - final shapeWriter = _BufferWriter(); - _packShape(shapeWriter, shape); - final shapeBuf = shapeWriter.takeBytes(); - final shapePtr = _stageBytes(shapeBuf); - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiShapesScale(shapePtr, shapeBuf.length, factor, 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 = _unpackShape(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(shapePtr); - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiShapesSumBytes = Uint64 Function(Pointer, Size, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiShapesSumBytes = int Function(Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiShapesSumBytes = _lib.lookupFunction< - _NativeWeaveffiShapesSumBytes, _DartWeaveffiShapesSumBytes>('weaveffi_shapes_sum_bytes'); - -/// Sum a list of bytes into a wide integer (numerics smoke) -int sumBytes(List values) { - final valuesPtr = values.isEmpty ? nullptr : calloc(values.length); - for (var i = 0; i < values.length; i++) { valuesPtr[i] = values[i]; } - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiShapesSumBytes(valuesPtr, values.length, err); - _checkError(err); - return result; - } finally { - if (valuesPtr != nullptr) calloc.free(valuesPtr); - calloc.free(err); - } -} - -typedef _NativeWeaveffiShapesMakeNumerics = Pointer Function(Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64, Float, Double, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiShapesMakeNumerics = Pointer Function(int, int, int, int, int, int, int, int, double, double, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiShapesMakeNumerics = _lib.lookupFunction< - _NativeWeaveffiShapesMakeNumerics, _DartWeaveffiShapesMakeNumerics>('weaveffi_shapes_make_numerics'); - -/// Build a Numerics struct from every numeric primitive -Numerics makeNumerics(int a, int b, int c, int d, int e, int f, int g, int h, double i, double j) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiShapesMakeNumerics(a, b, c, d, e, f, g, h, i, j, 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 = _unpackNumerics(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_shapes__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_shapes__pubspec_yaml.snap index 020ab447..6b354df6 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_shapes__pubspec_yaml.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_shapes__pubspec_yaml.snap @@ -1,16 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -name: weaveffi +name: shapes version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI +description: 'Native bindings for shapes' + environment: - sdk: '>=3.1.0 <4.0.0' + sdk: '>=3.12.0 <4.0.0' + dependencies: - ffi: ^2.0.0 + ffi: ^2.1.0 # END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_cs.snap new file mode 100644 index 00000000..192249f0 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_cs.snap @@ -0,0 +1,477 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace DocsEverywhere; + +/// Typed exception for the DocsErrorsError error domain (module docs). +public class DocsErrorsException : NativeException +{ + /// Returned when no document matches the requested id + public const int NotFound = 1; + + /// Raised when attempting to move into a forbidden state + public const int InvalidStatus = 2; + + /// Creates an exception carrying a domain error code. + public DocsErrorsException(int code, string message) : base(code, message) + { + } + + internal static new Exception FromError(int code, string message, byte[]? payload) + { + switch (code) + { + case NotFound: + return new DocsErrorsException(code, string.IsNullOrEmpty(message) ? "Document not found" : message); + case InvalidStatus: + return new DocsErrorsException(code, string.IsNullOrEmpty(message) ? "Invalid status transition" : message); + default: + return global::DocsEverywhere.NativeException.FromError(code, message, payload); + } + } +} + +/// +/// Lifecycle status of a document. +/// +/// Used to drive UI badges and filters. +/// +public enum Status +{ + /// Document is being edited + Draft = 0, + /// Document is publicly visible + Published = 1, + /// Document is hidden from listings + Archived = 2, +} + +/// +/// Represents a single document tracked by the system. +/// +/// Documents are persisted to disk and exposed via the public API. +/// +public sealed class Document +{ + /// Stable opaque identifier + public long Id { get; } + + /// Human-readable title shown in lists + public string Title { get; } + + /// Current lifecycle status + public Status Status { get; } + + /// Free-form tags for grouping + public string[] Tags { get; } + + /// Creates a from every field. + public Document(long id, string title, Status status, string[] tags) + { + Id = id; + Title = title; + Status = status; + Tags = tags; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteI64(Id); + writer.WriteString(Title); + writer.WriteI32((int)Status); + writer.WriteLen(Tags.Length); + foreach (var item0 in Tags) + { + writer.WriteString(item0); + } + } + + internal static Document ReadFrom(FfiBufferReader reader) + { + var fId = reader.ReadI64(); + var fTitle = reader.ReadString(); + var fStatus = (Status)reader.ReadI32(); + var fTagsCount = reader.ReadLen(); + var fTagsList = new List(reader.Capacity(fTagsCount)); + for (var i0 = 0; i0 < fTagsCount; i0++) + { + var fTagsItem = reader.ReadString(); + fTagsList.Add(fTagsItem); + } + var fTags = fTagsList.ToArray(); + return new Document(fId, fTitle, fStatus, fTags); + } +} + +/// +/// Consumer-implemented observer of document-saved notifications. +/// +/// Implementations must be safe to call from any thread. +/// +/// The native library may call any method from any thread until +/// it releases its last reference to the implementation. Object arguments +/// belong to the implementation. An exception thrown by a method reaches +/// the native caller as . +public interface ISavedListener +{ + /// Fires after a document has been persisted successfully + /// Identifier of the saved document + /// Monotonically increasing revision + void OnSaved(long docId, int revision); +} + +/// The process-wide docs_everywhere_docs_SavedListener_vtable and the trampolines behind it, +/// adapting an to the C ABI. +internal static unsafe class FfiVtable_docs_SavedListener +{ + [StructLayout(LayoutKind.Sequential)] + private struct Layout + { + public delegate* unmanaged[Cdecl] on_saved; + public delegate* unmanaged[Cdecl] free; + } + + /// The vtable's address, valid for the process lifetime. + internal static readonly IntPtr Pointer = Allocate(); + + private static IntPtr Allocate() + { + var vtable = (Layout*)NativeMemory.Alloc((nuint)sizeof(Layout)); + vtable->on_saved = &OnSavedTrampoline; + vtable->free = &FreeTrampoline; + return (IntPtr)vtable; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void OnSavedTrampoline(IntPtr ctx, long doc_id, int revision, FfiError* out_err) + { + try + { + Ffi.Target(ctx).OnSaved(doc_id, revision); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void FreeTrampoline(IntPtr ctx) + { + Ffi.Unregister(ctx); + } +} + +/// +/// An ordered collection of documents. +/// +/// Notebooks own their documents and release them when destroyed. +/// +public sealed unsafe class Notebook : IDisposable, IEquatable +{ + /// The native object; borrowed by every call. + internal NativeHandle Handle { get; } + + private Notebook(NativeHandle handle) + { + Handle = handle; + } + + /// Wraps one strong reference the caller transfers. + internal static Notebook Adopt(IntPtr ptr) + { + return new Notebook(new NativeHandle(ptr)); + } + + /// Mints a second strong reference the caller owns, as an + /// object token in a value buffer requires. + internal IntPtr CloneHandle() + { + return NativeMethods.docs_everywhere_docs_Notebook_clone(Handle); + } + + /// Create an empty notebook + public Notebook() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_new(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeException.FromError); + Handle = new NativeHandle(ffiResult); + } + + /// Add a document, returning its position in the notebook + /// The document to add + public int Insert(Document document) + { + var documentWriter = new FfiBufferWriter(); + document.WriteTo(documentWriter); + var ffiErr = default(FfiError); + fixed (byte* documentPtr = documentWriter.Written) + { + var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_insert(Handle, documentPtr, (nuint)documentWriter.Length, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeException.FromError); + return ffiResult; + } + } + + /// Look up a document by identifier, failing when none matches + /// Identifier of the document to find + /// The call reported a DocsErrorsError code. + public Document Find(long id) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_find(Handle, id, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.DocsErrorsException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::DocsEverywhere.Document.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + + /// The number of documents currently held + public int Size() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_size(Handle, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeException.FromError); + return ffiResult; + } + + /// The largest number of documents one notebook will hold + public static int MaxDocuments() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.docs_everywhere_docs_Notebook_max_documents(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeException.FromError); + return ffiResult; + } + + /// Releases this wrapper's reference. The native object is + /// dropped once its last reference is released, and an in-flight + /// call finishes first. + public void Dispose() + { + Handle.Dispose(); + } + + /// True when both wrappers reference the same native object. + public bool Equals(Notebook? other) + { + return other is not null && other.Handle.DangerousGetHandle() == Handle.DangerousGetHandle(); + } + + /// + public override bool Equals(object? obj) + { + return obj is Notebook other && Equals(other); + } + + /// + public override int GetHashCode() + { + return Handle.DangerousGetHandle().GetHashCode(); + } + + /// One strong reference, released through + /// docs_everywhere_docs_Notebook_destroy. + internal sealed class NativeHandle : SafeHandle + { + /// A null handle, passed for an absent optional object. + internal static readonly NativeHandle Null = new NativeHandle(); + + public NativeHandle() : base(IntPtr.Zero, true) + { + } + + internal NativeHandle(IntPtr ptr) : base(IntPtr.Zero, true) + { + SetHandle(ptr); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + NativeMethods.docs_everywhere_docs_Notebook_destroy(handle); + return true; + } + } +} + +public static unsafe class Docs +{ + /// + /// Create a brand new document. + /// + /// The document starts in `Draft` status; callers must publish + /// it explicitly to make it visible. + /// + /// Human-readable title for the new document + /// Initial set of tags (may be empty) + public static Document CreateDocument(string title, string[] tags) + { + var titleBytes = Ffi.Utf8(title); + var tagsWriter = new FfiBufferWriter(); + tagsWriter.WriteLen(tags.Length); + foreach (var item0 in tags) + { + tagsWriter.WriteString(item0); + } + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* titlePtr = titleBytes, tagsPtr = tagsWriter.Written) + { + var ffiResult = NativeMethods.docs_everywhere_docs_create_document(titlePtr, (nuint)titleBytes.Length, tagsPtr, (nuint)tagsWriter.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::DocsEverywhere.Document.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + } + + /// Look up a document by identifier + /// Identifier returned from create_document + public static Document? GetDocument(long id) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.docs_everywhere_docs_get_document(id, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + Document? ffiDecoded = null; + if (ffiDecodedReader.ReadBool()) + { + var ffiDecodedValue = global::DocsEverywhere.Document.ReadFrom(ffiDecodedReader); + ffiDecoded = ffiDecodedValue; + } + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + + /// Publish a document in the background + /// Identifier of the document to publish + public static Task PublishAsync(long id) + { + var ffiCall = new FfiCall(); + try + { + NativeMethods.docs_everywhere_docs_publish_async(id, &CompletePublishAsync, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompletePublishAsync(IntPtr context, FfiError* err, byte result) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::DocsEverywhere.NativeException.FromError); + return; + } + ffiCall.SetResult(result != 0); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + /// Register a listener for saved notifications + /// Receiver of saved notifications + public static void Watch(ISavedListener listener) + { + var listenerCtx = Ffi.Register(listener); + var ffiErr = default(FfiError); + try + { + NativeMethods.docs_everywhere_docs_watch(listenerCtx, FfiVtable_docs_SavedListener.Pointer, &ffiErr); + } + catch + { + Ffi.Unregister(listenerCtx); + throw; + } + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::DocsEverywhere.NativeException.FromError); + } +} + +internal static unsafe partial class NativeMethods +{ + static partial void VerifyChecksums() + { + VerifyChecksum("docs", &docs_everywhere_docs_checksum, 0x7950eec1e9cfc819UL); + } + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_checksum")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial ulong docs_everywhere_docs_checksum(); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_clone")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr docs_everywhere_docs_Notebook_clone(Notebook.NativeHandle self); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void docs_everywhere_docs_Notebook_destroy(IntPtr self); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_new")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr docs_everywhere_docs_Notebook_new(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_insert")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int docs_everywhere_docs_Notebook_insert(Notebook.NativeHandle self, byte* document_ptr, nuint document_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_find")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* docs_everywhere_docs_Notebook_find(Notebook.NativeHandle self, long id, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_size")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int docs_everywhere_docs_Notebook_size(Notebook.NativeHandle self, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_Notebook_max_documents")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int docs_everywhere_docs_Notebook_max_documents(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_create_document")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* docs_everywhere_docs_create_document(byte* title_ptr, nuint title_len, byte* tags_ptr, nuint tags_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_get_document")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* docs_everywhere_docs_get_document(long id, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_publish_async")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void docs_everywhere_docs_publish_async(long id, delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_docs_watch")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void docs_everywhere_docs_watch(IntPtr listener_ctx, IntPtr listener_vtable, FfiError* out_err); +} + +// END DocsEverywhere.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_csproj.snap new file mode 100644 index 00000000..45661135 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__DocsEverywhere_csproj.snap @@ -0,0 +1,26 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + + + + + net8.0 + DocsEverywhere + DocsEverywhere + DocsEverywhere + 0.1.0 + Native bindings for docs_everywhere + enable + true + true + + $(NoWarn);CS0618 + + + + + diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__README_md.snap index 96a3a5e8..0e5f6afb 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__README_md.snap @@ -1,13 +1,17 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI (.NET) +# DocsEverywhere (.NET) -Auto-generated P/Invoke bindings for the WeaveFFI native library. +Native bindings for docs_everywhere + +The `DocsEverywhere` namespace binds the `docs_everywhere` native library through +source-generated P/Invoke (`[LibraryImport]`), so it's trim- and +AOT-friendly. ## Build @@ -15,12 +19,14 @@ Auto-generated P/Invoke bindings for the WeaveFFI native library. dotnet build ``` -## Pack +Reference this project from yours (`dotnet add reference DocsEverywhere.csproj`) +or pack it (`dotnet pack -c Release`). -```bash -dotnet pack -``` +## Loading the native library -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). +The bindings load `docs_everywhere` with the platform's normal rules +(`libdocs_everywhere.dylib`, `libdocs_everywhere.so`, or `docs_everywhere.dll` next to the app or on the +library search path). Set `DOCS_EVERYWHERE_LIBRARY` to a full path to load a +specific file instead. diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__Runtime_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__Runtime_cs.snap new file mode 100644 index 00000000..fa8342d0 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__Runtime_cs.snap @@ -0,0 +1,698 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace DocsEverywhere; + +/// Raised for a failure the native library reports that isn't a typed +/// domain error: producer bugs, panics, marshalling failures, and exceptions +/// thrown by a callback-interface implementation. Every typed domain +/// exception derives from it. +public class NativeException : Exception +{ + /// An untyped producer error. + public const int GenericErrorCode = -1; + /// The producer panicked; the message carries the panic text. + public const int PanicErrorCode = -2; + /// An argument couldn't be lifted by the producer. + public const int MarshalErrorCode = -3; + /// A callback-interface implementation threw; the message carries + /// the original exception's message. + public const int ForeignErrorCode = -4; + /// The call was cancelled. Surfaces as + /// , never as this type. + public const int CancelledErrorCode = -5; + + /// The error code the native library reported. + public int Code { get; } + + /// Creates an exception carrying a native error code. + public NativeException(int code, string message) : base(message) + { + Code = code; + } + + /// Maps a runtime error code to its exception: cancellation to + /// , anything else to this + /// type. + internal static Exception FromError(int code, string message, byte[]? payload) + { + if (code == CancelledErrorCode) + { + return new OperationCanceledException(message); + } + return new NativeException(code, message); + } +} + +/// Maps a native error code, message, and serialized payload to the +/// exception a call throws. +internal delegate Exception FfiErrorMap(int code, string message, byte[]? payload); + +/// The native docs_everywhere_error slot. +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct FfiError +{ + public int Code; + public byte* Message; + public byte* PayloadPtr; + public nuint PayloadLen; +} + +/// Marshalling helpers shared by every generated call. +internal static unsafe class Ffi +{ + /// The UTF-8 bytes of a string argument. The native side borrows + /// them as a (pointer, length) pair, so interior NULs survive. + internal static byte[] Utf8(string value) + { + return Encoding.UTF8.GetBytes(value); + } + + /// Decodes a borrowed UTF-8 run; the native side keeps + /// ownership. + internal static string ReadString(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? "" : Encoding.UTF8.GetString(ptr, checked((int)len)); + } + + /// Copies a borrowed byte run; the native side keeps + /// ownership. + internal static byte[] ReadBytes(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? Array.Empty() : new ReadOnlySpan(ptr, checked((int)len)).ToArray(); + } + + /// Decodes a returned UTF-8 string and releases it. + internal static string TakeString(byte* ptr, nuint len) + { + try + { + return ReadString(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies returned bytes and releases them. + internal static byte[] TakeBytes(byte* ptr, nuint len) + { + try + { + return ReadBytes(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies a returned value buffer, releases it, and returns a + /// reader over the copy. + internal static FfiBufferReader TakeBuffer(byte* ptr, nuint len) + { + return new FfiBufferReader(TakeBytes(ptr, len)); + } + + /// Releases a caller-owned error slot and returns the exception + /// it described. + internal static Exception TakeError(FfiError* err, FfiErrorMap map) + { + var code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorClear(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + /// Releases a heap-boxed async error and returns the exception it + /// described. + internal static Exception TakeBoxedError(FfiError* err, FfiErrorMap map, out int code) + { + code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorFree(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + private static string ErrorMessage(FfiError* err) + { + return Marshal.PtrToStringUTF8((IntPtr)err->Message) ?? ""; + } + + /// Reports a callback-interface failure to the producer through + /// out_err. The producer copies the message. + internal static void SetForeignError(FfiError* err, Exception e) + { + var bytes = Encoding.UTF8.GetBytes(e.Message + "\0"); + fixed (byte* message = bytes) + { + NativeMethods.ErrorSet(err, NativeException.ForeignErrorCode, message); + } + } + + /// Pins a callback-interface implementation for the producer and + /// returns the ctx it hands back to every trampoline. The + /// producer releases it through the vtable's free. + internal static IntPtr Register(object implementation) + { + return GCHandle.ToIntPtr(GCHandle.Alloc(implementation)); + } + + /// Releases a ctx the producer never received (the call + /// failed before reaching it) or no longer holds. + internal static void Unregister(IntPtr ctx) + { + GCHandle.FromIntPtr(ctx).Free(); + } + + /// The implementation behind a trampoline's ctx. + internal static T Target(IntPtr ctx) where T : class + { + return (T)GCHandle.FromIntPtr(ctx).Target!; + } +} + +/// Imports of the native runtime surface plus the load-time contract +/// check. The generated API declares its own imports in the other half of +/// this partial class. +internal static unsafe partial class NativeMethods +{ + /// The native library's base name. + internal const string LibName = "docs_everywhere"; + + /// The environment variable naming an explicit library + /// path. + internal const string LibraryEnvVar = "DOCS_EVERYWHERE_LIBRARY"; + + /// The C ABI revision these bindings were generated + /// against. + internal const uint AbiVersion = 3; + + // Runs before the first native call through this class, so a library + // built for another ABI revision or another contract fails here instead + // of misreading memory later. + static NativeMethods() + { + try + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, Resolve); + } + catch (InvalidOperationException) + { + // The assembly already has a resolver; default probing still applies. + } + uint found; + try + { + found = AbiVersionOf(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't export docs_everywhere_abi_version; these bindings expect ABI revision {AbiVersion}", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"ABI mismatch: these bindings expect revision {AbiVersion} but the loaded {LibName} library reports revision {found}"); + } + VerifyChecksums(); + } + + static partial void VerifyChecksums(); + + private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) + { + if (name != LibName) + { + return IntPtr.Zero; + } + var path = Environment.GetEnvironmentVariable(LibraryEnvVar); + return string.IsNullOrEmpty(path) ? IntPtr.Zero : NativeLibrary.Load(path); + } + + /// Fails the load when module was + /// generated from a different contract than the loaded library + /// implements. + private static void VerifyChecksum(string module, delegate* checksum, ulong expected) + { + ulong found; + try + { + found = checksum(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't implement module '{module}'", e); + } + if (found != expected) + { + throw new InvalidOperationException( + $"contract mismatch for module '{module}': these bindings expect checksum 0x{expected:x16} but the loaded {LibName} library reports 0x{found:x16}; regenerate the bindings for this library"); + } + } + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_abi_version")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial uint AbiVersionOf(); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_error_set")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorSet(FfiError* err, int code, byte* message); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_error_clear")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorClear(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_error_free")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorFree(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_free_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void FreeBytes(byte* ptr, nuint len); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_cancel_token_create")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr CancelTokenCreate(); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_cancel_token_cancel")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenCancel(IntPtr token); + + [LibraryImport(LibName, EntryPoint = "docs_everywhere_cancel_token_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenDestroy(IntPtr token); +} + +/// One pending async call: the task it completes, the native +/// context that finds it again, and, for a cancellable function, the +/// native cancel token linked to the caller's +/// . +internal sealed class FfiCall +{ + private readonly TaskCompletionSource _tcs = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationToken _cancellation; + private readonly object _gate = new object(); + private GCHandle _self; + private IntPtr _token; + private CancellationTokenRegistration _registration; + private bool _completed; + private bool _cancelling; + + internal FfiCall(CancellationToken cancellation = default) + { + _cancellation = cancellation; + _self = GCHandle.Alloc(this); + } + + /// The task the completion resolves. + internal Task Task => _tcs.Task; + + /// The context argument for the launcher. + internal IntPtr Context => GCHandle.ToIntPtr(_self); + + /// Creates the native cancel token for the launcher and cancels + /// it when the caller's fires. + internal IntPtr CancelToken() + { + _token = NativeMethods.CancelTokenCreate(); + if (_cancellation.CanBeCanceled) + { + _registration = _cancellation.Register(static state => ((FfiCall)state!).Cancel(), this); + } + return _token; + } + + /// Recovers the call behind a completion's context. Runs + /// exactly once per launch, on a producer thread. + internal static FfiCall Complete(IntPtr context) + { + var handle = GCHandle.FromIntPtr(context); + var call = (FfiCall)handle.Target!; + handle.Free(); + call.Release(); + return call; + } + + /// Releases everything when the launcher threw before the + /// producer took the call, so no completion will arrive. + internal void Abandon() + { + _self.Free(); + Release(); + } + + internal void SetResult(T value) + { + _tcs.TrySetResult(value); + } + + internal void SetException(Exception e) + { + _tcs.TrySetException(e); + } + + /// Faults (or cancels, for the cancelled code) the task from a + /// heap-boxed error, releasing the box. + internal unsafe void SetError(FfiError* err, FfiErrorMap map) + { + var e = Ffi.TakeBoxedError(err, map, out var code); + if (code == NativeException.CancelledErrorCode) + { + _tcs.TrySetCanceled(_cancellation); + } + else + { + _tcs.TrySetException(e); + } + } + + // The registration callback and the completion race: whichever runs + // last destroys the consumer's token reference, so a cancel never + // touches a destroyed token. + private void Cancel() + { + lock (_gate) + { + if (_completed) + { + return; + } + _cancelling = true; + } + NativeMethods.CancelTokenCancel(_token); + bool destroy; + lock (_gate) + { + _cancelling = false; + destroy = _completed; + } + if (destroy) + { + NativeMethods.CancelTokenDestroy(_token); + } + } + + private void Release() + { + if (_token == IntPtr.Zero) + { + return; + } + bool destroy; + lock (_gate) + { + _completed = true; + destroy = !_cancelling; + } + if (destroy) + { + _registration.Dispose(); + NativeMethods.CancelTokenDestroy(_token); + } + } +} + +/// Owns one native iterator and destroys it exactly once, when +/// enumeration ends, the enumerator is disposed, or the garbage collector +/// finalizes an abandoned one. +internal sealed unsafe class FfiIteratorHandle : SafeHandle +{ + private readonly delegate* _destroy; + + internal FfiIteratorHandle(IntPtr iterator, delegate* destroy) + : base(IntPtr.Zero, true) + { + _destroy = destroy; + SetHandle(iterator); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + _destroy(handle); + return true; + } +} + +/// A lazily streamed sequence backed by a native iterator. It can be +/// enumerated once; each MoveNext pulls one item. +internal sealed unsafe class FfiSequence : IEnumerable, IEnumerator +{ + private readonly FfiIteratorHandle _iterator; + private readonly delegate* _next; + private int _started; + private T _current = default!; + + internal FfiSequence(FfiIteratorHandle iterator, delegate* next) + { + _iterator = iterator; + _next = next; + } + + public T Current => _current; + + object? IEnumerator.Current => _current; + + public IEnumerator GetEnumerator() + { + if (Interlocked.Exchange(ref _started, 1) != 0) + { + throw new InvalidOperationException("this sequence can be enumerated only once"); + } + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool MoveNext() + { + if (_iterator.IsClosed) + { + return false; + } + if (_next(_iterator, out _current)) + { + return true; + } + _iterator.Dispose(); + return false; + } + + public void Reset() + { + throw new NotSupportedException("a native iterator can't be reset"); + } + + public void Dispose() + { + _iterator.Dispose(); + } +} + +/// Serializes values into the value-buffer wire format +/// (little-endian, packed). +internal sealed class FfiBufferWriter +{ + private byte[] _buf = new byte[64]; + private int _len; + + /// The bytes written so far. + internal ReadOnlySpan Written => _buf.AsSpan(0, _len); + + /// The number of bytes written so far. + internal int Length => _len; + + private Span Grow(int extra) + { + if (_len + extra > _buf.Length) + { + Array.Resize(ref _buf, Math.Max(_buf.Length * 2, _len + extra)); + } + var span = _buf.AsSpan(_len, extra); + _len += extra; + return span; + } + + internal void WriteBool(bool v) => WriteU8(v ? (byte)1 : (byte)0); + + internal void WriteI8(sbyte v) => WriteU8((byte)v); + + internal void WriteU8(byte v) => Grow(1)[0] = v; + + internal void WriteI16(short v) => System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); + + internal void WriteU16(ushort v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); + + internal void WriteI32(int v) => System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); + + internal void WriteU32(uint v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); + + internal void WriteI64(long v) => System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); + + internal void WriteU64(ulong v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); + + internal void WriteF32(float v) => System.Buffers.Binary.BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); + + internal void WriteF64(double v) => System.Buffers.Binary.BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); + + internal void WriteLen(int len) => WriteU32((uint)len); + + internal void WriteString(string v) + { + var count = Encoding.UTF8.GetByteCount(v); + WriteLen(count); + Encoding.UTF8.GetBytes(v, Grow(count)); + } + + internal void WriteBytes(byte[] v) + { + WriteLen(v.Length); + v.CopyTo(Grow(v.Length)); + } + + /// Writes an object token. The token must carry its own strong + /// reference (a fresh clone), which the reader adopts. + internal void WriteObject(IntPtr token) => WriteU64((ulong)(nuint)token); +} + +/// Decodes values from the value-buffer wire format. A malformed +/// buffer is a producer or consumer bug and throws +/// . +internal sealed class FfiBufferReader +{ + private static readonly Encoding Utf8Strict = new UTF8Encoding(false, true); + + private readonly byte[] _data; + private int _pos; + + internal FfiBufferReader(byte[] data) + { + _data = data; + } + + private static InvalidOperationException Malformed(string what) + { + return new InvalidOperationException("malformed value buffer: " + what); + } + + private ReadOnlySpan Take(int n) + { + if (_data.Length - _pos < n) + { + throw Malformed("buffer exhausted"); + } + var span = _data.AsSpan(_pos, n); + _pos += n; + return span; + } + + internal bool ReadBool() + { + var b = ReadU8(); + if (b > 1) + { + throw Malformed("invalid bool byte"); + } + return b == 1; + } + + internal sbyte ReadI8() => (sbyte)ReadU8(); + + internal byte ReadU8() => Take(1)[0]; + + internal short ReadI16() => System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(Take(2)); + + internal ushort ReadU16() => System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); + + internal int ReadI32() => System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(Take(4)); + + internal uint ReadU32() => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); + + internal long ReadI64() => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(Take(8)); + + internal ulong ReadU64() => System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); + + internal float ReadF32() => System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(Take(4)); + + internal double ReadF64() => System.Buffers.Binary.BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); + + /// A collection count. Elements can be zero-sized, so the count + /// isn't checked against the remaining bytes; callers cap their + /// preallocation with . + internal int ReadLen() + { + var len = ReadU32(); + if (len > int.MaxValue) + { + throw Malformed("length prefix out of range"); + } + return (int)len; + } + + /// A preallocation size for elements + /// that a malformed count can't inflate. + internal int Capacity(int count) => Math.Min(count, _data.Length - _pos); + + internal string ReadString() + { + var bytes = Take(ReadLen()); + try + { + return Utf8Strict.GetString(bytes); + } + catch (ArgumentException) + { + throw Malformed("string is not valid UTF-8"); + } + } + + internal byte[] ReadBytes() => Take(ReadLen()).ToArray(); + + /// Reads an object token carrying one strong reference. + internal IntPtr ReadObject() + { + var token = ReadU64(); + if (token == 0) + { + throw Malformed("null object token"); + } + return (IntPtr)(nint)token; + } + + internal void ExpectEnd() + { + if (_pos != _data.Length) + { + throw Malformed("trailing bytes"); + } + } +} + +// END Runtime.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap deleted file mode 100644 index 73b2ef2a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap +++ /dev/null @@ -1,911 +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.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace WeaveFFI -{ - /// Raised for any WeaveFFI failure that isn't a typed domain - /// error: producer bugs, panics, marshalling failures, and exceptions - /// thrown by a callback-interface implementation. - public class WeaveFFIException : Exception - { - /// An untyped producer error. - public const int GenericErrorCode = -1; - /// The producer panicked; the message carries the panic text. - public const int PanicErrorCode = -2; - /// An argument couldn't be lifted by the producer. - public const int MarshalErrorCode = -3; - /// A callback-interface implementation threw; the message - /// carries the original exception's message. - public const int ForeignErrorCode = -4; - - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - /// Typed exception for the DocsErrorsError error domain (module docs). - public class DocsErrorsException : WeaveFFIException - { - /// Returned when no document matches the requested id - public const int NotFound = 1; - /// Raised when attempting to move into a forbidden state - public const int InvalidStatus = 2; - - public DocsErrorsException(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 NotFound: - return new DocsErrorsException(code, string.IsNullOrEmpty(message) ? "Document not found" : message); - case InvalidStatus: - return new DocsErrorsException(code, string.IsNullOrEmpty(message) ? "Invalid status transition" : 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 CheckDocsErrors(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 DocsErrorsException.FromCode(code, msg, payload); - } - } - } - - /// One strong reference to a native object, on its way into - /// a wrapper that adopts it. A distinct type (rather than a bare - /// IntPtr) so the adopting constructor never competes with a - /// public constructor taking an integer. - internal readonly struct WeaveFFIHandle - { - internal readonly IntPtr Value; - - internal WeaveFFIHandle(IntPtr value) - { - Value = value; - } - } - - 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 void WriteObject(IntPtr token) - { - WriteU64((ulong)(nuint)(nint)token); - } - - 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 IntPtr ReadObject() - { - var token = ReadU64(); - if (token == 0) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: null object token"); - } - return (nint)(nuint)token; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - /// - /// Lifecycle status of a document. - /// - /// Used to drive UI badges and filters. - /// - public enum Status - { - /// Document is being edited - Draft = 0, - /// Document is publicly visible - Published = 1, - /// Document is hidden from listings - Archived = 2, - } - - /// - /// Represents a single document tracked by the system. - /// - /// Documents are persisted to disk and exposed via the public API. - /// - public sealed class Document - { - /// Stable opaque identifier - public long Id { get; } - - /// Human-readable title shown in lists - public string Title { get; } - - /// Current lifecycle status - public Status Status { get; } - - /// Free-form tags for grouping - public string[] Tags { get; } - - public Document(long id, string title, Status status, string[] tags) - { - Id = id; - Title = title; - Status = status; - Tags = tags; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteString(Title); - writer.WriteI32((int)Status); - writer.WriteLen(Tags.Length); - foreach (var item0 in Tags) - { - writer.WriteString(item0); - } - } - - internal static Document ReadFrom(WeaveFFIBufferReader reader) - { - var fId = reader.ReadI64(); - var fTitle = reader.ReadString(); - var fStatus = (Status)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 Document(fId, fTitle, fStatus, fTags); - } - } - - /// - /// Consumer-implemented observer of document-saved notifications. - /// - /// Implementations must be safe to call from any thread. - /// - /// Implement this interface and pass an instance wherever the - /// native library expects it. The library may call any method from any - /// thread until it releases its last reference to the instance. Object - /// arguments are owned by the implementation, which should dispose them - /// when done. An exception thrown by a method is reported to the native - /// caller as . - public interface ISavedListener - { - /// Fires after a document has been persisted successfully - /// Identifier of the saved document - /// Monotonically increasing revision - void OnSaved(long docId, int revision); - } - - /// The process-wide weaveffi_docs_SavedListener_vtable vtable and the trampolines behind - /// it, adapting an to the C ABI. - internal static unsafe class WeaveFFIVtable_docs_SavedListener - { - [StructLayout(LayoutKind.Sequential)] - private struct Layout - { - public IntPtr on_saved; - public IntPtr free; - } - - /// Address of the one static vtable for this interface. It is - /// allocated once and never freed, so the producer may keep the pointer - /// for the process lifetime. - internal static readonly IntPtr Pointer = Allocate(); - - private static IntPtr Allocate() - { - var layout = new Layout - { - on_saved = (IntPtr)(delegate* unmanaged[Cdecl])&OnSavedTrampoline, - free = (IntPtr)(delegate* unmanaged[Cdecl])&FreeTrampoline, - }; - var mem = Marshal.AllocHGlobal(Marshal.SizeOf()); - Marshal.StructureToPtr(layout, mem, false); - return mem; - } - - private static ISavedListener Target(IntPtr ctx) - { - return (ISavedListener)GCHandle.FromIntPtr(ctx).Target!; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnSavedTrampoline(IntPtr ctx, long doc_id, int revision, IntPtr out_err) - { - try - { - var impl = Target(ctx); - impl.OnSaved(doc_id, revision); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void FreeTrampoline(IntPtr ctx) - { - GCHandle.FromIntPtr(ctx).Free(); - } - } - - /// - /// An ordered collection of documents. - /// - /// Notebooks own their documents and release them when destroyed. - /// - public class Notebook : IDisposable - { - private IntPtr _handle; - private int _released; - - /// Adopts one strong reference to a native object. - internal Notebook(WeaveFFIHandle handle) - { - _handle = handle.Value; - } - - /// The borrowed native pointer for the duration of a call. - /// The wrapper was disposed. - internal IntPtr Handle - { - get - { - var h = _handle; - if (h == IntPtr.Zero) - { - throw new ObjectDisposedException(nameof(Notebook)); - } - return h; - } - } - - /// Mints a second strong reference the caller owns (for - /// example to write this object into a value buffer). - internal IntPtr CloneHandle() - { - return NativeMethods.weaveffi_docs_Notebook_clone(Handle); - } - - /// Create an empty notebook - public Notebook() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_docs_Notebook_new(ref err); - WeaveFFIError.Check(err); - _handle = result; - } - - /// Add a document, returning its position in the notebook - /// The document to add - public int Insert(Document document) - { - var err = new WeaveFFIError(); - var documentWriter = new WeaveFFIBufferWriter(); - document.WriteTo(documentWriter); - var documentBuf = documentWriter.ToArray(); - var documentPin = GCHandle.Alloc(documentBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_docs_Notebook_insert(Handle, documentPin.AddrOfPinnedObject(), (UIntPtr)documentBuf.Length, ref err); - WeaveFFIError.Check(err); - return result; - } - finally - { - documentPin.Free(); - } - } - - /// Look up a document by identifier, failing when none matches - /// Identifier of the document to find - /// Thrown when the call reports a DocsErrorsError code. - public Document Find(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_docs_Notebook_find(Handle, id, out var outLen, ref err); - WeaveFFIError.CheckDocsErrors(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 = Document.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - - /// The number of documents currently held - public int Size() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_docs_Notebook_size(Handle, ref err); - WeaveFFIError.Check(err); - return result; - } - - /// The largest number of documents one notebook will hold - public static int MaxDocuments() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_docs_Notebook_max_documents(ref err); - WeaveFFIError.Check(err); - return result; - } - - /// Releases this wrapper's reference. The native object is - /// dropped when the producer releases its last reference; other - /// wrappers to the same object stay valid. - public void Dispose() - { - Release(); - GC.SuppressFinalize(this); - } - - ~Notebook() - { - Release(); - } - - private void Release() - { - if (System.Threading.Interlocked.Exchange(ref _released, 1) != 0) - { - return; - } - var h = _handle; - _handle = IntPtr.Zero; - if (h != IntPtr.Zero) - { - NativeMethods.weaveffi_docs_Notebook_destroy(h); - } - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - // The ABI revision these bindings were generated against. - internal const uint AbiVersion = 2; - - // 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(); - - // Fills a producer-owned error slot with a copy of `message`; callback - // trampolines report a thrown exception through it so no managed - // allocation is ever handed to the producer's allocator. - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_set(IntPtr err, int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string message); - - [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_docs_Notebook_clone", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_docs_Notebook_clone(IntPtr self); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_Notebook_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_docs_Notebook_destroy(IntPtr self); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_Notebook_new", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_docs_Notebook_new(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_Notebook_insert", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_docs_Notebook_insert(IntPtr self, IntPtr document_ptr, UIntPtr document_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_Notebook_find", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_docs_Notebook_find(IntPtr self, long id, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_Notebook_size", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_docs_Notebook_size(IntPtr self, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_Notebook_max_documents", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_docs_Notebook_max_documents(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_create_document", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_docs_create_document(IntPtr title, IntPtr tags_ptr, UIntPtr tags_len, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_get_document", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_docs_get_document(long id, out UIntPtr out_len, ref WeaveFFIError out_err); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void AsyncCb_weaveffi_docs_publish_async(IntPtr context, IntPtr err, byte result); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_publish_async_async", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_docs_publish_async_async(long id, AsyncCb_weaveffi_docs_publish_async callback, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_docs_watch", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_docs_watch(IntPtr listener_ctx, IntPtr listener_vtable, ref WeaveFFIError out_err); - - } - - public static class Docs - { - /// - /// Create a brand new document. - /// - /// The document starts in `Draft` status; callers must publish - /// it explicitly to make it visible. - /// - /// Human-readable title for the new document - /// Initial set of tags (may be empty) - public static Document CreateDocument(string title, string[] tags) - { - var err = new WeaveFFIError(); - var titlePtr = Marshal.StringToCoTaskMemUTF8(title); - var tagsWriter = new WeaveFFIBufferWriter(); - tagsWriter.WriteLen(tags.Length); - foreach (var item0 in tags) - { - tagsWriter.WriteString(item0); - } - var tagsBuf = tagsWriter.ToArray(); - var tagsPin = GCHandle.Alloc(tagsBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_docs_create_document(titlePtr, tagsPin.AddrOfPinnedObject(), (UIntPtr)tagsBuf.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 = Document.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - finally - { - Marshal.FreeCoTaskMem(titlePtr); - tagsPin.Free(); - } - } - - /// Look up a document by identifier - /// Identifier returned from create_document - public static Document? GetDocument(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_docs_get_document(id, 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); - Document? decoded = null; - if (decodedReader.ReadOptionFlag()) - { - var decodedValue = Document.ReadFrom(decodedReader); - decoded = decodedValue; - } - decodedReader.ExpectEnd(); - return decoded; - } - - /// Publish a document in the background - /// Identifier of the document to publish - public static async Task PublishAsync(long id) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - NativeMethods.AsyncCb_weaveffi_docs_publish_async 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 != 0); - } - finally - { - if (context != IntPtr.Zero) - { - GCHandle.FromIntPtr(context).Free(); - } - } - }; - var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); - var ctx = GCHandle.ToIntPtr(gcHandle); - try - { - NativeMethods.weaveffi_docs_publish_async_async(id, callback, ctx); - } - catch - { - if (gcHandle.IsAllocated) gcHandle.Free(); - throw; - } - return await tcs.Task; - } - - /// Register a listener for saved notifications - /// Receiver of saved notifications - public static void Watch(ISavedListener listener) - { - var err = new WeaveFFIError(); - var listenerCtx = GCHandle.ToIntPtr(GCHandle.Alloc(listener)); - NativeMethods.weaveffi_docs_watch(listenerCtx, WeaveFFIVtable_docs_SavedListener.Pointer, ref err); - WeaveFFIError.Check(err); - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__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_docs_everywhere__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__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_edge_cases__EdgeCases_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__EdgeCases_cs.snap new file mode 100644 index 00000000..afc7db45 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__EdgeCases_cs.snap @@ -0,0 +1,2045 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace EdgeCases; + +/// Typed exception for the EdgeErrorsError error domain (module edge.cases). +public class EdgeErrorsException : NativeException +{ + /// Resource is busy + public const int Busy = 7; + + /// Creates an exception carrying a domain error code. + public EdgeErrorsException(int code, string message) : base(code, message) + { + } + + internal static new Exception FromError(int code, string message, byte[]? payload) + { + switch (code) + { + case Busy: + return new EdgeErrorsException(code, string.IsNullOrEmpty(message) ? "Resource is busy" : message); + default: + return global::EdgeCases.NativeException.FromError(code, message, payload); + } + } +} + +/// Deprecated enum with non-contiguous values and keyword-like variants +[Obsolete("Use Mode instead")] +public enum Legacy +{ + None = 0, + Default = 5, + Class = 10, +} + +public enum Mode +{ + Fast = 1, + Safe = 2, +} + +public sealed class Leaf +{ + public float Weight { get; } + + /// Creates a from every field. + public Leaf(float weight) + { + Weight = weight; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteF32(Weight); + } + + internal static Leaf ReadFrom(FfiBufferReader reader) + { + 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; } + + /// Creates a from every field. + public Inner(Leaf leaf, Leaf[] leaves) + { + Leaf = leaf; + Leaves = leaves; + } + + internal void WriteTo(FfiBufferWriter writer) + { + Leaf.WriteTo(writer); + writer.WriteLen(Leaves.Length); + foreach (var item0 in Leaves) + { + item0.WriteTo(writer); + } + } + + internal static Inner ReadFrom(FfiBufferReader reader) + { + var fLeaf = global::EdgeCases.Leaf.ReadFrom(reader); + var fLeavesCount = reader.ReadLen(); + var fLeavesList = new List(reader.Capacity(fLeavesCount)); + for (var i0 = 0; i0 < fLeavesCount; i0++) + { + var fLeavesItem = global::EdgeCases.Leaf.ReadFrom(reader); + fLeavesList.Add(fLeavesItem); + } + var fLeaves = fLeavesList.ToArray(); + 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; } + + /// Creates a from every field. + public Outer(Inner inner, Inner? maybeInner, Dictionary byName, Mode[] modes, int[][] matrix, long?[] sparse, byte[]? blob, string? note, Dictionary byId, bool hidden, byte v2) + { + Inner = inner; + MaybeInner = maybeInner; + ByName = byName; + Modes = modes; + Matrix = matrix; + Sparse = sparse; + Blob = blob; + Note = note; + ById = byId; + Hidden = hidden; + V2 = v2; + } + + internal void WriteTo(FfiBufferWriter writer) + { + Inner.WriteTo(writer); + writer.WriteBool(MaybeInner != null); + if (MaybeInner != null) + { + MaybeInner!.WriteTo(writer); + } + 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) + { + writer.WriteBool(item0 != null); + if (item0 != null) + { + writer.WriteI64(item0.Value); + } + } + writer.WriteBool(Blob != null); + if (Blob != null) + { + writer.WriteBytes(Blob!); + } + writer.WriteBool(Note != null); + if (Note != null) + { + writer.WriteString(Note!); + } + 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(FfiBufferReader reader) + { + var fInner = global::EdgeCases.Inner.ReadFrom(reader); + Inner? fMaybeInner = null; + if (reader.ReadBool()) + { + var fMaybeInnerValue = global::EdgeCases.Inner.ReadFrom(reader); + fMaybeInner = fMaybeInnerValue; + } + var fByNameCount = reader.ReadLen(); + var fByName = new Dictionary(reader.Capacity(fByNameCount)); + for (var i0 = 0; i0 < fByNameCount; i0++) + { + var fByNameKey = reader.ReadString(); + var fByNameVal = global::EdgeCases.Inner.ReadFrom(reader); + fByName[fByNameKey] = fByNameVal; + } + var fModesCount = reader.ReadLen(); + var fModesList = new List(reader.Capacity(fModesCount)); + for (var i0 = 0; i0 < fModesCount; i0++) + { + var fModesItem = (Mode)reader.ReadI32(); + fModesList.Add(fModesItem); + } + var fModes = fModesList.ToArray(); + var fMatrixCount = reader.ReadLen(); + var fMatrixList = new List(reader.Capacity(fMatrixCount)); + for (var i0 = 0; i0 < fMatrixCount; i0++) + { + var fMatrixItemCount = reader.ReadLen(); + var fMatrixItemList = new List(reader.Capacity(fMatrixItemCount)); + for (var i1 = 0; i1 < fMatrixItemCount; i1++) + { + var fMatrixItemItem = reader.ReadI32(); + fMatrixItemList.Add(fMatrixItemItem); + } + var fMatrixItem = fMatrixItemList.ToArray(); + fMatrixList.Add(fMatrixItem); + } + var fMatrix = fMatrixList.ToArray(); + var fSparseCount = reader.ReadLen(); + var fSparseList = new List(reader.Capacity(fSparseCount)); + for (var i0 = 0; i0 < fSparseCount; i0++) + { + long? fSparseItem = null; + if (reader.ReadBool()) + { + var fSparseItemValue = reader.ReadI64(); + fSparseItem = fSparseItemValue; + } + fSparseList.Add(fSparseItem); + } + var fSparse = fSparseList.ToArray(); + byte[]? fBlob = null; + if (reader.ReadBool()) + { + var fBlobValue = reader.ReadBytes(); + fBlob = fBlobValue; + } + string? fNote = null; + if (reader.ReadBool()) + { + var fNoteValue = reader.ReadString(); + fNote = fNoteValue; + } + var fByIdCount = reader.ReadLen(); + var fById = new Dictionary(reader.Capacity(fByIdCount)); + for (var 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; } + + /// Creates a from every field. + public Keywords(int @class, int def, int import, int from, int func, int range, int register, int @volatile, int @new, int @default, int end, int @in, int @is, int @int, int var, int val, int self, int @this, int @object, int @string, int @case, int select, int @switch, int @void, int union, int @namespace, int @operator, int template, int package, int protocol, int guard, int defer, int inout, int @internal, int when, int fun, int data, int late, int part, int @out, int @ref, int @event, int @base, int lambda, int yield, int nil, int @null, int begin, int rescue, int module, int chan, int go, int let, int export, int delete, int typedef, int @sizeof, int @static, int super) + { + Class = @class; + Def = def; + Import = import; + From = from; + Func = func; + Range = range; + Register = register; + Volatile = @volatile; + New = @new; + Default = @default; + End = end; + In = @in; + Is = @is; + Int = @int; + Var = var; + Val = val; + Self = self; + This = @this; + Object = @object; + String = @string; + Case = @case; + Select = select; + Switch = @switch; + Void = @void; + Union = union; + Namespace = @namespace; + Operator = @operator; + Template = template; + Package = package; + Protocol = protocol; + Guard = guard; + Defer = defer; + Inout = inout; + Internal = @internal; + When = when; + Fun = fun; + Data = data; + Late = late; + Part = part; + Out = @out; + Ref = @ref; + Event = @event; + Base = @base; + Lambda = lambda; + Yield = yield; + Nil = nil; + Null = @null; + Begin = begin; + Rescue = rescue; + Module = module; + Chan = chan; + Go = go; + Let = let; + Export = export; + Delete = delete; + Typedef = typedef; + Sizeof = @sizeof; + Static = @static; + Super = super; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteI32(Class); + writer.WriteI32(Def); + writer.WriteI32(Import); + writer.WriteI32(From); + writer.WriteI32(Func); + writer.WriteI32(Range); + writer.WriteI32(Register); + writer.WriteI32(Volatile); + writer.WriteI32(New); + writer.WriteI32(Default); + writer.WriteI32(End); + writer.WriteI32(In); + writer.WriteI32(Is); + writer.WriteI32(Int); + writer.WriteI32(Var); + writer.WriteI32(Val); + writer.WriteI32(Self); + writer.WriteI32(This); + writer.WriteI32(Object); + writer.WriteI32(String); + writer.WriteI32(Case); + writer.WriteI32(Select); + writer.WriteI32(Switch); + writer.WriteI32(Void); + writer.WriteI32(Union); + writer.WriteI32(Namespace); + writer.WriteI32(Operator); + writer.WriteI32(Template); + writer.WriteI32(Package); + writer.WriteI32(Protocol); + writer.WriteI32(Guard); + writer.WriteI32(Defer); + writer.WriteI32(Inout); + writer.WriteI32(Internal); + writer.WriteI32(When); + writer.WriteI32(Fun); + writer.WriteI32(Data); + writer.WriteI32(Late); + writer.WriteI32(Part); + writer.WriteI32(Out); + writer.WriteI32(Ref); + writer.WriteI32(Event); + writer.WriteI32(Base); + writer.WriteI32(Lambda); + writer.WriteI32(Yield); + writer.WriteI32(Nil); + writer.WriteI32(Null); + writer.WriteI32(Begin); + writer.WriteI32(Rescue); + writer.WriteI32(Module); + writer.WriteI32(Chan); + writer.WriteI32(Go); + writer.WriteI32(Let); + writer.WriteI32(Export); + writer.WriteI32(Delete); + writer.WriteI32(Typedef); + writer.WriteI32(Sizeof); + writer.WriteI32(Static); + writer.WriteI32(Super); + } + + internal static Keywords ReadFrom(FfiBufferReader reader) + { + 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 +[Obsolete("Use Outer instead")] +public sealed class Retired +{ + public long Id { get; } + + /// Creates a from every field. + public Retired(long id) + { + Id = id; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteI64(Id); + } + + internal static Retired ReadFrom(FfiBufferReader reader) + { + var fId = reader.ReadI64(); + return new Retired(fId); + } +} + +/// Record carrying objects in every buffered position +public sealed class WidgetHolder +{ + public Widget Primary { get; } + + public Widget? Fallback { get; } + + public Widget[] Extras { get; } + + public Dictionary ByName { get; } + + /// Creates a from every field. + public WidgetHolder(Widget primary, Widget? fallback, Widget[] extras, Dictionary byName) + { + Primary = primary; + Fallback = fallback; + Extras = extras; + ByName = byName; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteObject(Primary.CloneHandle()); + writer.WriteBool(Fallback != null); + if (Fallback != null) + { + writer.WriteObject(Fallback!.CloneHandle()); + } + writer.WriteLen(Extras.Length); + foreach (var item0 in Extras) + { + writer.WriteObject(item0.CloneHandle()); + } + writer.WriteLen(ByName.Count); + foreach (var entry0 in ByName) + { + writer.WriteString(entry0.Key); + writer.WriteObject(entry0.Value.CloneHandle()); + } + } + + internal static WidgetHolder ReadFrom(FfiBufferReader reader) + { + var fPrimary = global::EdgeCases.Widget.Adopt(reader.ReadObject()); + Widget? fFallback = null; + if (reader.ReadBool()) + { + var fFallbackValue = global::EdgeCases.Widget.Adopt(reader.ReadObject()); + fFallback = fFallbackValue; + } + var fExtrasCount = reader.ReadLen(); + var fExtrasList = new List(reader.Capacity(fExtrasCount)); + for (var i0 = 0; i0 < fExtrasCount; i0++) + { + var fExtrasItem = global::EdgeCases.Widget.Adopt(reader.ReadObject()); + fExtrasList.Add(fExtrasItem); + } + var fExtras = fExtrasList.ToArray(); + var fByNameCount = reader.ReadLen(); + var fByName = new Dictionary(reader.Capacity(fByNameCount)); + for (var i0 = 0; i0 < fByNameCount; i0++) + { + var fByNameKey = reader.ReadString(); + var fByNameVal = global::EdgeCases.Widget.Adopt(reader.ReadObject()); + fByName[fByNameKey] = fByNameVal; + } + return new WidgetHolder(fPrimary, fFallback, fExtras, fByName); + } +} + +/// Callback interface carrying composite payloads and objects +/// The native library may call any method from any thread until +/// it releases its last reference to the implementation. Object arguments +/// belong to the implementation. An exception thrown by a method reaches +/// the native caller as . +public interface IBatchListener +{ + /// Composite payloads cross as value buffers + void OnBatch(Outer[] items, byte[] raw, Outer? first, Mode mode); + /// Objects cross as strong references the consumer must release + void OnWidget(Widget widget, Widget? fallback, Widget[] children); + void OnTick(); + /// Scalar return travels back through the vtable + bool ShouldContinue(int @class); + /// Enum return travels back as its integer discriminant + Mode PickMode(); +} + +/// The process-wide edge_cases_edge_cases_BatchListener_vtable and the trampolines behind it, +/// adapting an to the C ABI. +internal static unsafe class FfiVtable_edge_cases_BatchListener +{ + [StructLayout(LayoutKind.Sequential)] + private struct Layout + { + public delegate* unmanaged[Cdecl] on_batch; + public delegate* unmanaged[Cdecl] on_widget; + public delegate* unmanaged[Cdecl] on_tick; + public delegate* unmanaged[Cdecl] should_continue; + public delegate* unmanaged[Cdecl] pick_mode; + public delegate* unmanaged[Cdecl] free; + } + + /// The vtable's address, valid for the process lifetime. + internal static readonly IntPtr Pointer = Allocate(); + + private static IntPtr Allocate() + { + var vtable = (Layout*)NativeMemory.Alloc((nuint)sizeof(Layout)); + vtable->on_batch = &OnBatchTrampoline; + vtable->on_widget = &OnWidgetTrampoline; + vtable->on_tick = &OnTickTrampoline; + vtable->should_continue = &ShouldContinueTrampoline; + vtable->pick_mode = &PickModeTrampoline; + vtable->free = &FreeTrampoline; + return (IntPtr)vtable; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void OnBatchTrampoline(IntPtr ctx, byte* items_ptr, nuint items_len, byte* raw_ptr, nuint raw_len, byte* first_ptr, nuint first_len, int mode, FfiError* out_err) + { + try + { + var arg0Reader = new FfiBufferReader(Ffi.ReadBytes(items_ptr, items_len)); + var arg0Count = arg0Reader.ReadLen(); + var arg0List = new List(arg0Reader.Capacity(arg0Count)); + for (var i0 = 0; i0 < arg0Count; i0++) + { + var arg0Item = global::EdgeCases.Outer.ReadFrom(arg0Reader); + arg0List.Add(arg0Item); + } + var arg0 = arg0List.ToArray(); + arg0Reader.ExpectEnd(); + var arg2Reader = new FfiBufferReader(Ffi.ReadBytes(first_ptr, first_len)); + Outer? arg2 = null; + if (arg2Reader.ReadBool()) + { + var arg2Value = global::EdgeCases.Outer.ReadFrom(arg2Reader); + arg2 = arg2Value; + } + arg2Reader.ExpectEnd(); + Ffi.Target(ctx).OnBatch(arg0, Ffi.ReadBytes(raw_ptr, raw_len), arg2, (Mode)mode); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void OnWidgetTrampoline(IntPtr ctx, IntPtr widget, IntPtr fallback, byte* children_ptr, nuint children_len, FfiError* out_err) + { + try + { + var arg2Reader = new FfiBufferReader(Ffi.ReadBytes(children_ptr, children_len)); + var arg2Count = arg2Reader.ReadLen(); + var arg2List = new List(arg2Reader.Capacity(arg2Count)); + for (var i0 = 0; i0 < arg2Count; i0++) + { + var arg2Item = global::EdgeCases.Widget.Adopt(arg2Reader.ReadObject()); + arg2List.Add(arg2Item); + } + var arg2 = arg2List.ToArray(); + arg2Reader.ExpectEnd(); + Ffi.Target(ctx).OnWidget(global::EdgeCases.Widget.Adopt(widget), fallback == IntPtr.Zero ? null : global::EdgeCases.Widget.Adopt(fallback), arg2); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void OnTickTrampoline(IntPtr ctx, FfiError* out_err) + { + try + { + Ffi.Target(ctx).OnTick(); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static byte ShouldContinueTrampoline(IntPtr ctx, int @class, FfiError* out_err) + { + try + { + return (byte)(Ffi.Target(ctx).ShouldContinue(@class) ? 1 : 0); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + return default; + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static int PickModeTrampoline(IntPtr ctx, FfiError* out_err) + { + try + { + return (int)Ffi.Target(ctx).PickMode(); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + return default; + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void FreeTrampoline(IntPtr ctx) + { + Ffi.Unregister(ctx); + } +} + +/// Callback interface whose name and methods are keywords +/// The native library may call any method from any thread until +/// it releases its last reference to the implementation. Object arguments +/// belong to the implementation. An exception thrown by a method reaches +/// the native caller as . +public interface IKeyword +{ + void Delete(int self); + int Import(); +} + +/// The process-wide edge_cases_edge_cases_Keyword_vtable and the trampolines behind it, +/// adapting an to the C ABI. +internal static unsafe class FfiVtable_edge_cases_Keyword +{ + [StructLayout(LayoutKind.Sequential)] + private struct Layout + { + public delegate* unmanaged[Cdecl] delete; + public delegate* unmanaged[Cdecl] import; + public delegate* unmanaged[Cdecl] free; + } + + /// The vtable's address, valid for the process lifetime. + internal static readonly IntPtr Pointer = Allocate(); + + private static IntPtr Allocate() + { + var vtable = (Layout*)NativeMemory.Alloc((nuint)sizeof(Layout)); + vtable->delete = &DeleteTrampoline; + vtable->import = &ImportTrampoline; + vtable->free = &FreeTrampoline; + return (IntPtr)vtable; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void DeleteTrampoline(IntPtr ctx, int self, FfiError* out_err) + { + try + { + Ffi.Target(ctx).Delete(self); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static int ImportTrampoline(IntPtr ctx, FfiError* out_err) + { + try + { + return Ffi.Target(ctx).Import(); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + return default; + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void FreeTrampoline(IntPtr ctx) + { + Ffi.Unregister(ctx); + } +} + +/// Interface with no constructor; instances come from make_widget +public sealed unsafe class Widget : IDisposable, IEquatable +{ + /// The native object; borrowed by every call. + internal NativeHandle Handle { get; } + + private Widget(NativeHandle handle) + { + Handle = handle; + } + + /// Wraps one strong reference the caller transfers. + internal static Widget Adopt(IntPtr ptr) + { + return new Widget(new NativeHandle(ptr)); + } + + /// Mints a second strong reference the caller owns, as an + /// object token in a value buffer requires. + internal IntPtr CloneHandle() + { + return NativeMethods.edge_cases_edge_cases_Widget_clone(Handle); + } + + /// Void method that can fail + /// The call reported a EdgeErrorsError code. + public void Refresh() + { + var ffiErr = default(FfiError); + NativeMethods.edge_cases_edge_cases_Widget_refresh(Handle, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.EdgeErrorsException.FromError); + } + + public string Rename(string @class, string? import) + { + var classBytes = Ffi.Utf8(@class); + var importWriter = new FfiBufferWriter(); + importWriter.WriteBool(import != null); + if (import != null) + { + importWriter.WriteString(import!); + } + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* classPtr = classBytes, importPtr = importWriter.Written) + { + var ffiResult = NativeMethods.edge_cases_edge_cases_Widget_rename(Handle, classPtr, (nuint)classBytes.Length, importPtr, (nuint)importWriter.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } + } + + /// Async method with a struct result + /// The call reported a EdgeErrorsError code. + public Task Fetch(long id) + { + var ffiCall = new FfiCall(); + try + { + NativeMethods.edge_cases_edge_cases_Widget_fetch(Handle, id, &CompleteFetch, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteFetch(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.EdgeErrorsException.FromError); + return; + } + var ffiDecodedReader = Ffi.TakeBuffer(result_ptr, result_len); + var ffiDecoded = global::EdgeCases.Outer.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + ffiCall.SetResult(ffiDecoded); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + [Obsolete("Use rename instead")] + public string OldName() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.edge_cases_edge_cases_Widget_old_name(Handle, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } + + /// Releases this wrapper's reference. The native object is + /// dropped once its last reference is released, and an in-flight + /// call finishes first. + public void Dispose() + { + Handle.Dispose(); + } + + /// True when both wrappers reference the same native object. + public bool Equals(Widget? other) + { + return other is not null && other.Handle.DangerousGetHandle() == Handle.DangerousGetHandle(); + } + + /// + public override bool Equals(object? obj) + { + return obj is Widget other && Equals(other); + } + + /// + public override int GetHashCode() + { + return Handle.DangerousGetHandle().GetHashCode(); + } + + /// One strong reference, released through + /// edge_cases_edge_cases_Widget_destroy. + internal sealed class NativeHandle : SafeHandle + { + /// A null handle, passed for an absent optional object. + internal static readonly NativeHandle Null = new NativeHandle(); + + public NativeHandle() : base(IntPtr.Zero, true) + { + } + + internal NativeHandle(IntPtr ptr) : base(IntPtr.Zero, true) + { + SetHandle(ptr); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + NativeMethods.edge_cases_edge_cases_Widget_destroy(handle); + return true; + } + } +} + +/// Deprecated interface with a throwing constructor +[Obsolete("Use Widget instead")] +public sealed unsafe class Vault : IDisposable, IEquatable +{ + /// The native object; borrowed by every call. + internal NativeHandle Handle { get; } + + private Vault(NativeHandle handle) + { + Handle = handle; + } + + /// Wraps one strong reference the caller transfers. + internal static Vault Adopt(IntPtr ptr) + { + return new Vault(new NativeHandle(ptr)); + } + + /// Mints a second strong reference the caller owns, as an + /// object token in a value buffer requires. + internal IntPtr CloneHandle() + { + return NativeMethods.edge_cases_edge_cases_Vault_clone(Handle); + } + + /// Fails when the vault is busy + /// The call reported a EdgeErrorsError code. + public static Vault Open(string path) + { + var pathBytes = Ffi.Utf8(path); + var ffiErr = default(FfiError); + fixed (byte* pathPtr = pathBytes) + { + var ffiResult = NativeMethods.edge_cases_edge_cases_Vault_open(pathPtr, (nuint)pathBytes.Length, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.EdgeErrorsException.FromError); + return global::EdgeCases.Vault.Adopt(ffiResult); + } + } + + public Widget? Peek() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.edge_cases_edge_cases_Vault_peek(Handle, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return ffiResult == IntPtr.Zero ? null : global::EdgeCases.Widget.Adopt(ffiResult); + } + + /// Throwing static + /// The call reported a EdgeErrorsError code. + public static bool Probe() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.edge_cases_edge_cases_Vault_probe(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.EdgeErrorsException.FromError); + return ffiResult != 0; + } + + /// Releases this wrapper's reference. The native object is + /// dropped once its last reference is released, and an in-flight + /// call finishes first. + public void Dispose() + { + Handle.Dispose(); + } + + /// True when both wrappers reference the same native object. + public bool Equals(Vault? other) + { + return other is not null && other.Handle.DangerousGetHandle() == Handle.DangerousGetHandle(); + } + + /// + public override bool Equals(object? obj) + { + return obj is Vault other && Equals(other); + } + + /// + public override int GetHashCode() + { + return Handle.DangerousGetHandle().GetHashCode(); + } + + /// One strong reference, released through + /// edge_cases_edge_cases_Vault_destroy. + internal sealed class NativeHandle : SafeHandle + { + /// A null handle, passed for an absent optional object. + internal static readonly NativeHandle Null = new NativeHandle(); + + public NativeHandle() : base(IntPtr.Zero, true) + { + } + + internal NativeHandle(IntPtr ptr) : base(IntPtr.Zero, true) + { + SetHandle(ptr); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + NativeMethods.edge_cases_edge_cases_Vault_destroy(handle); + return true; + } + } +} + +public static unsafe class EdgeCases +{ + /// No parameters and no result + public static void Noop() + { + var ffiErr = default(FfiError); + NativeMethods.edge_cases_edge_cases_noop(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + } + + /// Function named after a keyword whose parameters are all keywords + public static int Select(int @class, string from, string[] func, int? range, bool type) + { + var fromBytes = Ffi.Utf8(from); + var funcWriter = new FfiBufferWriter(); + funcWriter.WriteLen(func.Length); + foreach (var item0 in func) + { + funcWriter.WriteString(item0); + } + var rangeWriter = new FfiBufferWriter(); + rangeWriter.WriteBool(range != null); + if (range != null) + { + rangeWriter.WriteI32(range.Value); + } + var ffiErr = default(FfiError); + fixed (byte* fromPtr = fromBytes, funcPtr = funcWriter.Written, rangePtr = rangeWriter.Written) + { + var ffiResult = NativeMethods.edge_cases_edge_cases_select(@class, fromPtr, (nuint)fromBytes.Length, funcPtr, (nuint)funcWriter.Length, rangePtr, (nuint)rangeWriter.Length, (byte)(type ? 1 : 0), &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return ffiResult; + } + } + + /// Optional list of optional structs, the deepest legal nesting + public static Outer?[]? NestedOptional(Outer?[]? items) + { + var itemsWriter = new FfiBufferWriter(); + itemsWriter.WriteBool(items != null); + if (items != null) + { + itemsWriter.WriteLen(items!.Length); + foreach (var item0 in items!) + { + itemsWriter.WriteBool(item0 != null); + if (item0 != null) + { + item0!.WriteTo(itemsWriter); + } + } + } + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* itemsPtr = itemsWriter.Written) + { + var ffiResult = NativeMethods.edge_cases_edge_cases_nested_optional(itemsPtr, (nuint)itemsWriter.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + Outer?[]? ffiDecoded = null; + if (ffiDecodedReader.ReadBool()) + { + var ffiDecodedValueCount = ffiDecodedReader.ReadLen(); + var ffiDecodedValueList = new List(ffiDecodedReader.Capacity(ffiDecodedValueCount)); + for (var i0 = 0; i0 < ffiDecodedValueCount; i0++) + { + Outer? ffiDecodedValueItem = null; + if (ffiDecodedReader.ReadBool()) + { + var ffiDecodedValueItemValue = global::EdgeCases.Outer.ReadFrom(ffiDecodedReader); + ffiDecodedValueItem = ffiDecodedValueItemValue; + } + ffiDecodedValueList.Add(ffiDecodedValueItem); + } + var ffiDecodedValue = ffiDecodedValueList.ToArray(); + ffiDecoded = ffiDecodedValue; + } + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + } + + public static Mode? OptionalParams(string? text, int? count, Outer? outer, string[]? tags, byte[]? blob, Mode? mode) + { + var textWriter = new FfiBufferWriter(); + textWriter.WriteBool(text != null); + if (text != null) + { + textWriter.WriteString(text!); + } + var countWriter = new FfiBufferWriter(); + countWriter.WriteBool(count != null); + if (count != null) + { + countWriter.WriteI32(count.Value); + } + var outerWriter = new FfiBufferWriter(); + outerWriter.WriteBool(outer != null); + if (outer != null) + { + outer!.WriteTo(outerWriter); + } + var tagsWriter = new FfiBufferWriter(); + tagsWriter.WriteBool(tags != null); + if (tags != null) + { + tagsWriter.WriteLen(tags!.Length); + foreach (var item0 in tags!) + { + tagsWriter.WriteString(item0); + } + } + var blobWriter = new FfiBufferWriter(); + blobWriter.WriteBool(blob != null); + if (blob != null) + { + blobWriter.WriteBytes(blob!); + } + var modeWriter = new FfiBufferWriter(); + modeWriter.WriteBool(mode != null); + if (mode != null) + { + modeWriter.WriteI32((int)mode.Value); + } + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* textPtr = textWriter.Written, countPtr = countWriter.Written, outerPtr = outerWriter.Written, tagsPtr = tagsWriter.Written, blobPtr = blobWriter.Written, modePtr = modeWriter.Written) + { + var ffiResult = NativeMethods.edge_cases_edge_cases_optional_params(textPtr, (nuint)textWriter.Length, countPtr, (nuint)countWriter.Length, outerPtr, (nuint)outerWriter.Length, tagsPtr, (nuint)tagsWriter.Length, blobPtr, (nuint)blobWriter.Length, modePtr, (nuint)modeWriter.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + Mode? ffiDecoded = null; + if (ffiDecodedReader.ReadBool()) + { + var ffiDecodedValue = (Mode)ffiDecodedReader.ReadI32(); + ffiDecoded = ffiDecodedValue; + } + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + } + + /// Two callback interface parameters on one function + public static void RegisterBatch(IBatchListener listener, IKeyword keyword) + { + var listenerCtx = Ffi.Register(listener); + var keywordCtx = Ffi.Register(keyword); + var ffiErr = default(FfiError); + try + { + NativeMethods.edge_cases_edge_cases_register_batch(listenerCtx, FfiVtable_edge_cases_BatchListener.Pointer, keywordCtx, FfiVtable_edge_cases_Keyword.Pointer, &ffiErr); + } + catch + { + Ffi.Unregister(listenerCtx); + Ffi.Unregister(keywordCtx); + throw; + } + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + } + + /// Objects in every buffered position + public static WidgetHolder WidgetsEverywhere(Widget[] list, Dictionary byName, Widget?[] sparse, WidgetHolder holder) + { + var listWriter = new FfiBufferWriter(); + listWriter.WriteLen(list.Length); + foreach (var item0 in list) + { + listWriter.WriteObject(item0.CloneHandle()); + } + var byNameWriter = new FfiBufferWriter(); + byNameWriter.WriteLen(byName.Count); + foreach (var entry0 in byName) + { + byNameWriter.WriteString(entry0.Key); + byNameWriter.WriteObject(entry0.Value.CloneHandle()); + } + var sparseWriter = new FfiBufferWriter(); + sparseWriter.WriteLen(sparse.Length); + foreach (var item0 in sparse) + { + sparseWriter.WriteBool(item0 != null); + if (item0 != null) + { + sparseWriter.WriteObject(item0!.CloneHandle()); + } + } + var holderWriter = new FfiBufferWriter(); + holder.WriteTo(holderWriter); + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* listPtr = listWriter.Written, byNamePtr = byNameWriter.Written, sparsePtr = sparseWriter.Written, holderPtr = holderWriter.Written) + { + var ffiResult = NativeMethods.edge_cases_edge_cases_widgets_everywhere(listPtr, (nuint)listWriter.Length, byNamePtr, (nuint)byNameWriter.Length, sparsePtr, (nuint)sparseWriter.Length, holderPtr, (nuint)holderWriter.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::EdgeCases.WidgetHolder.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + } + + /// Iterator of objects + /// Streams lazily, one native call per item. The sequence can be + /// enumerated once; disposing its enumerator (as foreach does) + /// releases the native iterator. + public static IEnumerable WidgetStream() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.edge_cases_edge_cases_widget_stream(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.edge_cases_edge_cases_WidgetStreamIterator_destroy), &NextWidgetStream); + } + + private static bool NextWidgetStream(FfiIteratorHandle iter, out Widget item) + { + IntPtr ffiItem = default; + var ffiErr = default(FfiError); + var ffiMore = NativeMethods.edge_cases_edge_cases_WidgetStreamIterator_next(iter, &ffiItem, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + if (ffiMore == 0) + { + item = default!; + return false; + } + item = global::EdgeCases.Widget.Adopt(ffiItem); + return true; + } + + /// Async returning an object + /// The call reported a EdgeErrorsError code. + public static Task AsyncWidget() + { + var ffiCall = new FfiCall(); + try + { + NativeMethods.edge_cases_edge_cases_async_widget(&CompleteAsyncWidget, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncWidget(IntPtr context, FfiError* err, IntPtr result) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.EdgeErrorsException.FromError); + return; + } + ffiCall.SetResult(global::EdgeCases.Widget.Adopt(result)); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + /// Factory for an interface with no constructor + public static Widget MakeWidget(string label) + { + var labelBytes = Ffi.Utf8(label); + var ffiErr = default(FfiError); + fixed (byte* labelPtr = labelBytes) + { + var ffiResult = NativeMethods.edge_cases_edge_cases_make_widget(labelPtr, (nuint)labelBytes.Length, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return global::EdgeCases.Widget.Adopt(ffiResult); + } + } + + /// Optional interface return + /// The call reported a EdgeErrorsError code. + public static Widget? FindWidget(long id) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.edge_cases_edge_cases_find_widget(id, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.EdgeErrorsException.FromError); + return ffiResult == IntPtr.Zero ? null : global::EdgeCases.Widget.Adopt(ffiResult); + } + + /// Interface parameter, including an optional one + public static string InspectWidget(Widget widget, Widget? fallback) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.edge_cases_edge_cases_inspect_widget(widget.Handle, fallback?.Handle ?? global::EdgeCases.Widget.NativeHandle.Null, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } + + /// Iterator of strings + /// Streams lazily, one native call per item. The sequence can be + /// enumerated once; disposing its enumerator (as foreach does) + /// releases the native iterator. + public static IEnumerable StringStream() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.edge_cases_edge_cases_string_stream(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.edge_cases_edge_cases_StringStreamIterator_destroy), &NextStringStream); + } + + private static bool NextStringStream(FfiIteratorHandle iter, out string item) + { + byte* ffiItem = default; + nuint ffiLen = default; + var ffiErr = default(FfiError); + var ffiMore = NativeMethods.edge_cases_edge_cases_StringStreamIterator_next(iter, &ffiItem, &ffiLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + if (ffiMore == 0) + { + item = default!; + return false; + } + item = Ffi.TakeString(ffiItem, ffiLen); + return true; + } + + /// Iterator of scalars + /// Streams lazily, one native call per item. The sequence can be + /// enumerated once; disposing its enumerator (as foreach does) + /// releases the native iterator. + public static IEnumerable ScalarStream() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.edge_cases_edge_cases_scalar_stream(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.edge_cases_edge_cases_ScalarStreamIterator_destroy), &NextScalarStream); + } + + private static bool NextScalarStream(FfiIteratorHandle iter, out int item) + { + int ffiItem = default; + var ffiErr = default(FfiError); + var ffiMore = NativeMethods.edge_cases_edge_cases_ScalarStreamIterator_next(iter, &ffiItem, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + if (ffiMore == 0) + { + item = default!; + return false; + } + item = ffiItem; + return true; + } + + /// Iterator of enums + /// Streams lazily, one native call per item. The sequence can be + /// enumerated once; disposing its enumerator (as foreach does) + /// releases the native iterator. + public static IEnumerable ModeStream() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.edge_cases_edge_cases_mode_stream(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.edge_cases_edge_cases_ModeStreamIterator_destroy), &NextModeStream); + } + + private static bool NextModeStream(FfiIteratorHandle iter, out Mode item) + { + int ffiItem = default; + var ffiErr = default(FfiError); + var ffiMore = NativeMethods.edge_cases_edge_cases_ModeStreamIterator_next(iter, &ffiItem, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + if (ffiMore == 0) + { + item = default!; + return false; + } + item = (Mode)ffiItem; + return true; + } + + /// Async with no result + public static Task AsyncVoid() + { + var ffiCall = new FfiCall(); + try + { + NativeMethods.edge_cases_edge_cases_async_void(&CompleteAsyncVoid, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncVoid(IntPtr context, FfiError* err) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.NativeException.FromError); + return; + } + ffiCall.SetResult(true); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + public static Task AsyncBytes(uint size) + { + var ffiCall = new FfiCall(); + try + { + NativeMethods.edge_cases_edge_cases_async_bytes(size, &CompleteAsyncBytes, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncBytes(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.NativeException.FromError); + return; + } + ffiCall.SetResult(Ffi.TakeBytes(result_ptr, result_len)); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + /// The call reported a EdgeErrorsError code. + public static Task AsyncStruct() + { + var ffiCall = new FfiCall(); + try + { + NativeMethods.edge_cases_edge_cases_async_struct(&CompleteAsyncStruct, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncStruct(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.EdgeErrorsException.FromError); + return; + } + var ffiDecodedReader = Ffi.TakeBuffer(result_ptr, result_len); + var ffiDecoded = global::EdgeCases.Outer.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + ffiCall.SetResult(ffiDecoded); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + /// The call reported a EdgeErrorsError code. + /// Cancels the native call; the task + /// then completes as canceled. + public static Task AsyncList(CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + var ffiCall = new FfiCall(cancellationToken); + try + { + NativeMethods.edge_cases_edge_cases_async_list(ffiCall.CancelToken(), &CompleteAsyncList, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncList(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.EdgeErrorsException.FromError); + return; + } + var ffiDecodedReader = Ffi.TakeBuffer(result_ptr, result_len); + var ffiDecodedCount = ffiDecodedReader.ReadLen(); + var ffiDecodedList = new List(ffiDecodedReader.Capacity(ffiDecodedCount)); + for (var i0 = 0; i0 < ffiDecodedCount; i0++) + { + var ffiDecodedItem = global::EdgeCases.Outer.ReadFrom(ffiDecodedReader); + ffiDecodedList.Add(ffiDecodedItem); + } + var ffiDecoded = ffiDecodedList.ToArray(); + ffiDecodedReader.ExpectEnd(); + ffiCall.SetResult(ffiDecoded); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + public static Task AsyncOptional() + { + var ffiCall = new FfiCall(); + try + { + NativeMethods.edge_cases_edge_cases_async_optional(&CompleteAsyncOptional, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncOptional(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.NativeException.FromError); + return; + } + var ffiDecodedReader = Ffi.TakeBuffer(result_ptr, result_len); + long? ffiDecoded = null; + if (ffiDecodedReader.ReadBool()) + { + var ffiDecodedValue = ffiDecodedReader.ReadI64(); + ffiDecoded = ffiDecodedValue; + } + ffiDecodedReader.ExpectEnd(); + ffiCall.SetResult(ffiDecoded); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + public static Task AsyncEnum() + { + var ffiCall = new FfiCall(); + try + { + NativeMethods.edge_cases_edge_cases_async_enum(&CompleteAsyncEnum, ffiCall.Context); + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncEnum(IntPtr context, FfiError* err, int result) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.NativeException.FromError); + return; + } + ffiCall.SetResult((Mode)result); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + public static Task AsyncKeywords(string @class, int import) + { + var classBytes = Ffi.Utf8(@class); + var ffiCall = new FfiCall(); + try + { + fixed (byte* classPtr = classBytes) + { + NativeMethods.edge_cases_edge_cases_async_keywords(classPtr, (nuint)classBytes.Length, import, &CompleteAsyncKeywords, ffiCall.Context); + } + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteAsyncKeywords(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::EdgeCases.NativeException.FromError); + return; + } + ffiCall.SetResult(Ffi.TakeString(result_ptr, result_len)); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + public static Keywords KeywordsRoundtrip(Keywords value) + { + var valueWriter = new FfiBufferWriter(); + value.WriteTo(valueWriter); + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* valuePtr = valueWriter.Written) + { + var ffiResult = NativeMethods.edge_cases_edge_cases_keywords_roundtrip(valuePtr, (nuint)valueWriter.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::EdgeCases.Keywords.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + } + + [Obsolete("Use Mode-based APIs")] + public static Retired LegacyValue(Legacy value) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.edge_cases_edge_cases_legacy_value((int)value, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::EdgeCases.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::EdgeCases.Retired.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } +} + +internal static unsafe partial class NativeMethods +{ + static partial void VerifyChecksums() + { + VerifyChecksum("edge_cases", &edge_cases_edge_cases_checksum, 0x1567f441073bd4aaUL); + } + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_checksum")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial ulong edge_cases_edge_cases_checksum(); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_clone")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_Widget_clone(Widget.NativeHandle self); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_Widget_destroy(IntPtr self); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_refresh")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_Widget_refresh(Widget.NativeHandle self, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_rename")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* edge_cases_edge_cases_Widget_rename(Widget.NativeHandle self, byte* class_ptr, nuint class_len, byte* import_ptr, nuint import_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_fetch")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_Widget_fetch(Widget.NativeHandle self, long id, delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Widget_old_name")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* edge_cases_edge_cases_Widget_old_name(Widget.NativeHandle self, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_clone")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_Vault_clone(Vault.NativeHandle self); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_Vault_destroy(IntPtr self); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_open")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_Vault_open(byte* path_ptr, nuint path_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_peek")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_Vault_peek(Vault.NativeHandle self, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_Vault_probe")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte edge_cases_edge_cases_Vault_probe(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_noop")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_noop(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_select")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int edge_cases_edge_cases_select(int @class, byte* from_ptr, nuint from_len, byte* func_ptr, nuint func_len, byte* range_ptr, nuint range_len, byte type_, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_nested_optional")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* edge_cases_edge_cases_nested_optional(byte* items_ptr, nuint items_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_optional_params")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* edge_cases_edge_cases_optional_params(byte* text_ptr, nuint text_len, byte* count_ptr, nuint count_len, byte* outer_ptr, nuint outer_len, byte* tags_ptr, nuint tags_len, byte* blob_ptr, nuint blob_len, byte* mode_ptr, nuint mode_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_register_batch")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_register_batch(IntPtr listener_ctx, IntPtr listener_vtable, IntPtr keyword_ctx, IntPtr keyword_vtable, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_widgets_everywhere")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* edge_cases_edge_cases_widgets_everywhere(byte* list_ptr, nuint list_len, byte* by_name_ptr, nuint by_name_len, byte* sparse_ptr, nuint sparse_len, byte* holder_ptr, nuint holder_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_widget_stream")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_widget_stream(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_WidgetStreamIterator_next")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int edge_cases_edge_cases_WidgetStreamIterator_next(FfiIteratorHandle iter, IntPtr* out_item, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_WidgetStreamIterator_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_WidgetStreamIterator_destroy(IntPtr iter); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_widget")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_async_widget(delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_make_widget")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_make_widget(byte* label_ptr, nuint label_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_find_widget")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_find_widget(long id, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_inspect_widget")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* edge_cases_edge_cases_inspect_widget(Widget.NativeHandle widget, Widget.NativeHandle fallback, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_string_stream")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_string_stream(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_StringStreamIterator_next")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int edge_cases_edge_cases_StringStreamIterator_next(FfiIteratorHandle iter, byte** out_item, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_StringStreamIterator_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_StringStreamIterator_destroy(IntPtr iter); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_scalar_stream")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_scalar_stream(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_ScalarStreamIterator_next")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int edge_cases_edge_cases_ScalarStreamIterator_next(FfiIteratorHandle iter, int* out_item, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_ScalarStreamIterator_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_ScalarStreamIterator_destroy(IntPtr iter); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_mode_stream")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr edge_cases_edge_cases_mode_stream(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_ModeStreamIterator_next")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int edge_cases_edge_cases_ModeStreamIterator_next(FfiIteratorHandle iter, int* out_item, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_ModeStreamIterator_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_ModeStreamIterator_destroy(IntPtr iter); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_void")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_async_void(delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_async_bytes(uint size, delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_struct")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_async_struct(delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_list")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_async_list(IntPtr cancel_token, delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_optional")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_async_optional(delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_enum")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_async_enum(delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_async_keywords")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void edge_cases_edge_cases_async_keywords(byte* class_ptr, nuint class_len, int import, delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_keywords_roundtrip")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* edge_cases_edge_cases_keywords_roundtrip(byte* value_ptr, nuint value_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_edge_cases_legacy_value")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* edge_cases_edge_cases_legacy_value(int value, nuint* out_len, FfiError* out_err); +} + +// END EdgeCases.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__EdgeCases_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__EdgeCases_csproj.snap new file mode 100644 index 00000000..7b8b128d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__EdgeCases_csproj.snap @@ -0,0 +1,26 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + + + + + net8.0 + EdgeCases + EdgeCases + EdgeCases + 0.1.0 + Native bindings for edge_cases + enable + true + true + + $(NoWarn);CS0618 + + + + + diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__README_md.snap index 96a3a5e8..83f75ad4 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__README_md.snap @@ -1,13 +1,17 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI (.NET) +# EdgeCases (.NET) -Auto-generated P/Invoke bindings for the WeaveFFI native library. +Native bindings for edge_cases + +The `EdgeCases` namespace binds the `edge_cases` native library through +source-generated P/Invoke (`[LibraryImport]`), so it's trim- and +AOT-friendly. ## Build @@ -15,12 +19,14 @@ Auto-generated P/Invoke bindings for the WeaveFFI native library. dotnet build ``` -## Pack +Reference this project from yours (`dotnet add reference EdgeCases.csproj`) +or pack it (`dotnet pack -c Release`). -```bash -dotnet pack -``` +## Loading the native library -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). +The bindings load `edge_cases` with the platform's normal rules +(`libedge_cases.dylib`, `libedge_cases.so`, or `edge_cases.dll` next to the app or on the +library search path). Set `EDGE_CASES_LIBRARY` to a full path to load a +specific file instead. diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__Runtime_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__Runtime_cs.snap new file mode 100644 index 00000000..51cdaa58 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__Runtime_cs.snap @@ -0,0 +1,698 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EdgeCases; + +/// Raised for a failure the native library reports that isn't a typed +/// domain error: producer bugs, panics, marshalling failures, and exceptions +/// thrown by a callback-interface implementation. Every typed domain +/// exception derives from it. +public class NativeException : Exception +{ + /// An untyped producer error. + public const int GenericErrorCode = -1; + /// The producer panicked; the message carries the panic text. + public const int PanicErrorCode = -2; + /// An argument couldn't be lifted by the producer. + public const int MarshalErrorCode = -3; + /// A callback-interface implementation threw; the message carries + /// the original exception's message. + public const int ForeignErrorCode = -4; + /// The call was cancelled. Surfaces as + /// , never as this type. + public const int CancelledErrorCode = -5; + + /// The error code the native library reported. + public int Code { get; } + + /// Creates an exception carrying a native error code. + public NativeException(int code, string message) : base(message) + { + Code = code; + } + + /// Maps a runtime error code to its exception: cancellation to + /// , anything else to this + /// type. + internal static Exception FromError(int code, string message, byte[]? payload) + { + if (code == CancelledErrorCode) + { + return new OperationCanceledException(message); + } + return new NativeException(code, message); + } +} + +/// Maps a native error code, message, and serialized payload to the +/// exception a call throws. +internal delegate Exception FfiErrorMap(int code, string message, byte[]? payload); + +/// The native edge_cases_error slot. +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct FfiError +{ + public int Code; + public byte* Message; + public byte* PayloadPtr; + public nuint PayloadLen; +} + +/// Marshalling helpers shared by every generated call. +internal static unsafe class Ffi +{ + /// The UTF-8 bytes of a string argument. The native side borrows + /// them as a (pointer, length) pair, so interior NULs survive. + internal static byte[] Utf8(string value) + { + return Encoding.UTF8.GetBytes(value); + } + + /// Decodes a borrowed UTF-8 run; the native side keeps + /// ownership. + internal static string ReadString(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? "" : Encoding.UTF8.GetString(ptr, checked((int)len)); + } + + /// Copies a borrowed byte run; the native side keeps + /// ownership. + internal static byte[] ReadBytes(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? Array.Empty() : new ReadOnlySpan(ptr, checked((int)len)).ToArray(); + } + + /// Decodes a returned UTF-8 string and releases it. + internal static string TakeString(byte* ptr, nuint len) + { + try + { + return ReadString(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies returned bytes and releases them. + internal static byte[] TakeBytes(byte* ptr, nuint len) + { + try + { + return ReadBytes(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies a returned value buffer, releases it, and returns a + /// reader over the copy. + internal static FfiBufferReader TakeBuffer(byte* ptr, nuint len) + { + return new FfiBufferReader(TakeBytes(ptr, len)); + } + + /// Releases a caller-owned error slot and returns the exception + /// it described. + internal static Exception TakeError(FfiError* err, FfiErrorMap map) + { + var code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorClear(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + /// Releases a heap-boxed async error and returns the exception it + /// described. + internal static Exception TakeBoxedError(FfiError* err, FfiErrorMap map, out int code) + { + code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorFree(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + private static string ErrorMessage(FfiError* err) + { + return Marshal.PtrToStringUTF8((IntPtr)err->Message) ?? ""; + } + + /// Reports a callback-interface failure to the producer through + /// out_err. The producer copies the message. + internal static void SetForeignError(FfiError* err, Exception e) + { + var bytes = Encoding.UTF8.GetBytes(e.Message + "\0"); + fixed (byte* message = bytes) + { + NativeMethods.ErrorSet(err, NativeException.ForeignErrorCode, message); + } + } + + /// Pins a callback-interface implementation for the producer and + /// returns the ctx it hands back to every trampoline. The + /// producer releases it through the vtable's free. + internal static IntPtr Register(object implementation) + { + return GCHandle.ToIntPtr(GCHandle.Alloc(implementation)); + } + + /// Releases a ctx the producer never received (the call + /// failed before reaching it) or no longer holds. + internal static void Unregister(IntPtr ctx) + { + GCHandle.FromIntPtr(ctx).Free(); + } + + /// The implementation behind a trampoline's ctx. + internal static T Target(IntPtr ctx) where T : class + { + return (T)GCHandle.FromIntPtr(ctx).Target!; + } +} + +/// Imports of the native runtime surface plus the load-time contract +/// check. The generated API declares its own imports in the other half of +/// this partial class. +internal static unsafe partial class NativeMethods +{ + /// The native library's base name. + internal const string LibName = "edge_cases"; + + /// The environment variable naming an explicit library + /// path. + internal const string LibraryEnvVar = "EDGE_CASES_LIBRARY"; + + /// The C ABI revision these bindings were generated + /// against. + internal const uint AbiVersion = 3; + + // Runs before the first native call through this class, so a library + // built for another ABI revision or another contract fails here instead + // of misreading memory later. + static NativeMethods() + { + try + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, Resolve); + } + catch (InvalidOperationException) + { + // The assembly already has a resolver; default probing still applies. + } + uint found; + try + { + found = AbiVersionOf(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't export edge_cases_abi_version; these bindings expect ABI revision {AbiVersion}", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"ABI mismatch: these bindings expect revision {AbiVersion} but the loaded {LibName} library reports revision {found}"); + } + VerifyChecksums(); + } + + static partial void VerifyChecksums(); + + private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) + { + if (name != LibName) + { + return IntPtr.Zero; + } + var path = Environment.GetEnvironmentVariable(LibraryEnvVar); + return string.IsNullOrEmpty(path) ? IntPtr.Zero : NativeLibrary.Load(path); + } + + /// Fails the load when module was + /// generated from a different contract than the loaded library + /// implements. + private static void VerifyChecksum(string module, delegate* checksum, ulong expected) + { + ulong found; + try + { + found = checksum(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't implement module '{module}'", e); + } + if (found != expected) + { + throw new InvalidOperationException( + $"contract mismatch for module '{module}': these bindings expect checksum 0x{expected:x16} but the loaded {LibName} library reports 0x{found:x16}; regenerate the bindings for this library"); + } + } + + [LibraryImport(LibName, EntryPoint = "edge_cases_abi_version")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial uint AbiVersionOf(); + + [LibraryImport(LibName, EntryPoint = "edge_cases_error_set")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorSet(FfiError* err, int code, byte* message); + + [LibraryImport(LibName, EntryPoint = "edge_cases_error_clear")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorClear(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_error_free")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorFree(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "edge_cases_free_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void FreeBytes(byte* ptr, nuint len); + + [LibraryImport(LibName, EntryPoint = "edge_cases_cancel_token_create")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr CancelTokenCreate(); + + [LibraryImport(LibName, EntryPoint = "edge_cases_cancel_token_cancel")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenCancel(IntPtr token); + + [LibraryImport(LibName, EntryPoint = "edge_cases_cancel_token_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenDestroy(IntPtr token); +} + +/// One pending async call: the task it completes, the native +/// context that finds it again, and, for a cancellable function, the +/// native cancel token linked to the caller's +/// . +internal sealed class FfiCall +{ + private readonly TaskCompletionSource _tcs = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationToken _cancellation; + private readonly object _gate = new object(); + private GCHandle _self; + private IntPtr _token; + private CancellationTokenRegistration _registration; + private bool _completed; + private bool _cancelling; + + internal FfiCall(CancellationToken cancellation = default) + { + _cancellation = cancellation; + _self = GCHandle.Alloc(this); + } + + /// The task the completion resolves. + internal Task Task => _tcs.Task; + + /// The context argument for the launcher. + internal IntPtr Context => GCHandle.ToIntPtr(_self); + + /// Creates the native cancel token for the launcher and cancels + /// it when the caller's fires. + internal IntPtr CancelToken() + { + _token = NativeMethods.CancelTokenCreate(); + if (_cancellation.CanBeCanceled) + { + _registration = _cancellation.Register(static state => ((FfiCall)state!).Cancel(), this); + } + return _token; + } + + /// Recovers the call behind a completion's context. Runs + /// exactly once per launch, on a producer thread. + internal static FfiCall Complete(IntPtr context) + { + var handle = GCHandle.FromIntPtr(context); + var call = (FfiCall)handle.Target!; + handle.Free(); + call.Release(); + return call; + } + + /// Releases everything when the launcher threw before the + /// producer took the call, so no completion will arrive. + internal void Abandon() + { + _self.Free(); + Release(); + } + + internal void SetResult(T value) + { + _tcs.TrySetResult(value); + } + + internal void SetException(Exception e) + { + _tcs.TrySetException(e); + } + + /// Faults (or cancels, for the cancelled code) the task from a + /// heap-boxed error, releasing the box. + internal unsafe void SetError(FfiError* err, FfiErrorMap map) + { + var e = Ffi.TakeBoxedError(err, map, out var code); + if (code == NativeException.CancelledErrorCode) + { + _tcs.TrySetCanceled(_cancellation); + } + else + { + _tcs.TrySetException(e); + } + } + + // The registration callback and the completion race: whichever runs + // last destroys the consumer's token reference, so a cancel never + // touches a destroyed token. + private void Cancel() + { + lock (_gate) + { + if (_completed) + { + return; + } + _cancelling = true; + } + NativeMethods.CancelTokenCancel(_token); + bool destroy; + lock (_gate) + { + _cancelling = false; + destroy = _completed; + } + if (destroy) + { + NativeMethods.CancelTokenDestroy(_token); + } + } + + private void Release() + { + if (_token == IntPtr.Zero) + { + return; + } + bool destroy; + lock (_gate) + { + _completed = true; + destroy = !_cancelling; + } + if (destroy) + { + _registration.Dispose(); + NativeMethods.CancelTokenDestroy(_token); + } + } +} + +/// Owns one native iterator and destroys it exactly once, when +/// enumeration ends, the enumerator is disposed, or the garbage collector +/// finalizes an abandoned one. +internal sealed unsafe class FfiIteratorHandle : SafeHandle +{ + private readonly delegate* _destroy; + + internal FfiIteratorHandle(IntPtr iterator, delegate* destroy) + : base(IntPtr.Zero, true) + { + _destroy = destroy; + SetHandle(iterator); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + _destroy(handle); + return true; + } +} + +/// A lazily streamed sequence backed by a native iterator. It can be +/// enumerated once; each MoveNext pulls one item. +internal sealed unsafe class FfiSequence : IEnumerable, IEnumerator +{ + private readonly FfiIteratorHandle _iterator; + private readonly delegate* _next; + private int _started; + private T _current = default!; + + internal FfiSequence(FfiIteratorHandle iterator, delegate* next) + { + _iterator = iterator; + _next = next; + } + + public T Current => _current; + + object? IEnumerator.Current => _current; + + public IEnumerator GetEnumerator() + { + if (Interlocked.Exchange(ref _started, 1) != 0) + { + throw new InvalidOperationException("this sequence can be enumerated only once"); + } + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool MoveNext() + { + if (_iterator.IsClosed) + { + return false; + } + if (_next(_iterator, out _current)) + { + return true; + } + _iterator.Dispose(); + return false; + } + + public void Reset() + { + throw new NotSupportedException("a native iterator can't be reset"); + } + + public void Dispose() + { + _iterator.Dispose(); + } +} + +/// Serializes values into the value-buffer wire format +/// (little-endian, packed). +internal sealed class FfiBufferWriter +{ + private byte[] _buf = new byte[64]; + private int _len; + + /// The bytes written so far. + internal ReadOnlySpan Written => _buf.AsSpan(0, _len); + + /// The number of bytes written so far. + internal int Length => _len; + + private Span Grow(int extra) + { + if (_len + extra > _buf.Length) + { + Array.Resize(ref _buf, Math.Max(_buf.Length * 2, _len + extra)); + } + var span = _buf.AsSpan(_len, extra); + _len += extra; + return span; + } + + internal void WriteBool(bool v) => WriteU8(v ? (byte)1 : (byte)0); + + internal void WriteI8(sbyte v) => WriteU8((byte)v); + + internal void WriteU8(byte v) => Grow(1)[0] = v; + + internal void WriteI16(short v) => System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); + + internal void WriteU16(ushort v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); + + internal void WriteI32(int v) => System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); + + internal void WriteU32(uint v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); + + internal void WriteI64(long v) => System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); + + internal void WriteU64(ulong v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); + + internal void WriteF32(float v) => System.Buffers.Binary.BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); + + internal void WriteF64(double v) => System.Buffers.Binary.BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); + + internal void WriteLen(int len) => WriteU32((uint)len); + + internal void WriteString(string v) + { + var count = Encoding.UTF8.GetByteCount(v); + WriteLen(count); + Encoding.UTF8.GetBytes(v, Grow(count)); + } + + internal void WriteBytes(byte[] v) + { + WriteLen(v.Length); + v.CopyTo(Grow(v.Length)); + } + + /// Writes an object token. The token must carry its own strong + /// reference (a fresh clone), which the reader adopts. + internal void WriteObject(IntPtr token) => WriteU64((ulong)(nuint)token); +} + +/// Decodes values from the value-buffer wire format. A malformed +/// buffer is a producer or consumer bug and throws +/// . +internal sealed class FfiBufferReader +{ + private static readonly Encoding Utf8Strict = new UTF8Encoding(false, true); + + private readonly byte[] _data; + private int _pos; + + internal FfiBufferReader(byte[] data) + { + _data = data; + } + + private static InvalidOperationException Malformed(string what) + { + return new InvalidOperationException("malformed value buffer: " + what); + } + + private ReadOnlySpan Take(int n) + { + if (_data.Length - _pos < n) + { + throw Malformed("buffer exhausted"); + } + var span = _data.AsSpan(_pos, n); + _pos += n; + return span; + } + + internal bool ReadBool() + { + var b = ReadU8(); + if (b > 1) + { + throw Malformed("invalid bool byte"); + } + return b == 1; + } + + internal sbyte ReadI8() => (sbyte)ReadU8(); + + internal byte ReadU8() => Take(1)[0]; + + internal short ReadI16() => System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(Take(2)); + + internal ushort ReadU16() => System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); + + internal int ReadI32() => System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(Take(4)); + + internal uint ReadU32() => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); + + internal long ReadI64() => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(Take(8)); + + internal ulong ReadU64() => System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); + + internal float ReadF32() => System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(Take(4)); + + internal double ReadF64() => System.Buffers.Binary.BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); + + /// A collection count. Elements can be zero-sized, so the count + /// isn't checked against the remaining bytes; callers cap their + /// preallocation with . + internal int ReadLen() + { + var len = ReadU32(); + if (len > int.MaxValue) + { + throw Malformed("length prefix out of range"); + } + return (int)len; + } + + /// A preallocation size for elements + /// that a malformed count can't inflate. + internal int Capacity(int count) => Math.Min(count, _data.Length - _pos); + + internal string ReadString() + { + var bytes = Take(ReadLen()); + try + { + return Utf8Strict.GetString(bytes); + } + catch (ArgumentException) + { + throw Malformed("string is not valid UTF-8"); + } + } + + internal byte[] ReadBytes() => Take(ReadLen()).ToArray(); + + /// Reads an object token carrying one strong reference. + internal IntPtr ReadObject() + { + var token = ReadU64(); + if (token == 0) + { + throw Malformed("null object token"); + } + return (IntPtr)(nint)token; + } + + internal void ExpectEnd() + { + if (_pos != _data.Length) + { + throw Malformed("trailing bytes"); + } + } +} + +// END Runtime.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_cs.snap deleted file mode 100644 index e438c25b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_cs.snap +++ /dev/null @@ -1,2749 +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.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace WeaveFFI -{ - /// Raised for any WeaveFFI failure that isn't a typed domain - /// error: producer bugs, panics, marshalling failures, and exceptions - /// thrown by a callback-interface implementation. - public class WeaveFFIException : Exception - { - /// An untyped producer error. - public const int GenericErrorCode = -1; - /// The producer panicked; the message carries the panic text. - public const int PanicErrorCode = -2; - /// An argument couldn't be lifted by the producer. - public const int MarshalErrorCode = -3; - /// A callback-interface implementation threw; the message - /// carries the original exception's message. - public const int ForeignErrorCode = -4; - - 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); - } - } - } - - /// One strong reference to a native object, on its way into - /// a wrapper that adopts it. A distinct type (rather than a bare - /// IntPtr) so the adopting constructor never competes with a - /// public constructor taking an integer. - internal readonly struct WeaveFFIHandle - { - internal readonly IntPtr Value; - - internal WeaveFFIHandle(IntPtr value) - { - Value = value; - } - } - - 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 void WriteObject(IntPtr token) - { - WriteU64((ulong)(nuint)(nint)token); - } - - 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 IntPtr ReadObject() - { - var token = ReadU64(); - if (token == 0) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: null object token"); - } - return (nint)(nuint)token; - } - - 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(); - } - } - - /// 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); - } - } - - /// Record carrying objects in every buffered position - public sealed class WidgetHolder - { - public Widget Primary { get; } - - public Widget? Fallback { get; } - - public Widget[] Extras { get; } - - public Dictionary ByName { get; } - - public WidgetHolder(Widget primary, Widget? fallback, Widget[] extras, Dictionary byName) - { - Primary = primary; - Fallback = fallback; - Extras = extras; - ByName = byName; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteObject(Primary.CloneHandle()); - if (Fallback != null) - { - writer.WriteOptionFlag(true); - writer.WriteObject(Fallback!.CloneHandle()); - } - else - { - writer.WriteOptionFlag(false); - } - writer.WriteLen(Extras.Length); - foreach (var item0 in Extras) - { - writer.WriteObject(item0.CloneHandle()); - } - writer.WriteLen(ByName.Count); - foreach (var entry0 in ByName) - { - writer.WriteString(entry0.Key); - writer.WriteObject(entry0.Value.CloneHandle()); - } - } - - internal static WidgetHolder ReadFrom(WeaveFFIBufferReader reader) - { - var fPrimary = new Widget(new WeaveFFIHandle(reader.ReadObject())); - Widget? fFallback = null; - if (reader.ReadOptionFlag()) - { - var fFallbackValue = new Widget(new WeaveFFIHandle(reader.ReadObject())); - fFallback = fFallbackValue; - } - var fExtrasCount = reader.ReadLen(); - var fExtras = new Widget[fExtrasCount]; - for (int i0 = 0; i0 < fExtrasCount; i0++) - { - var fExtrasItem = new Widget(new WeaveFFIHandle(reader.ReadObject())); - fExtras[i0] = fExtrasItem; - } - var fByNameCount = reader.ReadLen(); - var fByName = new Dictionary(fByNameCount); - for (int i0 = 0; i0 < fByNameCount; i0++) - { - var fByNameKey = reader.ReadString(); - var fByNameVal = new Widget(new WeaveFFIHandle(reader.ReadObject())); - fByName[fByNameKey] = fByNameVal; - } - return new WidgetHolder(fPrimary, fFallback, fExtras, fByName); - } - } - - /// Callback interface carrying composite payloads and objects - /// Implement this interface and pass an instance wherever the - /// native library expects it. The library may call any method from any - /// thread until it releases its last reference to the instance. Object - /// arguments are owned by the implementation, which should dispose them - /// when done. An exception thrown by a method is reported to the native - /// caller as . - public interface IBatchListener - { - /// Composite payloads cross as value buffers - void OnBatch(Outer[] items, byte[] raw, Outer? first, Mode mode); - /// Objects cross as strong references the consumer must release - void OnWidget(Widget widget, Widget? fallback, Widget[] children); - void OnTick(); - /// Scalar return travels back through the vtable - bool ShouldContinue(int @class); - /// Enum return travels back as its integer discriminant - Mode PickMode(); - } - - /// The process-wide weaveffi_edge_cases_BatchListener_vtable vtable and the trampolines behind - /// it, adapting an to the C ABI. - internal static unsafe class WeaveFFIVtable_edge_cases_BatchListener - { - [StructLayout(LayoutKind.Sequential)] - private struct Layout - { - public IntPtr on_batch; - public IntPtr on_widget; - public IntPtr on_tick; - public IntPtr should_continue; - public IntPtr pick_mode; - public IntPtr free; - } - - /// Address of the one static vtable for this interface. It is - /// allocated once and never freed, so the producer may keep the pointer - /// for the process lifetime. - internal static readonly IntPtr Pointer = Allocate(); - - private static IntPtr Allocate() - { - var layout = new Layout - { - on_batch = (IntPtr)(delegate* unmanaged[Cdecl])&OnBatchTrampoline, - on_widget = (IntPtr)(delegate* unmanaged[Cdecl])&OnWidgetTrampoline, - on_tick = (IntPtr)(delegate* unmanaged[Cdecl])&OnTickTrampoline, - should_continue = (IntPtr)(delegate* unmanaged[Cdecl])&ShouldContinueTrampoline, - pick_mode = (IntPtr)(delegate* unmanaged[Cdecl])&PickModeTrampoline, - free = (IntPtr)(delegate* unmanaged[Cdecl])&FreeTrampoline, - }; - var mem = Marshal.AllocHGlobal(Marshal.SizeOf()); - Marshal.StructureToPtr(layout, mem, false); - return mem; - } - - private static IBatchListener Target(IntPtr ctx) - { - return (IBatchListener)GCHandle.FromIntPtr(ctx).Target!; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnBatchTrampoline(IntPtr ctx, IntPtr items_ptr, UIntPtr items_len, IntPtr raw_ptr, UIntPtr raw_len, IntPtr first_ptr, UIntPtr first_len, int mode, IntPtr out_err) - { - try - { - var impl = Target(ctx); - 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(); - impl.OnBatch(arg0, arg1, arg2, (Mode)mode); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnWidgetTrampoline(IntPtr ctx, IntPtr widget, IntPtr fallback, IntPtr children_ptr, UIntPtr children_len, IntPtr out_err) - { - try - { - var impl = Target(ctx); - var arg2Buf = new byte[(int)children_len]; - if (children_ptr != IntPtr.Zero && (int)children_len > 0) Marshal.Copy(children_ptr, arg2Buf, 0, (int)children_len); - var arg2Reader = new WeaveFFIBufferReader(arg2Buf); - var arg2Count = arg2Reader.ReadLen(); - var arg2 = new Widget[arg2Count]; - for (int i0 = 0; i0 < arg2Count; i0++) - { - var arg2Item = new Widget(new WeaveFFIHandle(arg2Reader.ReadObject())); - arg2[i0] = arg2Item; - } - arg2Reader.ExpectEnd(); - impl.OnWidget(new Widget(new WeaveFFIHandle(widget)), fallback == IntPtr.Zero ? null : new Widget(new WeaveFFIHandle(fallback)), arg2); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnTickTrampoline(IntPtr ctx, IntPtr out_err) - { - try - { - var impl = Target(ctx); - impl.OnTick(); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static byte ShouldContinueTrampoline(IntPtr ctx, int @class, IntPtr out_err) - { - try - { - var impl = Target(ctx); - return (byte)(impl.ShouldContinue(@class) ? 1 : 0); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - return default; - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static int PickModeTrampoline(IntPtr ctx, IntPtr out_err) - { - try - { - var impl = Target(ctx); - return (int)impl.PickMode(); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - return default; - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void FreeTrampoline(IntPtr ctx) - { - GCHandle.FromIntPtr(ctx).Free(); - } - } - - /// Callback interface whose name and methods are keywords - /// Implement this interface and pass an instance wherever the - /// native library expects it. The library may call any method from any - /// thread until it releases its last reference to the instance. Object - /// arguments are owned by the implementation, which should dispose them - /// when done. An exception thrown by a method is reported to the native - /// caller as . - public interface IKeyword - { - void Delete(int self); - int Import(); - } - - /// The process-wide weaveffi_edge_cases_Keyword_vtable vtable and the trampolines behind - /// it, adapting an to the C ABI. - internal static unsafe class WeaveFFIVtable_edge_cases_Keyword - { - [StructLayout(LayoutKind.Sequential)] - private struct Layout - { - public IntPtr delete; - public IntPtr import; - public IntPtr free; - } - - /// Address of the one static vtable for this interface. It is - /// allocated once and never freed, so the producer may keep the pointer - /// for the process lifetime. - internal static readonly IntPtr Pointer = Allocate(); - - private static IntPtr Allocate() - { - var layout = new Layout - { - delete = (IntPtr)(delegate* unmanaged[Cdecl])&DeleteTrampoline, - import = (IntPtr)(delegate* unmanaged[Cdecl])&ImportTrampoline, - free = (IntPtr)(delegate* unmanaged[Cdecl])&FreeTrampoline, - }; - var mem = Marshal.AllocHGlobal(Marshal.SizeOf()); - Marshal.StructureToPtr(layout, mem, false); - return mem; - } - - private static IKeyword Target(IntPtr ctx) - { - return (IKeyword)GCHandle.FromIntPtr(ctx).Target!; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void DeleteTrampoline(IntPtr ctx, int self, IntPtr out_err) - { - try - { - var impl = Target(ctx); - impl.Delete(self); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static int ImportTrampoline(IntPtr ctx, IntPtr out_err) - { - try - { - var impl = Target(ctx); - return impl.Import(); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - return default; - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void FreeTrampoline(IntPtr ctx) - { - GCHandle.FromIntPtr(ctx).Free(); - } - } - - /// Interface with no constructor; instances come from make_widget - public class Widget : IDisposable - { - private IntPtr _handle; - private int _released; - - /// Adopts one strong reference to a native object. - internal Widget(WeaveFFIHandle handle) - { - _handle = handle.Value; - } - - /// The borrowed native pointer for the duration of a call. - /// The wrapper was disposed. - internal IntPtr Handle - { - get - { - var h = _handle; - if (h == IntPtr.Zero) - { - throw new ObjectDisposedException(nameof(Widget)); - } - return h; - } - } - - /// Mints a second strong reference the caller owns (for - /// example to write this object into a value buffer). - internal IntPtr CloneHandle() - { - return NativeMethods.weaveffi_edge_cases_Widget_clone(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 ?? ""; - } - - /// Releases this wrapper's reference. The native object is - /// dropped when the producer releases its last reference; other - /// wrappers to the same object stay valid. - public void Dispose() - { - Release(); - GC.SuppressFinalize(this); - } - - ~Widget() - { - Release(); - } - - private void Release() - { - if (System.Threading.Interlocked.Exchange(ref _released, 1) != 0) - { - return; - } - var h = _handle; - _handle = IntPtr.Zero; - if (h != IntPtr.Zero) - { - NativeMethods.weaveffi_edge_cases_Widget_destroy(h); - } - } - } - - /// Deprecated interface with a throwing constructor - public class Vault : IDisposable - { - private IntPtr _handle; - private int _released; - - /// Adopts one strong reference to a native object. - internal Vault(WeaveFFIHandle handle) - { - _handle = handle.Value; - } - - /// The borrowed native pointer for the duration of a call. - /// The wrapper was disposed. - internal IntPtr Handle - { - get - { - var h = _handle; - if (h == IntPtr.Zero) - { - throw new ObjectDisposedException(nameof(Vault)); - } - return h; - } - } - - /// Mints a second strong reference the caller owns (for - /// example to write this object into a value buffer). - internal IntPtr CloneHandle() - { - return NativeMethods.weaveffi_edge_cases_Vault_clone(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(new WeaveFFIHandle(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(new WeaveFFIHandle(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; - } - - /// Releases this wrapper's reference. The native object is - /// dropped when the producer releases its last reference; other - /// wrappers to the same object stay valid. - public void Dispose() - { - Release(); - GC.SuppressFinalize(this); - } - - ~Vault() - { - Release(); - } - - private void Release() - { - if (System.Threading.Interlocked.Exchange(ref _released, 1) != 0) - { - return; - } - var h = _handle; - _handle = IntPtr.Zero; - if (h != IntPtr.Zero) - { - NativeMethods.weaveffi_edge_cases_Vault_destroy(h); - } - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - // The ABI revision these bindings were generated against. - internal const uint AbiVersion = 2; - - // 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(); - - // Fills a producer-owned error slot with a copy of `message`; callback - // trampolines report a thrown exception through it so no managed - // allocation is ever handed to the producer's allocator. - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_set(IntPtr err, int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string message); - - [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_clone", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_edge_cases_Widget_clone(IntPtr self); - - [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 out_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 out_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 out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Vault_clone", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_edge_cases_Vault_clone(IntPtr self); - - [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 out_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 out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Vault_probe", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_edge_cases_Vault_probe(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_noop", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_edge_cases_noop(ref WeaveFFIError out_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 out_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 out_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 out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_register_batch", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_edge_cases_register_batch(IntPtr listener_ctx, IntPtr listener_vtable, IntPtr keyword_ctx, IntPtr keyword_vtable, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_widgets_everywhere", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_edge_cases_widgets_everywhere(IntPtr list_ptr, UIntPtr list_len, IntPtr by_name_ptr, UIntPtr by_name_len, IntPtr sparse_ptr, UIntPtr sparse_len, IntPtr holder_ptr, UIntPtr holder_len, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_widget_stream", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_edge_cases_widget_stream(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_WidgetStreamIterator_next", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_edge_cases_WidgetStreamIterator_next(IntPtr iter, out IntPtr out_item, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_WidgetStreamIterator_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_edge_cases_WidgetStreamIterator_destroy(IntPtr iter); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void AsyncCb_weaveffi_edge_cases_async_widget(IntPtr context, IntPtr err, IntPtr result); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_async_widget_async", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_edge_cases_async_widget_async(AsyncCb_weaveffi_edge_cases_async_widget callback, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_make_widget", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_edge_cases_make_widget(IntPtr label, ref WeaveFFIError out_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 out_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 out_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 out_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 out_err); - - } - - public static class EdgeCases - { - /// 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(); - } - } - - /// Two callback interface parameters on one function - public static void RegisterBatch(IBatchListener listener, IKeyword keyword) - { - var err = new WeaveFFIError(); - var listenerCtx = GCHandle.ToIntPtr(GCHandle.Alloc(listener)); - var keywordCtx = GCHandle.ToIntPtr(GCHandle.Alloc(keyword)); - NativeMethods.weaveffi_edge_cases_register_batch(listenerCtx, WeaveFFIVtable_edge_cases_BatchListener.Pointer, keywordCtx, WeaveFFIVtable_edge_cases_Keyword.Pointer, ref err); - WeaveFFIError.Check(err); - } - - /// Objects in every buffered position - public static WidgetHolder WidgetsEverywhere(Widget[] list, Dictionary byName, Widget?[] sparse, WidgetHolder holder) - { - var err = new WeaveFFIError(); - var listWriter = new WeaveFFIBufferWriter(); - listWriter.WriteLen(list.Length); - foreach (var item0 in list) - { - listWriter.WriteObject(item0.CloneHandle()); - } - var listBuf = listWriter.ToArray(); - var listPin = GCHandle.Alloc(listBuf, GCHandleType.Pinned); - var byNameWriter = new WeaveFFIBufferWriter(); - byNameWriter.WriteLen(byName.Count); - foreach (var entry0 in byName) - { - byNameWriter.WriteString(entry0.Key); - byNameWriter.WriteObject(entry0.Value.CloneHandle()); - } - var byNameBuf = byNameWriter.ToArray(); - var byNamePin = GCHandle.Alloc(byNameBuf, GCHandleType.Pinned); - var sparseWriter = new WeaveFFIBufferWriter(); - sparseWriter.WriteLen(sparse.Length); - foreach (var item0 in sparse) - { - if (item0 != null) - { - sparseWriter.WriteOptionFlag(true); - sparseWriter.WriteObject(item0!.CloneHandle()); - } - else - { - sparseWriter.WriteOptionFlag(false); - } - } - var sparseBuf = sparseWriter.ToArray(); - var sparsePin = GCHandle.Alloc(sparseBuf, GCHandleType.Pinned); - var holderWriter = new WeaveFFIBufferWriter(); - holder.WriteTo(holderWriter); - var holderBuf = holderWriter.ToArray(); - var holderPin = GCHandle.Alloc(holderBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_edge_cases_widgets_everywhere(listPin.AddrOfPinnedObject(), (UIntPtr)listBuf.Length, byNamePin.AddrOfPinnedObject(), (UIntPtr)byNameBuf.Length, sparsePin.AddrOfPinnedObject(), (UIntPtr)sparseBuf.Length, holderPin.AddrOfPinnedObject(), (UIntPtr)holderBuf.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 = WidgetHolder.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - finally - { - listPin.Free(); - byNamePin.Free(); - sparsePin.Free(); - holderPin.Free(); - } - } - - /// Iterator of objects - /// 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 WidgetStream() - { - var err = new WeaveFFIError(); - var iter = NativeMethods.weaveffi_edge_cases_widget_stream(ref err); - WeaveFFIError.Check(err); - return new WeaveFFIOnceEnumerable(EnumerateWidgetStream(iter)); - } - - private static IEnumerator EnumerateWidgetStream(IntPtr iter) - { - try - { - while (true) - { - var iterErr = new WeaveFFIError(); - if (NativeMethods.weaveffi_edge_cases_WidgetStreamIterator_next(iter, out var out_item, ref iterErr) == 0) - { - WeaveFFIError.Check(iterErr); - yield break; - } - WeaveFFIError.Check(iterErr); - yield return new Widget(new WeaveFFIHandle(out_item)); - } - } - finally - { - NativeMethods.weaveffi_edge_cases_WidgetStreamIterator_destroy(iter); - } - } - - /// Async returning an object - /// Thrown when the call reports a EdgeErrorsError code. - public static async Task AsyncWidget() - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - NativeMethods.AsyncCb_weaveffi_edge_cases_async_widget 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(EdgeErrorsException.FromCode(wErr.Code, msg, payload)); - return; - } - } - tcs.SetResult(new Widget(new WeaveFFIHandle(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_widget_async(callback, ctx); - } - catch - { - if (gcHandle.IsAllocated) gcHandle.Free(); - throw; - } - return await tcs.Task; - } - - /// 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(new WeaveFFIHandle(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(new WeaveFFIHandle(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 ?? ""; - } - - /// 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_edge_cases__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__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_edge_cases__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__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__KitchenSink_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_cs.snap new file mode 100644 index 00000000..8586ed9e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_cs.snap @@ -0,0 +1,929 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace KitchenSink; + +/// Typed exception for the KitchenErrorsError error domain (module kitchen). +public class KitchenErrorsException : NativeException +{ + /// Item not found + public const int NotFound = 1; + + /// Invalid input + public const int InvalidInput = 2; + + /// Creates an exception carrying a domain error code. + public KitchenErrorsException(int code, string message) : base(code, message) + { + } + + internal static new Exception FromError(int code, string message, byte[]? payload) + { + switch (code) + { + case NotFound: + return new KitchenErrorsException(code, string.IsNullOrEmpty(message) ? "Item not found" : message); + case InvalidInput: + return new KitchenErrorsException(code, string.IsNullOrEmpty(message) ? "Invalid input" : message); + default: + return global::KitchenSink.NativeException.FromError(code, message, payload); + } + } +} + +/// A reusable token referenced across modules +public sealed class Token +{ + public long Id { get; } + + public string Label { get; } + + /// Creates a from every field. + public Token(long id, string label) + { + Id = id; + Label = label; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteI64(Id); + writer.WriteString(Label); + } + + internal static Token ReadFrom(FfiBufferReader reader) + { + var fId = reader.ReadI64(); + var fLabel = reader.ReadString(); + return new Token(fId, fLabel); + } +} + +/// Task priority levels +public enum Priority +{ + /// Low priority + Low = 0, + /// Normal priority + Normal = 1, + /// High priority + High = 2, +} + +/// Kitchen sink struct exercising every field feature +public sealed class Item +{ + /// Stable identifier + public long Id { get; } + + /// Display name + public string Name { get; } + + /// Initial count + public int Count { get; } + + /// Whether the item is enabled + public bool Enabled { get; } + + /// Scaling ratio + public double Ratio { get; } + + public uint Nick { get; } + + public byte[] Payload { get; } + + public string[] Tags { get; } + + public Dictionary Attrs { get; } + + public long? Parent { get; } + + /// Cross-module struct reference + public Token Token { get; } + + public Priority Priority { get; } + + /// An optional object inside a record + public Gadget? Gadget { get; } + + /// Creates a from every field. + public Item(long id, string name, int count, bool enabled, double ratio, uint nick, byte[] payload, string[] tags, Dictionary attrs, long? parent, Token token, Priority priority, Gadget? gadget) + { + Id = id; + Name = name; + Count = count; + Enabled = enabled; + Ratio = ratio; + Nick = nick; + Payload = payload; + Tags = tags; + Attrs = attrs; + Parent = parent; + Token = token; + Priority = priority; + Gadget = gadget; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteI64(Id); + writer.WriteString(Name); + writer.WriteI32(Count); + writer.WriteBool(Enabled); + writer.WriteF64(Ratio); + writer.WriteU32(Nick); + writer.WriteBytes(Payload); + writer.WriteLen(Tags.Length); + foreach (var item0 in Tags) + { + writer.WriteString(item0); + } + writer.WriteLen(Attrs.Count); + foreach (var entry0 in Attrs) + { + writer.WriteString(entry0.Key); + writer.WriteString(entry0.Value); + } + writer.WriteBool(Parent != null); + if (Parent != null) + { + writer.WriteI64(Parent.Value); + } + Token.WriteTo(writer); + writer.WriteI32((int)Priority); + writer.WriteBool(Gadget != null); + if (Gadget != null) + { + writer.WriteObject(Gadget!.CloneHandle()); + } + } + + internal static Item ReadFrom(FfiBufferReader reader) + { + var fId = reader.ReadI64(); + var fName = reader.ReadString(); + var fCount = reader.ReadI32(); + var fEnabled = reader.ReadBool(); + var fRatio = reader.ReadF64(); + var fNick = reader.ReadU32(); + var fPayload = reader.ReadBytes(); + var fTagsCount = reader.ReadLen(); + var fTagsList = new List(reader.Capacity(fTagsCount)); + for (var i0 = 0; i0 < fTagsCount; i0++) + { + var fTagsItem = reader.ReadString(); + fTagsList.Add(fTagsItem); + } + var fTags = fTagsList.ToArray(); + var fAttrsCount = reader.ReadLen(); + var fAttrs = new Dictionary(reader.Capacity(fAttrsCount)); + for (var i0 = 0; i0 < fAttrsCount; i0++) + { + var fAttrsKey = reader.ReadString(); + var fAttrsVal = reader.ReadString(); + fAttrs[fAttrsKey] = fAttrsVal; + } + long? fParent = null; + if (reader.ReadBool()) + { + var fParentValue = reader.ReadI64(); + fParent = fParentValue; + } + var fToken = global::KitchenSink.Token.ReadFrom(reader); + var fPriority = (Priority)reader.ReadI32(); + Gadget? fGadget = null; + if (reader.ReadBool()) + { + var fGadgetValue = global::KitchenSink.Gadget.Adopt(reader.ReadObject()); + fGadget = fGadgetValue; + } + return new Item(fId, fName, fCount, fEnabled, fRatio, fNick, fPayload, fTags, fAttrs, fParent, fToken, fPriority, fGadget); + } +} + +/// Consumer-implemented observer of ready items +/// The native library may call any method from any thread until +/// it releases its last reference to the implementation. Object arguments +/// belong to the implementation. An exception thrown by a method reaches +/// the native caller as . +public interface IReadyListener +{ + /// Fires when an item is ready + void OnReady(int code, string msg); + /// Receives the item itself and says whether to keep listening + bool OnItem(Item item, Gadget gadget); +} + +/// The process-wide kitchen_sink_kitchen_ReadyListener_vtable and the trampolines behind it, +/// adapting an to the C ABI. +internal static unsafe class FfiVtable_kitchen_ReadyListener +{ + [StructLayout(LayoutKind.Sequential)] + private struct Layout + { + public delegate* unmanaged[Cdecl] on_ready; + public delegate* unmanaged[Cdecl] on_item; + public delegate* unmanaged[Cdecl] free; + } + + /// The vtable's address, valid for the process lifetime. + internal static readonly IntPtr Pointer = Allocate(); + + private static IntPtr Allocate() + { + var vtable = (Layout*)NativeMemory.Alloc((nuint)sizeof(Layout)); + vtable->on_ready = &OnReadyTrampoline; + vtable->on_item = &OnItemTrampoline; + vtable->free = &FreeTrampoline; + return (IntPtr)vtable; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void OnReadyTrampoline(IntPtr ctx, int code, byte* msg_ptr, nuint msg_len, FfiError* out_err) + { + try + { + Ffi.Target(ctx).OnReady(code, Ffi.ReadString(msg_ptr, msg_len)); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static byte OnItemTrampoline(IntPtr ctx, byte* item_ptr, nuint item_len, IntPtr gadget, FfiError* out_err) + { + try + { + var arg0Reader = new FfiBufferReader(Ffi.ReadBytes(item_ptr, item_len)); + var arg0 = global::KitchenSink.Item.ReadFrom(arg0Reader); + arg0Reader.ExpectEnd(); + return (byte)(Ffi.Target(ctx).OnItem(arg0, global::KitchenSink.Gadget.Adopt(gadget)) ? 1 : 0); + } + catch (Exception e) + { + Ffi.SetForeignError(out_err, e); + return default; + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void FreeTrampoline(IntPtr ctx) + { + Ffi.Unregister(ctx); + } +} + +/// A pocket-sized interface exercising the object surface +public sealed unsafe class Gadget : IDisposable, IEquatable +{ + /// The native object; borrowed by every call. + internal NativeHandle Handle { get; } + + private Gadget(NativeHandle handle) + { + Handle = handle; + } + + /// Wraps one strong reference the caller transfers. + internal static Gadget Adopt(IntPtr ptr) + { + return new Gadget(new NativeHandle(ptr)); + } + + /// Mints a second strong reference the caller owns, as an + /// object token in a value buffer requires. + internal IntPtr CloneHandle() + { + return NativeMethods.kitchen_sink_kitchen_Gadget_clone(Handle); + } + + /// Create a gadget with the given id + public Gadget(long id) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_new(id, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + Handle = new NativeHandle(ffiResult); + } + + /// Render the gadget as a human-readable string + public string Describe() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_describe(Handle, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } + + /// Poke the gadget a number of times, failing on a negative count + /// The call reported a KitchenErrorsError code. + public int Poke(int times) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_poke(Handle, times, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.KitchenErrorsException.FromError); + return ffiResult; + } + + /// The gadget subsystem version string + public static string Version() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_version(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } + + /// Releases this wrapper's reference. The native object is + /// dropped once its last reference is released, and an in-flight + /// call finishes first. + public void Dispose() + { + Handle.Dispose(); + } + + /// True when both wrappers reference the same native object. + public bool Equals(Gadget? other) + { + return other is not null && other.Handle.DangerousGetHandle() == Handle.DangerousGetHandle(); + } + + /// + public override bool Equals(object? obj) + { + return obj is Gadget other && Equals(other); + } + + /// + public override int GetHashCode() + { + return Handle.DangerousGetHandle().GetHashCode(); + } + + /// One strong reference, released through + /// kitchen_sink_kitchen_Gadget_destroy. + internal sealed class NativeHandle : SafeHandle + { + /// A null handle, passed for an absent optional object. + internal static readonly NativeHandle Null = new NativeHandle(); + + public NativeHandle() : base(IntPtr.Zero, true) + { + } + + internal NativeHandle(IntPtr ptr) : base(IntPtr.Zero, true) + { + SetHandle(ptr); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + NativeMethods.kitchen_sink_kitchen_Gadget_destroy(handle); + return true; + } + } +} + +public static unsafe class Shared +{ + /// Trivial cross-module helper + public static string Ping() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_shared_ping(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } +} + +public static unsafe class Kitchen +{ + /// Identity for bool + public static bool BoolId(bool x) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_bool_id((byte)(x ? 1 : 0), &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult != 0; + } + + public static int I32Id(int x) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_i32_id(x, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult; + } + + public static uint U32Id(uint x) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_u32_id(x, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult; + } + + public static long I64Id(long x) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_i64_id(x, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult; + } + + public static double F64Id(double x) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_f64_id(x, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult; + } + + public static string EchoString(string s) + { + var sBytes = Ffi.Utf8(s); + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* sPtr = sBytes) + { + var ffiResult = NativeMethods.kitchen_sink_kitchen_echo_string(sPtr, (nuint)sBytes.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } + } + + public static byte[] EchoBytes(ReadOnlySpan b) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* bPtr = b) + { + var ffiResult = NativeMethods.kitchen_sink_kitchen_echo_bytes(bPtr, (nuint)b.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return Ffi.TakeBytes(ffiResult, ffiOutLen); + } + } + + public static byte[] EchoBytes(byte[] b) => EchoBytes((ReadOnlySpan)b); + + /// Register a listener, returning how many are attached + public static int Subscribe(IReadyListener listener) + { + var listenerCtx = Ffi.Register(listener); + var ffiErr = default(FfiError); + int ffiResult; + try + { + ffiResult = NativeMethods.kitchen_sink_kitchen_subscribe(listenerCtx, FfiVtable_kitchen_ReadyListener.Pointer, &ffiErr); + } + catch + { + Ffi.Unregister(listenerCtx); + throw; + } + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult; + } + + /// Optional object in both directions + public static Gadget? MaybeGadget(Gadget? fallback) + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_maybe_gadget(fallback?.Handle ?? global::KitchenSink.Gadget.NativeHandle.Null, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult == IntPtr.Zero ? null : global::KitchenSink.Gadget.Adopt(ffiResult); + } + + /// A list of objects + public static Gadget[] Gadgets() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_gadgets(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecodedCount = ffiDecodedReader.ReadLen(); + var ffiDecodedList = new List(ffiDecodedReader.Capacity(ffiDecodedCount)); + for (var i0 = 0; i0 < ffiDecodedCount; i0++) + { + var ffiDecodedItem = global::KitchenSink.Gadget.Adopt(ffiDecodedReader.ReadObject()); + ffiDecodedList.Add(ffiDecodedItem); + } + var ffiDecoded = ffiDecodedList.ToArray(); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + + /// An iterator over objects + /// Streams lazily, one native call per item. The sequence can be + /// enumerated once; disposing its enumerator (as foreach does) + /// releases the native iterator. + public static IEnumerable StreamGadgets() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_stream_gadgets(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.kitchen_sink_kitchen_StreamGadgetsIterator_destroy), &NextStreamGadgets); + } + + private static bool NextStreamGadgets(FfiIteratorHandle iter, out Gadget item) + { + IntPtr ffiItem = default; + var ffiErr = default(FfiError); + var ffiMore = NativeMethods.kitchen_sink_kitchen_StreamGadgetsIterator_next(iter, &ffiItem, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + if (ffiMore == 0) + { + item = default!; + return false; + } + item = global::KitchenSink.Gadget.Adopt(ffiItem); + return true; + } + + /// Optional struct return + /// The call reported a KitchenErrorsError code. + public static Item? MaybeItem(long id) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_maybe_item(id, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.KitchenErrorsException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + Item? ffiDecoded = null; + if (ffiDecodedReader.ReadBool()) + { + var ffiDecodedValue = global::KitchenSink.Item.ReadFrom(ffiDecodedReader); + ffiDecoded = ffiDecodedValue; + } + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + + /// List of structs + public static Item[] ListItems() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_list_items(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecodedCount = ffiDecodedReader.ReadLen(); + var ffiDecodedList = new List(ffiDecodedReader.Capacity(ffiDecodedCount)); + for (var i0 = 0; i0 < ffiDecodedCount; i0++) + { + var ffiDecodedItem = global::KitchenSink.Item.ReadFrom(ffiDecodedReader); + ffiDecodedList.Add(ffiDecodedItem); + } + var ffiDecoded = ffiDecodedList.ToArray(); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + + /// Map return type + public static Dictionary GetAttrs() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_get_attrs(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecodedCount = ffiDecodedReader.ReadLen(); + var ffiDecoded = new Dictionary(ffiDecodedReader.Capacity(ffiDecodedCount)); + for (var i0 = 0; i0 < ffiDecodedCount; i0++) + { + var ffiDecodedKey = ffiDecodedReader.ReadString(); + var ffiDecodedVal = ffiDecodedReader.ReadI32(); + ffiDecoded[ffiDecodedKey] = ffiDecodedVal; + } + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + + /// Iterator return type + /// Streams lazily, one native call per item. The sequence can be + /// enumerated once; disposing its enumerator (as foreach does) + /// releases the native iterator. + public static IEnumerable StreamItems() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_stream_items(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.kitchen_sink_kitchen_StreamItemsIterator_destroy), &NextStreamItems); + } + + private static bool NextStreamItems(FfiIteratorHandle iter, out Item item) + { + byte* ffiItem = default; + nuint ffiLen = default; + var ffiErr = default(FfiError); + var ffiMore = NativeMethods.kitchen_sink_kitchen_StreamItemsIterator_next(iter, &ffiItem, &ffiLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + if (ffiMore == 0) + { + item = default!; + return false; + } + var ffiDecodedReader = Ffi.TakeBuffer(ffiItem, ffiLen); + var ffiDecoded = global::KitchenSink.Item.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + item = ffiDecoded; + return true; + } + + /// Returns the shared Token type from another module + public static Token CrossModuleToken() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_cross_module_token(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::KitchenSink.Token.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + + /// Async operation + public static Task DoAsync(string input) + { + var inputBytes = Ffi.Utf8(input); + var ffiCall = new FfiCall(); + try + { + fixed (byte* inputPtr = inputBytes) + { + NativeMethods.kitchen_sink_kitchen_do_async(inputPtr, (nuint)inputBytes.Length, &CompleteDoAsync, ffiCall.Context); + } + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteDoAsync(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::KitchenSink.NativeException.FromError); + return; + } + ffiCall.SetResult(Ffi.TakeString(result_ptr, result_len)); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + /// Cancellable async operation + /// Cancels the native call; the task + /// then completes as canceled. + public static Task DoCancellable(string input, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + var inputBytes = Ffi.Utf8(input); + var ffiCall = new FfiCall(cancellationToken); + try + { + fixed (byte* inputPtr = inputBytes) + { + NativeMethods.kitchen_sink_kitchen_do_cancellable(inputPtr, (nuint)inputBytes.Length, ffiCall.CancelToken(), &CompleteDoCancellable, ffiCall.Context); + } + } + catch + { + ffiCall.Abandon(); + throw; + } + return ffiCall.Task; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void CompleteDoCancellable(IntPtr context, FfiError* err, byte* result_ptr, nuint result_len) + { + var ffiCall = FfiCall.Complete(context); + try + { + if (err != null) + { + ffiCall.SetError(err, global::KitchenSink.NativeException.FromError); + return; + } + ffiCall.SetResult(Ffi.TakeString(result_ptr, result_len)); + } + catch (Exception e) + { + ffiCall.SetException(e); + } + } + + /// Legacy operation kept for compatibility + [Obsolete("Use new_op instead")] + public static int LegacyOp() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_legacy_op(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult; + } + + /// Replacement for legacy_op + public static int NewOp() + { + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_new_op(&ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return ffiResult; + } +} + +public static unsafe class KitchenNested +{ + /// Trivial nested-module function + public static string Hello() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.kitchen_sink_kitchen_nested_hello(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } +} + +internal static unsafe partial class NativeMethods +{ + static partial void VerifyChecksums() + { + VerifyChecksum("shared", &kitchen_sink_shared_checksum, 0x42c4ce2c8d0af052UL); + VerifyChecksum("kitchen", &kitchen_sink_kitchen_checksum, 0xd83da75b24b545ffUL); + } + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_shared_checksum")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial ulong kitchen_sink_shared_checksum(); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_checksum")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial ulong kitchen_sink_kitchen_checksum(); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_shared_ping")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_shared_ping(nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_Gadget_clone")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr kitchen_sink_kitchen_Gadget_clone(Gadget.NativeHandle self); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_Gadget_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_Gadget_destroy(IntPtr self); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_Gadget_new")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr kitchen_sink_kitchen_Gadget_new(long id, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_Gadget_describe")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_Gadget_describe(Gadget.NativeHandle self, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_Gadget_poke")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_Gadget_poke(Gadget.NativeHandle self, int times, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_Gadget_version")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_Gadget_version(nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_bool_id")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte kitchen_sink_kitchen_bool_id(byte x, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_i32_id")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_i32_id(int x, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_u32_id")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial uint kitchen_sink_kitchen_u32_id(uint x, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_i64_id")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial long kitchen_sink_kitchen_i64_id(long x, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_f64_id")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial double kitchen_sink_kitchen_f64_id(double x, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_echo_string")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_echo_string(byte* s_ptr, nuint s_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_echo_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_echo_bytes(byte* b_ptr, nuint b_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_subscribe")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_subscribe(IntPtr listener_ctx, IntPtr listener_vtable, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_maybe_gadget")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr kitchen_sink_kitchen_maybe_gadget(Gadget.NativeHandle fallback, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_gadgets")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_gadgets(nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_stream_gadgets")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr kitchen_sink_kitchen_stream_gadgets(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_StreamGadgetsIterator_next")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_StreamGadgetsIterator_next(FfiIteratorHandle iter, IntPtr* out_item, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_StreamGadgetsIterator_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_StreamGadgetsIterator_destroy(IntPtr iter); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_maybe_item")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_maybe_item(long id, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_list_items")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_list_items(nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_get_attrs")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_get_attrs(nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_stream_items")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr kitchen_sink_kitchen_stream_items(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_StreamItemsIterator_next")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_StreamItemsIterator_next(FfiIteratorHandle iter, byte** out_item, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_StreamItemsIterator_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_StreamItemsIterator_destroy(IntPtr iter); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_cross_module_token")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_cross_module_token(nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_do_async")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_do_async(byte* input_ptr, nuint input_len, delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_do_cancellable")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void kitchen_sink_kitchen_do_cancellable(byte* input_ptr, nuint input_len, IntPtr cancel_token, delegate* unmanaged[Cdecl] callback, IntPtr context); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_legacy_op")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_legacy_op(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_new_op")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial int kitchen_sink_kitchen_new_op(FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_kitchen_nested_hello")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* kitchen_sink_kitchen_nested_hello(nuint* out_len, FfiError* out_err); +} + +// END KitchenSink.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_csproj.snap new file mode 100644 index 00000000..b5955816 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__KitchenSink_csproj.snap @@ -0,0 +1,26 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + + + + + net8.0 + KitchenSink + KitchenSink + KitchenSink + 0.1.0 + Native bindings for kitchen_sink + enable + true + true + + $(NoWarn);CS0618 + + + + + diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__README_md.snap index 96a3a5e8..cbcaac4f 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__README_md.snap @@ -1,13 +1,17 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI (.NET) +# KitchenSink (.NET) -Auto-generated P/Invoke bindings for the WeaveFFI native library. +Native bindings for kitchen_sink + +The `KitchenSink` namespace binds the `kitchen_sink` native library through +source-generated P/Invoke (`[LibraryImport]`), so it's trim- and +AOT-friendly. ## Build @@ -15,12 +19,14 @@ Auto-generated P/Invoke bindings for the WeaveFFI native library. dotnet build ``` -## Pack +Reference this project from yours (`dotnet add reference KitchenSink.csproj`) +or pack it (`dotnet pack -c Release`). -```bash -dotnet pack -``` +## Loading the native library -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). +The bindings load `kitchen_sink` with the platform's normal rules +(`libkitchen_sink.dylib`, `libkitchen_sink.so`, or `kitchen_sink.dll` next to the app or on the +library search path). Set `KITCHEN_SINK_LIBRARY` to a full path to load a +specific file instead. diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__Runtime_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__Runtime_cs.snap new file mode 100644 index 00000000..a837fb34 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__Runtime_cs.snap @@ -0,0 +1,698 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace KitchenSink; + +/// Raised for a failure the native library reports that isn't a typed +/// domain error: producer bugs, panics, marshalling failures, and exceptions +/// thrown by a callback-interface implementation. Every typed domain +/// exception derives from it. +public class NativeException : Exception +{ + /// An untyped producer error. + public const int GenericErrorCode = -1; + /// The producer panicked; the message carries the panic text. + public const int PanicErrorCode = -2; + /// An argument couldn't be lifted by the producer. + public const int MarshalErrorCode = -3; + /// A callback-interface implementation threw; the message carries + /// the original exception's message. + public const int ForeignErrorCode = -4; + /// The call was cancelled. Surfaces as + /// , never as this type. + public const int CancelledErrorCode = -5; + + /// The error code the native library reported. + public int Code { get; } + + /// Creates an exception carrying a native error code. + public NativeException(int code, string message) : base(message) + { + Code = code; + } + + /// Maps a runtime error code to its exception: cancellation to + /// , anything else to this + /// type. + internal static Exception FromError(int code, string message, byte[]? payload) + { + if (code == CancelledErrorCode) + { + return new OperationCanceledException(message); + } + return new NativeException(code, message); + } +} + +/// Maps a native error code, message, and serialized payload to the +/// exception a call throws. +internal delegate Exception FfiErrorMap(int code, string message, byte[]? payload); + +/// The native kitchen_sink_error slot. +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct FfiError +{ + public int Code; + public byte* Message; + public byte* PayloadPtr; + public nuint PayloadLen; +} + +/// Marshalling helpers shared by every generated call. +internal static unsafe class Ffi +{ + /// The UTF-8 bytes of a string argument. The native side borrows + /// them as a (pointer, length) pair, so interior NULs survive. + internal static byte[] Utf8(string value) + { + return Encoding.UTF8.GetBytes(value); + } + + /// Decodes a borrowed UTF-8 run; the native side keeps + /// ownership. + internal static string ReadString(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? "" : Encoding.UTF8.GetString(ptr, checked((int)len)); + } + + /// Copies a borrowed byte run; the native side keeps + /// ownership. + internal static byte[] ReadBytes(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? Array.Empty() : new ReadOnlySpan(ptr, checked((int)len)).ToArray(); + } + + /// Decodes a returned UTF-8 string and releases it. + internal static string TakeString(byte* ptr, nuint len) + { + try + { + return ReadString(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies returned bytes and releases them. + internal static byte[] TakeBytes(byte* ptr, nuint len) + { + try + { + return ReadBytes(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies a returned value buffer, releases it, and returns a + /// reader over the copy. + internal static FfiBufferReader TakeBuffer(byte* ptr, nuint len) + { + return new FfiBufferReader(TakeBytes(ptr, len)); + } + + /// Releases a caller-owned error slot and returns the exception + /// it described. + internal static Exception TakeError(FfiError* err, FfiErrorMap map) + { + var code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorClear(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + /// Releases a heap-boxed async error and returns the exception it + /// described. + internal static Exception TakeBoxedError(FfiError* err, FfiErrorMap map, out int code) + { + code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorFree(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + private static string ErrorMessage(FfiError* err) + { + return Marshal.PtrToStringUTF8((IntPtr)err->Message) ?? ""; + } + + /// Reports a callback-interface failure to the producer through + /// out_err. The producer copies the message. + internal static void SetForeignError(FfiError* err, Exception e) + { + var bytes = Encoding.UTF8.GetBytes(e.Message + "\0"); + fixed (byte* message = bytes) + { + NativeMethods.ErrorSet(err, NativeException.ForeignErrorCode, message); + } + } + + /// Pins a callback-interface implementation for the producer and + /// returns the ctx it hands back to every trampoline. The + /// producer releases it through the vtable's free. + internal static IntPtr Register(object implementation) + { + return GCHandle.ToIntPtr(GCHandle.Alloc(implementation)); + } + + /// Releases a ctx the producer never received (the call + /// failed before reaching it) or no longer holds. + internal static void Unregister(IntPtr ctx) + { + GCHandle.FromIntPtr(ctx).Free(); + } + + /// The implementation behind a trampoline's ctx. + internal static T Target(IntPtr ctx) where T : class + { + return (T)GCHandle.FromIntPtr(ctx).Target!; + } +} + +/// Imports of the native runtime surface plus the load-time contract +/// check. The generated API declares its own imports in the other half of +/// this partial class. +internal static unsafe partial class NativeMethods +{ + /// The native library's base name. + internal const string LibName = "kitchen_sink"; + + /// The environment variable naming an explicit library + /// path. + internal const string LibraryEnvVar = "KITCHEN_SINK_LIBRARY"; + + /// The C ABI revision these bindings were generated + /// against. + internal const uint AbiVersion = 3; + + // Runs before the first native call through this class, so a library + // built for another ABI revision or another contract fails here instead + // of misreading memory later. + static NativeMethods() + { + try + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, Resolve); + } + catch (InvalidOperationException) + { + // The assembly already has a resolver; default probing still applies. + } + uint found; + try + { + found = AbiVersionOf(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't export kitchen_sink_abi_version; these bindings expect ABI revision {AbiVersion}", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"ABI mismatch: these bindings expect revision {AbiVersion} but the loaded {LibName} library reports revision {found}"); + } + VerifyChecksums(); + } + + static partial void VerifyChecksums(); + + private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) + { + if (name != LibName) + { + return IntPtr.Zero; + } + var path = Environment.GetEnvironmentVariable(LibraryEnvVar); + return string.IsNullOrEmpty(path) ? IntPtr.Zero : NativeLibrary.Load(path); + } + + /// Fails the load when module was + /// generated from a different contract than the loaded library + /// implements. + private static void VerifyChecksum(string module, delegate* checksum, ulong expected) + { + ulong found; + try + { + found = checksum(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't implement module '{module}'", e); + } + if (found != expected) + { + throw new InvalidOperationException( + $"contract mismatch for module '{module}': these bindings expect checksum 0x{expected:x16} but the loaded {LibName} library reports 0x{found:x16}; regenerate the bindings for this library"); + } + } + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_abi_version")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial uint AbiVersionOf(); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_error_set")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorSet(FfiError* err, int code, byte* message); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_error_clear")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorClear(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_error_free")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorFree(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_free_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void FreeBytes(byte* ptr, nuint len); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_cancel_token_create")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr CancelTokenCreate(); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_cancel_token_cancel")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenCancel(IntPtr token); + + [LibraryImport(LibName, EntryPoint = "kitchen_sink_cancel_token_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenDestroy(IntPtr token); +} + +/// One pending async call: the task it completes, the native +/// context that finds it again, and, for a cancellable function, the +/// native cancel token linked to the caller's +/// . +internal sealed class FfiCall +{ + private readonly TaskCompletionSource _tcs = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationToken _cancellation; + private readonly object _gate = new object(); + private GCHandle _self; + private IntPtr _token; + private CancellationTokenRegistration _registration; + private bool _completed; + private bool _cancelling; + + internal FfiCall(CancellationToken cancellation = default) + { + _cancellation = cancellation; + _self = GCHandle.Alloc(this); + } + + /// The task the completion resolves. + internal Task Task => _tcs.Task; + + /// The context argument for the launcher. + internal IntPtr Context => GCHandle.ToIntPtr(_self); + + /// Creates the native cancel token for the launcher and cancels + /// it when the caller's fires. + internal IntPtr CancelToken() + { + _token = NativeMethods.CancelTokenCreate(); + if (_cancellation.CanBeCanceled) + { + _registration = _cancellation.Register(static state => ((FfiCall)state!).Cancel(), this); + } + return _token; + } + + /// Recovers the call behind a completion's context. Runs + /// exactly once per launch, on a producer thread. + internal static FfiCall Complete(IntPtr context) + { + var handle = GCHandle.FromIntPtr(context); + var call = (FfiCall)handle.Target!; + handle.Free(); + call.Release(); + return call; + } + + /// Releases everything when the launcher threw before the + /// producer took the call, so no completion will arrive. + internal void Abandon() + { + _self.Free(); + Release(); + } + + internal void SetResult(T value) + { + _tcs.TrySetResult(value); + } + + internal void SetException(Exception e) + { + _tcs.TrySetException(e); + } + + /// Faults (or cancels, for the cancelled code) the task from a + /// heap-boxed error, releasing the box. + internal unsafe void SetError(FfiError* err, FfiErrorMap map) + { + var e = Ffi.TakeBoxedError(err, map, out var code); + if (code == NativeException.CancelledErrorCode) + { + _tcs.TrySetCanceled(_cancellation); + } + else + { + _tcs.TrySetException(e); + } + } + + // The registration callback and the completion race: whichever runs + // last destroys the consumer's token reference, so a cancel never + // touches a destroyed token. + private void Cancel() + { + lock (_gate) + { + if (_completed) + { + return; + } + _cancelling = true; + } + NativeMethods.CancelTokenCancel(_token); + bool destroy; + lock (_gate) + { + _cancelling = false; + destroy = _completed; + } + if (destroy) + { + NativeMethods.CancelTokenDestroy(_token); + } + } + + private void Release() + { + if (_token == IntPtr.Zero) + { + return; + } + bool destroy; + lock (_gate) + { + _completed = true; + destroy = !_cancelling; + } + if (destroy) + { + _registration.Dispose(); + NativeMethods.CancelTokenDestroy(_token); + } + } +} + +/// Owns one native iterator and destroys it exactly once, when +/// enumeration ends, the enumerator is disposed, or the garbage collector +/// finalizes an abandoned one. +internal sealed unsafe class FfiIteratorHandle : SafeHandle +{ + private readonly delegate* _destroy; + + internal FfiIteratorHandle(IntPtr iterator, delegate* destroy) + : base(IntPtr.Zero, true) + { + _destroy = destroy; + SetHandle(iterator); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + _destroy(handle); + return true; + } +} + +/// A lazily streamed sequence backed by a native iterator. It can be +/// enumerated once; each MoveNext pulls one item. +internal sealed unsafe class FfiSequence : IEnumerable, IEnumerator +{ + private readonly FfiIteratorHandle _iterator; + private readonly delegate* _next; + private int _started; + private T _current = default!; + + internal FfiSequence(FfiIteratorHandle iterator, delegate* next) + { + _iterator = iterator; + _next = next; + } + + public T Current => _current; + + object? IEnumerator.Current => _current; + + public IEnumerator GetEnumerator() + { + if (Interlocked.Exchange(ref _started, 1) != 0) + { + throw new InvalidOperationException("this sequence can be enumerated only once"); + } + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool MoveNext() + { + if (_iterator.IsClosed) + { + return false; + } + if (_next(_iterator, out _current)) + { + return true; + } + _iterator.Dispose(); + return false; + } + + public void Reset() + { + throw new NotSupportedException("a native iterator can't be reset"); + } + + public void Dispose() + { + _iterator.Dispose(); + } +} + +/// Serializes values into the value-buffer wire format +/// (little-endian, packed). +internal sealed class FfiBufferWriter +{ + private byte[] _buf = new byte[64]; + private int _len; + + /// The bytes written so far. + internal ReadOnlySpan Written => _buf.AsSpan(0, _len); + + /// The number of bytes written so far. + internal int Length => _len; + + private Span Grow(int extra) + { + if (_len + extra > _buf.Length) + { + Array.Resize(ref _buf, Math.Max(_buf.Length * 2, _len + extra)); + } + var span = _buf.AsSpan(_len, extra); + _len += extra; + return span; + } + + internal void WriteBool(bool v) => WriteU8(v ? (byte)1 : (byte)0); + + internal void WriteI8(sbyte v) => WriteU8((byte)v); + + internal void WriteU8(byte v) => Grow(1)[0] = v; + + internal void WriteI16(short v) => System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); + + internal void WriteU16(ushort v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); + + internal void WriteI32(int v) => System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); + + internal void WriteU32(uint v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); + + internal void WriteI64(long v) => System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); + + internal void WriteU64(ulong v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); + + internal void WriteF32(float v) => System.Buffers.Binary.BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); + + internal void WriteF64(double v) => System.Buffers.Binary.BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); + + internal void WriteLen(int len) => WriteU32((uint)len); + + internal void WriteString(string v) + { + var count = Encoding.UTF8.GetByteCount(v); + WriteLen(count); + Encoding.UTF8.GetBytes(v, Grow(count)); + } + + internal void WriteBytes(byte[] v) + { + WriteLen(v.Length); + v.CopyTo(Grow(v.Length)); + } + + /// Writes an object token. The token must carry its own strong + /// reference (a fresh clone), which the reader adopts. + internal void WriteObject(IntPtr token) => WriteU64((ulong)(nuint)token); +} + +/// Decodes values from the value-buffer wire format. A malformed +/// buffer is a producer or consumer bug and throws +/// . +internal sealed class FfiBufferReader +{ + private static readonly Encoding Utf8Strict = new UTF8Encoding(false, true); + + private readonly byte[] _data; + private int _pos; + + internal FfiBufferReader(byte[] data) + { + _data = data; + } + + private static InvalidOperationException Malformed(string what) + { + return new InvalidOperationException("malformed value buffer: " + what); + } + + private ReadOnlySpan Take(int n) + { + if (_data.Length - _pos < n) + { + throw Malformed("buffer exhausted"); + } + var span = _data.AsSpan(_pos, n); + _pos += n; + return span; + } + + internal bool ReadBool() + { + var b = ReadU8(); + if (b > 1) + { + throw Malformed("invalid bool byte"); + } + return b == 1; + } + + internal sbyte ReadI8() => (sbyte)ReadU8(); + + internal byte ReadU8() => Take(1)[0]; + + internal short ReadI16() => System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(Take(2)); + + internal ushort ReadU16() => System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); + + internal int ReadI32() => System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(Take(4)); + + internal uint ReadU32() => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); + + internal long ReadI64() => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(Take(8)); + + internal ulong ReadU64() => System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); + + internal float ReadF32() => System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(Take(4)); + + internal double ReadF64() => System.Buffers.Binary.BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); + + /// A collection count. Elements can be zero-sized, so the count + /// isn't checked against the remaining bytes; callers cap their + /// preallocation with . + internal int ReadLen() + { + var len = ReadU32(); + if (len > int.MaxValue) + { + throw Malformed("length prefix out of range"); + } + return (int)len; + } + + /// A preallocation size for elements + /// that a malformed count can't inflate. + internal int Capacity(int count) => Math.Min(count, _data.Length - _pos); + + internal string ReadString() + { + var bytes = Take(ReadLen()); + try + { + return Utf8Strict.GetString(bytes); + } + catch (ArgumentException) + { + throw Malformed("string is not valid UTF-8"); + } + } + + internal byte[] ReadBytes() => Take(ReadLen()).ToArray(); + + /// Reads an object token carrying one strong reference. + internal IntPtr ReadObject() + { + var token = ReadU64(); + if (token == 0) + { + throw Malformed("null object token"); + } + return (IntPtr)(nint)token; + } + + internal void ExpectEnd() + { + if (_pos != _data.Length) + { + throw Malformed("trailing bytes"); + } + } +} + +// END Runtime.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap deleted file mode 100644 index 0df40fae..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap +++ /dev/null @@ -1,1418 +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.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace WeaveFFI -{ - /// Raised for any WeaveFFI failure that isn't a typed domain - /// error: producer bugs, panics, marshalling failures, and exceptions - /// thrown by a callback-interface implementation. - public class WeaveFFIException : Exception - { - /// An untyped producer error. - public const int GenericErrorCode = -1; - /// The producer panicked; the message carries the panic text. - public const int PanicErrorCode = -2; - /// An argument couldn't be lifted by the producer. - public const int MarshalErrorCode = -3; - /// A callback-interface implementation threw; the message - /// carries the original exception's message. - public const int ForeignErrorCode = -4; - - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - /// Typed exception for the KitchenErrorsError error domain (module kitchen). - public class KitchenErrorsException : WeaveFFIException - { - /// Item not found - public const int NotFound = 1; - /// Invalid input - public const int InvalidInput = 2; - - public KitchenErrorsException(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 NotFound: - return new KitchenErrorsException(code, string.IsNullOrEmpty(message) ? "Item not found" : message); - case InvalidInput: - return new KitchenErrorsException(code, string.IsNullOrEmpty(message) ? "Invalid input" : 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 CheckKitchenErrors(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 KitchenErrorsException.FromCode(code, msg, payload); - } - } - } - - /// One strong reference to a native object, on its way into - /// a wrapper that adopts it. A distinct type (rather than a bare - /// IntPtr) so the adopting constructor never competes with a - /// public constructor taking an integer. - internal readonly struct WeaveFFIHandle - { - internal readonly IntPtr Value; - - internal WeaveFFIHandle(IntPtr value) - { - Value = value; - } - } - - 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 void WriteObject(IntPtr token) - { - WriteU64((ulong)(nuint)(nint)token); - } - - 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 IntPtr ReadObject() - { - var token = ReadU64(); - if (token == 0) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: null object token"); - } - return (nint)(nuint)token; - } - - 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(); - } - } - - /// A reusable token referenced across modules - public sealed class Token - { - public long Id { get; } - - public string Label { get; } - - public Token(long id, string label) - { - Id = id; - Label = label; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteString(Label); - } - - internal static Token ReadFrom(WeaveFFIBufferReader reader) - { - var fId = reader.ReadI64(); - var fLabel = reader.ReadString(); - return new Token(fId, fLabel); - } - } - - /// Task priority levels - public enum Priority - { - /// Low priority - Low = 0, - /// Normal priority - Normal = 1, - /// High priority - High = 2, - } - - /// Kitchen sink struct exercising every field feature - public sealed class Item - { - /// Stable identifier - public long Id { get; } - - /// Display name - public string Name { get; } - - /// Initial count - public int Count { get; } - - /// Whether the item is enabled - public bool Enabled { get; } - - /// Scaling ratio - public double Ratio { get; } - - public uint Nick { get; } - - public byte[] Payload { get; } - - public string[] Tags { get; } - - public Dictionary Attrs { get; } - - public long? Parent { get; } - - /// Cross-module struct reference - public Token Token { get; } - - public Priority Priority { get; } - - /// An optional object inside a record - public Gadget? Gadget { get; } - - public Item(long id, string name, int count, bool enabled, double ratio, uint nick, byte[] payload, string[] tags, Dictionary attrs, long? parent, Token token, Priority priority, Gadget? gadget) - { - Id = id; - Name = name; - Count = count; - Enabled = enabled; - Ratio = ratio; - Nick = nick; - Payload = payload; - Tags = tags; - Attrs = attrs; - Parent = parent; - Token = token; - Priority = priority; - Gadget = gadget; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteString(Name); - writer.WriteI32(Count); - writer.WriteBool(Enabled); - writer.WriteF64(Ratio); - writer.WriteU32(Nick); - writer.WriteBytes(Payload); - writer.WriteLen(Tags.Length); - foreach (var item0 in Tags) - { - writer.WriteString(item0); - } - writer.WriteLen(Attrs.Count); - foreach (var entry0 in Attrs) - { - writer.WriteString(entry0.Key); - writer.WriteString(entry0.Value); - } - if (Parent != null) - { - writer.WriteOptionFlag(true); - writer.WriteI64(Parent.Value); - } - else - { - writer.WriteOptionFlag(false); - } - Token.WriteTo(writer); - writer.WriteI32((int)Priority); - if (Gadget != null) - { - writer.WriteOptionFlag(true); - writer.WriteObject(Gadget!.CloneHandle()); - } - else - { - writer.WriteOptionFlag(false); - } - } - - internal static Item ReadFrom(WeaveFFIBufferReader reader) - { - var fId = reader.ReadI64(); - var fName = reader.ReadString(); - var fCount = reader.ReadI32(); - var fEnabled = reader.ReadBool(); - var fRatio = reader.ReadF64(); - var fNick = reader.ReadU32(); - var fPayload = reader.ReadBytes(); - var fTagsCount = reader.ReadLen(); - var fTags = new string[fTagsCount]; - for (int i0 = 0; i0 < fTagsCount; i0++) - { - var fTagsItem = reader.ReadString(); - fTags[i0] = fTagsItem; - } - var fAttrsCount = reader.ReadLen(); - var fAttrs = new Dictionary(fAttrsCount); - for (int i0 = 0; i0 < fAttrsCount; i0++) - { - var fAttrsKey = reader.ReadString(); - var fAttrsVal = reader.ReadString(); - fAttrs[fAttrsKey] = fAttrsVal; - } - long? fParent = null; - if (reader.ReadOptionFlag()) - { - var fParentValue = reader.ReadI64(); - fParent = fParentValue; - } - var fToken = Token.ReadFrom(reader); - var fPriority = (Priority)reader.ReadI32(); - Gadget? fGadget = null; - if (reader.ReadOptionFlag()) - { - var fGadgetValue = new Gadget(new WeaveFFIHandle(reader.ReadObject())); - fGadget = fGadgetValue; - } - return new Item(fId, fName, fCount, fEnabled, fRatio, fNick, fPayload, fTags, fAttrs, fParent, fToken, fPriority, fGadget); - } - } - - /// Consumer-implemented observer of ready items - /// Implement this interface and pass an instance wherever the - /// native library expects it. The library may call any method from any - /// thread until it releases its last reference to the instance. Object - /// arguments are owned by the implementation, which should dispose them - /// when done. An exception thrown by a method is reported to the native - /// caller as . - public interface IReadyListener - { - /// Fires when an item is ready - void OnReady(int code, string msg); - /// Receives the item itself and says whether to keep listening - bool OnItem(Item item, Gadget gadget); - } - - /// The process-wide weaveffi_kitchen_ReadyListener_vtable vtable and the trampolines behind - /// it, adapting an to the C ABI. - internal static unsafe class WeaveFFIVtable_kitchen_ReadyListener - { - [StructLayout(LayoutKind.Sequential)] - private struct Layout - { - public IntPtr on_ready; - public IntPtr on_item; - public IntPtr free; - } - - /// Address of the one static vtable for this interface. It is - /// allocated once and never freed, so the producer may keep the pointer - /// for the process lifetime. - internal static readonly IntPtr Pointer = Allocate(); - - private static IntPtr Allocate() - { - var layout = new Layout - { - on_ready = (IntPtr)(delegate* unmanaged[Cdecl])&OnReadyTrampoline, - on_item = (IntPtr)(delegate* unmanaged[Cdecl])&OnItemTrampoline, - free = (IntPtr)(delegate* unmanaged[Cdecl])&FreeTrampoline, - }; - var mem = Marshal.AllocHGlobal(Marshal.SizeOf()); - Marshal.StructureToPtr(layout, mem, false); - return mem; - } - - private static IReadyListener Target(IntPtr ctx) - { - return (IReadyListener)GCHandle.FromIntPtr(ctx).Target!; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void OnReadyTrampoline(IntPtr ctx, int code, IntPtr msg, IntPtr out_err) - { - try - { - var impl = Target(ctx); - impl.OnReady(code, Marshal.PtrToStringUTF8(msg) ?? ""); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static byte OnItemTrampoline(IntPtr ctx, IntPtr item_ptr, UIntPtr item_len, IntPtr gadget, IntPtr out_err) - { - try - { - var impl = Target(ctx); - var arg0Buf = new byte[(int)item_len]; - if (item_ptr != IntPtr.Zero && (int)item_len > 0) Marshal.Copy(item_ptr, arg0Buf, 0, (int)item_len); - var arg0Reader = new WeaveFFIBufferReader(arg0Buf); - var arg0 = Item.ReadFrom(arg0Reader); - arg0Reader.ExpectEnd(); - return (byte)(impl.OnItem(arg0, new Gadget(new WeaveFFIHandle(gadget))) ? 1 : 0); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - return default; - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void FreeTrampoline(IntPtr ctx) - { - GCHandle.FromIntPtr(ctx).Free(); - } - } - - /// A pocket-sized interface exercising the object surface - public class Gadget : IDisposable - { - private IntPtr _handle; - private int _released; - - /// Adopts one strong reference to a native object. - internal Gadget(WeaveFFIHandle handle) - { - _handle = handle.Value; - } - - /// The borrowed native pointer for the duration of a call. - /// The wrapper was disposed. - internal IntPtr Handle - { - get - { - var h = _handle; - if (h == IntPtr.Zero) - { - throw new ObjectDisposedException(nameof(Gadget)); - } - return h; - } - } - - /// Mints a second strong reference the caller owns (for - /// example to write this object into a value buffer). - internal IntPtr CloneHandle() - { - return NativeMethods.weaveffi_kitchen_Gadget_clone(Handle); - } - - /// Create a gadget with the given id - public Gadget(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_Gadget_new(id, ref err); - WeaveFFIError.Check(err); - _handle = result; - } - - /// Render the gadget as a human-readable string - public string Describe() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_Gadget_describe(Handle, ref err); - WeaveFFIError.Check(err); - var str = Marshal.PtrToStringUTF8(result); - NativeMethods.weaveffi_free_string(result); - return str ?? ""; - } - - /// Poke the gadget a number of times, failing on a negative count - /// Thrown when the call reports a KitchenErrorsError code. - public int Poke(int times) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_Gadget_poke(Handle, times, ref err); - WeaveFFIError.CheckKitchenErrors(err); - return result; - } - - /// The gadget subsystem version string - public static string Version() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_Gadget_version(ref err); - WeaveFFIError.Check(err); - var str = Marshal.PtrToStringUTF8(result); - NativeMethods.weaveffi_free_string(result); - return str ?? ""; - } - - /// Releases this wrapper's reference. The native object is - /// dropped when the producer releases its last reference; other - /// wrappers to the same object stay valid. - public void Dispose() - { - Release(); - GC.SuppressFinalize(this); - } - - ~Gadget() - { - Release(); - } - - private void Release() - { - if (System.Threading.Interlocked.Exchange(ref _released, 1) != 0) - { - return; - } - var h = _handle; - _handle = IntPtr.Zero; - if (h != IntPtr.Zero) - { - NativeMethods.weaveffi_kitchen_Gadget_destroy(h); - } - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - // The ABI revision these bindings were generated against. - internal const uint AbiVersion = 2; - - // 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(); - - // Fills a producer-owned error slot with a copy of `message`; callback - // trampolines report a thrown exception through it so no managed - // allocation is ever handed to the producer's allocator. - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_set(IntPtr err, int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string message); - - [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_shared_ping", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_shared_ping(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_Gadget_clone", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_Gadget_clone(IntPtr self); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_Gadget_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kitchen_Gadget_destroy(IntPtr self); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_Gadget_new", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_Gadget_new(long id, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_Gadget_describe", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_Gadget_describe(IntPtr self, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_Gadget_poke", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_kitchen_Gadget_poke(IntPtr self, int times, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_Gadget_version", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_Gadget_version(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_bool_id", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_kitchen_bool_id(byte x, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_i32_id", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_kitchen_i32_id(int x, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_u32_id", CallingConvention = CallingConvention.Cdecl)] - internal static extern uint weaveffi_kitchen_u32_id(uint x, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_i64_id", CallingConvention = CallingConvention.Cdecl)] - internal static extern long weaveffi_kitchen_i64_id(long x, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_f64_id", CallingConvention = CallingConvention.Cdecl)] - internal static extern double weaveffi_kitchen_f64_id(double x, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_echo_string", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_echo_string(IntPtr s, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_echo_bytes", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_echo_bytes(IntPtr b_ptr, UIntPtr b_len, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_subscribe", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_kitchen_subscribe(IntPtr listener_ctx, IntPtr listener_vtable, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_maybe_gadget", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_maybe_gadget(IntPtr fallback, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_gadgets", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_gadgets(out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_stream_gadgets", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_stream_gadgets(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_StreamGadgetsIterator_next", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_kitchen_StreamGadgetsIterator_next(IntPtr iter, out IntPtr out_item, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_StreamGadgetsIterator_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kitchen_StreamGadgetsIterator_destroy(IntPtr iter); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_maybe_item", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_maybe_item(long id, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_list_items", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_list_items(out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_get_attrs", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_get_attrs(out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_stream_items", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_stream_items(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_StreamItemsIterator_next", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_kitchen_StreamItemsIterator_next(IntPtr iter, out IntPtr out_item, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_StreamItemsIterator_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kitchen_StreamItemsIterator_destroy(IntPtr iter); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_cross_module_token", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_cross_module_token(out UIntPtr out_len, ref WeaveFFIError out_err); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void AsyncCb_weaveffi_kitchen_do_async(IntPtr context, IntPtr err, IntPtr result); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_do_async_async", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kitchen_do_async_async(IntPtr input, AsyncCb_weaveffi_kitchen_do_async callback, IntPtr context); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void AsyncCb_weaveffi_kitchen_do_cancellable(IntPtr context, IntPtr err, IntPtr result); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_do_cancellable_async", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kitchen_do_cancellable_async(IntPtr input, IntPtr cancel_token, AsyncCb_weaveffi_kitchen_do_cancellable callback, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_legacy_op", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_kitchen_legacy_op(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_new_op", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_kitchen_new_op(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kitchen_nested_hello", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kitchen_nested_hello(ref WeaveFFIError out_err); - - } - - public static class Shared - { - /// Trivial cross-module helper - public static string Ping() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_shared_ping(ref err); - WeaveFFIError.Check(err); - var str = Marshal.PtrToStringUTF8(result); - NativeMethods.weaveffi_free_string(result); - return str ?? ""; - } - - } - - public static class Kitchen - { - /// Identity for bool - public static bool BoolId(bool x) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_bool_id((byte)(x ? 1 : 0), ref err); - WeaveFFIError.Check(err); - return result != 0; - } - - public static int I32Id(int x) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_i32_id(x, ref err); - WeaveFFIError.Check(err); - return result; - } - - public static uint U32Id(uint x) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_u32_id(x, ref err); - WeaveFFIError.Check(err); - return result; - } - - public static long I64Id(long x) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_i64_id(x, ref err); - WeaveFFIError.Check(err); - return result; - } - - public static double F64Id(double x) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_f64_id(x, ref err); - WeaveFFIError.Check(err); - return result; - } - - public static string EchoString(string s) - { - var err = new WeaveFFIError(); - var sPtr = Marshal.StringToCoTaskMemUTF8(s); - try - { - var result = NativeMethods.weaveffi_kitchen_echo_string(sPtr, ref err); - WeaveFFIError.Check(err); - var str = Marshal.PtrToStringUTF8(result); - NativeMethods.weaveffi_free_string(result); - return str ?? ""; - } - finally - { - Marshal.FreeCoTaskMem(sPtr); - } - } - - public static byte[] EchoBytes(byte[] b) - { - var err = new WeaveFFIError(); - var bPin = GCHandle.Alloc(b, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_kitchen_echo_bytes(bPin.AddrOfPinnedObject(), (UIntPtr)b.Length, out var outLen, ref err); - WeaveFFIError.Check(err); - if (result == IntPtr.Zero) return Array.Empty(); - var arr = new byte[(int)outLen]; - Marshal.Copy(result, arr, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - return arr; - } - finally - { - bPin.Free(); - } - } - - /// Register a listener, returning how many are attached - public static int Subscribe(IReadyListener listener) - { - var err = new WeaveFFIError(); - var listenerCtx = GCHandle.ToIntPtr(GCHandle.Alloc(listener)); - var result = NativeMethods.weaveffi_kitchen_subscribe(listenerCtx, WeaveFFIVtable_kitchen_ReadyListener.Pointer, ref err); - WeaveFFIError.Check(err); - return result; - } - - /// Optional object in both directions - public static Gadget? MaybeGadget(Gadget? fallback) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_maybe_gadget(fallback?.Handle ?? IntPtr.Zero, ref err); - WeaveFFIError.Check(err); - return result == IntPtr.Zero ? null : new Gadget(new WeaveFFIHandle(result)); - } - - /// A list of objects - public static Gadget[] Gadgets() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_gadgets(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 decodedCount = decodedReader.ReadLen(); - var decoded = new Gadget[decodedCount]; - for (int i0 = 0; i0 < decodedCount; i0++) - { - var decodedItem = new Gadget(new WeaveFFIHandle(decodedReader.ReadObject())); - decoded[i0] = decodedItem; - } - decodedReader.ExpectEnd(); - return decoded; - } - - /// An iterator over objects - /// 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 StreamGadgets() - { - var err = new WeaveFFIError(); - var iter = NativeMethods.weaveffi_kitchen_stream_gadgets(ref err); - WeaveFFIError.Check(err); - return new WeaveFFIOnceEnumerable(EnumerateStreamGadgets(iter)); - } - - private static IEnumerator EnumerateStreamGadgets(IntPtr iter) - { - try - { - while (true) - { - var iterErr = new WeaveFFIError(); - if (NativeMethods.weaveffi_kitchen_StreamGadgetsIterator_next(iter, out var out_item, ref iterErr) == 0) - { - WeaveFFIError.Check(iterErr); - yield break; - } - WeaveFFIError.Check(iterErr); - yield return new Gadget(new WeaveFFIHandle(out_item)); - } - } - finally - { - NativeMethods.weaveffi_kitchen_StreamGadgetsIterator_destroy(iter); - } - } - - /// Optional struct return - /// Thrown when the call reports a KitchenErrorsError code. - public static Item? MaybeItem(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_maybe_item(id, out var outLen, ref err); - WeaveFFIError.CheckKitchenErrors(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); - Item? decoded = null; - if (decodedReader.ReadOptionFlag()) - { - var decodedValue = Item.ReadFrom(decodedReader); - decoded = decodedValue; - } - decodedReader.ExpectEnd(); - return decoded; - } - - /// List of structs - public static Item[] ListItems() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_list_items(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 decodedCount = decodedReader.ReadLen(); - var decoded = new Item[decodedCount]; - for (int i0 = 0; i0 < decodedCount; i0++) - { - var decodedItem = Item.ReadFrom(decodedReader); - decoded[i0] = decodedItem; - } - decodedReader.ExpectEnd(); - return decoded; - } - - /// Map return type - public static Dictionary GetAttrs() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_get_attrs(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 decodedCount = decodedReader.ReadLen(); - var decoded = new Dictionary(decodedCount); - for (int i0 = 0; i0 < decodedCount; i0++) - { - var decodedKey = decodedReader.ReadString(); - var decodedVal = decodedReader.ReadI32(); - decoded[decodedKey] = decodedVal; - } - decodedReader.ExpectEnd(); - return decoded; - } - - /// Iterator return type - /// 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 StreamItems() - { - var err = new WeaveFFIError(); - var iter = NativeMethods.weaveffi_kitchen_stream_items(ref err); - WeaveFFIError.Check(err); - return new WeaveFFIOnceEnumerable(EnumerateStreamItems(iter)); - } - - private static IEnumerator EnumerateStreamItems(IntPtr iter) - { - try - { - while (true) - { - var iterErr = new WeaveFFIError(); - if (NativeMethods.weaveffi_kitchen_StreamItemsIterator_next(iter, out var out_item, out var out_len, ref iterErr) == 0) - { - WeaveFFIError.Check(iterErr); - yield break; - } - WeaveFFIError.Check(iterErr); - var itemBuf = new byte[(int)out_len]; - if (out_item != IntPtr.Zero && (int)out_len > 0) Marshal.Copy(out_item, itemBuf, 0, (int)out_len); - NativeMethods.weaveffi_free_bytes(out_item, out_len); - var itemReader = new WeaveFFIBufferReader(itemBuf); - var item = Item.ReadFrom(itemReader); - itemReader.ExpectEnd(); - yield return item; - } - } - finally - { - NativeMethods.weaveffi_kitchen_StreamItemsIterator_destroy(iter); - } - } - - /// Returns the shared Token type from another module - public static Token CrossModuleToken() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_cross_module_token(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 = Token.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - - /// Async operation - public static async Task DoAsync(string input) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - NativeMethods.AsyncCb_weaveffi_kitchen_do_async 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 inputPtr = Marshal.StringToCoTaskMemUTF8(input); - try - { - try - { - NativeMethods.weaveffi_kitchen_do_async_async(inputPtr, callback, ctx); - } - catch - { - if (gcHandle.IsAllocated) gcHandle.Free(); - throw; - } - } - finally - { - Marshal.FreeCoTaskMem(inputPtr); - } - return await tcs.Task; - } - - /// Cancellable async operation - public static async Task DoCancellable(string input) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - NativeMethods.AsyncCb_weaveffi_kitchen_do_cancellable 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 inputPtr = Marshal.StringToCoTaskMemUTF8(input); - try - { - try - { - NativeMethods.weaveffi_kitchen_do_cancellable_async(inputPtr, IntPtr.Zero, callback, ctx); - } - catch - { - if (gcHandle.IsAllocated) gcHandle.Free(); - throw; - } - } - finally - { - Marshal.FreeCoTaskMem(inputPtr); - } - return await tcs.Task; - } - - /// Legacy operation kept for compatibility - [Obsolete("Use new_op instead")] - public static int LegacyOp() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_legacy_op(ref err); - WeaveFFIError.Check(err); - return result; - } - - /// Replacement for legacy_op - public static int NewOp() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_new_op(ref err); - WeaveFFIError.Check(err); - return result; - } - - } - - public static class KitchenNested - { - /// Trivial nested-module function - public static string Hello() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kitchen_nested_hello(ref err); - WeaveFFIError.Check(err); - var str = Marshal.PtrToStringUTF8(result); - NativeMethods.weaveffi_free_string(result); - return str ?? ""; - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__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_kitchen_sink__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__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__NestedModules_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__NestedModules_cs.snap new file mode 100644 index 00000000..883176ad --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__NestedModules_cs.snap @@ -0,0 +1,231 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace NestedModules; + +/// Typed exception for the GraphicsError error domain (module graphics). +public class GraphicsException : NativeException +{ + /// coordinate out of bounds + public const int OutOfBounds = 1; + + /// Creates an exception carrying a domain error code. + public GraphicsException(int code, string message) : base(code, message) + { + } + + internal static new Exception FromError(int code, string message, byte[]? payload) + { + switch (code) + { + case OutOfBounds: + return new GraphicsException(code, string.IsNullOrEmpty(message) ? "coordinate out of bounds" : message); + default: + return global::NestedModules.NativeException.FromError(code, message, payload); + } + } +} + +/// A measurement unit shared with nested submodules. +public enum Unit +{ + pixels = 0, + points = 1, +} + +/// Top-level struct that references a struct defined one level deeper. +public sealed class Canvas +{ + public int Width { get; } + + /// Reference down into a nested module + public Shape Root { get; } + + /// Creates a from every field. + public Canvas(int width, Shape root) + { + Width = width; + Root = root; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteI32(Width); + Root.WriteTo(writer); + } + + internal static Canvas ReadFrom(FfiBufferReader reader) + { + var fWidth = reader.ReadI32(); + var fRoot = global::NestedModules.Shape.ReadFrom(reader); + return new Canvas(fWidth, fRoot); + } +} + +/// A struct one level deep that references both an ancestor enum and a deeper struct. +public sealed class Shape +{ + public int Sides { get; } + + /// Reference up to the parent module's enum + public Unit Unit { get; } + + /// Reference down into a deeper module + public Detail Detail { get; } + + /// Creates a from every field. + public Shape(int sides, Unit unit, Detail detail) + { + Sides = sides; + Unit = unit; + Detail = detail; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteI32(Sides); + writer.WriteI32((int)Unit); + Detail.WriteTo(writer); + } + + internal static Shape ReadFrom(FfiBufferReader reader) + { + var fSides = reader.ReadI32(); + var fUnit = (Unit)reader.ReadI32(); + var fDetail = global::NestedModules.Detail.ReadFrom(reader); + return new Shape(fSides, fUnit, fDetail); + } +} + +/// A struct two levels deep. +public sealed class Detail +{ + public double Area { get; } + + /// Creates a from every field. + public Detail(double area) + { + Area = area; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteF64(Area); + } + + internal static Detail ReadFrom(FfiBufferReader reader) + { + var fArea = reader.ReadF64(); + return new Detail(fArea); + } +} + +public static unsafe class Graphics +{ + /// Construct a Canvas. + public static Canvas NewCanvas(int width) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.nested_modules_graphics_new_canvas(width, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::NestedModules.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::NestedModules.Canvas.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } +} + +public static unsafe class GraphicsShapes +{ + /// Construct a Shape. + public static Shape MakeShape(int sides) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.nested_modules_graphics_shapes_make_shape(sides, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::NestedModules.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::NestedModules.Shape.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + + /// Nested function returning a struct owned by an ancestor module. + public static Canvas Describe() + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.nested_modules_graphics_shapes_describe(&ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::NestedModules.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::NestedModules.Canvas.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } +} + +public static unsafe class GraphicsShapesGeometry +{ + /// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. + /// The call reported a GraphicsError code. + public static Detail Compute(Shape shape) + { + var shapeWriter = new FfiBufferWriter(); + shape.WriteTo(shapeWriter); + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* shapePtr = shapeWriter.Written) + { + var ffiResult = NativeMethods.nested_modules_graphics_shapes_geometry_compute(shapePtr, (nuint)shapeWriter.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::NestedModules.GraphicsException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::NestedModules.Detail.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + } +} + +internal static unsafe partial class NativeMethods +{ + static partial void VerifyChecksums() + { + VerifyChecksum("graphics", &nested_modules_graphics_checksum, 0x44d31b5b10fdb90dUL); + } + + [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_checksum")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial ulong nested_modules_graphics_checksum(); + + [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_new_canvas")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* nested_modules_graphics_new_canvas(int width, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_shapes_make_shape")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* nested_modules_graphics_shapes_make_shape(int sides, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_shapes_describe")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* nested_modules_graphics_shapes_describe(nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "nested_modules_graphics_shapes_geometry_compute")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* nested_modules_graphics_shapes_geometry_compute(byte* shape_ptr, nuint shape_len, nuint* out_len, FfiError* out_err); +} + +// END NestedModules.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__NestedModules_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__NestedModules_csproj.snap new file mode 100644 index 00000000..b98ed2a9 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__NestedModules_csproj.snap @@ -0,0 +1,26 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + + + + + net8.0 + NestedModules + NestedModules + NestedModules + 0.1.0 + Native bindings for nested_modules + enable + true + true + + $(NoWarn);CS0618 + + + + + diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__README_md.snap index 96a3a5e8..1cbc635b 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__README_md.snap @@ -1,13 +1,17 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI (.NET) +# NestedModules (.NET) -Auto-generated P/Invoke bindings for the WeaveFFI native library. +Native bindings for nested_modules + +The `NestedModules` namespace binds the `nested_modules` native library through +source-generated P/Invoke (`[LibraryImport]`), so it's trim- and +AOT-friendly. ## Build @@ -15,12 +19,14 @@ Auto-generated P/Invoke bindings for the WeaveFFI native library. dotnet build ``` -## Pack +Reference this project from yours (`dotnet add reference NestedModules.csproj`) +or pack it (`dotnet pack -c Release`). -```bash -dotnet pack -``` +## Loading the native library -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). +The bindings load `nested_modules` with the platform's normal rules +(`libnested_modules.dylib`, `libnested_modules.so`, or `nested_modules.dll` next to the app or on the +library search path). Set `NESTED_MODULES_LIBRARY` to a full path to load a +specific file instead. diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__Runtime_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__Runtime_cs.snap new file mode 100644 index 00000000..66aae836 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__Runtime_cs.snap @@ -0,0 +1,698 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NestedModules; + +/// Raised for a failure the native library reports that isn't a typed +/// domain error: producer bugs, panics, marshalling failures, and exceptions +/// thrown by a callback-interface implementation. Every typed domain +/// exception derives from it. +public class NativeException : Exception +{ + /// An untyped producer error. + public const int GenericErrorCode = -1; + /// The producer panicked; the message carries the panic text. + public const int PanicErrorCode = -2; + /// An argument couldn't be lifted by the producer. + public const int MarshalErrorCode = -3; + /// A callback-interface implementation threw; the message carries + /// the original exception's message. + public const int ForeignErrorCode = -4; + /// The call was cancelled. Surfaces as + /// , never as this type. + public const int CancelledErrorCode = -5; + + /// The error code the native library reported. + public int Code { get; } + + /// Creates an exception carrying a native error code. + public NativeException(int code, string message) : base(message) + { + Code = code; + } + + /// Maps a runtime error code to its exception: cancellation to + /// , anything else to this + /// type. + internal static Exception FromError(int code, string message, byte[]? payload) + { + if (code == CancelledErrorCode) + { + return new OperationCanceledException(message); + } + return new NativeException(code, message); + } +} + +/// Maps a native error code, message, and serialized payload to the +/// exception a call throws. +internal delegate Exception FfiErrorMap(int code, string message, byte[]? payload); + +/// The native nested_modules_error slot. +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct FfiError +{ + public int Code; + public byte* Message; + public byte* PayloadPtr; + public nuint PayloadLen; +} + +/// Marshalling helpers shared by every generated call. +internal static unsafe class Ffi +{ + /// The UTF-8 bytes of a string argument. The native side borrows + /// them as a (pointer, length) pair, so interior NULs survive. + internal static byte[] Utf8(string value) + { + return Encoding.UTF8.GetBytes(value); + } + + /// Decodes a borrowed UTF-8 run; the native side keeps + /// ownership. + internal static string ReadString(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? "" : Encoding.UTF8.GetString(ptr, checked((int)len)); + } + + /// Copies a borrowed byte run; the native side keeps + /// ownership. + internal static byte[] ReadBytes(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? Array.Empty() : new ReadOnlySpan(ptr, checked((int)len)).ToArray(); + } + + /// Decodes a returned UTF-8 string and releases it. + internal static string TakeString(byte* ptr, nuint len) + { + try + { + return ReadString(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies returned bytes and releases them. + internal static byte[] TakeBytes(byte* ptr, nuint len) + { + try + { + return ReadBytes(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies a returned value buffer, releases it, and returns a + /// reader over the copy. + internal static FfiBufferReader TakeBuffer(byte* ptr, nuint len) + { + return new FfiBufferReader(TakeBytes(ptr, len)); + } + + /// Releases a caller-owned error slot and returns the exception + /// it described. + internal static Exception TakeError(FfiError* err, FfiErrorMap map) + { + var code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorClear(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + /// Releases a heap-boxed async error and returns the exception it + /// described. + internal static Exception TakeBoxedError(FfiError* err, FfiErrorMap map, out int code) + { + code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorFree(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + private static string ErrorMessage(FfiError* err) + { + return Marshal.PtrToStringUTF8((IntPtr)err->Message) ?? ""; + } + + /// Reports a callback-interface failure to the producer through + /// out_err. The producer copies the message. + internal static void SetForeignError(FfiError* err, Exception e) + { + var bytes = Encoding.UTF8.GetBytes(e.Message + "\0"); + fixed (byte* message = bytes) + { + NativeMethods.ErrorSet(err, NativeException.ForeignErrorCode, message); + } + } + + /// Pins a callback-interface implementation for the producer and + /// returns the ctx it hands back to every trampoline. The + /// producer releases it through the vtable's free. + internal static IntPtr Register(object implementation) + { + return GCHandle.ToIntPtr(GCHandle.Alloc(implementation)); + } + + /// Releases a ctx the producer never received (the call + /// failed before reaching it) or no longer holds. + internal static void Unregister(IntPtr ctx) + { + GCHandle.FromIntPtr(ctx).Free(); + } + + /// The implementation behind a trampoline's ctx. + internal static T Target(IntPtr ctx) where T : class + { + return (T)GCHandle.FromIntPtr(ctx).Target!; + } +} + +/// Imports of the native runtime surface plus the load-time contract +/// check. The generated API declares its own imports in the other half of +/// this partial class. +internal static unsafe partial class NativeMethods +{ + /// The native library's base name. + internal const string LibName = "nested_modules"; + + /// The environment variable naming an explicit library + /// path. + internal const string LibraryEnvVar = "NESTED_MODULES_LIBRARY"; + + /// The C ABI revision these bindings were generated + /// against. + internal const uint AbiVersion = 3; + + // Runs before the first native call through this class, so a library + // built for another ABI revision or another contract fails here instead + // of misreading memory later. + static NativeMethods() + { + try + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, Resolve); + } + catch (InvalidOperationException) + { + // The assembly already has a resolver; default probing still applies. + } + uint found; + try + { + found = AbiVersionOf(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't export nested_modules_abi_version; these bindings expect ABI revision {AbiVersion}", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"ABI mismatch: these bindings expect revision {AbiVersion} but the loaded {LibName} library reports revision {found}"); + } + VerifyChecksums(); + } + + static partial void VerifyChecksums(); + + private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) + { + if (name != LibName) + { + return IntPtr.Zero; + } + var path = Environment.GetEnvironmentVariable(LibraryEnvVar); + return string.IsNullOrEmpty(path) ? IntPtr.Zero : NativeLibrary.Load(path); + } + + /// Fails the load when module was + /// generated from a different contract than the loaded library + /// implements. + private static void VerifyChecksum(string module, delegate* checksum, ulong expected) + { + ulong found; + try + { + found = checksum(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't implement module '{module}'", e); + } + if (found != expected) + { + throw new InvalidOperationException( + $"contract mismatch for module '{module}': these bindings expect checksum 0x{expected:x16} but the loaded {LibName} library reports 0x{found:x16}; regenerate the bindings for this library"); + } + } + + [LibraryImport(LibName, EntryPoint = "nested_modules_abi_version")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial uint AbiVersionOf(); + + [LibraryImport(LibName, EntryPoint = "nested_modules_error_set")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorSet(FfiError* err, int code, byte* message); + + [LibraryImport(LibName, EntryPoint = "nested_modules_error_clear")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorClear(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "nested_modules_error_free")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorFree(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "nested_modules_free_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void FreeBytes(byte* ptr, nuint len); + + [LibraryImport(LibName, EntryPoint = "nested_modules_cancel_token_create")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr CancelTokenCreate(); + + [LibraryImport(LibName, EntryPoint = "nested_modules_cancel_token_cancel")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenCancel(IntPtr token); + + [LibraryImport(LibName, EntryPoint = "nested_modules_cancel_token_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenDestroy(IntPtr token); +} + +/// One pending async call: the task it completes, the native +/// context that finds it again, and, for a cancellable function, the +/// native cancel token linked to the caller's +/// . +internal sealed class FfiCall +{ + private readonly TaskCompletionSource _tcs = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationToken _cancellation; + private readonly object _gate = new object(); + private GCHandle _self; + private IntPtr _token; + private CancellationTokenRegistration _registration; + private bool _completed; + private bool _cancelling; + + internal FfiCall(CancellationToken cancellation = default) + { + _cancellation = cancellation; + _self = GCHandle.Alloc(this); + } + + /// The task the completion resolves. + internal Task Task => _tcs.Task; + + /// The context argument for the launcher. + internal IntPtr Context => GCHandle.ToIntPtr(_self); + + /// Creates the native cancel token for the launcher and cancels + /// it when the caller's fires. + internal IntPtr CancelToken() + { + _token = NativeMethods.CancelTokenCreate(); + if (_cancellation.CanBeCanceled) + { + _registration = _cancellation.Register(static state => ((FfiCall)state!).Cancel(), this); + } + return _token; + } + + /// Recovers the call behind a completion's context. Runs + /// exactly once per launch, on a producer thread. + internal static FfiCall Complete(IntPtr context) + { + var handle = GCHandle.FromIntPtr(context); + var call = (FfiCall)handle.Target!; + handle.Free(); + call.Release(); + return call; + } + + /// Releases everything when the launcher threw before the + /// producer took the call, so no completion will arrive. + internal void Abandon() + { + _self.Free(); + Release(); + } + + internal void SetResult(T value) + { + _tcs.TrySetResult(value); + } + + internal void SetException(Exception e) + { + _tcs.TrySetException(e); + } + + /// Faults (or cancels, for the cancelled code) the task from a + /// heap-boxed error, releasing the box. + internal unsafe void SetError(FfiError* err, FfiErrorMap map) + { + var e = Ffi.TakeBoxedError(err, map, out var code); + if (code == NativeException.CancelledErrorCode) + { + _tcs.TrySetCanceled(_cancellation); + } + else + { + _tcs.TrySetException(e); + } + } + + // The registration callback and the completion race: whichever runs + // last destroys the consumer's token reference, so a cancel never + // touches a destroyed token. + private void Cancel() + { + lock (_gate) + { + if (_completed) + { + return; + } + _cancelling = true; + } + NativeMethods.CancelTokenCancel(_token); + bool destroy; + lock (_gate) + { + _cancelling = false; + destroy = _completed; + } + if (destroy) + { + NativeMethods.CancelTokenDestroy(_token); + } + } + + private void Release() + { + if (_token == IntPtr.Zero) + { + return; + } + bool destroy; + lock (_gate) + { + _completed = true; + destroy = !_cancelling; + } + if (destroy) + { + _registration.Dispose(); + NativeMethods.CancelTokenDestroy(_token); + } + } +} + +/// Owns one native iterator and destroys it exactly once, when +/// enumeration ends, the enumerator is disposed, or the garbage collector +/// finalizes an abandoned one. +internal sealed unsafe class FfiIteratorHandle : SafeHandle +{ + private readonly delegate* _destroy; + + internal FfiIteratorHandle(IntPtr iterator, delegate* destroy) + : base(IntPtr.Zero, true) + { + _destroy = destroy; + SetHandle(iterator); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + _destroy(handle); + return true; + } +} + +/// A lazily streamed sequence backed by a native iterator. It can be +/// enumerated once; each MoveNext pulls one item. +internal sealed unsafe class FfiSequence : IEnumerable, IEnumerator +{ + private readonly FfiIteratorHandle _iterator; + private readonly delegate* _next; + private int _started; + private T _current = default!; + + internal FfiSequence(FfiIteratorHandle iterator, delegate* next) + { + _iterator = iterator; + _next = next; + } + + public T Current => _current; + + object? IEnumerator.Current => _current; + + public IEnumerator GetEnumerator() + { + if (Interlocked.Exchange(ref _started, 1) != 0) + { + throw new InvalidOperationException("this sequence can be enumerated only once"); + } + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool MoveNext() + { + if (_iterator.IsClosed) + { + return false; + } + if (_next(_iterator, out _current)) + { + return true; + } + _iterator.Dispose(); + return false; + } + + public void Reset() + { + throw new NotSupportedException("a native iterator can't be reset"); + } + + public void Dispose() + { + _iterator.Dispose(); + } +} + +/// Serializes values into the value-buffer wire format +/// (little-endian, packed). +internal sealed class FfiBufferWriter +{ + private byte[] _buf = new byte[64]; + private int _len; + + /// The bytes written so far. + internal ReadOnlySpan Written => _buf.AsSpan(0, _len); + + /// The number of bytes written so far. + internal int Length => _len; + + private Span Grow(int extra) + { + if (_len + extra > _buf.Length) + { + Array.Resize(ref _buf, Math.Max(_buf.Length * 2, _len + extra)); + } + var span = _buf.AsSpan(_len, extra); + _len += extra; + return span; + } + + internal void WriteBool(bool v) => WriteU8(v ? (byte)1 : (byte)0); + + internal void WriteI8(sbyte v) => WriteU8((byte)v); + + internal void WriteU8(byte v) => Grow(1)[0] = v; + + internal void WriteI16(short v) => System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); + + internal void WriteU16(ushort v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); + + internal void WriteI32(int v) => System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); + + internal void WriteU32(uint v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); + + internal void WriteI64(long v) => System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); + + internal void WriteU64(ulong v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); + + internal void WriteF32(float v) => System.Buffers.Binary.BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); + + internal void WriteF64(double v) => System.Buffers.Binary.BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); + + internal void WriteLen(int len) => WriteU32((uint)len); + + internal void WriteString(string v) + { + var count = Encoding.UTF8.GetByteCount(v); + WriteLen(count); + Encoding.UTF8.GetBytes(v, Grow(count)); + } + + internal void WriteBytes(byte[] v) + { + WriteLen(v.Length); + v.CopyTo(Grow(v.Length)); + } + + /// Writes an object token. The token must carry its own strong + /// reference (a fresh clone), which the reader adopts. + internal void WriteObject(IntPtr token) => WriteU64((ulong)(nuint)token); +} + +/// Decodes values from the value-buffer wire format. A malformed +/// buffer is a producer or consumer bug and throws +/// . +internal sealed class FfiBufferReader +{ + private static readonly Encoding Utf8Strict = new UTF8Encoding(false, true); + + private readonly byte[] _data; + private int _pos; + + internal FfiBufferReader(byte[] data) + { + _data = data; + } + + private static InvalidOperationException Malformed(string what) + { + return new InvalidOperationException("malformed value buffer: " + what); + } + + private ReadOnlySpan Take(int n) + { + if (_data.Length - _pos < n) + { + throw Malformed("buffer exhausted"); + } + var span = _data.AsSpan(_pos, n); + _pos += n; + return span; + } + + internal bool ReadBool() + { + var b = ReadU8(); + if (b > 1) + { + throw Malformed("invalid bool byte"); + } + return b == 1; + } + + internal sbyte ReadI8() => (sbyte)ReadU8(); + + internal byte ReadU8() => Take(1)[0]; + + internal short ReadI16() => System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(Take(2)); + + internal ushort ReadU16() => System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); + + internal int ReadI32() => System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(Take(4)); + + internal uint ReadU32() => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); + + internal long ReadI64() => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(Take(8)); + + internal ulong ReadU64() => System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); + + internal float ReadF32() => System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(Take(4)); + + internal double ReadF64() => System.Buffers.Binary.BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); + + /// A collection count. Elements can be zero-sized, so the count + /// isn't checked against the remaining bytes; callers cap their + /// preallocation with . + internal int ReadLen() + { + var len = ReadU32(); + if (len > int.MaxValue) + { + throw Malformed("length prefix out of range"); + } + return (int)len; + } + + /// A preallocation size for elements + /// that a malformed count can't inflate. + internal int Capacity(int count) => Math.Min(count, _data.Length - _pos); + + internal string ReadString() + { + var bytes = Take(ReadLen()); + try + { + return Utf8Strict.GetString(bytes); + } + catch (ArgumentException) + { + throw Malformed("string is not valid UTF-8"); + } + } + + internal byte[] ReadBytes() => Take(ReadLen()).ToArray(); + + /// Reads an object token carrying one strong reference. + internal IntPtr ReadObject() + { + var token = ReadU64(); + if (token == 0) + { + throw Malformed("null object token"); + } + return (IntPtr)(nint)token; + } + + internal void ExpectEnd() + { + if (_pos != _data.Length) + { + throw Malformed("trailing bytes"); + } + } +} + +// END Runtime.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap deleted file mode 100644 index 56741e7d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap +++ /dev/null @@ -1,663 +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 -{ - /// Raised for any WeaveFFI failure that isn't a typed domain - /// error: producer bugs, panics, marshalling failures, and exceptions - /// thrown by a callback-interface implementation. - public class WeaveFFIException : Exception - { - /// An untyped producer error. - public const int GenericErrorCode = -1; - /// The producer panicked; the message carries the panic text. - public const int PanicErrorCode = -2; - /// An argument couldn't be lifted by the producer. - public const int MarshalErrorCode = -3; - /// A callback-interface implementation threw; the message - /// carries the original exception's message. - public const int ForeignErrorCode = -4; - - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - /// Typed exception for the GraphicsError error domain (module graphics). - public class GraphicsException : WeaveFFIException - { - /// coordinate out of bounds - public const int OutOfBounds = 1; - - public GraphicsException(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 OutOfBounds: - return new GraphicsException(code, string.IsNullOrEmpty(message) ? "coordinate out of bounds" : 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 CheckGraphics(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 GraphicsException.FromCode(code, msg, payload); - } - } - } - - /// One strong reference to a native object, on its way into - /// a wrapper that adopts it. A distinct type (rather than a bare - /// IntPtr) so the adopting constructor never competes with a - /// public constructor taking an integer. - internal readonly struct WeaveFFIHandle - { - internal readonly IntPtr Value; - - internal WeaveFFIHandle(IntPtr value) - { - Value = value; - } - } - - 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 void WriteObject(IntPtr token) - { - WriteU64((ulong)(nuint)(nint)token); - } - - 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 IntPtr ReadObject() - { - var token = ReadU64(); - if (token == 0) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: null object token"); - } - return (nint)(nuint)token; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - /// A measurement unit shared with nested submodules. - public enum Unit - { - pixels = 0, - points = 1, - } - - /// Top-level struct that references a struct defined one level deeper. - public sealed class Canvas - { - public int Width { get; } - - /// Reference down into a nested module - public Shape Root { get; } - - public Canvas(int width, Shape root) - { - Width = width; - Root = root; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI32(Width); - Root.WriteTo(writer); - } - - internal static Canvas ReadFrom(WeaveFFIBufferReader reader) - { - var fWidth = reader.ReadI32(); - var fRoot = Shape.ReadFrom(reader); - return new Canvas(fWidth, fRoot); - } - } - - /// A struct one level deep that references both an ancestor enum and a deeper struct. - public sealed class Shape - { - public int Sides { get; } - - /// Reference up to the parent module's enum - public Unit Unit { get; } - - /// Reference down into a deeper module - public Detail Detail { get; } - - public Shape(int sides, Unit unit, Detail detail) - { - Sides = sides; - Unit = unit; - Detail = detail; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI32(Sides); - writer.WriteI32((int)Unit); - Detail.WriteTo(writer); - } - - internal static Shape ReadFrom(WeaveFFIBufferReader reader) - { - var fSides = reader.ReadI32(); - var fUnit = (Unit)reader.ReadI32(); - var fDetail = Detail.ReadFrom(reader); - return new Shape(fSides, fUnit, fDetail); - } - } - - /// A struct two levels deep. - public sealed class Detail - { - public double Area { get; } - - public Detail(double area) - { - Area = area; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteF64(Area); - } - - internal static Detail ReadFrom(WeaveFFIBufferReader reader) - { - var fArea = reader.ReadF64(); - return new Detail(fArea); - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - // The ABI revision these bindings were generated against. - internal const uint AbiVersion = 2; - - // 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(); - - // Fills a producer-owned error slot with a copy of `message`; callback - // trampolines report a thrown exception through it so no managed - // allocation is ever handed to the producer's allocator. - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_set(IntPtr err, int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string message); - - [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_graphics_new_canvas", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_graphics_new_canvas(int width, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_graphics_shapes_make_shape", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_graphics_shapes_make_shape(int sides, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_graphics_shapes_describe", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_graphics_shapes_describe(out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_graphics_shapes_geometry_compute", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_graphics_shapes_geometry_compute(IntPtr shape_ptr, UIntPtr shape_len, out UIntPtr out_len, ref WeaveFFIError out_err); - - } - - public static class Graphics - { - /// Construct a Canvas. - public static Canvas NewCanvas(int width) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_graphics_new_canvas(width, 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 = Canvas.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - - } - - public static class GraphicsShapes - { - /// Construct a Shape. - public static Shape MakeShape(int sides) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_graphics_shapes_make_shape(sides, 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 = Shape.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - - /// Nested function returning a struct owned by an ancestor module. - public static Canvas Describe() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_graphics_shapes_describe(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 = Canvas.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - - } - - public static class GraphicsShapesGeometry - { - /// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - /// Thrown when the call reports a GraphicsError code. - public static Detail Compute(Shape shape) - { - var err = new WeaveFFIError(); - var shapeWriter = new WeaveFFIBufferWriter(); - shape.WriteTo(shapeWriter); - var shapeBuf = shapeWriter.ToArray(); - var shapePin = GCHandle.Alloc(shapeBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_graphics_shapes_geometry_compute(shapePin.AddrOfPinnedObject(), (UIntPtr)shapeBuf.Length, out var outLen, ref err); - WeaveFFIError.CheckGraphics(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 = Detail.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - finally - { - shapePin.Free(); - } - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__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_nested_modules__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__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_shapes__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__README_md.snap index 96a3a5e8..d46dda8a 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__README_md.snap @@ -1,13 +1,17 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI (.NET) +# Shapes (.NET) -Auto-generated P/Invoke bindings for the WeaveFFI native library. +Native bindings for shapes + +The `Shapes` namespace binds the `shapes` native library through +source-generated P/Invoke (`[LibraryImport]`), so it's trim- and +AOT-friendly. ## Build @@ -15,12 +19,14 @@ Auto-generated P/Invoke bindings for the WeaveFFI native library. dotnet build ``` -## Pack +Reference this project from yours (`dotnet add reference Shapes.csproj`) +or pack it (`dotnet pack -c Release`). -```bash -dotnet pack -``` +## Loading the native library -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). +The bindings load `shapes` with the platform's normal rules +(`libshapes.dylib`, `libshapes.so`, or `shapes.dll` next to the app or on the +library search path). Set `SHAPES_LIBRARY` to a full path to load a +specific file instead. diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Runtime_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Runtime_cs.snap new file mode 100644 index 00000000..c73f86c0 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Runtime_cs.snap @@ -0,0 +1,698 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Shapes; + +/// Raised for a failure the native library reports that isn't a typed +/// domain error: producer bugs, panics, marshalling failures, and exceptions +/// thrown by a callback-interface implementation. Every typed domain +/// exception derives from it. +public class NativeException : Exception +{ + /// An untyped producer error. + public const int GenericErrorCode = -1; + /// The producer panicked; the message carries the panic text. + public const int PanicErrorCode = -2; + /// An argument couldn't be lifted by the producer. + public const int MarshalErrorCode = -3; + /// A callback-interface implementation threw; the message carries + /// the original exception's message. + public const int ForeignErrorCode = -4; + /// The call was cancelled. Surfaces as + /// , never as this type. + public const int CancelledErrorCode = -5; + + /// The error code the native library reported. + public int Code { get; } + + /// Creates an exception carrying a native error code. + public NativeException(int code, string message) : base(message) + { + Code = code; + } + + /// Maps a runtime error code to its exception: cancellation to + /// , anything else to this + /// type. + internal static Exception FromError(int code, string message, byte[]? payload) + { + if (code == CancelledErrorCode) + { + return new OperationCanceledException(message); + } + return new NativeException(code, message); + } +} + +/// Maps a native error code, message, and serialized payload to the +/// exception a call throws. +internal delegate Exception FfiErrorMap(int code, string message, byte[]? payload); + +/// The native shapes_error slot. +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct FfiError +{ + public int Code; + public byte* Message; + public byte* PayloadPtr; + public nuint PayloadLen; +} + +/// Marshalling helpers shared by every generated call. +internal static unsafe class Ffi +{ + /// The UTF-8 bytes of a string argument. The native side borrows + /// them as a (pointer, length) pair, so interior NULs survive. + internal static byte[] Utf8(string value) + { + return Encoding.UTF8.GetBytes(value); + } + + /// Decodes a borrowed UTF-8 run; the native side keeps + /// ownership. + internal static string ReadString(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? "" : Encoding.UTF8.GetString(ptr, checked((int)len)); + } + + /// Copies a borrowed byte run; the native side keeps + /// ownership. + internal static byte[] ReadBytes(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? Array.Empty() : new ReadOnlySpan(ptr, checked((int)len)).ToArray(); + } + + /// Decodes a returned UTF-8 string and releases it. + internal static string TakeString(byte* ptr, nuint len) + { + try + { + return ReadString(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies returned bytes and releases them. + internal static byte[] TakeBytes(byte* ptr, nuint len) + { + try + { + return ReadBytes(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies a returned value buffer, releases it, and returns a + /// reader over the copy. + internal static FfiBufferReader TakeBuffer(byte* ptr, nuint len) + { + return new FfiBufferReader(TakeBytes(ptr, len)); + } + + /// Releases a caller-owned error slot and returns the exception + /// it described. + internal static Exception TakeError(FfiError* err, FfiErrorMap map) + { + var code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorClear(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + /// Releases a heap-boxed async error and returns the exception it + /// described. + internal static Exception TakeBoxedError(FfiError* err, FfiErrorMap map, out int code) + { + code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorFree(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + private static string ErrorMessage(FfiError* err) + { + return Marshal.PtrToStringUTF8((IntPtr)err->Message) ?? ""; + } + + /// Reports a callback-interface failure to the producer through + /// out_err. The producer copies the message. + internal static void SetForeignError(FfiError* err, Exception e) + { + var bytes = Encoding.UTF8.GetBytes(e.Message + "\0"); + fixed (byte* message = bytes) + { + NativeMethods.ErrorSet(err, NativeException.ForeignErrorCode, message); + } + } + + /// Pins a callback-interface implementation for the producer and + /// returns the ctx it hands back to every trampoline. The + /// producer releases it through the vtable's free. + internal static IntPtr Register(object implementation) + { + return GCHandle.ToIntPtr(GCHandle.Alloc(implementation)); + } + + /// Releases a ctx the producer never received (the call + /// failed before reaching it) or no longer holds. + internal static void Unregister(IntPtr ctx) + { + GCHandle.FromIntPtr(ctx).Free(); + } + + /// The implementation behind a trampoline's ctx. + internal static T Target(IntPtr ctx) where T : class + { + return (T)GCHandle.FromIntPtr(ctx).Target!; + } +} + +/// Imports of the native runtime surface plus the load-time contract +/// check. The generated API declares its own imports in the other half of +/// this partial class. +internal static unsafe partial class NativeMethods +{ + /// The native library's base name. + internal const string LibName = "shapes"; + + /// The environment variable naming an explicit library + /// path. + internal const string LibraryEnvVar = "SHAPES_LIBRARY"; + + /// The C ABI revision these bindings were generated + /// against. + internal const uint AbiVersion = 3; + + // Runs before the first native call through this class, so a library + // built for another ABI revision or another contract fails here instead + // of misreading memory later. + static NativeMethods() + { + try + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, Resolve); + } + catch (InvalidOperationException) + { + // The assembly already has a resolver; default probing still applies. + } + uint found; + try + { + found = AbiVersionOf(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't export shapes_abi_version; these bindings expect ABI revision {AbiVersion}", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"ABI mismatch: these bindings expect revision {AbiVersion} but the loaded {LibName} library reports revision {found}"); + } + VerifyChecksums(); + } + + static partial void VerifyChecksums(); + + private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) + { + if (name != LibName) + { + return IntPtr.Zero; + } + var path = Environment.GetEnvironmentVariable(LibraryEnvVar); + return string.IsNullOrEmpty(path) ? IntPtr.Zero : NativeLibrary.Load(path); + } + + /// Fails the load when module was + /// generated from a different contract than the loaded library + /// implements. + private static void VerifyChecksum(string module, delegate* checksum, ulong expected) + { + ulong found; + try + { + found = checksum(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't implement module '{module}'", e); + } + if (found != expected) + { + throw new InvalidOperationException( + $"contract mismatch for module '{module}': these bindings expect checksum 0x{expected:x16} but the loaded {LibName} library reports 0x{found:x16}; regenerate the bindings for this library"); + } + } + + [LibraryImport(LibName, EntryPoint = "shapes_abi_version")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial uint AbiVersionOf(); + + [LibraryImport(LibName, EntryPoint = "shapes_error_set")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorSet(FfiError* err, int code, byte* message); + + [LibraryImport(LibName, EntryPoint = "shapes_error_clear")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorClear(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "shapes_error_free")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorFree(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "shapes_free_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void FreeBytes(byte* ptr, nuint len); + + [LibraryImport(LibName, EntryPoint = "shapes_cancel_token_create")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr CancelTokenCreate(); + + [LibraryImport(LibName, EntryPoint = "shapes_cancel_token_cancel")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenCancel(IntPtr token); + + [LibraryImport(LibName, EntryPoint = "shapes_cancel_token_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenDestroy(IntPtr token); +} + +/// One pending async call: the task it completes, the native +/// context that finds it again, and, for a cancellable function, the +/// native cancel token linked to the caller's +/// . +internal sealed class FfiCall +{ + private readonly TaskCompletionSource _tcs = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationToken _cancellation; + private readonly object _gate = new object(); + private GCHandle _self; + private IntPtr _token; + private CancellationTokenRegistration _registration; + private bool _completed; + private bool _cancelling; + + internal FfiCall(CancellationToken cancellation = default) + { + _cancellation = cancellation; + _self = GCHandle.Alloc(this); + } + + /// The task the completion resolves. + internal Task Task => _tcs.Task; + + /// The context argument for the launcher. + internal IntPtr Context => GCHandle.ToIntPtr(_self); + + /// Creates the native cancel token for the launcher and cancels + /// it when the caller's fires. + internal IntPtr CancelToken() + { + _token = NativeMethods.CancelTokenCreate(); + if (_cancellation.CanBeCanceled) + { + _registration = _cancellation.Register(static state => ((FfiCall)state!).Cancel(), this); + } + return _token; + } + + /// Recovers the call behind a completion's context. Runs + /// exactly once per launch, on a producer thread. + internal static FfiCall Complete(IntPtr context) + { + var handle = GCHandle.FromIntPtr(context); + var call = (FfiCall)handle.Target!; + handle.Free(); + call.Release(); + return call; + } + + /// Releases everything when the launcher threw before the + /// producer took the call, so no completion will arrive. + internal void Abandon() + { + _self.Free(); + Release(); + } + + internal void SetResult(T value) + { + _tcs.TrySetResult(value); + } + + internal void SetException(Exception e) + { + _tcs.TrySetException(e); + } + + /// Faults (or cancels, for the cancelled code) the task from a + /// heap-boxed error, releasing the box. + internal unsafe void SetError(FfiError* err, FfiErrorMap map) + { + var e = Ffi.TakeBoxedError(err, map, out var code); + if (code == NativeException.CancelledErrorCode) + { + _tcs.TrySetCanceled(_cancellation); + } + else + { + _tcs.TrySetException(e); + } + } + + // The registration callback and the completion race: whichever runs + // last destroys the consumer's token reference, so a cancel never + // touches a destroyed token. + private void Cancel() + { + lock (_gate) + { + if (_completed) + { + return; + } + _cancelling = true; + } + NativeMethods.CancelTokenCancel(_token); + bool destroy; + lock (_gate) + { + _cancelling = false; + destroy = _completed; + } + if (destroy) + { + NativeMethods.CancelTokenDestroy(_token); + } + } + + private void Release() + { + if (_token == IntPtr.Zero) + { + return; + } + bool destroy; + lock (_gate) + { + _completed = true; + destroy = !_cancelling; + } + if (destroy) + { + _registration.Dispose(); + NativeMethods.CancelTokenDestroy(_token); + } + } +} + +/// Owns one native iterator and destroys it exactly once, when +/// enumeration ends, the enumerator is disposed, or the garbage collector +/// finalizes an abandoned one. +internal sealed unsafe class FfiIteratorHandle : SafeHandle +{ + private readonly delegate* _destroy; + + internal FfiIteratorHandle(IntPtr iterator, delegate* destroy) + : base(IntPtr.Zero, true) + { + _destroy = destroy; + SetHandle(iterator); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + _destroy(handle); + return true; + } +} + +/// A lazily streamed sequence backed by a native iterator. It can be +/// enumerated once; each MoveNext pulls one item. +internal sealed unsafe class FfiSequence : IEnumerable, IEnumerator +{ + private readonly FfiIteratorHandle _iterator; + private readonly delegate* _next; + private int _started; + private T _current = default!; + + internal FfiSequence(FfiIteratorHandle iterator, delegate* next) + { + _iterator = iterator; + _next = next; + } + + public T Current => _current; + + object? IEnumerator.Current => _current; + + public IEnumerator GetEnumerator() + { + if (Interlocked.Exchange(ref _started, 1) != 0) + { + throw new InvalidOperationException("this sequence can be enumerated only once"); + } + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool MoveNext() + { + if (_iterator.IsClosed) + { + return false; + } + if (_next(_iterator, out _current)) + { + return true; + } + _iterator.Dispose(); + return false; + } + + public void Reset() + { + throw new NotSupportedException("a native iterator can't be reset"); + } + + public void Dispose() + { + _iterator.Dispose(); + } +} + +/// Serializes values into the value-buffer wire format +/// (little-endian, packed). +internal sealed class FfiBufferWriter +{ + private byte[] _buf = new byte[64]; + private int _len; + + /// The bytes written so far. + internal ReadOnlySpan Written => _buf.AsSpan(0, _len); + + /// The number of bytes written so far. + internal int Length => _len; + + private Span Grow(int extra) + { + if (_len + extra > _buf.Length) + { + Array.Resize(ref _buf, Math.Max(_buf.Length * 2, _len + extra)); + } + var span = _buf.AsSpan(_len, extra); + _len += extra; + return span; + } + + internal void WriteBool(bool v) => WriteU8(v ? (byte)1 : (byte)0); + + internal void WriteI8(sbyte v) => WriteU8((byte)v); + + internal void WriteU8(byte v) => Grow(1)[0] = v; + + internal void WriteI16(short v) => System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); + + internal void WriteU16(ushort v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); + + internal void WriteI32(int v) => System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); + + internal void WriteU32(uint v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); + + internal void WriteI64(long v) => System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); + + internal void WriteU64(ulong v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); + + internal void WriteF32(float v) => System.Buffers.Binary.BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); + + internal void WriteF64(double v) => System.Buffers.Binary.BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); + + internal void WriteLen(int len) => WriteU32((uint)len); + + internal void WriteString(string v) + { + var count = Encoding.UTF8.GetByteCount(v); + WriteLen(count); + Encoding.UTF8.GetBytes(v, Grow(count)); + } + + internal void WriteBytes(byte[] v) + { + WriteLen(v.Length); + v.CopyTo(Grow(v.Length)); + } + + /// Writes an object token. The token must carry its own strong + /// reference (a fresh clone), which the reader adopts. + internal void WriteObject(IntPtr token) => WriteU64((ulong)(nuint)token); +} + +/// Decodes values from the value-buffer wire format. A malformed +/// buffer is a producer or consumer bug and throws +/// . +internal sealed class FfiBufferReader +{ + private static readonly Encoding Utf8Strict = new UTF8Encoding(false, true); + + private readonly byte[] _data; + private int _pos; + + internal FfiBufferReader(byte[] data) + { + _data = data; + } + + private static InvalidOperationException Malformed(string what) + { + return new InvalidOperationException("malformed value buffer: " + what); + } + + private ReadOnlySpan Take(int n) + { + if (_data.Length - _pos < n) + { + throw Malformed("buffer exhausted"); + } + var span = _data.AsSpan(_pos, n); + _pos += n; + return span; + } + + internal bool ReadBool() + { + var b = ReadU8(); + if (b > 1) + { + throw Malformed("invalid bool byte"); + } + return b == 1; + } + + internal sbyte ReadI8() => (sbyte)ReadU8(); + + internal byte ReadU8() => Take(1)[0]; + + internal short ReadI16() => System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(Take(2)); + + internal ushort ReadU16() => System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); + + internal int ReadI32() => System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(Take(4)); + + internal uint ReadU32() => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); + + internal long ReadI64() => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(Take(8)); + + internal ulong ReadU64() => System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); + + internal float ReadF32() => System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(Take(4)); + + internal double ReadF64() => System.Buffers.Binary.BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); + + /// A collection count. Elements can be zero-sized, so the count + /// isn't checked against the remaining bytes; callers cap their + /// preallocation with . + internal int ReadLen() + { + var len = ReadU32(); + if (len > int.MaxValue) + { + throw Malformed("length prefix out of range"); + } + return (int)len; + } + + /// A preallocation size for elements + /// that a malformed count can't inflate. + internal int Capacity(int count) => Math.Min(count, _data.Length - _pos); + + internal string ReadString() + { + var bytes = Take(ReadLen()); + try + { + return Utf8Strict.GetString(bytes); + } + catch (ArgumentException) + { + throw Malformed("string is not valid UTF-8"); + } + } + + internal byte[] ReadBytes() => Take(ReadLen()).ToArray(); + + /// Reads an object token carrying one strong reference. + internal IntPtr ReadObject() + { + var token = ReadU64(); + if (token == 0) + { + throw Malformed("null object token"); + } + return (IntPtr)(nint)token; + } + + internal void ExpectEnd() + { + if (_pos != _data.Length) + { + throw Malformed("trailing bytes"); + } + } +} + +// END Runtime.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_cs.snap new file mode 100644 index 00000000..97a4573d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_cs.snap @@ -0,0 +1,332 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Shapes; + +/// An algebraic shape (sum type with associated data) +public abstract class Shape +{ + private Shape() + { + } + + /// The empty shape (a unit variant) + public sealed class Empty : Shape + { + } + + /// A circle with a radius + public sealed class Circle : Shape + { + /// Radius in points + public double Radius { get; } + + /// Creates a from every field. + public Circle(double radius) + { + Radius = radius; + } + } + + /// An axis-aligned rectangle + public sealed class Rectangle : Shape + { + public float Width { get; } + + public float Height { get; } + + /// Creates a from every field. + public Rectangle(float width, float height) + { + Width = width; + Height = height; + } + } + + /// An integer-bounded shape (exercises narrow + wide ints) + public sealed class Bounded : Shape + { + public short Min { get; } + + public short Max { get; } + + public ulong Weight { get; } + + /// Creates a from every field. + public Bounded(short min, short max, ulong weight) + { + Min = min; + Max = max; + Weight = weight; + } + } + + /// A labeled shape with a small count + public sealed class Labeled : Shape + { + public string Label { get; } + + public byte Count { get; } + + /// Creates a from every field. + public Labeled(string label, byte count) + { + Label = label; + Count = count; + } + } + + internal void WriteTo(FfiBufferWriter writer) + { + switch (this) + { + case Empty _: + writer.WriteI32(0); + break; + case Circle v: + writer.WriteI32(1); + writer.WriteF64(v.Radius); + break; + case Rectangle v: + writer.WriteI32(2); + writer.WriteF32(v.Width); + writer.WriteF32(v.Height); + break; + case Bounded v: + writer.WriteI32(3); + writer.WriteI16(v.Min); + writer.WriteI16(v.Max); + writer.WriteU64(v.Weight); + break; + case Labeled v: + writer.WriteI32(4); + writer.WriteString(v.Label); + writer.WriteU8(v.Count); + break; + default: + throw new InvalidOperationException("unknown Shape variant"); + } + } + + internal static Shape ReadFrom(FfiBufferReader reader) + { + var tag = reader.ReadI32(); + switch (tag) + { + case 0: + { + return new Empty(); + } + case 1: + { + var fRadius = reader.ReadF64(); + return new Circle(fRadius); + } + case 2: + { + var fWidth = reader.ReadF32(); + var fHeight = reader.ReadF32(); + return new Rectangle(fWidth, fHeight); + } + case 3: + { + var fMin = reader.ReadI16(); + var fMax = reader.ReadI16(); + var fWeight = reader.ReadU64(); + return new Bounded(fMin, fMax, fWeight); + } + case 4: + { + var fLabel = reader.ReadString(); + var fCount = reader.ReadU8(); + return new Labeled(fLabel, fCount); + } + default: + throw new InvalidOperationException("malformed value buffer: unknown Shape tag " + tag); + } + } +} + +/// A plain C-style enum (no payloads) +public enum Channel +{ + Red = 0, + Green = 1, + Blue = 2, +} + +/// One field per supported numeric primitive +public sealed class Numerics +{ + public sbyte A { get; } + + public short B { get; } + + public int C { get; } + + public long D { get; } + + public byte E { get; } + + public ushort F { get; } + + public uint G { get; } + + public ulong H { get; } + + public float I { get; } + + public double J { get; } + + /// Creates a from every field. + public Numerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j) + { + A = a; + B = b; + C = c; + D = d; + E = e; + F = f; + G = g; + H = h; + I = i; + J = j; + } + + internal void WriteTo(FfiBufferWriter writer) + { + writer.WriteI8(A); + writer.WriteI16(B); + writer.WriteI32(C); + writer.WriteI64(D); + writer.WriteU8(E); + writer.WriteU16(F); + writer.WriteU32(G); + writer.WriteU64(H); + writer.WriteF32(I); + writer.WriteF64(J); + } + + internal static Numerics ReadFrom(FfiBufferReader reader) + { + var fA = reader.ReadI8(); + var fB = reader.ReadI16(); + var fC = reader.ReadI32(); + var fD = reader.ReadI64(); + var fE = reader.ReadU8(); + var fF = reader.ReadU16(); + var fG = reader.ReadU32(); + var fH = reader.ReadU64(); + var fI = reader.ReadF32(); + var fJ = reader.ReadF64(); + return new Numerics(fA, fB, fC, fD, fE, fF, fG, fH, fI, fJ); + } +} + +/// Rich (algebraic) enums and the expanded numeric primitives. +public static unsafe class Shapes +{ + /// Render a shape to a string + public static string Describe(Shape shape) + { + var shapeWriter = new FfiBufferWriter(); + shape.WriteTo(shapeWriter); + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* shapePtr = shapeWriter.Written) + { + var ffiResult = NativeMethods.shapes_shapes_describe(shapePtr, (nuint)shapeWriter.Length, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::Shapes.NativeException.FromError); + return Ffi.TakeString(ffiResult, ffiOutLen); + } + } + + /// Scale a shape by a factor, returning a new shape + public static Shape Scale(Shape shape, double factor) + { + var shapeWriter = new FfiBufferWriter(); + shape.WriteTo(shapeWriter); + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + fixed (byte* shapePtr = shapeWriter.Written) + { + var ffiResult = NativeMethods.shapes_shapes_scale(shapePtr, (nuint)shapeWriter.Length, factor, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::Shapes.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::Shapes.Shape.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } + } + + /// Sum a list of bytes into a wide integer (numerics smoke) + public static ulong SumBytes(ReadOnlySpan values) + { + var ffiErr = default(FfiError); + fixed (byte* valuesPtr = values) + { + var ffiResult = NativeMethods.shapes_shapes_sum_bytes(valuesPtr, (nuint)values.Length, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::Shapes.NativeException.FromError); + return ffiResult; + } + } + + /// Sum a list of bytes into a wide integer (numerics smoke) + public static ulong SumBytes(byte[] values) => SumBytes((ReadOnlySpan)values); + + /// Build a Numerics struct from every numeric primitive + public static Numerics MakeNumerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j) + { + var ffiErr = default(FfiError); + nuint ffiOutLen = 0; + var ffiResult = NativeMethods.shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &ffiOutLen, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::Shapes.NativeException.FromError); + var ffiDecodedReader = Ffi.TakeBuffer(ffiResult, ffiOutLen); + var ffiDecoded = global::Shapes.Numerics.ReadFrom(ffiDecodedReader); + ffiDecodedReader.ExpectEnd(); + return ffiDecoded; + } +} + +internal static unsafe partial class NativeMethods +{ + static partial void VerifyChecksums() + { + VerifyChecksum("shapes", &shapes_shapes_checksum, 0x06acf433ffa2a0f0UL); + } + + [LibraryImport(LibName, EntryPoint = "shapes_shapes_checksum")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial ulong shapes_shapes_checksum(); + + [LibraryImport(LibName, EntryPoint = "shapes_shapes_describe")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* shapes_shapes_describe(byte* shape_ptr, nuint shape_len, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "shapes_shapes_scale")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* shapes_shapes_scale(byte* shape_ptr, nuint shape_len, double factor, nuint* out_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "shapes_shapes_sum_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial ulong shapes_shapes_sum_bytes(byte* values_ptr, nuint values_len, FfiError* out_err); + + [LibraryImport(LibName, EntryPoint = "shapes_shapes_make_numerics")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial byte* shapes_shapes_make_numerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j, nuint* out_len, FfiError* out_err); +} + +// END Shapes.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_csproj.snap new file mode 100644 index 00000000..1b6f1159 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__Shapes_csproj.snap @@ -0,0 +1,26 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + + + + + net8.0 + Shapes + Shapes + Shapes + 0.1.0 + Native bindings for shapes + enable + true + true + + $(NoWarn);CS0618 + + + + + diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap deleted file mode 100644 index 7a65341c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap +++ /dev/null @@ -1,748 +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 -{ - /// Raised for any WeaveFFI failure that isn't a typed domain - /// error: producer bugs, panics, marshalling failures, and exceptions - /// thrown by a callback-interface implementation. - public class WeaveFFIException : Exception - { - /// An untyped producer error. - public const int GenericErrorCode = -1; - /// The producer panicked; the message carries the panic text. - public const int PanicErrorCode = -2; - /// An argument couldn't be lifted by the producer. - public const int MarshalErrorCode = -3; - /// A callback-interface implementation threw; the message - /// carries the original exception's message. - public const int ForeignErrorCode = -4; - - 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); - } - } - } - - /// One strong reference to a native object, on its way into - /// a wrapper that adopts it. A distinct type (rather than a bare - /// IntPtr) so the adopting constructor never competes with a - /// public constructor taking an integer. - internal readonly struct WeaveFFIHandle - { - internal readonly IntPtr Value; - - internal WeaveFFIHandle(IntPtr value) - { - Value = value; - } - } - - 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 void WriteObject(IntPtr token) - { - WriteU64((ulong)(nuint)(nint)token); - } - - 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 IntPtr ReadObject() - { - var token = ReadU64(); - if (token == 0) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: null object token"); - } - return (nint)(nuint)token; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - /// An algebraic shape (sum type with associated data) - public abstract class Shape - { - private Shape() - { - } - - /// The empty shape (a unit variant) - public sealed class Empty : Shape - { - } - - /// A circle with a radius - public sealed class Circle : Shape - { - /// Radius in points - public double Radius { get; } - - public Circle(double radius) - { - Radius = radius; - } - } - - /// An axis-aligned rectangle - public sealed class Rectangle : Shape - { - public float Width { get; } - - public float Height { get; } - - public Rectangle(float width, float height) - { - Width = width; - Height = height; - } - } - - /// An integer-bounded shape (exercises narrow + wide ints) - public sealed class Bounded : Shape - { - public short Min { get; } - - public short Max { get; } - - public ulong Weight { get; } - - public Bounded(short min, short max, ulong weight) - { - Min = min; - Max = max; - Weight = weight; - } - } - - /// A labeled shape with a small count - public sealed class Labeled : Shape - { - public string Label { get; } - - public byte Count { get; } - - public Labeled(string label, byte count) - { - Label = label; - Count = count; - } - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - switch (this) - { - case Empty _: - writer.WriteI32(0); - break; - case Circle v: - writer.WriteI32(1); - writer.WriteF64(v.Radius); - break; - case Rectangle v: - writer.WriteI32(2); - writer.WriteF32(v.Width); - writer.WriteF32(v.Height); - break; - case Bounded v: - writer.WriteI32(3); - writer.WriteI16(v.Min); - writer.WriteI16(v.Max); - writer.WriteU64(v.Weight); - break; - case Labeled v: - writer.WriteI32(4); - writer.WriteString(v.Label); - writer.WriteU8(v.Count); - break; - default: - throw new InvalidOperationException("unknown Shape variant"); - } - } - - internal static Shape ReadFrom(WeaveFFIBufferReader reader) - { - var tag = reader.ReadI32(); - switch (tag) - { - case 0: - return new Empty(); - case 1: - { - var fRadius = reader.ReadF64(); - return new Circle(fRadius); - } - case 2: - { - var fWidth = reader.ReadF32(); - var fHeight = reader.ReadF32(); - return new Rectangle(fWidth, fHeight); - } - case 3: - { - var fMin = reader.ReadI16(); - var fMax = reader.ReadI16(); - var fWeight = reader.ReadU64(); - return new Bounded(fMin, fMax, fWeight); - } - case 4: - { - var fLabel = reader.ReadString(); - var fCount = reader.ReadU8(); - return new Labeled(fLabel, fCount); - } - default: - throw new InvalidOperationException("malformed WeaveFFI value buffer: unknown Shape tag " + tag); - } - } - } - - /// A plain C-style enum (no payloads) - public enum Channel - { - Red = 0, - Green = 1, - Blue = 2, - } - - /// One field per supported numeric primitive - public sealed class Numerics - { - public sbyte A { get; } - - public short B { get; } - - public int C { get; } - - public long D { get; } - - public byte E { get; } - - public ushort F { get; } - - public uint G { get; } - - public ulong H { get; } - - public float I { get; } - - public double J { get; } - - public Numerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j) - { - A = a; - B = b; - C = c; - D = d; - E = e; - F = f; - G = g; - H = h; - I = i; - J = j; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI8(A); - writer.WriteI16(B); - writer.WriteI32(C); - writer.WriteI64(D); - writer.WriteU8(E); - writer.WriteU16(F); - writer.WriteU32(G); - writer.WriteU64(H); - writer.WriteF32(I); - writer.WriteF64(J); - } - - internal static Numerics ReadFrom(WeaveFFIBufferReader reader) - { - var fA = reader.ReadI8(); - var fB = reader.ReadI16(); - var fC = reader.ReadI32(); - var fD = reader.ReadI64(); - var fE = reader.ReadU8(); - var fF = reader.ReadU16(); - var fG = reader.ReadU32(); - var fH = reader.ReadU64(); - var fI = reader.ReadF32(); - var fJ = reader.ReadF64(); - return new Numerics(fA, fB, fC, fD, fE, fF, fG, fH, fI, fJ); - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - // The ABI revision these bindings were generated against. - internal const uint AbiVersion = 2; - - // 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(); - - // Fills a producer-owned error slot with a copy of `message`; callback - // trampolines report a thrown exception through it so no managed - // allocation is ever handed to the producer's allocator. - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_set(IntPtr err, int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string message); - - [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_shapes_describe", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_shapes_describe(IntPtr shape_ptr, UIntPtr shape_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_shapes_scale", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_shapes_scale(IntPtr shape_ptr, UIntPtr shape_len, double factor, out UIntPtr out_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_shapes_sum_bytes", CallingConvention = CallingConvention.Cdecl)] - internal static extern ulong weaveffi_shapes_sum_bytes(IntPtr values_ptr, UIntPtr values_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_shapes_make_numerics", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_shapes_make_numerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j, out UIntPtr out_len, ref WeaveFFIError out_err); - - } - - public static class Shapes - { - /// Render a shape to a string - public static string Describe(Shape shape) - { - var err = new WeaveFFIError(); - var shapeWriter = new WeaveFFIBufferWriter(); - shape.WriteTo(shapeWriter); - var shapeBuf = shapeWriter.ToArray(); - var shapePin = GCHandle.Alloc(shapeBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_shapes_describe(shapePin.AddrOfPinnedObject(), (UIntPtr)shapeBuf.Length, ref err); - WeaveFFIError.Check(err); - var str = Marshal.PtrToStringUTF8(result); - NativeMethods.weaveffi_free_string(result); - return str ?? ""; - } - finally - { - shapePin.Free(); - } - } - - /// Scale a shape by a factor, returning a new shape - public static Shape Scale(Shape shape, double factor) - { - var err = new WeaveFFIError(); - var shapeWriter = new WeaveFFIBufferWriter(); - shape.WriteTo(shapeWriter); - var shapeBuf = shapeWriter.ToArray(); - var shapePin = GCHandle.Alloc(shapeBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_shapes_scale(shapePin.AddrOfPinnedObject(), (UIntPtr)shapeBuf.Length, factor, 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 = Shape.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - finally - { - shapePin.Free(); - } - } - - /// Sum a list of bytes into a wide integer (numerics smoke) - public static ulong SumBytes(byte[] values) - { - var err = new WeaveFFIError(); - var valuesPin = GCHandle.Alloc(values, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_shapes_sum_bytes(valuesPin.AddrOfPinnedObject(), (UIntPtr)values.Length, ref err); - WeaveFFIError.Check(err); - return result; - } - finally - { - valuesPin.Free(); - } - } - - /// Build a Numerics struct from every numeric primitive - public static Numerics MakeNumerics(sbyte a, short b, int c, long d, byte e, ushort f, uint g, ulong h, float i, double j) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, 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 = Numerics.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__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_shapes__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__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/go_docs_everywhere__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__README_md.snap index 7d3c4f86..a562400c 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__README_md.snap @@ -1,53 +1,49 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Go Bindings +# docs_everywhere (Go) -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`) +cgo bindings for the `docs_everywhere` native library. ## 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`: +The module ships its own copy of the C header (`docs_everywhere.h`) and links with +`-ldocs_everywhere`. Point the linker at the directory holding `libdocs_everywhere.so`, +`libdocs_everywhere.dylib`, or `docs_everywhere.dll`, and make the library findable at +run time: ```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" +export CGO_LDFLAGS="-L/path/to/lib" +export LD_LIBRARY_PATH="/path/to/lib" # DYLD_LIBRARY_PATH on macOS +go build ./... ``` -2. Build or run your Go project that imports this module: +Depend on the module from your own project with a `require` (and, for a +local checkout, a `replace`) of `docs_everywhere`, then import it: -```sh -go build ./... +```go +import "docs_everywhere" ``` -## 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. - -Interfaces are reference-counted objects: each wrapper holds one strong -reference that `Close()` releases (a finalizer releases it if you forget), and -a nullable interface is a nil wrapper pointer. Callback interfaces are Go -`interface` types: implement one and pass the value to any function that -accepts it; the native library may call it from any thread, and a panic in -your implementation is reported to the native caller instead of crashing. +Go 1.23 or newer is required. + +## Usage notes + +- Records are plain structs and rich enums are sealed interfaces; both cross + the boundary by value. +- Interfaces are reference-counted objects. Call `Close` when you're done + with one (a finalizer releases it otherwise); it's safe to call twice or + while a call is in flight. +- Async functions take a `context.Context` first. Cancellable ones cancel + the native call when the context is done. +- Callback interfaces are Go interfaces you implement. The library may call + them from any thread, and a panic is reported to the native caller as a + foreign error instead of crashing. +- Loading checks the library's ABI revision and contract checksums and + panics with the stale module's name on a mismatch. diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__bindings_go.snap new file mode 100644 index 00000000..4a756837 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__bindings_go.snap @@ -0,0 +1,383 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package docs_everywhere + +/* +#cgo LDFLAGS: -ldocs_everywhere +#include "docs_everywhere.h" +__attribute__((unused)) static void* wvHandlePtr(uintptr_t h) { return (void*)h; } +extern void goWv_docs_everywhere_docs_SavedListener_on_saved(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); +extern void goWv_docs_everywhere_docs_SavedListener_free(void* ctx); +static const docs_everywhere_docs_SavedListener_vtable wvVtable_docs_everywhere_docs_SavedListener_vtable = { + goWv_docs_everywhere_docs_SavedListener_on_saved, + goWv_docs_everywhere_docs_SavedListener_free, +}; +__attribute__((unused)) static const docs_everywhere_docs_SavedListener_vtable* wvVtablePtr_docs_everywhere_docs_SavedListener_vtable(void) { return &wvVtable_docs_everywhere_docs_SavedListener_vtable; } +extern void goWv_docs_everywhere_docs_publish_async_callback(void* context_, docs_everywhere_error* err, bool result); +*/ +import "C" + +import ( + "context" + "fmt" + "runtime" + "runtime/cgo" + "unsafe" +) + +func init() { + wvCheckABI() + wvCheckModule("docs", 0x7950eec1e9cfc819, uint64(C.docs_everywhere_docs_checksum())) +} + +// DocsErrorsError is a typed error reported by the `docs` module. +type DocsErrorsError struct { + // Code is the numeric ABI error code (one of the DocsErrorsError constants). + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *DocsErrorsError) Error() string { + return fmt.Sprintf("docs: %s (code %d)", e.Message, e.Code) +} + +// DocsErrorsError codes. +const ( + // DocsErrorsErrorNotFound: Returned when no document matches the requested id + DocsErrorsErrorNotFound int32 = 1 + // DocsErrorsErrorInvalidStatus: Raised when attempting to move into a forbidden state + DocsErrorsErrorInvalidStatus int32 = 2 +) + +// wvMapDocs converts a non-zero code from the `docs` domain into a +// *DocsErrorsError, falling back to the generic *Error for unknown codes. +func wvMapDocs(f wvFailure) error { + switch f.code { + case DocsErrorsErrorNotFound: + if f.message == "" { + f.message = "Document not found" + } + return &DocsErrorsError{Code: f.code, Message: f.message} + case DocsErrorsErrorInvalidStatus: + if f.message == "" { + f.message = "Invalid status transition" + } + return &DocsErrorsError{Code: f.code, Message: f.message} + default: + return f.err() + } +} + +// Status: Lifecycle status of a document. +// +// Used to drive UI badges and filters. +type Status int32 + +const ( + // StatusDraft: Document is being edited + StatusDraft Status = 0 + // StatusPublished: Document is publicly visible + StatusPublished Status = 1 + // StatusArchived: Document is hidden from listings + StatusArchived Status = 2 +) + +// Document: Represents a single document tracked by the system. +// +// Documents are persisted to disk and exposed via the public API. +type Document struct { + // Id: Stable opaque identifier + Id int64 + // Title: Human-readable title shown in lists + Title string + // Status: Current lifecycle status + Status Status + // Tags: Free-form tags for grouping + Tags []string +} + +// wvPackDocument appends v to w in the value-buffer wire format. +func wvPackDocument(w *wvWriter, v Document) { + w.writeI64(v.Id) + w.writeString(v.Title) + w.writeI32(int32(v.Status)) + w.writeLen(len(v.Tags)) + for _, eTags0 := range v.Tags { + w.writeString(eTags0) + } +} + +// wvUnpackDocument decodes one Document from r. +func wvUnpackDocument(r *wvReader) Document { + var v Document + v.Id = r.readI64() + v.Title = r.readString() + v.Status = Status(r.readI32()) + nTags0 := r.readLen() + v.Tags = make([]string, 0, r.capHint(nTags0)) + for range nTags0 { + var eTags0 string + eTags0 = r.readString() + v.Tags = append(v.Tags, eTags0) + } + return v +} + +// SavedListener: Consumer-implemented observer of document-saved notifications. +// +// Implementations must be safe to call from any thread. +// +// Implement this interface in Go and pass the value to native functions +// that accept it. +// +// The native library may call any method from any thread until it releases +// the implementation. A panic in a method is reported to the native caller +// as a foreign error (code -4) instead of crashing the process. +type SavedListener interface { + // OnSaved: Fires after a document has been persisted successfully + // + // Parameters: + // - doc_id: Identifier of the saved document + // - revision: Monotonically increasing revision + OnSaved(docId int64, revision int32) +} + +//export goWv_docs_everywhere_docs_SavedListener_on_saved +func goWv_docs_everywhere_docs_SavedListener_on_saved(ctx unsafe.Pointer, doc_id C.int64_t, revision C.int32_t, out_err *C.docs_everywhere_error) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(SavedListener) + arg0 := int64(doc_id) + arg1 := int32(revision) + impl.OnSaved(arg0, arg1) +} + +//export goWv_docs_everywhere_docs_SavedListener_free +func goWv_docs_everywhere_docs_SavedListener_free(ctx unsafe.Pointer) { + cgo.Handle(uintptr(ctx)).Delete() +} + +// Notebook: An ordered collection of documents. +// +// Notebooks own their documents and release them when destroyed. +// +// Each wrapper holds one strong reference; Close releases it, and a +// finalizer releases it if the wrapper is garbage collected first. +type Notebook struct { + ref wvRef +} + +func wvDestroyNotebook(ptr unsafe.Pointer) { + C.docs_everywhere_docs_Notebook_destroy((*C.docs_everywhere_docs_Notebook)(ptr)) +} + +// wvAdoptNotebook adopts one owned strong reference into a new wrapper. A null +// pointer adopts to nil. +func wvAdoptNotebook(ptr *C.docs_everywhere_docs_Notebook) *Notebook { + if ptr == nil { + return nil + } + s := &Notebook{} + s.ref.init(unsafe.Pointer(ptr), wvDestroyNotebook) + runtime.SetFinalizer(s, (*Notebook).Close) + return s +} + +// native borrows the wrapper's pointer for one call; pair it with a +// deferred s.ref.release(). +func (s *Notebook) native() *C.docs_everywhere_docs_Notebook { + if s == nil { + panic("docs_everywhere: nil *Notebook") + } + return (*C.docs_everywhere_docs_Notebook)(s.ref.acquire("Notebook")) +} + +// wvTokenNotebook clones o's reference into a value-buffer object token. The +// wrapper keeps its own reference; the token carries the new one. +func wvTokenNotebook(o *Notebook) uint64 { + ptr := o.native() + defer o.ref.release() + return uint64(uintptr(unsafe.Pointer(C.docs_everywhere_docs_Notebook_clone(ptr)))) +} + +// wvUntokenNotebook adopts the strong reference carried by a value-buffer object +// token. +func wvUntokenNotebook(token uint64) *Notebook { + if token == 0 { + panic("docs_everywhere: malformed value buffer: null object token") + } + return wvAdoptNotebook((*C.docs_everywhere_docs_Notebook)(C.wvHandlePtr(C.uintptr_t(token)))) +} + +// Close releases the wrapper's strong reference and always returns nil. +// It's idempotent and safe to call from any goroutine, even while a call +// on the wrapper is in flight: the reference is then released when that +// call returns. The object itself is dropped once its last reference, from +// any wrapper, record, or the native side, is gone. +func (s *Notebook) Close() error { + runtime.SetFinalizer(s, nil) + s.ref.close() + return nil +} + +// NewNotebook: Create an empty notebook +func NewNotebook() *Notebook { + var cErr C.docs_everywhere_error + cRet := C.docs_everywhere_docs_Notebook_new(&cErr) + wvTrap(&cErr) + return wvAdoptNotebook(cRet) +} + +// Insert: Add a document, returning its position in the notebook +// +// Parameters: +// - document: The document to add +func (s *Notebook) Insert(document Document) int32 { + cSelf := s.native() + defer s.ref.release() + wDocument := &wvWriter{} + wvPackDocument(wDocument, document) + cDocumentPtr, cDocumentLen := wvBytes(wDocument.buf) + var cErr C.docs_everywhere_error + cRet := C.docs_everywhere_docs_Notebook_insert(cSelf, cDocumentPtr, cDocumentLen, &cErr) + wvTrap(&cErr) + return int32(cRet) +} + +// Find: Look up a document by identifier, failing when none matches +// +// Parameters: +// - id: Identifier of the document to find +func (s *Notebook) Find(id int64) (Document, error) { + cSelf := s.native() + defer s.ref.release() + var cRetLen C.size_t + var cErr C.docs_everywhere_error + cRet := C.docs_everywhere_docs_Notebook_find(cSelf, C.int64_t(id), &cRetLen, &cErr) + if cErr.code != 0 { + return Document{}, wvMapDocs(wvTakeError(&cErr)) + } + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Document + ret = wvUnpackDocument(rRet) + rRet.expectEnd() + return ret, nil +} + +// Size: The number of documents currently held +func (s *Notebook) Size() int32 { + cSelf := s.native() + defer s.ref.release() + var cErr C.docs_everywhere_error + cRet := C.docs_everywhere_docs_Notebook_size(cSelf, &cErr) + wvTrap(&cErr) + return int32(cRet) +} + +// NotebookMaxDocuments: The largest number of documents one notebook will hold +func NotebookMaxDocuments() int32 { + var cErr C.docs_everywhere_error + cRet := C.docs_everywhere_docs_Notebook_max_documents(&cErr) + wvTrap(&cErr) + return int32(cRet) +} + +// CreateDocument: Create a brand new document. +// +// The document starts in `Draft` status; callers must publish +// it explicitly to make it visible. +// +// Parameters: +// - title: Human-readable title for the new document +// - tags: Initial set of tags (may be empty) +func CreateDocument(title string, tags []string) Document { + cTitlePtr, cTitleLen := wvStr(title) + wTags := &wvWriter{} + wTags.writeLen(len(tags)) + for _, eTags0 := range tags { + wTags.writeString(eTags0) + } + cTagsPtr, cTagsLen := wvBytes(wTags.buf) + var cRetLen C.size_t + var cErr C.docs_everywhere_error + cRet := C.docs_everywhere_docs_create_document(cTitlePtr, cTitleLen, cTagsPtr, cTagsLen, &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Document + ret = wvUnpackDocument(rRet) + rRet.expectEnd() + return ret +} + +// GetDocument: Look up a document by identifier +// +// Parameters: +// - id: Identifier returned from create_document +func GetDocument(id int64) *Document { + var cRetLen C.size_t + var cErr C.docs_everywhere_error + cRet := C.docs_everywhere_docs_get_document(C.int64_t(id), &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret *Document + if rRet.readOptionFlag() { + var oRet0 Document + oRet0 = wvUnpackDocument(rRet) + ret = &oRet0 + } + rRet.expectEnd() + return ret +} + +//export goWv_docs_everywhere_docs_publish_async_callback +func goWv_docs_everywhere_docs_publish_async_callback(context_ unsafe.Pointer, err *C.docs_everywhere_error, result C._Bool) { + wvComplete(context_, err, func() bool { + return bool(result) + }) +} + +// PublishAsync: Publish a document in the background +// +// Parameters: +// - id: Identifier of the document to publish +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func PublishAsync(ctx context.Context, id int64) (bool, error) { + if err := ctx.Err(); err != nil { + return false, err + } + wvDone := make(chan wvOutcome[bool], 1) + wvHandle := cgo.NewHandle(wvDone) + C.docs_everywhere_docs_publish_async(C.int64_t(id), C.docs_everywhere_docs_publish_async_callback(unsafe.Pointer(C.goWv_docs_everywhere_docs_publish_async_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return false, err + } + if fail != nil { + panic(fail.err()) + } + return res, nil +} + +// Watch: Register a listener for saved notifications +// +// Parameters: +// - listener: Receiver of saved notifications +func Watch(listener SavedListener) { + hListener := cgo.NewHandle(listener) + var cErr C.docs_everywhere_error + C.docs_everywhere_docs_watch(C.wvHandlePtr(C.uintptr_t(hListener)), C.wvVtablePtr_docs_everywhere_docs_SavedListener_vtable(), &cErr) + wvTrap(&cErr) +} + +// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__codec_go.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__codec_go.snap new file mode 100644 index 00000000..0db665ad --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__codec_go.snap @@ -0,0 +1,205 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package docs_everywhere + +import ( + "encoding/binary" + "math" + "unicode/utf8" +) + +// wvWriter serializes values into the value-buffer format: little-endian, +// packed, u32 length prefixes. +type wvWriter struct { + buf []byte +} + +func (w *wvWriter) writeBool(v bool) { + if v { + w.buf = append(w.buf, 1) + } else { + w.buf = append(w.buf, 0) + } +} + +func (w *wvWriter) writeI8(v int8) { + w.buf = append(w.buf, byte(v)) +} + +func (w *wvWriter) writeU8(v uint8) { + w.buf = append(w.buf, v) +} + +func (w *wvWriter) writeI16(v int16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v)) +} + +func (w *wvWriter) writeU16(v uint16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, v) +} + +func (w *wvWriter) writeI32(v int32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v)) +} + +func (w *wvWriter) writeU32(v uint32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, v) +} + +func (w *wvWriter) writeI64(v int64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v)) +} + +func (w *wvWriter) writeU64(v uint64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, v) +} + +func (w *wvWriter) writeF32(v float32) { + w.writeU32(math.Float32bits(v)) +} + +func (w *wvWriter) writeF64(v float64) { + w.writeU64(math.Float64bits(v)) +} + +func (w *wvWriter) writeLen(n int) { + if n < 0 || uint64(n) > math.MaxUint32 { + panic("docs_everywhere: 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 value-buffer format. A malformed buffer +// is a producer/consumer contract violation, so every read panics instead of +// returning a typed domain error. +type wvReader struct { + buf []byte + pos int +} + +func wvMalformed(context string) { + panic("docs_everywhere: malformed value buffer: " + context) +} + +func (r *wvReader) take(n int, context string) []byte { + if n < 0 || len(r.buf)-r.pos < n { + wvMalformed(context) + } + b := r.buf[r.pos : r.pos+n] + r.pos += n + return b +} + +func (r *wvReader) readBool() bool { + switch r.take(1, "bool")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("bool byte out of range") + return false +} + +func (r *wvReader) readI8() int8 { + return int8(r.take(1, "i8")[0]) +} + +func (r *wvReader) readU8() uint8 { + return r.take(1, "u8")[0] +} + +func (r *wvReader) readI16() int16 { + return int16(binary.LittleEndian.Uint16(r.take(2, "i16"))) +} + +func (r *wvReader) readU16() uint16 { + return binary.LittleEndian.Uint16(r.take(2, "u16")) +} + +func (r *wvReader) readI32() int32 { + return int32(binary.LittleEndian.Uint32(r.take(4, "i32"))) +} + +func (r *wvReader) readU32() uint32 { + return binary.LittleEndian.Uint32(r.take(4, "u32")) +} + +func (r *wvReader) readI64() int64 { + return int64(binary.LittleEndian.Uint64(r.take(8, "i64"))) +} + +func (r *wvReader) readU64() uint64 { + return binary.LittleEndian.Uint64(r.take(8, "u64")) +} + +func (r *wvReader) readF32() float32 { + return math.Float32frombits(r.readU32()) +} + +func (r *wvReader) readF64() float64 { + return math.Float64frombits(r.readU64()) +} + +// readLen reads a u32 element count. Elements may encode to zero bytes, so +// the count isn't checked against the remaining buffer; callers cap their +// preallocation with capHint instead. +func (r *wvReader) readLen() int { + return int(r.readU32()) +} + +// capHint bounds a preallocation for n decoded elements by the bytes left in +// the buffer, so a corrupt count can't force a huge allocation. +func (r *wvReader) capHint(n int) int { + return min(n, len(r.buf)-r.pos) +} + +func (r *wvReader) readString() string { + b := r.take(r.readLen(), "string bytes") + if !utf8.Valid(b) { + wvMalformed("string is not valid UTF-8") + } + return string(b) +} + +func (r *wvReader) readBytes() []byte { + return append([]byte{}, r.take(r.readLen(), "byte buffer")...) +} + +func (r *wvReader) readOptionFlag() bool { + switch r.take(1, "option flag")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("option flag byte out of range") + return false +} + +func (r *wvReader) expectEnd() { + if r.pos != len(r.buf) { + wvMalformed("trailing bytes after value") + } +} + +// END codec.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__docs_everywhere_h.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__docs_everywhere_h.snap new file mode 100644 index 00000000..31ab85e2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__docs_everywhere_h.snap @@ -0,0 +1,196 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef DOCS_EVERYWHERE_H +#define DOCS_EVERYWHERE_H + +#include +#include +#include + +#ifndef DOCS_EVERYWHERE_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef DOCS_EVERYWHERE_BUILD +# define DOCS_EVERYWHERE_API __declspec(dllexport) +# else +# define DOCS_EVERYWHERE_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define DOCS_EVERYWHERE_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define DOCS_EVERYWHERE_API __attribute__((visibility("default"))) +# else +# define DOCS_EVERYWHERE_API +# endif +#endif + +#ifndef DOCS_EVERYWHERE_DEPRECATED +# if defined(_MSC_VER) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define DOCS_EVERYWHERE_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports docs_everywhere_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define DOCS_EVERYWHERE_ABI_VERSION 3u +DOCS_EVERYWHERE_API uint32_t docs_everywhere_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by docs_everywhere_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct docs_everywhere_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} docs_everywhere_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_set(docs_everywhere_error* err, int32_t code, const char* message); +DOCS_EVERYWHERE_API void docs_everywhere_error_clear(docs_everywhere_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + docs_everywhere_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_free(docs_everywhere_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +DOCS_EVERYWHERE_API void docs_everywhere_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct docs_everywhere_cancel_token docs_everywhere_cancel_token; +DOCS_EVERYWHERE_API docs_everywhere_cancel_token* docs_everywhere_cancel_token_create(void); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_cancel(docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API bool docs_everywhere_cancel_token_is_cancelled(const docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_destroy(docs_everywhere_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +DOCS_EVERYWHERE_API uint8_t* docs_everywhere_alloc(uint32_t size); +DOCS_EVERYWHERE_API void docs_everywhere_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +DOCS_EVERYWHERE_API uint64_t docs_everywhere_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define DOCS_EVERYWHERE_DOCS_CHECKSUM 0x7950eec1e9cfc819ull +DOCS_EVERYWHERE_API uint64_t docs_everywhere_docs_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with docs_everywhere_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * docs_everywhere_buffer.h declares a C struct and codec for each. + */ + +/** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ +typedef enum { + /** Document is being edited */ + docs_everywhere_docs_Status_Draft = 0, + /** Document is publicly visible */ + docs_everywhere_docs_Status_Published = 1, + /** Document is hidden from listings */ + docs_everywhere_docs_Status_Archived = 2 +} docs_everywhere_docs_Status; +/** Error codes reported by throwing functions in the `docs` module tree. */ +typedef enum { + /** Returned when no document matches the requested id */ + docs_everywhere_docs_DocsErrors_not_found = 1, + /** Raised when attempting to move into a forbidden state */ + docs_everywhere_docs_DocsErrors_invalid_status = 2 +} docs_everywhere_docs_DocsErrors; +typedef struct docs_everywhere_docs_Notebook docs_everywhere_docs_Notebook; +/** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + * + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct docs_everywhere_docs_SavedListener_vtable { + /** Fires after a document has been persisted successfully */ + void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); + void (*free)(void* ctx); +} docs_everywhere_docs_SavedListener_vtable; +typedef void (*docs_everywhere_docs_publish_async_callback)(void* context, docs_everywhere_error* err, bool result); + +// Module: docs +/** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ +/** Create an empty notebook */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_new(docs_everywhere_error* out_err); +/** The largest number of documents one notebook will hold */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_max_documents(docs_everywhere_error* out_err); +/** Add a document, returning its position in the notebook */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_insert(const docs_everywhere_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, docs_everywhere_error* out_err); +/** Look up a document by identifier, failing when none matches */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_Notebook_find(const docs_everywhere_docs_Notebook* self, int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** The number of documents currently held */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_size(const docs_everywhere_docs_Notebook* self, docs_everywhere_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_clone(const docs_everywhere_docs_Notebook* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_Notebook_destroy(docs_everywhere_docs_Notebook* self); + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_create_document(const uint8_t* title_ptr, size_t title_len, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, docs_everywhere_error* out_err); +/** Look up a document by identifier */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_get_document(int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** Publish a document in the background */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_publish_async(int64_t id, docs_everywhere_docs_publish_async_callback callback, void* context); +/** Register a listener for saved notifications */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_watch(void* listener_ctx, const docs_everywhere_docs_SavedListener_vtable* listener_vtable, docs_everywhere_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // DOCS_EVERYWHERE_H + +// END docs_everywhere.h diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__go_mod.snap index 73a087a7..3e527b48 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__go_mod.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__go_mod.snap @@ -1,11 +1,11 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -module weaveffi +module docs_everywhere go 1.23 diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__runtime_go.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__runtime_go.snap new file mode 100644 index 00000000..6fd7b814 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__runtime_go.snap @@ -0,0 +1,305 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package docs_everywhere + +/* +#include +#include "docs_everywhere.h" +*/ +import "C" + +import ( + "context" + "fmt" + "runtime/cgo" + "sync/atomic" + "unsafe" +) + +// wvABIVersion is the C ABI revision these bindings were generated against. +const wvABIVersion uint32 = 3 + +// The runtime error codes this package produces or interprets itself. +const ( + wvCodeMarshal int32 = -3 + wvCodeForeign int32 = -4 + wvCodeCancelled int32 = -5 +) + +// Error reports a failure that no typed error domain claims: an unknown +// code, a generic failure (code -1), a producer panic (-2), a marshalling +// failure (-3), or a Go callback-interface implementation that panicked +// (-4). The code is preserved so callers can tell them apart. +type Error struct { + // Code is the numeric ABI error code. + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *Error) Error() string { + return fmt.Sprintf("docs_everywhere: %s (code %d)", e.Message, e.Code) +} + +// DebugLive reports how many resources of one kind the native library holds: +// 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned +// allocations. It's always 0 unless the library was built with leak checks +// enabled; tests use it to assert that nothing leaked. +func DebugLive(kind int32) uint64 { + return uint64(C.docs_everywhere_debug_live(C.int32_t(kind))) +} + +// wvCheckABI panics when the linked library implements a different C ABI +// revision than these bindings were generated for. +func wvCheckABI() { + if found := uint32(C.docs_everywhere_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("docs_everywhere: ABI mismatch: these bindings expect revision %d but the linked library reports revision %d", wvABIVersion, found)) + } +} + +// wvCheckModule panics when the linked library's contract checksum for a +// top-level module differs from the one these bindings were generated +// against, which means the bindings are stale. +func wvCheckModule(module string, want, found uint64) { + if found != want { + panic(fmt.Sprintf("docs_everywhere: module %q doesn't match the linked library (bindings checksum %#016x, library checksum %#016x); regenerate the bindings", module, want, found)) + } +} + +// ── Errors ── + +// wvFailure is a non-zero error slot copied into Go memory. +type wvFailure struct { + code int32 + message string + payload []byte +} + +// err converts the failure into the generic *Error. +func (f wvFailure) err() error { + return &Error{Code: f.code, Message: f.message} +} + +func wvCopyFailure(cErr *C.docs_everywhere_error) wvFailure { + f := wvFailure{code: int32(cErr.code)} + if cErr.message != nil { + f.message = C.GoString(cErr.message) + } + if cErr.payload_ptr != nil { + f.payload = wvBorrowBytes(cErr.payload_ptr, cErr.payload_len) + } + return f +} + +// wvTakeError copies a non-zero error slot and clears it. +func wvTakeError(cErr *C.docs_everywhere_error) wvFailure { + f := wvCopyFailure(cErr) + C.docs_everywhere_error_clear(cErr) + return f +} + +// wvTakeBoxedError copies the heap-boxed error an async completion receives +// and frees the box. +func wvTakeBoxedError(cErr *C.docs_everywhere_error) wvFailure { + f := wvCopyFailure(cErr) + C.docs_everywhere_error_free(cErr) + return f +} + +// wvTrap panics with the generic *Error when the slot reports a failure. +// Wrappers of functions that declare no error domain check their slot with +// it: a failure there can only be a producer bug, a marshalling failure, or +// a callback-interface implementation that panicked. +func wvTrap(cErr *C.docs_everywhere_error) { + if cErr.code != 0 { + panic(wvTakeError(cErr).err()) + } +} + +// wvForeignError reports a panic recovered inside a callback-interface +// trampoline through the producer's error slot (code -4). The producer +// copies the message, so it's only borrowed for the call. +func wvForeignError(outErr *C.docs_everywhere_error, recovered any) { + msg := C.CString(fmt.Sprint(recovered)) + defer C.free(unsafe.Pointer(msg)) + C.docs_everywhere_error_set(outErr, C.int32_t(wvCodeForeign), msg) +} + +// ── Strings and bytes ── + +// wvStr returns a borrowed (pointer, length) view of s's UTF-8 bytes for the +// duration of one call. Strings may contain NUL bytes. +func wvStr(s string) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(s))), C.size_t(len(s)) +} + +// wvBytes returns a borrowed (pointer, length) view of b for the duration of +// one call. +func wvBytes(b []byte) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.SliceData(b))), C.size_t(len(b)) +} + +// wvBorrowString copies a borrowed UTF-8 run into a Go string. +func wvBorrowString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + return string(unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) +} + +// wvTakeString copies a returned UTF-8 run into a Go string and releases the +// producer's allocation. +func wvTakeString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + s := wvBorrowString(ptr, n) + C.docs_everywhere_free_bytes(ptr, n) + return s +} + +// wvBorrowBytes copies a borrowed byte run into Go memory. A null pointer +// copies to nil. +func wvBorrowBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + return append([]byte{}, unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)...) +} + +// wvTakeBytes copies a returned byte run (raw bytes or a value buffer) into +// Go memory and releases the producer's allocation. +func wvTakeBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + b := wvBorrowBytes(ptr, n) + C.docs_everywhere_free_bytes(ptr, n) + return b +} + +// ── Objects ── + +// wvRef owns one strong reference to a native object on behalf of a wrapper. +// Calls borrow the pointer between acquire and release. Close may race them: +// the reference is destroyed exactly once, by close when no call is in +// flight, or else by the last in-flight call to finish. +type wvRef struct { + ptr unsafe.Pointer + destroy func(unsafe.Pointer) + // state counts in-flight calls in its upper bits; the low bit is set + // once close has run. + state atomic.Uint64 +} + +func (r *wvRef) init(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { + r.ptr = ptr + r.destroy = destroy +} + +// acquire borrows the pointer for one call, panicking when the wrapper was +// already closed. +func (r *wvRef) acquire(typeName string) unsafe.Pointer { + for { + s := r.state.Load() + if s&1 != 0 { + panic("docs_everywhere: " + typeName + " used after Close") + } + if r.state.CompareAndSwap(s, s+2) { + return r.ptr + } + } +} + +// release ends one call's borrow. +func (r *wvRef) release() { + if r.state.Add(^uint64(1)) == 1 { + r.destroy(r.ptr) + } +} + +// close releases the wrapper's reference once no call is in flight. Calling +// it again is a no-op. +func (r *wvRef) close() { + for { + s := r.state.Load() + if s&1 != 0 { + return + } + if r.state.CompareAndSwap(s, s|1) { + if s == 0 { + r.destroy(r.ptr) + } + return + } + } +} + +// ── Async ── + +// wvOutcome is one async completion: a converted result, or the failure the +// producer reported. +type wvOutcome[T any] struct { + val T + fail *wvFailure +} + +// wvComplete delivers one async completion to the waiting wrapper. It runs on +// a producer thread inside an exported trampoline, so it never panics: a +// result that fails to decode is delivered as a marshalling failure. +func wvComplete[T any](handle unsafe.Pointer, cErr *C.docs_everywhere_error, convert func() T) { + h := cgo.Handle(uintptr(handle)) + done := h.Value().(chan wvOutcome[T]) + h.Delete() + done <- wvSettle(cErr, convert) +} + +func wvSettle[T any](cErr *C.docs_everywhere_error, convert func() T) (o wvOutcome[T]) { + if cErr != nil { + if f := wvTakeBoxedError(cErr); f.code != 0 { + o.fail = &f + return o + } + } + defer func() { + if r := recover(); r != nil { + o = wvOutcome[T]{fail: &wvFailure{code: wvCodeMarshal, message: fmt.Sprint(r)}} + } + }() + o.val = convert() + return o +} + +// wvAwait blocks until an async call completes or ctx is done. With a cancel +// token, ctx's cancellation cancels the native call and waits for its +// completion, which the producer delivers promptly; without one, the call is +// abandoned and ctx.Err() returned at once. A cancelled completion (code -5) +// returns ctx.Err(), or context.Canceled when ctx itself isn't done. +func wvAwait[T any](ctx context.Context, done chan wvOutcome[T], token *C.docs_everywhere_cancel_token) (T, *wvFailure, error) { + var o wvOutcome[T] + select { + case o = <-done: + case <-ctx.Done(): + if token == nil { + var zero T + return zero, nil, ctx.Err() + } + C.docs_everywhere_cancel_token_cancel(token) + o = <-done + } + if o.fail != nil && o.fail.code == wvCodeCancelled { + var zero T + if err := ctx.Err(); err != nil { + return zero, nil, err + } + return zero, nil, context.Canceled + } + return o.val, o.fail, nil +} + +// END runtime.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap deleted file mode 100644 index b41bc717..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap +++ /dev/null @@ -1,678 +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 -static void* wvHandlePtr(uintptr_t h) { return (void*)h; } -extern void goWv_weaveffi_docs_SavedListener_on_saved(void* ctx, int64_t doc_id, int32_t revision, weaveffi_error* out_err); -extern void goWv_weaveffi_docs_SavedListener_free(void* ctx); -static const weaveffi_docs_SavedListener_vtable wvVtable_weaveffi_docs_SavedListener = { - goWv_weaveffi_docs_SavedListener_on_saved, - goWv_weaveffi_docs_SavedListener_free, -}; -static const weaveffi_docs_SavedListener_vtable* wvVtablePtr_weaveffi_docs_SavedListener(void) { return &wvVtable_weaveffi_docs_SavedListener; } -extern void goWv_weaveffi_docs_publish_async_callback(void* context, weaveffi_error* err, bool result); -*/ -import "C" - -import ( - "encoding/binary" - "fmt" - "math" - "runtime" - "runtime/cgo" - "unicode/utf8" - "unsafe" -) - -// The ABI revision these bindings were generated against. -const wvABIVersion uint32 = 2 - -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, a -// producer panic (code -2), or a Go callback-interface implementation -// that panicked (code -4). -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, a marshalling failure, or a callback-interface -// implementation that panicked. The panic value is a *WeaveFFIError, so a -// recover() site can still inspect the code. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - panic(wvBrandError(wvTakeError(cErr))) - } -} - -// 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)) -} - -// wvForeignError reports a panic recovered inside a callback-interface -// trampoline through the producer's error slot (code -4). The message is -// borrowed for the call: the producer copies it with its own allocator. -func wvForeignError(outErr *C.weaveffi_error, recovered any) { - msg := C.CString(fmt.Sprint(recovered)) - defer C.free(unsafe.Pointer(msg)) - C.weaveffi_error_set(outErr, -4, msg) -} - -// DocsErrorsError is a typed error reported by the `docs` module. -type DocsErrorsError struct { - // Code is the numeric ABI error code (one of the DocsErrorsError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *DocsErrorsError) Error() string { - return fmt.Sprintf("docs: %s (code %d)", e.Message, e.Code) -} - -// DocsErrorsError codes. -const ( - // DocsErrorsErrorNotFound: Returned when no document matches the requested id - DocsErrorsErrorNotFound int32 = 1 - // DocsErrorsErrorInvalidStatus: Raised when attempting to move into a forbidden state - DocsErrorsErrorInvalidStatus int32 = 2 -) - -// wvMapDocs converts a non-zero code from the `docs` domain into a -// *DocsErrorsError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapDocs(code int32, message string, payload []byte) error { - switch code { - case DocsErrorsErrorNotFound: - if message == "" { - message = "Document not found" - } - return &DocsErrorsError{Code: code, Message: message} - case DocsErrorsErrorInvalidStatus: - if message == "" { - message = "Invalid status transition" - } - return &DocsErrorsError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -// Status: Lifecycle status of a document. -// -// Used to drive UI badges and filters. -type Status int32 - -const ( - // StatusDraft: Document is being edited - StatusDraft Status = 0 - // StatusPublished: Document is publicly visible - StatusPublished Status = 1 - // StatusArchived: Document is hidden from listings - StatusArchived Status = 2 -) - -// Document: Represents a single document tracked by the system. -// -// Documents are persisted to disk and exposed via the public API. -type Document struct { - // Id: Stable opaque identifier - Id int64 - // Title: Human-readable title shown in lists - Title string - // Status: Current lifecycle status - Status Status - // Tags: Free-form tags for grouping - Tags []string -} - -// wvPackDocument appends v to w in the value-buffer wire format. -func wvPackDocument(w *wvWriter, v Document) { - w.writeI64(v.Id) - w.writeString(v.Title) - w.writeI32(int32(v.Status)) - w.writeLen(len(v.Tags)) - for _, eTags0 := range v.Tags { - w.writeString(eTags0) - } -} - -// wvUnpackDocument decodes one Document from r. -func wvUnpackDocument(r *wvReader) Document { - var v Document - v.Id = r.readI64() - v.Title = r.readString() - v.Status = Status(r.readI32()) - nTags0 := r.readLen() - v.Tags = make([]string, nTags0) - for iTags0 := range v.Tags { - v.Tags[iTags0] = r.readString() - } - return v -} - -// SavedListener: Consumer-implemented observer of document-saved notifications. -// -// Implementations must be safe to call from any thread. -// -// Implement this interface in Go and pass the value to native functions -// that accept it. -// -// The native library may call any method from any thread until it releases -// the implementation. A panic in a method is reported to the native caller -// as a foreign error (code -4) instead of crashing the process. -type SavedListener interface { - // OnSaved: Fires after a document has been persisted successfully - // - // Parameters: - // - doc_id: Identifier of the saved document - // - revision: Monotonically increasing revision - OnSaved(docId int64, revision int32) -} - -//export goWv_weaveffi_docs_SavedListener_on_saved -func goWv_weaveffi_docs_SavedListener_on_saved(ctx unsafe.Pointer, doc_id C.int64_t, revision C.int32_t, out_err *C.weaveffi_error) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(SavedListener) - arg0 := int64(doc_id) - arg1 := int32(revision) - impl.OnSaved(arg0, arg1) -} - -//export goWv_weaveffi_docs_SavedListener_free -func goWv_weaveffi_docs_SavedListener_free(ctx unsafe.Pointer) { - cgo.Handle(uintptr(ctx)).Delete() -} - -// Notebook: An ordered collection of documents. -// -// Notebooks own their documents and release them when destroyed. -// -// Each wrapper holds one strong reference; Close releases it, and a -// finalizer releases it if the wrapper is garbage collected first. -type Notebook struct { - ptr *C.weaveffi_docs_Notebook -} - -// wvAdoptNotebook adopts one owned strong reference into a new wrapper. A null -// pointer adopts to nil. -func wvAdoptNotebook(ptr *C.weaveffi_docs_Notebook) *Notebook { - if ptr == nil { - return nil - } - s := &Notebook{ptr: ptr} - runtime.SetFinalizer(s, (*Notebook).Close) - return s -} - -// wvTokenNotebook clones o's reference into a value-buffer object token. The -// wrapper keeps its own reference; the token carries the new one. -func wvTokenNotebook(o *Notebook) uint64 { - if o == nil || o.ptr == nil { - panic("weaveffi: nil or closed Notebook cannot be encoded in a non-optional position") - } - return uint64(uintptr(unsafe.Pointer(C.weaveffi_docs_Notebook_clone(o.ptr)))) -} - -// wvUntokenNotebook adopts the strong reference carried by a value-buffer object -// token. -func wvUntokenNotebook(token uint64) *Notebook { - if token == 0 { - panic("weaveffi: malformed value buffer: null object token") - } - return wvAdoptNotebook((*C.weaveffi_docs_Notebook)(C.wvHandlePtr(C.uintptr_t(token)))) -} - -// NewNotebook: Create an empty notebook -func NewNotebook() *Notebook { - var cErr C.weaveffi_error - result := C.weaveffi_docs_Notebook_new(&cErr) - wvTrap(&cErr) - return wvAdoptNotebook(result) -} - -// Insert: Add a document, returning its position in the notebook -// -// Parameters: -// - document: The document to add -func (s *Notebook) Insert(document Document) int32 { - if s.ptr == nil { - panic("weaveffi: Notebook used after Close") - } - defer runtime.KeepAlive(s) - wDocument := &wvWriter{} - wvPackDocument(wDocument, document) - var cDocumentPtr *C.uint8_t - if len(wDocument.buf) > 0 { - cDocumentPtr = (*C.uint8_t)(unsafe.Pointer(&wDocument.buf[0])) - } - var cErr C.weaveffi_error - result := C.weaveffi_docs_Notebook_insert(s.ptr, cDocumentPtr, C.size_t(len(wDocument.buf)), &cErr) - wvTrap(&cErr) - return int32(result) -} - -// Find: Look up a document by identifier, failing when none matches -// -// Parameters: -// - id: Identifier of the document to find -func (s *Notebook) Find(id int64) (Document, error) { - if s.ptr == nil { - panic("weaveffi: Notebook used after Close") - } - defer runtime.KeepAlive(s) - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_docs_Notebook_find(s.ptr, C.int64_t(id), &cOutLen, &cErr) - if cErr.code != 0 { - return Document{}, wvMapDocs(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Document - goResult = wvUnpackDocument(rRes) - rRes.expectEnd() - return goResult, nil -} - -// Size: The number of documents currently held -func (s *Notebook) Size() int32 { - if s.ptr == nil { - panic("weaveffi: Notebook used after Close") - } - defer runtime.KeepAlive(s) - var cErr C.weaveffi_error - result := C.weaveffi_docs_Notebook_size(s.ptr, &cErr) - wvTrap(&cErr) - return int32(result) -} - -// NotebookMaxDocuments: The largest number of documents one notebook will hold -func NotebookMaxDocuments() int32 { - var cErr C.weaveffi_error - result := C.weaveffi_docs_Notebook_max_documents(&cErr) - wvTrap(&cErr) - return int32(result) -} - -// Close releases the wrapper's strong reference. Calling it more than once -// is harmless; the object itself is dropped by the native library when its -// last reference (from any wrapper, record, or producer) is released. -func (s *Notebook) Close() { - if s.ptr != nil { - C.weaveffi_docs_Notebook_destroy(s.ptr) - s.ptr = nil - runtime.SetFinalizer(s, nil) - } -} - -// CreateDocument: Create a brand new document. -// -// The document starts in `Draft` status; callers must publish -// it explicitly to make it visible. -// -// Parameters: -// - title: Human-readable title for the new document -// - tags: Initial set of tags (may be empty) -func CreateDocument(title string, tags []string) Document { - cTitle := C.CString(title) - defer C.free(unsafe.Pointer(cTitle)) - wTags := &wvWriter{} - wTags.writeLen(len(tags)) - for _, eTags0 := range tags { - wTags.writeString(eTags0) - } - var cTagsPtr *C.uint8_t - if len(wTags.buf) > 0 { - cTagsPtr = (*C.uint8_t)(unsafe.Pointer(&wTags.buf[0])) - } - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_docs_create_document(cTitle, cTagsPtr, C.size_t(len(wTags.buf)), &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Document - goResult = wvUnpackDocument(rRes) - rRes.expectEnd() - return goResult -} - -// GetDocument: Look up a document by identifier -// -// Parameters: -// - id: Identifier returned from create_document -func GetDocument(id int64) *Document { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_docs_get_document(C.int64_t(id), &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult *Document - if rRes.readOptionFlag() { - var oRes0 Document - oRes0 = wvUnpackDocument(rRes) - goResult = &oRes0 - } - rRes.expectEnd() - return goResult -} - -type wvOutcomeDocsPublishAsync struct { - val bool - err error -} - -//export goWv_weaveffi_docs_publish_async_callback -func goWv_weaveffi_docs_publish_async_callback(context unsafe.Pointer, err *C.weaveffi_error, result C._Bool) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeDocsPublishAsync) - h.Delete() - if err != nil && err.code != 0 { - ch <- wvOutcomeDocsPublishAsync{err: wvBrandError(wvTakeBoxedError(err))} - return - } - ch <- wvOutcomeDocsPublishAsync{val: cToBool(result)} -} - -// PublishAsync: Publish a document in the background -// -// Parameters: -// - id: Identifier of the document to publish -// Blocks the calling goroutine until the async producer completes. -func PublishAsync(id int64) bool { - ch := make(chan wvOutcomeDocsPublishAsync, 1) - h := cgo.NewHandle(ch) - C.weaveffi_docs_publish_async_async(C.int64_t(id), C.weaveffi_docs_publish_async_callback(unsafe.Pointer(C.goWv_weaveffi_docs_publish_async_callback)), C.wvHandlePtr(C.uintptr_t(h))) - outcome := <-ch - if outcome.err != nil { - panic(outcome.err) - } - return outcome.val -} - -// Watch: Register a listener for saved notifications -// -// Parameters: -// - listener: Receiver of saved notifications -func Watch(listener SavedListener) { - hListener := cgo.NewHandle(listener) - var cErr C.weaveffi_error - C.weaveffi_docs_watch(C.wvHandlePtr(C.uintptr_t(hListener)), C.wvVtablePtr_weaveffi_docs_SavedListener(), &cErr) - wvTrap(&cErr) -} - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__README_md.snap index 7d3c4f86..e837672a 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__README_md.snap @@ -1,53 +1,49 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Go Bindings +# edge_cases (Go) -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`) +cgo bindings for the `edge_cases` native library. ## 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`: +The module ships its own copy of the C header (`edge_cases.h`) and links with +`-ledge_cases`. Point the linker at the directory holding `libedge_cases.so`, +`libedge_cases.dylib`, or `edge_cases.dll`, and make the library findable at +run time: ```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" +export CGO_LDFLAGS="-L/path/to/lib" +export LD_LIBRARY_PATH="/path/to/lib" # DYLD_LIBRARY_PATH on macOS +go build ./... ``` -2. Build or run your Go project that imports this module: +Depend on the module from your own project with a `require` (and, for a +local checkout, a `replace`) of `edge_cases`, then import it: -```sh -go build ./... +```go +import "edge_cases" ``` -## 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. - -Interfaces are reference-counted objects: each wrapper holds one strong -reference that `Close()` releases (a finalizer releases it if you forget), and -a nullable interface is a nil wrapper pointer. Callback interfaces are Go -`interface` types: implement one and pass the value to any function that -accepts it; the native library may call it from any thread, and a panic in -your implementation is reported to the native caller instead of crashing. +Go 1.23 or newer is required. + +## Usage notes + +- Records are plain structs and rich enums are sealed interfaces; both cross + the boundary by value. +- Interfaces are reference-counted objects. Call `Close` when you're done + with one (a finalizer releases it otherwise); it's safe to call twice or + while a call is in flight. +- Async functions take a `context.Context` first. Cancellable ones cancel + the native call when the context is done. +- Callback interfaces are Go interfaces you implement. The library may call + them from any thread, and a panic is reported to the native caller as a + foreign error instead of crashing. +- Loading checks the library's ABI revision and contract checksums and + panics with the stale module's name on a mismatch. diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__bindings_go.snap new file mode 100644 index 00000000..9ed0f38d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__bindings_go.snap @@ -0,0 +1,1557 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package edge_cases + +/* +#cgo LDFLAGS: -ledge_cases +#cgo CFLAGS: -Wno-deprecated-declarations +#include "edge_cases.h" +__attribute__((unused)) static void* wvHandlePtr(uintptr_t h) { return (void*)h; } +extern void goWv_edge_cases_edge_cases_BatchListener_on_batch(void* ctx, uint8_t* items_ptr, size_t items_len, uint8_t* raw_ptr, size_t raw_len, uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); +extern void goWv_edge_cases_edge_cases_BatchListener_on_widget(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); +extern void goWv_edge_cases_edge_cases_BatchListener_on_tick(void* ctx, edge_cases_error* out_err); +extern bool goWv_edge_cases_edge_cases_BatchListener_should_continue(void* ctx, int32_t class, edge_cases_error* out_err); +extern edge_cases_edge_cases_Mode goWv_edge_cases_edge_cases_BatchListener_pick_mode(void* ctx, edge_cases_error* out_err); +extern void goWv_edge_cases_edge_cases_BatchListener_free(void* ctx); +static const edge_cases_edge_cases_BatchListener_vtable wvVtable_edge_cases_edge_cases_BatchListener_vtable = { + (void (*)(void*, const uint8_t*, size_t, const uint8_t*, size_t, const uint8_t*, size_t, edge_cases_edge_cases_Mode, edge_cases_error*))goWv_edge_cases_edge_cases_BatchListener_on_batch, + (void (*)(void*, edge_cases_edge_cases_Widget*, edge_cases_edge_cases_Widget*, const uint8_t*, size_t, edge_cases_error*))goWv_edge_cases_edge_cases_BatchListener_on_widget, + goWv_edge_cases_edge_cases_BatchListener_on_tick, + goWv_edge_cases_edge_cases_BatchListener_should_continue, + goWv_edge_cases_edge_cases_BatchListener_pick_mode, + goWv_edge_cases_edge_cases_BatchListener_free, +}; +__attribute__((unused)) static const edge_cases_edge_cases_BatchListener_vtable* wvVtablePtr_edge_cases_edge_cases_BatchListener_vtable(void) { return &wvVtable_edge_cases_edge_cases_BatchListener_vtable; } +extern void goWv_edge_cases_edge_cases_Keyword_delete(void* ctx, int32_t self, edge_cases_error* out_err); +extern int32_t goWv_edge_cases_edge_cases_Keyword_import(void* ctx, edge_cases_error* out_err); +extern void goWv_edge_cases_edge_cases_Keyword_free(void* ctx); +static const edge_cases_edge_cases_Keyword_vtable wvVtable_edge_cases_edge_cases_Keyword_vtable = { + goWv_edge_cases_edge_cases_Keyword_delete, + goWv_edge_cases_edge_cases_Keyword_import, + goWv_edge_cases_edge_cases_Keyword_free, +}; +__attribute__((unused)) static const edge_cases_edge_cases_Keyword_vtable* wvVtablePtr_edge_cases_edge_cases_Keyword_vtable(void) { return &wvVtable_edge_cases_edge_cases_Keyword_vtable; } +extern void goWv_edge_cases_edge_cases_async_widget_callback(void* context_, edge_cases_error* err, edge_cases_edge_cases_Widget* result); +extern void goWv_edge_cases_edge_cases_async_void_callback(void* context_, edge_cases_error* err); +extern void goWv_edge_cases_edge_cases_async_bytes_callback(void* context_, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_edge_cases_edge_cases_async_struct_callback(void* context_, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_edge_cases_edge_cases_async_list_callback(void* context_, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_edge_cases_edge_cases_async_optional_callback(void* context_, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_edge_cases_edge_cases_async_enum_callback(void* context_, edge_cases_error* err, edge_cases_edge_cases_Mode result); +extern void goWv_edge_cases_edge_cases_async_keywords_callback(void* context_, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_edge_cases_edge_cases_Widget_fetch_callback(void* context_, edge_cases_error* err, uint8_t* result_ptr, size_t result_len); +*/ +import "C" + +import ( + "context" + "fmt" + "iter" + "runtime" + "runtime/cgo" + "unsafe" +) + +func init() { + wvCheckABI() + wvCheckModule("edge_cases", 0x1567f441073bd4aa, uint64(C.edge_cases_edge_cases_checksum())) +} + +// 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 *Error for unknown codes. +func wvMapEdgeCases(f wvFailure) error { + switch f.code { + case EdgeErrorsErrorBusy: + if f.message == "" { + f.message = "Resource is busy" + } + return &EdgeErrorsError{Code: f.code, Message: f.message} + default: + return f.err() + } +} + +// 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, 0, r.capHint(nLeaves0)) + for range nLeaves0 { + var eLeaves0 Leaf + eLeaves0 = wvUnpackLeaf(r) + v.Leaves = append(v.Leaves, eLeaves0) + } + 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, r.capHint(nByName0)) + for range nByName0 { + var kByName0 string + kByName0 = r.readString() + var vByName0 Inner + vByName0 = wvUnpackInner(r) + v.ByName[kByName0] = vByName0 + } + nModes0 := r.readLen() + v.Modes = make([]Mode, 0, r.capHint(nModes0)) + for range nModes0 { + var eModes0 Mode + eModes0 = Mode(r.readI32()) + v.Modes = append(v.Modes, eModes0) + } + nMatrix0 := r.readLen() + v.Matrix = make([][]int32, 0, r.capHint(nMatrix0)) + for range nMatrix0 { + var eMatrix0 []int32 + nMatrix1 := r.readLen() + eMatrix0 = make([]int32, 0, r.capHint(nMatrix1)) + for range nMatrix1 { + var eMatrix1 int32 + eMatrix1 = r.readI32() + eMatrix0 = append(eMatrix0, eMatrix1) + } + v.Matrix = append(v.Matrix, eMatrix0) + } + nSparse0 := r.readLen() + v.Sparse = make([]*int64, 0, r.capHint(nSparse0)) + for range nSparse0 { + var eSparse0 *int64 + if r.readOptionFlag() { + var oSparse1 int64 + oSparse1 = r.readI64() + eSparse0 = &oSparse1 + } + v.Sparse = append(v.Sparse, eSparse0) + } + 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, r.capHint(nById0)) + for range nById0 { + 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 +} + +// WidgetHolder: Record carrying objects in every buffered position +type WidgetHolder struct { + Primary *Widget + Fallback *Widget + Extras []*Widget + ByName map[string]*Widget +} + +// wvPackWidgetHolder appends v to w in the value-buffer wire format. +func wvPackWidgetHolder(w *wvWriter, v WidgetHolder) { + w.writeU64(wvTokenWidget(v.Primary)) + if v.Fallback == nil { + w.writeOptionFlag(false) + } else { + w.writeOptionFlag(true) + w.writeU64(wvTokenWidget(v.Fallback)) + } + w.writeLen(len(v.Extras)) + for _, eExtras0 := range v.Extras { + w.writeU64(wvTokenWidget(eExtras0)) + } + w.writeLen(len(v.ByName)) + for kByName0, vByName0 := range v.ByName { + w.writeString(kByName0) + w.writeU64(wvTokenWidget(vByName0)) + } +} + +// wvUnpackWidgetHolder decodes one WidgetHolder from r. +func wvUnpackWidgetHolder(r *wvReader) WidgetHolder { + var v WidgetHolder + v.Primary = wvUntokenWidget(r.readU64()) + if r.readOptionFlag() { + var oFallback0 *Widget + oFallback0 = wvUntokenWidget(r.readU64()) + v.Fallback = oFallback0 + } + nExtras0 := r.readLen() + v.Extras = make([]*Widget, 0, r.capHint(nExtras0)) + for range nExtras0 { + var eExtras0 *Widget + eExtras0 = wvUntokenWidget(r.readU64()) + v.Extras = append(v.Extras, eExtras0) + } + nByName0 := r.readLen() + v.ByName = make(map[string]*Widget, r.capHint(nByName0)) + for range nByName0 { + var kByName0 string + kByName0 = r.readString() + var vByName0 *Widget + vByName0 = wvUntokenWidget(r.readU64()) + v.ByName[kByName0] = vByName0 + } + return v +} + +// BatchListener: Callback interface carrying composite payloads and objects +// +// Implement this interface in Go and pass the value to native functions +// that accept it. +// +// The native library may call any method from any thread until it releases +// the implementation. A panic in a method is reported to the native caller +// as a foreign error (code -4) instead of crashing the process. +type BatchListener interface { + // OnBatch: Composite payloads cross as value buffers + OnBatch(items []Outer, raw []byte, first *Outer, mode Mode) + // OnWidget: Objects cross as strong references the consumer must release + OnWidget(widget *Widget, fallback *Widget, children []*Widget) + OnTick() + // ShouldContinue: Scalar return travels back through the vtable + ShouldContinue(class int32) bool + // PickMode: Enum return travels back as its integer discriminant + PickMode() Mode +} + +//export goWv_edge_cases_edge_cases_BatchListener_on_batch +func goWv_edge_cases_edge_cases_BatchListener_on_batch(ctx unsafe.Pointer, items_ptr *C.uint8_t, items_len C.size_t, raw_ptr *C.uint8_t, raw_len C.size_t, first_ptr *C.uint8_t, first_len C.size_t, mode C.edge_cases_edge_cases_Mode, out_err *C.edge_cases_error) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) + rArg0 := &wvReader{buf: wvBorrowBytes(items_ptr, items_len)} + var arg0 []Outer + nArg00 := rArg0.readLen() + arg0 = make([]Outer, 0, rArg0.capHint(nArg00)) + for range nArg00 { + var eArg00 Outer + eArg00 = wvUnpackOuter(rArg0) + arg0 = append(arg0, eArg00) + } + rArg0.expectEnd() + arg1 := wvBorrowBytes(raw_ptr, raw_len) + rArg2 := &wvReader{buf: wvBorrowBytes(first_ptr, first_len)} + var arg2 *Outer + if rArg2.readOptionFlag() { + var oArg20 Outer + oArg20 = wvUnpackOuter(rArg2) + arg2 = &oArg20 + } + rArg2.expectEnd() + arg3 := Mode(mode) + impl.OnBatch(arg0, arg1, arg2, arg3) +} + +//export goWv_edge_cases_edge_cases_BatchListener_on_widget +func goWv_edge_cases_edge_cases_BatchListener_on_widget(ctx unsafe.Pointer, widget *C.edge_cases_edge_cases_Widget, fallback *C.edge_cases_edge_cases_Widget, children_ptr *C.uint8_t, children_len C.size_t, out_err *C.edge_cases_error) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) + arg0 := wvAdoptWidget(widget) + arg1 := wvAdoptWidget(fallback) + rArg2 := &wvReader{buf: wvBorrowBytes(children_ptr, children_len)} + var arg2 []*Widget + nArg20 := rArg2.readLen() + arg2 = make([]*Widget, 0, rArg2.capHint(nArg20)) + for range nArg20 { + var eArg20 *Widget + eArg20 = wvUntokenWidget(rArg2.readU64()) + arg2 = append(arg2, eArg20) + } + rArg2.expectEnd() + impl.OnWidget(arg0, arg1, arg2) +} + +//export goWv_edge_cases_edge_cases_BatchListener_on_tick +func goWv_edge_cases_edge_cases_BatchListener_on_tick(ctx unsafe.Pointer, out_err *C.edge_cases_error) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) + impl.OnTick() +} + +//export goWv_edge_cases_edge_cases_BatchListener_should_continue +func goWv_edge_cases_edge_cases_BatchListener_should_continue(ctx unsafe.Pointer, class C.int32_t, out_err *C.edge_cases_error) (ret C._Bool) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) + arg0 := int32(class) + ret = C._Bool(impl.ShouldContinue(arg0)) + return +} + +//export goWv_edge_cases_edge_cases_BatchListener_pick_mode +func goWv_edge_cases_edge_cases_BatchListener_pick_mode(ctx unsafe.Pointer, out_err *C.edge_cases_error) (ret C.edge_cases_edge_cases_Mode) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) + ret = C.edge_cases_edge_cases_Mode(impl.PickMode()) + return +} + +//export goWv_edge_cases_edge_cases_BatchListener_free +func goWv_edge_cases_edge_cases_BatchListener_free(ctx unsafe.Pointer) { + cgo.Handle(uintptr(ctx)).Delete() +} + +// Keyword: Callback interface whose name and methods are keywords +// +// Implement this interface in Go and pass the value to native functions +// that accept it. +// +// The native library may call any method from any thread until it releases +// the implementation. A panic in a method is reported to the native caller +// as a foreign error (code -4) instead of crashing the process. +type Keyword interface { + Delete(self int32) + Import() int32 +} + +//export goWv_edge_cases_edge_cases_Keyword_delete +func goWv_edge_cases_edge_cases_Keyword_delete(ctx unsafe.Pointer, self C.int32_t, out_err *C.edge_cases_error) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(Keyword) + arg0 := int32(self) + impl.Delete(arg0) +} + +//export goWv_edge_cases_edge_cases_Keyword_import +func goWv_edge_cases_edge_cases_Keyword_import(ctx unsafe.Pointer, out_err *C.edge_cases_error) (ret C.int32_t) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(Keyword) + ret = C.int32_t(impl.Import()) + return +} + +//export goWv_edge_cases_edge_cases_Keyword_free +func goWv_edge_cases_edge_cases_Keyword_free(ctx unsafe.Pointer) { + cgo.Handle(uintptr(ctx)).Delete() +} + +// Widget: Interface with no constructor; instances come from make_widget +// +// Each wrapper holds one strong reference; Close releases it, and a +// finalizer releases it if the wrapper is garbage collected first. +type Widget struct { + ref wvRef +} + +func wvDestroyWidget(ptr unsafe.Pointer) { + C.edge_cases_edge_cases_Widget_destroy((*C.edge_cases_edge_cases_Widget)(ptr)) +} + +// wvAdoptWidget adopts one owned strong reference into a new wrapper. A null +// pointer adopts to nil. +func wvAdoptWidget(ptr *C.edge_cases_edge_cases_Widget) *Widget { + if ptr == nil { + return nil + } + s := &Widget{} + s.ref.init(unsafe.Pointer(ptr), wvDestroyWidget) + runtime.SetFinalizer(s, (*Widget).Close) + return s +} + +// native borrows the wrapper's pointer for one call; pair it with a +// deferred s.ref.release(). +func (s *Widget) native() *C.edge_cases_edge_cases_Widget { + if s == nil { + panic("edge_cases: nil *Widget") + } + return (*C.edge_cases_edge_cases_Widget)(s.ref.acquire("Widget")) +} + +// wvTokenWidget clones o's reference into a value-buffer object token. The +// wrapper keeps its own reference; the token carries the new one. +func wvTokenWidget(o *Widget) uint64 { + ptr := o.native() + defer o.ref.release() + return uint64(uintptr(unsafe.Pointer(C.edge_cases_edge_cases_Widget_clone(ptr)))) +} + +// wvUntokenWidget adopts the strong reference carried by a value-buffer object +// token. +func wvUntokenWidget(token uint64) *Widget { + if token == 0 { + panic("edge_cases: malformed value buffer: null object token") + } + return wvAdoptWidget((*C.edge_cases_edge_cases_Widget)(C.wvHandlePtr(C.uintptr_t(token)))) +} + +// Close releases the wrapper's strong reference and always returns nil. +// It's idempotent and safe to call from any goroutine, even while a call +// on the wrapper is in flight: the reference is then released when that +// call returns. The object itself is dropped once its last reference, from +// any wrapper, record, or the native side, is gone. +func (s *Widget) Close() error { + runtime.SetFinalizer(s, nil) + s.ref.close() + return nil +} + +// Refresh: Void method that can fail +func (s *Widget) Refresh() error { + cSelf := s.native() + defer s.ref.release() + var cErr C.edge_cases_error + C.edge_cases_edge_cases_Widget_refresh(cSelf, &cErr) + if cErr.code != 0 { + return wvMapEdgeCases(wvTakeError(&cErr)) + } + return nil +} + +func (s *Widget) Rename(class string, import_ *string) string { + cSelf := s.native() + defer s.ref.release() + cClassPtr, cClassLen := wvStr(class) + wImport := &wvWriter{} + if import_ == nil { + wImport.writeOptionFlag(false) + } else { + wImport.writeOptionFlag(true) + wImport.writeString(*import_) + } + cImportPtr, cImportLen := wvBytes(wImport.buf) + var cRetLen C.size_t + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_Widget_rename(cSelf, cClassPtr, cClassLen, cImportPtr, cImportLen, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +//export goWv_edge_cases_edge_cases_Widget_fetch_callback +func goWv_edge_cases_edge_cases_Widget_fetch_callback(context_ unsafe.Pointer, err *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { + wvComplete(context_, err, func() Outer { + rRet := &wvReader{buf: wvTakeBytes(result_ptr, result_len)} + var ret Outer + ret = wvUnpackOuter(rRet) + rRet.expectEnd() + return ret + }) +} + +// Fetch: Async method with a struct result +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func (s *Widget) Fetch(ctx context.Context, id int64) (Outer, error) { + if err := ctx.Err(); err != nil { + return Outer{}, err + } + cSelf := s.native() + defer s.ref.release() + wvDone := make(chan wvOutcome[Outer], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_Widget_fetch(cSelf, C.int64_t(id), C.edge_cases_edge_cases_Widget_fetch_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_Widget_fetch_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return Outer{}, err + } + if fail != nil { + return Outer{}, wvMapEdgeCases(*fail) + } + return res, nil +} + +// Deprecated: Use rename instead +func (s *Widget) OldName() string { + cSelf := s.native() + defer s.ref.release() + var cRetLen C.size_t + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_Widget_old_name(cSelf, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +// Vault: Deprecated interface with a throwing constructor +// +// Each wrapper holds one strong reference; Close releases it, and a +// finalizer releases it if the wrapper is garbage collected first. +// +// Deprecated: Use Widget instead +type Vault struct { + ref wvRef +} + +func wvDestroyVault(ptr unsafe.Pointer) { + C.edge_cases_edge_cases_Vault_destroy((*C.edge_cases_edge_cases_Vault)(ptr)) +} + +// wvAdoptVault adopts one owned strong reference into a new wrapper. A null +// pointer adopts to nil. +func wvAdoptVault(ptr *C.edge_cases_edge_cases_Vault) *Vault { + if ptr == nil { + return nil + } + s := &Vault{} + s.ref.init(unsafe.Pointer(ptr), wvDestroyVault) + runtime.SetFinalizer(s, (*Vault).Close) + return s +} + +// native borrows the wrapper's pointer for one call; pair it with a +// deferred s.ref.release(). +func (s *Vault) native() *C.edge_cases_edge_cases_Vault { + if s == nil { + panic("edge_cases: nil *Vault") + } + return (*C.edge_cases_edge_cases_Vault)(s.ref.acquire("Vault")) +} + +// wvTokenVault clones o's reference into a value-buffer object token. The +// wrapper keeps its own reference; the token carries the new one. +func wvTokenVault(o *Vault) uint64 { + ptr := o.native() + defer o.ref.release() + return uint64(uintptr(unsafe.Pointer(C.edge_cases_edge_cases_Vault_clone(ptr)))) +} + +// wvUntokenVault adopts the strong reference carried by a value-buffer object +// token. +func wvUntokenVault(token uint64) *Vault { + if token == 0 { + panic("edge_cases: malformed value buffer: null object token") + } + return wvAdoptVault((*C.edge_cases_edge_cases_Vault)(C.wvHandlePtr(C.uintptr_t(token)))) +} + +// Close releases the wrapper's strong reference and always returns nil. +// It's idempotent and safe to call from any goroutine, even while a call +// on the wrapper is in flight: the reference is then released when that +// call returns. The object itself is dropped once its last reference, from +// any wrapper, record, or the native side, is gone. +func (s *Vault) Close() error { + runtime.SetFinalizer(s, nil) + s.ref.close() + return nil +} + +// OpenVault: Fails when the vault is busy +func OpenVault(path string) (*Vault, error) { + cPathPtr, cPathLen := wvStr(path) + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_Vault_open(cPathPtr, cPathLen, &cErr) + if cErr.code != 0 { + return nil, wvMapEdgeCases(wvTakeError(&cErr)) + } + return wvAdoptVault(cRet), nil +} + +func (s *Vault) Peek() *Widget { + cSelf := s.native() + defer s.ref.release() + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_Vault_peek(cSelf, &cErr) + wvTrap(&cErr) + return wvAdoptWidget(cRet) +} + +// VaultProbe: Throwing static +func VaultProbe() (bool, error) { + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_Vault_probe(&cErr) + if cErr.code != 0 { + return false, wvMapEdgeCases(wvTakeError(&cErr)) + } + return bool(cRet), nil +} + +// Noop: No parameters and no result +func Noop() { + var cErr C.edge_cases_error + C.edge_cases_edge_cases_noop(&cErr) + wvTrap(&cErr) +} + +// Select: Function named after a keyword whose parameters are all keywords +func Select(class int32, from string, func_ []string, range_ *int32, type_ bool) int32 { + cFromPtr, cFromLen := wvStr(from) + wFunc := &wvWriter{} + wFunc.writeLen(len(func_)) + for _, eFunc0 := range func_ { + wFunc.writeString(eFunc0) + } + cFuncPtr, cFuncLen := wvBytes(wFunc.buf) + wRange := &wvWriter{} + if range_ == nil { + wRange.writeOptionFlag(false) + } else { + wRange.writeOptionFlag(true) + wRange.writeI32(*range_) + } + cRangePtr, cRangeLen := wvBytes(wRange.buf) + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_select(C.int32_t(class), cFromPtr, cFromLen, cFuncPtr, cFuncLen, cRangePtr, cRangeLen, C._Bool(type_), &cErr) + wvTrap(&cErr) + return int32(cRet) +} + +// NestedOptional: Optional list of optional structs, the deepest legal nesting +func NestedOptional(items []*Outer) []*Outer { + 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) + } + } + } + cItemsPtr, cItemsLen := wvBytes(wItems.buf) + var cRetLen C.size_t + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_nested_optional(cItemsPtr, cItemsLen, &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret []*Outer + if rRet.readOptionFlag() { + var oRet0 []*Outer + nRet1 := rRet.readLen() + oRet0 = make([]*Outer, 0, rRet.capHint(nRet1)) + for range nRet1 { + var eRet1 *Outer + if rRet.readOptionFlag() { + var oRet2 Outer + oRet2 = wvUnpackOuter(rRet) + eRet1 = &oRet2 + } + oRet0 = append(oRet0, eRet1) + } + ret = oRet0 + } + rRet.expectEnd() + return ret +} + +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) + } + cTextPtr, cTextLen := wvBytes(wText.buf) + wCount := &wvWriter{} + if count == nil { + wCount.writeOptionFlag(false) + } else { + wCount.writeOptionFlag(true) + wCount.writeI32(*count) + } + cCountPtr, cCountLen := wvBytes(wCount.buf) + wOuter := &wvWriter{} + if outer == nil { + wOuter.writeOptionFlag(false) + } else { + wOuter.writeOptionFlag(true) + wvPackOuter(wOuter, *outer) + } + cOuterPtr, cOuterLen := wvBytes(wOuter.buf) + wTags := &wvWriter{} + if tags == nil { + wTags.writeOptionFlag(false) + } else { + wTags.writeOptionFlag(true) + wTags.writeLen(len(tags)) + for _, eTags1 := range tags { + wTags.writeString(eTags1) + } + } + cTagsPtr, cTagsLen := wvBytes(wTags.buf) + wBlob := &wvWriter{} + if blob == nil { + wBlob.writeOptionFlag(false) + } else { + wBlob.writeOptionFlag(true) + wBlob.writeBytes(blob) + } + cBlobPtr, cBlobLen := wvBytes(wBlob.buf) + wMode := &wvWriter{} + if mode == nil { + wMode.writeOptionFlag(false) + } else { + wMode.writeOptionFlag(true) + wMode.writeI32(int32(*mode)) + } + cModePtr, cModeLen := wvBytes(wMode.buf) + var cRetLen C.size_t + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_optional_params(cTextPtr, cTextLen, cCountPtr, cCountLen, cOuterPtr, cOuterLen, cTagsPtr, cTagsLen, cBlobPtr, cBlobLen, cModePtr, cModeLen, &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret *Mode + if rRet.readOptionFlag() { + var oRet0 Mode + oRet0 = Mode(rRet.readI32()) + ret = &oRet0 + } + rRet.expectEnd() + return ret +} + +// RegisterBatch: Two callback interface parameters on one function +func RegisterBatch(listener BatchListener, keyword Keyword) { + hListener := cgo.NewHandle(listener) + hKeyword := cgo.NewHandle(keyword) + var cErr C.edge_cases_error + C.edge_cases_edge_cases_register_batch(C.wvHandlePtr(C.uintptr_t(hListener)), C.wvVtablePtr_edge_cases_edge_cases_BatchListener_vtable(), C.wvHandlePtr(C.uintptr_t(hKeyword)), C.wvVtablePtr_edge_cases_edge_cases_Keyword_vtable(), &cErr) + wvTrap(&cErr) +} + +// WidgetsEverywhere: Objects in every buffered position +func WidgetsEverywhere(list []*Widget, byName map[string]*Widget, sparse []*Widget, holder WidgetHolder) WidgetHolder { + wList := &wvWriter{} + wList.writeLen(len(list)) + for _, eList0 := range list { + wList.writeU64(wvTokenWidget(eList0)) + } + cListPtr, cListLen := wvBytes(wList.buf) + wByName := &wvWriter{} + wByName.writeLen(len(byName)) + for kByName0, vByName0 := range byName { + wByName.writeString(kByName0) + wByName.writeU64(wvTokenWidget(vByName0)) + } + cByNamePtr, cByNameLen := wvBytes(wByName.buf) + wSparse := &wvWriter{} + wSparse.writeLen(len(sparse)) + for _, eSparse0 := range sparse { + if eSparse0 == nil { + wSparse.writeOptionFlag(false) + } else { + wSparse.writeOptionFlag(true) + wSparse.writeU64(wvTokenWidget(eSparse0)) + } + } + cSparsePtr, cSparseLen := wvBytes(wSparse.buf) + wHolder := &wvWriter{} + wvPackWidgetHolder(wHolder, holder) + cHolderPtr, cHolderLen := wvBytes(wHolder.buf) + var cRetLen C.size_t + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_widgets_everywhere(cListPtr, cListLen, cByNamePtr, cByNameLen, cSparsePtr, cSparseLen, cHolderPtr, cHolderLen, &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret WidgetHolder + ret = wvUnpackWidgetHolder(rRet) + rRet.expectEnd() + return ret +} + +// WidgetStream: Iterator of objects +// +// The sequence launches the native iterator on each range and calls next once +// per element; the iterator is destroyed when the range ends, early or not. +func WidgetStream() iter.Seq[*Widget] { + return func(yield func(*Widget) bool) { + var cErr C.edge_cases_error + cIter := C.edge_cases_edge_cases_widget_stream(&cErr) + wvTrap(&cErr) + defer C.edge_cases_edge_cases_WidgetStreamIterator_destroy(cIter) + for { + var cItem *C.edge_cases_edge_cases_Widget + var cIterErr C.edge_cases_error + more := C.edge_cases_edge_cases_WidgetStreamIterator_next(cIter, &cItem, &cIterErr) != 0 + wvTrap(&cIterErr) + if !more { + return + } + item := wvAdoptWidget(cItem) + if !yield(item) { + return + } + } + } +} + +//export goWv_edge_cases_edge_cases_async_widget_callback +func goWv_edge_cases_edge_cases_async_widget_callback(context_ unsafe.Pointer, err *C.edge_cases_error, result *C.edge_cases_edge_cases_Widget) { + wvComplete(context_, err, func() *Widget { + return wvAdoptWidget(result) + }) +} + +// AsyncWidget: Async returning an object +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func AsyncWidget(ctx context.Context) (*Widget, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + wvDone := make(chan wvOutcome[*Widget], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_async_widget(C.edge_cases_edge_cases_async_widget_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_widget_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return nil, err + } + if fail != nil { + return nil, wvMapEdgeCases(*fail) + } + return res, nil +} + +// MakeWidget: Factory for an interface with no constructor +func MakeWidget(label string) *Widget { + cLabelPtr, cLabelLen := wvStr(label) + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_make_widget(cLabelPtr, cLabelLen, &cErr) + wvTrap(&cErr) + return wvAdoptWidget(cRet) +} + +// FindWidget: Optional interface return +func FindWidget(id int64) (*Widget, error) { + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_find_widget(C.int64_t(id), &cErr) + if cErr.code != 0 { + return nil, wvMapEdgeCases(wvTakeError(&cErr)) + } + return wvAdoptWidget(cRet), nil +} + +// InspectWidget: Interface parameter, including an optional one +func InspectWidget(widget *Widget, fallback *Widget) string { + cWidget := widget.native() + defer widget.ref.release() + var cFallback *C.edge_cases_edge_cases_Widget + if fallback != nil { + cFallback = fallback.native() + defer fallback.ref.release() + } + var cRetLen C.size_t + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_inspect_widget(cWidget, cFallback, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +// StringStream: Iterator of strings +// +// The sequence launches the native iterator on each range and calls next once +// per element; the iterator is destroyed when the range ends, early or not. +func StringStream() iter.Seq[string] { + return func(yield func(string) bool) { + var cErr C.edge_cases_error + cIter := C.edge_cases_edge_cases_string_stream(&cErr) + wvTrap(&cErr) + defer C.edge_cases_edge_cases_StringStreamIterator_destroy(cIter) + for { + var cItem *C.uint8_t + var cItemLen C.size_t + var cIterErr C.edge_cases_error + more := C.edge_cases_edge_cases_StringStreamIterator_next(cIter, &cItem, &cItemLen, &cIterErr) != 0 + wvTrap(&cIterErr) + if !more { + return + } + item := wvTakeString(cItem, cItemLen) + if !yield(item) { + return + } + } + } +} + +// ScalarStream: Iterator of scalars +// +// The sequence launches the native iterator on each range and calls next once +// per element; the iterator is destroyed when the range ends, early or not. +func ScalarStream() iter.Seq[int32] { + return func(yield func(int32) bool) { + var cErr C.edge_cases_error + cIter := C.edge_cases_edge_cases_scalar_stream(&cErr) + wvTrap(&cErr) + defer C.edge_cases_edge_cases_ScalarStreamIterator_destroy(cIter) + for { + var cItem C.int32_t + var cIterErr C.edge_cases_error + more := C.edge_cases_edge_cases_ScalarStreamIterator_next(cIter, &cItem, &cIterErr) != 0 + wvTrap(&cIterErr) + if !more { + return + } + item := int32(cItem) + if !yield(item) { + return + } + } + } +} + +// ModeStream: Iterator of enums +// +// The sequence launches the native iterator on each range and calls next once +// per element; the iterator is destroyed when the range ends, early or not. +func ModeStream() iter.Seq[Mode] { + return func(yield func(Mode) bool) { + var cErr C.edge_cases_error + cIter := C.edge_cases_edge_cases_mode_stream(&cErr) + wvTrap(&cErr) + defer C.edge_cases_edge_cases_ModeStreamIterator_destroy(cIter) + for { + var cItem C.edge_cases_edge_cases_Mode + var cIterErr C.edge_cases_error + more := C.edge_cases_edge_cases_ModeStreamIterator_next(cIter, &cItem, &cIterErr) != 0 + wvTrap(&cIterErr) + if !more { + return + } + item := Mode(cItem) + if !yield(item) { + return + } + } + } +} + +//export goWv_edge_cases_edge_cases_async_void_callback +func goWv_edge_cases_edge_cases_async_void_callback(context_ unsafe.Pointer, err *C.edge_cases_error) { + wvComplete(context_, err, func() struct{} { + return struct{}{} + }) +} + +// AsyncVoid: Async with no result +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func AsyncVoid(ctx context.Context) error { + if err := ctx.Err(); err != nil { + return err + } + wvDone := make(chan wvOutcome[struct{}], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_async_void(C.edge_cases_edge_cases_async_void_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_void_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + _, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return err + } + if fail != nil { + panic(fail.err()) + } + return nil +} + +//export goWv_edge_cases_edge_cases_async_bytes_callback +func goWv_edge_cases_edge_cases_async_bytes_callback(context_ unsafe.Pointer, err *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { + wvComplete(context_, err, func() []byte { + return wvTakeBytes(result_ptr, result_len) + }) +} + +// AsyncBytes calls the native async function. +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func AsyncBytes(ctx context.Context, size uint32) ([]byte, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + wvDone := make(chan wvOutcome[[]byte], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_async_bytes(C.uint32_t(size), C.edge_cases_edge_cases_async_bytes_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_bytes_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return nil, err + } + if fail != nil { + panic(fail.err()) + } + return res, nil +} + +//export goWv_edge_cases_edge_cases_async_struct_callback +func goWv_edge_cases_edge_cases_async_struct_callback(context_ unsafe.Pointer, err *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { + wvComplete(context_, err, func() Outer { + rRet := &wvReader{buf: wvTakeBytes(result_ptr, result_len)} + var ret Outer + ret = wvUnpackOuter(rRet) + rRet.expectEnd() + return ret + }) +} + +// AsyncStruct calls the native async function. +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func AsyncStruct(ctx context.Context) (Outer, error) { + if err := ctx.Err(); err != nil { + return Outer{}, err + } + wvDone := make(chan wvOutcome[Outer], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_async_struct(C.edge_cases_edge_cases_async_struct_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_struct_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return Outer{}, err + } + if fail != nil { + return Outer{}, wvMapEdgeCases(*fail) + } + return res, nil +} + +//export goWv_edge_cases_edge_cases_async_list_callback +func goWv_edge_cases_edge_cases_async_list_callback(context_ unsafe.Pointer, err *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { + wvComplete(context_, err, func() []Outer { + rRet := &wvReader{buf: wvTakeBytes(result_ptr, result_len)} + var ret []Outer + nRet0 := rRet.readLen() + ret = make([]Outer, 0, rRet.capHint(nRet0)) + for range nRet0 { + var eRet0 Outer + eRet0 = wvUnpackOuter(rRet) + ret = append(ret, eRet0) + } + rRet.expectEnd() + return ret + }) +} + +// AsyncList calls the native async function. +// +// It blocks until the call completes. Cancelling ctx cancels the native +// call, which then returns ctx.Err(). +func AsyncList(ctx context.Context) ([]Outer, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + wvToken := C.edge_cases_cancel_token_create() + defer C.edge_cases_cancel_token_destroy(wvToken) + wvDone := make(chan wvOutcome[[]Outer], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_async_list(wvToken, C.edge_cases_edge_cases_async_list_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_list_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, wvToken) + if err != nil { + return nil, err + } + if fail != nil { + return nil, wvMapEdgeCases(*fail) + } + return res, nil +} + +//export goWv_edge_cases_edge_cases_async_optional_callback +func goWv_edge_cases_edge_cases_async_optional_callback(context_ unsafe.Pointer, err *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { + wvComplete(context_, err, func() *int64 { + rRet := &wvReader{buf: wvTakeBytes(result_ptr, result_len)} + var ret *int64 + if rRet.readOptionFlag() { + var oRet0 int64 + oRet0 = rRet.readI64() + ret = &oRet0 + } + rRet.expectEnd() + return ret + }) +} + +// AsyncOptional calls the native async function. +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func AsyncOptional(ctx context.Context) (*int64, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + wvDone := make(chan wvOutcome[*int64], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_async_optional(C.edge_cases_edge_cases_async_optional_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_optional_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return nil, err + } + if fail != nil { + panic(fail.err()) + } + return res, nil +} + +//export goWv_edge_cases_edge_cases_async_enum_callback +func goWv_edge_cases_edge_cases_async_enum_callback(context_ unsafe.Pointer, err *C.edge_cases_error, result C.edge_cases_edge_cases_Mode) { + wvComplete(context_, err, func() Mode { + return Mode(result) + }) +} + +// AsyncEnum calls the native async function. +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func AsyncEnum(ctx context.Context) (Mode, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + wvDone := make(chan wvOutcome[Mode], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_async_enum(C.edge_cases_edge_cases_async_enum_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_enum_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return 0, err + } + if fail != nil { + panic(fail.err()) + } + return res, nil +} + +//export goWv_edge_cases_edge_cases_async_keywords_callback +func goWv_edge_cases_edge_cases_async_keywords_callback(context_ unsafe.Pointer, err *C.edge_cases_error, result_ptr *C.uint8_t, result_len C.size_t) { + wvComplete(context_, err, func() string { + return wvTakeString(result_ptr, result_len) + }) +} + +// AsyncKeywords calls the native async function. +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func AsyncKeywords(ctx context.Context, class string, import_ int32) (string, error) { + if err := ctx.Err(); err != nil { + return "", err + } + cClassPtr, cClassLen := wvStr(class) + wvDone := make(chan wvOutcome[string], 1) + wvHandle := cgo.NewHandle(wvDone) + C.edge_cases_edge_cases_async_keywords(cClassPtr, cClassLen, C.int32_t(import_), C.edge_cases_edge_cases_async_keywords_callback(unsafe.Pointer(C.goWv_edge_cases_edge_cases_async_keywords_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return "", err + } + if fail != nil { + panic(fail.err()) + } + return res, nil +} + +func KeywordsRoundtrip(value Keywords) Keywords { + wValue := &wvWriter{} + wvPackKeywords(wValue, value) + cValuePtr, cValueLen := wvBytes(wValue.buf) + var cRetLen C.size_t + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_keywords_roundtrip(cValuePtr, cValueLen, &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Keywords + ret = wvUnpackKeywords(rRet) + rRet.expectEnd() + return ret +} + +// Deprecated: Use Mode-based APIs +func LegacyValue(value Legacy) Retired { + var cRetLen C.size_t + var cErr C.edge_cases_error + cRet := C.edge_cases_edge_cases_legacy_value(C.edge_cases_edge_cases_Legacy(value), &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Retired + ret = wvUnpackRetired(rRet) + rRet.expectEnd() + return ret +} + +// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__codec_go.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__codec_go.snap new file mode 100644 index 00000000..16ddb647 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__codec_go.snap @@ -0,0 +1,205 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package edge_cases + +import ( + "encoding/binary" + "math" + "unicode/utf8" +) + +// wvWriter serializes values into the value-buffer format: little-endian, +// packed, u32 length prefixes. +type wvWriter struct { + buf []byte +} + +func (w *wvWriter) writeBool(v bool) { + if v { + w.buf = append(w.buf, 1) + } else { + w.buf = append(w.buf, 0) + } +} + +func (w *wvWriter) writeI8(v int8) { + w.buf = append(w.buf, byte(v)) +} + +func (w *wvWriter) writeU8(v uint8) { + w.buf = append(w.buf, v) +} + +func (w *wvWriter) writeI16(v int16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v)) +} + +func (w *wvWriter) writeU16(v uint16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, v) +} + +func (w *wvWriter) writeI32(v int32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v)) +} + +func (w *wvWriter) writeU32(v uint32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, v) +} + +func (w *wvWriter) writeI64(v int64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v)) +} + +func (w *wvWriter) writeU64(v uint64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, v) +} + +func (w *wvWriter) writeF32(v float32) { + w.writeU32(math.Float32bits(v)) +} + +func (w *wvWriter) writeF64(v float64) { + w.writeU64(math.Float64bits(v)) +} + +func (w *wvWriter) writeLen(n int) { + if n < 0 || uint64(n) > math.MaxUint32 { + panic("edge_cases: 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 value-buffer format. A malformed buffer +// is a producer/consumer contract violation, so every read panics instead of +// returning a typed domain error. +type wvReader struct { + buf []byte + pos int +} + +func wvMalformed(context string) { + panic("edge_cases: malformed value buffer: " + context) +} + +func (r *wvReader) take(n int, context string) []byte { + if n < 0 || len(r.buf)-r.pos < n { + wvMalformed(context) + } + b := r.buf[r.pos : r.pos+n] + r.pos += n + return b +} + +func (r *wvReader) readBool() bool { + switch r.take(1, "bool")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("bool byte out of range") + return false +} + +func (r *wvReader) readI8() int8 { + return int8(r.take(1, "i8")[0]) +} + +func (r *wvReader) readU8() uint8 { + return r.take(1, "u8")[0] +} + +func (r *wvReader) readI16() int16 { + return int16(binary.LittleEndian.Uint16(r.take(2, "i16"))) +} + +func (r *wvReader) readU16() uint16 { + return binary.LittleEndian.Uint16(r.take(2, "u16")) +} + +func (r *wvReader) readI32() int32 { + return int32(binary.LittleEndian.Uint32(r.take(4, "i32"))) +} + +func (r *wvReader) readU32() uint32 { + return binary.LittleEndian.Uint32(r.take(4, "u32")) +} + +func (r *wvReader) readI64() int64 { + return int64(binary.LittleEndian.Uint64(r.take(8, "i64"))) +} + +func (r *wvReader) readU64() uint64 { + return binary.LittleEndian.Uint64(r.take(8, "u64")) +} + +func (r *wvReader) readF32() float32 { + return math.Float32frombits(r.readU32()) +} + +func (r *wvReader) readF64() float64 { + return math.Float64frombits(r.readU64()) +} + +// readLen reads a u32 element count. Elements may encode to zero bytes, so +// the count isn't checked against the remaining buffer; callers cap their +// preallocation with capHint instead. +func (r *wvReader) readLen() int { + return int(r.readU32()) +} + +// capHint bounds a preallocation for n decoded elements by the bytes left in +// the buffer, so a corrupt count can't force a huge allocation. +func (r *wvReader) capHint(n int) int { + return min(n, len(r.buf)-r.pos) +} + +func (r *wvReader) readString() string { + b := r.take(r.readLen(), "string bytes") + if !utf8.Valid(b) { + wvMalformed("string is not valid UTF-8") + } + return string(b) +} + +func (r *wvReader) readBytes() []byte { + return append([]byte{}, r.take(r.readLen(), "byte buffer")...) +} + +func (r *wvReader) readOptionFlag() bool { + switch r.take(1, "option flag")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("option flag byte out of range") + return false +} + +func (r *wvReader) expectEnd() { + if r.pos != len(r.buf) { + wvMalformed("trailing bytes after value") + } +} + +// END codec.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__edge_cases_h.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__edge_cases_h.snap new file mode 100644 index 00000000..94864086 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__edge_cases_h.snap @@ -0,0 +1,245 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef EDGE_CASES_H +#define EDGE_CASES_H + +#include +#include +#include + +#ifndef EDGE_CASES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef EDGE_CASES_BUILD +# define EDGE_CASES_API __declspec(dllexport) +# else +# define EDGE_CASES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define EDGE_CASES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define EDGE_CASES_API __attribute__((visibility("default"))) +# else +# define EDGE_CASES_API +# endif +#endif + +#ifndef EDGE_CASES_DEPRECATED +# if defined(_MSC_VER) +# define EDGE_CASES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define EDGE_CASES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define EDGE_CASES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports edge_cases_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define EDGE_CASES_ABI_VERSION 3u +EDGE_CASES_API uint32_t edge_cases_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by edge_cases_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct edge_cases_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} edge_cases_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +EDGE_CASES_API void edge_cases_error_set(edge_cases_error* err, int32_t code, const char* message); +EDGE_CASES_API void edge_cases_error_clear(edge_cases_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + edge_cases_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +EDGE_CASES_API void edge_cases_error_free(edge_cases_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +EDGE_CASES_API void edge_cases_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct edge_cases_cancel_token edge_cases_cancel_token; +EDGE_CASES_API edge_cases_cancel_token* edge_cases_cancel_token_create(void); +EDGE_CASES_API void edge_cases_cancel_token_cancel(edge_cases_cancel_token* token); +EDGE_CASES_API bool edge_cases_cancel_token_is_cancelled(const edge_cases_cancel_token* token); +EDGE_CASES_API void edge_cases_cancel_token_destroy(edge_cases_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +EDGE_CASES_API uint8_t* edge_cases_alloc(uint32_t size); +EDGE_CASES_API void edge_cases_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +EDGE_CASES_API uint64_t edge_cases_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define EDGE_CASES_EDGE_CASES_CHECKSUM 0x1567f441073bd4aaull +EDGE_CASES_API uint64_t edge_cases_edge_cases_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with edge_cases_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * edge_cases_buffer.h declares a C struct and codec for each. + */ + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +typedef enum { edge_cases_edge_cases_Legacy_None = 0, edge_cases_edge_cases_Legacy_Default = 5, edge_cases_edge_cases_Legacy_Class = 10 } edge_cases_edge_cases_Legacy; +typedef enum { edge_cases_edge_cases_Mode_Fast = 1, edge_cases_edge_cases_Mode_Safe = 2 } edge_cases_edge_cases_Mode; +/** Error codes reported by throwing functions in the `edge_cases` module tree. */ +typedef enum { edge_cases_edge_cases_EdgeErrors_Busy = 7 } edge_cases_edge_cases_EdgeErrors; +typedef struct edge_cases_edge_cases_Widget edge_cases_edge_cases_Widget; +typedef struct edge_cases_edge_cases_Vault edge_cases_edge_cases_Vault; +typedef struct edge_cases_edge_cases_WidgetStreamIterator edge_cases_edge_cases_WidgetStreamIterator; +typedef struct edge_cases_edge_cases_StringStreamIterator edge_cases_edge_cases_StringStreamIterator; +typedef struct edge_cases_edge_cases_ScalarStreamIterator edge_cases_edge_cases_ScalarStreamIterator; +typedef struct edge_cases_edge_cases_ModeStreamIterator edge_cases_edge_cases_ModeStreamIterator; +/** + * Callback interface carrying composite payloads and objects + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_BatchListener_vtable { + /** Composite payloads cross as value buffers */ + void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); + /** Objects cross as strong references the consumer must release */ + void (*on_widget)(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); + void (*on_tick)(void* ctx, edge_cases_error* out_err); + /** Scalar return travels back through the vtable */ + bool (*should_continue)(void* ctx, int32_t class_, edge_cases_error* out_err); + /** Enum return travels back as its integer discriminant */ + edge_cases_edge_cases_Mode (*pick_mode)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_BatchListener_vtable; +/** + * Callback interface whose name and methods are keywords + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_Keyword_vtable { + void (*delete_)(void* ctx, int32_t self, edge_cases_error* out_err); + int32_t (*import)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_Keyword_vtable; +typedef void (*edge_cases_edge_cases_async_widget_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result); +typedef void (*edge_cases_edge_cases_async_void_callback)(void* context, edge_cases_error* err); +typedef void (*edge_cases_edge_cases_async_bytes_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_struct_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_list_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_optional_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_enum_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result); +typedef void (*edge_cases_edge_cases_async_keywords_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_Widget_fetch_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: edge_cases +/** Interface with no constructor; instances come from make_widget */ +/** Void method that can fail */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_refresh(const edge_cases_edge_cases_Widget* self, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_rename(const edge_cases_edge_cases_Widget* self, const uint8_t* class_ptr, size_t class_len, const uint8_t* import_ptr, size_t import_len, size_t* out_len, edge_cases_error* out_err); +/** Async method with a struct result */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_fetch(const edge_cases_edge_cases_Widget* self, int64_t id, edge_cases_edge_cases_Widget_fetch_callback callback, void* context); +EDGE_CASES_DEPRECATED("Use rename instead") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_old_name(const edge_cases_edge_cases_Widget* self, size_t* out_len, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Widget_clone(const edge_cases_edge_cases_Widget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_destroy(edge_cases_edge_cases_Widget* self); + +/** Deprecated interface with a throwing constructor */ +/** Fails when the vault is busy */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_open(const uint8_t* path_ptr, size_t path_len, edge_cases_error* out_err); +/** Throwing static */ +EDGE_CASES_API bool edge_cases_edge_cases_Vault_probe(edge_cases_error* out_err); +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Vault_peek(const edge_cases_edge_cases_Vault* self, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_clone(const edge_cases_edge_cases_Vault* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Vault_destroy(edge_cases_edge_cases_Vault* self); + +/** No parameters and no result */ +EDGE_CASES_API void edge_cases_edge_cases_noop(edge_cases_error* out_err); +/** Function named after a keyword whose parameters are all keywords */ +EDGE_CASES_API int32_t edge_cases_edge_cases_select(int32_t class_, const uint8_t* from_ptr, size_t from_len, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, edge_cases_error* out_err); +/** Optional list of optional structs, the deepest legal nesting */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_optional_params(const uint8_t* text_ptr, size_t text_len, const uint8_t* count_ptr, size_t count_len, const uint8_t* outer_ptr, size_t outer_len, const uint8_t* tags_ptr, size_t tags_len, const uint8_t* blob_ptr, size_t blob_len, const uint8_t* mode_ptr, size_t mode_len, size_t* out_len, edge_cases_error* out_err); +/** Two callback interface parameters on one function */ +EDGE_CASES_API void edge_cases_edge_cases_register_batch(void* listener_ctx, const edge_cases_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const edge_cases_edge_cases_Keyword_vtable* keyword_vtable, edge_cases_error* out_err); +/** Objects in every buffered position */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, edge_cases_error* out_err); +/** Iterator of objects */ +EDGE_CASES_API edge_cases_edge_cases_WidgetStreamIterator* edge_cases_edge_cases_widget_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_WidgetStreamIterator_next(edge_cases_edge_cases_WidgetStreamIterator* iter, edge_cases_edge_cases_Widget** out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_WidgetStreamIterator_destroy(edge_cases_edge_cases_WidgetStreamIterator* iter); +/** Async returning an object */ +EDGE_CASES_API void edge_cases_edge_cases_async_widget(edge_cases_edge_cases_async_widget_callback callback, void* context); +/** Factory for an interface with no constructor */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_make_widget(const uint8_t* label_ptr, size_t label_len, edge_cases_error* out_err); +/** Optional interface return */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_find_widget(int64_t id, edge_cases_error* out_err); +/** Interface parameter, including an optional one */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_inspect_widget(const edge_cases_edge_cases_Widget* widget, const edge_cases_edge_cases_Widget* fallback, size_t* out_len, edge_cases_error* out_err); +/** Iterator of strings */ +EDGE_CASES_API edge_cases_edge_cases_StringStreamIterator* edge_cases_edge_cases_string_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_StringStreamIterator_next(edge_cases_edge_cases_StringStreamIterator* iter, const uint8_t** out_item, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_StringStreamIterator_destroy(edge_cases_edge_cases_StringStreamIterator* iter); +/** Iterator of scalars */ +EDGE_CASES_API edge_cases_edge_cases_ScalarStreamIterator* edge_cases_edge_cases_scalar_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ScalarStreamIterator_next(edge_cases_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ScalarStreamIterator_destroy(edge_cases_edge_cases_ScalarStreamIterator* iter); +/** Iterator of enums */ +EDGE_CASES_API edge_cases_edge_cases_ModeStreamIterator* edge_cases_edge_cases_mode_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ModeStreamIterator_next(edge_cases_edge_cases_ModeStreamIterator* iter, edge_cases_edge_cases_Mode* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ModeStreamIterator_destroy(edge_cases_edge_cases_ModeStreamIterator* iter); +/** Async with no result */ +EDGE_CASES_API void edge_cases_edge_cases_async_void(edge_cases_edge_cases_async_void_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_bytes(uint32_t size, edge_cases_edge_cases_async_bytes_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_struct(edge_cases_edge_cases_async_struct_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_list(edge_cases_cancel_token* cancel_token, edge_cases_edge_cases_async_list_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_optional(edge_cases_edge_cases_async_optional_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_enum(edge_cases_edge_cases_async_enum_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_keywords(const uint8_t* class_ptr, size_t class_len, int32_t import, edge_cases_edge_cases_async_keywords_callback callback, void* context); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_DEPRECATED("Use Mode-based APIs") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy value, size_t* out_len, edge_cases_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // EDGE_CASES_H + +// END edge_cases.h diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__go_mod.snap index 73a087a7..66173700 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__go_mod.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__go_mod.snap @@ -1,11 +1,11 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -module weaveffi +module edge_cases go 1.23 diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__runtime_go.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__runtime_go.snap new file mode 100644 index 00000000..ab9dfdca --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__runtime_go.snap @@ -0,0 +1,305 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package edge_cases + +/* +#include +#include "edge_cases.h" +*/ +import "C" + +import ( + "context" + "fmt" + "runtime/cgo" + "sync/atomic" + "unsafe" +) + +// wvABIVersion is the C ABI revision these bindings were generated against. +const wvABIVersion uint32 = 3 + +// The runtime error codes this package produces or interprets itself. +const ( + wvCodeMarshal int32 = -3 + wvCodeForeign int32 = -4 + wvCodeCancelled int32 = -5 +) + +// Error reports a failure that no typed error domain claims: an unknown +// code, a generic failure (code -1), a producer panic (-2), a marshalling +// failure (-3), or a Go callback-interface implementation that panicked +// (-4). The code is preserved so callers can tell them apart. +type Error struct { + // Code is the numeric ABI error code. + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *Error) Error() string { + return fmt.Sprintf("edge_cases: %s (code %d)", e.Message, e.Code) +} + +// DebugLive reports how many resources of one kind the native library holds: +// 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned +// allocations. It's always 0 unless the library was built with leak checks +// enabled; tests use it to assert that nothing leaked. +func DebugLive(kind int32) uint64 { + return uint64(C.edge_cases_debug_live(C.int32_t(kind))) +} + +// wvCheckABI panics when the linked library implements a different C ABI +// revision than these bindings were generated for. +func wvCheckABI() { + if found := uint32(C.edge_cases_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("edge_cases: ABI mismatch: these bindings expect revision %d but the linked library reports revision %d", wvABIVersion, found)) + } +} + +// wvCheckModule panics when the linked library's contract checksum for a +// top-level module differs from the one these bindings were generated +// against, which means the bindings are stale. +func wvCheckModule(module string, want, found uint64) { + if found != want { + panic(fmt.Sprintf("edge_cases: module %q doesn't match the linked library (bindings checksum %#016x, library checksum %#016x); regenerate the bindings", module, want, found)) + } +} + +// ── Errors ── + +// wvFailure is a non-zero error slot copied into Go memory. +type wvFailure struct { + code int32 + message string + payload []byte +} + +// err converts the failure into the generic *Error. +func (f wvFailure) err() error { + return &Error{Code: f.code, Message: f.message} +} + +func wvCopyFailure(cErr *C.edge_cases_error) wvFailure { + f := wvFailure{code: int32(cErr.code)} + if cErr.message != nil { + f.message = C.GoString(cErr.message) + } + if cErr.payload_ptr != nil { + f.payload = wvBorrowBytes(cErr.payload_ptr, cErr.payload_len) + } + return f +} + +// wvTakeError copies a non-zero error slot and clears it. +func wvTakeError(cErr *C.edge_cases_error) wvFailure { + f := wvCopyFailure(cErr) + C.edge_cases_error_clear(cErr) + return f +} + +// wvTakeBoxedError copies the heap-boxed error an async completion receives +// and frees the box. +func wvTakeBoxedError(cErr *C.edge_cases_error) wvFailure { + f := wvCopyFailure(cErr) + C.edge_cases_error_free(cErr) + return f +} + +// wvTrap panics with the generic *Error when the slot reports a failure. +// Wrappers of functions that declare no error domain check their slot with +// it: a failure there can only be a producer bug, a marshalling failure, or +// a callback-interface implementation that panicked. +func wvTrap(cErr *C.edge_cases_error) { + if cErr.code != 0 { + panic(wvTakeError(cErr).err()) + } +} + +// wvForeignError reports a panic recovered inside a callback-interface +// trampoline through the producer's error slot (code -4). The producer +// copies the message, so it's only borrowed for the call. +func wvForeignError(outErr *C.edge_cases_error, recovered any) { + msg := C.CString(fmt.Sprint(recovered)) + defer C.free(unsafe.Pointer(msg)) + C.edge_cases_error_set(outErr, C.int32_t(wvCodeForeign), msg) +} + +// ── Strings and bytes ── + +// wvStr returns a borrowed (pointer, length) view of s's UTF-8 bytes for the +// duration of one call. Strings may contain NUL bytes. +func wvStr(s string) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(s))), C.size_t(len(s)) +} + +// wvBytes returns a borrowed (pointer, length) view of b for the duration of +// one call. +func wvBytes(b []byte) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.SliceData(b))), C.size_t(len(b)) +} + +// wvBorrowString copies a borrowed UTF-8 run into a Go string. +func wvBorrowString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + return string(unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) +} + +// wvTakeString copies a returned UTF-8 run into a Go string and releases the +// producer's allocation. +func wvTakeString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + s := wvBorrowString(ptr, n) + C.edge_cases_free_bytes(ptr, n) + return s +} + +// wvBorrowBytes copies a borrowed byte run into Go memory. A null pointer +// copies to nil. +func wvBorrowBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + return append([]byte{}, unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)...) +} + +// wvTakeBytes copies a returned byte run (raw bytes or a value buffer) into +// Go memory and releases the producer's allocation. +func wvTakeBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + b := wvBorrowBytes(ptr, n) + C.edge_cases_free_bytes(ptr, n) + return b +} + +// ── Objects ── + +// wvRef owns one strong reference to a native object on behalf of a wrapper. +// Calls borrow the pointer between acquire and release. Close may race them: +// the reference is destroyed exactly once, by close when no call is in +// flight, or else by the last in-flight call to finish. +type wvRef struct { + ptr unsafe.Pointer + destroy func(unsafe.Pointer) + // state counts in-flight calls in its upper bits; the low bit is set + // once close has run. + state atomic.Uint64 +} + +func (r *wvRef) init(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { + r.ptr = ptr + r.destroy = destroy +} + +// acquire borrows the pointer for one call, panicking when the wrapper was +// already closed. +func (r *wvRef) acquire(typeName string) unsafe.Pointer { + for { + s := r.state.Load() + if s&1 != 0 { + panic("edge_cases: " + typeName + " used after Close") + } + if r.state.CompareAndSwap(s, s+2) { + return r.ptr + } + } +} + +// release ends one call's borrow. +func (r *wvRef) release() { + if r.state.Add(^uint64(1)) == 1 { + r.destroy(r.ptr) + } +} + +// close releases the wrapper's reference once no call is in flight. Calling +// it again is a no-op. +func (r *wvRef) close() { + for { + s := r.state.Load() + if s&1 != 0 { + return + } + if r.state.CompareAndSwap(s, s|1) { + if s == 0 { + r.destroy(r.ptr) + } + return + } + } +} + +// ── Async ── + +// wvOutcome is one async completion: a converted result, or the failure the +// producer reported. +type wvOutcome[T any] struct { + val T + fail *wvFailure +} + +// wvComplete delivers one async completion to the waiting wrapper. It runs on +// a producer thread inside an exported trampoline, so it never panics: a +// result that fails to decode is delivered as a marshalling failure. +func wvComplete[T any](handle unsafe.Pointer, cErr *C.edge_cases_error, convert func() T) { + h := cgo.Handle(uintptr(handle)) + done := h.Value().(chan wvOutcome[T]) + h.Delete() + done <- wvSettle(cErr, convert) +} + +func wvSettle[T any](cErr *C.edge_cases_error, convert func() T) (o wvOutcome[T]) { + if cErr != nil { + if f := wvTakeBoxedError(cErr); f.code != 0 { + o.fail = &f + return o + } + } + defer func() { + if r := recover(); r != nil { + o = wvOutcome[T]{fail: &wvFailure{code: wvCodeMarshal, message: fmt.Sprint(r)}} + } + }() + o.val = convert() + return o +} + +// wvAwait blocks until an async call completes or ctx is done. With a cancel +// token, ctx's cancellation cancels the native call and waits for its +// completion, which the producer delivers promptly; without one, the call is +// abandoned and ctx.Err() returned at once. A cancelled completion (code -5) +// returns ctx.Err(), or context.Canceled when ctx itself isn't done. +func wvAwait[T any](ctx context.Context, done chan wvOutcome[T], token *C.edge_cases_cancel_token) (T, *wvFailure, error) { + var o wvOutcome[T] + select { + case o = <-done: + case <-ctx.Done(): + if token == nil { + var zero T + return zero, nil, ctx.Err() + } + C.edge_cases_cancel_token_cancel(token) + o = <-done + } + if o.fail != nil && o.fail.code == wvCodeCancelled { + var zero T + if err := ctx.Err(); err != nil { + return zero, nil, err + } + return zero, nil, context.Canceled + } + return o.val, o.fail, nil +} + +// END runtime.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__weaveffi_go.snap deleted file mode 100644 index d1d98c2a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__weaveffi_go.snap +++ /dev/null @@ -1,1900 +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 -static void* wvHandlePtr(uintptr_t h) { return (void*)h; } -extern void goWv_weaveffi_edge_cases_BatchListener_on_batch(void* ctx, uint8_t* items_ptr, size_t items_len, uint8_t* raw_ptr, size_t raw_len, uint8_t* first_ptr, size_t first_len, weaveffi_edge_cases_Mode mode, weaveffi_error* out_err); -extern void goWv_weaveffi_edge_cases_BatchListener_on_widget(void* ctx, weaveffi_edge_cases_Widget* widget, weaveffi_edge_cases_Widget* fallback, uint8_t* children_ptr, size_t children_len, weaveffi_error* out_err); -extern void goWv_weaveffi_edge_cases_BatchListener_on_tick(void* ctx, weaveffi_error* out_err); -extern bool goWv_weaveffi_edge_cases_BatchListener_should_continue(void* ctx, int32_t class, weaveffi_error* out_err); -extern weaveffi_edge_cases_Mode goWv_weaveffi_edge_cases_BatchListener_pick_mode(void* ctx, weaveffi_error* out_err); -extern void goWv_weaveffi_edge_cases_BatchListener_free(void* ctx); -static const weaveffi_edge_cases_BatchListener_vtable wvVtable_weaveffi_edge_cases_BatchListener = { - (void (*)(void*, const uint8_t*, size_t, const uint8_t*, size_t, const uint8_t*, size_t, weaveffi_edge_cases_Mode, weaveffi_error*))goWv_weaveffi_edge_cases_BatchListener_on_batch, - (void (*)(void*, weaveffi_edge_cases_Widget*, weaveffi_edge_cases_Widget*, const uint8_t*, size_t, weaveffi_error*))goWv_weaveffi_edge_cases_BatchListener_on_widget, - goWv_weaveffi_edge_cases_BatchListener_on_tick, - goWv_weaveffi_edge_cases_BatchListener_should_continue, - goWv_weaveffi_edge_cases_BatchListener_pick_mode, - goWv_weaveffi_edge_cases_BatchListener_free, -}; -static const weaveffi_edge_cases_BatchListener_vtable* wvVtablePtr_weaveffi_edge_cases_BatchListener(void) { return &wvVtable_weaveffi_edge_cases_BatchListener; } -extern void goWv_weaveffi_edge_cases_Keyword_delete(void* ctx, int32_t self, weaveffi_error* out_err); -extern int32_t goWv_weaveffi_edge_cases_Keyword_import(void* ctx, weaveffi_error* out_err); -extern void goWv_weaveffi_edge_cases_Keyword_free(void* ctx); -static const weaveffi_edge_cases_Keyword_vtable wvVtable_weaveffi_edge_cases_Keyword = { - goWv_weaveffi_edge_cases_Keyword_delete, - goWv_weaveffi_edge_cases_Keyword_import, - goWv_weaveffi_edge_cases_Keyword_free, -}; -static const weaveffi_edge_cases_Keyword_vtable* wvVtablePtr_weaveffi_edge_cases_Keyword(void) { return &wvVtable_weaveffi_edge_cases_Keyword; } -extern void goWv_weaveffi_edge_cases_async_widget_callback(void* context, weaveffi_error* err, weaveffi_edge_cases_Widget* result); -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" - "runtime" - "runtime/cgo" - "unicode/utf8" - "unsafe" -) - -// The ABI revision these bindings were generated against. -const wvABIVersion uint32 = 2 - -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, a -// producer panic (code -2), or a Go callback-interface implementation -// that panicked (code -4). -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, a marshalling failure, or a callback-interface -// implementation that panicked. The panic value is a *WeaveFFIError, so a -// recover() site can still inspect the code. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - panic(wvBrandError(wvTakeError(cErr))) - } -} - -// 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)) -} - -// wvForeignError reports a panic recovered inside a callback-interface -// trampoline through the producer's error slot (code -4). The message is -// borrowed for the call: the producer copies it with its own allocator. -func wvForeignError(outErr *C.weaveffi_error, recovered any) { - msg := C.CString(fmt.Sprint(recovered)) - defer C.free(unsafe.Pointer(msg)) - C.weaveffi_error_set(outErr, -4, msg) -} - -// 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 -} - -// WidgetHolder: Record carrying objects in every buffered position -type WidgetHolder struct { - Primary *Widget - Fallback *Widget - Extras []*Widget - ByName map[string]*Widget -} - -// wvPackWidgetHolder appends v to w in the value-buffer wire format. -func wvPackWidgetHolder(w *wvWriter, v WidgetHolder) { - w.writeU64(wvTokenWidget(v.Primary)) - if v.Fallback == nil { - w.writeOptionFlag(false) - } else { - w.writeOptionFlag(true) - w.writeU64(wvTokenWidget(v.Fallback)) - } - w.writeLen(len(v.Extras)) - for _, eExtras0 := range v.Extras { - w.writeU64(wvTokenWidget(eExtras0)) - } - w.writeLen(len(v.ByName)) - for kByName0, vByName0 := range v.ByName { - w.writeString(kByName0) - w.writeU64(wvTokenWidget(vByName0)) - } -} - -// wvUnpackWidgetHolder decodes one WidgetHolder from r. -func wvUnpackWidgetHolder(r *wvReader) WidgetHolder { - var v WidgetHolder - v.Primary = wvUntokenWidget(r.readU64()) - if r.readOptionFlag() { - var oFallback0 *Widget - oFallback0 = wvUntokenWidget(r.readU64()) - v.Fallback = oFallback0 - } - nExtras0 := r.readLen() - v.Extras = make([]*Widget, nExtras0) - for iExtras0 := range v.Extras { - v.Extras[iExtras0] = wvUntokenWidget(r.readU64()) - } - nByName0 := r.readLen() - v.ByName = make(map[string]*Widget, nByName0) - for iByName0 := 0; iByName0 < nByName0; iByName0++ { - var kByName0 string - kByName0 = r.readString() - var vByName0 *Widget - vByName0 = wvUntokenWidget(r.readU64()) - v.ByName[kByName0] = vByName0 - } - return v -} - -// BatchListener: Callback interface carrying composite payloads and objects -// -// Implement this interface in Go and pass the value to native functions -// that accept it. -// -// The native library may call any method from any thread until it releases -// the implementation. A panic in a method is reported to the native caller -// as a foreign error (code -4) instead of crashing the process. -type BatchListener interface { - // OnBatch: Composite payloads cross as value buffers - OnBatch(items []Outer, raw []byte, first *Outer, mode Mode) - // OnWidget: Objects cross as strong references the consumer must release - OnWidget(widget *Widget, fallback *Widget, children []*Widget) - OnTick() - // ShouldContinue: Scalar return travels back through the vtable - ShouldContinue(class int32) bool - // PickMode: Enum return travels back as its integer discriminant - PickMode() Mode -} - -//export goWv_weaveffi_edge_cases_BatchListener_on_batch -func goWv_weaveffi_edge_cases_BatchListener_on_batch(ctx unsafe.Pointer, items_ptr *C.uint8_t, items_len C.size_t, raw_ptr *C.uint8_t, raw_len C.size_t, first_ptr *C.uint8_t, first_len C.size_t, mode C.weaveffi_edge_cases_Mode, out_err *C.weaveffi_error) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) - 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) - impl.OnBatch(arg0, arg1, arg2, arg3) -} - -//export goWv_weaveffi_edge_cases_BatchListener_on_widget -func goWv_weaveffi_edge_cases_BatchListener_on_widget(ctx unsafe.Pointer, widget *C.weaveffi_edge_cases_Widget, fallback *C.weaveffi_edge_cases_Widget, children_ptr *C.uint8_t, children_len C.size_t, out_err *C.weaveffi_error) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) - arg0 := wvAdoptWidget(widget) - arg1 := wvAdoptWidget(fallback) - rArg2 := &wvReader{buf: wvBorrowBuffer(children_ptr, children_len)} - var arg2 []*Widget - nArg20 := rArg2.readLen() - arg2 = make([]*Widget, nArg20) - for iArg20 := range arg2 { - arg2[iArg20] = wvUntokenWidget(rArg2.readU64()) - } - rArg2.expectEnd() - impl.OnWidget(arg0, arg1, arg2) -} - -//export goWv_weaveffi_edge_cases_BatchListener_on_tick -func goWv_weaveffi_edge_cases_BatchListener_on_tick(ctx unsafe.Pointer, out_err *C.weaveffi_error) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) - impl.OnTick() -} - -//export goWv_weaveffi_edge_cases_BatchListener_should_continue -func goWv_weaveffi_edge_cases_BatchListener_should_continue(ctx unsafe.Pointer, class C.int32_t, out_err *C.weaveffi_error) (ret C._Bool) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) - arg0 := int32(class) - ret = boolToC(impl.ShouldContinue(arg0)) - return -} - -//export goWv_weaveffi_edge_cases_BatchListener_pick_mode -func goWv_weaveffi_edge_cases_BatchListener_pick_mode(ctx unsafe.Pointer, out_err *C.weaveffi_error) (ret C.weaveffi_edge_cases_Mode) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(BatchListener) - ret = C.weaveffi_edge_cases_Mode(impl.PickMode()) - return -} - -//export goWv_weaveffi_edge_cases_BatchListener_free -func goWv_weaveffi_edge_cases_BatchListener_free(ctx unsafe.Pointer) { - cgo.Handle(uintptr(ctx)).Delete() -} - -// Keyword: Callback interface whose name and methods are keywords -// -// Implement this interface in Go and pass the value to native functions -// that accept it. -// -// The native library may call any method from any thread until it releases -// the implementation. A panic in a method is reported to the native caller -// as a foreign error (code -4) instead of crashing the process. -type Keyword interface { - Delete(self int32) - Import() int32 -} - -//export goWv_weaveffi_edge_cases_Keyword_delete -func goWv_weaveffi_edge_cases_Keyword_delete(ctx unsafe.Pointer, self C.int32_t, out_err *C.weaveffi_error) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(Keyword) - arg0 := int32(self) - impl.Delete(arg0) -} - -//export goWv_weaveffi_edge_cases_Keyword_import -func goWv_weaveffi_edge_cases_Keyword_import(ctx unsafe.Pointer, out_err *C.weaveffi_error) (ret C.int32_t) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(Keyword) - ret = C.int32_t(impl.Import()) - return -} - -//export goWv_weaveffi_edge_cases_Keyword_free -func goWv_weaveffi_edge_cases_Keyword_free(ctx unsafe.Pointer) { - cgo.Handle(uintptr(ctx)).Delete() -} - -// Widget: Interface with no constructor; instances come from make_widget -// -// Each wrapper holds one strong reference; Close releases it, and a -// finalizer releases it if the wrapper is garbage collected first. -type Widget struct { - ptr *C.weaveffi_edge_cases_Widget -} - -// wvAdoptWidget adopts one owned strong reference into a new wrapper. A null -// pointer adopts to nil. -func wvAdoptWidget(ptr *C.weaveffi_edge_cases_Widget) *Widget { - if ptr == nil { - return nil - } - s := &Widget{ptr: ptr} - runtime.SetFinalizer(s, (*Widget).Close) - return s -} - -// wvTokenWidget clones o's reference into a value-buffer object token. The -// wrapper keeps its own reference; the token carries the new one. -func wvTokenWidget(o *Widget) uint64 { - if o == nil || o.ptr == nil { - panic("weaveffi: nil or closed Widget cannot be encoded in a non-optional position") - } - return uint64(uintptr(unsafe.Pointer(C.weaveffi_edge_cases_Widget_clone(o.ptr)))) -} - -// wvUntokenWidget adopts the strong reference carried by a value-buffer object -// token. -func wvUntokenWidget(token uint64) *Widget { - if token == 0 { - panic("weaveffi: malformed value buffer: null object token") - } - return wvAdoptWidget((*C.weaveffi_edge_cases_Widget)(C.wvHandlePtr(C.uintptr_t(token)))) -} - -// Refresh: Void method that can fail -func (s *Widget) Refresh() error { - if s.ptr == nil { - panic("weaveffi: Widget used after Close") - } - defer runtime.KeepAlive(s) - 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 { - if s.ptr == nil { - panic("weaveffi: Widget used after Close") - } - defer runtime.KeepAlive(s) - 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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesWidgetFetch) - h.Delete() - 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) { - if s.ptr == nil { - panic("weaveffi: Widget used after Close") - } - defer runtime.KeepAlive(s) - ch := make(chan wvOutcomeEdgeCasesWidgetFetch, 1) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - outcome := <-ch - if outcome.err != nil { - return Outer{}, outcome.err - } - return outcome.val, nil -} - -// Deprecated: Use rename instead -func (s *Widget) OldName() string { - if s.ptr == nil { - panic("weaveffi: Widget used after Close") - } - defer runtime.KeepAlive(s) - 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 -} - -// Close releases the wrapper's strong reference. Calling it more than once -// is harmless; the object itself is dropped by the native library when its -// last reference (from any wrapper, record, or producer) is released. -func (s *Widget) Close() { - if s.ptr != nil { - C.weaveffi_edge_cases_Widget_destroy(s.ptr) - s.ptr = nil - runtime.SetFinalizer(s, nil) - } -} - -// Vault: Deprecated interface with a throwing constructor -// -// Each wrapper holds one strong reference; Close releases it, and a -// finalizer releases it if the wrapper is garbage collected first. -// -// Deprecated: Use Widget instead -type Vault struct { - ptr *C.weaveffi_edge_cases_Vault -} - -// wvAdoptVault adopts one owned strong reference into a new wrapper. A null -// pointer adopts to nil. -func wvAdoptVault(ptr *C.weaveffi_edge_cases_Vault) *Vault { - if ptr == nil { - return nil - } - s := &Vault{ptr: ptr} - runtime.SetFinalizer(s, (*Vault).Close) - return s -} - -// wvTokenVault clones o's reference into a value-buffer object token. The -// wrapper keeps its own reference; the token carries the new one. -func wvTokenVault(o *Vault) uint64 { - if o == nil || o.ptr == nil { - panic("weaveffi: nil or closed Vault cannot be encoded in a non-optional position") - } - return uint64(uintptr(unsafe.Pointer(C.weaveffi_edge_cases_Vault_clone(o.ptr)))) -} - -// wvUntokenVault adopts the strong reference carried by a value-buffer object -// token. -func wvUntokenVault(token uint64) *Vault { - if token == 0 { - panic("weaveffi: malformed value buffer: null object token") - } - return wvAdoptVault((*C.weaveffi_edge_cases_Vault)(C.wvHandlePtr(C.uintptr_t(token)))) -} - -// 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 wvAdoptVault(result), nil -} - -func (s *Vault) Peek() *Widget { - if s.ptr == nil { - panic("weaveffi: Vault used after Close") - } - defer runtime.KeepAlive(s) - var cErr C.weaveffi_error - result := C.weaveffi_edge_cases_Vault_peek(s.ptr, &cErr) - wvTrap(&cErr) - return wvAdoptWidget(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 -} - -// Close releases the wrapper's strong reference. Calling it more than once -// is harmless; the object itself is dropped by the native library when its -// last reference (from any wrapper, record, or producer) is released. -func (s *Vault) Close() { - if s.ptr != nil { - C.weaveffi_edge_cases_Vault_destroy(s.ptr) - s.ptr = nil - runtime.SetFinalizer(s, nil) - } -} - -// 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 -} - -// RegisterBatch: Two callback interface parameters on one function -func RegisterBatch(listener BatchListener, keyword Keyword) { - hListener := cgo.NewHandle(listener) - hKeyword := cgo.NewHandle(keyword) - var cErr C.weaveffi_error - C.weaveffi_edge_cases_register_batch(C.wvHandlePtr(C.uintptr_t(hListener)), C.wvVtablePtr_weaveffi_edge_cases_BatchListener(), C.wvHandlePtr(C.uintptr_t(hKeyword)), C.wvVtablePtr_weaveffi_edge_cases_Keyword(), &cErr) - wvTrap(&cErr) -} - -// WidgetsEverywhere: Objects in every buffered position -func WidgetsEverywhere(list []*Widget, byName map[string]*Widget, sparse []*Widget, holder WidgetHolder) WidgetHolder { - wList := &wvWriter{} - wList.writeLen(len(list)) - for _, eList0 := range list { - wList.writeU64(wvTokenWidget(eList0)) - } - var cListPtr *C.uint8_t - if len(wList.buf) > 0 { - cListPtr = (*C.uint8_t)(unsafe.Pointer(&wList.buf[0])) - } - wByName := &wvWriter{} - wByName.writeLen(len(byName)) - for kByName0, vByName0 := range byName { - wByName.writeString(kByName0) - wByName.writeU64(wvTokenWidget(vByName0)) - } - var cByNamePtr *C.uint8_t - if len(wByName.buf) > 0 { - cByNamePtr = (*C.uint8_t)(unsafe.Pointer(&wByName.buf[0])) - } - wSparse := &wvWriter{} - wSparse.writeLen(len(sparse)) - for _, eSparse0 := range sparse { - if eSparse0 == nil { - wSparse.writeOptionFlag(false) - } else { - wSparse.writeOptionFlag(true) - wSparse.writeU64(wvTokenWidget(eSparse0)) - } - } - var cSparsePtr *C.uint8_t - if len(wSparse.buf) > 0 { - cSparsePtr = (*C.uint8_t)(unsafe.Pointer(&wSparse.buf[0])) - } - wHolder := &wvWriter{} - wvPackWidgetHolder(wHolder, holder) - var cHolderPtr *C.uint8_t - if len(wHolder.buf) > 0 { - cHolderPtr = (*C.uint8_t)(unsafe.Pointer(&wHolder.buf[0])) - } - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_edge_cases_widgets_everywhere(cListPtr, C.size_t(len(wList.buf)), cByNamePtr, C.size_t(len(wByName.buf)), cSparsePtr, C.size_t(len(wSparse.buf)), cHolderPtr, C.size_t(len(wHolder.buf)), &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult WidgetHolder - goResult = wvUnpackWidgetHolder(rRes) - rRes.expectEnd() - return goResult -} - -// WidgetStream: Iterator of objects -// 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 WidgetStream() iter.Seq[*Widget] { - return func(yield func(*Widget) bool) { - var cErr C.weaveffi_error - it := C.weaveffi_edge_cases_widget_stream(&cErr) - wvTrap(&cErr) - defer C.weaveffi_edge_cases_WidgetStreamIterator_destroy(it) - for { - var outItem *C.weaveffi_edge_cases_Widget - var iterErr C.weaveffi_error - ok := C.weaveffi_edge_cases_WidgetStreamIterator_next(it, &outItem, &iterErr) != 0 - wvTrap(&iterErr) - if !ok { - return - } - item := wvAdoptWidget(outItem) - if !yield(item) { - return - } - } - } -} - -type wvOutcomeEdgeCasesAsyncWidget struct { - val *Widget - err error -} - -//export goWv_weaveffi_edge_cases_async_widget_callback -func goWv_weaveffi_edge_cases_async_widget_callback(context unsafe.Pointer, err *C.weaveffi_error, result *C.weaveffi_edge_cases_Widget) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesAsyncWidget) - h.Delete() - if err != nil && err.code != 0 { - ch <- wvOutcomeEdgeCasesAsyncWidget{err: wvMapEdgeCases(wvTakeBoxedError(err))} - return - } - ch <- wvOutcomeEdgeCasesAsyncWidget{val: wvAdoptWidget(result)} -} - -// AsyncWidget: Async returning an object -// Blocks the calling goroutine until the async producer completes. -func AsyncWidget() (*Widget, error) { - ch := make(chan wvOutcomeEdgeCasesAsyncWidget, 1) - h := cgo.NewHandle(ch) - C.weaveffi_edge_cases_async_widget_async(C.weaveffi_edge_cases_async_widget_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_async_widget_callback)), C.wvHandlePtr(C.uintptr_t(h))) - outcome := <-ch - if outcome.err != nil { - return nil, outcome.err - } - return outcome.val, nil -} - -// 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 wvAdoptWidget(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)) - } - return wvAdoptWidget(result), nil -} - -// InspectWidget: Interface parameter, including an optional one -func InspectWidget(widget *Widget, fallback *Widget) string { - defer runtime.KeepAlive(widget) - defer runtime.KeepAlive(fallback) - 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 -} - -// 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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesAsyncVoid) - h.Delete() - 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) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - 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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesAsyncBytes) - h.Delete() - 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) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - 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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesAsyncStruct) - h.Delete() - 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) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - 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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesAsyncList) - h.Delete() - 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) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - 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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesAsyncOptional) - h.Delete() - 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) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - 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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesAsyncEnum) - h.Delete() - 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) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - 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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeEdgeCasesAsyncKeywords) - h.Delete() - 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) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - 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_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__README_md.snap index 7d3c4f86..3a991a52 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__README_md.snap @@ -1,53 +1,49 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Go Bindings +# kitchen_sink (Go) -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`) +cgo bindings for the `kitchen_sink` native library. ## 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`: +The module ships its own copy of the C header (`kitchen_sink.h`) and links with +`-lkitchen_sink`. Point the linker at the directory holding `libkitchen_sink.so`, +`libkitchen_sink.dylib`, or `kitchen_sink.dll`, and make the library findable at +run time: ```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" +export CGO_LDFLAGS="-L/path/to/lib" +export LD_LIBRARY_PATH="/path/to/lib" # DYLD_LIBRARY_PATH on macOS +go build ./... ``` -2. Build or run your Go project that imports this module: +Depend on the module from your own project with a `require` (and, for a +local checkout, a `replace`) of `kitchen_sink`, then import it: -```sh -go build ./... +```go +import "kitchen_sink" ``` -## 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. - -Interfaces are reference-counted objects: each wrapper holds one strong -reference that `Close()` releases (a finalizer releases it if you forget), and -a nullable interface is a nil wrapper pointer. Callback interfaces are Go -`interface` types: implement one and pass the value to any function that -accepts it; the native library may call it from any thread, and a panic in -your implementation is reported to the native caller instead of crashing. +Go 1.23 or newer is required. + +## Usage notes + +- Records are plain structs and rich enums are sealed interfaces; both cross + the boundary by value. +- Interfaces are reference-counted objects. Call `Close` when you're done + with one (a finalizer releases it otherwise); it's safe to call twice or + while a call is in flight. +- Async functions take a `context.Context` first. Cancellable ones cancel + the native call when the context is done. +- Callback interfaces are Go interfaces you implement. The library may call + them from any thread, and a panic is reported to the native caller as a + foreign error instead of crashing. +- Loading checks the library's ABI revision and contract checksums and + panics with the stale module's name on a mismatch. diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__bindings_go.snap new file mode 100644 index 00000000..b727ce48 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__bindings_go.snap @@ -0,0 +1,684 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +/* +#cgo LDFLAGS: -lkitchen_sink +#cgo CFLAGS: -Wno-deprecated-declarations +#include "kitchen_sink.h" +__attribute__((unused)) static void* wvHandlePtr(uintptr_t h) { return (void*)h; } +extern void goWv_kitchen_sink_kitchen_ReadyListener_on_ready(void* ctx, int32_t code, uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); +extern bool goWv_kitchen_sink_kitchen_ReadyListener_on_item(void* ctx, uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); +extern void goWv_kitchen_sink_kitchen_ReadyListener_free(void* ctx); +static const kitchen_sink_kitchen_ReadyListener_vtable wvVtable_kitchen_sink_kitchen_ReadyListener_vtable = { + (void (*)(void*, int32_t, const uint8_t*, size_t, kitchen_sink_error*))goWv_kitchen_sink_kitchen_ReadyListener_on_ready, + (bool (*)(void*, const uint8_t*, size_t, kitchen_sink_kitchen_Gadget*, kitchen_sink_error*))goWv_kitchen_sink_kitchen_ReadyListener_on_item, + goWv_kitchen_sink_kitchen_ReadyListener_free, +}; +__attribute__((unused)) static const kitchen_sink_kitchen_ReadyListener_vtable* wvVtablePtr_kitchen_sink_kitchen_ReadyListener_vtable(void) { return &wvVtable_kitchen_sink_kitchen_ReadyListener_vtable; } +extern void goWv_kitchen_sink_kitchen_do_async_callback(void* context_, kitchen_sink_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_kitchen_sink_kitchen_do_cancellable_callback(void* context_, kitchen_sink_error* err, uint8_t* result_ptr, size_t result_len); +*/ +import "C" + +import ( + "context" + "fmt" + "iter" + "runtime" + "runtime/cgo" + "unsafe" +) + +func init() { + wvCheckABI() + wvCheckModule("shared", 0x42c4ce2c8d0af052, uint64(C.kitchen_sink_shared_checksum())) + wvCheckModule("kitchen", 0xd83da75b24b545ff, uint64(C.kitchen_sink_kitchen_checksum())) +} + +// Token: A reusable token referenced across modules +type Token struct { + Id int64 + Label string +} + +// wvPackToken appends v to w in the value-buffer wire format. +func wvPackToken(w *wvWriter, v Token) { + w.writeI64(v.Id) + w.writeString(v.Label) +} + +// wvUnpackToken decodes one Token from r. +func wvUnpackToken(r *wvReader) Token { + var v Token + v.Id = r.readI64() + v.Label = r.readString() + return v +} + +// Ping: Trivial cross-module helper +func Ping() string { + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_shared_ping(&cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +// KitchenErrorsError is a typed error reported by the `kitchen` module. +type KitchenErrorsError struct { + // Code is the numeric ABI error code (one of the KitchenErrorsError constants). + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *KitchenErrorsError) Error() string { + return fmt.Sprintf("kitchen: %s (code %d)", e.Message, e.Code) +} + +// KitchenErrorsError codes. +const ( + // KitchenErrorsErrorNotFound: Item not found + KitchenErrorsErrorNotFound int32 = 1 + // KitchenErrorsErrorInvalidInput: Invalid input + KitchenErrorsErrorInvalidInput int32 = 2 +) + +// wvMapKitchen converts a non-zero code from the `kitchen` domain into a +// *KitchenErrorsError, falling back to the generic *Error for unknown codes. +func wvMapKitchen(f wvFailure) error { + switch f.code { + case KitchenErrorsErrorNotFound: + if f.message == "" { + f.message = "Item not found" + } + return &KitchenErrorsError{Code: f.code, Message: f.message} + case KitchenErrorsErrorInvalidInput: + if f.message == "" { + f.message = "Invalid input" + } + return &KitchenErrorsError{Code: f.code, Message: f.message} + default: + return f.err() + } +} + +// Priority: Task priority levels +type Priority int32 + +const ( + // PriorityLow: Low priority + PriorityLow Priority = 0 + // PriorityNormal: Normal priority + PriorityNormal Priority = 1 + // PriorityHigh: High priority + PriorityHigh Priority = 2 +) + +// Item: Kitchen sink struct exercising every field feature +type Item struct { + // Id: Stable identifier + Id int64 + // Name: Display name + Name string + // Count: Initial count + Count int32 + // Enabled: Whether the item is enabled + Enabled bool + // Ratio: Scaling ratio + Ratio float64 + Nick uint32 + Payload []byte + Tags []string + Attrs map[string]string + Parent *int64 + // Token: Cross-module struct reference + Token Token + Priority Priority + // Gadget: An optional object inside a record + Gadget *Gadget +} + +// wvPackItem appends v to w in the value-buffer wire format. +func wvPackItem(w *wvWriter, v Item) { + w.writeI64(v.Id) + w.writeString(v.Name) + w.writeI32(v.Count) + w.writeBool(v.Enabled) + w.writeF64(v.Ratio) + w.writeU32(v.Nick) + w.writeBytes(v.Payload) + w.writeLen(len(v.Tags)) + for _, eTags0 := range v.Tags { + w.writeString(eTags0) + } + w.writeLen(len(v.Attrs)) + for kAttrs0, vAttrs0 := range v.Attrs { + w.writeString(kAttrs0) + w.writeString(vAttrs0) + } + if v.Parent == nil { + w.writeOptionFlag(false) + } else { + w.writeOptionFlag(true) + w.writeI64(*v.Parent) + } + wvPackToken(w, v.Token) + w.writeI32(int32(v.Priority)) + if v.Gadget == nil { + w.writeOptionFlag(false) + } else { + w.writeOptionFlag(true) + w.writeU64(wvTokenGadget(v.Gadget)) + } +} + +// wvUnpackItem decodes one Item from r. +func wvUnpackItem(r *wvReader) Item { + var v Item + v.Id = r.readI64() + v.Name = r.readString() + v.Count = r.readI32() + v.Enabled = r.readBool() + v.Ratio = r.readF64() + v.Nick = r.readU32() + v.Payload = r.readBytes() + nTags0 := r.readLen() + v.Tags = make([]string, 0, r.capHint(nTags0)) + for range nTags0 { + var eTags0 string + eTags0 = r.readString() + v.Tags = append(v.Tags, eTags0) + } + nAttrs0 := r.readLen() + v.Attrs = make(map[string]string, r.capHint(nAttrs0)) + for range nAttrs0 { + var kAttrs0 string + kAttrs0 = r.readString() + var vAttrs0 string + vAttrs0 = r.readString() + v.Attrs[kAttrs0] = vAttrs0 + } + if r.readOptionFlag() { + var oParent0 int64 + oParent0 = r.readI64() + v.Parent = &oParent0 + } + v.Token = wvUnpackToken(r) + v.Priority = Priority(r.readI32()) + if r.readOptionFlag() { + var oGadget0 *Gadget + oGadget0 = wvUntokenGadget(r.readU64()) + v.Gadget = oGadget0 + } + return v +} + +// ReadyListener: Consumer-implemented observer of ready items +// +// Implement this interface in Go and pass the value to native functions +// that accept it. +// +// The native library may call any method from any thread until it releases +// the implementation. A panic in a method is reported to the native caller +// as a foreign error (code -4) instead of crashing the process. +type ReadyListener interface { + // OnReady: Fires when an item is ready + OnReady(code int32, msg string) + // OnItem: Receives the item itself and says whether to keep listening + OnItem(item Item, gadget *Gadget) bool +} + +//export goWv_kitchen_sink_kitchen_ReadyListener_on_ready +func goWv_kitchen_sink_kitchen_ReadyListener_on_ready(ctx unsafe.Pointer, code C.int32_t, msg_ptr *C.uint8_t, msg_len C.size_t, out_err *C.kitchen_sink_error) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(ReadyListener) + arg0 := int32(code) + arg1 := wvBorrowString(msg_ptr, msg_len) + impl.OnReady(arg0, arg1) +} + +//export goWv_kitchen_sink_kitchen_ReadyListener_on_item +func goWv_kitchen_sink_kitchen_ReadyListener_on_item(ctx unsafe.Pointer, item_ptr *C.uint8_t, item_len C.size_t, gadget *C.kitchen_sink_kitchen_Gadget, out_err *C.kitchen_sink_error) (ret C._Bool) { + defer func() { + if r := recover(); r != nil { + wvForeignError(out_err, r) + } + }() + impl := cgo.Handle(uintptr(ctx)).Value().(ReadyListener) + rArg0 := &wvReader{buf: wvBorrowBytes(item_ptr, item_len)} + var arg0 Item + arg0 = wvUnpackItem(rArg0) + rArg0.expectEnd() + arg1 := wvAdoptGadget(gadget) + ret = C._Bool(impl.OnItem(arg0, arg1)) + return +} + +//export goWv_kitchen_sink_kitchen_ReadyListener_free +func goWv_kitchen_sink_kitchen_ReadyListener_free(ctx unsafe.Pointer) { + cgo.Handle(uintptr(ctx)).Delete() +} + +// Gadget: A pocket-sized interface exercising the object surface +// +// Each wrapper holds one strong reference; Close releases it, and a +// finalizer releases it if the wrapper is garbage collected first. +type Gadget struct { + ref wvRef +} + +func wvDestroyGadget(ptr unsafe.Pointer) { + C.kitchen_sink_kitchen_Gadget_destroy((*C.kitchen_sink_kitchen_Gadget)(ptr)) +} + +// wvAdoptGadget adopts one owned strong reference into a new wrapper. A null +// pointer adopts to nil. +func wvAdoptGadget(ptr *C.kitchen_sink_kitchen_Gadget) *Gadget { + if ptr == nil { + return nil + } + s := &Gadget{} + s.ref.init(unsafe.Pointer(ptr), wvDestroyGadget) + runtime.SetFinalizer(s, (*Gadget).Close) + return s +} + +// native borrows the wrapper's pointer for one call; pair it with a +// deferred s.ref.release(). +func (s *Gadget) native() *C.kitchen_sink_kitchen_Gadget { + if s == nil { + panic("kitchen_sink: nil *Gadget") + } + return (*C.kitchen_sink_kitchen_Gadget)(s.ref.acquire("Gadget")) +} + +// wvTokenGadget clones o's reference into a value-buffer object token. The +// wrapper keeps its own reference; the token carries the new one. +func wvTokenGadget(o *Gadget) uint64 { + ptr := o.native() + defer o.ref.release() + return uint64(uintptr(unsafe.Pointer(C.kitchen_sink_kitchen_Gadget_clone(ptr)))) +} + +// wvUntokenGadget adopts the strong reference carried by a value-buffer object +// token. +func wvUntokenGadget(token uint64) *Gadget { + if token == 0 { + panic("kitchen_sink: malformed value buffer: null object token") + } + return wvAdoptGadget((*C.kitchen_sink_kitchen_Gadget)(C.wvHandlePtr(C.uintptr_t(token)))) +} + +// Close releases the wrapper's strong reference and always returns nil. +// It's idempotent and safe to call from any goroutine, even while a call +// on the wrapper is in flight: the reference is then released when that +// call returns. The object itself is dropped once its last reference, from +// any wrapper, record, or the native side, is gone. +func (s *Gadget) Close() error { + runtime.SetFinalizer(s, nil) + s.ref.close() + return nil +} + +// NewGadget: Create a gadget with the given id +func NewGadget(id int64) *Gadget { + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_Gadget_new(C.int64_t(id), &cErr) + wvTrap(&cErr) + return wvAdoptGadget(cRet) +} + +// Describe: Render the gadget as a human-readable string +func (s *Gadget) Describe() string { + cSelf := s.native() + defer s.ref.release() + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_Gadget_describe(cSelf, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +// Poke: Poke the gadget a number of times, failing on a negative count +func (s *Gadget) Poke(times int32) (int32, error) { + cSelf := s.native() + defer s.ref.release() + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_Gadget_poke(cSelf, C.int32_t(times), &cErr) + if cErr.code != 0 { + return 0, wvMapKitchen(wvTakeError(&cErr)) + } + return int32(cRet), nil +} + +// GadgetVersion: The gadget subsystem version string +func GadgetVersion() string { + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_Gadget_version(&cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +// BoolId: Identity for bool +func BoolId(x bool) bool { + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_bool_id(C._Bool(x), &cErr) + wvTrap(&cErr) + return bool(cRet) +} + +func I32Id(x int32) int32 { + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_i32_id(C.int32_t(x), &cErr) + wvTrap(&cErr) + return int32(cRet) +} + +func U32Id(x uint32) uint32 { + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_u32_id(C.uint32_t(x), &cErr) + wvTrap(&cErr) + return uint32(cRet) +} + +func I64Id(x int64) int64 { + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_i64_id(C.int64_t(x), &cErr) + wvTrap(&cErr) + return int64(cRet) +} + +func F64Id(x float64) float64 { + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_f64_id(C.double(x), &cErr) + wvTrap(&cErr) + return float64(cRet) +} + +func EchoString(s string) string { + cSPtr, cSLen := wvStr(s) + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_echo_string(cSPtr, cSLen, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +func EchoBytes(b []byte) []byte { + cBPtr, cBLen := wvBytes(b) + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_echo_bytes(cBPtr, cBLen, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeBytes(cRet, cRetLen) +} + +// Subscribe: Register a listener, returning how many are attached +func Subscribe(listener ReadyListener) int32 { + hListener := cgo.NewHandle(listener) + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_subscribe(C.wvHandlePtr(C.uintptr_t(hListener)), C.wvVtablePtr_kitchen_sink_kitchen_ReadyListener_vtable(), &cErr) + wvTrap(&cErr) + return int32(cRet) +} + +// MaybeGadget: Optional object in both directions +func MaybeGadget(fallback *Gadget) *Gadget { + var cFallback *C.kitchen_sink_kitchen_Gadget + if fallback != nil { + cFallback = fallback.native() + defer fallback.ref.release() + } + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_maybe_gadget(cFallback, &cErr) + wvTrap(&cErr) + return wvAdoptGadget(cRet) +} + +// Gadgets: A list of objects +func Gadgets() []*Gadget { + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_gadgets(&cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret []*Gadget + nRet0 := rRet.readLen() + ret = make([]*Gadget, 0, rRet.capHint(nRet0)) + for range nRet0 { + var eRet0 *Gadget + eRet0 = wvUntokenGadget(rRet.readU64()) + ret = append(ret, eRet0) + } + rRet.expectEnd() + return ret +} + +// StreamGadgets: An iterator over objects +// +// The sequence launches the native iterator on each range and calls next once +// per element; the iterator is destroyed when the range ends, early or not. +func StreamGadgets() iter.Seq[*Gadget] { + return func(yield func(*Gadget) bool) { + var cErr C.kitchen_sink_error + cIter := C.kitchen_sink_kitchen_stream_gadgets(&cErr) + wvTrap(&cErr) + defer C.kitchen_sink_kitchen_StreamGadgetsIterator_destroy(cIter) + for { + var cItem *C.kitchen_sink_kitchen_Gadget + var cIterErr C.kitchen_sink_error + more := C.kitchen_sink_kitchen_StreamGadgetsIterator_next(cIter, &cItem, &cIterErr) != 0 + wvTrap(&cIterErr) + if !more { + return + } + item := wvAdoptGadget(cItem) + if !yield(item) { + return + } + } + } +} + +// MaybeItem: Optional struct return +func MaybeItem(id int64) (*Item, error) { + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_maybe_item(C.int64_t(id), &cRetLen, &cErr) + if cErr.code != 0 { + return nil, wvMapKitchen(wvTakeError(&cErr)) + } + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret *Item + if rRet.readOptionFlag() { + var oRet0 Item + oRet0 = wvUnpackItem(rRet) + ret = &oRet0 + } + rRet.expectEnd() + return ret, nil +} + +// ListItems: List of structs +func ListItems() []Item { + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_list_items(&cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret []Item + nRet0 := rRet.readLen() + ret = make([]Item, 0, rRet.capHint(nRet0)) + for range nRet0 { + var eRet0 Item + eRet0 = wvUnpackItem(rRet) + ret = append(ret, eRet0) + } + rRet.expectEnd() + return ret +} + +// GetAttrs: Map return type +func GetAttrs() map[string]int32 { + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_get_attrs(&cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret map[string]int32 + nRet0 := rRet.readLen() + ret = make(map[string]int32, rRet.capHint(nRet0)) + for range nRet0 { + var kRet0 string + kRet0 = rRet.readString() + var vRet0 int32 + vRet0 = rRet.readI32() + ret[kRet0] = vRet0 + } + rRet.expectEnd() + return ret +} + +// StreamItems: Iterator return type +// +// The sequence launches the native iterator on each range and calls next once +// per element; the iterator is destroyed when the range ends, early or not. +func StreamItems() iter.Seq[Item] { + return func(yield func(Item) bool) { + var cErr C.kitchen_sink_error + cIter := C.kitchen_sink_kitchen_stream_items(&cErr) + wvTrap(&cErr) + defer C.kitchen_sink_kitchen_StreamItemsIterator_destroy(cIter) + for { + var cItem *C.uint8_t + var cItemLen C.size_t + var cIterErr C.kitchen_sink_error + more := C.kitchen_sink_kitchen_StreamItemsIterator_next(cIter, &cItem, &cItemLen, &cIterErr) != 0 + wvTrap(&cIterErr) + if !more { + return + } + rItem := &wvReader{buf: wvTakeBytes(cItem, cItemLen)} + var item Item + item = wvUnpackItem(rItem) + rItem.expectEnd() + if !yield(item) { + return + } + } + } +} + +// CrossModuleToken: Returns the shared Token type from another module +func CrossModuleToken() Token { + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_cross_module_token(&cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Token + ret = wvUnpackToken(rRet) + rRet.expectEnd() + return ret +} + +//export goWv_kitchen_sink_kitchen_do_async_callback +func goWv_kitchen_sink_kitchen_do_async_callback(context_ unsafe.Pointer, err *C.kitchen_sink_error, result_ptr *C.uint8_t, result_len C.size_t) { + wvComplete(context_, err, func() string { + return wvTakeString(result_ptr, result_len) + }) +} + +// DoAsync: Async operation +// +// It blocks until the call completes or ctx is done; in the latter case it +// returns ctx.Err() at once and the native result is discarded. +func DoAsync(ctx context.Context, input string) (string, error) { + if err := ctx.Err(); err != nil { + return "", err + } + cInputPtr, cInputLen := wvStr(input) + wvDone := make(chan wvOutcome[string], 1) + wvHandle := cgo.NewHandle(wvDone) + C.kitchen_sink_kitchen_do_async(cInputPtr, cInputLen, C.kitchen_sink_kitchen_do_async_callback(unsafe.Pointer(C.goWv_kitchen_sink_kitchen_do_async_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, nil) + if err != nil { + return "", err + } + if fail != nil { + panic(fail.err()) + } + return res, nil +} + +//export goWv_kitchen_sink_kitchen_do_cancellable_callback +func goWv_kitchen_sink_kitchen_do_cancellable_callback(context_ unsafe.Pointer, err *C.kitchen_sink_error, result_ptr *C.uint8_t, result_len C.size_t) { + wvComplete(context_, err, func() string { + return wvTakeString(result_ptr, result_len) + }) +} + +// DoCancellable: Cancellable async operation +// +// It blocks until the call completes. Cancelling ctx cancels the native +// call, which then returns ctx.Err(). +func DoCancellable(ctx context.Context, input string) (string, error) { + if err := ctx.Err(); err != nil { + return "", err + } + cInputPtr, cInputLen := wvStr(input) + wvToken := C.kitchen_sink_cancel_token_create() + defer C.kitchen_sink_cancel_token_destroy(wvToken) + wvDone := make(chan wvOutcome[string], 1) + wvHandle := cgo.NewHandle(wvDone) + C.kitchen_sink_kitchen_do_cancellable(cInputPtr, cInputLen, wvToken, C.kitchen_sink_kitchen_do_cancellable_callback(unsafe.Pointer(C.goWv_kitchen_sink_kitchen_do_cancellable_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle))) + res, fail, err := wvAwait(ctx, wvDone, wvToken) + if err != nil { + return "", err + } + if fail != nil { + panic(fail.err()) + } + return res, nil +} + +// LegacyOp: Legacy operation kept for compatibility +// +// Deprecated: Use new_op instead +func LegacyOp() int32 { + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_legacy_op(&cErr) + wvTrap(&cErr) + return int32(cRet) +} + +// NewOp: Replacement for legacy_op +func NewOp() int32 { + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_new_op(&cErr) + wvTrap(&cErr) + return int32(cRet) +} + +// Hello: Trivial nested-module function +func Hello() string { + var cRetLen C.size_t + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_nested_hello(&cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__codec_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__codec_go.snap new file mode 100644 index 00000000..cf805789 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__codec_go.snap @@ -0,0 +1,205 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +import ( + "encoding/binary" + "math" + "unicode/utf8" +) + +// wvWriter serializes values into the value-buffer format: little-endian, +// packed, u32 length prefixes. +type wvWriter struct { + buf []byte +} + +func (w *wvWriter) writeBool(v bool) { + if v { + w.buf = append(w.buf, 1) + } else { + w.buf = append(w.buf, 0) + } +} + +func (w *wvWriter) writeI8(v int8) { + w.buf = append(w.buf, byte(v)) +} + +func (w *wvWriter) writeU8(v uint8) { + w.buf = append(w.buf, v) +} + +func (w *wvWriter) writeI16(v int16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v)) +} + +func (w *wvWriter) writeU16(v uint16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, v) +} + +func (w *wvWriter) writeI32(v int32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v)) +} + +func (w *wvWriter) writeU32(v uint32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, v) +} + +func (w *wvWriter) writeI64(v int64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v)) +} + +func (w *wvWriter) writeU64(v uint64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, v) +} + +func (w *wvWriter) writeF32(v float32) { + w.writeU32(math.Float32bits(v)) +} + +func (w *wvWriter) writeF64(v float64) { + w.writeU64(math.Float64bits(v)) +} + +func (w *wvWriter) writeLen(n int) { + if n < 0 || uint64(n) > math.MaxUint32 { + panic("kitchen_sink: value-buffer length exceeds u32 range") + } + w.writeU32(uint32(n)) +} + +func (w *wvWriter) writeString(v string) { + w.writeLen(len(v)) + w.buf = append(w.buf, v...) +} + +func (w *wvWriter) writeBytes(v []byte) { + w.writeLen(len(v)) + w.buf = append(w.buf, v...) +} + +func (w *wvWriter) writeOptionFlag(present bool) { + w.writeBool(present) +} + +// wvReader decodes values from the value-buffer format. A malformed buffer +// is a producer/consumer contract violation, so every read panics instead of +// returning a typed domain error. +type wvReader struct { + buf []byte + pos int +} + +func wvMalformed(context string) { + panic("kitchen_sink: malformed value buffer: " + context) +} + +func (r *wvReader) take(n int, context string) []byte { + if n < 0 || len(r.buf)-r.pos < n { + wvMalformed(context) + } + b := r.buf[r.pos : r.pos+n] + r.pos += n + return b +} + +func (r *wvReader) readBool() bool { + switch r.take(1, "bool")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("bool byte out of range") + return false +} + +func (r *wvReader) readI8() int8 { + return int8(r.take(1, "i8")[0]) +} + +func (r *wvReader) readU8() uint8 { + return r.take(1, "u8")[0] +} + +func (r *wvReader) readI16() int16 { + return int16(binary.LittleEndian.Uint16(r.take(2, "i16"))) +} + +func (r *wvReader) readU16() uint16 { + return binary.LittleEndian.Uint16(r.take(2, "u16")) +} + +func (r *wvReader) readI32() int32 { + return int32(binary.LittleEndian.Uint32(r.take(4, "i32"))) +} + +func (r *wvReader) readU32() uint32 { + return binary.LittleEndian.Uint32(r.take(4, "u32")) +} + +func (r *wvReader) readI64() int64 { + return int64(binary.LittleEndian.Uint64(r.take(8, "i64"))) +} + +func (r *wvReader) readU64() uint64 { + return binary.LittleEndian.Uint64(r.take(8, "u64")) +} + +func (r *wvReader) readF32() float32 { + return math.Float32frombits(r.readU32()) +} + +func (r *wvReader) readF64() float64 { + return math.Float64frombits(r.readU64()) +} + +// readLen reads a u32 element count. Elements may encode to zero bytes, so +// the count isn't checked against the remaining buffer; callers cap their +// preallocation with capHint instead. +func (r *wvReader) readLen() int { + return int(r.readU32()) +} + +// capHint bounds a preallocation for n decoded elements by the bytes left in +// the buffer, so a corrupt count can't force a huge allocation. +func (r *wvReader) capHint(n int) int { + return min(n, len(r.buf)-r.pos) +} + +func (r *wvReader) readString() string { + b := r.take(r.readLen(), "string bytes") + if !utf8.Valid(b) { + wvMalformed("string is not valid UTF-8") + } + return string(b) +} + +func (r *wvReader) readBytes() []byte { + return append([]byte{}, r.take(r.readLen(), "byte buffer")...) +} + +func (r *wvReader) readOptionFlag() bool { + switch r.take(1, "option flag")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("option flag byte out of range") + return false +} + +func (r *wvReader) expectEnd() { + if r.pos != len(r.buf) { + wvMalformed("trailing bytes after value") + } +} + +// END codec.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__go_mod.snap index 73a087a7..4a40225f 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__go_mod.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__go_mod.snap @@ -1,11 +1,11 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -module weaveffi +module kitchen_sink go 1.23 diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__kitchen_sink_h.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__kitchen_sink_h.snap new file mode 100644 index 00000000..14faeffb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__kitchen_sink_h.snap @@ -0,0 +1,219 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef KITCHEN_SINK_H +#define KITCHEN_SINK_H + +#include +#include +#include + +#ifndef KITCHEN_SINK_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef KITCHEN_SINK_BUILD +# define KITCHEN_SINK_API __declspec(dllexport) +# else +# define KITCHEN_SINK_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define KITCHEN_SINK_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define KITCHEN_SINK_API __attribute__((visibility("default"))) +# else +# define KITCHEN_SINK_API +# endif +#endif + +#ifndef KITCHEN_SINK_DEPRECATED +# if defined(_MSC_VER) +# define KITCHEN_SINK_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define KITCHEN_SINK_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define KITCHEN_SINK_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports kitchen_sink_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define KITCHEN_SINK_ABI_VERSION 3u +KITCHEN_SINK_API uint32_t kitchen_sink_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by kitchen_sink_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct kitchen_sink_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} kitchen_sink_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +KITCHEN_SINK_API void kitchen_sink_error_set(kitchen_sink_error* err, int32_t code, const char* message); +KITCHEN_SINK_API void kitchen_sink_error_clear(kitchen_sink_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + kitchen_sink_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_error_free(kitchen_sink_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +KITCHEN_SINK_API void kitchen_sink_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct kitchen_sink_cancel_token kitchen_sink_cancel_token; +KITCHEN_SINK_API kitchen_sink_cancel_token* kitchen_sink_cancel_token_create(void); +KITCHEN_SINK_API void kitchen_sink_cancel_token_cancel(kitchen_sink_cancel_token* token); +KITCHEN_SINK_API bool kitchen_sink_cancel_token_is_cancelled(const kitchen_sink_cancel_token* token); +KITCHEN_SINK_API void kitchen_sink_cancel_token_destroy(kitchen_sink_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +KITCHEN_SINK_API uint8_t* kitchen_sink_alloc(uint32_t size); +KITCHEN_SINK_API void kitchen_sink_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +KITCHEN_SINK_API uint64_t kitchen_sink_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define KITCHEN_SINK_SHARED_CHECKSUM 0x42c4ce2c8d0af052ull +KITCHEN_SINK_API uint64_t kitchen_sink_shared_checksum(void); +#define KITCHEN_SINK_KITCHEN_CHECKSUM 0xd83da75b24b545ffull +KITCHEN_SINK_API uint64_t kitchen_sink_kitchen_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with kitchen_sink_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * kitchen_sink_buffer.h declares a C struct and codec for each. + */ + +/** Task priority levels */ +typedef enum { + /** Low priority */ + kitchen_sink_kitchen_Priority_Low = 0, + /** Normal priority */ + kitchen_sink_kitchen_Priority_Normal = 1, + /** High priority */ + kitchen_sink_kitchen_Priority_High = 2 +} kitchen_sink_kitchen_Priority; +/** Error codes reported by throwing functions in the `kitchen` module tree. */ +typedef enum { kitchen_sink_kitchen_KitchenErrors_NotFound = 1, kitchen_sink_kitchen_KitchenErrors_InvalidInput = 2 } kitchen_sink_kitchen_KitchenErrors; +typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget; +typedef struct kitchen_sink_kitchen_StreamGadgetsIterator kitchen_sink_kitchen_StreamGadgetsIterator; +typedef struct kitchen_sink_kitchen_StreamItemsIterator kitchen_sink_kitchen_StreamItemsIterator; +/** + * Consumer-implemented observer of ready items + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct kitchen_sink_kitchen_ReadyListener_vtable { + /** Fires when an item is ready */ + void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); + /** Receives the item itself and says whether to keep listening */ + bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); + void (*free)(void* ctx); +} kitchen_sink_kitchen_ReadyListener_vtable; +typedef void (*kitchen_sink_kitchen_do_async_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*kitchen_sink_kitchen_do_cancellable_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: shared +/** Trivial cross-module helper */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_shared_ping(size_t* out_len, kitchen_sink_error* out_err); + +// Module: kitchen +/** A pocket-sized interface exercising the object surface */ +/** Create a gadget with the given id */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(int64_t id, kitchen_sink_error* out_err); +/** The gadget subsystem version string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_version(size_t* out_len, kitchen_sink_error* out_err); +/** Render the gadget as a human-readable string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err); +/** Poke the gadget a number of times, failing on a negative count */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_Gadget_poke(const kitchen_sink_kitchen_Gadget* self, int32_t times, kitchen_sink_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_kitchen_Gadget_destroy(kitchen_sink_kitchen_Gadget* self); + +/** Identity for bool */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_bool_id(bool x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_i32_id(int32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API uint32_t kitchen_sink_kitchen_u32_id(uint32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int64_t kitchen_sink_kitchen_i64_id(int64_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API double kitchen_sink_kitchen_f64_id(double x, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_string(const uint8_t* s_ptr, size_t s_len, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_bytes(const uint8_t* b_ptr, size_t b_len, size_t* out_len, kitchen_sink_error* out_err); +/** Register a listener, returning how many are attached */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err); +/** Optional object in both directions */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_maybe_gadget(const kitchen_sink_kitchen_Gadget* fallback, kitchen_sink_error* out_err); +/** A list of objects */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_gadgets(size_t* out_len, kitchen_sink_error* out_err); +/** An iterator over objects */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamGadgetsIterator* kitchen_sink_kitchen_stream_gadgets(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamGadgetsIterator_next(kitchen_sink_kitchen_StreamGadgetsIterator* iter, kitchen_sink_kitchen_Gadget** out_item, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamGadgetsIterator_destroy(kitchen_sink_kitchen_StreamGadgetsIterator* iter); +/** Optional struct return */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_maybe_item(int64_t id, size_t* out_len, kitchen_sink_error* out_err); +/** List of structs */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_list_items(size_t* out_len, kitchen_sink_error* out_err); +/** Map return type */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_get_attrs(size_t* out_len, kitchen_sink_error* out_err); +/** Iterator return type */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamItemsIterator* kitchen_sink_kitchen_stream_items(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamItemsIterator_next(kitchen_sink_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamItemsIterator_destroy(kitchen_sink_kitchen_StreamItemsIterator* iter); +/** Returns the shared Token type from another module */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_cross_module_token(size_t* out_len, kitchen_sink_error* out_err); +/** Async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_async(const uint8_t* input_ptr, size_t input_len, kitchen_sink_kitchen_do_async_callback callback, void* context); +/** Cancellable async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_ptr, size_t input_len, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_do_cancellable_callback callback, void* context); +/** Legacy operation kept for compatibility */ +KITCHEN_SINK_DEPRECATED("Use new_op instead") +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_legacy_op(kitchen_sink_error* out_err); +/** Replacement for legacy_op */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_new_op(kitchen_sink_error* out_err); + +// Module: kitchen_nested +/** Trivial nested-module function */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_nested_hello(size_t* out_len, kitchen_sink_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // KITCHEN_SINK_H + +// END kitchen_sink.h diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__runtime_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__runtime_go.snap new file mode 100644 index 00000000..1bebbcb4 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__runtime_go.snap @@ -0,0 +1,305 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +/* +#include +#include "kitchen_sink.h" +*/ +import "C" + +import ( + "context" + "fmt" + "runtime/cgo" + "sync/atomic" + "unsafe" +) + +// wvABIVersion is the C ABI revision these bindings were generated against. +const wvABIVersion uint32 = 3 + +// The runtime error codes this package produces or interprets itself. +const ( + wvCodeMarshal int32 = -3 + wvCodeForeign int32 = -4 + wvCodeCancelled int32 = -5 +) + +// Error reports a failure that no typed error domain claims: an unknown +// code, a generic failure (code -1), a producer panic (-2), a marshalling +// failure (-3), or a Go callback-interface implementation that panicked +// (-4). The code is preserved so callers can tell them apart. +type Error struct { + // Code is the numeric ABI error code. + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *Error) Error() string { + return fmt.Sprintf("kitchen_sink: %s (code %d)", e.Message, e.Code) +} + +// DebugLive reports how many resources of one kind the native library holds: +// 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned +// allocations. It's always 0 unless the library was built with leak checks +// enabled; tests use it to assert that nothing leaked. +func DebugLive(kind int32) uint64 { + return uint64(C.kitchen_sink_debug_live(C.int32_t(kind))) +} + +// wvCheckABI panics when the linked library implements a different C ABI +// revision than these bindings were generated for. +func wvCheckABI() { + if found := uint32(C.kitchen_sink_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("kitchen_sink: ABI mismatch: these bindings expect revision %d but the linked library reports revision %d", wvABIVersion, found)) + } +} + +// wvCheckModule panics when the linked library's contract checksum for a +// top-level module differs from the one these bindings were generated +// against, which means the bindings are stale. +func wvCheckModule(module string, want, found uint64) { + if found != want { + panic(fmt.Sprintf("kitchen_sink: module %q doesn't match the linked library (bindings checksum %#016x, library checksum %#016x); regenerate the bindings", module, want, found)) + } +} + +// ── Errors ── + +// wvFailure is a non-zero error slot copied into Go memory. +type wvFailure struct { + code int32 + message string + payload []byte +} + +// err converts the failure into the generic *Error. +func (f wvFailure) err() error { + return &Error{Code: f.code, Message: f.message} +} + +func wvCopyFailure(cErr *C.kitchen_sink_error) wvFailure { + f := wvFailure{code: int32(cErr.code)} + if cErr.message != nil { + f.message = C.GoString(cErr.message) + } + if cErr.payload_ptr != nil { + f.payload = wvBorrowBytes(cErr.payload_ptr, cErr.payload_len) + } + return f +} + +// wvTakeError copies a non-zero error slot and clears it. +func wvTakeError(cErr *C.kitchen_sink_error) wvFailure { + f := wvCopyFailure(cErr) + C.kitchen_sink_error_clear(cErr) + return f +} + +// wvTakeBoxedError copies the heap-boxed error an async completion receives +// and frees the box. +func wvTakeBoxedError(cErr *C.kitchen_sink_error) wvFailure { + f := wvCopyFailure(cErr) + C.kitchen_sink_error_free(cErr) + return f +} + +// wvTrap panics with the generic *Error when the slot reports a failure. +// Wrappers of functions that declare no error domain check their slot with +// it: a failure there can only be a producer bug, a marshalling failure, or +// a callback-interface implementation that panicked. +func wvTrap(cErr *C.kitchen_sink_error) { + if cErr.code != 0 { + panic(wvTakeError(cErr).err()) + } +} + +// wvForeignError reports a panic recovered inside a callback-interface +// trampoline through the producer's error slot (code -4). The producer +// copies the message, so it's only borrowed for the call. +func wvForeignError(outErr *C.kitchen_sink_error, recovered any) { + msg := C.CString(fmt.Sprint(recovered)) + defer C.free(unsafe.Pointer(msg)) + C.kitchen_sink_error_set(outErr, C.int32_t(wvCodeForeign), msg) +} + +// ── Strings and bytes ── + +// wvStr returns a borrowed (pointer, length) view of s's UTF-8 bytes for the +// duration of one call. Strings may contain NUL bytes. +func wvStr(s string) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(s))), C.size_t(len(s)) +} + +// wvBytes returns a borrowed (pointer, length) view of b for the duration of +// one call. +func wvBytes(b []byte) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.SliceData(b))), C.size_t(len(b)) +} + +// wvBorrowString copies a borrowed UTF-8 run into a Go string. +func wvBorrowString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + return string(unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) +} + +// wvTakeString copies a returned UTF-8 run into a Go string and releases the +// producer's allocation. +func wvTakeString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + s := wvBorrowString(ptr, n) + C.kitchen_sink_free_bytes(ptr, n) + return s +} + +// wvBorrowBytes copies a borrowed byte run into Go memory. A null pointer +// copies to nil. +func wvBorrowBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + return append([]byte{}, unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)...) +} + +// wvTakeBytes copies a returned byte run (raw bytes or a value buffer) into +// Go memory and releases the producer's allocation. +func wvTakeBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + b := wvBorrowBytes(ptr, n) + C.kitchen_sink_free_bytes(ptr, n) + return b +} + +// ── Objects ── + +// wvRef owns one strong reference to a native object on behalf of a wrapper. +// Calls borrow the pointer between acquire and release. Close may race them: +// the reference is destroyed exactly once, by close when no call is in +// flight, or else by the last in-flight call to finish. +type wvRef struct { + ptr unsafe.Pointer + destroy func(unsafe.Pointer) + // state counts in-flight calls in its upper bits; the low bit is set + // once close has run. + state atomic.Uint64 +} + +func (r *wvRef) init(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { + r.ptr = ptr + r.destroy = destroy +} + +// acquire borrows the pointer for one call, panicking when the wrapper was +// already closed. +func (r *wvRef) acquire(typeName string) unsafe.Pointer { + for { + s := r.state.Load() + if s&1 != 0 { + panic("kitchen_sink: " + typeName + " used after Close") + } + if r.state.CompareAndSwap(s, s+2) { + return r.ptr + } + } +} + +// release ends one call's borrow. +func (r *wvRef) release() { + if r.state.Add(^uint64(1)) == 1 { + r.destroy(r.ptr) + } +} + +// close releases the wrapper's reference once no call is in flight. Calling +// it again is a no-op. +func (r *wvRef) close() { + for { + s := r.state.Load() + if s&1 != 0 { + return + } + if r.state.CompareAndSwap(s, s|1) { + if s == 0 { + r.destroy(r.ptr) + } + return + } + } +} + +// ── Async ── + +// wvOutcome is one async completion: a converted result, or the failure the +// producer reported. +type wvOutcome[T any] struct { + val T + fail *wvFailure +} + +// wvComplete delivers one async completion to the waiting wrapper. It runs on +// a producer thread inside an exported trampoline, so it never panics: a +// result that fails to decode is delivered as a marshalling failure. +func wvComplete[T any](handle unsafe.Pointer, cErr *C.kitchen_sink_error, convert func() T) { + h := cgo.Handle(uintptr(handle)) + done := h.Value().(chan wvOutcome[T]) + h.Delete() + done <- wvSettle(cErr, convert) +} + +func wvSettle[T any](cErr *C.kitchen_sink_error, convert func() T) (o wvOutcome[T]) { + if cErr != nil { + if f := wvTakeBoxedError(cErr); f.code != 0 { + o.fail = &f + return o + } + } + defer func() { + if r := recover(); r != nil { + o = wvOutcome[T]{fail: &wvFailure{code: wvCodeMarshal, message: fmt.Sprint(r)}} + } + }() + o.val = convert() + return o +} + +// wvAwait blocks until an async call completes or ctx is done. With a cancel +// token, ctx's cancellation cancels the native call and waits for its +// completion, which the producer delivers promptly; without one, the call is +// abandoned and ctx.Err() returned at once. A cancelled completion (code -5) +// returns ctx.Err(), or context.Canceled when ctx itself isn't done. +func wvAwait[T any](ctx context.Context, done chan wvOutcome[T], token *C.kitchen_sink_cancel_token) (T, *wvFailure, error) { + var o wvOutcome[T] + select { + case o = <-done: + case <-ctx.Done(): + if token == nil { + var zero T + return zero, nil, ctx.Err() + } + C.kitchen_sink_cancel_token_cancel(token) + o = <-done + } + if o.fail != nil && o.fail.code == wvCodeCancelled { + var zero T + if err := ctx.Err(); err != nil { + return zero, nil, err + } + return zero, nil, context.Canceled + } + return o.val, o.fail, nil +} + +// END runtime.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap deleted file mode 100644 index e82fe20b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap +++ /dev/null @@ -1,999 +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 -static void* wvHandlePtr(uintptr_t h) { return (void*)h; } -extern void goWv_weaveffi_kitchen_ReadyListener_on_ready(void* ctx, int32_t code, char* msg, weaveffi_error* out_err); -extern bool goWv_weaveffi_kitchen_ReadyListener_on_item(void* ctx, uint8_t* item_ptr, size_t item_len, weaveffi_kitchen_Gadget* gadget, weaveffi_error* out_err); -extern void goWv_weaveffi_kitchen_ReadyListener_free(void* ctx); -static const weaveffi_kitchen_ReadyListener_vtable wvVtable_weaveffi_kitchen_ReadyListener = { - (void (*)(void*, int32_t, const char*, weaveffi_error*))goWv_weaveffi_kitchen_ReadyListener_on_ready, - (bool (*)(void*, const uint8_t*, size_t, weaveffi_kitchen_Gadget*, weaveffi_error*))goWv_weaveffi_kitchen_ReadyListener_on_item, - goWv_weaveffi_kitchen_ReadyListener_free, -}; -static const weaveffi_kitchen_ReadyListener_vtable* wvVtablePtr_weaveffi_kitchen_ReadyListener(void) { return &wvVtable_weaveffi_kitchen_ReadyListener; } -extern void goWv_weaveffi_kitchen_do_async_callback(void* context, weaveffi_error* err, char* result); -extern void goWv_weaveffi_kitchen_do_cancellable_callback(void* context, weaveffi_error* err, char* result); -*/ -import "C" - -import ( - "encoding/binary" - "fmt" - "iter" - "math" - "runtime" - "runtime/cgo" - "unicode/utf8" - "unsafe" -) - -// The ABI revision these bindings were generated against. -const wvABIVersion uint32 = 2 - -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, a -// producer panic (code -2), or a Go callback-interface implementation -// that panicked (code -4). -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, a marshalling failure, or a callback-interface -// implementation that panicked. The panic value is a *WeaveFFIError, so a -// recover() site can still inspect the code. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - panic(wvBrandError(wvTakeError(cErr))) - } -} - -// 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)) -} - -// wvForeignError reports a panic recovered inside a callback-interface -// trampoline through the producer's error slot (code -4). The message is -// borrowed for the call: the producer copies it with its own allocator. -func wvForeignError(outErr *C.weaveffi_error, recovered any) { - msg := C.CString(fmt.Sprint(recovered)) - defer C.free(unsafe.Pointer(msg)) - C.weaveffi_error_set(outErr, -4, msg) -} - -// Token: A reusable token referenced across modules -type Token struct { - Id int64 - Label string -} - -// wvPackToken appends v to w in the value-buffer wire format. -func wvPackToken(w *wvWriter, v Token) { - w.writeI64(v.Id) - w.writeString(v.Label) -} - -// wvUnpackToken decodes one Token from r. -func wvUnpackToken(r *wvReader) Token { - var v Token - v.Id = r.readI64() - v.Label = r.readString() - return v -} - -// Ping: Trivial cross-module helper -func Ping() string { - var cErr C.weaveffi_error - result := C.weaveffi_shared_ping(&cErr) - wvTrap(&cErr) - goResult := C.GoString(result) - C.weaveffi_free_string(result) - return goResult -} - -// KitchenErrorsError is a typed error reported by the `kitchen` module. -type KitchenErrorsError struct { - // Code is the numeric ABI error code (one of the KitchenErrorsError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *KitchenErrorsError) Error() string { - return fmt.Sprintf("kitchen: %s (code %d)", e.Message, e.Code) -} - -// KitchenErrorsError codes. -const ( - // KitchenErrorsErrorNotFound: Item not found - KitchenErrorsErrorNotFound int32 = 1 - // KitchenErrorsErrorInvalidInput: Invalid input - KitchenErrorsErrorInvalidInput int32 = 2 -) - -// wvMapKitchen converts a non-zero code from the `kitchen` domain into a -// *KitchenErrorsError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapKitchen(code int32, message string, payload []byte) error { - switch code { - case KitchenErrorsErrorNotFound: - if message == "" { - message = "Item not found" - } - return &KitchenErrorsError{Code: code, Message: message} - case KitchenErrorsErrorInvalidInput: - if message == "" { - message = "Invalid input" - } - return &KitchenErrorsError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -// Priority: Task priority levels -type Priority int32 - -const ( - // PriorityLow: Low priority - PriorityLow Priority = 0 - // PriorityNormal: Normal priority - PriorityNormal Priority = 1 - // PriorityHigh: High priority - PriorityHigh Priority = 2 -) - -// Item: Kitchen sink struct exercising every field feature -type Item struct { - // Id: Stable identifier - Id int64 - // Name: Display name - Name string - // Count: Initial count - Count int32 - // Enabled: Whether the item is enabled - Enabled bool - // Ratio: Scaling ratio - Ratio float64 - Nick uint32 - Payload []byte - Tags []string - Attrs map[string]string - Parent *int64 - // Token: Cross-module struct reference - Token Token - Priority Priority - // Gadget: An optional object inside a record - Gadget *Gadget -} - -// wvPackItem appends v to w in the value-buffer wire format. -func wvPackItem(w *wvWriter, v Item) { - w.writeI64(v.Id) - w.writeString(v.Name) - w.writeI32(v.Count) - w.writeBool(v.Enabled) - w.writeF64(v.Ratio) - w.writeU32(v.Nick) - w.writeBytes(v.Payload) - w.writeLen(len(v.Tags)) - for _, eTags0 := range v.Tags { - w.writeString(eTags0) - } - w.writeLen(len(v.Attrs)) - for kAttrs0, vAttrs0 := range v.Attrs { - w.writeString(kAttrs0) - w.writeString(vAttrs0) - } - if v.Parent == nil { - w.writeOptionFlag(false) - } else { - w.writeOptionFlag(true) - w.writeI64((*v.Parent)) - } - wvPackToken(w, v.Token) - w.writeI32(int32(v.Priority)) - if v.Gadget == nil { - w.writeOptionFlag(false) - } else { - w.writeOptionFlag(true) - w.writeU64(wvTokenGadget(v.Gadget)) - } -} - -// wvUnpackItem decodes one Item from r. -func wvUnpackItem(r *wvReader) Item { - var v Item - v.Id = r.readI64() - v.Name = r.readString() - v.Count = r.readI32() - v.Enabled = r.readBool() - v.Ratio = r.readF64() - v.Nick = r.readU32() - v.Payload = r.readBytes() - nTags0 := r.readLen() - v.Tags = make([]string, nTags0) - for iTags0 := range v.Tags { - v.Tags[iTags0] = r.readString() - } - nAttrs0 := r.readLen() - v.Attrs = make(map[string]string, nAttrs0) - for iAttrs0 := 0; iAttrs0 < nAttrs0; iAttrs0++ { - var kAttrs0 string - kAttrs0 = r.readString() - var vAttrs0 string - vAttrs0 = r.readString() - v.Attrs[kAttrs0] = vAttrs0 - } - if r.readOptionFlag() { - var oParent0 int64 - oParent0 = r.readI64() - v.Parent = &oParent0 - } - v.Token = wvUnpackToken(r) - v.Priority = Priority(r.readI32()) - if r.readOptionFlag() { - var oGadget0 *Gadget - oGadget0 = wvUntokenGadget(r.readU64()) - v.Gadget = oGadget0 - } - return v -} - -// ReadyListener: Consumer-implemented observer of ready items -// -// Implement this interface in Go and pass the value to native functions -// that accept it. -// -// The native library may call any method from any thread until it releases -// the implementation. A panic in a method is reported to the native caller -// as a foreign error (code -4) instead of crashing the process. -type ReadyListener interface { - // OnReady: Fires when an item is ready - OnReady(code int32, msg string) - // OnItem: Receives the item itself and says whether to keep listening - OnItem(item Item, gadget *Gadget) bool -} - -//export goWv_weaveffi_kitchen_ReadyListener_on_ready -func goWv_weaveffi_kitchen_ReadyListener_on_ready(ctx unsafe.Pointer, code C.int32_t, msg *C.char, out_err *C.weaveffi_error) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(ReadyListener) - arg0 := int32(code) - arg1 := "" - if msg != nil { - arg1 = C.GoString(msg) - } - impl.OnReady(arg0, arg1) -} - -//export goWv_weaveffi_kitchen_ReadyListener_on_item -func goWv_weaveffi_kitchen_ReadyListener_on_item(ctx unsafe.Pointer, item_ptr *C.uint8_t, item_len C.size_t, gadget *C.weaveffi_kitchen_Gadget, out_err *C.weaveffi_error) (ret C._Bool) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(ReadyListener) - rArg0 := &wvReader{buf: wvBorrowBuffer(item_ptr, item_len)} - var arg0 Item - arg0 = wvUnpackItem(rArg0) - rArg0.expectEnd() - arg1 := wvAdoptGadget(gadget) - ret = boolToC(impl.OnItem(arg0, arg1)) - return -} - -//export goWv_weaveffi_kitchen_ReadyListener_free -func goWv_weaveffi_kitchen_ReadyListener_free(ctx unsafe.Pointer) { - cgo.Handle(uintptr(ctx)).Delete() -} - -// Gadget: A pocket-sized interface exercising the object surface -// -// Each wrapper holds one strong reference; Close releases it, and a -// finalizer releases it if the wrapper is garbage collected first. -type Gadget struct { - ptr *C.weaveffi_kitchen_Gadget -} - -// wvAdoptGadget adopts one owned strong reference into a new wrapper. A null -// pointer adopts to nil. -func wvAdoptGadget(ptr *C.weaveffi_kitchen_Gadget) *Gadget { - if ptr == nil { - return nil - } - s := &Gadget{ptr: ptr} - runtime.SetFinalizer(s, (*Gadget).Close) - return s -} - -// wvTokenGadget clones o's reference into a value-buffer object token. The -// wrapper keeps its own reference; the token carries the new one. -func wvTokenGadget(o *Gadget) uint64 { - if o == nil || o.ptr == nil { - panic("weaveffi: nil or closed Gadget cannot be encoded in a non-optional position") - } - return uint64(uintptr(unsafe.Pointer(C.weaveffi_kitchen_Gadget_clone(o.ptr)))) -} - -// wvUntokenGadget adopts the strong reference carried by a value-buffer object -// token. -func wvUntokenGadget(token uint64) *Gadget { - if token == 0 { - panic("weaveffi: malformed value buffer: null object token") - } - return wvAdoptGadget((*C.weaveffi_kitchen_Gadget)(C.wvHandlePtr(C.uintptr_t(token)))) -} - -// NewGadget: Create a gadget with the given id -func NewGadget(id int64) *Gadget { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_Gadget_new(C.int64_t(id), &cErr) - wvTrap(&cErr) - return wvAdoptGadget(result) -} - -// Describe: Render the gadget as a human-readable string -func (s *Gadget) Describe() string { - if s.ptr == nil { - panic("weaveffi: Gadget used after Close") - } - defer runtime.KeepAlive(s) - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_Gadget_describe(s.ptr, &cErr) - wvTrap(&cErr) - goResult := C.GoString(result) - C.weaveffi_free_string(result) - return goResult -} - -// Poke: Poke the gadget a number of times, failing on a negative count -func (s *Gadget) Poke(times int32) (int32, error) { - if s.ptr == nil { - panic("weaveffi: Gadget used after Close") - } - defer runtime.KeepAlive(s) - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_Gadget_poke(s.ptr, C.int32_t(times), &cErr) - if cErr.code != 0 { - return 0, wvMapKitchen(wvTakeError(&cErr)) - } - return int32(result), nil -} - -// GadgetVersion: The gadget subsystem version string -func GadgetVersion() string { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_Gadget_version(&cErr) - wvTrap(&cErr) - goResult := C.GoString(result) - C.weaveffi_free_string(result) - return goResult -} - -// Close releases the wrapper's strong reference. Calling it more than once -// is harmless; the object itself is dropped by the native library when its -// last reference (from any wrapper, record, or producer) is released. -func (s *Gadget) Close() { - if s.ptr != nil { - C.weaveffi_kitchen_Gadget_destroy(s.ptr) - s.ptr = nil - runtime.SetFinalizer(s, nil) - } -} - -// BoolId: Identity for bool -func BoolId(x bool) bool { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_bool_id(boolToC(x), &cErr) - wvTrap(&cErr) - return cToBool(result) -} - -func I32Id(x int32) int32 { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_i32_id(C.int32_t(x), &cErr) - wvTrap(&cErr) - return int32(result) -} - -func U32Id(x uint32) uint32 { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_u32_id(C.uint32_t(x), &cErr) - wvTrap(&cErr) - return uint32(result) -} - -func I64Id(x int64) int64 { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_i64_id(C.int64_t(x), &cErr) - wvTrap(&cErr) - return int64(result) -} - -func F64Id(x float64) float64 { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_f64_id(C.double(x), &cErr) - wvTrap(&cErr) - return float64(result) -} - -func EchoString(s string) string { - cS := C.CString(s) - defer C.free(unsafe.Pointer(cS)) - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_echo_string(cS, &cErr) - wvTrap(&cErr) - goResult := C.GoString(result) - C.weaveffi_free_string(result) - return goResult -} - -func EchoBytes(b []byte) []byte { - var cBPtr *C.uint8_t - cBLen := C.size_t(len(b)) - if len(b) > 0 { - cBPtr = (*C.uint8_t)(unsafe.Pointer(&b[0])) - } - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_echo_bytes(cBPtr, cBLen, &cOutLen, &cErr) - wvTrap(&cErr) - if result == nil { - return nil - } - goResult := C.GoBytes(unsafe.Pointer(result), C.int(cOutLen)) - C.weaveffi_free_bytes(result, cOutLen) - return goResult -} - -// Subscribe: Register a listener, returning how many are attached -func Subscribe(listener ReadyListener) int32 { - hListener := cgo.NewHandle(listener) - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_subscribe(C.wvHandlePtr(C.uintptr_t(hListener)), C.wvVtablePtr_weaveffi_kitchen_ReadyListener(), &cErr) - wvTrap(&cErr) - return int32(result) -} - -// MaybeGadget: Optional object in both directions -func MaybeGadget(fallback *Gadget) *Gadget { - defer runtime.KeepAlive(fallback) - var cFallback *C.weaveffi_kitchen_Gadget - if fallback != nil { - cFallback = fallback.ptr - } - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_maybe_gadget(cFallback, &cErr) - wvTrap(&cErr) - return wvAdoptGadget(result) -} - -// Gadgets: A list of objects -func Gadgets() []*Gadget { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_gadgets(&cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult []*Gadget - nRes0 := rRes.readLen() - goResult = make([]*Gadget, nRes0) - for iRes0 := range goResult { - goResult[iRes0] = wvUntokenGadget(rRes.readU64()) - } - rRes.expectEnd() - return goResult -} - -// StreamGadgets: An iterator over objects -// 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 StreamGadgets() iter.Seq[*Gadget] { - return func(yield func(*Gadget) bool) { - var cErr C.weaveffi_error - it := C.weaveffi_kitchen_stream_gadgets(&cErr) - wvTrap(&cErr) - defer C.weaveffi_kitchen_StreamGadgetsIterator_destroy(it) - for { - var outItem *C.weaveffi_kitchen_Gadget - var iterErr C.weaveffi_error - ok := C.weaveffi_kitchen_StreamGadgetsIterator_next(it, &outItem, &iterErr) != 0 - wvTrap(&iterErr) - if !ok { - return - } - item := wvAdoptGadget(outItem) - if !yield(item) { - return - } - } - } -} - -// MaybeItem: Optional struct return -func MaybeItem(id int64) (*Item, error) { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_maybe_item(C.int64_t(id), &cOutLen, &cErr) - if cErr.code != 0 { - return nil, wvMapKitchen(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult *Item - if rRes.readOptionFlag() { - var oRes0 Item - oRes0 = wvUnpackItem(rRes) - goResult = &oRes0 - } - rRes.expectEnd() - return goResult, nil -} - -// ListItems: List of structs -func ListItems() []Item { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_list_items(&cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult []Item - nRes0 := rRes.readLen() - goResult = make([]Item, nRes0) - for iRes0 := range goResult { - goResult[iRes0] = wvUnpackItem(rRes) - } - rRes.expectEnd() - return goResult -} - -// GetAttrs: Map return type -func GetAttrs() map[string]int32 { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_get_attrs(&cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult map[string]int32 - nRes0 := rRes.readLen() - goResult = make(map[string]int32, nRes0) - for iRes0 := 0; iRes0 < nRes0; iRes0++ { - var kRes0 string - kRes0 = rRes.readString() - var vRes0 int32 - vRes0 = rRes.readI32() - goResult[kRes0] = vRes0 - } - rRes.expectEnd() - return goResult -} - -// StreamItems: Iterator return type -// 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 StreamItems() iter.Seq[Item] { - return func(yield func(Item) bool) { - var cErr C.weaveffi_error - it := C.weaveffi_kitchen_stream_items(&cErr) - wvTrap(&cErr) - defer C.weaveffi_kitchen_StreamItemsIterator_destroy(it) - for { - var outItem *C.uint8_t - var outLen C.size_t - var iterErr C.weaveffi_error - ok := C.weaveffi_kitchen_StreamItemsIterator_next(it, &outItem, &outLen, &iterErr) != 0 - wvTrap(&iterErr) - if !ok { - return - } - rItem := &wvReader{buf: wvCopyBuffer(outItem, outLen)} - var item Item - item = wvUnpackItem(rItem) - rItem.expectEnd() - if !yield(item) { - return - } - } - } -} - -// CrossModuleToken: Returns the shared Token type from another module -func CrossModuleToken() Token { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_cross_module_token(&cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Token - goResult = wvUnpackToken(rRes) - rRes.expectEnd() - return goResult -} - -type wvOutcomeKitchenDoAsync struct { - val string - err error -} - -//export goWv_weaveffi_kitchen_do_async_callback -func goWv_weaveffi_kitchen_do_async_callback(context unsafe.Pointer, err *C.weaveffi_error, result *C.char) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeKitchenDoAsync) - h.Delete() - if err != nil && err.code != 0 { - ch <- wvOutcomeKitchenDoAsync{err: wvBrandError(wvTakeBoxedError(err))} - return - } - val := "" - if result != nil { - val = C.GoString(result) - C.weaveffi_free_string(result) - } - ch <- wvOutcomeKitchenDoAsync{val: val} -} - -// DoAsync: Async operation -// Blocks the calling goroutine until the async producer completes. -func DoAsync(input string) string { - ch := make(chan wvOutcomeKitchenDoAsync, 1) - h := cgo.NewHandle(ch) - cInput := C.CString(input) - defer C.free(unsafe.Pointer(cInput)) - C.weaveffi_kitchen_do_async_async(cInput, C.weaveffi_kitchen_do_async_callback(unsafe.Pointer(C.goWv_weaveffi_kitchen_do_async_callback)), C.wvHandlePtr(C.uintptr_t(h))) - outcome := <-ch - if outcome.err != nil { - panic(outcome.err) - } - return outcome.val -} - -type wvOutcomeKitchenDoCancellable struct { - val string - err error -} - -//export goWv_weaveffi_kitchen_do_cancellable_callback -func goWv_weaveffi_kitchen_do_cancellable_callback(context unsafe.Pointer, err *C.weaveffi_error, result *C.char) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeKitchenDoCancellable) - h.Delete() - if err != nil && err.code != 0 { - ch <- wvOutcomeKitchenDoCancellable{err: wvBrandError(wvTakeBoxedError(err))} - return - } - val := "" - if result != nil { - val = C.GoString(result) - C.weaveffi_free_string(result) - } - ch <- wvOutcomeKitchenDoCancellable{val: val} -} - -// DoCancellable: Cancellable async operation -// Blocks the calling goroutine until the async producer completes. -func DoCancellable(input string) string { - ch := make(chan wvOutcomeKitchenDoCancellable, 1) - h := cgo.NewHandle(ch) - cInput := C.CString(input) - defer C.free(unsafe.Pointer(cInput)) - C.weaveffi_kitchen_do_cancellable_async(cInput, nil, C.weaveffi_kitchen_do_cancellable_callback(unsafe.Pointer(C.goWv_weaveffi_kitchen_do_cancellable_callback)), C.wvHandlePtr(C.uintptr_t(h))) - outcome := <-ch - if outcome.err != nil { - panic(outcome.err) - } - return outcome.val -} - -// LegacyOp: Legacy operation kept for compatibility -// Deprecated: Use new_op instead -func LegacyOp() int32 { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_legacy_op(&cErr) - wvTrap(&cErr) - return int32(result) -} - -// NewOp: Replacement for legacy_op -func NewOp() int32 { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_new_op(&cErr) - wvTrap(&cErr) - return int32(result) -} - -// Hello: Trivial nested-module function -func Hello() string { - var cErr C.weaveffi_error - result := C.weaveffi_kitchen_nested_hello(&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_nested_modules__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__README_md.snap index 7d3c4f86..0a167f95 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__README_md.snap @@ -1,53 +1,49 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Go Bindings +# nested_modules (Go) -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`) +cgo bindings for the `nested_modules` native library. ## 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`: +The module ships its own copy of the C header (`nested_modules.h`) and links with +`-lnested_modules`. Point the linker at the directory holding `libnested_modules.so`, +`libnested_modules.dylib`, or `nested_modules.dll`, and make the library findable at +run time: ```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" +export CGO_LDFLAGS="-L/path/to/lib" +export LD_LIBRARY_PATH="/path/to/lib" # DYLD_LIBRARY_PATH on macOS +go build ./... ``` -2. Build or run your Go project that imports this module: +Depend on the module from your own project with a `require` (and, for a +local checkout, a `replace`) of `nested_modules`, then import it: -```sh -go build ./... +```go +import "nested_modules" ``` -## 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. - -Interfaces are reference-counted objects: each wrapper holds one strong -reference that `Close()` releases (a finalizer releases it if you forget), and -a nullable interface is a nil wrapper pointer. Callback interfaces are Go -`interface` types: implement one and pass the value to any function that -accepts it; the native library may call it from any thread, and a panic in -your implementation is reported to the native caller instead of crashing. +Go 1.23 or newer is required. + +## Usage notes + +- Records are plain structs and rich enums are sealed interfaces; both cross + the boundary by value. +- Interfaces are reference-counted objects. Call `Close` when you're done + with one (a finalizer releases it otherwise); it's safe to call twice or + while a call is in flight. +- Async functions take a `context.Context` first. Cancellable ones cancel + the native call when the context is done. +- Callback interfaces are Go interfaces you implement. The library may call + them from any thread, and a panic is reported to the native caller as a + foreign error instead of crashing. +- Loading checks the library's ABI revision and contract checksums and + panics with the stale module's name on a mismatch. diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__bindings_go.snap new file mode 100644 index 00000000..a20a9b6d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__bindings_go.snap @@ -0,0 +1,185 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package nested_modules + +/* +#cgo LDFLAGS: -lnested_modules +#include "nested_modules.h" +*/ +import "C" + +import ( + "fmt" +) + +func init() { + wvCheckABI() + wvCheckModule("graphics", 0x44d31b5b10fdb90d, uint64(C.nested_modules_graphics_checksum())) +} + +// GraphicsError is a typed error reported by the `graphics` module. +type GraphicsError struct { + // Code is the numeric ABI error code (one of the GraphicsError constants). + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *GraphicsError) Error() string { + return fmt.Sprintf("graphics: %s (code %d)", e.Message, e.Code) +} + +// GraphicsError codes. +const ( + // GraphicsErrorOutOfBounds coordinate out of bounds + GraphicsErrorOutOfBounds int32 = 1 +) + +// wvMapGraphics converts a non-zero code from the `graphics` domain into a +// *GraphicsError, falling back to the generic *Error for unknown codes. +func wvMapGraphics(f wvFailure) error { + switch f.code { + case GraphicsErrorOutOfBounds: + if f.message == "" { + f.message = "coordinate out of bounds" + } + return &GraphicsError{Code: f.code, Message: f.message} + default: + return f.err() + } +} + +// Unit: A measurement unit shared with nested submodules. +type Unit int32 + +const ( + UnitPixels Unit = 0 + UnitPoints Unit = 1 +) + +// Canvas: Top-level struct that references a struct defined one level deeper. +type Canvas struct { + Width int32 + // Root: Reference down into a nested module + Root Shape +} + +// wvPackCanvas appends v to w in the value-buffer wire format. +func wvPackCanvas(w *wvWriter, v Canvas) { + w.writeI32(v.Width) + wvPackShape(w, v.Root) +} + +// wvUnpackCanvas decodes one Canvas from r. +func wvUnpackCanvas(r *wvReader) Canvas { + var v Canvas + v.Width = r.readI32() + v.Root = wvUnpackShape(r) + return v +} + +// NewCanvas: Construct a Canvas. +func NewCanvas(width int32) Canvas { + var cRetLen C.size_t + var cErr C.nested_modules_error + cRet := C.nested_modules_graphics_new_canvas(C.int32_t(width), &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Canvas + ret = wvUnpackCanvas(rRet) + rRet.expectEnd() + return ret +} + +// Shape: A struct one level deep that references both an ancestor enum and a deeper struct. +type Shape struct { + Sides int32 + // Unit: Reference up to the parent module's enum + Unit Unit + // Detail: Reference down into a deeper module + Detail Detail +} + +// wvPackShape appends v to w in the value-buffer wire format. +func wvPackShape(w *wvWriter, v Shape) { + w.writeI32(v.Sides) + w.writeI32(int32(v.Unit)) + wvPackDetail(w, v.Detail) +} + +// wvUnpackShape decodes one Shape from r. +func wvUnpackShape(r *wvReader) Shape { + var v Shape + v.Sides = r.readI32() + v.Unit = Unit(r.readI32()) + v.Detail = wvUnpackDetail(r) + return v +} + +// MakeShape: Construct a Shape. +func MakeShape(sides int32) Shape { + var cRetLen C.size_t + var cErr C.nested_modules_error + cRet := C.nested_modules_graphics_shapes_make_shape(C.int32_t(sides), &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Shape + ret = wvUnpackShape(rRet) + rRet.expectEnd() + return ret +} + +// Describe: Nested function returning a struct owned by an ancestor module. +func Describe() Canvas { + var cRetLen C.size_t + var cErr C.nested_modules_error + cRet := C.nested_modules_graphics_shapes_describe(&cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Canvas + ret = wvUnpackCanvas(rRet) + rRet.expectEnd() + return ret +} + +// Detail: A struct two levels deep. +type Detail struct { + Area float64 +} + +// wvPackDetail appends v to w in the value-buffer wire format. +func wvPackDetail(w *wvWriter, v Detail) { + w.writeF64(v.Area) +} + +// wvUnpackDetail decodes one Detail from r. +func wvUnpackDetail(r *wvReader) Detail { + var v Detail + v.Area = r.readF64() + return v +} + +// Compute: Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. +func Compute(shape Shape) (Detail, error) { + wShape := &wvWriter{} + wvPackShape(wShape, shape) + cShapePtr, cShapeLen := wvBytes(wShape.buf) + var cRetLen C.size_t + var cErr C.nested_modules_error + cRet := C.nested_modules_graphics_shapes_geometry_compute(cShapePtr, cShapeLen, &cRetLen, &cErr) + if cErr.code != 0 { + return Detail{}, wvMapGraphics(wvTakeError(&cErr)) + } + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Detail + ret = wvUnpackDetail(rRet) + rRet.expectEnd() + return ret, nil +} + +// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__codec_go.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__codec_go.snap new file mode 100644 index 00000000..a9534aec --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__codec_go.snap @@ -0,0 +1,205 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package nested_modules + +import ( + "encoding/binary" + "math" + "unicode/utf8" +) + +// wvWriter serializes values into the value-buffer format: little-endian, +// packed, u32 length prefixes. +type wvWriter struct { + buf []byte +} + +func (w *wvWriter) writeBool(v bool) { + if v { + w.buf = append(w.buf, 1) + } else { + w.buf = append(w.buf, 0) + } +} + +func (w *wvWriter) writeI8(v int8) { + w.buf = append(w.buf, byte(v)) +} + +func (w *wvWriter) writeU8(v uint8) { + w.buf = append(w.buf, v) +} + +func (w *wvWriter) writeI16(v int16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v)) +} + +func (w *wvWriter) writeU16(v uint16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, v) +} + +func (w *wvWriter) writeI32(v int32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v)) +} + +func (w *wvWriter) writeU32(v uint32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, v) +} + +func (w *wvWriter) writeI64(v int64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v)) +} + +func (w *wvWriter) writeU64(v uint64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, v) +} + +func (w *wvWriter) writeF32(v float32) { + w.writeU32(math.Float32bits(v)) +} + +func (w *wvWriter) writeF64(v float64) { + w.writeU64(math.Float64bits(v)) +} + +func (w *wvWriter) writeLen(n int) { + if n < 0 || uint64(n) > math.MaxUint32 { + panic("nested_modules: 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 value-buffer format. A malformed buffer +// is a producer/consumer contract violation, so every read panics instead of +// returning a typed domain error. +type wvReader struct { + buf []byte + pos int +} + +func wvMalformed(context string) { + panic("nested_modules: malformed value buffer: " + context) +} + +func (r *wvReader) take(n int, context string) []byte { + if n < 0 || len(r.buf)-r.pos < n { + wvMalformed(context) + } + b := r.buf[r.pos : r.pos+n] + r.pos += n + return b +} + +func (r *wvReader) readBool() bool { + switch r.take(1, "bool")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("bool byte out of range") + return false +} + +func (r *wvReader) readI8() int8 { + return int8(r.take(1, "i8")[0]) +} + +func (r *wvReader) readU8() uint8 { + return r.take(1, "u8")[0] +} + +func (r *wvReader) readI16() int16 { + return int16(binary.LittleEndian.Uint16(r.take(2, "i16"))) +} + +func (r *wvReader) readU16() uint16 { + return binary.LittleEndian.Uint16(r.take(2, "u16")) +} + +func (r *wvReader) readI32() int32 { + return int32(binary.LittleEndian.Uint32(r.take(4, "i32"))) +} + +func (r *wvReader) readU32() uint32 { + return binary.LittleEndian.Uint32(r.take(4, "u32")) +} + +func (r *wvReader) readI64() int64 { + return int64(binary.LittleEndian.Uint64(r.take(8, "i64"))) +} + +func (r *wvReader) readU64() uint64 { + return binary.LittleEndian.Uint64(r.take(8, "u64")) +} + +func (r *wvReader) readF32() float32 { + return math.Float32frombits(r.readU32()) +} + +func (r *wvReader) readF64() float64 { + return math.Float64frombits(r.readU64()) +} + +// readLen reads a u32 element count. Elements may encode to zero bytes, so +// the count isn't checked against the remaining buffer; callers cap their +// preallocation with capHint instead. +func (r *wvReader) readLen() int { + return int(r.readU32()) +} + +// capHint bounds a preallocation for n decoded elements by the bytes left in +// the buffer, so a corrupt count can't force a huge allocation. +func (r *wvReader) capHint(n int) int { + return min(n, len(r.buf)-r.pos) +} + +func (r *wvReader) readString() string { + b := r.take(r.readLen(), "string bytes") + if !utf8.Valid(b) { + wvMalformed("string is not valid UTF-8") + } + return string(b) +} + +func (r *wvReader) readBytes() []byte { + return append([]byte{}, r.take(r.readLen(), "byte buffer")...) +} + +func (r *wvReader) readOptionFlag() bool { + switch r.take(1, "option flag")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("option flag byte out of range") + return false +} + +func (r *wvReader) expectEnd() { + if r.pos != len(r.buf) { + wvMalformed("trailing bytes after value") + } +} + +// END codec.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__go_mod.snap index 73a087a7..d61da40e 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__go_mod.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__go_mod.snap @@ -1,11 +1,11 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -module weaveffi +module nested_modules go 1.23 diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__nested_modules_h.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__nested_modules_h.snap new file mode 100644 index 00000000..27c97f2b --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__nested_modules_h.snap @@ -0,0 +1,144 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NESTED_MODULES_H +#define NESTED_MODULES_H + +#include +#include +#include + +#ifndef NESTED_MODULES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef NESTED_MODULES_BUILD +# define NESTED_MODULES_API __declspec(dllexport) +# else +# define NESTED_MODULES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define NESTED_MODULES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define NESTED_MODULES_API __attribute__((visibility("default"))) +# else +# define NESTED_MODULES_API +# endif +#endif + +#ifndef NESTED_MODULES_DEPRECATED +# if defined(_MSC_VER) +# define NESTED_MODULES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define NESTED_MODULES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define NESTED_MODULES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports nested_modules_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define NESTED_MODULES_ABI_VERSION 3u +NESTED_MODULES_API uint32_t nested_modules_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by nested_modules_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct nested_modules_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} nested_modules_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +NESTED_MODULES_API void nested_modules_error_set(nested_modules_error* err, int32_t code, const char* message); +NESTED_MODULES_API void nested_modules_error_clear(nested_modules_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + nested_modules_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +NESTED_MODULES_API void nested_modules_error_free(nested_modules_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +NESTED_MODULES_API void nested_modules_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct nested_modules_cancel_token nested_modules_cancel_token; +NESTED_MODULES_API nested_modules_cancel_token* nested_modules_cancel_token_create(void); +NESTED_MODULES_API void nested_modules_cancel_token_cancel(nested_modules_cancel_token* token); +NESTED_MODULES_API bool nested_modules_cancel_token_is_cancelled(const nested_modules_cancel_token* token); +NESTED_MODULES_API void nested_modules_cancel_token_destroy(nested_modules_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +NESTED_MODULES_API uint8_t* nested_modules_alloc(uint32_t size); +NESTED_MODULES_API void nested_modules_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +NESTED_MODULES_API uint64_t nested_modules_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define NESTED_MODULES_GRAPHICS_CHECKSUM 0x44d31b5b10fdb90dull +NESTED_MODULES_API uint64_t nested_modules_graphics_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with nested_modules_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * nested_modules_buffer.h declares a C struct and codec for each. + */ + +/** A measurement unit shared with nested submodules. */ +typedef enum { nested_modules_graphics_Unit_pixels = 0, nested_modules_graphics_Unit_points = 1 } nested_modules_graphics_Unit; +/** Error codes reported by throwing functions in the `graphics` module tree. */ +typedef enum { nested_modules_graphics_GraphicsError_OutOfBounds = 1 } nested_modules_graphics_GraphicsError; + +// Module: graphics +/** Construct a Canvas. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_new_canvas(int32_t width, size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes +/** Construct a Shape. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_make_shape(int32_t sides, size_t* out_len, nested_modules_error* out_err); +/** Nested function returning a struct owned by an ancestor module. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_describe(size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes_geometry +/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, nested_modules_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // NESTED_MODULES_H + +// END nested_modules.h diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__runtime_go.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__runtime_go.snap new file mode 100644 index 00000000..3a7876cf --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__runtime_go.snap @@ -0,0 +1,305 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package nested_modules + +/* +#include +#include "nested_modules.h" +*/ +import "C" + +import ( + "context" + "fmt" + "runtime/cgo" + "sync/atomic" + "unsafe" +) + +// wvABIVersion is the C ABI revision these bindings were generated against. +const wvABIVersion uint32 = 3 + +// The runtime error codes this package produces or interprets itself. +const ( + wvCodeMarshal int32 = -3 + wvCodeForeign int32 = -4 + wvCodeCancelled int32 = -5 +) + +// Error reports a failure that no typed error domain claims: an unknown +// code, a generic failure (code -1), a producer panic (-2), a marshalling +// failure (-3), or a Go callback-interface implementation that panicked +// (-4). The code is preserved so callers can tell them apart. +type Error struct { + // Code is the numeric ABI error code. + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *Error) Error() string { + return fmt.Sprintf("nested_modules: %s (code %d)", e.Message, e.Code) +} + +// DebugLive reports how many resources of one kind the native library holds: +// 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned +// allocations. It's always 0 unless the library was built with leak checks +// enabled; tests use it to assert that nothing leaked. +func DebugLive(kind int32) uint64 { + return uint64(C.nested_modules_debug_live(C.int32_t(kind))) +} + +// wvCheckABI panics when the linked library implements a different C ABI +// revision than these bindings were generated for. +func wvCheckABI() { + if found := uint32(C.nested_modules_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("nested_modules: ABI mismatch: these bindings expect revision %d but the linked library reports revision %d", wvABIVersion, found)) + } +} + +// wvCheckModule panics when the linked library's contract checksum for a +// top-level module differs from the one these bindings were generated +// against, which means the bindings are stale. +func wvCheckModule(module string, want, found uint64) { + if found != want { + panic(fmt.Sprintf("nested_modules: module %q doesn't match the linked library (bindings checksum %#016x, library checksum %#016x); regenerate the bindings", module, want, found)) + } +} + +// ── Errors ── + +// wvFailure is a non-zero error slot copied into Go memory. +type wvFailure struct { + code int32 + message string + payload []byte +} + +// err converts the failure into the generic *Error. +func (f wvFailure) err() error { + return &Error{Code: f.code, Message: f.message} +} + +func wvCopyFailure(cErr *C.nested_modules_error) wvFailure { + f := wvFailure{code: int32(cErr.code)} + if cErr.message != nil { + f.message = C.GoString(cErr.message) + } + if cErr.payload_ptr != nil { + f.payload = wvBorrowBytes(cErr.payload_ptr, cErr.payload_len) + } + return f +} + +// wvTakeError copies a non-zero error slot and clears it. +func wvTakeError(cErr *C.nested_modules_error) wvFailure { + f := wvCopyFailure(cErr) + C.nested_modules_error_clear(cErr) + return f +} + +// wvTakeBoxedError copies the heap-boxed error an async completion receives +// and frees the box. +func wvTakeBoxedError(cErr *C.nested_modules_error) wvFailure { + f := wvCopyFailure(cErr) + C.nested_modules_error_free(cErr) + return f +} + +// wvTrap panics with the generic *Error when the slot reports a failure. +// Wrappers of functions that declare no error domain check their slot with +// it: a failure there can only be a producer bug, a marshalling failure, or +// a callback-interface implementation that panicked. +func wvTrap(cErr *C.nested_modules_error) { + if cErr.code != 0 { + panic(wvTakeError(cErr).err()) + } +} + +// wvForeignError reports a panic recovered inside a callback-interface +// trampoline through the producer's error slot (code -4). The producer +// copies the message, so it's only borrowed for the call. +func wvForeignError(outErr *C.nested_modules_error, recovered any) { + msg := C.CString(fmt.Sprint(recovered)) + defer C.free(unsafe.Pointer(msg)) + C.nested_modules_error_set(outErr, C.int32_t(wvCodeForeign), msg) +} + +// ── Strings and bytes ── + +// wvStr returns a borrowed (pointer, length) view of s's UTF-8 bytes for the +// duration of one call. Strings may contain NUL bytes. +func wvStr(s string) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(s))), C.size_t(len(s)) +} + +// wvBytes returns a borrowed (pointer, length) view of b for the duration of +// one call. +func wvBytes(b []byte) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.SliceData(b))), C.size_t(len(b)) +} + +// wvBorrowString copies a borrowed UTF-8 run into a Go string. +func wvBorrowString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + return string(unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) +} + +// wvTakeString copies a returned UTF-8 run into a Go string and releases the +// producer's allocation. +func wvTakeString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + s := wvBorrowString(ptr, n) + C.nested_modules_free_bytes(ptr, n) + return s +} + +// wvBorrowBytes copies a borrowed byte run into Go memory. A null pointer +// copies to nil. +func wvBorrowBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + return append([]byte{}, unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)...) +} + +// wvTakeBytes copies a returned byte run (raw bytes or a value buffer) into +// Go memory and releases the producer's allocation. +func wvTakeBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + b := wvBorrowBytes(ptr, n) + C.nested_modules_free_bytes(ptr, n) + return b +} + +// ── Objects ── + +// wvRef owns one strong reference to a native object on behalf of a wrapper. +// Calls borrow the pointer between acquire and release. Close may race them: +// the reference is destroyed exactly once, by close when no call is in +// flight, or else by the last in-flight call to finish. +type wvRef struct { + ptr unsafe.Pointer + destroy func(unsafe.Pointer) + // state counts in-flight calls in its upper bits; the low bit is set + // once close has run. + state atomic.Uint64 +} + +func (r *wvRef) init(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { + r.ptr = ptr + r.destroy = destroy +} + +// acquire borrows the pointer for one call, panicking when the wrapper was +// already closed. +func (r *wvRef) acquire(typeName string) unsafe.Pointer { + for { + s := r.state.Load() + if s&1 != 0 { + panic("nested_modules: " + typeName + " used after Close") + } + if r.state.CompareAndSwap(s, s+2) { + return r.ptr + } + } +} + +// release ends one call's borrow. +func (r *wvRef) release() { + if r.state.Add(^uint64(1)) == 1 { + r.destroy(r.ptr) + } +} + +// close releases the wrapper's reference once no call is in flight. Calling +// it again is a no-op. +func (r *wvRef) close() { + for { + s := r.state.Load() + if s&1 != 0 { + return + } + if r.state.CompareAndSwap(s, s|1) { + if s == 0 { + r.destroy(r.ptr) + } + return + } + } +} + +// ── Async ── + +// wvOutcome is one async completion: a converted result, or the failure the +// producer reported. +type wvOutcome[T any] struct { + val T + fail *wvFailure +} + +// wvComplete delivers one async completion to the waiting wrapper. It runs on +// a producer thread inside an exported trampoline, so it never panics: a +// result that fails to decode is delivered as a marshalling failure. +func wvComplete[T any](handle unsafe.Pointer, cErr *C.nested_modules_error, convert func() T) { + h := cgo.Handle(uintptr(handle)) + done := h.Value().(chan wvOutcome[T]) + h.Delete() + done <- wvSettle(cErr, convert) +} + +func wvSettle[T any](cErr *C.nested_modules_error, convert func() T) (o wvOutcome[T]) { + if cErr != nil { + if f := wvTakeBoxedError(cErr); f.code != 0 { + o.fail = &f + return o + } + } + defer func() { + if r := recover(); r != nil { + o = wvOutcome[T]{fail: &wvFailure{code: wvCodeMarshal, message: fmt.Sprint(r)}} + } + }() + o.val = convert() + return o +} + +// wvAwait blocks until an async call completes or ctx is done. With a cancel +// token, ctx's cancellation cancels the native call and waits for its +// completion, which the producer delivers promptly; without one, the call is +// abandoned and ctx.Err() returned at once. A cancelled completion (code -5) +// returns ctx.Err(), or context.Canceled when ctx itself isn't done. +func wvAwait[T any](ctx context.Context, done chan wvOutcome[T], token *C.nested_modules_cancel_token) (T, *wvFailure, error) { + var o wvOutcome[T] + select { + case o = <-done: + case <-ctx.Done(): + if token == nil { + var zero T + return zero, nil, ctx.Err() + } + C.nested_modules_cancel_token_cancel(token) + o = <-done + } + if o.fail != nil && o.fail.code == wvCodeCancelled { + var zero T + if err := ctx.Err(); err != nil { + return zero, nil, err + } + return zero, nil, context.Canceled + } + return o.val, o.fail, nil +} + +// END runtime.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap deleted file mode 100644 index 92d3337e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap +++ /dev/null @@ -1,468 +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" -) - -// The ABI revision these bindings were generated against. -const wvABIVersion uint32 = 2 - -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, a -// producer panic (code -2), or a Go callback-interface implementation -// that panicked (code -4). -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, a marshalling failure, or a callback-interface -// implementation that panicked. The panic value is a *WeaveFFIError, so a -// recover() site can still inspect the code. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - panic(wvBrandError(wvTakeError(cErr))) - } -} - -// 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)) -} - -// GraphicsError is a typed error reported by the `graphics` module. -type GraphicsError struct { - // Code is the numeric ABI error code (one of the GraphicsError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *GraphicsError) Error() string { - return fmt.Sprintf("graphics: %s (code %d)", e.Message, e.Code) -} - -// GraphicsError codes. -const ( - // GraphicsErrorOutOfBounds coordinate out of bounds - GraphicsErrorOutOfBounds int32 = 1 -) - -// wvMapGraphics converts a non-zero code from the `graphics` domain into a -// *GraphicsError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapGraphics(code int32, message string, payload []byte) error { - switch code { - case GraphicsErrorOutOfBounds: - if message == "" { - message = "coordinate out of bounds" - } - return &GraphicsError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -// Unit: A measurement unit shared with nested submodules. -type Unit int32 - -const ( - UnitPixels Unit = 0 - UnitPoints Unit = 1 -) - -// Canvas: Top-level struct that references a struct defined one level deeper. -type Canvas struct { - Width int32 - // Root: Reference down into a nested module - Root Shape -} - -// wvPackCanvas appends v to w in the value-buffer wire format. -func wvPackCanvas(w *wvWriter, v Canvas) { - w.writeI32(v.Width) - wvPackShape(w, v.Root) -} - -// wvUnpackCanvas decodes one Canvas from r. -func wvUnpackCanvas(r *wvReader) Canvas { - var v Canvas - v.Width = r.readI32() - v.Root = wvUnpackShape(r) - return v -} - -// NewCanvas: Construct a Canvas. -func NewCanvas(width int32) Canvas { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_graphics_new_canvas(C.int32_t(width), &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Canvas - goResult = wvUnpackCanvas(rRes) - rRes.expectEnd() - return goResult -} - -// Shape: A struct one level deep that references both an ancestor enum and a deeper struct. -type Shape struct { - Sides int32 - // Unit: Reference up to the parent module's enum - Unit Unit - // Detail: Reference down into a deeper module - Detail Detail -} - -// wvPackShape appends v to w in the value-buffer wire format. -func wvPackShape(w *wvWriter, v Shape) { - w.writeI32(v.Sides) - w.writeI32(int32(v.Unit)) - wvPackDetail(w, v.Detail) -} - -// wvUnpackShape decodes one Shape from r. -func wvUnpackShape(r *wvReader) Shape { - var v Shape - v.Sides = r.readI32() - v.Unit = Unit(r.readI32()) - v.Detail = wvUnpackDetail(r) - return v -} - -// MakeShape: Construct a Shape. -func MakeShape(sides int32) Shape { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_graphics_shapes_make_shape(C.int32_t(sides), &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Shape - goResult = wvUnpackShape(rRes) - rRes.expectEnd() - return goResult -} - -// Describe: Nested function returning a struct owned by an ancestor module. -func Describe() Canvas { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_graphics_shapes_describe(&cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Canvas - goResult = wvUnpackCanvas(rRes) - rRes.expectEnd() - return goResult -} - -// Detail: A struct two levels deep. -type Detail struct { - Area float64 -} - -// wvPackDetail appends v to w in the value-buffer wire format. -func wvPackDetail(w *wvWriter, v Detail) { - w.writeF64(v.Area) -} - -// wvUnpackDetail decodes one Detail from r. -func wvUnpackDetail(r *wvReader) Detail { - var v Detail - v.Area = r.readF64() - return v -} - -// Compute: Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. -func Compute(shape Shape) (Detail, error) { - wShape := &wvWriter{} - wvPackShape(wShape, shape) - var cShapePtr *C.uint8_t - if len(wShape.buf) > 0 { - cShapePtr = (*C.uint8_t)(unsafe.Pointer(&wShape.buf[0])) - } - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_graphics_shapes_geometry_compute(cShapePtr, C.size_t(len(wShape.buf)), &cOutLen, &cErr) - if cErr.code != 0 { - return Detail{}, wvMapGraphics(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Detail - goResult = wvUnpackDetail(rRes) - rRes.expectEnd() - return goResult, nil -} - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__README_md.snap index 7d3c4f86..9bf2eb9d 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_shapes__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_shapes__README_md.snap @@ -1,53 +1,49 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Go Bindings +# shapes (Go) -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`) +cgo bindings for the `shapes` native library. ## 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`: +The module ships its own copy of the C header (`shapes.h`) and links with +`-lshapes`. Point the linker at the directory holding `libshapes.so`, +`libshapes.dylib`, or `shapes.dll`, and make the library findable at +run time: ```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" +export CGO_LDFLAGS="-L/path/to/lib" +export LD_LIBRARY_PATH="/path/to/lib" # DYLD_LIBRARY_PATH on macOS +go build ./... ``` -2. Build or run your Go project that imports this module: +Depend on the module from your own project with a `require` (and, for a +local checkout, a `replace`) of `shapes`, then import it: -```sh -go build ./... +```go +import "shapes" ``` -## 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. - -Interfaces are reference-counted objects: each wrapper holds one strong -reference that `Close()` releases (a finalizer releases it if you forget), and -a nullable interface is a nil wrapper pointer. Callback interfaces are Go -`interface` types: implement one and pass the value to any function that -accepts it; the native library may call it from any thread, and a panic in -your implementation is reported to the native caller instead of crashing. +Go 1.23 or newer is required. + +## Usage notes + +- Records are plain structs and rich enums are sealed interfaces; both cross + the boundary by value. +- Interfaces are reference-counted objects. Call `Close` when you're done + with one (a finalizer releases it otherwise); it's safe to call twice or + while a call is in flight. +- Async functions take a `context.Context` first. Cancellable ones cancel + the native call when the context is done. +- Callback interfaces are Go interfaces you implement. The library may call + them from any thread, and a panic is reported to the native caller as a + foreign error instead of crashing. +- Loading checks the library's ABI revision and contract checksums and + panics with the stale module's name on a mismatch. diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__bindings_go.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__bindings_go.snap new file mode 100644 index 00000000..8df9b2ec --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_shapes__bindings_go.snap @@ -0,0 +1,226 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package shapes + +/* +#cgo LDFLAGS: -lshapes +#include "shapes.h" +*/ +import "C" + +func init() { + wvCheckABI() + wvCheckModule("shapes", 0x06acf433ffa2a0f0, uint64(C.shapes_shapes_checksum())) +} + +// Shape: An algebraic shape (sum type with associated data) +// +// Shape is a sealed sum type: exactly one of its variant structs is the +// value at a time. +type Shape interface { + isShape() +} + +// ShapeEmpty: The empty shape (a unit variant) +type ShapeEmpty struct{} + +func (ShapeEmpty) isShape() {} + +// ShapeCircle: A circle with a radius +type ShapeCircle struct { + // Radius: Radius in points + Radius float64 +} + +func (ShapeCircle) isShape() {} + +// ShapeRectangle: An axis-aligned rectangle +type ShapeRectangle struct { + Width float32 + Height float32 +} + +func (ShapeRectangle) isShape() {} + +// ShapeBounded: An integer-bounded shape (exercises narrow + wide ints) +type ShapeBounded struct { + Min int16 + Max int16 + Weight uint64 +} + +func (ShapeBounded) isShape() {} + +// ShapeLabeled: A labeled shape with a small count +type ShapeLabeled struct { + Label string + Count uint8 +} + +func (ShapeLabeled) isShape() {} + +// wvPackShape appends v to w in the value-buffer wire format. +func wvPackShape(w *wvWriter, v Shape) { + switch x := v.(type) { + case ShapeEmpty: + w.writeI32(0) + case ShapeCircle: + w.writeI32(1) + w.writeF64(x.Radius) + case ShapeRectangle: + w.writeI32(2) + w.writeF32(x.Width) + w.writeF32(x.Height) + case ShapeBounded: + w.writeI32(3) + w.writeI16(x.Min) + w.writeI16(x.Max) + w.writeU64(x.Weight) + case ShapeLabeled: + w.writeI32(4) + w.writeString(x.Label) + w.writeU8(x.Count) + default: + panic("shapes: Shape value is not one of its variants") + } +} + +// wvUnpackShape decodes one Shape from r. +func wvUnpackShape(r *wvReader) Shape { + switch r.readI32() { + case 0: + return ShapeEmpty{} + case 1: + var x ShapeCircle + x.Radius = r.readF64() + return x + case 2: + var x ShapeRectangle + x.Width = r.readF32() + x.Height = r.readF32() + return x + case 3: + var x ShapeBounded + x.Min = r.readI16() + x.Max = r.readI16() + x.Weight = r.readU64() + return x + case 4: + var x ShapeLabeled + x.Label = r.readString() + x.Count = r.readU8() + return x + default: + panic("shapes: malformed value buffer: Shape tag out of range") + } +} + +// Channel: A plain C-style enum (no payloads) +type Channel int32 + +const ( + ChannelRed Channel = 0 + ChannelGreen Channel = 1 + ChannelBlue Channel = 2 +) + +// Numerics: One field per supported numeric primitive +type Numerics struct { + A int8 + B int16 + C int32 + D int64 + E uint8 + F uint16 + G uint32 + H uint64 + I float32 + J float64 +} + +// wvPackNumerics appends v to w in the value-buffer wire format. +func wvPackNumerics(w *wvWriter, v Numerics) { + w.writeI8(v.A) + w.writeI16(v.B) + w.writeI32(v.C) + w.writeI64(v.D) + w.writeU8(v.E) + w.writeU16(v.F) + w.writeU32(v.G) + w.writeU64(v.H) + w.writeF32(v.I) + w.writeF64(v.J) +} + +// wvUnpackNumerics decodes one Numerics from r. +func wvUnpackNumerics(r *wvReader) Numerics { + var v Numerics + v.A = r.readI8() + v.B = r.readI16() + v.C = r.readI32() + v.D = r.readI64() + v.E = r.readU8() + v.F = r.readU16() + v.G = r.readU32() + v.H = r.readU64() + v.I = r.readF32() + v.J = r.readF64() + return v +} + +// Describe: Render a shape to a string +func Describe(shape Shape) string { + wShape := &wvWriter{} + wvPackShape(wShape, shape) + cShapePtr, cShapeLen := wvBytes(wShape.buf) + var cRetLen C.size_t + var cErr C.shapes_error + cRet := C.shapes_shapes_describe(cShapePtr, cShapeLen, &cRetLen, &cErr) + wvTrap(&cErr) + return wvTakeString(cRet, cRetLen) +} + +// Scale: Scale a shape by a factor, returning a new shape +func Scale(shape Shape, factor float64) Shape { + wShape := &wvWriter{} + wvPackShape(wShape, shape) + cShapePtr, cShapeLen := wvBytes(wShape.buf) + var cRetLen C.size_t + var cErr C.shapes_error + cRet := C.shapes_shapes_scale(cShapePtr, cShapeLen, C.double(factor), &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Shape + ret = wvUnpackShape(rRet) + rRet.expectEnd() + return ret +} + +// SumBytes: Sum a list of bytes into a wide integer (numerics smoke) +func SumBytes(values []byte) uint64 { + cValuesPtr, cValuesLen := wvBytes(values) + var cErr C.shapes_error + cRet := C.shapes_shapes_sum_bytes(cValuesPtr, cValuesLen, &cErr) + wvTrap(&cErr) + return uint64(cRet) +} + +// MakeNumerics: Build a Numerics struct from every numeric primitive +func MakeNumerics(a int8, b int16, c int32, d int64, e uint8, f uint16, g uint32, h uint64, i float32, j float64) Numerics { + var cRetLen C.size_t + var cErr C.shapes_error + cRet := C.shapes_shapes_make_numerics(C.int8_t(a), C.int16_t(b), C.int32_t(c), C.int64_t(d), C.uint8_t(e), C.uint16_t(f), C.uint32_t(g), C.uint64_t(h), C.float(i), C.double(j), &cRetLen, &cErr) + wvTrap(&cErr) + rRet := &wvReader{buf: wvTakeBytes(cRet, cRetLen)} + var ret Numerics + ret = wvUnpackNumerics(rRet) + rRet.expectEnd() + return ret +} + +// END bindings.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__codec_go.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__codec_go.snap new file mode 100644 index 00000000..abdff52a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_shapes__codec_go.snap @@ -0,0 +1,205 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package shapes + +import ( + "encoding/binary" + "math" + "unicode/utf8" +) + +// wvWriter serializes values into the value-buffer format: little-endian, +// packed, u32 length prefixes. +type wvWriter struct { + buf []byte +} + +func (w *wvWriter) writeBool(v bool) { + if v { + w.buf = append(w.buf, 1) + } else { + w.buf = append(w.buf, 0) + } +} + +func (w *wvWriter) writeI8(v int8) { + w.buf = append(w.buf, byte(v)) +} + +func (w *wvWriter) writeU8(v uint8) { + w.buf = append(w.buf, v) +} + +func (w *wvWriter) writeI16(v int16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v)) +} + +func (w *wvWriter) writeU16(v uint16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, v) +} + +func (w *wvWriter) writeI32(v int32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v)) +} + +func (w *wvWriter) writeU32(v uint32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, v) +} + +func (w *wvWriter) writeI64(v int64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v)) +} + +func (w *wvWriter) writeU64(v uint64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, v) +} + +func (w *wvWriter) writeF32(v float32) { + w.writeU32(math.Float32bits(v)) +} + +func (w *wvWriter) writeF64(v float64) { + w.writeU64(math.Float64bits(v)) +} + +func (w *wvWriter) writeLen(n int) { + if n < 0 || uint64(n) > math.MaxUint32 { + panic("shapes: 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 value-buffer format. A malformed buffer +// is a producer/consumer contract violation, so every read panics instead of +// returning a typed domain error. +type wvReader struct { + buf []byte + pos int +} + +func wvMalformed(context string) { + panic("shapes: malformed value buffer: " + context) +} + +func (r *wvReader) take(n int, context string) []byte { + if n < 0 || len(r.buf)-r.pos < n { + wvMalformed(context) + } + b := r.buf[r.pos : r.pos+n] + r.pos += n + return b +} + +func (r *wvReader) readBool() bool { + switch r.take(1, "bool")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("bool byte out of range") + return false +} + +func (r *wvReader) readI8() int8 { + return int8(r.take(1, "i8")[0]) +} + +func (r *wvReader) readU8() uint8 { + return r.take(1, "u8")[0] +} + +func (r *wvReader) readI16() int16 { + return int16(binary.LittleEndian.Uint16(r.take(2, "i16"))) +} + +func (r *wvReader) readU16() uint16 { + return binary.LittleEndian.Uint16(r.take(2, "u16")) +} + +func (r *wvReader) readI32() int32 { + return int32(binary.LittleEndian.Uint32(r.take(4, "i32"))) +} + +func (r *wvReader) readU32() uint32 { + return binary.LittleEndian.Uint32(r.take(4, "u32")) +} + +func (r *wvReader) readI64() int64 { + return int64(binary.LittleEndian.Uint64(r.take(8, "i64"))) +} + +func (r *wvReader) readU64() uint64 { + return binary.LittleEndian.Uint64(r.take(8, "u64")) +} + +func (r *wvReader) readF32() float32 { + return math.Float32frombits(r.readU32()) +} + +func (r *wvReader) readF64() float64 { + return math.Float64frombits(r.readU64()) +} + +// readLen reads a u32 element count. Elements may encode to zero bytes, so +// the count isn't checked against the remaining buffer; callers cap their +// preallocation with capHint instead. +func (r *wvReader) readLen() int { + return int(r.readU32()) +} + +// capHint bounds a preallocation for n decoded elements by the bytes left in +// the buffer, so a corrupt count can't force a huge allocation. +func (r *wvReader) capHint(n int) int { + return min(n, len(r.buf)-r.pos) +} + +func (r *wvReader) readString() string { + b := r.take(r.readLen(), "string bytes") + if !utf8.Valid(b) { + wvMalformed("string is not valid UTF-8") + } + return string(b) +} + +func (r *wvReader) readBytes() []byte { + return append([]byte{}, r.take(r.readLen(), "byte buffer")...) +} + +func (r *wvReader) readOptionFlag() bool { + switch r.take(1, "option flag")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("option flag byte out of range") + return false +} + +func (r *wvReader) expectEnd() { + if r.pos != len(r.buf) { + wvMalformed("trailing bytes after value") + } +} + +// END codec.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__go_mod.snap index 73a087a7..f9f7bd34 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_shapes__go_mod.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_shapes__go_mod.snap @@ -1,11 +1,11 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -module weaveffi +module shapes go 1.23 diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__runtime_go.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__runtime_go.snap new file mode 100644 index 00000000..e8f30115 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_shapes__runtime_go.snap @@ -0,0 +1,305 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package shapes + +/* +#include +#include "shapes.h" +*/ +import "C" + +import ( + "context" + "fmt" + "runtime/cgo" + "sync/atomic" + "unsafe" +) + +// wvABIVersion is the C ABI revision these bindings were generated against. +const wvABIVersion uint32 = 3 + +// The runtime error codes this package produces or interprets itself. +const ( + wvCodeMarshal int32 = -3 + wvCodeForeign int32 = -4 + wvCodeCancelled int32 = -5 +) + +// Error reports a failure that no typed error domain claims: an unknown +// code, a generic failure (code -1), a producer panic (-2), a marshalling +// failure (-3), or a Go callback-interface implementation that panicked +// (-4). The code is preserved so callers can tell them apart. +type Error struct { + // Code is the numeric ABI error code. + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *Error) Error() string { + return fmt.Sprintf("shapes: %s (code %d)", e.Message, e.Code) +} + +// DebugLive reports how many resources of one kind the native library holds: +// 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned +// allocations. It's always 0 unless the library was built with leak checks +// enabled; tests use it to assert that nothing leaked. +func DebugLive(kind int32) uint64 { + return uint64(C.shapes_debug_live(C.int32_t(kind))) +} + +// wvCheckABI panics when the linked library implements a different C ABI +// revision than these bindings were generated for. +func wvCheckABI() { + if found := uint32(C.shapes_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("shapes: ABI mismatch: these bindings expect revision %d but the linked library reports revision %d", wvABIVersion, found)) + } +} + +// wvCheckModule panics when the linked library's contract checksum for a +// top-level module differs from the one these bindings were generated +// against, which means the bindings are stale. +func wvCheckModule(module string, want, found uint64) { + if found != want { + panic(fmt.Sprintf("shapes: module %q doesn't match the linked library (bindings checksum %#016x, library checksum %#016x); regenerate the bindings", module, want, found)) + } +} + +// ── Errors ── + +// wvFailure is a non-zero error slot copied into Go memory. +type wvFailure struct { + code int32 + message string + payload []byte +} + +// err converts the failure into the generic *Error. +func (f wvFailure) err() error { + return &Error{Code: f.code, Message: f.message} +} + +func wvCopyFailure(cErr *C.shapes_error) wvFailure { + f := wvFailure{code: int32(cErr.code)} + if cErr.message != nil { + f.message = C.GoString(cErr.message) + } + if cErr.payload_ptr != nil { + f.payload = wvBorrowBytes(cErr.payload_ptr, cErr.payload_len) + } + return f +} + +// wvTakeError copies a non-zero error slot and clears it. +func wvTakeError(cErr *C.shapes_error) wvFailure { + f := wvCopyFailure(cErr) + C.shapes_error_clear(cErr) + return f +} + +// wvTakeBoxedError copies the heap-boxed error an async completion receives +// and frees the box. +func wvTakeBoxedError(cErr *C.shapes_error) wvFailure { + f := wvCopyFailure(cErr) + C.shapes_error_free(cErr) + return f +} + +// wvTrap panics with the generic *Error when the slot reports a failure. +// Wrappers of functions that declare no error domain check their slot with +// it: a failure there can only be a producer bug, a marshalling failure, or +// a callback-interface implementation that panicked. +func wvTrap(cErr *C.shapes_error) { + if cErr.code != 0 { + panic(wvTakeError(cErr).err()) + } +} + +// wvForeignError reports a panic recovered inside a callback-interface +// trampoline through the producer's error slot (code -4). The producer +// copies the message, so it's only borrowed for the call. +func wvForeignError(outErr *C.shapes_error, recovered any) { + msg := C.CString(fmt.Sprint(recovered)) + defer C.free(unsafe.Pointer(msg)) + C.shapes_error_set(outErr, C.int32_t(wvCodeForeign), msg) +} + +// ── Strings and bytes ── + +// wvStr returns a borrowed (pointer, length) view of s's UTF-8 bytes for the +// duration of one call. Strings may contain NUL bytes. +func wvStr(s string) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(s))), C.size_t(len(s)) +} + +// wvBytes returns a borrowed (pointer, length) view of b for the duration of +// one call. +func wvBytes(b []byte) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.SliceData(b))), C.size_t(len(b)) +} + +// wvBorrowString copies a borrowed UTF-8 run into a Go string. +func wvBorrowString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + return string(unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) +} + +// wvTakeString copies a returned UTF-8 run into a Go string and releases the +// producer's allocation. +func wvTakeString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + s := wvBorrowString(ptr, n) + C.shapes_free_bytes(ptr, n) + return s +} + +// wvBorrowBytes copies a borrowed byte run into Go memory. A null pointer +// copies to nil. +func wvBorrowBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + return append([]byte{}, unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)...) +} + +// wvTakeBytes copies a returned byte run (raw bytes or a value buffer) into +// Go memory and releases the producer's allocation. +func wvTakeBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + b := wvBorrowBytes(ptr, n) + C.shapes_free_bytes(ptr, n) + return b +} + +// ── Objects ── + +// wvRef owns one strong reference to a native object on behalf of a wrapper. +// Calls borrow the pointer between acquire and release. Close may race them: +// the reference is destroyed exactly once, by close when no call is in +// flight, or else by the last in-flight call to finish. +type wvRef struct { + ptr unsafe.Pointer + destroy func(unsafe.Pointer) + // state counts in-flight calls in its upper bits; the low bit is set + // once close has run. + state atomic.Uint64 +} + +func (r *wvRef) init(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { + r.ptr = ptr + r.destroy = destroy +} + +// acquire borrows the pointer for one call, panicking when the wrapper was +// already closed. +func (r *wvRef) acquire(typeName string) unsafe.Pointer { + for { + s := r.state.Load() + if s&1 != 0 { + panic("shapes: " + typeName + " used after Close") + } + if r.state.CompareAndSwap(s, s+2) { + return r.ptr + } + } +} + +// release ends one call's borrow. +func (r *wvRef) release() { + if r.state.Add(^uint64(1)) == 1 { + r.destroy(r.ptr) + } +} + +// close releases the wrapper's reference once no call is in flight. Calling +// it again is a no-op. +func (r *wvRef) close() { + for { + s := r.state.Load() + if s&1 != 0 { + return + } + if r.state.CompareAndSwap(s, s|1) { + if s == 0 { + r.destroy(r.ptr) + } + return + } + } +} + +// ── Async ── + +// wvOutcome is one async completion: a converted result, or the failure the +// producer reported. +type wvOutcome[T any] struct { + val T + fail *wvFailure +} + +// wvComplete delivers one async completion to the waiting wrapper. It runs on +// a producer thread inside an exported trampoline, so it never panics: a +// result that fails to decode is delivered as a marshalling failure. +func wvComplete[T any](handle unsafe.Pointer, cErr *C.shapes_error, convert func() T) { + h := cgo.Handle(uintptr(handle)) + done := h.Value().(chan wvOutcome[T]) + h.Delete() + done <- wvSettle(cErr, convert) +} + +func wvSettle[T any](cErr *C.shapes_error, convert func() T) (o wvOutcome[T]) { + if cErr != nil { + if f := wvTakeBoxedError(cErr); f.code != 0 { + o.fail = &f + return o + } + } + defer func() { + if r := recover(); r != nil { + o = wvOutcome[T]{fail: &wvFailure{code: wvCodeMarshal, message: fmt.Sprint(r)}} + } + }() + o.val = convert() + return o +} + +// wvAwait blocks until an async call completes or ctx is done. With a cancel +// token, ctx's cancellation cancels the native call and waits for its +// completion, which the producer delivers promptly; without one, the call is +// abandoned and ctx.Err() returned at once. A cancelled completion (code -5) +// returns ctx.Err(), or context.Canceled when ctx itself isn't done. +func wvAwait[T any](ctx context.Context, done chan wvOutcome[T], token *C.shapes_cancel_token) (T, *wvFailure, error) { + var o wvOutcome[T] + select { + case o = <-done: + case <-ctx.Done(): + if token == nil { + var zero T + return zero, nil, ctx.Err() + } + C.shapes_cancel_token_cancel(token) + o = <-done + } + if o.fail != nil && o.fail.code == wvCodeCancelled { + var zero T + if err := ctx.Err(); err != nil { + return zero, nil, err + } + return zero, nil, context.Canceled + } + return o.val, o.fail, nil +} + +// END runtime.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__shapes_h.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__shapes_h.snap new file mode 100644 index 00000000..df20c3f5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_shapes__shapes_h.snap @@ -0,0 +1,151 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef SHAPES_H +#define SHAPES_H + +#include +#include +#include + +#ifndef SHAPES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef SHAPES_BUILD +# define SHAPES_API __declspec(dllexport) +# else +# define SHAPES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define SHAPES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define SHAPES_API __attribute__((visibility("default"))) +# else +# define SHAPES_API +# endif +#endif + +#ifndef SHAPES_DEPRECATED +# if defined(_MSC_VER) +# define SHAPES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define SHAPES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define SHAPES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports shapes_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define SHAPES_ABI_VERSION 3u +SHAPES_API uint32_t shapes_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by shapes_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct shapes_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} shapes_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +SHAPES_API void shapes_error_set(shapes_error* err, int32_t code, const char* message); +SHAPES_API void shapes_error_clear(shapes_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + shapes_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +SHAPES_API void shapes_error_free(shapes_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +SHAPES_API void shapes_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct shapes_cancel_token shapes_cancel_token; +SHAPES_API shapes_cancel_token* shapes_cancel_token_create(void); +SHAPES_API void shapes_cancel_token_cancel(shapes_cancel_token* token); +SHAPES_API bool shapes_cancel_token_is_cancelled(const shapes_cancel_token* token); +SHAPES_API void shapes_cancel_token_destroy(shapes_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +SHAPES_API uint8_t* shapes_alloc(uint32_t size); +SHAPES_API void shapes_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +SHAPES_API uint64_t shapes_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define SHAPES_SHAPES_CHECKSUM 0x06acf433ffa2a0f0ull +SHAPES_API uint64_t shapes_shapes_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with shapes_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * shapes_buffer.h declares a C struct and codec for each. + */ + +/** An algebraic shape (sum type with associated data) */ +typedef enum { + /** The empty shape (a unit variant) */ + shapes_shapes_Shape_Empty = 0, + /** A circle with a radius */ + shapes_shapes_Shape_Circle = 1, + /** An axis-aligned rectangle */ + shapes_shapes_Shape_Rectangle = 2, + /** An integer-bounded shape (exercises narrow + wide ints) */ + shapes_shapes_Shape_Bounded = 3, + /** A labeled shape with a small count */ + shapes_shapes_Shape_Labeled = 4 +} shapes_shapes_Shape_Tag; +/** A plain C-style enum (no payloads) */ +typedef enum { shapes_shapes_Channel_Red = 0, shapes_shapes_Channel_Green = 1, shapes_shapes_Channel_Blue = 2 } shapes_shapes_Channel; + +// Module: shapes +/** Render a shape to a string */ +SHAPES_API const uint8_t* shapes_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, shapes_error* out_err); +/** Scale a shape by a factor, returning a new shape */ +SHAPES_API const uint8_t* shapes_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, shapes_error* out_err); +/** Sum a list of bytes into a wide integer (numerics smoke) */ +SHAPES_API uint64_t shapes_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, shapes_error* out_err); +/** Build a Numerics struct from every numeric primitive */ +SHAPES_API const uint8_t* shapes_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, shapes_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // SHAPES_H + +// END shapes.h diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap deleted file mode 100644 index 79eafd73..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap +++ /dev/null @@ -1,521 +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" -) - -// The ABI revision these bindings were generated against. -const wvABIVersion uint32 = 2 - -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, a -// producer panic (code -2), or a Go callback-interface implementation -// that panicked (code -4). -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, a marshalling failure, or a callback-interface -// implementation that panicked. The panic value is a *WeaveFFIError, so a -// recover() site can still inspect the code. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - panic(wvBrandError(wvTakeError(cErr))) - } -} - -// 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)) -} - -// Shape: An algebraic shape (sum type with associated data) -// -// Shape is a sealed sum type: exactly one of its variant structs is the -// value at a time. -type Shape interface { - isShape() -} - -// ShapeEmpty: The empty shape (a unit variant) -type ShapeEmpty struct{} - -func (ShapeEmpty) isShape() {} - -// ShapeCircle: A circle with a radius -type ShapeCircle struct { - // Radius: Radius in points - Radius float64 -} - -func (ShapeCircle) isShape() {} - -// ShapeRectangle: An axis-aligned rectangle -type ShapeRectangle struct { - Width float32 - Height float32 -} - -func (ShapeRectangle) isShape() {} - -// ShapeBounded: An integer-bounded shape (exercises narrow + wide ints) -type ShapeBounded struct { - Min int16 - Max int16 - Weight uint64 -} - -func (ShapeBounded) isShape() {} - -// ShapeLabeled: A labeled shape with a small count -type ShapeLabeled struct { - Label string - Count uint8 -} - -func (ShapeLabeled) isShape() {} - -// wvPackShape appends v to w in the value-buffer wire format. -func wvPackShape(w *wvWriter, v Shape) { - switch x := v.(type) { - case ShapeEmpty: - w.writeI32(0) - case ShapeCircle: - w.writeI32(1) - w.writeF64(x.Radius) - case ShapeRectangle: - w.writeI32(2) - w.writeF32(x.Width) - w.writeF32(x.Height) - case ShapeBounded: - w.writeI32(3) - w.writeI16(x.Min) - w.writeI16(x.Max) - w.writeU64(x.Weight) - case ShapeLabeled: - w.writeI32(4) - w.writeString(x.Label) - w.writeU8(x.Count) - default: - panic("weaveffi: Shape value is not one of its variants") - } -} - -// wvUnpackShape decodes one Shape from r. -func wvUnpackShape(r *wvReader) Shape { - switch r.readI32() { - case 0: - return ShapeEmpty{} - case 1: - var x ShapeCircle - x.Radius = r.readF64() - return x - case 2: - var x ShapeRectangle - x.Width = r.readF32() - x.Height = r.readF32() - return x - case 3: - var x ShapeBounded - x.Min = r.readI16() - x.Max = r.readI16() - x.Weight = r.readU64() - return x - case 4: - var x ShapeLabeled - x.Label = r.readString() - x.Count = r.readU8() - return x - default: - panic("weaveffi: malformed value buffer: Shape tag out of range") - } -} - -// Channel: A plain C-style enum (no payloads) -type Channel int32 - -const ( - ChannelRed Channel = 0 - ChannelGreen Channel = 1 - ChannelBlue Channel = 2 -) - -// Numerics: One field per supported numeric primitive -type Numerics struct { - A int8 - B int16 - C int32 - D int64 - E uint8 - F uint16 - G uint32 - H uint64 - I float32 - J float64 -} - -// wvPackNumerics appends v to w in the value-buffer wire format. -func wvPackNumerics(w *wvWriter, v Numerics) { - w.writeI8(v.A) - w.writeI16(v.B) - w.writeI32(v.C) - w.writeI64(v.D) - w.writeU8(v.E) - w.writeU16(v.F) - w.writeU32(v.G) - w.writeU64(v.H) - w.writeF32(v.I) - w.writeF64(v.J) -} - -// wvUnpackNumerics decodes one Numerics from r. -func wvUnpackNumerics(r *wvReader) Numerics { - var v Numerics - v.A = r.readI8() - v.B = r.readI16() - v.C = r.readI32() - v.D = r.readI64() - v.E = r.readU8() - v.F = r.readU16() - v.G = r.readU32() - v.H = r.readU64() - v.I = r.readF32() - v.J = r.readF64() - return v -} - -// Describe: Render a shape to a string -func Describe(shape Shape) string { - wShape := &wvWriter{} - wvPackShape(wShape, shape) - var cShapePtr *C.uint8_t - if len(wShape.buf) > 0 { - cShapePtr = (*C.uint8_t)(unsafe.Pointer(&wShape.buf[0])) - } - var cErr C.weaveffi_error - result := C.weaveffi_shapes_describe(cShapePtr, C.size_t(len(wShape.buf)), &cErr) - wvTrap(&cErr) - goResult := C.GoString(result) - C.weaveffi_free_string(result) - return goResult -} - -// Scale: Scale a shape by a factor, returning a new shape -func Scale(shape Shape, factor float64) Shape { - wShape := &wvWriter{} - wvPackShape(wShape, shape) - var cShapePtr *C.uint8_t - if len(wShape.buf) > 0 { - cShapePtr = (*C.uint8_t)(unsafe.Pointer(&wShape.buf[0])) - } - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_shapes_scale(cShapePtr, C.size_t(len(wShape.buf)), C.double(factor), &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Shape - goResult = wvUnpackShape(rRes) - rRes.expectEnd() - return goResult -} - -// SumBytes: Sum a list of bytes into a wide integer (numerics smoke) -func SumBytes(values []byte) uint64 { - var cValuesPtr *C.uint8_t - cValuesLen := C.size_t(len(values)) - if len(values) > 0 { - cValuesPtr = (*C.uint8_t)(unsafe.Pointer(&values[0])) - } - var cErr C.weaveffi_error - result := C.weaveffi_shapes_sum_bytes(cValuesPtr, cValuesLen, &cErr) - wvTrap(&cErr) - return uint64(result) -} - -// MakeNumerics: Build a Numerics struct from every numeric primitive -func MakeNumerics(a int8, b int16, c int32, d int64, e uint8, f uint16, g uint32, h uint64, i float32, j float64) Numerics { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_shapes_make_numerics(C.int8_t(a), C.int16_t(b), C.int32_t(c), C.int64_t(d), C.uint8_t(e), C.uint16_t(f), C.uint32_t(g), C.uint64_t(h), C.float(i), C.double(j), &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Numerics - goResult = wvUnpackNumerics(rRes) - rRes.expectEnd() - return goResult -} - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__build_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__build_gradle_kts.snap index 374b107b..d2bbb125 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__build_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__build_gradle_kts.snap @@ -1,36 +1,63 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") version "1.9.22" + id("com.android.library") version "8.7.3" + id("org.jetbrains.kotlin.android") version "2.0.21" + `maven-publish` } +group = "docs_everywhere" +version = "0.1.0" + android { - namespace = "com.weaveffi" - compileSdk = 34 + namespace = "docs_everywhere" + compileSdk = 35 defaultConfig { - // java.lang.ref.Cleaner, which backs every wrapper's disposal, is API 33. - minSdk = 33 - externalNativeBuild { - cmake { - cppFlags("") - } - } + minSdk = 21 + consumerProguardFiles("consumer-rules.pro") } + // Builds libdocs_everywhere_jni.so per ABI, linked against the producer library in + // src/main/jniLibs// (see src/main/cpp/CMakeLists.txt). externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + } } dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") +} + +publishing { + publications { + register("release") { + artifactId = "docs_everywhere" + afterEvaluate { + from(components["release"]) + } + } + } } // END build.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__consumer_rules_pro.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__consumer_rules_pro.snap new file mode 100644 index 00000000..bd595a8f --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__consumer_rules_pro.snap @@ -0,0 +1,11 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# The JNI shim looks classes and methods up by name. +-keep class docs_everywhere.** { *; } + +# END consumer-rules.pro diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__settings_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__settings_gradle_kts.snap index 69449d7c..96cacab4 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__settings_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__settings_gradle_kts.snap @@ -1,10 +1,25 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -rootProject.name = "weaveffi" +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "docs_everywhere" // END settings.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_CMakeLists_txt.snap index 32fa1b44..0fccae2a 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_CMakeLists_txt.snap @@ -1,13 +1,34 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -cmake_minimum_required(VERSION 3.22) -project(weaveffi_jni C) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) +cmake_minimum_required(VERSION 3.18) +project(docs_everywhere_jni C) + +# The directory holding the producer library (libdocs_everywhere). Android builds +# default to the per-ABI copy under src/main/jniLibs/. +set(DOCS_EVERYWHERE_LIBRARY_DIR "" CACHE PATH "Directory holding the docs_everywhere producer library") +if(NOT DOCS_EVERYWHERE_LIBRARY_DIR AND ANDROID) + set(DOCS_EVERYWHERE_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}") +endif() +if(DOCS_EVERYWHERE_LIBRARY_DIR) + find_library(DOCS_EVERYWHERE_PRODUCER NAMES docs_everywhere PATHS "${DOCS_EVERYWHERE_LIBRARY_DIR}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED) +else() + set(CMAKE_FIND_FRAMEWORK NEVER) + find_library(DOCS_EVERYWHERE_PRODUCER NAMES docs_everywhere REQUIRED) +endif() + +add_library(docs_everywhere_jni SHARED docs_everywhere_jni.c) +target_include_directories(docs_everywhere_jni PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(docs_everywhere_jni PRIVATE "${DOCS_EVERYWHERE_PRODUCER}") +if(NOT ANDROID) + find_package(JNI REQUIRED) + find_package(Threads REQUIRED) + target_include_directories(docs_everywhere_jni PRIVATE ${JNI_INCLUDE_DIRS}) + target_link_libraries(docs_everywhere_jni PRIVATE Threads::Threads) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_h.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_h.snap new file mode 100644 index 00000000..31ab85e2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_h.snap @@ -0,0 +1,196 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef DOCS_EVERYWHERE_H +#define DOCS_EVERYWHERE_H + +#include +#include +#include + +#ifndef DOCS_EVERYWHERE_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef DOCS_EVERYWHERE_BUILD +# define DOCS_EVERYWHERE_API __declspec(dllexport) +# else +# define DOCS_EVERYWHERE_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define DOCS_EVERYWHERE_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define DOCS_EVERYWHERE_API __attribute__((visibility("default"))) +# else +# define DOCS_EVERYWHERE_API +# endif +#endif + +#ifndef DOCS_EVERYWHERE_DEPRECATED +# if defined(_MSC_VER) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define DOCS_EVERYWHERE_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports docs_everywhere_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define DOCS_EVERYWHERE_ABI_VERSION 3u +DOCS_EVERYWHERE_API uint32_t docs_everywhere_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by docs_everywhere_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct docs_everywhere_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} docs_everywhere_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_set(docs_everywhere_error* err, int32_t code, const char* message); +DOCS_EVERYWHERE_API void docs_everywhere_error_clear(docs_everywhere_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + docs_everywhere_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_free(docs_everywhere_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +DOCS_EVERYWHERE_API void docs_everywhere_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct docs_everywhere_cancel_token docs_everywhere_cancel_token; +DOCS_EVERYWHERE_API docs_everywhere_cancel_token* docs_everywhere_cancel_token_create(void); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_cancel(docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API bool docs_everywhere_cancel_token_is_cancelled(const docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_destroy(docs_everywhere_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +DOCS_EVERYWHERE_API uint8_t* docs_everywhere_alloc(uint32_t size); +DOCS_EVERYWHERE_API void docs_everywhere_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +DOCS_EVERYWHERE_API uint64_t docs_everywhere_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define DOCS_EVERYWHERE_DOCS_CHECKSUM 0x7950eec1e9cfc819ull +DOCS_EVERYWHERE_API uint64_t docs_everywhere_docs_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with docs_everywhere_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * docs_everywhere_buffer.h declares a C struct and codec for each. + */ + +/** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ +typedef enum { + /** Document is being edited */ + docs_everywhere_docs_Status_Draft = 0, + /** Document is publicly visible */ + docs_everywhere_docs_Status_Published = 1, + /** Document is hidden from listings */ + docs_everywhere_docs_Status_Archived = 2 +} docs_everywhere_docs_Status; +/** Error codes reported by throwing functions in the `docs` module tree. */ +typedef enum { + /** Returned when no document matches the requested id */ + docs_everywhere_docs_DocsErrors_not_found = 1, + /** Raised when attempting to move into a forbidden state */ + docs_everywhere_docs_DocsErrors_invalid_status = 2 +} docs_everywhere_docs_DocsErrors; +typedef struct docs_everywhere_docs_Notebook docs_everywhere_docs_Notebook; +/** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + * + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct docs_everywhere_docs_SavedListener_vtable { + /** Fires after a document has been persisted successfully */ + void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); + void (*free)(void* ctx); +} docs_everywhere_docs_SavedListener_vtable; +typedef void (*docs_everywhere_docs_publish_async_callback)(void* context, docs_everywhere_error* err, bool result); + +// Module: docs +/** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ +/** Create an empty notebook */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_new(docs_everywhere_error* out_err); +/** The largest number of documents one notebook will hold */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_max_documents(docs_everywhere_error* out_err); +/** Add a document, returning its position in the notebook */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_insert(const docs_everywhere_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, docs_everywhere_error* out_err); +/** Look up a document by identifier, failing when none matches */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_Notebook_find(const docs_everywhere_docs_Notebook* self, int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** The number of documents currently held */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_size(const docs_everywhere_docs_Notebook* self, docs_everywhere_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_clone(const docs_everywhere_docs_Notebook* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_Notebook_destroy(docs_everywhere_docs_Notebook* self); + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_create_document(const uint8_t* title_ptr, size_t title_len, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, docs_everywhere_error* out_err); +/** Look up a document by identifier */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_get_document(int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** Publish a document in the background */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_publish_async(int64_t id, docs_everywhere_docs_publish_async_callback callback, void* context); +/** Register a listener for saved notifications */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_watch(void* listener_ctx, const docs_everywhere_docs_SavedListener_vtable* listener_vtable, docs_everywhere_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // DOCS_EVERYWHERE_H + +// END docs_everywhere.h diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_jni_c.snap new file mode 100644 index 00000000..7be6f786 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_docs_everywhere_jni_c.snap @@ -0,0 +1,501 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#include +#include +#include +#include +#include +#include + +/* The shim forwards deprecated functions too, so it doesn't warn about them. */ +#define DOCS_EVERYWHERE_DEPRECATED(msg) +#include "docs_everywhere.h" + +/* Every static name in this file starts with `Jni_`, which no C ABI symbol + (all lowercase `docs_everywhere_...`) and no JNI export (`Java_...`) can + spell. */ + +static JavaVM* Jni_vm = NULL; +static jclass Jni_bridge = NULL; +static jmethodID Jni_error = NULL; + +/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value + buffers all cross as one. */ +typedef struct { + jbyteArray array; + jbyte* elems; + const uint8_t* ptr; + size_t len; +} Jni_bytes; + +static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { + Jni_bytes b = {array, NULL, NULL, 0}; + if (array == NULL) { + return b; + } + b.len = (size_t)(*env)->GetArrayLength(env, array); + b.elems = (*env)->GetByteArrayElements(env, array, NULL); + b.ptr = (const uint8_t*)b.elems; + if (b.elems == NULL) { + b.len = 0; + } + return b; +} + +static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { + if (b->elems != NULL) { + (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); + } +} + +/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid + input, so NULL + 0 is the empty array). */ +static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = (*env)->NewByteArray(env, (jsize)len); + if (out != NULL && len > 0 && ptr != NULL) { + (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); + } + return out; +} + +/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ +static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = Jni_new_bytes(env, ptr, len); + if (ptr != NULL) { + docs_everywhere_free_bytes((uint8_t*)ptr, len); + } + return out; +} + +/* Throws the Kotlin exception for `err` (mapped through domain `domain`, + 0 for the generic one) and clears `err`. */ +static inline void Jni_throw(JNIEnv* env, docs_everywhere_error* err, jint domain) { + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + docs_everywhere_error_clear(err); + if ((*env)->ExceptionCheck(env)) { + return; + } + jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); + if (ex != NULL && !(*env)->ExceptionCheck(env)) { + (*env)->Throw(env, ex); + } +} + +static jint Jni_load_error(JNIEnv* env, const char* message) { + jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); + if (cls != NULL) { + (*env)->ThrowNew(env, cls, message); + } + return JNI_ERR; +} + +/* Generated below: the contract checks and the method IDs the shim caches. */ +static jint Jni_load(JNIEnv* env); + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env = NULL; + (void)reserved; + Jni_vm = vm; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + if (docs_everywhere_abi_version() != 3u) { + return Jni_load_error(env, "docs_everywhere: the native library implements a different C ABI revision than these bindings (expected 3)"); + } + jclass bridge = (*env)->FindClass(env, "docs_everywhere/JniBridge"); + if (bridge == NULL) { + return JNI_ERR; + } + Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); + Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Ldocs_everywhere/FfiException;"); + if (Jni_error == NULL) { + return JNI_ERR; + } + if (Jni_load(env) != JNI_OK) { + return JNI_ERR; + } + return JNI_VERSION_1_6; +} + +JNIEXPORT jlong JNICALL Java_docs_1everywhere_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { + (void)env; + (void)cls; + return (jlong)docs_everywhere_debug_live((int32_t)kind); +} + +/* Producer threads (async completions, callback calls, vtable `free`) are + usually not JVM threads. The first time one calls in it attaches as a + daemon and keeps its JNIEnv; a thread-local destructor detaches it when + the thread exits, so a busy producer thread pays for one attach, not one + per call. */ +#if defined(_WIN32) +static void Jni_remember_attach(void) {} +#else +#include + +static pthread_key_t Jni_env_key; +static pthread_once_t Jni_env_once = PTHREAD_ONCE_INIT; + +static void Jni_detach(void* env) { + (void)env; + if (Jni_vm != NULL) { + (*Jni_vm)->DetachCurrentThread(Jni_vm); + } +} + +static void Jni_make_env_key(void) { + pthread_key_create(&Jni_env_key, Jni_detach); +} + +static void Jni_remember_attach(void) { + pthread_once(&Jni_env_once, Jni_make_env_key); + pthread_setspecific(Jni_env_key, (void*)Jni_vm); +} +#endif + +/* The calling thread's JNIEnv, attaching the thread if needed; NULL when + the JVM is unavailable. */ +static JNIEnv* Jni_env(void) { + JNIEnv* env = NULL; + if (Jni_vm == NULL) { + return NULL; + } + if ((*Jni_vm)->GetEnv(Jni_vm, (void**)&env, JNI_VERSION_1_6) == JNI_OK) { + return env; + } + /* `void*` converts to the `JNIEnv**` Android declares and the `void**` + the JDK declares. */ + if ((*Jni_vm)->AttachCurrentThreadAsDaemon(Jni_vm, (void*)&env, NULL) != JNI_OK) { + return NULL; + } + Jni_remember_attach(); + return env; +} + +/* Async completions deliver to the pinned Kotlin `NativeCompletion`. */ +static jmethodID Jni_on_unit = NULL; +static jmethodID Jni_on_boolean = NULL; +static jmethodID Jni_on_byte = NULL; +static jmethodID Jni_on_short = NULL; +static jmethodID Jni_on_int = NULL; +static jmethodID Jni_on_long = NULL; +static jmethodID Jni_on_float = NULL; +static jmethodID Jni_on_double = NULL; +static jmethodID Jni_on_bytes = NULL; +static jmethodID Jni_on_error = NULL; + +static jint Jni_load_async(JNIEnv* env) { + jclass cls = (*env)->FindClass(env, "docs_everywhere/NativeCompletion"); + if (cls == NULL) { + return JNI_ERR; + } + Jni_on_unit = (*env)->GetMethodID(env, cls, "onUnit", "()V"); + Jni_on_boolean = (*env)->GetMethodID(env, cls, "onBoolean", "(Z)V"); + Jni_on_byte = (*env)->GetMethodID(env, cls, "onByte", "(B)V"); + Jni_on_short = (*env)->GetMethodID(env, cls, "onShort", "(S)V"); + Jni_on_int = (*env)->GetMethodID(env, cls, "onInt", "(I)V"); + Jni_on_long = (*env)->GetMethodID(env, cls, "onLong", "(J)V"); + Jni_on_float = (*env)->GetMethodID(env, cls, "onFloat", "(F)V"); + Jni_on_double = (*env)->GetMethodID(env, cls, "onDouble", "(D)V"); + Jni_on_bytes = (*env)->GetMethodID(env, cls, "onBytes", "([B)V"); + Jni_on_error = (*env)->GetMethodID(env, cls, "onError", "(I[B[B)V"); + if (Jni_on_unit == NULL || Jni_on_error == NULL || Jni_on_bytes == NULL) { + return JNI_ERR; + } + return JNI_OK; +} + +/* Starts a completion on the producer thread: returns the JNIEnv with a + local frame pushed, or NULL when the call failed (the failure, or the + loss of the JVM, has then been handled and `context` released). */ +static JNIEnv* Jni_complete_begin(void* context, docs_everywhere_error* err) { + JNIEnv* env = Jni_env(); + if (env == NULL) { + docs_everywhere_error_free(err); + return NULL; + } + if ((*env)->PushLocalFrame(env, 8) != 0) { + (*env)->ExceptionClear(env); + docs_everywhere_error_free(err); + return NULL; + } + if (err == NULL) { + return env; + } + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + docs_everywhere_error_free(err); + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + (*env)->DeleteGlobalRef(env, (jobject)context); + return NULL; +} + +/* Finishes a successful completion: reports anything the Kotlin side threw + (it has no caller to propagate to) and releases `context`. */ +static void Jni_complete_end(JNIEnv* env, void* context) { + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + (*env)->DeleteGlobalRef(env, (jobject)context); +} + +JNIEXPORT jlong JNICALL Java_docs_1everywhere_JniBridge_cancel_1token_1create(JNIEnv* env, jclass cls) { + (void)env; + (void)cls; + return (jlong)(intptr_t)docs_everywhere_cancel_token_create(); +} + +JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_cancel_1token_1cancel(JNIEnv* env, jclass cls, jlong token) { + (void)env; + (void)cls; + docs_everywhere_cancel_token_cancel((docs_everywhere_cancel_token*)(intptr_t)token); +} + +JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_cancel_1token_1destroy(JNIEnv* env, jclass cls, jlong token) { + (void)env; + (void)cls; + docs_everywhere_cancel_token_destroy((docs_everywhere_cancel_token*)(intptr_t)token); +} + +/* Callback interfaces: the producer holds a global reference to the Kotlin + implementation as `ctx`; trampolines call the matching static dispatch + method on JniBridge from whatever thread the producer uses. */ +static jmethodID Jni_describe = NULL; + +static jint Jni_load_callbacks(JNIEnv* env) { + Jni_describe = (*env)->GetStaticMethodID(env, Jni_bridge, "describe", "(Ljava/lang/Throwable;)[B"); + return Jni_describe != NULL ? JNI_OK : JNI_ERR; +} + +/* Enters a trampoline: the JNIEnv with a local frame pushed, or NULL after + reporting the failure through `out_err`. */ +static JNIEnv* Jni_callback_begin(docs_everywhere_error* out_err) { + JNIEnv* env = Jni_env(); + if (env == NULL) { + docs_everywhere_error_set(out_err, -4, "could not attach the calling thread to the JVM"); + return NULL; + } + if ((*env)->PushLocalFrame(env, 16) != 0) { + (*env)->ExceptionClear(env); + docs_everywhere_error_set(out_err, -4, "JNI local frame exhausted"); + return NULL; + } + return env; +} + +/* Leaves a trampoline. When the implementation threw, reports its + `toString()` through `out_err` with code -4 and returns 1; nothing ever + unwinds through the producer's frames. */ +static int Jni_callback_end(JNIEnv* env, docs_everywhere_error* out_err) { + int failed = 0; + if ((*env)->ExceptionCheck(env)) { + failed = 1; + jthrowable ex = (*env)->ExceptionOccurred(env); + (*env)->ExceptionClear(env); + jbyteArray text = (jbyteArray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_describe, ex); + char* message = NULL; + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + } else if (text != NULL) { + jsize len = (*env)->GetArrayLength(env, text); + message = (char*)malloc((size_t)len + 1u); + if (message != NULL) { + (*env)->GetByteArrayRegion(env, text, 0, len, (jbyte*)message); + message[len] = '\0'; + } + } + docs_everywhere_error_set(out_err, -4, message != NULL ? message : "callback implementation threw"); + free(message); + } + (*env)->PopLocalFrame(env, NULL); + return failed; +} + +/* The vtable `free` entry: drops the producer's reference to the Kotlin + implementation. */ +static void Jni_release_callback(void* ctx) { + JNIEnv* env = Jni_env(); + if (env != NULL && ctx != NULL) { + (*env)->DeleteGlobalRef(env, (jobject)ctx); + } +} + +static jmethodID Jni_mid_docs_SavedListener_on_saved = NULL; + +static void Jni_docs_SavedListener_on_saved(void* ctx, int64_t p_doc_id, int32_t p_revision, docs_everywhere_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return; + } + (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_docs_SavedListener_on_saved, (jobject)ctx, (jlong)p_doc_id, (jint)p_revision); + Jni_callback_end(env, out_err); +} + +static const docs_everywhere_docs_SavedListener_vtable Jni_docs_SavedListener_vtable = {Jni_docs_SavedListener_on_saved, Jni_release_callback}; + +static jint Jni_load(JNIEnv* env) { + (void)env; + if (docs_everywhere_docs_checksum() != UINT64_C(0x7950eec1e9cfc819)) { + return Jni_load_error(env, "docs_everywhere: module 'docs' does not match these bindings (its contract checksum differs); regenerate the bindings or rebuild the library"); + } + if (Jni_load_async(env) != JNI_OK) { + return JNI_ERR; + } + if (Jni_load_callbacks(env) != JNI_OK) { + return JNI_ERR; + } + Jni_mid_docs_SavedListener_on_saved = (*env)->GetStaticMethodID(env, Jni_bridge, "docs_SavedListener_on_saved", "(Ldocs_everywhere/SavedListener;JI)V"); + if (Jni_mid_docs_SavedListener_on_saved == NULL) { + return JNI_ERR; + } + return JNI_OK; +} + +JNIEXPORT jlong JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1clone(JNIEnv* env, jclass cls, jlong self) { + (void)env; + (void)cls; + return (jlong)(intptr_t)docs_everywhere_docs_Notebook_clone((const docs_everywhere_docs_Notebook*)(intptr_t)self); +} + +JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1destroy(JNIEnv* env, jclass cls, jlong self) { + (void)env; + (void)cls; + docs_everywhere_docs_Notebook_destroy((docs_everywhere_docs_Notebook*)(intptr_t)self); +} + +JNIEXPORT jbyteArray JNICALL Java_docs_1everywhere_JniBridge_docs_1create_1document(JNIEnv* env, jclass cls, jbyteArray p_title, jbyteArray p_tags) { + docs_everywhere_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_title_b = Jni_borrow_bytes(env, p_title); + Jni_bytes p_tags_b = Jni_borrow_bytes(env, p_tags); + size_t out_len = 0; + const uint8_t* rv = docs_everywhere_docs_create_document(p_title_b.ptr, p_title_b.len, p_tags_b.ptr, p_tags_b.len, &out_len, &err); + Jni_release_bytes(env, &p_title_b); + Jni_release_bytes(env, &p_tags_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_docs_1everywhere_JniBridge_docs_1get_1document(JNIEnv* env, jclass cls, jlong p_id) { + docs_everywhere_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = docs_everywhere_docs_get_document((int64_t)p_id, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +static void Jni_done_docs_publish_async(void* context, docs_everywhere_error* err, bool result) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_boolean, result ? JNI_TRUE : JNI_FALSE); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_docs_1publish_1async(JNIEnv* env, jclass cls, jlong p_id, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + docs_everywhere_docs_publish_async((int64_t)p_id, Jni_done_docs_publish_async, context); +} + +JNIEXPORT void JNICALL Java_docs_1everywhere_JniBridge_docs_1watch(JNIEnv* env, jclass cls, jobject p_listener) { + docs_everywhere_error err = {0, NULL, NULL, 0}; + (void)cls; + docs_everywhere_docs_watch((void*)(*env)->NewGlobalRef(env, p_listener), &Jni_docs_SavedListener_vtable, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return; + } +} + +JNIEXPORT jlong JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1new(JNIEnv* env, jclass cls) { + docs_everywhere_error err = {0, NULL, NULL, 0}; + (void)cls; + docs_everywhere_docs_Notebook* rv = docs_everywhere_docs_Notebook_new(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jint JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1insert(JNIEnv* env, jclass cls, jlong self, jbyteArray p_document) { + docs_everywhere_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_document_b = Jni_borrow_bytes(env, p_document); + int32_t rv = docs_everywhere_docs_Notebook_insert((const docs_everywhere_docs_Notebook*)(intptr_t)self, p_document_b.ptr, p_document_b.len, &err); + Jni_release_bytes(env, &p_document_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1find(JNIEnv* env, jclass cls, jlong self, jlong p_id) { + docs_everywhere_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = docs_everywhere_docs_Notebook_find((const docs_everywhere_docs_Notebook*)(intptr_t)self, (int64_t)p_id, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jint JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1size(JNIEnv* env, jclass cls, jlong self) { + docs_everywhere_error err = {0, NULL, NULL, 0}; + (void)cls; + int32_t rv = docs_everywhere_docs_Notebook_size((const docs_everywhere_docs_Notebook*)(intptr_t)self, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jint JNICALL Java_docs_1everywhere_JniBridge_docs_1Notebook_1max_1documents(JNIEnv* env, jclass cls) { + docs_everywhere_error err = {0, NULL, NULL, 0}; + (void)cls; + int32_t rv = docs_everywhere_docs_Notebook_max_documents(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jint)rv; +} + + +// END docs_everywhere_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index 1838ee51..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,378 +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" - -/* Copies `s` as a NUL-terminated standard UTF-8 string (JNI's modified - UTF-8 would encode supplementary characters as surrogate pairs the - producer rejects). Lone surrogates become U+FFFD. Release with free(). - Returns NULL for a NULL string, for a string holding U+0000 (which no - C string can carry), or on allocation failure; the producer then - reports the null argument through its marshalling error. */ -static char* weaveffi_jni_string_to_utf8(JNIEnv* env, jstring s) { - if (s == NULL) { return NULL; } - jsize n = (*env)->GetStringLength(env, s); - const jchar* u = (*env)->GetStringChars(env, s, NULL); - if (u == NULL) { return NULL; } - char* out = (char*)malloc((size_t)n * 4u + 1u); - if (out == NULL) { (*env)->ReleaseStringChars(env, s, u); return NULL; } - size_t o = 0; - for (jsize i = 0; i < n; i++) { - uint32_t c = u[i]; - if (c == 0u) { free(out); (*env)->ReleaseStringChars(env, s, u); return NULL; } - if (c >= 0xD800u && c <= 0xDBFFu && i + 1 < n && u[i + 1] >= 0xDC00u && u[i + 1] <= 0xDFFFu) { - c = 0x10000u + ((c - 0xD800u) << 10) + ((uint32_t)u[i + 1] - 0xDC00u); - i++; - } - else if (c >= 0xD800u && c <= 0xDFFFu) { c = 0xFFFDu; } - if (c < 0x80u) { out[o++] = (char)c; } - else if (c < 0x800u) { out[o++] = (char)(0xC0u | (c >> 6)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else if (c < 0x10000u) { out[o++] = (char)(0xE0u | (c >> 12)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else { out[o++] = (char)(0xF0u | (c >> 18)); out[o++] = (char)(0x80u | ((c >> 12) & 0x3Fu)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - } - out[o] = 0; - (*env)->ReleaseStringChars(env, s, u); - return out; -} - -/* Builds a jstring from standard UTF-8 (NewStringUTF expects modified UTF-8 - and mangles supplementary characters). NULL yields the empty string and - a malformed sequence yields U+FFFD, so this never fails on producer - output; it returns NULL only when the JVM could not allocate. */ -static jstring weaveffi_jni_utf8_to_string(JNIEnv* env, const char* s) { - if (s == NULL) { s = ""; } - size_t n = strlen(s); - jchar* buf = (jchar*)malloc((n + 1u) * sizeof(jchar)); - if (buf == NULL) { return NULL; } - size_t i = 0, o = 0; - while (i < n) { - unsigned char b = (unsigned char)s[i]; - uint32_t c; size_t len; - if (b < 0x80u) { c = b; len = 1; } - else if ((b & 0xE0u) == 0xC0u) { c = b & 0x1Fu; len = 2; } - else if ((b & 0xF0u) == 0xE0u) { c = b & 0x0Fu; len = 3; } - else if ((b & 0xF8u) == 0xF0u) { c = b & 0x07u; len = 4; } - else { c = 0xFFFDu; len = 1; } - if (i + len > n) { c = 0xFFFDu; len = 1; } - for (size_t k = 1; k < len; k++) { - unsigned char cb = (unsigned char)s[i + k]; - if ((cb & 0xC0u) != 0x80u) { c = 0xFFFDu; len = k; break; } - c = (c << 6) | (cb & 0x3Fu); - } - i += len; - if (c > 0x10FFFFu) { c = 0xFFFDu; } - if (c >= 0x10000u) { c -= 0x10000u; buf[o++] = (jchar)(0xD800u + (c >> 10)); buf[o++] = (jchar)(0xDC00u + (c & 0x3FFu)); } - else { buf[o++] = (jchar)c; } - } - jstring out = (*env)->NewString(env, buf, (jsize)o); - free(buf); - return out; -} - -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 = weaveffi_jni_utf8_to_string(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_docs_DocsErrors(JNIEnv* env, weaveffi_error* err) { - jclass exClass = (*env)->FindClass(env, "com/weaveffi/DocsErrorsException"); - 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 = weaveffi_jni_utf8_to_string(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 JavaVM* weaveffi_jni_vm = NULL; -static jclass weaveffi_jni_entry_class = NULL; -static jmethodID weaveffi_jni_dispatch_exc = NULL; -static jmethodID weaveffi_jni_throwable_to_string = NULL; -static jclass weaveffi_docs_SavedListener_jni_cls = NULL; -static jmethodID weaveffi_docs_SavedListener_jni_mid_on_saved = NULL; - -static jint weaveffi_jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { (*env)->ThrowNew(env, cls, message); } - return JNI_ERR; -} - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - (void)reserved; - weaveffi_jni_vm = vm; - JNIEnv* env = NULL; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - if (weaveffi_abi_version() != 2u) { - return weaveffi_jni_load_error(env, "WeaveFFI: the native library implements a different C ABI revision than these bindings (expected 2)"); - } - jclass entry = (*env)->FindClass(env, "com/weaveffi/WeaveFFI"); - if (entry == NULL) { return JNI_ERR; } - weaveffi_jni_entry_class = (jclass)(*env)->NewGlobalRef(env, entry); - 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); } - jclass throwable = (*env)->FindClass(env, "java/lang/Throwable"); - if (throwable == NULL) { return JNI_ERR; } - weaveffi_jni_throwable_to_string = (*env)->GetMethodID(env, throwable, "toString", "()Ljava/lang/String;"); - if (weaveffi_jni_throwable_to_string == NULL) { return JNI_ERR; } - jclass weaveffi_docs_SavedListener_jni_cls_local = (*env)->FindClass(env, "com/weaveffi/SavedListenerJni"); - if (weaveffi_docs_SavedListener_jni_cls_local == NULL) { return JNI_ERR; } - weaveffi_docs_SavedListener_jni_cls = (jclass)(*env)->NewGlobalRef(env, weaveffi_docs_SavedListener_jni_cls_local); - weaveffi_docs_SavedListener_jni_mid_on_saved = (*env)->GetStaticMethodID(env, weaveffi_docs_SavedListener_jni_cls, "onSaved", "(Lcom/weaveffi/SavedListener;JI)V"); - if (weaveffi_docs_SavedListener_jni_mid_on_saved == NULL) { return JNI_ERR; } - return JNI_VERSION_1_6; -} - -/* Returns 1 when this call attached the thread (the caller must detach), - 0 otherwise. *env is NULL when no JNIEnv could be obtained. */ -static int weaveffi_jni_attach(JNIEnv** env) { - *env = NULL; - if (weaveffi_jni_vm == NULL) { return 0; } - if ((*weaveffi_jni_vm)->GetEnv(weaveffi_jni_vm, (void**)env, JNI_VERSION_1_6) == JNI_OK) { return 0; } - if ((*weaveffi_jni_vm)->AttachCurrentThread(weaveffi_jni_vm, (void*)env, NULL) != JNI_OK) { *env = NULL; return 0; } - return 1; -} - -static void weaveffi_jni_detach(int attached) { - if (attached) { (*weaveffi_jni_vm)->DetachCurrentThread(weaveffi_jni_vm); } -} - -typedef struct { - jobject callback; -} weaveffi_jni_async_ctx; - -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_jni_report_foreign(JNIEnv* env, weaveffi_error* out_err) { - jthrowable ex = (*env)->ExceptionOccurred(env); - (*env)->ExceptionClear(env); - jstring jmsg = NULL; - if (ex != NULL && weaveffi_jni_throwable_to_string != NULL) { - jmsg = (jstring)(*env)->CallObjectMethod(env, ex, weaveffi_jni_throwable_to_string); - if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); jmsg = NULL; } - } - char* msg = weaveffi_jni_string_to_utf8(env, jmsg); - weaveffi_error_set(out_err, -4, msg != NULL ? msg : "callback interface implementation threw"); - free(msg); -} - -static void weaveffi_docs_SavedListener_jni_on_saved(void* ctx, int64_t doc_id, int32_t revision, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return; - } - jlong _a0 = (jlong)doc_id; - jint _a1 = (jint)revision; - (*env)->CallStaticVoidMethod(env, weaveffi_docs_SavedListener_jni_cls, weaveffi_docs_SavedListener_jni_mid_on_saved, (jobject)ctx, _a0, _a1); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); -} - -static void weaveffi_docs_SavedListener_jni_free(void* ctx) { - if (ctx == NULL) { return; } - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env != NULL) { (*env)->DeleteGlobalRef(env, (jobject)ctx); } - weaveffi_jni_detach(attached); -} - -static const weaveffi_docs_SavedListener_vtable weaveffi_docs_SavedListener_jni_vtable = { weaveffi_docs_SavedListener_jni_on_saved, weaveffi_docs_SavedListener_jni_free }; - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_createDocumentJni(JNIEnv* env, jclass clazz, jstring title, jbyteArray tags) { - weaveffi_error err = {0, NULL, NULL, 0}; - char* title_chars = weaveffi_jni_string_to_utf8(env, title); - jbyte* tags_elems = (*env)->GetByteArrayElements(env, tags, NULL); - jsize tags_len = (*env)->GetArrayLength(env, tags); - size_t out_len = 0; - const uint8_t* rv = weaveffi_docs_create_document(title_chars, (const uint8_t*)tags_elems, (size_t)tags_len, &out_len, &err); - free(title_chars); - (*env)->ReleaseByteArrayElements(env, tags, tags_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_getDocumentJni(JNIEnv* env, jclass clazz, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_docs_get_document((int64_t)id, &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; -} - -static void weaveffi_docs_publish_async_jni_cb(void* context, weaveffi_error* err, bool result) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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 boxed_cls = (*env)->FindClass(env, "java/lang/Boolean"); - jobject boxed = (*env)->CallStaticObjectMethod(env, boxed_cls, (*env)->GetStaticMethodID(env, boxed_cls, "valueOf", "(Z)Ljava/lang/Boolean;"), result ? JNI_TRUE : JNI_FALSE); - 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); - free(ctx); - weaveffi_jni_detach(attached); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_publishAsyncAsync(JNIEnv* env, jclass clazz, jlong id, jobject callback) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); - ctx->callback = (*env)->NewGlobalRef(env, callback); - weaveffi_docs_publish_async_async((int64_t)id, weaveffi_docs_publish_async_jni_cb, ctx); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_watch(JNIEnv* env, jclass clazz, jobject listener) { - weaveffi_error err = {0, NULL, NULL, 0}; - jobject listener_ref = (*env)->NewGlobalRef(env, listener); - weaveffi_docs_watch((void*)listener_ref, &weaveffi_docs_SavedListener_jni_vtable, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - - } - return; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Notebook_nativeNew(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - weaveffi_docs_Notebook* rv = weaveffi_docs_Notebook_new(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_Notebook_nativeMaxDocuments(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_docs_Notebook_max_documents(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_Notebook_nativeInsert(JNIEnv* env, jclass clazz, jlong selfHandle, jbyteArray document) { - weaveffi_error err = {0, NULL, NULL, 0}; - jbyte* document_elems = (*env)->GetByteArrayElements(env, document, NULL); - jsize document_len = (*env)->GetArrayLength(env, document); - int32_t rv = weaveffi_docs_Notebook_insert((const weaveffi_docs_Notebook*)(intptr_t)selfHandle, (const uint8_t*)document_elems, (size_t)document_len, &err); - (*env)->ReleaseByteArrayElements(env, document, document_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_Notebook_nativeFind(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_docs_Notebook_find((const weaveffi_docs_Notebook*)(intptr_t)selfHandle, (int64_t)id, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_docs_DocsErrors(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 jint JNICALL Java_com_weaveffi_Notebook_nativeSize(JNIEnv* env, jclass clazz, jlong selfHandle) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_docs_Notebook_size((const weaveffi_docs_Notebook*)(intptr_t)selfHandle, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Notebook_nativeClone(JNIEnv* env, jclass clazz, jlong handle) { - return (jlong)(intptr_t)weaveffi_docs_Notebook_clone((const weaveffi_docs_Notebook*)(intptr_t)handle); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_Notebook_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_docs_Notebook_destroy((weaveffi_docs_Notebook*)(intptr_t)handle); -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index da3c4bf1..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,415 +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 { WeaveNativeLibrary.ensureLoaded() } - - @Volatile private var callbackExceptionHandler: ((Throwable) -> Unit)? = null - - /** - * Installs a handler for exceptions thrown while an async result is - * delivered on a native producer thread. 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. Exceptions thrown by callback interface methods are - * not routed here: they abort the producer call that invoked them and - * surface to the original caller as a [WeaveFFIException] with code -4. - */ - @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 - } - - /** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - * @param title Human-readable title for the new document - * @param tags Initial set of tags (may be empty) - */ - @JvmStatic private external fun createDocumentJni(title: String, tags: ByteArray): ByteArray - @JvmStatic fun createDocument(title: String, tags: List): Document = weaveDecode(createDocumentJni(title, weaveEncode { w -> w.writeList(tags) { v0 -> w.writeString(v0) } })) { r -> unpackDocument(r) } - /** - * Look up a document by identifier - * @param id Identifier returned from create_document - */ - @JvmStatic private external fun getDocumentJni(id: Long): ByteArray - @JvmStatic fun getDocument(id: Long): Document? = weaveDecode(getDocumentJni(id)) { r -> r.readOptional { unpackDocument(r) } } - /** - * Publish a document in the background - * @param id Identifier of the document to publish - */ - @JvmStatic private external fun publishAsyncAsync(id: Long, callback: Any) - @JvmStatic suspend fun publishAsync(id: Long): Boolean = suspendCancellableCoroutine { cont -> - publishAsyncAsync(id, WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) - } - /** - * Register a listener for saved notifications - * @param listener Receiver of saved notifications - */ - @JvmStatic external fun watch(listener: SavedListener): Unit - } -} - -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -enum class Status(val value: Int) { - /** Document is being edited */ - Draft(0), - /** Document is publicly visible */ - Published(1), - /** Document is hidden from listings */ - Archived(2); - - companion object { - fun fromValue(value: Int): Status = entries.first { it.value == value } - } -} - -/** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ -data class Document( - /** Stable opaque identifier */ - val id: Long, - /** Human-readable title shown in lists */ - val title: String, - /** Current lifecycle status */ - val status: Status, - /** Free-form tags for grouping */ - val tags: List, -) - -internal fun packDocument(w: WeaveBufferWriter, v: Document) { - w.writeI64(v.id) - w.writeString(v.title) - w.writeI32(v.status.value) - w.writeList(v.tags) { v0 -> w.writeString(v0) } -} - -internal fun unpackDocument(r: WeaveBufferReader): Document = Document( - r.readI64(), - r.readString(), - Status.fromValue(r.readI32()), - r.readList { r.readString() }, -) - -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - */ -interface SavedListener { - /** - * Fires after a document has been persisted successfully - * @param docId Identifier of the saved document - * @param revision Monotonically increasing revision - */ - fun onSaved(docId: Long, revision: Int) -} - -/** JNI dispatch shims for [SavedListener]; the native vtable trampolines call these, nothing else does. */ -internal object SavedListenerJni { - @JvmStatic fun onSaved(impl: SavedListener, docId: Long, revision: Int) { impl.onSaved(docId, revision) } -} - -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ -class Notebook private constructor(handle: Long) : AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - - /** The native pointer, borrowed for one call; throws after [close]. */ - internal val handle: Long get() = ref.get() - - /** A new strong reference, written into value buffers as an object token. */ - internal fun cloneHandle(): Long = nativeClone(ref.get()) - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - /** Adopts one strong native reference (a constructor return, a `_clone`, or a buffer token) into a wrapper that owes its `_destroy`. */ - internal fun fromHandle(handle: Long): Notebook = Notebook(handle) - @JvmStatic private external fun nativeNew(): Long - @JvmStatic private external fun nativeMaxDocuments(): Int - @JvmStatic private external fun nativeInsert(selfHandle: Long, document: ByteArray): Int - @JvmStatic private external fun nativeFind(selfHandle: Long, id: Long): ByteArray - @JvmStatic private external fun nativeSize(selfHandle: Long): Int - @JvmStatic private external fun nativeClone(handle: Long): Long - @JvmStatic private external fun nativeDestroy(handle: Long) - - /** - * Create an empty notebook - */ - operator fun invoke(): Notebook = Notebook.fromHandle(nativeNew()) - - /** - * The largest number of documents one notebook will hold - */ - fun maxDocuments(): Int = nativeMaxDocuments() - } - - /** - * Add a document, returning its position in the notebook - * @param document The document to add - */ - fun insert(document: Document): Int = nativeInsert(handle, weaveEncode { w -> packDocument(w, document) }) - - /** - * Look up a document by identifier, failing when none matches - * @param id Identifier of the document to find - */ - fun find(id: Long): Document = weaveDecode(nativeFind(handle, id)) { r -> unpackDocument(r) } - - /** - * The number of documents currently held - */ - fun size(): Int = nativeSize(handle) - - /** Releases this wrapper's native reference; safe to call more than once. */ - override fun close() { - cleanable.clean() - } -} - -/** Generic WeaveFFI failure: panics, marshalling errors, callback implementation failures (code -4), and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Typed error domain `DocsErrors` declared by module `docs`. */ -sealed class DocsErrorsException(code: Int, message: String) : WeaveFFIException(code, message) { - /** Returned when no document matches the requested id */ - class NotFound(message: String = "Document not found") : DocsErrorsException(1, message) - /** Raised when attempting to move into a forbidden state */ - class InvalidStatus(message: String = "Invalid status transition") : DocsErrorsException(2, message) - - companion object { - /** Map a raw `DocsErrors` 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 -> NotFound(message) - 2 -> InvalidStatus(message) - else -> WeaveFFIException(code, message) - } - } -} - -/** Loads the native library once; every companion `init` calls [ensureLoaded]. */ -internal object WeaveNativeLibrary { - init { System.loadLibrary("weaveffi") } - - fun ensureLoaded() {} -} - -/** - * One strong native reference, released exactly once. Wrappers register an - * instance with [weaveCleaner] so an unreachable, never-closed wrapper still - * releases its reference; an explicit `close()` runs the same action first - * and the cleaner then does nothing. - */ -internal class WeaveNativeRef(handle: Long, private val release: (Long) -> Unit) : Runnable { - private val handle = java.util.concurrent.atomic.AtomicLong(handle) - - /** The live pointer, borrowed for the duration of one native call. */ - fun get(): Long { - val h = handle.get() - check(h != 0L) { "WeaveFFI object used after close()" } - return h - } - - /** The live pointer, or `0L` once released. */ - fun peek(): Long = handle.get() - - override fun run() { - val h = handle.getAndSet(0L) - if (h != 0L) release(h) - } -} - -/** The process-wide cleaner backing every generated wrapper's disposal backstop. */ -internal val weaveCleaner: java.lang.ref.Cleaner = java.lang.ref.Cleaner.create() - -/** 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()) - /** An object token: one strong reference the caller adopts. Zero is never valid here. */ - fun readObject(): Long { - val token = readI64() - if (token == 0L) malformed("null object token") - return token - } - 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/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Async_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Async_kt.snap new file mode 100644 index 00000000..dac0ddce --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Async_kt.snap @@ -0,0 +1,124 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package docs_everywhere + +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Bridges one native async call to a suspended coroutine. The JNI shim pins + * this object with a global reference and calls exactly one `on*` method, + * from whichever producer thread completes the call. + */ +internal class NativeCompletion( + private val cont: CancellableContinuation, + private val domain: Int, + private val convert: (Any?) -> T, + cancellable: Boolean, +) { + // The native cancel token, or 0 when the call isn't cancellable or has + // completed. Guarded by `this` so a late cancel never touches a freed + // token; a completion that lands inside `cancel` (on the same thread) + // defers the release until the cancel returns. + private var token: Long = if (cancellable) JniBridge.cancel_token_create() else 0L + private var cancelling = false + private var released = false + + val cancelToken: Long + get() = synchronized(this) { token } + + fun cancel() { + synchronized(this) { + if (token == 0L) return + cancelling = true + try { + JniBridge.cancel_token_cancel(token) + } finally { + cancelling = false + } + if (released) destroyToken() + } + } + + fun releaseToken() { + synchronized(this) { + if (cancelling) released = true else destroyToken() + } + } + + private fun destroyToken() { + if (token != 0L) JniBridge.cancel_token_destroy(token) + token = 0L + } + + fun onUnit() = succeed(Unit) + + fun onBoolean(value: Boolean) = succeed(value) + + fun onByte(value: Byte) = succeed(value) + + fun onShort(value: Short) = succeed(value) + + fun onInt(value: Int) = succeed(value) + + fun onLong(value: Long) = succeed(value) + + fun onFloat(value: Float) = succeed(value) + + fun onDouble(value: Double) = succeed(value) + + fun onBytes(value: ByteArray) = succeed(value) + + fun onError(code: Int, message: ByteArray, payload: ByteArray?) { + releaseToken() + val error = if (code == -5) { + CancellationException(decodeUtf8(message)) + } else { + JniBridge.error(domain, code, message, payload) + } + cont.resumeWithException(error) + } + + private fun succeed(raw: Any?) { + releaseToken() + val result = try { + convert(raw) + } catch (t: Throwable) { + cont.resumeWithException(t) + return + } + cont.resume(result) + } +} + +/** + * Starts a native async call and suspends until it completes. [launch] gets + * the cancel token (0 unless [cancellable]) and the completion to hand to the + * shim. Cancelling the coroutine cancels the native call; a cancelled call + * (code -5) resumes with [CancellationException]. + */ +internal suspend fun awaitNative( + cancellable: Boolean, + domain: Int, + convert: (Any?) -> T, + launch: (Long, NativeCompletion) -> Unit, +): T = suspendCancellableCoroutine { cont -> + val completion = NativeCompletion(cont, domain, convert, cancellable) + cont.invokeOnCancellation { completion.cancel() } + try { + launch(completion.cancelToken, completion) + } catch (t: Throwable) { + completion.releaseToken() + throw t + } +} + +// END Async.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Buffers_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Buffers_kt.snap new file mode 100644 index 00000000..2abe38cb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Buffers_kt.snap @@ -0,0 +1,255 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package docs_everywhere + +/** + * Growable little-endian writer for the value-buffer wire format that + * records, rich enums, optionals, lists, and maps cross the ABI in. + */ +internal class BufferWriter { + private var buf = ByteArray(64) + private var len = 0 + private var minted: ArrayList Unit>>? = null + + private fun reserve(extra: Int) { + if (len + extra <= buf.size) return + var cap = buf.size * 2 + while (cap < len + extra) cap *= 2 + buf = buf.copyOf(cap) + } + + fun toByteArray(): ByteArray = buf.copyOf(len) + + fun writeBool(v: Boolean) { + reserve(1) + buf[len++] = if (v) 1 else 0 + } + + fun writeI8(v: Byte) { + reserve(1) + buf[len++] = v + } + + fun writeU8(v: Byte) = writeI8(v) + + fun writeI16(v: Short) { + reserve(2) + val b = v.toInt() + buf[len++] = b.toByte() + buf[len++] = (b shr 8).toByte() + } + + fun writeU16(v: Short) = writeI16(v) + + fun writeI32(v: Int) { + reserve(4) + buf[len++] = v.toByte() + buf[len++] = (v shr 8).toByte() + buf[len++] = (v shr 16).toByte() + buf[len++] = (v shr 24).toByte() + } + + fun writeU32(v: Long) = writeI32(v.toInt()) + + fun writeI64(v: Long) { + writeI32(v.toInt()) + writeI32((v ushr 32).toInt()) + } + + fun writeU64(v: Long) = writeI64(v) + + fun writeF32(v: Float) = writeI32(v.toRawBits()) + + fun writeF64(v: Double) = writeI64(v.toRawBits()) + + fun writeString(v: String) = writeBytes(encodeUtf8(v)) + + fun writeBytes(v: ByteArray) { + writeI32(v.size) + reserve(v.size) + v.copyInto(buf, len) + len += v.size + } + + /** + * An object token: one new strong reference the reader adopts, which + * [abandon] releases with [release] if the encoding fails. + */ + fun writeObject(token: Long, release: (Long) -> Unit) { + val list = minted ?: ArrayList Unit>>().also { minted = it } + list.add(token to release) + writeI64(token) + } + + /** Releases every object token written so far, for an encoding that failed midway. */ + fun abandon() { + minted?.forEach { (token, release) -> release(token) } + minted = null + } + + inline fun writeOptional(v: T?, write: (T) -> Unit) { + if (v == null) { + writeBool(false) + } else { + writeBool(true) + write(v) + } + } + + inline fun writeList(v: List, write: (T) -> Unit) { + writeI32(v.size) + for (e in v) write(e) + } + + inline fun writeMap(v: Map, writeKey: (K) -> Unit, writeValue: (V) -> Unit) { + writeI32(v.size) + for ((k, e) in v) { + writeKey(k) + writeValue(e) + } + } +} + +/** + * Validating little-endian reader for the value-buffer wire format. A + * truncated buffer, an invalid flag byte, a zero object token, an oversized + * length, or trailing bytes raise [FfiException] code -3. + */ +internal class BufferReader(private val buf: ByteArray) { + private var pos = 0 + + private fun malformed(detail: String): Nothing = + throw FfiException(-3, "malformed value buffer: $detail") + + private fun take(n: Int): Int { + if (n > buf.size - pos) malformed("truncated buffer") + val at = pos + pos += n + return at + } + + fun readBool(): Boolean = when (buf[take(1)].toInt()) { + 0 -> false + 1 -> true + else -> malformed("invalid bool byte") + } + + fun readI8(): Byte = buf[take(1)] + + fun readU8(): Byte = readI8() + + fun readI16(): Short { + val at = take(2) + return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() + } + + fun readU16(): Short = readI16() + + 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 readU64(): Long = readI64() + + fun readF32(): Float = Float.fromBits(readI32()) + + fun readF64(): Double = Double.fromBits(readI64()) + + private fun readLen(): Int { + val n = readI32() + if (n < 0 || n > buf.size - pos) malformed("length prefix exceeds remaining bytes") + return n + } + + fun readString(): String { + val n = readLen() + val at = take(n) + return try { + Charsets.UTF_8.newDecoder().decode(java.nio.ByteBuffer.wrap(buf, at, n)).toString() + } catch (e: java.nio.charset.CharacterCodingException) { + malformed("string is not valid UTF-8") + } + } + + fun readBytes(): ByteArray { + val n = readLen() + val at = take(n) + return buf.copyOfRange(at, at + n) + } + + /** An object token: one strong reference the caller adopts. */ + fun readObject(): Long { + val token = readI64() + if (token == 0L) malformed("null object token") + return token + } + + inline fun readOptional(read: () -> T): T? = if (readBool()) read() else null + + inline fun readList(read: () -> T): List { + val n = readCount() + val out = ArrayList(minOf(n, 4096)) + repeat(n) { out.add(read()) } + return out + } + + inline fun readMap(readKey: () -> K, readValue: () -> V): Map { + val n = readCount() + val out = LinkedHashMap(minOf(n, 4096)) + repeat(n) { + val k = readKey() + out[k] = readValue() + } + return out + } + + /** An element count; elements may encode to zero bytes, so only the sign is checked. */ + fun readCount(): Int { + val n = readI32() + if (n < 0) malformed("negative element count") + return n + } + + fun expectEnd() { + if (pos != buf.size) malformed("trailing bytes after value") + } +} + +/** Encodes one value into a fresh value buffer. */ +internal inline fun encodeBuffer(write: (BufferWriter) -> Unit): ByteArray { + val w = BufferWriter() + try { + write(w) + } catch (t: Throwable) { + w.abandon() + throw t + } + return w.toByteArray() +} + +/** Decodes one value from [bytes], rejecting malformed or trailing bytes. */ +internal inline fun decodeBuffer(bytes: ByteArray, read: (BufferReader) -> T): T { + val r = BufferReader(bytes) + val v = read(r) + r.expectEnd() + return v +} + +// END Buffers.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Docs_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Docs_kt.snap new file mode 100644 index 00000000..c6802312 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Docs_kt.snap @@ -0,0 +1,175 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package docs_everywhere + +/** Errors of the `DocsErrors` domain. */ +sealed class DocsErrorsException(code: Int, message: String) : FfiException(code, message) { + /** Returned when no document matches the requested id */ + class NotFound(message: String = "Document not found") : DocsErrorsException(1, message) + /** Raised when attempting to move into a forbidden state */ + class InvalidStatus(message: String = "Invalid status transition") : DocsErrorsException(2, message) + + internal companion object { + /** The exception for a raw code and its serialized payload. */ + fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) { + 1 -> NotFound(message) + 2 -> InvalidStatus(message) + else -> FfiException(code, message) + } + } +} + +/** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ +enum class Status(val value: Int) { + /** Document is being edited */ + Draft(0), + /** Document is publicly visible */ + Published(1), + /** Document is hidden from listings */ + Archived(2); + + companion object { + /** The variant whose ABI value is [value]. */ + fun fromValue(value: Int): Status = entries.firstOrNull { it.value == value } + ?: throw FfiException(-3, "unknown Status value $value") + } +} + +/** + * Represents a single document tracked by the system. + * + * Documents are persisted to disk and exposed via the public API. + */ +data class Document( + /** Stable opaque identifier */ + val id: Long, + /** Human-readable title shown in lists */ + val title: String, + /** Current lifecycle status */ + val status: Status, + /** Free-form tags for grouping */ + val tags: List, +) + +internal fun packDocument(_w: BufferWriter, _v: Document) { + _w.writeI64(_v.id) + _w.writeString(_v.title) + _w.writeI32(_v.status.value) + _w.writeList(_v.tags) { _v0 -> _w.writeString(_v0) } +} + +internal fun unpackDocument(_r: BufferReader): Document = Document( + _r.readI64(), + _r.readString(), + Status.fromValue(_r.readI32()), + _r.readList { _r.readString() }, +) + +/** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + */ +interface SavedListener { + /** + * Fires after a document has been persisted successfully + * @param docId Identifier of the saved document + * @param revision Monotonically increasing revision + */ + fun onSaved(docId: Long, revision: Int) +} + +/** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ +class Notebook private constructor(address: Long) : AutoCloseable { + internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::docs_Notebook_destroy)) + + /** A new strong reference to this object, as written into a value buffer. */ + internal fun cloneHandle(): Long = handle.borrow { JniBridge.docs_Notebook_clone(it) } + + /** + * Add a document, returning its position in the notebook + * @param document The document to add + */ + fun insert(document: Document): Int = handle.borrow { _self -> + JniBridge.docs_Notebook_insert(_self, encodeBuffer { _w -> packDocument(_w, document) }) + } + + /** + * Look up a document by identifier, failing when none matches + * @param id Identifier of the document to find + */ + fun find(id: Long): Document = handle.borrow { _self -> + decodeBuffer(JniBridge.docs_Notebook_find(_self, id)) { _r -> unpackDocument(_r) } + } + + /** The number of documents currently held */ + fun size(): Int = handle.borrow { _self -> + JniBridge.docs_Notebook_size(_self) + } + + /** Releases this wrapper's native reference; safe to call more than once. */ + override fun close() = handle.close() + + companion object { + /** Adopts one strong reference (a return value or a buffer token). */ + internal fun fromHandle(address: Long): Notebook = Notebook(address) + + /** [fromHandle], with address 0 meaning no object. */ + internal fun fromHandleOrNull(address: Long): Notebook? = if (address == 0L) null else Notebook(address) + + /** Create an empty notebook */ + operator fun invoke(): Notebook = Notebook.fromHandle(JniBridge.docs_Notebook_new()) + + /** The largest number of documents one notebook will hold */ + fun maxDocuments(): Int = JniBridge.docs_Notebook_max_documents() + } +} + +object Docs { + /** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + * @param title Human-readable title for the new document + * @param tags Initial set of tags (may be empty) + */ + fun createDocument(title: String, tags: List): Document = decodeBuffer(JniBridge.docs_create_document(encodeUtf8(title), encodeBuffer { _w -> _w.writeList(tags) { _v0 -> _w.writeString(_v0) } })) { _r -> unpackDocument(_r) } + + /** + * Look up a document by identifier + * @param id Identifier returned from create_document + */ + fun getDocument(id: Long): Document? = decodeBuffer(JniBridge.docs_get_document(id)) { _r -> _r.readOptional { unpackDocument(_r) } } + + /** + * Publish a document in the background + * @param id Identifier of the document to publish + */ + suspend fun publishAsync(id: Long): Boolean = awaitNative(false, 0, { it as Boolean }) { _, _done -> + JniBridge.docs_publish_async(id, _done) + } + + /** + * Register a listener for saved notifications + * @param listener Receiver of saved notifications + */ + fun watch(listener: SavedListener) { + JniBridge.docs_watch(listener) + } +} + +// END Docs.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_JniBridge_kt.snap new file mode 100644 index 00000000..99527dfc --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_JniBridge_kt.snap @@ -0,0 +1,55 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package docs_everywhere + +/** + * The JNI entry points of `libdocs_everywhere_jni`: one native per C symbol, named after + * it, plus the hooks the shim calls back into. Internal plumbing for the + * public wrappers. + */ +internal object JniBridge { + init { + NativeLibrary.ensureLoaded() + } + + /** The exception for a native failure; domain 0 is the generic one. */ + @JvmStatic + @Suppress("UNUSED_PARAMETER") + fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): FfiException { + val text = decodeUtf8(message) + return when (domain) { + 1 -> DocsErrorsException.fromCode(code, text, payload) + else -> FfiException(code, text) + } + } + + /** The message a failed callback reports to the producer (code -4). */ + @JvmStatic + fun describe(error: Throwable): ByteArray = encodeUtf8(error.toString()) + + @JvmStatic external fun debug_live(kind: Int): Long + @JvmStatic external fun cancel_token_create(): Long + @JvmStatic external fun cancel_token_cancel(token: Long) + @JvmStatic external fun cancel_token_destroy(token: Long) + @JvmStatic external fun docs_Notebook_clone(_self: Long): Long + @JvmStatic external fun docs_Notebook_destroy(_self: Long) + @JvmStatic external fun docs_create_document(title: ByteArray, tags: ByteArray): ByteArray + @JvmStatic external fun docs_get_document(id: Long): ByteArray + @JvmStatic external fun docs_publish_async(id: Long, _done: NativeCompletion<*>) + @JvmStatic external fun docs_watch(listener: SavedListener) + @JvmStatic external fun docs_Notebook_new(): Long + @JvmStatic external fun docs_Notebook_insert(_self: Long, document: ByteArray): Int + @JvmStatic external fun docs_Notebook_find(_self: Long, id: Long): ByteArray + @JvmStatic external fun docs_Notebook_size(_self: Long): Int + @JvmStatic external fun docs_Notebook_max_documents(): Int + + @JvmStatic + fun docs_SavedListener_on_saved(_impl: SavedListener, docId: Long, revision: Int) { _impl.onSaved(docId, revision) } +} + +// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Runtime_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Runtime_kt.snap new file mode 100644 index 00000000..722c2842 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_docs_everywhere__src_main_kotlin_docs_everywhere_Runtime_kt.snap @@ -0,0 +1,212 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package docs_everywhere + +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * A failure reported by the native library: a producer panic (code -2), a + * marshalling failure (-3), a callback implementation that threw (-4), or any + * code these bindings don't know. Typed error domains subclass it. + */ +open class FfiException(val code: Int, message: String) : Exception(message) + +/** + * A lazy sequence streamed from the native library, one element per step. + * The native iterator is released when the sequence is exhausted, when + * [close] is called, or once this object becomes unreachable, whichever + * comes first. + */ +class NativeIterator internal constructor( + address: Long, + release: (Long) -> Unit, + private val step: (Long) -> Any?, +) : Iterator, AutoCloseable { + private val handle = NativeCleaner.register(this, NativeHandle(address, release)) + private var lookahead: Any? = EMPTY + + override fun hasNext(): Boolean { + if (lookahead !== EMPTY) return true + if (handle.isClosed) return false + val item = handle.borrow(step) + if (item === DONE) { + close() + return false + } + lookahead = item + return true + } + + override fun next(): T { + if (!hasNext()) throw NoSuchElementException() + val item = lookahead + lookahead = EMPTY + @Suppress("UNCHECKED_CAST") + return item as T + } + + /** Releases the native iterator; safe to call more than once. */ + override fun close() = handle.close() + + internal companion object { + /** What a step function returns once the native sequence is exhausted. */ + val DONE = Any() + private val EMPTY = Any() + } +} + +/** Loads the producer library and then the JNI shim, exactly once. */ +internal object NativeLibrary { + private const val LIBRARY = "docs_everywhere" + private const val JNI_LIBRARY = "docs_everywhere_jni" + + init { + val explicit = System.getenv("DOCS_EVERYWHERE_LIBRARY") + if (!explicit.isNullOrEmpty()) { + System.load(explicit) + } else if (!loadBundled(LIBRARY)) { + // The shim's own link dependency usually finds the producer when + // it isn't on java.library.path, so a miss here is only fatal if + // loading the shim fails too. + try { + System.loadLibrary(LIBRARY) + } catch (ignored: UnsatisfiedLinkError) { + } + } + if (!loadBundled(JNI_LIBRARY)) System.loadLibrary(JNI_LIBRARY) + } + + fun ensureLoaded() {} + + /** + * Loads `base` from the `natives/-/` classpath resources a + * packaged desktop build bundles; false when it isn't bundled. + */ + private fun loadBundled(base: String): Boolean { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + val osId = when { + os.contains("mac") || os.contains("darwin") -> "darwin" + os.contains("win") -> "windows" + else -> "linux" + } + val archId = if (arch == "aarch64" || arch == "arm64") "arm64" else "x64" + val file = when (osId) { + "darwin" -> "lib$base.dylib" + "windows" -> "$base.dll" + else -> "lib$base.so" + } + val stream = NativeLibrary::class.java.getResourceAsStream("/natives/$osId-$archId/$file") ?: return false + val dir = java.nio.file.Files.createTempDirectory("$base-natives") + val target = dir.resolve(file) + stream.use { java.nio.file.Files.copy(it, target) } + target.toFile().deleteOnExit() + dir.toFile().deleteOnExit() + System.load(target.toAbsolutePath().toString()) + return true + } +} + +/** + * One strong reference to a native object, released exactly once: by + * [close], or by [NativeCleaner] once the owning wrapper is unreachable. + * Every call borrows the address for its duration, and a close that races an + * in-flight call defers the release until the last borrow ends, so the object + * is never freed while native code is using it. + */ +internal class NativeHandle(val address: Long, private val release: (Long) -> Unit) { + // Bit 0 is the closed flag; the remaining bits count in-flight borrows. + private val state = AtomicLong(0L) + + val isClosed: Boolean get() = state.get() and 1L != 0L + + fun acquire(): Long { + while (true) { + val s = state.get() + check(s and 1L == 0L) { "native object used after close()" } + if (state.compareAndSet(s, s + 2L)) return address + } + } + + fun unacquire() { + if (state.addAndGet(-2L) == 1L) release(address) + } + + fun close() { + while (true) { + val s = state.get() + if (s and 1L != 0L) return + if (state.compareAndSet(s, s or 1L)) { + if (s == 0L) release(address) + return + } + } + } +} + +/** Runs [block] with the native address borrowed for its duration. */ +internal inline fun NativeHandle.borrow(block: (Long) -> R): R { + val address = acquire() + try { + return block(address) + } finally { + unacquire() + } +} + +/** [borrow] for an optional object: a missing object lends address 0. */ +internal inline fun NativeHandle?.borrowOrNull(block: (Long) -> R): R = + if (this == null) block(0L) else borrow(block) + +/** + * Releases the native reference of every wrapper that becomes unreachable + * without being closed. A daemon thread drains a phantom-reference queue, so + * this works on every Android API level and every JVM. + */ +internal object NativeCleaner { + private val queue = ReferenceQueue() + private val pending: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) + + private class Entry(owner: Any, val handle: NativeHandle, queue: ReferenceQueue) : + PhantomReference(owner, queue) + + init { + val thread = Thread({ drain() }, "docs_everywhere-cleaner") + thread.isDaemon = true + thread.start() + } + + /** Releases [handle] once [owner] is unreachable; returns [handle]. */ + fun register(owner: Any, handle: NativeHandle): NativeHandle { + pending.add(Entry(owner, handle, queue)) + return handle + } + + private fun drain() { + while (true) { + val entry = queue.remove() as Entry + pending.remove(entry) + try { + entry.handle.close() + } catch (_: Throwable) { + } + } + } +} + +/** UTF-8 bytes of [value], for a string crossing into native code. */ +internal fun encodeUtf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8) + +/** The string a native UTF-8 byte sequence spells. */ +internal fun decodeUtf8(bytes: ByteArray): String = String(bytes, Charsets.UTF_8) + +// END Runtime.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__build_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__build_gradle_kts.snap index 374b107b..fecef99e 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__build_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__build_gradle_kts.snap @@ -1,36 +1,63 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") version "1.9.22" + id("com.android.library") version "8.7.3" + id("org.jetbrains.kotlin.android") version "2.0.21" + `maven-publish` } +group = "edge_cases" +version = "0.1.0" + android { - namespace = "com.weaveffi" - compileSdk = 34 + namespace = "edge_cases" + compileSdk = 35 defaultConfig { - // java.lang.ref.Cleaner, which backs every wrapper's disposal, is API 33. - minSdk = 33 - externalNativeBuild { - cmake { - cppFlags("") - } - } + minSdk = 21 + consumerProguardFiles("consumer-rules.pro") } + // Builds libedge_cases_jni.so per ABI, linked against the producer library in + // src/main/jniLibs// (see src/main/cpp/CMakeLists.txt). externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + } } dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") +} + +publishing { + publications { + register("release") { + artifactId = "edge_cases" + afterEvaluate { + from(components["release"]) + } + } + } } // END build.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__consumer_rules_pro.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__consumer_rules_pro.snap new file mode 100644 index 00000000..f9bfb9c5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__consumer_rules_pro.snap @@ -0,0 +1,11 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# The JNI shim looks classes and methods up by name. +-keep class edge_cases.** { *; } + +# END consumer-rules.pro diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__settings_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__settings_gradle_kts.snap index 69449d7c..5ae1f746 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__settings_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__settings_gradle_kts.snap @@ -1,10 +1,25 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -rootProject.name = "weaveffi" +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "edge_cases" // END settings.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_CMakeLists_txt.snap index 32fa1b44..5bb79e2a 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_CMakeLists_txt.snap @@ -1,13 +1,34 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -cmake_minimum_required(VERSION 3.22) -project(weaveffi_jni C) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) +cmake_minimum_required(VERSION 3.18) +project(edge_cases_jni C) + +# The directory holding the producer library (libedge_cases). Android builds +# default to the per-ABI copy under src/main/jniLibs/. +set(EDGE_CASES_LIBRARY_DIR "" CACHE PATH "Directory holding the edge_cases producer library") +if(NOT EDGE_CASES_LIBRARY_DIR AND ANDROID) + set(EDGE_CASES_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}") +endif() +if(EDGE_CASES_LIBRARY_DIR) + find_library(EDGE_CASES_PRODUCER NAMES edge_cases PATHS "${EDGE_CASES_LIBRARY_DIR}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED) +else() + set(CMAKE_FIND_FRAMEWORK NEVER) + find_library(EDGE_CASES_PRODUCER NAMES edge_cases REQUIRED) +endif() + +add_library(edge_cases_jni SHARED edge_cases_jni.c) +target_include_directories(edge_cases_jni PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(edge_cases_jni PRIVATE "${EDGE_CASES_PRODUCER}") +if(NOT ANDROID) + find_package(JNI REQUIRED) + find_package(Threads REQUIRED) + target_include_directories(edge_cases_jni PRIVATE ${JNI_INCLUDE_DIRS}) + target_link_libraries(edge_cases_jni PRIVATE Threads::Threads) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_h.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_h.snap new file mode 100644 index 00000000..94864086 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_h.snap @@ -0,0 +1,245 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef EDGE_CASES_H +#define EDGE_CASES_H + +#include +#include +#include + +#ifndef EDGE_CASES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef EDGE_CASES_BUILD +# define EDGE_CASES_API __declspec(dllexport) +# else +# define EDGE_CASES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define EDGE_CASES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define EDGE_CASES_API __attribute__((visibility("default"))) +# else +# define EDGE_CASES_API +# endif +#endif + +#ifndef EDGE_CASES_DEPRECATED +# if defined(_MSC_VER) +# define EDGE_CASES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define EDGE_CASES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define EDGE_CASES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports edge_cases_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define EDGE_CASES_ABI_VERSION 3u +EDGE_CASES_API uint32_t edge_cases_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by edge_cases_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct edge_cases_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} edge_cases_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +EDGE_CASES_API void edge_cases_error_set(edge_cases_error* err, int32_t code, const char* message); +EDGE_CASES_API void edge_cases_error_clear(edge_cases_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + edge_cases_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +EDGE_CASES_API void edge_cases_error_free(edge_cases_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +EDGE_CASES_API void edge_cases_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct edge_cases_cancel_token edge_cases_cancel_token; +EDGE_CASES_API edge_cases_cancel_token* edge_cases_cancel_token_create(void); +EDGE_CASES_API void edge_cases_cancel_token_cancel(edge_cases_cancel_token* token); +EDGE_CASES_API bool edge_cases_cancel_token_is_cancelled(const edge_cases_cancel_token* token); +EDGE_CASES_API void edge_cases_cancel_token_destroy(edge_cases_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +EDGE_CASES_API uint8_t* edge_cases_alloc(uint32_t size); +EDGE_CASES_API void edge_cases_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +EDGE_CASES_API uint64_t edge_cases_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define EDGE_CASES_EDGE_CASES_CHECKSUM 0x1567f441073bd4aaull +EDGE_CASES_API uint64_t edge_cases_edge_cases_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with edge_cases_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * edge_cases_buffer.h declares a C struct and codec for each. + */ + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +typedef enum { edge_cases_edge_cases_Legacy_None = 0, edge_cases_edge_cases_Legacy_Default = 5, edge_cases_edge_cases_Legacy_Class = 10 } edge_cases_edge_cases_Legacy; +typedef enum { edge_cases_edge_cases_Mode_Fast = 1, edge_cases_edge_cases_Mode_Safe = 2 } edge_cases_edge_cases_Mode; +/** Error codes reported by throwing functions in the `edge_cases` module tree. */ +typedef enum { edge_cases_edge_cases_EdgeErrors_Busy = 7 } edge_cases_edge_cases_EdgeErrors; +typedef struct edge_cases_edge_cases_Widget edge_cases_edge_cases_Widget; +typedef struct edge_cases_edge_cases_Vault edge_cases_edge_cases_Vault; +typedef struct edge_cases_edge_cases_WidgetStreamIterator edge_cases_edge_cases_WidgetStreamIterator; +typedef struct edge_cases_edge_cases_StringStreamIterator edge_cases_edge_cases_StringStreamIterator; +typedef struct edge_cases_edge_cases_ScalarStreamIterator edge_cases_edge_cases_ScalarStreamIterator; +typedef struct edge_cases_edge_cases_ModeStreamIterator edge_cases_edge_cases_ModeStreamIterator; +/** + * Callback interface carrying composite payloads and objects + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_BatchListener_vtable { + /** Composite payloads cross as value buffers */ + void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); + /** Objects cross as strong references the consumer must release */ + void (*on_widget)(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); + void (*on_tick)(void* ctx, edge_cases_error* out_err); + /** Scalar return travels back through the vtable */ + bool (*should_continue)(void* ctx, int32_t class_, edge_cases_error* out_err); + /** Enum return travels back as its integer discriminant */ + edge_cases_edge_cases_Mode (*pick_mode)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_BatchListener_vtable; +/** + * Callback interface whose name and methods are keywords + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_Keyword_vtable { + void (*delete_)(void* ctx, int32_t self, edge_cases_error* out_err); + int32_t (*import)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_Keyword_vtable; +typedef void (*edge_cases_edge_cases_async_widget_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result); +typedef void (*edge_cases_edge_cases_async_void_callback)(void* context, edge_cases_error* err); +typedef void (*edge_cases_edge_cases_async_bytes_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_struct_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_list_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_optional_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_enum_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result); +typedef void (*edge_cases_edge_cases_async_keywords_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_Widget_fetch_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: edge_cases +/** Interface with no constructor; instances come from make_widget */ +/** Void method that can fail */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_refresh(const edge_cases_edge_cases_Widget* self, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_rename(const edge_cases_edge_cases_Widget* self, const uint8_t* class_ptr, size_t class_len, const uint8_t* import_ptr, size_t import_len, size_t* out_len, edge_cases_error* out_err); +/** Async method with a struct result */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_fetch(const edge_cases_edge_cases_Widget* self, int64_t id, edge_cases_edge_cases_Widget_fetch_callback callback, void* context); +EDGE_CASES_DEPRECATED("Use rename instead") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_old_name(const edge_cases_edge_cases_Widget* self, size_t* out_len, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Widget_clone(const edge_cases_edge_cases_Widget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_destroy(edge_cases_edge_cases_Widget* self); + +/** Deprecated interface with a throwing constructor */ +/** Fails when the vault is busy */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_open(const uint8_t* path_ptr, size_t path_len, edge_cases_error* out_err); +/** Throwing static */ +EDGE_CASES_API bool edge_cases_edge_cases_Vault_probe(edge_cases_error* out_err); +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Vault_peek(const edge_cases_edge_cases_Vault* self, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_clone(const edge_cases_edge_cases_Vault* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Vault_destroy(edge_cases_edge_cases_Vault* self); + +/** No parameters and no result */ +EDGE_CASES_API void edge_cases_edge_cases_noop(edge_cases_error* out_err); +/** Function named after a keyword whose parameters are all keywords */ +EDGE_CASES_API int32_t edge_cases_edge_cases_select(int32_t class_, const uint8_t* from_ptr, size_t from_len, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, edge_cases_error* out_err); +/** Optional list of optional structs, the deepest legal nesting */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_optional_params(const uint8_t* text_ptr, size_t text_len, const uint8_t* count_ptr, size_t count_len, const uint8_t* outer_ptr, size_t outer_len, const uint8_t* tags_ptr, size_t tags_len, const uint8_t* blob_ptr, size_t blob_len, const uint8_t* mode_ptr, size_t mode_len, size_t* out_len, edge_cases_error* out_err); +/** Two callback interface parameters on one function */ +EDGE_CASES_API void edge_cases_edge_cases_register_batch(void* listener_ctx, const edge_cases_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const edge_cases_edge_cases_Keyword_vtable* keyword_vtable, edge_cases_error* out_err); +/** Objects in every buffered position */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, edge_cases_error* out_err); +/** Iterator of objects */ +EDGE_CASES_API edge_cases_edge_cases_WidgetStreamIterator* edge_cases_edge_cases_widget_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_WidgetStreamIterator_next(edge_cases_edge_cases_WidgetStreamIterator* iter, edge_cases_edge_cases_Widget** out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_WidgetStreamIterator_destroy(edge_cases_edge_cases_WidgetStreamIterator* iter); +/** Async returning an object */ +EDGE_CASES_API void edge_cases_edge_cases_async_widget(edge_cases_edge_cases_async_widget_callback callback, void* context); +/** Factory for an interface with no constructor */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_make_widget(const uint8_t* label_ptr, size_t label_len, edge_cases_error* out_err); +/** Optional interface return */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_find_widget(int64_t id, edge_cases_error* out_err); +/** Interface parameter, including an optional one */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_inspect_widget(const edge_cases_edge_cases_Widget* widget, const edge_cases_edge_cases_Widget* fallback, size_t* out_len, edge_cases_error* out_err); +/** Iterator of strings */ +EDGE_CASES_API edge_cases_edge_cases_StringStreamIterator* edge_cases_edge_cases_string_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_StringStreamIterator_next(edge_cases_edge_cases_StringStreamIterator* iter, const uint8_t** out_item, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_StringStreamIterator_destroy(edge_cases_edge_cases_StringStreamIterator* iter); +/** Iterator of scalars */ +EDGE_CASES_API edge_cases_edge_cases_ScalarStreamIterator* edge_cases_edge_cases_scalar_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ScalarStreamIterator_next(edge_cases_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ScalarStreamIterator_destroy(edge_cases_edge_cases_ScalarStreamIterator* iter); +/** Iterator of enums */ +EDGE_CASES_API edge_cases_edge_cases_ModeStreamIterator* edge_cases_edge_cases_mode_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ModeStreamIterator_next(edge_cases_edge_cases_ModeStreamIterator* iter, edge_cases_edge_cases_Mode* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ModeStreamIterator_destroy(edge_cases_edge_cases_ModeStreamIterator* iter); +/** Async with no result */ +EDGE_CASES_API void edge_cases_edge_cases_async_void(edge_cases_edge_cases_async_void_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_bytes(uint32_t size, edge_cases_edge_cases_async_bytes_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_struct(edge_cases_edge_cases_async_struct_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_list(edge_cases_cancel_token* cancel_token, edge_cases_edge_cases_async_list_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_optional(edge_cases_edge_cases_async_optional_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_enum(edge_cases_edge_cases_async_enum_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_keywords(const uint8_t* class_ptr, size_t class_len, int32_t import, edge_cases_edge_cases_async_keywords_callback callback, void* context); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_DEPRECATED("Use Mode-based APIs") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy value, size_t* out_len, edge_cases_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // EDGE_CASES_H + +// END edge_cases.h diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_jni_c.snap new file mode 100644 index 00000000..415540fc --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_edge_cases_jni_c.snap @@ -0,0 +1,1000 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#include +#include +#include +#include +#include +#include + +/* The shim forwards deprecated functions too, so it doesn't warn about them. */ +#define EDGE_CASES_DEPRECATED(msg) +#include "edge_cases.h" + +/* Every static name in this file starts with `Jni_`, which no C ABI symbol + (all lowercase `edge_cases_...`) and no JNI export (`Java_...`) can + spell. */ + +static JavaVM* Jni_vm = NULL; +static jclass Jni_bridge = NULL; +static jmethodID Jni_error = NULL; + +/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value + buffers all cross as one. */ +typedef struct { + jbyteArray array; + jbyte* elems; + const uint8_t* ptr; + size_t len; +} Jni_bytes; + +static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { + Jni_bytes b = {array, NULL, NULL, 0}; + if (array == NULL) { + return b; + } + b.len = (size_t)(*env)->GetArrayLength(env, array); + b.elems = (*env)->GetByteArrayElements(env, array, NULL); + b.ptr = (const uint8_t*)b.elems; + if (b.elems == NULL) { + b.len = 0; + } + return b; +} + +static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { + if (b->elems != NULL) { + (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); + } +} + +/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid + input, so NULL + 0 is the empty array). */ +static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = (*env)->NewByteArray(env, (jsize)len); + if (out != NULL && len > 0 && ptr != NULL) { + (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); + } + return out; +} + +/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ +static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = Jni_new_bytes(env, ptr, len); + if (ptr != NULL) { + edge_cases_free_bytes((uint8_t*)ptr, len); + } + return out; +} + +/* Throws the Kotlin exception for `err` (mapped through domain `domain`, + 0 for the generic one) and clears `err`. */ +static inline void Jni_throw(JNIEnv* env, edge_cases_error* err, jint domain) { + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + edge_cases_error_clear(err); + if ((*env)->ExceptionCheck(env)) { + return; + } + jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); + if (ex != NULL && !(*env)->ExceptionCheck(env)) { + (*env)->Throw(env, ex); + } +} + +static jint Jni_load_error(JNIEnv* env, const char* message) { + jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); + if (cls != NULL) { + (*env)->ThrowNew(env, cls, message); + } + return JNI_ERR; +} + +/* Generated below: the contract checks and the method IDs the shim caches. */ +static jint Jni_load(JNIEnv* env); + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env = NULL; + (void)reserved; + Jni_vm = vm; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + if (edge_cases_abi_version() != 3u) { + return Jni_load_error(env, "edge_cases: the native library implements a different C ABI revision than these bindings (expected 3)"); + } + jclass bridge = (*env)->FindClass(env, "edge_cases/JniBridge"); + if (bridge == NULL) { + return JNI_ERR; + } + Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); + Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Ledge_cases/FfiException;"); + if (Jni_error == NULL) { + return JNI_ERR; + } + if (Jni_load(env) != JNI_OK) { + return JNI_ERR; + } + return JNI_VERSION_1_6; +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { + (void)env; + (void)cls; + return (jlong)edge_cases_debug_live((int32_t)kind); +} + +/* Producer threads (async completions, callback calls, vtable `free`) are + usually not JVM threads. The first time one calls in it attaches as a + daemon and keeps its JNIEnv; a thread-local destructor detaches it when + the thread exits, so a busy producer thread pays for one attach, not one + per call. */ +#if defined(_WIN32) +static void Jni_remember_attach(void) {} +#else +#include + +static pthread_key_t Jni_env_key; +static pthread_once_t Jni_env_once = PTHREAD_ONCE_INIT; + +static void Jni_detach(void* env) { + (void)env; + if (Jni_vm != NULL) { + (*Jni_vm)->DetachCurrentThread(Jni_vm); + } +} + +static void Jni_make_env_key(void) { + pthread_key_create(&Jni_env_key, Jni_detach); +} + +static void Jni_remember_attach(void) { + pthread_once(&Jni_env_once, Jni_make_env_key); + pthread_setspecific(Jni_env_key, (void*)Jni_vm); +} +#endif + +/* The calling thread's JNIEnv, attaching the thread if needed; NULL when + the JVM is unavailable. */ +static JNIEnv* Jni_env(void) { + JNIEnv* env = NULL; + if (Jni_vm == NULL) { + return NULL; + } + if ((*Jni_vm)->GetEnv(Jni_vm, (void**)&env, JNI_VERSION_1_6) == JNI_OK) { + return env; + } + /* `void*` converts to the `JNIEnv**` Android declares and the `void**` + the JDK declares. */ + if ((*Jni_vm)->AttachCurrentThreadAsDaemon(Jni_vm, (void*)&env, NULL) != JNI_OK) { + return NULL; + } + Jni_remember_attach(); + return env; +} + +/* Async completions deliver to the pinned Kotlin `NativeCompletion`. */ +static jmethodID Jni_on_unit = NULL; +static jmethodID Jni_on_boolean = NULL; +static jmethodID Jni_on_byte = NULL; +static jmethodID Jni_on_short = NULL; +static jmethodID Jni_on_int = NULL; +static jmethodID Jni_on_long = NULL; +static jmethodID Jni_on_float = NULL; +static jmethodID Jni_on_double = NULL; +static jmethodID Jni_on_bytes = NULL; +static jmethodID Jni_on_error = NULL; + +static jint Jni_load_async(JNIEnv* env) { + jclass cls = (*env)->FindClass(env, "edge_cases/NativeCompletion"); + if (cls == NULL) { + return JNI_ERR; + } + Jni_on_unit = (*env)->GetMethodID(env, cls, "onUnit", "()V"); + Jni_on_boolean = (*env)->GetMethodID(env, cls, "onBoolean", "(Z)V"); + Jni_on_byte = (*env)->GetMethodID(env, cls, "onByte", "(B)V"); + Jni_on_short = (*env)->GetMethodID(env, cls, "onShort", "(S)V"); + Jni_on_int = (*env)->GetMethodID(env, cls, "onInt", "(I)V"); + Jni_on_long = (*env)->GetMethodID(env, cls, "onLong", "(J)V"); + Jni_on_float = (*env)->GetMethodID(env, cls, "onFloat", "(F)V"); + Jni_on_double = (*env)->GetMethodID(env, cls, "onDouble", "(D)V"); + Jni_on_bytes = (*env)->GetMethodID(env, cls, "onBytes", "([B)V"); + Jni_on_error = (*env)->GetMethodID(env, cls, "onError", "(I[B[B)V"); + if (Jni_on_unit == NULL || Jni_on_error == NULL || Jni_on_bytes == NULL) { + return JNI_ERR; + } + return JNI_OK; +} + +/* Starts a completion on the producer thread: returns the JNIEnv with a + local frame pushed, or NULL when the call failed (the failure, or the + loss of the JVM, has then been handled and `context` released). */ +static JNIEnv* Jni_complete_begin(void* context, edge_cases_error* err) { + JNIEnv* env = Jni_env(); + if (env == NULL) { + edge_cases_error_free(err); + return NULL; + } + if ((*env)->PushLocalFrame(env, 8) != 0) { + (*env)->ExceptionClear(env); + edge_cases_error_free(err); + return NULL; + } + if (err == NULL) { + return env; + } + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + edge_cases_error_free(err); + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + (*env)->DeleteGlobalRef(env, (jobject)context); + return NULL; +} + +/* Finishes a successful completion: reports anything the Kotlin side threw + (it has no caller to propagate to) and releases `context`. */ +static void Jni_complete_end(JNIEnv* env, void* context) { + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + (*env)->DeleteGlobalRef(env, (jobject)context); +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_cancel_1token_1create(JNIEnv* env, jclass cls) { + (void)env; + (void)cls; + return (jlong)(intptr_t)edge_cases_cancel_token_create(); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_cancel_1token_1cancel(JNIEnv* env, jclass cls, jlong token) { + (void)env; + (void)cls; + edge_cases_cancel_token_cancel((edge_cases_cancel_token*)(intptr_t)token); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_cancel_1token_1destroy(JNIEnv* env, jclass cls, jlong token) { + (void)env; + (void)cls; + edge_cases_cancel_token_destroy((edge_cases_cancel_token*)(intptr_t)token); +} + +/* Callback interfaces: the producer holds a global reference to the Kotlin + implementation as `ctx`; trampolines call the matching static dispatch + method on JniBridge from whatever thread the producer uses. */ +static jmethodID Jni_describe = NULL; + +static jint Jni_load_callbacks(JNIEnv* env) { + Jni_describe = (*env)->GetStaticMethodID(env, Jni_bridge, "describe", "(Ljava/lang/Throwable;)[B"); + return Jni_describe != NULL ? JNI_OK : JNI_ERR; +} + +/* Enters a trampoline: the JNIEnv with a local frame pushed, or NULL after + reporting the failure through `out_err`. */ +static JNIEnv* Jni_callback_begin(edge_cases_error* out_err) { + JNIEnv* env = Jni_env(); + if (env == NULL) { + edge_cases_error_set(out_err, -4, "could not attach the calling thread to the JVM"); + return NULL; + } + if ((*env)->PushLocalFrame(env, 16) != 0) { + (*env)->ExceptionClear(env); + edge_cases_error_set(out_err, -4, "JNI local frame exhausted"); + return NULL; + } + return env; +} + +/* Leaves a trampoline. When the implementation threw, reports its + `toString()` through `out_err` with code -4 and returns 1; nothing ever + unwinds through the producer's frames. */ +static int Jni_callback_end(JNIEnv* env, edge_cases_error* out_err) { + int failed = 0; + if ((*env)->ExceptionCheck(env)) { + failed = 1; + jthrowable ex = (*env)->ExceptionOccurred(env); + (*env)->ExceptionClear(env); + jbyteArray text = (jbyteArray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_describe, ex); + char* message = NULL; + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + } else if (text != NULL) { + jsize len = (*env)->GetArrayLength(env, text); + message = (char*)malloc((size_t)len + 1u); + if (message != NULL) { + (*env)->GetByteArrayRegion(env, text, 0, len, (jbyte*)message); + message[len] = '\0'; + } + } + edge_cases_error_set(out_err, -4, message != NULL ? message : "callback implementation threw"); + free(message); + } + (*env)->PopLocalFrame(env, NULL); + return failed; +} + +/* The vtable `free` entry: drops the producer's reference to the Kotlin + implementation. */ +static void Jni_release_callback(void* ctx) { + JNIEnv* env = Jni_env(); + if (env != NULL && ctx != NULL) { + (*env)->DeleteGlobalRef(env, (jobject)ctx); + } +} + +static jmethodID Jni_mid_edge_cases_BatchListener_on_batch = NULL; +static jmethodID Jni_mid_edge_cases_BatchListener_on_widget = NULL; +static jmethodID Jni_mid_edge_cases_BatchListener_on_tick = NULL; +static jmethodID Jni_mid_edge_cases_BatchListener_should_continue = NULL; +static jmethodID Jni_mid_edge_cases_BatchListener_pick_mode = NULL; + +static void Jni_edge_cases_BatchListener_on_batch(void* ctx, const uint8_t* p_items_ptr, size_t p_items_len, const uint8_t* p_raw_ptr, size_t p_raw_len, const uint8_t* p_first_ptr, size_t p_first_len, edge_cases_edge_cases_Mode p_mode, edge_cases_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return; + } + (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_on_batch, (jobject)ctx, Jni_new_bytes(env, p_items_ptr, p_items_len), Jni_new_bytes(env, p_raw_ptr, p_raw_len), Jni_new_bytes(env, p_first_ptr, p_first_len), (jint)p_mode); + Jni_callback_end(env, out_err); +} + +static void Jni_edge_cases_BatchListener_on_widget(void* ctx, edge_cases_edge_cases_Widget* p_widget, edge_cases_edge_cases_Widget* p_fallback, const uint8_t* p_children_ptr, size_t p_children_len, edge_cases_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return; + } + (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_on_widget, (jobject)ctx, (jlong)(intptr_t)p_widget, (jlong)(intptr_t)p_fallback, Jni_new_bytes(env, p_children_ptr, p_children_len)); + Jni_callback_end(env, out_err); +} + +static void Jni_edge_cases_BatchListener_on_tick(void* ctx, edge_cases_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return; + } + (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_on_tick, (jobject)ctx); + Jni_callback_end(env, out_err); +} + +static bool Jni_edge_cases_BatchListener_should_continue(void* ctx, int32_t p_class, edge_cases_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return (bool)0; + } + jboolean rv = (*env)->CallStaticBooleanMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_should_continue, (jobject)ctx, (jint)p_class); + if (Jni_callback_end(env, out_err)) { + return (bool)0; + } + return rv == JNI_TRUE; +} + +static edge_cases_edge_cases_Mode Jni_edge_cases_BatchListener_pick_mode(void* ctx, edge_cases_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return (edge_cases_edge_cases_Mode)0; + } + jint rv = (*env)->CallStaticIntMethod(env, Jni_bridge, Jni_mid_edge_cases_BatchListener_pick_mode, (jobject)ctx); + if (Jni_callback_end(env, out_err)) { + return (edge_cases_edge_cases_Mode)0; + } + return (edge_cases_edge_cases_Mode)rv; +} + +static const edge_cases_edge_cases_BatchListener_vtable Jni_edge_cases_BatchListener_vtable = {Jni_edge_cases_BatchListener_on_batch, Jni_edge_cases_BatchListener_on_widget, Jni_edge_cases_BatchListener_on_tick, Jni_edge_cases_BatchListener_should_continue, Jni_edge_cases_BatchListener_pick_mode, Jni_release_callback}; + +static jmethodID Jni_mid_edge_cases_Keyword_delete = NULL; +static jmethodID Jni_mid_edge_cases_Keyword_import = NULL; + +static void Jni_edge_cases_Keyword_delete(void* ctx, int32_t p_self, edge_cases_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return; + } + (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_edge_cases_Keyword_delete, (jobject)ctx, (jint)p_self); + Jni_callback_end(env, out_err); +} + +static int32_t Jni_edge_cases_Keyword_import(void* ctx, edge_cases_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return (int32_t)0; + } + jint rv = (*env)->CallStaticIntMethod(env, Jni_bridge, Jni_mid_edge_cases_Keyword_import, (jobject)ctx); + if (Jni_callback_end(env, out_err)) { + return (int32_t)0; + } + return (int32_t)rv; +} + +static const edge_cases_edge_cases_Keyword_vtable Jni_edge_cases_Keyword_vtable = {Jni_edge_cases_Keyword_delete, Jni_edge_cases_Keyword_import, Jni_release_callback}; + +static jint Jni_load(JNIEnv* env) { + (void)env; + if (edge_cases_edge_cases_checksum() != UINT64_C(0x1567f441073bd4aa)) { + return Jni_load_error(env, "edge_cases: module 'edge_cases' does not match these bindings (its contract checksum differs); regenerate the bindings or rebuild the library"); + } + if (Jni_load_async(env) != JNI_OK) { + return JNI_ERR; + } + if (Jni_load_callbacks(env) != JNI_OK) { + return JNI_ERR; + } + Jni_mid_edge_cases_BatchListener_on_batch = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_on_batch", "(Ledge_cases/BatchListener;[B[B[BI)V"); + if (Jni_mid_edge_cases_BatchListener_on_batch == NULL) { + return JNI_ERR; + } + Jni_mid_edge_cases_BatchListener_on_widget = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_on_widget", "(Ledge_cases/BatchListener;JJ[B)V"); + if (Jni_mid_edge_cases_BatchListener_on_widget == NULL) { + return JNI_ERR; + } + Jni_mid_edge_cases_BatchListener_on_tick = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_on_tick", "(Ledge_cases/BatchListener;)V"); + if (Jni_mid_edge_cases_BatchListener_on_tick == NULL) { + return JNI_ERR; + } + Jni_mid_edge_cases_BatchListener_should_continue = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_should_continue", "(Ledge_cases/BatchListener;I)Z"); + if (Jni_mid_edge_cases_BatchListener_should_continue == NULL) { + return JNI_ERR; + } + Jni_mid_edge_cases_BatchListener_pick_mode = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_BatchListener_pick_mode", "(Ledge_cases/BatchListener;)I"); + if (Jni_mid_edge_cases_BatchListener_pick_mode == NULL) { + return JNI_ERR; + } + Jni_mid_edge_cases_Keyword_delete = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_Keyword_delete", "(Ledge_cases/Keyword;I)V"); + if (Jni_mid_edge_cases_Keyword_delete == NULL) { + return JNI_ERR; + } + Jni_mid_edge_cases_Keyword_import = (*env)->GetStaticMethodID(env, Jni_bridge, "edge_cases_Keyword_import", "(Ledge_cases/Keyword;)I"); + if (Jni_mid_edge_cases_Keyword_import == NULL) { + return JNI_ERR; + } + return JNI_OK; +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1clone(JNIEnv* env, jclass cls, jlong self) { + (void)env; + (void)cls; + return (jlong)(intptr_t)edge_cases_edge_cases_Widget_clone((const edge_cases_edge_cases_Widget*)(intptr_t)self); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1destroy(JNIEnv* env, jclass cls, jlong self) { + (void)env; + (void)cls; + edge_cases_edge_cases_Widget_destroy((edge_cases_edge_cases_Widget*)(intptr_t)self); +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1clone(JNIEnv* env, jclass cls, jlong self) { + (void)env; + (void)cls; + return (jlong)(intptr_t)edge_cases_edge_cases_Vault_clone((const edge_cases_edge_cases_Vault*)(intptr_t)self); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1destroy(JNIEnv* env, jclass cls, jlong self) { + (void)env; + (void)cls; + edge_cases_edge_cases_Vault_destroy((edge_cases_edge_cases_Vault*)(intptr_t)self); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1noop(JNIEnv* env, jclass cls) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_noop(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return; + } +} + +JNIEXPORT jint JNICALL Java_edge_1cases_JniBridge_edge_1cases_1select(JNIEnv* env, jclass cls, jint p_class, jbyteArray p_from, jbyteArray p_func, jbyteArray p_range, jboolean p_type_) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_from_b = Jni_borrow_bytes(env, p_from); + Jni_bytes p_func_b = Jni_borrow_bytes(env, p_func); + Jni_bytes p_range_b = Jni_borrow_bytes(env, p_range); + int32_t rv = edge_cases_edge_cases_select((int32_t)p_class, p_from_b.ptr, p_from_b.len, p_func_b.ptr, p_func_b.len, p_range_b.ptr, p_range_b.len, p_type_ == JNI_TRUE, &err); + Jni_release_bytes(env, &p_from_b); + Jni_release_bytes(env, &p_func_b); + Jni_release_bytes(env, &p_range_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1nested_1optional(JNIEnv* env, jclass cls, jbyteArray p_items) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_items_b = Jni_borrow_bytes(env, p_items); + size_t out_len = 0; + const uint8_t* rv = edge_cases_edge_cases_nested_optional(p_items_b.ptr, p_items_b.len, &out_len, &err); + Jni_release_bytes(env, &p_items_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1optional_1params(JNIEnv* env, jclass cls, jbyteArray p_text, jbyteArray p_count, jbyteArray p_outer, jbyteArray p_tags, jbyteArray p_blob, jbyteArray p_mode) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_text_b = Jni_borrow_bytes(env, p_text); + Jni_bytes p_count_b = Jni_borrow_bytes(env, p_count); + Jni_bytes p_outer_b = Jni_borrow_bytes(env, p_outer); + Jni_bytes p_tags_b = Jni_borrow_bytes(env, p_tags); + Jni_bytes p_blob_b = Jni_borrow_bytes(env, p_blob); + Jni_bytes p_mode_b = Jni_borrow_bytes(env, p_mode); + size_t out_len = 0; + const uint8_t* rv = edge_cases_edge_cases_optional_params(p_text_b.ptr, p_text_b.len, p_count_b.ptr, p_count_b.len, p_outer_b.ptr, p_outer_b.len, p_tags_b.ptr, p_tags_b.len, p_blob_b.ptr, p_blob_b.len, p_mode_b.ptr, p_mode_b.len, &out_len, &err); + Jni_release_bytes(env, &p_text_b); + Jni_release_bytes(env, &p_count_b); + Jni_release_bytes(env, &p_outer_b); + Jni_release_bytes(env, &p_tags_b); + Jni_release_bytes(env, &p_blob_b); + Jni_release_bytes(env, &p_mode_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1register_1batch(JNIEnv* env, jclass cls, jobject p_listener, jobject p_keyword) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_register_batch((void*)(*env)->NewGlobalRef(env, p_listener), &Jni_edge_cases_BatchListener_vtable, (void*)(*env)->NewGlobalRef(env, p_keyword), &Jni_edge_cases_Keyword_vtable, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return; + } +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1widgets_1everywhere(JNIEnv* env, jclass cls, jbyteArray p_list, jbyteArray p_by_name, jbyteArray p_sparse, jbyteArray p_holder) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_list_b = Jni_borrow_bytes(env, p_list); + Jni_bytes p_by_name_b = Jni_borrow_bytes(env, p_by_name); + Jni_bytes p_sparse_b = Jni_borrow_bytes(env, p_sparse); + Jni_bytes p_holder_b = Jni_borrow_bytes(env, p_holder); + size_t out_len = 0; + const uint8_t* rv = edge_cases_edge_cases_widgets_everywhere(p_list_b.ptr, p_list_b.len, p_by_name_b.ptr, p_by_name_b.len, p_sparse_b.ptr, p_sparse_b.len, p_holder_b.ptr, p_holder_b.len, &out_len, &err); + Jni_release_bytes(env, &p_list_b); + Jni_release_bytes(env, &p_by_name_b); + Jni_release_bytes(env, &p_sparse_b); + Jni_release_bytes(env, &p_holder_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1widget_1stream(JNIEnv* env, jclass cls) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_WidgetStreamIterator* rv = edge_cases_edge_cases_widget_stream(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1WidgetStreamIterator_1next(JNIEnv* env, jclass cls, jlong iter, jlongArray out) { + edge_cases_error err = {0, NULL, NULL, 0}; + edge_cases_edge_cases_Widget* item = (edge_cases_edge_cases_Widget*)0; + (void)cls; + int32_t has = edge_cases_edge_cases_WidgetStreamIterator_next((edge_cases_edge_cases_WidgetStreamIterator*)(intptr_t)iter, &item, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return JNI_FALSE; + } + if (has == 0) { + return JNI_FALSE; + } + jlong value = (jlong)(intptr_t)item; + (*env)->SetLongArrayRegion(env, out, 0, 1, &value); + return JNI_TRUE; +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1WidgetStreamIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { + (void)env; + (void)cls; + edge_cases_edge_cases_WidgetStreamIterator_destroy((edge_cases_edge_cases_WidgetStreamIterator*)(intptr_t)iter); +} + +static void Jni_done_edge_cases_async_widget(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_long, (jlong)(intptr_t)result); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1widget(JNIEnv* env, jclass cls, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + edge_cases_edge_cases_async_widget(Jni_done_edge_cases_async_widget, context); +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1make_1widget(JNIEnv* env, jclass cls, jbyteArray p_label) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_label_b = Jni_borrow_bytes(env, p_label); + edge_cases_edge_cases_Widget* rv = edge_cases_edge_cases_make_widget(p_label_b.ptr, p_label_b.len, &err); + Jni_release_bytes(env, &p_label_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1find_1widget(JNIEnv* env, jclass cls, jlong p_id) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_Widget* rv = edge_cases_edge_cases_find_widget((int64_t)p_id, &err); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1inspect_1widget(JNIEnv* env, jclass cls, jlong p_widget, jlong p_fallback) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = edge_cases_edge_cases_inspect_widget((const edge_cases_edge_cases_Widget*)(intptr_t)p_widget, (const edge_cases_edge_cases_Widget*)(intptr_t)p_fallback, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1string_1stream(JNIEnv* env, jclass cls) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_StringStreamIterator* rv = edge_cases_edge_cases_string_stream(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1StringStreamIterator_1next(JNIEnv* env, jclass cls, jlong iter) { + edge_cases_error err = {0, NULL, NULL, 0}; + const uint8_t* item = NULL; + size_t out_len = 0; + (void)cls; + int32_t has = edge_cases_edge_cases_StringStreamIterator_next((edge_cases_edge_cases_StringStreamIterator*)(intptr_t)iter, &item, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + if (has == 0) { + return NULL; + } + return Jni_take_bytes(env, item, out_len); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1StringStreamIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { + (void)env; + (void)cls; + edge_cases_edge_cases_StringStreamIterator_destroy((edge_cases_edge_cases_StringStreamIterator*)(intptr_t)iter); +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1scalar_1stream(JNIEnv* env, jclass cls) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_ScalarStreamIterator* rv = edge_cases_edge_cases_scalar_stream(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1ScalarStreamIterator_1next(JNIEnv* env, jclass cls, jlong iter, jintArray out) { + edge_cases_error err = {0, NULL, NULL, 0}; + int32_t item = (int32_t)0; + (void)cls; + int32_t has = edge_cases_edge_cases_ScalarStreamIterator_next((edge_cases_edge_cases_ScalarStreamIterator*)(intptr_t)iter, &item, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return JNI_FALSE; + } + if (has == 0) { + return JNI_FALSE; + } + jint value = (jint)item; + (*env)->SetIntArrayRegion(env, out, 0, 1, &value); + return JNI_TRUE; +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1ScalarStreamIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { + (void)env; + (void)cls; + edge_cases_edge_cases_ScalarStreamIterator_destroy((edge_cases_edge_cases_ScalarStreamIterator*)(intptr_t)iter); +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1mode_1stream(JNIEnv* env, jclass cls) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_ModeStreamIterator* rv = edge_cases_edge_cases_mode_stream(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1ModeStreamIterator_1next(JNIEnv* env, jclass cls, jlong iter, jintArray out) { + edge_cases_error err = {0, NULL, NULL, 0}; + edge_cases_edge_cases_Mode item = (edge_cases_edge_cases_Mode)0; + (void)cls; + int32_t has = edge_cases_edge_cases_ModeStreamIterator_next((edge_cases_edge_cases_ModeStreamIterator*)(intptr_t)iter, &item, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return JNI_FALSE; + } + if (has == 0) { + return JNI_FALSE; + } + jint value = (jint)item; + (*env)->SetIntArrayRegion(env, out, 0, 1, &value); + return JNI_TRUE; +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1ModeStreamIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { + (void)env; + (void)cls; + edge_cases_edge_cases_ModeStreamIterator_destroy((edge_cases_edge_cases_ModeStreamIterator*)(intptr_t)iter); +} + +static void Jni_done_edge_cases_async_void(void* context, edge_cases_error* err) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_unit); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1void(JNIEnv* env, jclass cls, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + edge_cases_edge_cases_async_void(Jni_done_edge_cases_async_void, context); +} + +static void Jni_done_edge_cases_async_bytes(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1bytes(JNIEnv* env, jclass cls, jlong p_size, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + edge_cases_edge_cases_async_bytes((uint32_t)p_size, Jni_done_edge_cases_async_bytes, context); +} + +static void Jni_done_edge_cases_async_struct(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1struct(JNIEnv* env, jclass cls, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + edge_cases_edge_cases_async_struct(Jni_done_edge_cases_async_struct, context); +} + +static void Jni_done_edge_cases_async_list(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1list(JNIEnv* env, jclass cls, jlong cancel_token, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + edge_cases_edge_cases_async_list((edge_cases_cancel_token*)(intptr_t)cancel_token, Jni_done_edge_cases_async_list, context); +} + +static void Jni_done_edge_cases_async_optional(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1optional(JNIEnv* env, jclass cls, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + edge_cases_edge_cases_async_optional(Jni_done_edge_cases_async_optional, context); +} + +static void Jni_done_edge_cases_async_enum(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_int, (jint)result); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1enum(JNIEnv* env, jclass cls, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + edge_cases_edge_cases_async_enum(Jni_done_edge_cases_async_enum, context); +} + +static void Jni_done_edge_cases_async_keywords(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1async_1keywords(JNIEnv* env, jclass cls, jbyteArray p_class, jint p_import, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + Jni_bytes p_class_b = Jni_borrow_bytes(env, p_class); + edge_cases_edge_cases_async_keywords(p_class_b.ptr, p_class_b.len, (int32_t)p_import, Jni_done_edge_cases_async_keywords, context); + Jni_release_bytes(env, &p_class_b); +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1keywords_1roundtrip(JNIEnv* env, jclass cls, jbyteArray p_value) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_value_b = Jni_borrow_bytes(env, p_value); + size_t out_len = 0; + const uint8_t* rv = edge_cases_edge_cases_keywords_roundtrip(p_value_b.ptr, p_value_b.len, &out_len, &err); + Jni_release_bytes(env, &p_value_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1legacy_1value(JNIEnv* env, jclass cls, jint p_value) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = edge_cases_edge_cases_legacy_value((edge_cases_edge_cases_Legacy)p_value, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1refresh(JNIEnv* env, jclass cls, jlong self) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_Widget_refresh((const edge_cases_edge_cases_Widget*)(intptr_t)self, &err); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return; + } +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1rename(JNIEnv* env, jclass cls, jlong self, jbyteArray p_class, jbyteArray p_import) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_class_b = Jni_borrow_bytes(env, p_class); + Jni_bytes p_import_b = Jni_borrow_bytes(env, p_import); + size_t out_len = 0; + const uint8_t* rv = edge_cases_edge_cases_Widget_rename((const edge_cases_edge_cases_Widget*)(intptr_t)self, p_class_b.ptr, p_class_b.len, p_import_b.ptr, p_import_b.len, &out_len, &err); + Jni_release_bytes(env, &p_class_b); + Jni_release_bytes(env, &p_import_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +static void Jni_done_edge_cases_Widget_fetch(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1fetch(JNIEnv* env, jclass cls, jlong self, jlong p_id, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + edge_cases_edge_cases_Widget_fetch((const edge_cases_edge_cases_Widget*)(intptr_t)self, (int64_t)p_id, Jni_done_edge_cases_Widget_fetch, context); +} + +JNIEXPORT jbyteArray JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Widget_1old_1name(JNIEnv* env, jclass cls, jlong self) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = edge_cases_edge_cases_Widget_old_name((const edge_cases_edge_cases_Widget*)(intptr_t)self, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1open(JNIEnv* env, jclass cls, jbyteArray p_path) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_path_b = Jni_borrow_bytes(env, p_path); + edge_cases_edge_cases_Vault* rv = edge_cases_edge_cases_Vault_open(p_path_b.ptr, p_path_b.len, &err); + Jni_release_bytes(env, &p_path_b); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jlong JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1peek(JNIEnv* env, jclass cls, jlong self) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + edge_cases_edge_cases_Widget* rv = edge_cases_edge_cases_Vault_peek((const edge_cases_edge_cases_Vault*)(intptr_t)self, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jboolean JNICALL Java_edge_1cases_JniBridge_edge_1cases_1Vault_1probe(JNIEnv* env, jclass cls) { + edge_cases_error err = {0, NULL, NULL, 0}; + (void)cls; + bool rv = edge_cases_edge_cases_Vault_probe(&err); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return 0; + } + return rv ? JNI_TRUE : JNI_FALSE; +} + + +// END edge_cases_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index 89ee5663..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,1153 +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" - -/* Copies `s` as a NUL-terminated standard UTF-8 string (JNI's modified - UTF-8 would encode supplementary characters as surrogate pairs the - producer rejects). Lone surrogates become U+FFFD. Release with free(). - Returns NULL for a NULL string, for a string holding U+0000 (which no - C string can carry), or on allocation failure; the producer then - reports the null argument through its marshalling error. */ -static char* weaveffi_jni_string_to_utf8(JNIEnv* env, jstring s) { - if (s == NULL) { return NULL; } - jsize n = (*env)->GetStringLength(env, s); - const jchar* u = (*env)->GetStringChars(env, s, NULL); - if (u == NULL) { return NULL; } - char* out = (char*)malloc((size_t)n * 4u + 1u); - if (out == NULL) { (*env)->ReleaseStringChars(env, s, u); return NULL; } - size_t o = 0; - for (jsize i = 0; i < n; i++) { - uint32_t c = u[i]; - if (c == 0u) { free(out); (*env)->ReleaseStringChars(env, s, u); return NULL; } - if (c >= 0xD800u && c <= 0xDBFFu && i + 1 < n && u[i + 1] >= 0xDC00u && u[i + 1] <= 0xDFFFu) { - c = 0x10000u + ((c - 0xD800u) << 10) + ((uint32_t)u[i + 1] - 0xDC00u); - i++; - } - else if (c >= 0xD800u && c <= 0xDFFFu) { c = 0xFFFDu; } - if (c < 0x80u) { out[o++] = (char)c; } - else if (c < 0x800u) { out[o++] = (char)(0xC0u | (c >> 6)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else if (c < 0x10000u) { out[o++] = (char)(0xE0u | (c >> 12)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else { out[o++] = (char)(0xF0u | (c >> 18)); out[o++] = (char)(0x80u | ((c >> 12) & 0x3Fu)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - } - out[o] = 0; - (*env)->ReleaseStringChars(env, s, u); - return out; -} - -/* Builds a jstring from standard UTF-8 (NewStringUTF expects modified UTF-8 - and mangles supplementary characters). NULL yields the empty string and - a malformed sequence yields U+FFFD, so this never fails on producer - output; it returns NULL only when the JVM could not allocate. */ -static jstring weaveffi_jni_utf8_to_string(JNIEnv* env, const char* s) { - if (s == NULL) { s = ""; } - size_t n = strlen(s); - jchar* buf = (jchar*)malloc((n + 1u) * sizeof(jchar)); - if (buf == NULL) { return NULL; } - size_t i = 0, o = 0; - while (i < n) { - unsigned char b = (unsigned char)s[i]; - uint32_t c; size_t len; - if (b < 0x80u) { c = b; len = 1; } - else if ((b & 0xE0u) == 0xC0u) { c = b & 0x1Fu; len = 2; } - else if ((b & 0xF0u) == 0xE0u) { c = b & 0x0Fu; len = 3; } - else if ((b & 0xF8u) == 0xF0u) { c = b & 0x07u; len = 4; } - else { c = 0xFFFDu; len = 1; } - if (i + len > n) { c = 0xFFFDu; len = 1; } - for (size_t k = 1; k < len; k++) { - unsigned char cb = (unsigned char)s[i + k]; - if ((cb & 0xC0u) != 0x80u) { c = 0xFFFDu; len = k; break; } - c = (c << 6) | (cb & 0x3Fu); - } - i += len; - if (c > 0x10FFFFu) { c = 0xFFFDu; } - if (c >= 0x10000u) { c -= 0x10000u; buf[o++] = (jchar)(0xD800u + (c >> 10)); buf[o++] = (jchar)(0xDC00u + (c & 0x3FFu)); } - else { buf[o++] = (jchar)c; } - } - jstring out = (*env)->NewString(env, buf, (jsize)o); - free(buf); - return out; -} - -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 = weaveffi_jni_utf8_to_string(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 = weaveffi_jni_utf8_to_string(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 JavaVM* weaveffi_jni_vm = NULL; -static jclass weaveffi_jni_entry_class = NULL; -static jmethodID weaveffi_jni_dispatch_exc = NULL; -static jmethodID weaveffi_jni_throwable_to_string = NULL; -static jclass weaveffi_edge_cases_BatchListener_jni_cls = NULL; -static jmethodID weaveffi_edge_cases_BatchListener_jni_mid_on_batch = NULL; -static jmethodID weaveffi_edge_cases_BatchListener_jni_mid_on_widget = NULL; -static jmethodID weaveffi_edge_cases_BatchListener_jni_mid_on_tick = NULL; -static jmethodID weaveffi_edge_cases_BatchListener_jni_mid_should_continue = NULL; -static jmethodID weaveffi_edge_cases_BatchListener_jni_mid_pick_mode = NULL; -static jclass weaveffi_edge_cases_Keyword_jni_cls = NULL; -static jmethodID weaveffi_edge_cases_Keyword_jni_mid_delete = NULL; -static jmethodID weaveffi_edge_cases_Keyword_jni_mid_import = NULL; - -static jint weaveffi_jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { (*env)->ThrowNew(env, cls, message); } - return JNI_ERR; -} - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - (void)reserved; - weaveffi_jni_vm = vm; - JNIEnv* env = NULL; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - if (weaveffi_abi_version() != 2u) { - return weaveffi_jni_load_error(env, "WeaveFFI: the native library implements a different C ABI revision than these bindings (expected 2)"); - } - jclass entry = (*env)->FindClass(env, "com/weaveffi/WeaveFFI"); - if (entry == NULL) { return JNI_ERR; } - weaveffi_jni_entry_class = (jclass)(*env)->NewGlobalRef(env, entry); - 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); } - jclass throwable = (*env)->FindClass(env, "java/lang/Throwable"); - if (throwable == NULL) { return JNI_ERR; } - weaveffi_jni_throwable_to_string = (*env)->GetMethodID(env, throwable, "toString", "()Ljava/lang/String;"); - if (weaveffi_jni_throwable_to_string == NULL) { return JNI_ERR; } - jclass weaveffi_edge_cases_BatchListener_jni_cls_local = (*env)->FindClass(env, "com/weaveffi/BatchListenerJni"); - if (weaveffi_edge_cases_BatchListener_jni_cls_local == NULL) { return JNI_ERR; } - weaveffi_edge_cases_BatchListener_jni_cls = (jclass)(*env)->NewGlobalRef(env, weaveffi_edge_cases_BatchListener_jni_cls_local); - weaveffi_edge_cases_BatchListener_jni_mid_on_batch = (*env)->GetStaticMethodID(env, weaveffi_edge_cases_BatchListener_jni_cls, "onBatch", "(Lcom/weaveffi/BatchListener;[B[B[BI)V"); - if (weaveffi_edge_cases_BatchListener_jni_mid_on_batch == NULL) { return JNI_ERR; } - weaveffi_edge_cases_BatchListener_jni_mid_on_widget = (*env)->GetStaticMethodID(env, weaveffi_edge_cases_BatchListener_jni_cls, "onWidget", "(Lcom/weaveffi/BatchListener;JJ[B)V"); - if (weaveffi_edge_cases_BatchListener_jni_mid_on_widget == NULL) { return JNI_ERR; } - weaveffi_edge_cases_BatchListener_jni_mid_on_tick = (*env)->GetStaticMethodID(env, weaveffi_edge_cases_BatchListener_jni_cls, "onTick", "(Lcom/weaveffi/BatchListener;)V"); - if (weaveffi_edge_cases_BatchListener_jni_mid_on_tick == NULL) { return JNI_ERR; } - weaveffi_edge_cases_BatchListener_jni_mid_should_continue = (*env)->GetStaticMethodID(env, weaveffi_edge_cases_BatchListener_jni_cls, "shouldContinue", "(Lcom/weaveffi/BatchListener;I)Z"); - if (weaveffi_edge_cases_BatchListener_jni_mid_should_continue == NULL) { return JNI_ERR; } - weaveffi_edge_cases_BatchListener_jni_mid_pick_mode = (*env)->GetStaticMethodID(env, weaveffi_edge_cases_BatchListener_jni_cls, "pickMode", "(Lcom/weaveffi/BatchListener;)I"); - if (weaveffi_edge_cases_BatchListener_jni_mid_pick_mode == NULL) { return JNI_ERR; } - jclass weaveffi_edge_cases_Keyword_jni_cls_local = (*env)->FindClass(env, "com/weaveffi/KeywordJni"); - if (weaveffi_edge_cases_Keyword_jni_cls_local == NULL) { return JNI_ERR; } - weaveffi_edge_cases_Keyword_jni_cls = (jclass)(*env)->NewGlobalRef(env, weaveffi_edge_cases_Keyword_jni_cls_local); - weaveffi_edge_cases_Keyword_jni_mid_delete = (*env)->GetStaticMethodID(env, weaveffi_edge_cases_Keyword_jni_cls, "delete", "(Lcom/weaveffi/Keyword;I)V"); - if (weaveffi_edge_cases_Keyword_jni_mid_delete == NULL) { return JNI_ERR; } - weaveffi_edge_cases_Keyword_jni_mid_import = (*env)->GetStaticMethodID(env, weaveffi_edge_cases_Keyword_jni_cls, "import", "(Lcom/weaveffi/Keyword;)I"); - if (weaveffi_edge_cases_Keyword_jni_mid_import == NULL) { return JNI_ERR; } - return JNI_VERSION_1_6; -} - -/* Returns 1 when this call attached the thread (the caller must detach), - 0 otherwise. *env is NULL when no JNIEnv could be obtained. */ -static int weaveffi_jni_attach(JNIEnv** env) { - *env = NULL; - if (weaveffi_jni_vm == NULL) { return 0; } - if ((*weaveffi_jni_vm)->GetEnv(weaveffi_jni_vm, (void**)env, JNI_VERSION_1_6) == JNI_OK) { return 0; } - if ((*weaveffi_jni_vm)->AttachCurrentThread(weaveffi_jni_vm, (void*)env, NULL) != JNI_OK) { *env = NULL; return 0; } - return 1; -} - -static void weaveffi_jni_detach(int attached) { - if (attached) { (*weaveffi_jni_vm)->DetachCurrentThread(weaveffi_jni_vm); } -} - -typedef struct { - jobject callback; -} weaveffi_jni_async_ctx; - -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_jni_report_foreign(JNIEnv* env, weaveffi_error* out_err) { - jthrowable ex = (*env)->ExceptionOccurred(env); - (*env)->ExceptionClear(env); - jstring jmsg = NULL; - if (ex != NULL && weaveffi_jni_throwable_to_string != NULL) { - jmsg = (jstring)(*env)->CallObjectMethod(env, ex, weaveffi_jni_throwable_to_string); - if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); jmsg = NULL; } - } - char* msg = weaveffi_jni_string_to_utf8(env, jmsg); - weaveffi_error_set(out_err, -4, msg != NULL ? msg : "callback interface implementation threw"); - free(msg); -} - -static void weaveffi_edge_cases_BatchListener_jni_on_batch(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, weaveffi_edge_cases_Mode mode, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - 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); } - jint _a3 = (jint)mode; - (*env)->CallStaticVoidMethod(env, weaveffi_edge_cases_BatchListener_jni_cls, weaveffi_edge_cases_BatchListener_jni_mid_on_batch, (jobject)ctx, _a0, _a1, _a2, _a3); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); -} - -static void weaveffi_edge_cases_BatchListener_jni_on_widget(void* ctx, weaveffi_edge_cases_Widget* widget, weaveffi_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return; - } - jlong _a0 = (jlong)(intptr_t)widget; - jlong _a1 = (jlong)(intptr_t)fallback; - jbyteArray _a2 = (*env)->NewByteArray(env, (jsize)children_len); - if (_a2 && children_ptr) { (*env)->SetByteArrayRegion(env, _a2, 0, (jsize)children_len, (const jbyte*)children_ptr); } - (*env)->CallStaticVoidMethod(env, weaveffi_edge_cases_BatchListener_jni_cls, weaveffi_edge_cases_BatchListener_jni_mid_on_widget, (jobject)ctx, _a0, _a1, _a2); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); -} - -static void weaveffi_edge_cases_BatchListener_jni_on_tick(void* ctx, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return; - } - (*env)->CallStaticVoidMethod(env, weaveffi_edge_cases_BatchListener_jni_cls, weaveffi_edge_cases_BatchListener_jni_mid_on_tick, (jobject)ctx); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); -} - -static bool weaveffi_edge_cases_BatchListener_jni_should_continue(void* ctx, int32_t class, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return false; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return false; - } - jint _a0 = (jint)class; - jboolean _rv = (*env)->CallStaticBooleanMethod(env, weaveffi_edge_cases_BatchListener_jni_cls, weaveffi_edge_cases_BatchListener_jni_mid_should_continue, (jobject)ctx, _a0); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - _rv = (jboolean)false; - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); - return _rv == JNI_TRUE; -} - -static weaveffi_edge_cases_Mode weaveffi_edge_cases_BatchListener_jni_pick_mode(void* ctx, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return 0; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return 0; - } - jint _rv = (*env)->CallStaticIntMethod(env, weaveffi_edge_cases_BatchListener_jni_cls, weaveffi_edge_cases_BatchListener_jni_mid_pick_mode, (jobject)ctx); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - _rv = (jint)0; - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); - return (weaveffi_edge_cases_Mode)_rv; -} - -static void weaveffi_edge_cases_BatchListener_jni_free(void* ctx) { - if (ctx == NULL) { return; } - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env != NULL) { (*env)->DeleteGlobalRef(env, (jobject)ctx); } - weaveffi_jni_detach(attached); -} - -static const weaveffi_edge_cases_BatchListener_vtable weaveffi_edge_cases_BatchListener_jni_vtable = { weaveffi_edge_cases_BatchListener_jni_on_batch, weaveffi_edge_cases_BatchListener_jni_on_widget, weaveffi_edge_cases_BatchListener_jni_on_tick, weaveffi_edge_cases_BatchListener_jni_should_continue, weaveffi_edge_cases_BatchListener_jni_pick_mode, weaveffi_edge_cases_BatchListener_jni_free }; - -static void weaveffi_edge_cases_Keyword_jni_delete(void* ctx, int32_t self, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return; - } - jint _a0 = (jint)self; - (*env)->CallStaticVoidMethod(env, weaveffi_edge_cases_Keyword_jni_cls, weaveffi_edge_cases_Keyword_jni_mid_delete, (jobject)ctx, _a0); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); -} - -static int32_t weaveffi_edge_cases_Keyword_jni_import(void* ctx, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return 0; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return 0; - } - jint _rv = (*env)->CallStaticIntMethod(env, weaveffi_edge_cases_Keyword_jni_cls, weaveffi_edge_cases_Keyword_jni_mid_import, (jobject)ctx); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - _rv = (jint)0; - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); - return (int32_t)_rv; -} - -static void weaveffi_edge_cases_Keyword_jni_free(void* ctx) { - if (ctx == NULL) { return; } - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env != NULL) { (*env)->DeleteGlobalRef(env, (jobject)ctx); } - weaveffi_jni_detach(attached); -} - -static const weaveffi_edge_cases_Keyword_vtable weaveffi_edge_cases_Keyword_jni_vtable = { weaveffi_edge_cases_Keyword_jni_delete, weaveffi_edge_cases_Keyword_jni_import, weaveffi_edge_cases_Keyword_jni_free }; - -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}; - char* from_chars = weaveffi_jni_string_to_utf8(env, from); - 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); - free(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_registerBatch(JNIEnv* env, jclass clazz, jobject listener, jobject keyword) { - weaveffi_error err = {0, NULL, NULL, 0}; - jobject listener_ref = (*env)->NewGlobalRef(env, listener); - jobject keyword_ref = (*env)->NewGlobalRef(env, keyword); - weaveffi_edge_cases_register_batch((void*)listener_ref, &weaveffi_edge_cases_BatchListener_jni_vtable, (void*)keyword_ref, &weaveffi_edge_cases_Keyword_jni_vtable, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - - } - return; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_widgetsEverywhereJni(JNIEnv* env, jclass clazz, jbyteArray list, jbyteArray by_name, jbyteArray sparse, jbyteArray holder) { - weaveffi_error err = {0, NULL, NULL, 0}; - jbyte* list_elems = (*env)->GetByteArrayElements(env, list, NULL); - jsize list_len = (*env)->GetArrayLength(env, list); - jbyte* by_name_elems = (*env)->GetByteArrayElements(env, by_name, NULL); - jsize by_name_len = (*env)->GetArrayLength(env, by_name); - jbyte* sparse_elems = (*env)->GetByteArrayElements(env, sparse, NULL); - jsize sparse_len = (*env)->GetArrayLength(env, sparse); - jbyte* holder_elems = (*env)->GetByteArrayElements(env, holder, NULL); - jsize holder_len = (*env)->GetArrayLength(env, holder); - size_t out_len = 0; - const uint8_t* rv = weaveffi_edge_cases_widgets_everywhere((const uint8_t*)list_elems, (size_t)list_len, (const uint8_t*)by_name_elems, (size_t)by_name_len, (const uint8_t*)sparse_elems, (size_t)sparse_len, (const uint8_t*)holder_elems, (size_t)holder_len, &out_len, &err); - (*env)->ReleaseByteArrayElements(env, list, list_elems, JNI_ABORT); - (*env)->ReleaseByteArrayElements(env, by_name, by_name_elems, JNI_ABORT); - (*env)->ReleaseByteArrayElements(env, sparse, sparse_elems, JNI_ABORT); - (*env)->ReleaseByteArrayElements(env, holder, holder_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 jlong JNICALL Java_com_weaveffi_WeaveFFI_widgetStreamJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - weaveffi_edge_cases_WidgetStreamIterator* _iter = weaveffi_edge_cases_widget_stream(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong)(intptr_t)_iter; -} - -static void weaveffi_edge_cases_async_widget_jni_cb(void* context, weaveffi_error* err, weaveffi_edge_cases_Widget* result) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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 boxed_cls = (*env)->FindClass(env, "java/lang/Long"); - jobject boxed = (*env)->CallStaticObjectMethod(env, boxed_cls, (*env)->GetStaticMethodID(env, boxed_cls, "valueOf", "(J)Ljava/lang/Long;"), (jlong)(intptr_t)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); - free(ctx); - weaveffi_jni_detach(attached); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_asyncWidgetAsync(JNIEnv* env, jclass clazz, jobject callback) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); - ctx->callback = (*env)->NewGlobalRef(env, callback); - weaveffi_edge_cases_async_widget_async(weaveffi_edge_cases_async_widget_jni_cb, ctx); -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_makeWidgetJni(JNIEnv* env, jclass clazz, jstring label) { - weaveffi_error err = {0, NULL, NULL, 0}; - char* label_chars = weaveffi_jni_string_to_utf8(env, label); - weaveffi_edge_cases_Widget* rv = weaveffi_edge_cases_make_widget(label_chars, &err); - free(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 = weaveffi_jni_utf8_to_string(env, rv); - weaveffi_free_string(rv); - return out; -} - -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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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); - free(ctx); - weaveffi_jni_detach(attached); -} - -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)); - 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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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); - free(ctx); - weaveffi_jni_detach(attached); -} - -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)); - 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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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); - free(ctx); - weaveffi_jni_detach(attached); -} - -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)); - 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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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); - free(ctx); - weaveffi_jni_detach(attached); -} - -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)); - 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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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); - free(ctx); - weaveffi_jni_detach(attached); -} - -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)); - 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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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 boxed_cls = (*env)->FindClass(env, "java/lang/Integer"); - jobject boxed = (*env)->CallStaticObjectMethod(env, boxed_cls, (*env)->GetStaticMethodID(env, boxed_cls, "valueOf", "(I)Ljava/lang/Integer;"), (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); - free(ctx); - weaveffi_jni_detach(attached); -} - -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)); - 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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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 = (jobject)weaveffi_jni_utf8_to_string(env, result); - 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); - free(ctx); - weaveffi_jni_detach(attached); -} - -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)); - ctx->callback = (*env)->NewGlobalRef(env, callback); - char* class_chars = weaveffi_jni_string_to_utf8(env, class); - weaveffi_edge_cases_async_keywords_async(class_chars, (int32_t)import, weaveffi_edge_cases_async_keywords_jni_cb, ctx); - free(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}; - char* class_chars = weaveffi_jni_string_to_utf8(env, class); - 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); - free(class_chars); - (*env)->ReleaseByteArrayElements(env, import, import_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jstring out = weaveffi_jni_utf8_to_string(env, rv); - 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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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); - free(ctx); - weaveffi_jni_detach(attached); -} - -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)); - 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 = weaveffi_jni_utf8_to_string(env, rv); - weaveffi_free_string(rv); - return out; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Widget_nativeClone(JNIEnv* env, jclass clazz, jlong handle) { - return (jlong)(intptr_t)weaveffi_edge_cases_Widget_clone((const weaveffi_edge_cases_Widget*)(intptr_t)handle); -} - -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}; - char* path_chars = weaveffi_jni_string_to_utf8(env, path); - weaveffi_edge_cases_Vault* rv = weaveffi_edge_cases_Vault_open(path_chars, &err); - free(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 jlong JNICALL Java_com_weaveffi_Vault_nativeClone(JNIEnv* env, jclass clazz, jlong handle) { - return (jlong)(intptr_t)weaveffi_edge_cases_Vault_clone((const weaveffi_edge_cases_Vault*)(intptr_t)handle); -} - -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_EdgeCasesWidgetStreamIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_edge_cases_WidgetStreamIterator* _iter = (weaveffi_edge_cases_WidgetStreamIterator*)(intptr_t)handle; - weaveffi_edge_cases_Widget* _item = (weaveffi_edge_cases_Widget*)0; - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t _has = weaveffi_edge_cases_WidgetStreamIterator_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/Long"); - jobject _jitem = (*env)->CallStaticObjectMethod(env, _jitem_cls, (*env)->GetStaticMethodID(env, _jitem_cls, "valueOf", "(J)Ljava/lang/Long;"), (jlong)(intptr_t)_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_EdgeCasesWidgetStreamIterator_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_edge_cases_WidgetStreamIterator_destroy((weaveffi_edge_cases_WidgetStreamIterator*)(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 = weaveffi_jni_utf8_to_string(env, _item); - 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/kotlin_edge_cases__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index e107e3e8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,904 +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 { WeaveNativeLibrary.ensureLoaded() } - - @Volatile private var callbackExceptionHandler: ((Throwable) -> Unit)? = null - - /** - * Installs a handler for exceptions thrown while an async result is - * delivered on a native producer thread. 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. Exceptions thrown by callback interface methods are - * not routed here: they abort the producer call that invoked them and - * surface to the original caller as a [WeaveFFIException] with code -4. - */ - @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 - } - - /** - * 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()) } } - /** - * Two callback interface parameters on one function - */ - @JvmStatic external fun registerBatch(listener: BatchListener, keyword: Keyword): Unit - /** - * Objects in every buffered position - */ - @JvmStatic private external fun widgetsEverywhereJni(list: ByteArray, byName: ByteArray, sparse: ByteArray, holder: ByteArray): ByteArray - @JvmStatic fun widgetsEverywhere(list: List, byName: Map, sparse: List, holder: WidgetHolder): WidgetHolder = weaveDecode(widgetsEverywhereJni(weaveEncode { w -> w.writeList(list) { v0 -> w.writeI64(v0.cloneHandle()) } }, weaveEncode { w -> w.writeMap(byName, { k0 -> w.writeString(k0) }, { v0 -> w.writeI64(v0.cloneHandle()) }) }, weaveEncode { w -> w.writeList(sparse) { v0 -> w.writeOptional(v0) { v1 -> w.writeI64(v1.cloneHandle()) } } }, weaveEncode { w -> packWidgetHolder(w, holder) })) { r -> unpackWidgetHolder(r) } - /** - * Iterator of objects - */ - @JvmStatic private external fun widgetStreamJni(): Long - @JvmStatic fun widgetStream(): Iterator = EdgeCasesWidgetStreamIterator(widgetStreamJni()) - /** - * Async returning an object - */ - @JvmStatic private external fun asyncWidgetAsync(callback: Any) - @JvmStatic suspend fun asyncWidget(): Widget { - val raw: Long = suspendCancellableCoroutine { cont -> - asyncWidgetAsync(WeaveContinuation(cont) { code, message, payload -> EdgeErrorsException.fromCode(code, message, payload) }) - } - return Widget.fromHandle(raw) - } - /** - * Factory for an interface with no constructor - */ - @JvmStatic private external fun makeWidgetJni(label: String): Long - @JvmStatic fun makeWidget(label: String): Widget = Widget.fromHandle(makeWidgetJni(label)) - /** - * Optional interface return - */ - @JvmStatic private external fun findWidgetJni(id: Long): Long? - @JvmStatic fun findWidget(id: Long): Widget? = findWidgetJni(id)?.let { Widget.fromHandle(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) - /** - * 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(), -) - -/** Record carrying objects in every buffered position */ -data class WidgetHolder(val primary: Widget, val fallback: Widget?, val extras: List, val by_name: Map) - -internal fun packWidgetHolder(w: WeaveBufferWriter, v: WidgetHolder) { - w.writeI64(v.primary.cloneHandle()) - w.writeOptional(v.fallback) { v0 -> w.writeI64(v0.cloneHandle()) } - w.writeList(v.extras) { v0 -> w.writeI64(v0.cloneHandle()) } - w.writeMap(v.by_name, { k0 -> w.writeString(k0) }, { v0 -> w.writeI64(v0.cloneHandle()) }) -} - -internal fun unpackWidgetHolder(r: WeaveBufferReader): WidgetHolder = WidgetHolder( - Widget.fromHandle(r.readObject()), - r.readOptional { Widget.fromHandle(r.readObject()) }, - r.readList { Widget.fromHandle(r.readObject()) }, - r.readMap({ r.readString() }, { Widget.fromHandle(r.readObject()) }), -) - -/** Callback interface carrying composite payloads and objects */ -interface BatchListener { - /** - * Composite payloads cross as value buffers - */ - fun onBatch(items: List, raw: ByteArray, first: Outer?, mode: Mode) - /** - * Objects cross as strong references the consumer must release - */ - fun onWidget(widget: Widget, fallback: Widget?, children: List) - fun onTick() - /** - * Scalar return travels back through the vtable - */ - fun shouldContinue(class_: Int): Boolean - /** - * Enum return travels back as its integer discriminant - */ - fun pickMode(): Mode -} - -/** JNI dispatch shims for [BatchListener]; the native vtable trampolines call these, nothing else does. */ -internal object BatchListenerJni { - @JvmStatic fun onBatch(impl: BatchListener, items: ByteArray, raw: ByteArray, first: ByteArray, mode: Int) { impl.onBatch(weaveDecode(items) { r -> r.readList { unpackOuter(r) } }, raw, weaveDecode(first) { r -> r.readOptional { unpackOuter(r) } }, Mode.fromValue(mode)) } - @JvmStatic fun onWidget(impl: BatchListener, widget: Long, fallback: Long, children: ByteArray) { impl.onWidget(Widget.fromHandle(widget), fallback.takeIf { it != 0L }?.let { Widget.fromHandle(it) }, weaveDecode(children) { r -> r.readList { Widget.fromHandle(r.readObject()) } }) } - @JvmStatic fun onTick(impl: BatchListener) { impl.onTick() } - @JvmStatic fun shouldContinue(impl: BatchListener, class_: Int): Boolean = impl.shouldContinue(class_) - @JvmStatic fun pickMode(impl: BatchListener): Int = impl.pickMode().value -} - -/** Callback interface whose name and methods are keywords */ -interface Keyword { - fun delete(self: Int) - fun import(): Int -} - -/** JNI dispatch shims for [Keyword]; the native vtable trampolines call these, nothing else does. */ -internal object KeywordJni { - @JvmStatic fun delete(impl: Keyword, self: Int) { impl.delete(self) } - @JvmStatic fun import(impl: Keyword): Int = impl.import() -} - -/** Interface with no constructor; instances come from make_widget */ -class Widget private constructor(handle: Long) : AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - - /** The native pointer, borrowed for one call; throws after [close]. */ - internal val handle: Long get() = ref.get() - - /** A new strong reference, written into value buffers as an object token. */ - internal fun cloneHandle(): Long = nativeClone(ref.get()) - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - /** Adopts one strong native reference (a constructor return, a `_clone`, or a buffer token) into a wrapper that owes its `_destroy`. */ - internal fun fromHandle(handle: Long): Widget = Widget(handle) - @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 nativeClone(handle: Long): Long - @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) - - /** Releases this wrapper's native reference; safe to call more than once. */ - override fun close() { - cleanable.clean() - } -} - -/** Deprecated interface with a throwing constructor */ -class Vault private constructor(handle: Long) : AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - - /** The native pointer, borrowed for one call; throws after [close]. */ - internal val handle: Long get() = ref.get() - - /** A new strong reference, written into value buffers as an object token. */ - internal fun cloneHandle(): Long = nativeClone(ref.get()) - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - /** Adopts one strong native reference (a constructor return, a `_clone`, or a buffer token) into a wrapper that owes its `_destroy`. */ - internal fun fromHandle(handle: Long): Vault = Vault(handle) - @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 nativeClone(handle: Long): Long - @JvmStatic private external fun nativeDestroy(handle: Long) - - /** - * Fails when the vault is busy - */ - fun open(path: String): Vault = Vault.fromHandle(nativeOpen(path)) - - /** - * Throwing static - */ - fun probe(): Boolean = nativeProbe() - } - - fun peek(): Widget? = nativePeek(handle)?.let { Widget.fromHandle(it) } - - /** Releases this wrapper's native reference; safe to call more than once. */ - override fun close() { - cleanable.clean() - } -} - -/** - * A lazy iterator over the `Widget` elements streamed by [widgetStream]. 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 `Cleaner` if the iterator is abandoned, whichever comes first. - */ -class EdgeCasesWidgetStreamIterator internal constructor(handle: Long) : Iterator, AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (ref.peek() == 0L) return false - val slot = nativeNext(ref.get()) - if (slot == null) { - close() - return false - } - nextSlot = slot - return true - } - - override fun next(): Widget { - if (!hasNext()) throw NoSuchElementException() - val raw = nextSlot!![0] - nextSlot = null - return Widget.fromHandle(raw as Long) - } - - /** Releases the native iterator; safe to call more than once. */ - override fun close() { - cleanable.clean() - } - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - @JvmStatic private external fun nativeNext(handle: Long): Array? - @JvmStatic private external fun nativeDestroy(handle: Long) - } -} - -/** - * 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 `Cleaner` if the iterator is abandoned, whichever comes first. - */ -class EdgeCasesStringStreamIterator internal constructor(handle: Long) : Iterator, AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (ref.peek() == 0L) return false - val slot = nativeNext(ref.get()) - 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 - } - - /** Releases the native iterator; safe to call more than once. */ - override fun close() { - cleanable.clean() - } - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - @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 `Cleaner` if the iterator is abandoned, whichever comes first. - */ -class EdgeCasesScalarStreamIterator internal constructor(handle: Long) : Iterator, AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (ref.peek() == 0L) return false - val slot = nativeNext(ref.get()) - 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 - } - - /** Releases the native iterator; safe to call more than once. */ - override fun close() { - cleanable.clean() - } - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - @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 `Cleaner` if the iterator is abandoned, whichever comes first. - */ -class EdgeCasesModeStreamIterator internal constructor(handle: Long) : Iterator, AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (ref.peek() == 0L) return false - val slot = nativeNext(ref.get()) - 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) - } - - /** Releases the native iterator; safe to call more than once. */ - override fun close() { - cleanable.clean() - } - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - @JvmStatic private external fun nativeNext(handle: Long): Array? - @JvmStatic private external fun nativeDestroy(handle: Long) - } -} - -/** Generic WeaveFFI failure: panics, marshalling errors, callback implementation failures (code -4), 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) - } - } -} - -/** Loads the native library once; every companion `init` calls [ensureLoaded]. */ -internal object WeaveNativeLibrary { - init { System.loadLibrary("weaveffi") } - - fun ensureLoaded() {} -} - -/** - * One strong native reference, released exactly once. Wrappers register an - * instance with [weaveCleaner] so an unreachable, never-closed wrapper still - * releases its reference; an explicit `close()` runs the same action first - * and the cleaner then does nothing. - */ -internal class WeaveNativeRef(handle: Long, private val release: (Long) -> Unit) : Runnable { - private val handle = java.util.concurrent.atomic.AtomicLong(handle) - - /** The live pointer, borrowed for the duration of one native call. */ - fun get(): Long { - val h = handle.get() - check(h != 0L) { "WeaveFFI object used after close()" } - return h - } - - /** The live pointer, or `0L` once released. */ - fun peek(): Long = handle.get() - - override fun run() { - val h = handle.getAndSet(0L) - if (h != 0L) release(h) - } -} - -/** The process-wide cleaner backing every generated wrapper's disposal backstop. */ -internal val weaveCleaner: java.lang.ref.Cleaner = java.lang.ref.Cleaner.create() - -/** 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()) - /** An object token: one strong reference the caller adopts. Zero is never valid here. */ - fun readObject(): Long { - val token = readI64() - if (token == 0L) malformed("null object token") - return token - } - 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/kotlin_edge_cases__src_main_kotlin_edge_cases_Async_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Async_kt.snap new file mode 100644 index 00000000..3ca21041 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Async_kt.snap @@ -0,0 +1,124 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package edge_cases + +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Bridges one native async call to a suspended coroutine. The JNI shim pins + * this object with a global reference and calls exactly one `on*` method, + * from whichever producer thread completes the call. + */ +internal class NativeCompletion( + private val cont: CancellableContinuation, + private val domain: Int, + private val convert: (Any?) -> T, + cancellable: Boolean, +) { + // The native cancel token, or 0 when the call isn't cancellable or has + // completed. Guarded by `this` so a late cancel never touches a freed + // token; a completion that lands inside `cancel` (on the same thread) + // defers the release until the cancel returns. + private var token: Long = if (cancellable) JniBridge.cancel_token_create() else 0L + private var cancelling = false + private var released = false + + val cancelToken: Long + get() = synchronized(this) { token } + + fun cancel() { + synchronized(this) { + if (token == 0L) return + cancelling = true + try { + JniBridge.cancel_token_cancel(token) + } finally { + cancelling = false + } + if (released) destroyToken() + } + } + + fun releaseToken() { + synchronized(this) { + if (cancelling) released = true else destroyToken() + } + } + + private fun destroyToken() { + if (token != 0L) JniBridge.cancel_token_destroy(token) + token = 0L + } + + fun onUnit() = succeed(Unit) + + fun onBoolean(value: Boolean) = succeed(value) + + fun onByte(value: Byte) = succeed(value) + + fun onShort(value: Short) = succeed(value) + + fun onInt(value: Int) = succeed(value) + + fun onLong(value: Long) = succeed(value) + + fun onFloat(value: Float) = succeed(value) + + fun onDouble(value: Double) = succeed(value) + + fun onBytes(value: ByteArray) = succeed(value) + + fun onError(code: Int, message: ByteArray, payload: ByteArray?) { + releaseToken() + val error = if (code == -5) { + CancellationException(decodeUtf8(message)) + } else { + JniBridge.error(domain, code, message, payload) + } + cont.resumeWithException(error) + } + + private fun succeed(raw: Any?) { + releaseToken() + val result = try { + convert(raw) + } catch (t: Throwable) { + cont.resumeWithException(t) + return + } + cont.resume(result) + } +} + +/** + * Starts a native async call and suspends until it completes. [launch] gets + * the cancel token (0 unless [cancellable]) and the completion to hand to the + * shim. Cancelling the coroutine cancels the native call; a cancelled call + * (code -5) resumes with [CancellationException]. + */ +internal suspend fun awaitNative( + cancellable: Boolean, + domain: Int, + convert: (Any?) -> T, + launch: (Long, NativeCompletion) -> Unit, +): T = suspendCancellableCoroutine { cont -> + val completion = NativeCompletion(cont, domain, convert, cancellable) + cont.invokeOnCancellation { completion.cancel() } + try { + launch(completion.cancelToken, completion) + } catch (t: Throwable) { + completion.releaseToken() + throw t + } +} + +// END Async.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Buffers_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Buffers_kt.snap new file mode 100644 index 00000000..2fc4f91c --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Buffers_kt.snap @@ -0,0 +1,255 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package edge_cases + +/** + * Growable little-endian writer for the value-buffer wire format that + * records, rich enums, optionals, lists, and maps cross the ABI in. + */ +internal class BufferWriter { + private var buf = ByteArray(64) + private var len = 0 + private var minted: ArrayList Unit>>? = null + + private fun reserve(extra: Int) { + if (len + extra <= buf.size) return + var cap = buf.size * 2 + while (cap < len + extra) cap *= 2 + buf = buf.copyOf(cap) + } + + fun toByteArray(): ByteArray = buf.copyOf(len) + + fun writeBool(v: Boolean) { + reserve(1) + buf[len++] = if (v) 1 else 0 + } + + fun writeI8(v: Byte) { + reserve(1) + buf[len++] = v + } + + fun writeU8(v: Byte) = writeI8(v) + + fun writeI16(v: Short) { + reserve(2) + val b = v.toInt() + buf[len++] = b.toByte() + buf[len++] = (b shr 8).toByte() + } + + fun writeU16(v: Short) = writeI16(v) + + fun writeI32(v: Int) { + reserve(4) + buf[len++] = v.toByte() + buf[len++] = (v shr 8).toByte() + buf[len++] = (v shr 16).toByte() + buf[len++] = (v shr 24).toByte() + } + + fun writeU32(v: Long) = writeI32(v.toInt()) + + fun writeI64(v: Long) { + writeI32(v.toInt()) + writeI32((v ushr 32).toInt()) + } + + fun writeU64(v: Long) = writeI64(v) + + fun writeF32(v: Float) = writeI32(v.toRawBits()) + + fun writeF64(v: Double) = writeI64(v.toRawBits()) + + fun writeString(v: String) = writeBytes(encodeUtf8(v)) + + fun writeBytes(v: ByteArray) { + writeI32(v.size) + reserve(v.size) + v.copyInto(buf, len) + len += v.size + } + + /** + * An object token: one new strong reference the reader adopts, which + * [abandon] releases with [release] if the encoding fails. + */ + fun writeObject(token: Long, release: (Long) -> Unit) { + val list = minted ?: ArrayList Unit>>().also { minted = it } + list.add(token to release) + writeI64(token) + } + + /** Releases every object token written so far, for an encoding that failed midway. */ + fun abandon() { + minted?.forEach { (token, release) -> release(token) } + minted = null + } + + inline fun writeOptional(v: T?, write: (T) -> Unit) { + if (v == null) { + writeBool(false) + } else { + writeBool(true) + write(v) + } + } + + inline fun writeList(v: List, write: (T) -> Unit) { + writeI32(v.size) + for (e in v) write(e) + } + + inline fun writeMap(v: Map, writeKey: (K) -> Unit, writeValue: (V) -> Unit) { + writeI32(v.size) + for ((k, e) in v) { + writeKey(k) + writeValue(e) + } + } +} + +/** + * Validating little-endian reader for the value-buffer wire format. A + * truncated buffer, an invalid flag byte, a zero object token, an oversized + * length, or trailing bytes raise [FfiException] code -3. + */ +internal class BufferReader(private val buf: ByteArray) { + private var pos = 0 + + private fun malformed(detail: String): Nothing = + throw FfiException(-3, "malformed value buffer: $detail") + + private fun take(n: Int): Int { + if (n > buf.size - pos) malformed("truncated buffer") + val at = pos + pos += n + return at + } + + fun readBool(): Boolean = when (buf[take(1)].toInt()) { + 0 -> false + 1 -> true + else -> malformed("invalid bool byte") + } + + fun readI8(): Byte = buf[take(1)] + + fun readU8(): Byte = readI8() + + fun readI16(): Short { + val at = take(2) + return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() + } + + fun readU16(): Short = readI16() + + 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 readU64(): Long = readI64() + + fun readF32(): Float = Float.fromBits(readI32()) + + fun readF64(): Double = Double.fromBits(readI64()) + + private fun readLen(): Int { + val n = readI32() + if (n < 0 || n > buf.size - pos) malformed("length prefix exceeds remaining bytes") + return n + } + + fun readString(): String { + val n = readLen() + val at = take(n) + return try { + Charsets.UTF_8.newDecoder().decode(java.nio.ByteBuffer.wrap(buf, at, n)).toString() + } catch (e: java.nio.charset.CharacterCodingException) { + malformed("string is not valid UTF-8") + } + } + + fun readBytes(): ByteArray { + val n = readLen() + val at = take(n) + return buf.copyOfRange(at, at + n) + } + + /** An object token: one strong reference the caller adopts. */ + fun readObject(): Long { + val token = readI64() + if (token == 0L) malformed("null object token") + return token + } + + inline fun readOptional(read: () -> T): T? = if (readBool()) read() else null + + inline fun readList(read: () -> T): List { + val n = readCount() + val out = ArrayList(minOf(n, 4096)) + repeat(n) { out.add(read()) } + return out + } + + inline fun readMap(readKey: () -> K, readValue: () -> V): Map { + val n = readCount() + val out = LinkedHashMap(minOf(n, 4096)) + repeat(n) { + val k = readKey() + out[k] = readValue() + } + return out + } + + /** An element count; elements may encode to zero bytes, so only the sign is checked. */ + fun readCount(): Int { + val n = readI32() + if (n < 0) malformed("negative element count") + return n + } + + fun expectEnd() { + if (pos != buf.size) malformed("trailing bytes after value") + } +} + +/** Encodes one value into a fresh value buffer. */ +internal inline fun encodeBuffer(write: (BufferWriter) -> Unit): ByteArray { + val w = BufferWriter() + try { + write(w) + } catch (t: Throwable) { + w.abandon() + throw t + } + return w.toByteArray() +} + +/** Decodes one value from [bytes], rejecting malformed or trailing bytes. */ +internal inline fun decodeBuffer(bytes: ByteArray, read: (BufferReader) -> T): T { + val r = BufferReader(bytes) + val v = read(r) + r.expectEnd() + return v +} + +// END Buffers.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_EdgeCases_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_EdgeCases_kt.snap new file mode 100644 index 00000000..2d1dc03c --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_EdgeCases_kt.snap @@ -0,0 +1,456 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +@file:Suppress("DEPRECATION") + +package edge_cases + +/** Errors of the `EdgeErrors` domain. */ +sealed class EdgeErrorsException(code: Int, message: String) : FfiException(code, message) { + class Busy(message: String = "Resource is busy") : EdgeErrorsException(7, message) + + internal companion object { + /** The exception for a raw code and its serialized payload. */ + fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) { + 7 -> Busy(message) + else -> FfiException(code, message) + } + } +} + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +@Deprecated("Use Mode instead") +enum class Legacy(val value: Int) { + None(0), + Default(5), + Class(10); + + companion object { + /** The variant whose ABI value is [value]. */ + fun fromValue(value: Int): Legacy = entries.firstOrNull { it.value == value } + ?: throw FfiException(-3, "unknown Legacy value $value") + } +} + +enum class Mode(val value: Int) { + Fast(1), + Safe(2); + + companion object { + /** The variant whose ABI value is [value]. */ + fun fromValue(value: Int): Mode = entries.firstOrNull { it.value == value } + ?: throw FfiException(-3, "unknown Mode value $value") + } +} + +data class Leaf(val weight: Float) + +internal fun packLeaf(_w: BufferWriter, _v: Leaf) { + _w.writeF32(_v.weight) +} + +internal fun unpackLeaf(_r: BufferReader): Leaf = Leaf( + _r.readF32(), +) + +/** Middle layer of a three-deep struct nesting */ +data class Inner(val leaf: Leaf, val leaves: List) + +internal fun packInner(_w: BufferWriter, _v: Inner) { + packLeaf(_w, _v.leaf) + _w.writeList(_v.leaves) { _v0 -> packLeaf(_w, _v0) } +} + +internal fun unpackInner(_r: BufferReader): 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: BufferWriter, _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.writeU8(_v.v2) +} + +internal fun unpackOuter(_r: BufferReader): 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.readU8(), +) + +/** Every field is a reserved word somewhere */ +data class Keywords(val class_: Int, val def: Int, val import: Int, val from: Int, val func: Int, val range: Int, val register: Int, val volatile: Int, val new: Int, val default: Int, val end: Int, val in_: Int, val is_: Int, val int: Int, val var_: Int, val val_: Int, val self: Int, val this_: Int, val object_: Int, val string: Int, val case: Int, val select: Int, val switch: Int, val void: Int, val union: Int, val namespace: Int, val operator: Int, val template: Int, val package_: Int, val protocol: Int, val guard: Int, val defer: Int, val inout: Int, val internal: Int, val when_: Int, val fun_: Int, val data: Int, val late: Int, val part: Int, val out: Int, val ref: Int, val event: Int, val base: Int, val lambda: Int, val yield: Int, val nil: Int, val null_: Int, val begin: Int, val rescue: Int, val module: Int, val chan: Int, val go: Int, val let: Int, val export: Int, val delete: Int, val typedef: Int, val sizeof: Int, val static: Int, val super_: Int) + +internal fun packKeywords(_w: BufferWriter, _v: Keywords) { + _w.writeI32(_v.class_) + _w.writeI32(_v.def) + _w.writeI32(_v.import) + _w.writeI32(_v.from) + _w.writeI32(_v.func) + _w.writeI32(_v.range) + _w.writeI32(_v.register) + _w.writeI32(_v.volatile) + _w.writeI32(_v.new) + _w.writeI32(_v.default) + _w.writeI32(_v.end) + _w.writeI32(_v.in_) + _w.writeI32(_v.is_) + _w.writeI32(_v.int) + _w.writeI32(_v.var_) + _w.writeI32(_v.val_) + _w.writeI32(_v.self) + _w.writeI32(_v.this_) + _w.writeI32(_v.object_) + _w.writeI32(_v.string) + _w.writeI32(_v.case) + _w.writeI32(_v.select) + _w.writeI32(_v.switch) + _w.writeI32(_v.void) + _w.writeI32(_v.union) + _w.writeI32(_v.namespace) + _w.writeI32(_v.operator) + _w.writeI32(_v.template) + _w.writeI32(_v.package_) + _w.writeI32(_v.protocol) + _w.writeI32(_v.guard) + _w.writeI32(_v.defer) + _w.writeI32(_v.inout) + _w.writeI32(_v.internal) + _w.writeI32(_v.when_) + _w.writeI32(_v.fun_) + _w.writeI32(_v.data) + _w.writeI32(_v.late) + _w.writeI32(_v.part) + _w.writeI32(_v.out) + _w.writeI32(_v.ref) + _w.writeI32(_v.event) + _w.writeI32(_v.base) + _w.writeI32(_v.lambda) + _w.writeI32(_v.yield) + _w.writeI32(_v.nil) + _w.writeI32(_v.null_) + _w.writeI32(_v.begin) + _w.writeI32(_v.rescue) + _w.writeI32(_v.module) + _w.writeI32(_v.chan) + _w.writeI32(_v.go) + _w.writeI32(_v.let) + _w.writeI32(_v.export) + _w.writeI32(_v.delete) + _w.writeI32(_v.typedef) + _w.writeI32(_v.sizeof) + _w.writeI32(_v.static) + _w.writeI32(_v.super_) +} + +internal fun unpackKeywords(_r: BufferReader): Keywords = Keywords( + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), + _r.readI32(), +) + +/** Deprecated struct */ +@Deprecated("Use Outer instead") +data class Retired(val id: Long) + +internal fun packRetired(_w: BufferWriter, _v: Retired) { + _w.writeI64(_v.id) +} + +internal fun unpackRetired(_r: BufferReader): Retired = Retired( + _r.readI64(), +) + +/** Record carrying objects in every buffered position */ +data class WidgetHolder(val primary: Widget, val fallback: Widget?, val extras: List, val by_name: Map) + +internal fun packWidgetHolder(_w: BufferWriter, _v: WidgetHolder) { + _w.writeObject(_v.primary.cloneHandle(), JniBridge::edge_cases_Widget_destroy) + _w.writeOptional(_v.fallback) { _v0 -> _w.writeObject(_v0.cloneHandle(), JniBridge::edge_cases_Widget_destroy) } + _w.writeList(_v.extras) { _v0 -> _w.writeObject(_v0.cloneHandle(), JniBridge::edge_cases_Widget_destroy) } + _w.writeMap(_v.by_name, { _k0 -> _w.writeString(_k0) }, { _v0 -> _w.writeObject(_v0.cloneHandle(), JniBridge::edge_cases_Widget_destroy) }) +} + +internal fun unpackWidgetHolder(_r: BufferReader): WidgetHolder = WidgetHolder( + Widget.fromHandle(_r.readObject()), + _r.readOptional { Widget.fromHandle(_r.readObject()) }, + _r.readList { Widget.fromHandle(_r.readObject()) }, + _r.readMap({ _r.readString() }, { Widget.fromHandle(_r.readObject()) }), +) + +/** Callback interface carrying composite payloads and objects */ +interface BatchListener { + /** Composite payloads cross as value buffers */ + fun onBatch(items: List, raw: ByteArray, first: Outer?, mode: Mode) + + /** Objects cross as strong references the consumer must release */ + fun onWidget(widget: Widget, fallback: Widget?, children: List) + + fun onTick() + + /** Scalar return travels back through the vtable */ + fun shouldContinue(class_: Int): Boolean + + /** Enum return travels back as its integer discriminant */ + fun pickMode(): Mode +} + +/** Callback interface whose name and methods are keywords */ +interface Keyword { + fun delete(self: Int) + + fun import(): Int +} + +/** Interface with no constructor; instances come from make_widget */ +class Widget private constructor(address: Long) : AutoCloseable { + internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::edge_cases_Widget_destroy)) + + /** A new strong reference to this object, as written into a value buffer. */ + internal fun cloneHandle(): Long = handle.borrow { JniBridge.edge_cases_Widget_clone(it) } + + /** Void method that can fail */ + fun refresh() { + handle.borrow { _self -> + JniBridge.edge_cases_Widget_refresh(_self) + } + } + + fun rename(class_: String, import: String?): String = handle.borrow { _self -> + decodeUtf8(JniBridge.edge_cases_Widget_rename(_self, encodeUtf8(class_), encodeBuffer { _w -> _w.writeOptional(import) { _v0 -> _w.writeString(_v0) } })) + } + + /** Async method with a struct result */ + suspend fun fetch(id: Long): Outer = awaitNative(false, 1, { decodeBuffer(it as ByteArray) { _r -> unpackOuter(_r) } }) { _, _done -> + handle.borrow { _self -> + JniBridge.edge_cases_Widget_fetch(_self, id, _done) + } + } + + @Deprecated("Use rename instead") + fun oldName(): String = handle.borrow { _self -> + decodeUtf8(JniBridge.edge_cases_Widget_old_name(_self)) + } + + /** Releases this wrapper's native reference; safe to call more than once. */ + override fun close() = handle.close() + + companion object { + /** Adopts one strong reference (a return value or a buffer token). */ + internal fun fromHandle(address: Long): Widget = Widget(address) + + /** [fromHandle], with address 0 meaning no object. */ + internal fun fromHandleOrNull(address: Long): Widget? = if (address == 0L) null else Widget(address) + } +} + +/** Deprecated interface with a throwing constructor */ +@Deprecated("Use Widget instead") +class Vault private constructor(address: Long) : AutoCloseable { + internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::edge_cases_Vault_destroy)) + + /** A new strong reference to this object, as written into a value buffer. */ + internal fun cloneHandle(): Long = handle.borrow { JniBridge.edge_cases_Vault_clone(it) } + + fun peek(): Widget? = handle.borrow { _self -> + Widget.fromHandleOrNull(JniBridge.edge_cases_Vault_peek(_self)) + } + + /** Releases this wrapper's native reference; safe to call more than once. */ + override fun close() = handle.close() + + companion object { + /** Adopts one strong reference (a return value or a buffer token). */ + internal fun fromHandle(address: Long): Vault = Vault(address) + + /** [fromHandle], with address 0 meaning no object. */ + internal fun fromHandleOrNull(address: Long): Vault? = if (address == 0L) null else Vault(address) + + /** Fails when the vault is busy */ + fun open(path: String): Vault = Vault.fromHandle(JniBridge.edge_cases_Vault_open(encodeUtf8(path))) + + /** Throwing static */ + fun probe(): Boolean = JniBridge.edge_cases_Vault_probe() + } +} + +object EdgeCases { + /** No parameters and no result */ + fun noop() { + JniBridge.edge_cases_noop() + } + + /** Function named after a keyword whose parameters are all keywords */ + fun select(class_: Int, from: String, func: List, range: Int?, type: Boolean): Int = JniBridge.edge_cases_select(class_, encodeUtf8(from), encodeBuffer { _w -> _w.writeList(func) { _v0 -> _w.writeString(_v0) } }, encodeBuffer { _w -> _w.writeOptional(range) { _v0 -> _w.writeI32(_v0) } }, type) + + /** Optional list of optional structs, the deepest legal nesting */ + fun nestedOptional(items: List?): List? = decodeBuffer(JniBridge.edge_cases_nested_optional(encodeBuffer { _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) } } } } + + fun optionalParams(text: String?, count: Int?, outer: Outer?, tags: List?, blob: ByteArray?, mode: Mode?): Mode? = decodeBuffer(JniBridge.edge_cases_optional_params(encodeBuffer { _w -> _w.writeOptional(text) { _v0 -> _w.writeString(_v0) } }, encodeBuffer { _w -> _w.writeOptional(count) { _v0 -> _w.writeI32(_v0) } }, encodeBuffer { _w -> _w.writeOptional(outer) { _v0 -> packOuter(_w, _v0) } }, encodeBuffer { _w -> _w.writeOptional(tags) { _v0 -> _w.writeList(_v0) { _v1 -> _w.writeString(_v1) } } }, encodeBuffer { _w -> _w.writeOptional(blob) { _v0 -> _w.writeBytes(_v0) } }, encodeBuffer { _w -> _w.writeOptional(mode) { _v0 -> _w.writeI32(_v0.value) } })) { _r -> _r.readOptional { Mode.fromValue(_r.readI32()) } } + + /** Two callback interface parameters on one function */ + fun registerBatch(listener: BatchListener, keyword: Keyword) { + JniBridge.edge_cases_register_batch(listener, keyword) + } + + /** Objects in every buffered position */ + fun widgetsEverywhere(list: List, byName: Map, sparse: List, holder: WidgetHolder): WidgetHolder = decodeBuffer(JniBridge.edge_cases_widgets_everywhere(encodeBuffer { _w -> _w.writeList(list) { _v0 -> _w.writeObject(_v0.cloneHandle(), JniBridge::edge_cases_Widget_destroy) } }, encodeBuffer { _w -> _w.writeMap(byName, { _k0 -> _w.writeString(_k0) }, { _v0 -> _w.writeObject(_v0.cloneHandle(), JniBridge::edge_cases_Widget_destroy) }) }, encodeBuffer { _w -> _w.writeList(sparse) { _v0 -> _w.writeOptional(_v0) { _v1 -> _w.writeObject(_v1.cloneHandle(), JniBridge::edge_cases_Widget_destroy) } } }, encodeBuffer { _w -> packWidgetHolder(_w, holder) })) { _r -> unpackWidgetHolder(_r) } + + /** Iterator of objects */ + fun widgetStream(): NativeIterator = NativeIterator(JniBridge.edge_cases_widget_stream(), JniBridge::edge_cases_WidgetStreamIterator_destroy) { _it -> + val _slot = LongArray(1) + if (JniBridge.edge_cases_WidgetStreamIterator_next(_it, _slot)) Widget.fromHandle(_slot[0]) else NativeIterator.DONE + } + + /** Async returning an object */ + suspend fun asyncWidget(): Widget = awaitNative(false, 1, { Widget.fromHandle(it as Long) }) { _, _done -> + JniBridge.edge_cases_async_widget(_done) + } + + /** Factory for an interface with no constructor */ + fun makeWidget(label: String): Widget = Widget.fromHandle(JniBridge.edge_cases_make_widget(encodeUtf8(label))) + + /** Optional interface return */ + fun findWidget(id: Long): Widget? = Widget.fromHandleOrNull(JniBridge.edge_cases_find_widget(id)) + + /** Interface parameter, including an optional one */ + fun inspectWidget(widget: Widget, fallback: Widget?): String = widget.handle.borrow { _h0 -> + fallback?.handle.borrowOrNull { _h1 -> + decodeUtf8(JniBridge.edge_cases_inspect_widget(_h0, _h1)) + } + } + + /** Iterator of strings */ + fun stringStream(): NativeIterator = NativeIterator(JniBridge.edge_cases_string_stream(), JniBridge::edge_cases_StringStreamIterator_destroy) { _it -> + val _item = JniBridge.edge_cases_StringStreamIterator_next(_it) + if (_item == null) NativeIterator.DONE else decodeUtf8(_item) + } + + /** Iterator of scalars */ + fun scalarStream(): NativeIterator = NativeIterator(JniBridge.edge_cases_scalar_stream(), JniBridge::edge_cases_ScalarStreamIterator_destroy) { _it -> + val _slot = IntArray(1) + if (JniBridge.edge_cases_ScalarStreamIterator_next(_it, _slot)) _slot[0] else NativeIterator.DONE + } + + /** Iterator of enums */ + fun modeStream(): NativeIterator = NativeIterator(JniBridge.edge_cases_mode_stream(), JniBridge::edge_cases_ModeStreamIterator_destroy) { _it -> + val _slot = IntArray(1) + if (JniBridge.edge_cases_ModeStreamIterator_next(_it, _slot)) Mode.fromValue(_slot[0]) else NativeIterator.DONE + } + + /** Async with no result */ + suspend fun asyncVoid() { + awaitNative(false, 0, { }) { _, _done -> + JniBridge.edge_cases_async_void(_done) + } + } + + suspend fun asyncBytes(size: Long): ByteArray = awaitNative(false, 0, { it as ByteArray }) { _, _done -> + JniBridge.edge_cases_async_bytes(size, _done) + } + + suspend fun asyncStruct(): Outer = awaitNative(false, 1, { decodeBuffer(it as ByteArray) { _r -> unpackOuter(_r) } }) { _, _done -> + JniBridge.edge_cases_async_struct(_done) + } + + suspend fun asyncList(): List = awaitNative(true, 1, { decodeBuffer(it as ByteArray) { _r -> _r.readList { unpackOuter(_r) } } }) { _token, _done -> + JniBridge.edge_cases_async_list(_token, _done) + } + + suspend fun asyncOptional(): Long? = awaitNative(false, 0, { decodeBuffer(it as ByteArray) { _r -> _r.readOptional { _r.readI64() } } }) { _, _done -> + JniBridge.edge_cases_async_optional(_done) + } + + suspend fun asyncEnum(): Mode = awaitNative(false, 0, { Mode.fromValue(it as Int) }) { _, _done -> + JniBridge.edge_cases_async_enum(_done) + } + + suspend fun asyncKeywords(class_: String, import: Int): String = awaitNative(false, 0, { decodeUtf8(it as ByteArray) }) { _, _done -> + JniBridge.edge_cases_async_keywords(encodeUtf8(class_), import, _done) + } + + fun keywordsRoundtrip(value: Keywords): Keywords = decodeBuffer(JniBridge.edge_cases_keywords_roundtrip(encodeBuffer { _w -> packKeywords(_w, value) })) { _r -> unpackKeywords(_r) } + + @Deprecated("Use Mode-based APIs") + fun legacyValue(value: Legacy): Retired = decodeBuffer(JniBridge.edge_cases_legacy_value(value.value)) { _r -> unpackRetired(_r) } +} + +// END EdgeCases.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_JniBridge_kt.snap new file mode 100644 index 00000000..6ff7f386 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_JniBridge_kt.snap @@ -0,0 +1,106 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +@file:Suppress("DEPRECATION") + +package edge_cases + +/** + * The JNI entry points of `libedge_cases_jni`: one native per C symbol, named after + * it, plus the hooks the shim calls back into. Internal plumbing for the + * public wrappers. + */ +internal object JniBridge { + init { + NativeLibrary.ensureLoaded() + } + + /** The exception for a native failure; domain 0 is the generic one. */ + @JvmStatic + @Suppress("UNUSED_PARAMETER") + fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): FfiException { + val text = decodeUtf8(message) + return when (domain) { + 1 -> EdgeErrorsException.fromCode(code, text, payload) + else -> FfiException(code, text) + } + } + + /** The message a failed callback reports to the producer (code -4). */ + @JvmStatic + fun describe(error: Throwable): ByteArray = encodeUtf8(error.toString()) + + @JvmStatic external fun debug_live(kind: Int): Long + @JvmStatic external fun cancel_token_create(): Long + @JvmStatic external fun cancel_token_cancel(token: Long) + @JvmStatic external fun cancel_token_destroy(token: Long) + @JvmStatic external fun edge_cases_Widget_clone(_self: Long): Long + @JvmStatic external fun edge_cases_Widget_destroy(_self: Long) + @JvmStatic external fun edge_cases_Vault_clone(_self: Long): Long + @JvmStatic external fun edge_cases_Vault_destroy(_self: Long) + @JvmStatic external fun edge_cases_noop() + @JvmStatic external fun edge_cases_select(class_: Int, from: ByteArray, func: ByteArray, range: ByteArray, type: Boolean): Int + @JvmStatic external fun edge_cases_nested_optional(items: ByteArray): ByteArray + @JvmStatic external fun edge_cases_optional_params(text: ByteArray, count: ByteArray, outer: ByteArray, tags: ByteArray, blob: ByteArray, mode: ByteArray): ByteArray + @JvmStatic external fun edge_cases_register_batch(listener: BatchListener, keyword: Keyword) + @JvmStatic external fun edge_cases_widgets_everywhere(list: ByteArray, byName: ByteArray, sparse: ByteArray, holder: ByteArray): ByteArray + @JvmStatic external fun edge_cases_widget_stream(): Long + @JvmStatic external fun edge_cases_WidgetStreamIterator_next(_iter: Long, _out: LongArray): Boolean + @JvmStatic external fun edge_cases_WidgetStreamIterator_destroy(_iter: Long) + @JvmStatic external fun edge_cases_async_widget(_done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_make_widget(label: ByteArray): Long + @JvmStatic external fun edge_cases_find_widget(id: Long): Long + @JvmStatic external fun edge_cases_inspect_widget(widget: Long, fallback: Long): ByteArray + @JvmStatic external fun edge_cases_string_stream(): Long + @JvmStatic external fun edge_cases_StringStreamIterator_next(_iter: Long): ByteArray? + @JvmStatic external fun edge_cases_StringStreamIterator_destroy(_iter: Long) + @JvmStatic external fun edge_cases_scalar_stream(): Long + @JvmStatic external fun edge_cases_ScalarStreamIterator_next(_iter: Long, _out: IntArray): Boolean + @JvmStatic external fun edge_cases_ScalarStreamIterator_destroy(_iter: Long) + @JvmStatic external fun edge_cases_mode_stream(): Long + @JvmStatic external fun edge_cases_ModeStreamIterator_next(_iter: Long, _out: IntArray): Boolean + @JvmStatic external fun edge_cases_ModeStreamIterator_destroy(_iter: Long) + @JvmStatic external fun edge_cases_async_void(_done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_async_bytes(size: Long, _done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_async_struct(_done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_async_list(_token: Long, _done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_async_optional(_done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_async_enum(_done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_async_keywords(class_: ByteArray, import: Int, _done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_keywords_roundtrip(value: ByteArray): ByteArray + @JvmStatic external fun edge_cases_legacy_value(value: Int): ByteArray + @JvmStatic external fun edge_cases_Widget_refresh(_self: Long) + @JvmStatic external fun edge_cases_Widget_rename(_self: Long, class_: ByteArray, import: ByteArray): ByteArray + @JvmStatic external fun edge_cases_Widget_fetch(_self: Long, id: Long, _done: NativeCompletion<*>) + @JvmStatic external fun edge_cases_Widget_old_name(_self: Long): ByteArray + @JvmStatic external fun edge_cases_Vault_open(path: ByteArray): Long + @JvmStatic external fun edge_cases_Vault_peek(_self: Long): Long + @JvmStatic external fun edge_cases_Vault_probe(): Boolean + + @JvmStatic + fun edge_cases_BatchListener_on_batch(_impl: BatchListener, items: ByteArray, raw: ByteArray, first: ByteArray, mode: Int) { _impl.onBatch(decodeBuffer(items) { _r -> _r.readList { unpackOuter(_r) } }, raw, decodeBuffer(first) { _r -> _r.readOptional { unpackOuter(_r) } }, Mode.fromValue(mode)) } + + @JvmStatic + fun edge_cases_BatchListener_on_widget(_impl: BatchListener, widget: Long, fallback: Long, children: ByteArray) { _impl.onWidget(Widget.fromHandle(widget), Widget.fromHandleOrNull(fallback), decodeBuffer(children) { _r -> _r.readList { Widget.fromHandle(_r.readObject()) } }) } + + @JvmStatic + fun edge_cases_BatchListener_on_tick(_impl: BatchListener) { _impl.onTick() } + + @JvmStatic + fun edge_cases_BatchListener_should_continue(_impl: BatchListener, class_: Int): Boolean = _impl.shouldContinue(class_) + + @JvmStatic + fun edge_cases_BatchListener_pick_mode(_impl: BatchListener): Int = _impl.pickMode().value + + @JvmStatic + fun edge_cases_Keyword_delete(_impl: Keyword, self: Int) { _impl.delete(self) } + + @JvmStatic + fun edge_cases_Keyword_import(_impl: Keyword): Int = _impl.import() +} + +// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Runtime_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Runtime_kt.snap new file mode 100644 index 00000000..788c6b10 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_edge_cases__src_main_kotlin_edge_cases_Runtime_kt.snap @@ -0,0 +1,212 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package edge_cases + +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * A failure reported by the native library: a producer panic (code -2), a + * marshalling failure (-3), a callback implementation that threw (-4), or any + * code these bindings don't know. Typed error domains subclass it. + */ +open class FfiException(val code: Int, message: String) : Exception(message) + +/** + * A lazy sequence streamed from the native library, one element per step. + * The native iterator is released when the sequence is exhausted, when + * [close] is called, or once this object becomes unreachable, whichever + * comes first. + */ +class NativeIterator internal constructor( + address: Long, + release: (Long) -> Unit, + private val step: (Long) -> Any?, +) : Iterator, AutoCloseable { + private val handle = NativeCleaner.register(this, NativeHandle(address, release)) + private var lookahead: Any? = EMPTY + + override fun hasNext(): Boolean { + if (lookahead !== EMPTY) return true + if (handle.isClosed) return false + val item = handle.borrow(step) + if (item === DONE) { + close() + return false + } + lookahead = item + return true + } + + override fun next(): T { + if (!hasNext()) throw NoSuchElementException() + val item = lookahead + lookahead = EMPTY + @Suppress("UNCHECKED_CAST") + return item as T + } + + /** Releases the native iterator; safe to call more than once. */ + override fun close() = handle.close() + + internal companion object { + /** What a step function returns once the native sequence is exhausted. */ + val DONE = Any() + private val EMPTY = Any() + } +} + +/** Loads the producer library and then the JNI shim, exactly once. */ +internal object NativeLibrary { + private const val LIBRARY = "edge_cases" + private const val JNI_LIBRARY = "edge_cases_jni" + + init { + val explicit = System.getenv("EDGE_CASES_LIBRARY") + if (!explicit.isNullOrEmpty()) { + System.load(explicit) + } else if (!loadBundled(LIBRARY)) { + // The shim's own link dependency usually finds the producer when + // it isn't on java.library.path, so a miss here is only fatal if + // loading the shim fails too. + try { + System.loadLibrary(LIBRARY) + } catch (ignored: UnsatisfiedLinkError) { + } + } + if (!loadBundled(JNI_LIBRARY)) System.loadLibrary(JNI_LIBRARY) + } + + fun ensureLoaded() {} + + /** + * Loads `base` from the `natives/-/` classpath resources a + * packaged desktop build bundles; false when it isn't bundled. + */ + private fun loadBundled(base: String): Boolean { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + val osId = when { + os.contains("mac") || os.contains("darwin") -> "darwin" + os.contains("win") -> "windows" + else -> "linux" + } + val archId = if (arch == "aarch64" || arch == "arm64") "arm64" else "x64" + val file = when (osId) { + "darwin" -> "lib$base.dylib" + "windows" -> "$base.dll" + else -> "lib$base.so" + } + val stream = NativeLibrary::class.java.getResourceAsStream("/natives/$osId-$archId/$file") ?: return false + val dir = java.nio.file.Files.createTempDirectory("$base-natives") + val target = dir.resolve(file) + stream.use { java.nio.file.Files.copy(it, target) } + target.toFile().deleteOnExit() + dir.toFile().deleteOnExit() + System.load(target.toAbsolutePath().toString()) + return true + } +} + +/** + * One strong reference to a native object, released exactly once: by + * [close], or by [NativeCleaner] once the owning wrapper is unreachable. + * Every call borrows the address for its duration, and a close that races an + * in-flight call defers the release until the last borrow ends, so the object + * is never freed while native code is using it. + */ +internal class NativeHandle(val address: Long, private val release: (Long) -> Unit) { + // Bit 0 is the closed flag; the remaining bits count in-flight borrows. + private val state = AtomicLong(0L) + + val isClosed: Boolean get() = state.get() and 1L != 0L + + fun acquire(): Long { + while (true) { + val s = state.get() + check(s and 1L == 0L) { "native object used after close()" } + if (state.compareAndSet(s, s + 2L)) return address + } + } + + fun unacquire() { + if (state.addAndGet(-2L) == 1L) release(address) + } + + fun close() { + while (true) { + val s = state.get() + if (s and 1L != 0L) return + if (state.compareAndSet(s, s or 1L)) { + if (s == 0L) release(address) + return + } + } + } +} + +/** Runs [block] with the native address borrowed for its duration. */ +internal inline fun NativeHandle.borrow(block: (Long) -> R): R { + val address = acquire() + try { + return block(address) + } finally { + unacquire() + } +} + +/** [borrow] for an optional object: a missing object lends address 0. */ +internal inline fun NativeHandle?.borrowOrNull(block: (Long) -> R): R = + if (this == null) block(0L) else borrow(block) + +/** + * Releases the native reference of every wrapper that becomes unreachable + * without being closed. A daemon thread drains a phantom-reference queue, so + * this works on every Android API level and every JVM. + */ +internal object NativeCleaner { + private val queue = ReferenceQueue() + private val pending: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) + + private class Entry(owner: Any, val handle: NativeHandle, queue: ReferenceQueue) : + PhantomReference(owner, queue) + + init { + val thread = Thread({ drain() }, "edge_cases-cleaner") + thread.isDaemon = true + thread.start() + } + + /** Releases [handle] once [owner] is unreachable; returns [handle]. */ + fun register(owner: Any, handle: NativeHandle): NativeHandle { + pending.add(Entry(owner, handle, queue)) + return handle + } + + private fun drain() { + while (true) { + val entry = queue.remove() as Entry + pending.remove(entry) + try { + entry.handle.close() + } catch (_: Throwable) { + } + } + } +} + +/** UTF-8 bytes of [value], for a string crossing into native code. */ +internal fun encodeUtf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8) + +/** The string a native UTF-8 byte sequence spells. */ +internal fun decodeUtf8(bytes: ByteArray): String = String(bytes, Charsets.UTF_8) + +// END Runtime.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__build_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__build_gradle_kts.snap index 374b107b..4e443da1 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__build_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__build_gradle_kts.snap @@ -1,36 +1,63 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") version "1.9.22" + id("com.android.library") version "8.7.3" + id("org.jetbrains.kotlin.android") version "2.0.21" + `maven-publish` } +group = "kitchen_sink" +version = "0.1.0" + android { - namespace = "com.weaveffi" - compileSdk = 34 + namespace = "kitchen_sink" + compileSdk = 35 defaultConfig { - // java.lang.ref.Cleaner, which backs every wrapper's disposal, is API 33. - minSdk = 33 - externalNativeBuild { - cmake { - cppFlags("") - } - } + minSdk = 21 + consumerProguardFiles("consumer-rules.pro") } + // Builds libkitchen_sink_jni.so per ABI, linked against the producer library in + // src/main/jniLibs// (see src/main/cpp/CMakeLists.txt). externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + } } dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") +} + +publishing { + publications { + register("release") { + artifactId = "kitchen_sink" + afterEvaluate { + from(components["release"]) + } + } + } } // END build.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__consumer_rules_pro.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__consumer_rules_pro.snap new file mode 100644 index 00000000..38aee25a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__consumer_rules_pro.snap @@ -0,0 +1,11 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# The JNI shim looks classes and methods up by name. +-keep class kitchen_sink.** { *; } + +# END consumer-rules.pro diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__settings_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__settings_gradle_kts.snap index 69449d7c..30bdfbbf 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__settings_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__settings_gradle_kts.snap @@ -1,10 +1,25 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -rootProject.name = "weaveffi" +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "kitchen_sink" // END settings.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_CMakeLists_txt.snap index 32fa1b44..4ed25966 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_CMakeLists_txt.snap @@ -1,13 +1,34 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -cmake_minimum_required(VERSION 3.22) -project(weaveffi_jni C) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) +cmake_minimum_required(VERSION 3.18) +project(kitchen_sink_jni C) + +# The directory holding the producer library (libkitchen_sink). Android builds +# default to the per-ABI copy under src/main/jniLibs/. +set(KITCHEN_SINK_LIBRARY_DIR "" CACHE PATH "Directory holding the kitchen_sink producer library") +if(NOT KITCHEN_SINK_LIBRARY_DIR AND ANDROID) + set(KITCHEN_SINK_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}") +endif() +if(KITCHEN_SINK_LIBRARY_DIR) + find_library(KITCHEN_SINK_PRODUCER NAMES kitchen_sink PATHS "${KITCHEN_SINK_LIBRARY_DIR}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED) +else() + set(CMAKE_FIND_FRAMEWORK NEVER) + find_library(KITCHEN_SINK_PRODUCER NAMES kitchen_sink REQUIRED) +endif() + +add_library(kitchen_sink_jni SHARED kitchen_sink_jni.c) +target_include_directories(kitchen_sink_jni PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(kitchen_sink_jni PRIVATE "${KITCHEN_SINK_PRODUCER}") +if(NOT ANDROID) + find_package(JNI REQUIRED) + find_package(Threads REQUIRED) + target_include_directories(kitchen_sink_jni PRIVATE ${JNI_INCLUDE_DIRS}) + target_link_libraries(kitchen_sink_jni PRIVATE Threads::Threads) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_h.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_h.snap new file mode 100644 index 00000000..14faeffb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_h.snap @@ -0,0 +1,219 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef KITCHEN_SINK_H +#define KITCHEN_SINK_H + +#include +#include +#include + +#ifndef KITCHEN_SINK_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef KITCHEN_SINK_BUILD +# define KITCHEN_SINK_API __declspec(dllexport) +# else +# define KITCHEN_SINK_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define KITCHEN_SINK_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define KITCHEN_SINK_API __attribute__((visibility("default"))) +# else +# define KITCHEN_SINK_API +# endif +#endif + +#ifndef KITCHEN_SINK_DEPRECATED +# if defined(_MSC_VER) +# define KITCHEN_SINK_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define KITCHEN_SINK_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define KITCHEN_SINK_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports kitchen_sink_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define KITCHEN_SINK_ABI_VERSION 3u +KITCHEN_SINK_API uint32_t kitchen_sink_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by kitchen_sink_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct kitchen_sink_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} kitchen_sink_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +KITCHEN_SINK_API void kitchen_sink_error_set(kitchen_sink_error* err, int32_t code, const char* message); +KITCHEN_SINK_API void kitchen_sink_error_clear(kitchen_sink_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + kitchen_sink_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_error_free(kitchen_sink_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +KITCHEN_SINK_API void kitchen_sink_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct kitchen_sink_cancel_token kitchen_sink_cancel_token; +KITCHEN_SINK_API kitchen_sink_cancel_token* kitchen_sink_cancel_token_create(void); +KITCHEN_SINK_API void kitchen_sink_cancel_token_cancel(kitchen_sink_cancel_token* token); +KITCHEN_SINK_API bool kitchen_sink_cancel_token_is_cancelled(const kitchen_sink_cancel_token* token); +KITCHEN_SINK_API void kitchen_sink_cancel_token_destroy(kitchen_sink_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +KITCHEN_SINK_API uint8_t* kitchen_sink_alloc(uint32_t size); +KITCHEN_SINK_API void kitchen_sink_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +KITCHEN_SINK_API uint64_t kitchen_sink_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define KITCHEN_SINK_SHARED_CHECKSUM 0x42c4ce2c8d0af052ull +KITCHEN_SINK_API uint64_t kitchen_sink_shared_checksum(void); +#define KITCHEN_SINK_KITCHEN_CHECKSUM 0xd83da75b24b545ffull +KITCHEN_SINK_API uint64_t kitchen_sink_kitchen_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with kitchen_sink_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * kitchen_sink_buffer.h declares a C struct and codec for each. + */ + +/** Task priority levels */ +typedef enum { + /** Low priority */ + kitchen_sink_kitchen_Priority_Low = 0, + /** Normal priority */ + kitchen_sink_kitchen_Priority_Normal = 1, + /** High priority */ + kitchen_sink_kitchen_Priority_High = 2 +} kitchen_sink_kitchen_Priority; +/** Error codes reported by throwing functions in the `kitchen` module tree. */ +typedef enum { kitchen_sink_kitchen_KitchenErrors_NotFound = 1, kitchen_sink_kitchen_KitchenErrors_InvalidInput = 2 } kitchen_sink_kitchen_KitchenErrors; +typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget; +typedef struct kitchen_sink_kitchen_StreamGadgetsIterator kitchen_sink_kitchen_StreamGadgetsIterator; +typedef struct kitchen_sink_kitchen_StreamItemsIterator kitchen_sink_kitchen_StreamItemsIterator; +/** + * Consumer-implemented observer of ready items + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct kitchen_sink_kitchen_ReadyListener_vtable { + /** Fires when an item is ready */ + void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); + /** Receives the item itself and says whether to keep listening */ + bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); + void (*free)(void* ctx); +} kitchen_sink_kitchen_ReadyListener_vtable; +typedef void (*kitchen_sink_kitchen_do_async_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*kitchen_sink_kitchen_do_cancellable_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: shared +/** Trivial cross-module helper */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_shared_ping(size_t* out_len, kitchen_sink_error* out_err); + +// Module: kitchen +/** A pocket-sized interface exercising the object surface */ +/** Create a gadget with the given id */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(int64_t id, kitchen_sink_error* out_err); +/** The gadget subsystem version string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_version(size_t* out_len, kitchen_sink_error* out_err); +/** Render the gadget as a human-readable string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err); +/** Poke the gadget a number of times, failing on a negative count */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_Gadget_poke(const kitchen_sink_kitchen_Gadget* self, int32_t times, kitchen_sink_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_kitchen_Gadget_destroy(kitchen_sink_kitchen_Gadget* self); + +/** Identity for bool */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_bool_id(bool x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_i32_id(int32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API uint32_t kitchen_sink_kitchen_u32_id(uint32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int64_t kitchen_sink_kitchen_i64_id(int64_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API double kitchen_sink_kitchen_f64_id(double x, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_string(const uint8_t* s_ptr, size_t s_len, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_bytes(const uint8_t* b_ptr, size_t b_len, size_t* out_len, kitchen_sink_error* out_err); +/** Register a listener, returning how many are attached */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err); +/** Optional object in both directions */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_maybe_gadget(const kitchen_sink_kitchen_Gadget* fallback, kitchen_sink_error* out_err); +/** A list of objects */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_gadgets(size_t* out_len, kitchen_sink_error* out_err); +/** An iterator over objects */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamGadgetsIterator* kitchen_sink_kitchen_stream_gadgets(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamGadgetsIterator_next(kitchen_sink_kitchen_StreamGadgetsIterator* iter, kitchen_sink_kitchen_Gadget** out_item, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamGadgetsIterator_destroy(kitchen_sink_kitchen_StreamGadgetsIterator* iter); +/** Optional struct return */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_maybe_item(int64_t id, size_t* out_len, kitchen_sink_error* out_err); +/** List of structs */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_list_items(size_t* out_len, kitchen_sink_error* out_err); +/** Map return type */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_get_attrs(size_t* out_len, kitchen_sink_error* out_err); +/** Iterator return type */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamItemsIterator* kitchen_sink_kitchen_stream_items(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamItemsIterator_next(kitchen_sink_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamItemsIterator_destroy(kitchen_sink_kitchen_StreamItemsIterator* iter); +/** Returns the shared Token type from another module */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_cross_module_token(size_t* out_len, kitchen_sink_error* out_err); +/** Async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_async(const uint8_t* input_ptr, size_t input_len, kitchen_sink_kitchen_do_async_callback callback, void* context); +/** Cancellable async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_ptr, size_t input_len, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_do_cancellable_callback callback, void* context); +/** Legacy operation kept for compatibility */ +KITCHEN_SINK_DEPRECATED("Use new_op instead") +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_legacy_op(kitchen_sink_error* out_err); +/** Replacement for legacy_op */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_new_op(kitchen_sink_error* out_err); + +// Module: kitchen_nested +/** Trivial nested-module function */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_nested_hello(size_t* out_len, kitchen_sink_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // KITCHEN_SINK_H + +// END kitchen_sink.h diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_jni_c.snap new file mode 100644 index 00000000..feb61be5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_kitchen_sink_jni_c.snap @@ -0,0 +1,768 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#include +#include +#include +#include +#include +#include + +/* The shim forwards deprecated functions too, so it doesn't warn about them. */ +#define KITCHEN_SINK_DEPRECATED(msg) +#include "kitchen_sink.h" + +/* Every static name in this file starts with `Jni_`, which no C ABI symbol + (all lowercase `kitchen_sink_...`) and no JNI export (`Java_...`) can + spell. */ + +static JavaVM* Jni_vm = NULL; +static jclass Jni_bridge = NULL; +static jmethodID Jni_error = NULL; + +/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value + buffers all cross as one. */ +typedef struct { + jbyteArray array; + jbyte* elems; + const uint8_t* ptr; + size_t len; +} Jni_bytes; + +static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { + Jni_bytes b = {array, NULL, NULL, 0}; + if (array == NULL) { + return b; + } + b.len = (size_t)(*env)->GetArrayLength(env, array); + b.elems = (*env)->GetByteArrayElements(env, array, NULL); + b.ptr = (const uint8_t*)b.elems; + if (b.elems == NULL) { + b.len = 0; + } + return b; +} + +static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { + if (b->elems != NULL) { + (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); + } +} + +/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid + input, so NULL + 0 is the empty array). */ +static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = (*env)->NewByteArray(env, (jsize)len); + if (out != NULL && len > 0 && ptr != NULL) { + (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); + } + return out; +} + +/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ +static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = Jni_new_bytes(env, ptr, len); + if (ptr != NULL) { + kitchen_sink_free_bytes((uint8_t*)ptr, len); + } + return out; +} + +/* Throws the Kotlin exception for `err` (mapped through domain `domain`, + 0 for the generic one) and clears `err`. */ +static inline void Jni_throw(JNIEnv* env, kitchen_sink_error* err, jint domain) { + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + kitchen_sink_error_clear(err); + if ((*env)->ExceptionCheck(env)) { + return; + } + jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); + if (ex != NULL && !(*env)->ExceptionCheck(env)) { + (*env)->Throw(env, ex); + } +} + +static jint Jni_load_error(JNIEnv* env, const char* message) { + jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); + if (cls != NULL) { + (*env)->ThrowNew(env, cls, message); + } + return JNI_ERR; +} + +/* Generated below: the contract checks and the method IDs the shim caches. */ +static jint Jni_load(JNIEnv* env); + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env = NULL; + (void)reserved; + Jni_vm = vm; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + if (kitchen_sink_abi_version() != 3u) { + return Jni_load_error(env, "kitchen_sink: the native library implements a different C ABI revision than these bindings (expected 3)"); + } + jclass bridge = (*env)->FindClass(env, "kitchen_sink/JniBridge"); + if (bridge == NULL) { + return JNI_ERR; + } + Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); + Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Lkitchen_sink/FfiException;"); + if (Jni_error == NULL) { + return JNI_ERR; + } + if (Jni_load(env) != JNI_OK) { + return JNI_ERR; + } + return JNI_VERSION_1_6; +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { + (void)env; + (void)cls; + return (jlong)kitchen_sink_debug_live((int32_t)kind); +} + +/* Producer threads (async completions, callback calls, vtable `free`) are + usually not JVM threads. The first time one calls in it attaches as a + daemon and keeps its JNIEnv; a thread-local destructor detaches it when + the thread exits, so a busy producer thread pays for one attach, not one + per call. */ +#if defined(_WIN32) +static void Jni_remember_attach(void) {} +#else +#include + +static pthread_key_t Jni_env_key; +static pthread_once_t Jni_env_once = PTHREAD_ONCE_INIT; + +static void Jni_detach(void* env) { + (void)env; + if (Jni_vm != NULL) { + (*Jni_vm)->DetachCurrentThread(Jni_vm); + } +} + +static void Jni_make_env_key(void) { + pthread_key_create(&Jni_env_key, Jni_detach); +} + +static void Jni_remember_attach(void) { + pthread_once(&Jni_env_once, Jni_make_env_key); + pthread_setspecific(Jni_env_key, (void*)Jni_vm); +} +#endif + +/* The calling thread's JNIEnv, attaching the thread if needed; NULL when + the JVM is unavailable. */ +static JNIEnv* Jni_env(void) { + JNIEnv* env = NULL; + if (Jni_vm == NULL) { + return NULL; + } + if ((*Jni_vm)->GetEnv(Jni_vm, (void**)&env, JNI_VERSION_1_6) == JNI_OK) { + return env; + } + /* `void*` converts to the `JNIEnv**` Android declares and the `void**` + the JDK declares. */ + if ((*Jni_vm)->AttachCurrentThreadAsDaemon(Jni_vm, (void*)&env, NULL) != JNI_OK) { + return NULL; + } + Jni_remember_attach(); + return env; +} + +/* Async completions deliver to the pinned Kotlin `NativeCompletion`. */ +static jmethodID Jni_on_unit = NULL; +static jmethodID Jni_on_boolean = NULL; +static jmethodID Jni_on_byte = NULL; +static jmethodID Jni_on_short = NULL; +static jmethodID Jni_on_int = NULL; +static jmethodID Jni_on_long = NULL; +static jmethodID Jni_on_float = NULL; +static jmethodID Jni_on_double = NULL; +static jmethodID Jni_on_bytes = NULL; +static jmethodID Jni_on_error = NULL; + +static jint Jni_load_async(JNIEnv* env) { + jclass cls = (*env)->FindClass(env, "kitchen_sink/NativeCompletion"); + if (cls == NULL) { + return JNI_ERR; + } + Jni_on_unit = (*env)->GetMethodID(env, cls, "onUnit", "()V"); + Jni_on_boolean = (*env)->GetMethodID(env, cls, "onBoolean", "(Z)V"); + Jni_on_byte = (*env)->GetMethodID(env, cls, "onByte", "(B)V"); + Jni_on_short = (*env)->GetMethodID(env, cls, "onShort", "(S)V"); + Jni_on_int = (*env)->GetMethodID(env, cls, "onInt", "(I)V"); + Jni_on_long = (*env)->GetMethodID(env, cls, "onLong", "(J)V"); + Jni_on_float = (*env)->GetMethodID(env, cls, "onFloat", "(F)V"); + Jni_on_double = (*env)->GetMethodID(env, cls, "onDouble", "(D)V"); + Jni_on_bytes = (*env)->GetMethodID(env, cls, "onBytes", "([B)V"); + Jni_on_error = (*env)->GetMethodID(env, cls, "onError", "(I[B[B)V"); + if (Jni_on_unit == NULL || Jni_on_error == NULL || Jni_on_bytes == NULL) { + return JNI_ERR; + } + return JNI_OK; +} + +/* Starts a completion on the producer thread: returns the JNIEnv with a + local frame pushed, or NULL when the call failed (the failure, or the + loss of the JVM, has then been handled and `context` released). */ +static JNIEnv* Jni_complete_begin(void* context, kitchen_sink_error* err) { + JNIEnv* env = Jni_env(); + if (env == NULL) { + kitchen_sink_error_free(err); + return NULL; + } + if ((*env)->PushLocalFrame(env, 8) != 0) { + (*env)->ExceptionClear(env); + kitchen_sink_error_free(err); + return NULL; + } + if (err == NULL) { + return env; + } + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + kitchen_sink_error_free(err); + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + (*env)->DeleteGlobalRef(env, (jobject)context); + return NULL; +} + +/* Finishes a successful completion: reports anything the Kotlin side threw + (it has no caller to propagate to) and releases `context`. */ +static void Jni_complete_end(JNIEnv* env, void* context) { + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + (*env)->DeleteGlobalRef(env, (jobject)context); +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_cancel_1token_1create(JNIEnv* env, jclass cls) { + (void)env; + (void)cls; + return (jlong)(intptr_t)kitchen_sink_cancel_token_create(); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_cancel_1token_1cancel(JNIEnv* env, jclass cls, jlong token) { + (void)env; + (void)cls; + kitchen_sink_cancel_token_cancel((kitchen_sink_cancel_token*)(intptr_t)token); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_cancel_1token_1destroy(JNIEnv* env, jclass cls, jlong token) { + (void)env; + (void)cls; + kitchen_sink_cancel_token_destroy((kitchen_sink_cancel_token*)(intptr_t)token); +} + +/* Callback interfaces: the producer holds a global reference to the Kotlin + implementation as `ctx`; trampolines call the matching static dispatch + method on JniBridge from whatever thread the producer uses. */ +static jmethodID Jni_describe = NULL; + +static jint Jni_load_callbacks(JNIEnv* env) { + Jni_describe = (*env)->GetStaticMethodID(env, Jni_bridge, "describe", "(Ljava/lang/Throwable;)[B"); + return Jni_describe != NULL ? JNI_OK : JNI_ERR; +} + +/* Enters a trampoline: the JNIEnv with a local frame pushed, or NULL after + reporting the failure through `out_err`. */ +static JNIEnv* Jni_callback_begin(kitchen_sink_error* out_err) { + JNIEnv* env = Jni_env(); + if (env == NULL) { + kitchen_sink_error_set(out_err, -4, "could not attach the calling thread to the JVM"); + return NULL; + } + if ((*env)->PushLocalFrame(env, 16) != 0) { + (*env)->ExceptionClear(env); + kitchen_sink_error_set(out_err, -4, "JNI local frame exhausted"); + return NULL; + } + return env; +} + +/* Leaves a trampoline. When the implementation threw, reports its + `toString()` through `out_err` with code -4 and returns 1; nothing ever + unwinds through the producer's frames. */ +static int Jni_callback_end(JNIEnv* env, kitchen_sink_error* out_err) { + int failed = 0; + if ((*env)->ExceptionCheck(env)) { + failed = 1; + jthrowable ex = (*env)->ExceptionOccurred(env); + (*env)->ExceptionClear(env); + jbyteArray text = (jbyteArray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_describe, ex); + char* message = NULL; + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + } else if (text != NULL) { + jsize len = (*env)->GetArrayLength(env, text); + message = (char*)malloc((size_t)len + 1u); + if (message != NULL) { + (*env)->GetByteArrayRegion(env, text, 0, len, (jbyte*)message); + message[len] = '\0'; + } + } + kitchen_sink_error_set(out_err, -4, message != NULL ? message : "callback implementation threw"); + free(message); + } + (*env)->PopLocalFrame(env, NULL); + return failed; +} + +/* The vtable `free` entry: drops the producer's reference to the Kotlin + implementation. */ +static void Jni_release_callback(void* ctx) { + JNIEnv* env = Jni_env(); + if (env != NULL && ctx != NULL) { + (*env)->DeleteGlobalRef(env, (jobject)ctx); + } +} + +static jmethodID Jni_mid_kitchen_ReadyListener_on_ready = NULL; +static jmethodID Jni_mid_kitchen_ReadyListener_on_item = NULL; + +static void Jni_kitchen_ReadyListener_on_ready(void* ctx, int32_t p_code, const uint8_t* p_msg_ptr, size_t p_msg_len, kitchen_sink_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return; + } + (*env)->CallStaticVoidMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_on_ready, (jobject)ctx, (jint)p_code, Jni_new_bytes(env, p_msg_ptr, p_msg_len)); + Jni_callback_end(env, out_err); +} + +static bool Jni_kitchen_ReadyListener_on_item(void* ctx, const uint8_t* p_item_ptr, size_t p_item_len, kitchen_sink_kitchen_Gadget* p_gadget, kitchen_sink_error* out_err) { + JNIEnv* env = Jni_callback_begin(out_err); + if (env == NULL) { + return (bool)0; + } + jboolean rv = (*env)->CallStaticBooleanMethod(env, Jni_bridge, Jni_mid_kitchen_ReadyListener_on_item, (jobject)ctx, Jni_new_bytes(env, p_item_ptr, p_item_len), (jlong)(intptr_t)p_gadget); + if (Jni_callback_end(env, out_err)) { + return (bool)0; + } + return rv == JNI_TRUE; +} + +static const kitchen_sink_kitchen_ReadyListener_vtable Jni_kitchen_ReadyListener_vtable = {Jni_kitchen_ReadyListener_on_ready, Jni_kitchen_ReadyListener_on_item, Jni_release_callback}; + +static jint Jni_load(JNIEnv* env) { + (void)env; + if (kitchen_sink_shared_checksum() != UINT64_C(0x42c4ce2c8d0af052)) { + return Jni_load_error(env, "kitchen_sink: module 'shared' does not match these bindings (its contract checksum differs); regenerate the bindings or rebuild the library"); + } + if (kitchen_sink_kitchen_checksum() != UINT64_C(0xd83da75b24b545ff)) { + return Jni_load_error(env, "kitchen_sink: module 'kitchen' does not match these bindings (its contract checksum differs); regenerate the bindings or rebuild the library"); + } + if (Jni_load_async(env) != JNI_OK) { + return JNI_ERR; + } + if (Jni_load_callbacks(env) != JNI_OK) { + return JNI_ERR; + } + Jni_mid_kitchen_ReadyListener_on_ready = (*env)->GetStaticMethodID(env, Jni_bridge, "kitchen_ReadyListener_on_ready", "(Lkitchen_sink/ReadyListener;I[B)V"); + if (Jni_mid_kitchen_ReadyListener_on_ready == NULL) { + return JNI_ERR; + } + Jni_mid_kitchen_ReadyListener_on_item = (*env)->GetStaticMethodID(env, Jni_bridge, "kitchen_ReadyListener_on_item", "(Lkitchen_sink/ReadyListener;[BJ)Z"); + if (Jni_mid_kitchen_ReadyListener_on_item == NULL) { + return JNI_ERR; + } + return JNI_OK; +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_shared_1ping(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_shared_ping(&out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1clone(JNIEnv* env, jclass cls, jlong self) { + (void)env; + (void)cls; + return (jlong)(intptr_t)kitchen_sink_kitchen_Gadget_clone((const kitchen_sink_kitchen_Gadget*)(intptr_t)self); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1destroy(JNIEnv* env, jclass cls, jlong self) { + (void)env; + (void)cls; + kitchen_sink_kitchen_Gadget_destroy((kitchen_sink_kitchen_Gadget*)(intptr_t)self); +} + +JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1bool_1id(JNIEnv* env, jclass cls, jboolean p_x) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + bool rv = kitchen_sink_kitchen_bool_id(p_x == JNI_TRUE, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return rv ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1i32_1id(JNIEnv* env, jclass cls, jint p_x) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + int32_t rv = kitchen_sink_kitchen_i32_id((int32_t)p_x, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1u32_1id(JNIEnv* env, jclass cls, jlong p_x) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + uint32_t rv = kitchen_sink_kitchen_u32_id((uint32_t)p_x, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)rv; +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1i64_1id(JNIEnv* env, jclass cls, jlong p_x) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + int64_t rv = kitchen_sink_kitchen_i64_id((int64_t)p_x, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)rv; +} + +JNIEXPORT jdouble JNICALL Java_kitchen_1sink_JniBridge_kitchen_1f64_1id(JNIEnv* env, jclass cls, jdouble p_x) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + double rv = kitchen_sink_kitchen_f64_id((double)p_x, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jdouble)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1echo_1string(JNIEnv* env, jclass cls, jbyteArray p_s) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_s_b = Jni_borrow_bytes(env, p_s); + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_echo_string(p_s_b.ptr, p_s_b.len, &out_len, &err); + Jni_release_bytes(env, &p_s_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1echo_1bytes(JNIEnv* env, jclass cls, jbyteArray p_b) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_b_b = Jni_borrow_bytes(env, p_b); + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_echo_bytes(p_b_b.ptr, p_b_b.len, &out_len, &err); + Jni_release_bytes(env, &p_b_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1subscribe(JNIEnv* env, jclass cls, jobject p_listener) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + int32_t rv = kitchen_sink_kitchen_subscribe((void*)(*env)->NewGlobalRef(env, p_listener), &Jni_kitchen_ReadyListener_vtable, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1maybe_1gadget(JNIEnv* env, jclass cls, jlong p_fallback) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + kitchen_sink_kitchen_Gadget* rv = kitchen_sink_kitchen_maybe_gadget((const kitchen_sink_kitchen_Gadget*)(intptr_t)p_fallback, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1gadgets(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_gadgets(&out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1stream_1gadgets(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + kitchen_sink_kitchen_StreamGadgetsIterator* rv = kitchen_sink_kitchen_stream_gadgets(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jboolean JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamGadgetsIterator_1next(JNIEnv* env, jclass cls, jlong iter, jlongArray out) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + kitchen_sink_kitchen_Gadget* item = (kitchen_sink_kitchen_Gadget*)0; + (void)cls; + int32_t has = kitchen_sink_kitchen_StreamGadgetsIterator_next((kitchen_sink_kitchen_StreamGadgetsIterator*)(intptr_t)iter, &item, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return JNI_FALSE; + } + if (has == 0) { + return JNI_FALSE; + } + jlong value = (jlong)(intptr_t)item; + (*env)->SetLongArrayRegion(env, out, 0, 1, &value); + return JNI_TRUE; +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamGadgetsIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { + (void)env; + (void)cls; + kitchen_sink_kitchen_StreamGadgetsIterator_destroy((kitchen_sink_kitchen_StreamGadgetsIterator*)(intptr_t)iter); +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1maybe_1item(JNIEnv* env, jclass cls, jlong p_id) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_maybe_item((int64_t)p_id, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1list_1items(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_list_items(&out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1get_1attrs(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_get_attrs(&out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1stream_1items(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + kitchen_sink_kitchen_StreamItemsIterator* rv = kitchen_sink_kitchen_stream_items(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamItemsIterator_1next(JNIEnv* env, jclass cls, jlong iter) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + const uint8_t* item = NULL; + size_t out_len = 0; + (void)cls; + int32_t has = kitchen_sink_kitchen_StreamItemsIterator_next((kitchen_sink_kitchen_StreamItemsIterator*)(intptr_t)iter, &item, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + if (has == 0) { + return NULL; + } + return Jni_take_bytes(env, item, out_len); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1StreamItemsIterator_1destroy(JNIEnv* env, jclass cls, jlong iter) { + (void)env; + (void)cls; + kitchen_sink_kitchen_StreamItemsIterator_destroy((kitchen_sink_kitchen_StreamItemsIterator*)(intptr_t)iter); +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1cross_1module_1token(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_cross_module_token(&out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +static void Jni_done_kitchen_do_async(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1do_1async(JNIEnv* env, jclass cls, jbyteArray p_input, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + Jni_bytes p_input_b = Jni_borrow_bytes(env, p_input); + kitchen_sink_kitchen_do_async(p_input_b.ptr, p_input_b.len, Jni_done_kitchen_do_async, context); + Jni_release_bytes(env, &p_input_b); +} + +static void Jni_done_kitchen_do_cancellable(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { + JNIEnv* env = Jni_complete_begin(context, err); + if (env == NULL) { + return; + } + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)); + Jni_complete_end(env, context); +} + +JNIEXPORT void JNICALL Java_kitchen_1sink_JniBridge_kitchen_1do_1cancellable(JNIEnv* env, jclass cls, jbyteArray p_input, jlong cancel_token, jobject completion) { + void* context = (void*)(*env)->NewGlobalRef(env, completion); + (void)cls; + Jni_bytes p_input_b = Jni_borrow_bytes(env, p_input); + kitchen_sink_kitchen_do_cancellable(p_input_b.ptr, p_input_b.len, (kitchen_sink_cancel_token*)(intptr_t)cancel_token, Jni_done_kitchen_do_cancellable, context); + Jni_release_bytes(env, &p_input_b); +} + +JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1legacy_1op(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + int32_t rv = kitchen_sink_kitchen_legacy_op(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1new_1op(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + int32_t rv = kitchen_sink_kitchen_new_op(&err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jlong JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1new(JNIEnv* env, jclass cls, jlong p_id) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + kitchen_sink_kitchen_Gadget* rv = kitchen_sink_kitchen_Gadget_new((int64_t)p_id, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1describe(JNIEnv* env, jclass cls, jlong self) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_Gadget_describe((const kitchen_sink_kitchen_Gadget*)(intptr_t)self, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jint JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1poke(JNIEnv* env, jclass cls, jlong self, jint p_times) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + int32_t rv = kitchen_sink_kitchen_Gadget_poke((const kitchen_sink_kitchen_Gadget*)(intptr_t)self, (int32_t)p_times, &err); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return 0; + } + return (jint)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1Gadget_1version(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_Gadget_version(&out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_kitchen_1sink_JniBridge_kitchen_1nested_1hello(JNIEnv* env, jclass cls) { + kitchen_sink_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = kitchen_sink_kitchen_nested_hello(&out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + + +// END kitchen_sink_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index 207da6c9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,683 +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" - -/* Copies `s` as a NUL-terminated standard UTF-8 string (JNI's modified - UTF-8 would encode supplementary characters as surrogate pairs the - producer rejects). Lone surrogates become U+FFFD. Release with free(). - Returns NULL for a NULL string, for a string holding U+0000 (which no - C string can carry), or on allocation failure; the producer then - reports the null argument through its marshalling error. */ -static char* weaveffi_jni_string_to_utf8(JNIEnv* env, jstring s) { - if (s == NULL) { return NULL; } - jsize n = (*env)->GetStringLength(env, s); - const jchar* u = (*env)->GetStringChars(env, s, NULL); - if (u == NULL) { return NULL; } - char* out = (char*)malloc((size_t)n * 4u + 1u); - if (out == NULL) { (*env)->ReleaseStringChars(env, s, u); return NULL; } - size_t o = 0; - for (jsize i = 0; i < n; i++) { - uint32_t c = u[i]; - if (c == 0u) { free(out); (*env)->ReleaseStringChars(env, s, u); return NULL; } - if (c >= 0xD800u && c <= 0xDBFFu && i + 1 < n && u[i + 1] >= 0xDC00u && u[i + 1] <= 0xDFFFu) { - c = 0x10000u + ((c - 0xD800u) << 10) + ((uint32_t)u[i + 1] - 0xDC00u); - i++; - } - else if (c >= 0xD800u && c <= 0xDFFFu) { c = 0xFFFDu; } - if (c < 0x80u) { out[o++] = (char)c; } - else if (c < 0x800u) { out[o++] = (char)(0xC0u | (c >> 6)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else if (c < 0x10000u) { out[o++] = (char)(0xE0u | (c >> 12)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else { out[o++] = (char)(0xF0u | (c >> 18)); out[o++] = (char)(0x80u | ((c >> 12) & 0x3Fu)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - } - out[o] = 0; - (*env)->ReleaseStringChars(env, s, u); - return out; -} - -/* Builds a jstring from standard UTF-8 (NewStringUTF expects modified UTF-8 - and mangles supplementary characters). NULL yields the empty string and - a malformed sequence yields U+FFFD, so this never fails on producer - output; it returns NULL only when the JVM could not allocate. */ -static jstring weaveffi_jni_utf8_to_string(JNIEnv* env, const char* s) { - if (s == NULL) { s = ""; } - size_t n = strlen(s); - jchar* buf = (jchar*)malloc((n + 1u) * sizeof(jchar)); - if (buf == NULL) { return NULL; } - size_t i = 0, o = 0; - while (i < n) { - unsigned char b = (unsigned char)s[i]; - uint32_t c; size_t len; - if (b < 0x80u) { c = b; len = 1; } - else if ((b & 0xE0u) == 0xC0u) { c = b & 0x1Fu; len = 2; } - else if ((b & 0xF0u) == 0xE0u) { c = b & 0x0Fu; len = 3; } - else if ((b & 0xF8u) == 0xF0u) { c = b & 0x07u; len = 4; } - else { c = 0xFFFDu; len = 1; } - if (i + len > n) { c = 0xFFFDu; len = 1; } - for (size_t k = 1; k < len; k++) { - unsigned char cb = (unsigned char)s[i + k]; - if ((cb & 0xC0u) != 0x80u) { c = 0xFFFDu; len = k; break; } - c = (c << 6) | (cb & 0x3Fu); - } - i += len; - if (c > 0x10FFFFu) { c = 0xFFFDu; } - if (c >= 0x10000u) { c -= 0x10000u; buf[o++] = (jchar)(0xD800u + (c >> 10)); buf[o++] = (jchar)(0xDC00u + (c & 0x3FFu)); } - else { buf[o++] = (jchar)c; } - } - jstring out = (*env)->NewString(env, buf, (jsize)o); - free(buf); - return out; -} - -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 = weaveffi_jni_utf8_to_string(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_kitchen_KitchenErrors(JNIEnv* env, weaveffi_error* err) { - jclass exClass = (*env)->FindClass(env, "com/weaveffi/KitchenErrorsException"); - 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 = weaveffi_jni_utf8_to_string(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 JavaVM* weaveffi_jni_vm = NULL; -static jclass weaveffi_jni_entry_class = NULL; -static jmethodID weaveffi_jni_dispatch_exc = NULL; -static jmethodID weaveffi_jni_throwable_to_string = NULL; -static jclass weaveffi_kitchen_ReadyListener_jni_cls = NULL; -static jmethodID weaveffi_kitchen_ReadyListener_jni_mid_on_ready = NULL; -static jmethodID weaveffi_kitchen_ReadyListener_jni_mid_on_item = NULL; - -static jint weaveffi_jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { (*env)->ThrowNew(env, cls, message); } - return JNI_ERR; -} - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - (void)reserved; - weaveffi_jni_vm = vm; - JNIEnv* env = NULL; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - if (weaveffi_abi_version() != 2u) { - return weaveffi_jni_load_error(env, "WeaveFFI: the native library implements a different C ABI revision than these bindings (expected 2)"); - } - jclass entry = (*env)->FindClass(env, "com/weaveffi/WeaveFFI"); - if (entry == NULL) { return JNI_ERR; } - weaveffi_jni_entry_class = (jclass)(*env)->NewGlobalRef(env, entry); - 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); } - jclass throwable = (*env)->FindClass(env, "java/lang/Throwable"); - if (throwable == NULL) { return JNI_ERR; } - weaveffi_jni_throwable_to_string = (*env)->GetMethodID(env, throwable, "toString", "()Ljava/lang/String;"); - if (weaveffi_jni_throwable_to_string == NULL) { return JNI_ERR; } - jclass weaveffi_kitchen_ReadyListener_jni_cls_local = (*env)->FindClass(env, "com/weaveffi/ReadyListenerJni"); - if (weaveffi_kitchen_ReadyListener_jni_cls_local == NULL) { return JNI_ERR; } - weaveffi_kitchen_ReadyListener_jni_cls = (jclass)(*env)->NewGlobalRef(env, weaveffi_kitchen_ReadyListener_jni_cls_local); - weaveffi_kitchen_ReadyListener_jni_mid_on_ready = (*env)->GetStaticMethodID(env, weaveffi_kitchen_ReadyListener_jni_cls, "onReady", "(Lcom/weaveffi/ReadyListener;ILjava/lang/String;)V"); - if (weaveffi_kitchen_ReadyListener_jni_mid_on_ready == NULL) { return JNI_ERR; } - weaveffi_kitchen_ReadyListener_jni_mid_on_item = (*env)->GetStaticMethodID(env, weaveffi_kitchen_ReadyListener_jni_cls, "onItem", "(Lcom/weaveffi/ReadyListener;[BJ)Z"); - if (weaveffi_kitchen_ReadyListener_jni_mid_on_item == NULL) { return JNI_ERR; } - return JNI_VERSION_1_6; -} - -/* Returns 1 when this call attached the thread (the caller must detach), - 0 otherwise. *env is NULL when no JNIEnv could be obtained. */ -static int weaveffi_jni_attach(JNIEnv** env) { - *env = NULL; - if (weaveffi_jni_vm == NULL) { return 0; } - if ((*weaveffi_jni_vm)->GetEnv(weaveffi_jni_vm, (void**)env, JNI_VERSION_1_6) == JNI_OK) { return 0; } - if ((*weaveffi_jni_vm)->AttachCurrentThread(weaveffi_jni_vm, (void*)env, NULL) != JNI_OK) { *env = NULL; return 0; } - return 1; -} - -static void weaveffi_jni_detach(int attached) { - if (attached) { (*weaveffi_jni_vm)->DetachCurrentThread(weaveffi_jni_vm); } -} - -typedef struct { - jobject callback; -} weaveffi_jni_async_ctx; - -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_jni_report_foreign(JNIEnv* env, weaveffi_error* out_err) { - jthrowable ex = (*env)->ExceptionOccurred(env); - (*env)->ExceptionClear(env); - jstring jmsg = NULL; - if (ex != NULL && weaveffi_jni_throwable_to_string != NULL) { - jmsg = (jstring)(*env)->CallObjectMethod(env, ex, weaveffi_jni_throwable_to_string); - if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); jmsg = NULL; } - } - char* msg = weaveffi_jni_string_to_utf8(env, jmsg); - weaveffi_error_set(out_err, -4, msg != NULL ? msg : "callback interface implementation threw"); - free(msg); -} - -static void weaveffi_kitchen_ReadyListener_jni_on_ready(void* ctx, int32_t code, const char* msg, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return; - } - jint _a0 = (jint)code; - jstring _a1 = weaveffi_jni_utf8_to_string(env, msg); - (*env)->CallStaticVoidMethod(env, weaveffi_kitchen_ReadyListener_jni_cls, weaveffi_kitchen_ReadyListener_jni_mid_on_ready, (jobject)ctx, _a0, _a1); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); -} - -static bool weaveffi_kitchen_ReadyListener_jni_on_item(void* ctx, const uint8_t* item_ptr, size_t item_len, weaveffi_kitchen_Gadget* gadget, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return false; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { - (*env)->ExceptionClear(env); - weaveffi_error_set(out_err, -4, "WeaveFFI: JNI local frame exhausted"); - weaveffi_jni_detach(attached); - return false; - } - jbyteArray _a0 = (*env)->NewByteArray(env, (jsize)item_len); - if (_a0 && item_ptr) { (*env)->SetByteArrayRegion(env, _a0, 0, (jsize)item_len, (const jbyte*)item_ptr); } - jlong _a1 = (jlong)(intptr_t)gadget; - jboolean _rv = (*env)->CallStaticBooleanMethod(env, weaveffi_kitchen_ReadyListener_jni_cls, weaveffi_kitchen_ReadyListener_jni_mid_on_item, (jobject)ctx, _a0, _a1); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - _rv = (jboolean)false; - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); - return _rv == JNI_TRUE; -} - -static void weaveffi_kitchen_ReadyListener_jni_free(void* ctx) { - if (ctx == NULL) { return; } - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env != NULL) { (*env)->DeleteGlobalRef(env, (jobject)ctx); } - weaveffi_jni_detach(attached); -} - -static const weaveffi_kitchen_ReadyListener_vtable weaveffi_kitchen_ReadyListener_jni_vtable = { weaveffi_kitchen_ReadyListener_jni_on_ready, weaveffi_kitchen_ReadyListener_jni_on_item, weaveffi_kitchen_ReadyListener_jni_free }; - -JNIEXPORT jstring JNICALL Java_com_weaveffi_WeaveFFI_ping(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* rv = weaveffi_shared_ping(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jstring out = weaveffi_jni_utf8_to_string(env, rv); - weaveffi_free_string(rv); - return out; -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_WeaveFFI_boolId(JNIEnv* env, jclass clazz, jboolean x) { - weaveffi_error err = {0, NULL, NULL, 0}; - bool rv = weaveffi_kitchen_bool_id((bool)(x == JNI_TRUE), &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_WeaveFFI_i32Id(JNIEnv* env, jclass clazz, jint x) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_kitchen_i32_id((int32_t)x, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_u32Id(JNIEnv* env, jclass clazz, jlong x) { - weaveffi_error err = {0, NULL, NULL, 0}; - uint32_t rv = weaveffi_kitchen_u32_id((uint32_t)x, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong) rv; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_i64Id(JNIEnv* env, jclass clazz, jlong x) { - weaveffi_error err = {0, NULL, NULL, 0}; - int64_t rv = weaveffi_kitchen_i64_id((int64_t)x, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong) rv; -} - -JNIEXPORT jdouble JNICALL Java_com_weaveffi_WeaveFFI_f64Id(JNIEnv* env, jclass clazz, jdouble x) { - weaveffi_error err = {0, NULL, NULL, 0}; - double rv = weaveffi_kitchen_f64_id((double)x, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0.0; - } - return (jdouble) rv; -} - -JNIEXPORT jstring JNICALL Java_com_weaveffi_WeaveFFI_echoString(JNIEnv* env, jclass clazz, jstring s) { - weaveffi_error err = {0, NULL, NULL, 0}; - char* s_chars = weaveffi_jni_string_to_utf8(env, s); - const char* rv = weaveffi_kitchen_echo_string(s_chars, &err); - free(s_chars); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jstring out = weaveffi_jni_utf8_to_string(env, rv); - weaveffi_free_string(rv); - return out; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_echoBytes(JNIEnv* env, jclass clazz, jbyteArray b) { - weaveffi_error err = {0, NULL, NULL, 0}; - jboolean b_is_copy = 0; - jbyte* b_elems = (*env)->GetByteArrayElements(env, b, &b_is_copy); - jsize b_len = (*env)->GetArrayLength(env, b); - size_t out_len = 0; - const uint8_t* rv = weaveffi_kitchen_echo_bytes((const uint8_t*)b_elems, (size_t)b_len, &out_len, &err); - (*env)->ReleaseByteArrayElements(env, b, b_elems, 0); - 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 jint JNICALL Java_com_weaveffi_WeaveFFI_subscribe(JNIEnv* env, jclass clazz, jobject listener) { - weaveffi_error err = {0, NULL, NULL, 0}; - jobject listener_ref = (*env)->NewGlobalRef(env, listener); - int32_t rv = weaveffi_kitchen_subscribe((void*)listener_ref, &weaveffi_kitchen_ReadyListener_jni_vtable, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jobject JNICALL Java_com_weaveffi_WeaveFFI_maybeGadgetJni(JNIEnv* env, jclass clazz, 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); - } - weaveffi_kitchen_Gadget* rv = weaveffi_kitchen_maybe_gadget((const weaveffi_kitchen_Gadget*)(intptr_t)fallback_val, &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 jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_gadgetsJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_kitchen_gadgets(&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 jlong JNICALL Java_com_weaveffi_WeaveFFI_streamGadgetsJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - weaveffi_kitchen_StreamGadgetsIterator* _iter = weaveffi_kitchen_stream_gadgets(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong)(intptr_t)_iter; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_maybeItemJni(JNIEnv* env, jclass clazz, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_kitchen_maybe_item((int64_t)id, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_kitchen_KitchenErrors(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_listItemsJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_kitchen_list_items(&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 jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_getAttrsJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_kitchen_get_attrs(&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 jlong JNICALL Java_com_weaveffi_WeaveFFI_streamItemsJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - weaveffi_kitchen_StreamItemsIterator* _iter = weaveffi_kitchen_stream_items(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong)(intptr_t)_iter; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_crossModuleTokenJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_kitchen_cross_module_token(&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; -} - -static void weaveffi_kitchen_do_async_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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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 = (jobject)weaveffi_jni_utf8_to_string(env, result); - 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); - free(ctx); - weaveffi_jni_detach(attached); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_doAsyncAsync(JNIEnv* env, jclass clazz, jstring input, jobject callback) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); - ctx->callback = (*env)->NewGlobalRef(env, callback); - char* input_chars = weaveffi_jni_string_to_utf8(env, input); - weaveffi_kitchen_do_async_async(input_chars, weaveffi_kitchen_do_async_jni_cb, ctx); - free(input_chars); -} - -static void weaveffi_kitchen_do_cancellable_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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = weaveffi_jni_utf8_to_string(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 = (jobject)weaveffi_jni_utf8_to_string(env, result); - 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); - free(ctx); - weaveffi_jni_detach(attached); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_doCancellableAsync(JNIEnv* env, jclass clazz, jstring input, jlong cancelToken, jobject callback) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); - ctx->callback = (*env)->NewGlobalRef(env, callback); - char* input_chars = weaveffi_jni_string_to_utf8(env, input); - weaveffi_kitchen_do_cancellable_async(input_chars, (weaveffi_cancel_token*)(intptr_t)cancelToken, weaveffi_kitchen_do_cancellable_jni_cb, ctx); - free(input_chars); -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_WeaveFFI_legacyOp(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_kitchen_legacy_op(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_WeaveFFI_newOp(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_kitchen_new_op(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jstring JNICALL Java_com_weaveffi_WeaveFFI_hello(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* rv = weaveffi_kitchen_nested_hello(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jstring out = weaveffi_jni_utf8_to_string(env, rv); - weaveffi_free_string(rv); - return out; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Gadget_nativeNew(JNIEnv* env, jclass clazz, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - weaveffi_kitchen_Gadget* rv = weaveffi_kitchen_Gadget_new((int64_t)id, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong)(intptr_t)rv; -} - -JNIEXPORT jstring JNICALL Java_com_weaveffi_Gadget_nativeVersion(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* rv = weaveffi_kitchen_Gadget_version(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jstring out = weaveffi_jni_utf8_to_string(env, rv); - weaveffi_free_string(rv); - return out; -} - -JNIEXPORT jstring JNICALL Java_com_weaveffi_Gadget_nativeDescribe(JNIEnv* env, jclass clazz, jlong selfHandle) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* rv = weaveffi_kitchen_Gadget_describe((const weaveffi_kitchen_Gadget*)(intptr_t)selfHandle, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jstring out = weaveffi_jni_utf8_to_string(env, rv); - weaveffi_free_string(rv); - return out; -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_Gadget_nativePoke(JNIEnv* env, jclass clazz, jlong selfHandle, jint times) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_kitchen_Gadget_poke((const weaveffi_kitchen_Gadget*)(intptr_t)selfHandle, (int32_t)times, &err); - if (err.code != 0) { - throw_weaveffi_kitchen_KitchenErrors(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Gadget_nativeClone(JNIEnv* env, jclass clazz, jlong handle) { - return (jlong)(intptr_t)weaveffi_kitchen_Gadget_clone((const weaveffi_kitchen_Gadget*)(intptr_t)handle); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_Gadget_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_kitchen_Gadget_destroy((weaveffi_kitchen_Gadget*)(intptr_t)handle); -} - -JNIEXPORT jobjectArray JNICALL Java_com_weaveffi_KitchenStreamGadgetsIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_kitchen_StreamGadgetsIterator* _iter = (weaveffi_kitchen_StreamGadgetsIterator*)(intptr_t)handle; - weaveffi_kitchen_Gadget* _item = (weaveffi_kitchen_Gadget*)0; - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t _has = weaveffi_kitchen_StreamGadgetsIterator_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/Long"); - jobject _jitem = (*env)->CallStaticObjectMethod(env, _jitem_cls, (*env)->GetStaticMethodID(env, _jitem_cls, "valueOf", "(J)Ljava/lang/Long;"), (jlong)(intptr_t)_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_KitchenStreamGadgetsIterator_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_kitchen_StreamGadgetsIterator_destroy((weaveffi_kitchen_StreamGadgetsIterator*)(intptr_t)handle); -} - -JNIEXPORT jobjectArray JNICALL Java_com_weaveffi_KitchenStreamItemsIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_kitchen_StreamItemsIterator* _iter = (weaveffi_kitchen_StreamItemsIterator*)(intptr_t)handle; - const uint8_t* _item = (const uint8_t*)0; - size_t _item_len = 0; - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t _has = weaveffi_kitchen_StreamItemsIterator_next(_iter, &_item, &_item_len, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - if (_has == 0) { return NULL; } - jbyteArray _jitem = (*env)->NewByteArray(env, (jsize)_item_len); - if (_jitem && _item) { (*env)->SetByteArrayRegion(env, _jitem, 0, (jsize)_item_len, (const jbyte*)_item); } - weaveffi_free_bytes((uint8_t*)_item, _item_len); - 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_KitchenStreamItemsIterator_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_kitchen_StreamItemsIterator_destroy((weaveffi_kitchen_StreamItemsIterator*)(intptr_t)handle); -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index 2ad67787..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,577 +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 { WeaveNativeLibrary.ensureLoaded() } - - @Volatile private var callbackExceptionHandler: ((Throwable) -> Unit)? = null - - /** - * Installs a handler for exceptions thrown while an async result is - * delivered on a native producer thread. 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. Exceptions thrown by callback interface methods are - * not routed here: they abort the producer call that invoked them and - * surface to the original caller as a [WeaveFFIException] with code -4. - */ - @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 - } - - /** - * Trivial cross-module helper - */ - @JvmStatic external fun ping(): String - /** - * Identity for bool - */ - @JvmStatic external fun boolId(x: Boolean): Boolean - @JvmStatic external fun i32Id(x: Int): Int - @JvmStatic external fun u32Id(x: Long): Long - @JvmStatic external fun i64Id(x: Long): Long - @JvmStatic external fun f64Id(x: Double): Double - @JvmStatic external fun echoString(s: String): String - @JvmStatic external fun echoBytes(b: ByteArray): ByteArray - /** - * Register a listener, returning how many are attached - */ - @JvmStatic external fun subscribe(listener: ReadyListener): Int - /** - * Optional object in both directions - */ - @JvmStatic private external fun maybeGadgetJni(fallback: Long?): Long? - @JvmStatic fun maybeGadget(fallback: Gadget?): Gadget? = maybeGadgetJni(fallback?.handle)?.let { Gadget.fromHandle(it) } - /** - * A list of objects - */ - @JvmStatic private external fun gadgetsJni(): ByteArray - @JvmStatic fun gadgets(): List = weaveDecode(gadgetsJni()) { r -> r.readList { Gadget.fromHandle(r.readObject()) } } - /** - * An iterator over objects - */ - @JvmStatic private external fun streamGadgetsJni(): Long - @JvmStatic fun streamGadgets(): Iterator = KitchenStreamGadgetsIterator(streamGadgetsJni()) - /** - * Optional struct return - */ - @JvmStatic private external fun maybeItemJni(id: Long): ByteArray - @JvmStatic fun maybeItem(id: Long): Item? = weaveDecode(maybeItemJni(id)) { r -> r.readOptional { unpackItem(r) } } - /** - * List of structs - */ - @JvmStatic private external fun listItemsJni(): ByteArray - @JvmStatic fun listItems(): List = weaveDecode(listItemsJni()) { r -> r.readList { unpackItem(r) } } - /** - * Map return type - */ - @JvmStatic private external fun getAttrsJni(): ByteArray - @JvmStatic fun getAttrs(): Map = weaveDecode(getAttrsJni()) { r -> r.readMap({ r.readString() }, { r.readI32() }) } - /** - * Iterator return type - */ - @JvmStatic private external fun streamItemsJni(): Long - @JvmStatic fun streamItems(): Iterator = KitchenStreamItemsIterator(streamItemsJni()) - /** - * Returns the shared Token type from another module - */ - @JvmStatic private external fun crossModuleTokenJni(): ByteArray - @JvmStatic fun crossModuleToken(): Token = weaveDecode(crossModuleTokenJni()) { r -> unpackToken(r) } - /** - * Async operation - */ - @JvmStatic private external fun doAsyncAsync(input: String, callback: Any) - @JvmStatic suspend fun doAsync(input: String): String = suspendCancellableCoroutine { cont -> - doAsyncAsync(input, WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) - } - /** - * Cancellable async operation - */ - @JvmStatic private external fun doCancellableAsync(input: String, cancelToken: Long, callback: Any) - @JvmStatic suspend fun doCancellable(input: String): String = suspendCancellableCoroutine { cont -> - doCancellableAsync(input, 0L, WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) - } - /** - * Legacy operation kept for compatibility - */ - @Deprecated("Use new_op instead") - @JvmStatic external fun legacyOp(): Int - /** - * Replacement for legacy_op - */ - @JvmStatic external fun newOp(): Int - /** - * Trivial nested-module function - */ - @JvmStatic external fun hello(): String - } -} - -/** A reusable token referenced across modules */ -data class Token(val id: Long, val label: String) - -internal fun packToken(w: WeaveBufferWriter, v: Token) { - w.writeI64(v.id) - w.writeString(v.label) -} - -internal fun unpackToken(r: WeaveBufferReader): Token = Token( - r.readI64(), - r.readString(), -) - -/** Task priority levels */ -enum class Priority(val value: Int) { - /** Low priority */ - Low(0), - /** Normal priority */ - Normal(1), - /** High priority */ - High(2); - - companion object { - fun fromValue(value: Int): Priority = entries.first { it.value == value } - } -} - -/** Kitchen sink struct exercising every field feature */ -data class Item( - /** Stable identifier */ - val id: Long, - /** Display name */ - val name: String, - /** Initial count */ - val count: Int, - /** Whether the item is enabled */ - val enabled: Boolean, - /** Scaling ratio */ - val ratio: Double, - val nick: Long, - val payload: ByteArray, - val tags: List, - val attrs: Map, - val parent: Long?, - /** Cross-module struct reference */ - val token: Token, - val priority: Priority, - /** An optional object inside a record */ - val gadget: Gadget?, -) - -internal fun packItem(w: WeaveBufferWriter, v: Item) { - w.writeI64(v.id) - w.writeString(v.name) - w.writeI32(v.count) - w.writeBool(v.enabled) - w.writeF64(v.ratio) - w.writeU32(v.nick) - w.writeBytes(v.payload) - w.writeList(v.tags) { v0 -> w.writeString(v0) } - w.writeMap(v.attrs, { k0 -> w.writeString(k0) }, { v0 -> w.writeString(v0) }) - w.writeOptional(v.parent) { v0 -> w.writeI64(v0) } - packToken(w, v.token) - w.writeI32(v.priority.value) - w.writeOptional(v.gadget) { v0 -> w.writeI64(v0.cloneHandle()) } -} - -internal fun unpackItem(r: WeaveBufferReader): Item = Item( - r.readI64(), - r.readString(), - r.readI32(), - r.readBool(), - r.readF64(), - r.readU32(), - r.readBytes(), - r.readList { r.readString() }, - r.readMap({ r.readString() }, { r.readString() }), - r.readOptional { r.readI64() }, - unpackToken(r), - Priority.fromValue(r.readI32()), - r.readOptional { Gadget.fromHandle(r.readObject()) }, -) - -/** Consumer-implemented observer of ready items */ -interface ReadyListener { - /** - * Fires when an item is ready - */ - fun onReady(code: Int, msg: String) - /** - * Receives the item itself and says whether to keep listening - */ - fun onItem(item: Item, gadget: Gadget): Boolean -} - -/** JNI dispatch shims for [ReadyListener]; the native vtable trampolines call these, nothing else does. */ -internal object ReadyListenerJni { - @JvmStatic fun onReady(impl: ReadyListener, code: Int, msg: String) { impl.onReady(code, msg) } - @JvmStatic fun onItem(impl: ReadyListener, item: ByteArray, gadget: Long): Boolean = impl.onItem(weaveDecode(item) { r -> unpackItem(r) }, Gadget.fromHandle(gadget)) -} - -/** A pocket-sized interface exercising the object surface */ -class Gadget private constructor(handle: Long) : AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - - /** The native pointer, borrowed for one call; throws after [close]. */ - internal val handle: Long get() = ref.get() - - /** A new strong reference, written into value buffers as an object token. */ - internal fun cloneHandle(): Long = nativeClone(ref.get()) - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - /** Adopts one strong native reference (a constructor return, a `_clone`, or a buffer token) into a wrapper that owes its `_destroy`. */ - internal fun fromHandle(handle: Long): Gadget = Gadget(handle) - @JvmStatic private external fun nativeNew(id: Long): Long - @JvmStatic private external fun nativeVersion(): String - @JvmStatic private external fun nativeDescribe(selfHandle: Long): String - @JvmStatic private external fun nativePoke(selfHandle: Long, times: Int): Int - @JvmStatic private external fun nativeClone(handle: Long): Long - @JvmStatic private external fun nativeDestroy(handle: Long) - - /** - * Create a gadget with the given id - */ - operator fun invoke(id: Long): Gadget = Gadget.fromHandle(nativeNew(id)) - - /** - * The gadget subsystem version string - */ - fun version(): String = nativeVersion() - } - - /** - * Render the gadget as a human-readable string - */ - fun describe(): String = nativeDescribe(handle) - - /** - * Poke the gadget a number of times, failing on a negative count - */ - fun poke(times: Int): Int = nativePoke(handle, times) - - /** Releases this wrapper's native reference; safe to call more than once. */ - override fun close() { - cleanable.clean() - } -} - -/** - * A lazy iterator over the `Gadget` elements streamed by [streamGadgets]. 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 `Cleaner` if the iterator is abandoned, whichever comes first. - */ -class KitchenStreamGadgetsIterator internal constructor(handle: Long) : Iterator, AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (ref.peek() == 0L) return false - val slot = nativeNext(ref.get()) - if (slot == null) { - close() - return false - } - nextSlot = slot - return true - } - - override fun next(): Gadget { - if (!hasNext()) throw NoSuchElementException() - val raw = nextSlot!![0] - nextSlot = null - return Gadget.fromHandle(raw as Long) - } - - /** Releases the native iterator; safe to call more than once. */ - override fun close() { - cleanable.clean() - } - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - @JvmStatic private external fun nativeNext(handle: Long): Array? - @JvmStatic private external fun nativeDestroy(handle: Long) - } -} - -/** - * A lazy iterator over the `Item` elements streamed by [streamItems]. 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 `Cleaner` if the iterator is abandoned, whichever comes first. - */ -class KitchenStreamItemsIterator internal constructor(handle: Long) : Iterator, AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (ref.peek() == 0L) return false - val slot = nativeNext(ref.get()) - if (slot == null) { - close() - return false - } - nextSlot = slot - return true - } - - override fun next(): Item { - if (!hasNext()) throw NoSuchElementException() - val raw = nextSlot!![0] - nextSlot = null - return weaveDecode((raw as ByteArray)) { r -> unpackItem(r) } - } - - /** Releases the native iterator; safe to call more than once. */ - override fun close() { - cleanable.clean() - } - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - @JvmStatic private external fun nativeNext(handle: Long): Array? - @JvmStatic private external fun nativeDestroy(handle: Long) - } -} - -/** Generic WeaveFFI failure: panics, marshalling errors, callback implementation failures (code -4), and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Typed error domain `KitchenErrors` declared by module `kitchen`. */ -sealed class KitchenErrorsException(code: Int, message: String) : WeaveFFIException(code, message) { - class NotFound(message: String = "Item not found") : KitchenErrorsException(1, message) - class InvalidInput(message: String = "Invalid input") : KitchenErrorsException(2, message) - - companion object { - /** Map a raw `KitchenErrors` 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 -> NotFound(message) - 2 -> InvalidInput(message) - else -> WeaveFFIException(code, message) - } - } -} - -/** Loads the native library once; every companion `init` calls [ensureLoaded]. */ -internal object WeaveNativeLibrary { - init { System.loadLibrary("weaveffi") } - - fun ensureLoaded() {} -} - -/** - * One strong native reference, released exactly once. Wrappers register an - * instance with [weaveCleaner] so an unreachable, never-closed wrapper still - * releases its reference; an explicit `close()` runs the same action first - * and the cleaner then does nothing. - */ -internal class WeaveNativeRef(handle: Long, private val release: (Long) -> Unit) : Runnable { - private val handle = java.util.concurrent.atomic.AtomicLong(handle) - - /** The live pointer, borrowed for the duration of one native call. */ - fun get(): Long { - val h = handle.get() - check(h != 0L) { "WeaveFFI object used after close()" } - return h - } - - /** The live pointer, or `0L` once released. */ - fun peek(): Long = handle.get() - - override fun run() { - val h = handle.getAndSet(0L) - if (h != 0L) release(h) - } -} - -/** The process-wide cleaner backing every generated wrapper's disposal backstop. */ -internal val weaveCleaner: java.lang.ref.Cleaner = java.lang.ref.Cleaner.create() - -/** 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()) - /** An object token: one strong reference the caller adopts. Zero is never valid here. */ - fun readObject(): Long { - val token = readI64() - if (token == 0L) malformed("null object token") - return token - } - 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/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Async_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Async_kt.snap new file mode 100644 index 00000000..1b5bacb0 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Async_kt.snap @@ -0,0 +1,124 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Bridges one native async call to a suspended coroutine. The JNI shim pins + * this object with a global reference and calls exactly one `on*` method, + * from whichever producer thread completes the call. + */ +internal class NativeCompletion( + private val cont: CancellableContinuation, + private val domain: Int, + private val convert: (Any?) -> T, + cancellable: Boolean, +) { + // The native cancel token, or 0 when the call isn't cancellable or has + // completed. Guarded by `this` so a late cancel never touches a freed + // token; a completion that lands inside `cancel` (on the same thread) + // defers the release until the cancel returns. + private var token: Long = if (cancellable) JniBridge.cancel_token_create() else 0L + private var cancelling = false + private var released = false + + val cancelToken: Long + get() = synchronized(this) { token } + + fun cancel() { + synchronized(this) { + if (token == 0L) return + cancelling = true + try { + JniBridge.cancel_token_cancel(token) + } finally { + cancelling = false + } + if (released) destroyToken() + } + } + + fun releaseToken() { + synchronized(this) { + if (cancelling) released = true else destroyToken() + } + } + + private fun destroyToken() { + if (token != 0L) JniBridge.cancel_token_destroy(token) + token = 0L + } + + fun onUnit() = succeed(Unit) + + fun onBoolean(value: Boolean) = succeed(value) + + fun onByte(value: Byte) = succeed(value) + + fun onShort(value: Short) = succeed(value) + + fun onInt(value: Int) = succeed(value) + + fun onLong(value: Long) = succeed(value) + + fun onFloat(value: Float) = succeed(value) + + fun onDouble(value: Double) = succeed(value) + + fun onBytes(value: ByteArray) = succeed(value) + + fun onError(code: Int, message: ByteArray, payload: ByteArray?) { + releaseToken() + val error = if (code == -5) { + CancellationException(decodeUtf8(message)) + } else { + JniBridge.error(domain, code, message, payload) + } + cont.resumeWithException(error) + } + + private fun succeed(raw: Any?) { + releaseToken() + val result = try { + convert(raw) + } catch (t: Throwable) { + cont.resumeWithException(t) + return + } + cont.resume(result) + } +} + +/** + * Starts a native async call and suspends until it completes. [launch] gets + * the cancel token (0 unless [cancellable]) and the completion to hand to the + * shim. Cancelling the coroutine cancels the native call; a cancelled call + * (code -5) resumes with [CancellationException]. + */ +internal suspend fun awaitNative( + cancellable: Boolean, + domain: Int, + convert: (Any?) -> T, + launch: (Long, NativeCompletion) -> Unit, +): T = suspendCancellableCoroutine { cont -> + val completion = NativeCompletion(cont, domain, convert, cancellable) + cont.invokeOnCancellation { completion.cancel() } + try { + launch(completion.cancelToken, completion) + } catch (t: Throwable) { + completion.releaseToken() + throw t + } +} + +// END Async.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Buffers_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Buffers_kt.snap new file mode 100644 index 00000000..f712dd29 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Buffers_kt.snap @@ -0,0 +1,255 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +/** + * Growable little-endian writer for the value-buffer wire format that + * records, rich enums, optionals, lists, and maps cross the ABI in. + */ +internal class BufferWriter { + private var buf = ByteArray(64) + private var len = 0 + private var minted: ArrayList Unit>>? = null + + private fun reserve(extra: Int) { + if (len + extra <= buf.size) return + var cap = buf.size * 2 + while (cap < len + extra) cap *= 2 + buf = buf.copyOf(cap) + } + + fun toByteArray(): ByteArray = buf.copyOf(len) + + fun writeBool(v: Boolean) { + reserve(1) + buf[len++] = if (v) 1 else 0 + } + + fun writeI8(v: Byte) { + reserve(1) + buf[len++] = v + } + + fun writeU8(v: Byte) = writeI8(v) + + fun writeI16(v: Short) { + reserve(2) + val b = v.toInt() + buf[len++] = b.toByte() + buf[len++] = (b shr 8).toByte() + } + + fun writeU16(v: Short) = writeI16(v) + + fun writeI32(v: Int) { + reserve(4) + buf[len++] = v.toByte() + buf[len++] = (v shr 8).toByte() + buf[len++] = (v shr 16).toByte() + buf[len++] = (v shr 24).toByte() + } + + fun writeU32(v: Long) = writeI32(v.toInt()) + + fun writeI64(v: Long) { + writeI32(v.toInt()) + writeI32((v ushr 32).toInt()) + } + + fun writeU64(v: Long) = writeI64(v) + + fun writeF32(v: Float) = writeI32(v.toRawBits()) + + fun writeF64(v: Double) = writeI64(v.toRawBits()) + + fun writeString(v: String) = writeBytes(encodeUtf8(v)) + + fun writeBytes(v: ByteArray) { + writeI32(v.size) + reserve(v.size) + v.copyInto(buf, len) + len += v.size + } + + /** + * An object token: one new strong reference the reader adopts, which + * [abandon] releases with [release] if the encoding fails. + */ + fun writeObject(token: Long, release: (Long) -> Unit) { + val list = minted ?: ArrayList Unit>>().also { minted = it } + list.add(token to release) + writeI64(token) + } + + /** Releases every object token written so far, for an encoding that failed midway. */ + fun abandon() { + minted?.forEach { (token, release) -> release(token) } + minted = null + } + + inline fun writeOptional(v: T?, write: (T) -> Unit) { + if (v == null) { + writeBool(false) + } else { + writeBool(true) + write(v) + } + } + + inline fun writeList(v: List, write: (T) -> Unit) { + writeI32(v.size) + for (e in v) write(e) + } + + inline fun writeMap(v: Map, writeKey: (K) -> Unit, writeValue: (V) -> Unit) { + writeI32(v.size) + for ((k, e) in v) { + writeKey(k) + writeValue(e) + } + } +} + +/** + * Validating little-endian reader for the value-buffer wire format. A + * truncated buffer, an invalid flag byte, a zero object token, an oversized + * length, or trailing bytes raise [FfiException] code -3. + */ +internal class BufferReader(private val buf: ByteArray) { + private var pos = 0 + + private fun malformed(detail: String): Nothing = + throw FfiException(-3, "malformed value buffer: $detail") + + private fun take(n: Int): Int { + if (n > buf.size - pos) malformed("truncated buffer") + val at = pos + pos += n + return at + } + + fun readBool(): Boolean = when (buf[take(1)].toInt()) { + 0 -> false + 1 -> true + else -> malformed("invalid bool byte") + } + + fun readI8(): Byte = buf[take(1)] + + fun readU8(): Byte = readI8() + + fun readI16(): Short { + val at = take(2) + return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() + } + + fun readU16(): Short = readI16() + + 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 readU64(): Long = readI64() + + fun readF32(): Float = Float.fromBits(readI32()) + + fun readF64(): Double = Double.fromBits(readI64()) + + private fun readLen(): Int { + val n = readI32() + if (n < 0 || n > buf.size - pos) malformed("length prefix exceeds remaining bytes") + return n + } + + fun readString(): String { + val n = readLen() + val at = take(n) + return try { + Charsets.UTF_8.newDecoder().decode(java.nio.ByteBuffer.wrap(buf, at, n)).toString() + } catch (e: java.nio.charset.CharacterCodingException) { + malformed("string is not valid UTF-8") + } + } + + fun readBytes(): ByteArray { + val n = readLen() + val at = take(n) + return buf.copyOfRange(at, at + n) + } + + /** An object token: one strong reference the caller adopts. */ + fun readObject(): Long { + val token = readI64() + if (token == 0L) malformed("null object token") + return token + } + + inline fun readOptional(read: () -> T): T? = if (readBool()) read() else null + + inline fun readList(read: () -> T): List { + val n = readCount() + val out = ArrayList(minOf(n, 4096)) + repeat(n) { out.add(read()) } + return out + } + + inline fun readMap(readKey: () -> K, readValue: () -> V): Map { + val n = readCount() + val out = LinkedHashMap(minOf(n, 4096)) + repeat(n) { + val k = readKey() + out[k] = readValue() + } + return out + } + + /** An element count; elements may encode to zero bytes, so only the sign is checked. */ + fun readCount(): Int { + val n = readI32() + if (n < 0) malformed("negative element count") + return n + } + + fun expectEnd() { + if (pos != buf.size) malformed("trailing bytes after value") + } +} + +/** Encodes one value into a fresh value buffer. */ +internal inline fun encodeBuffer(write: (BufferWriter) -> Unit): ByteArray { + val w = BufferWriter() + try { + write(w) + } catch (t: Throwable) { + w.abandon() + throw t + } + return w.toByteArray() +} + +/** Decodes one value from [bytes], rejecting malformed or trailing bytes. */ +internal inline fun decodeBuffer(bytes: ByteArray, read: (BufferReader) -> T): T { + val r = BufferReader(bytes) + val v = read(r) + r.expectEnd() + return v +} + +// END Buffers.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_JniBridge_kt.snap new file mode 100644 index 00000000..c7b0826d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_JniBridge_kt.snap @@ -0,0 +1,79 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +/** + * The JNI entry points of `libkitchen_sink_jni`: one native per C symbol, named after + * it, plus the hooks the shim calls back into. Internal plumbing for the + * public wrappers. + */ +internal object JniBridge { + init { + NativeLibrary.ensureLoaded() + } + + /** The exception for a native failure; domain 0 is the generic one. */ + @JvmStatic + @Suppress("UNUSED_PARAMETER") + fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): FfiException { + val text = decodeUtf8(message) + return when (domain) { + 1 -> KitchenErrorsException.fromCode(code, text, payload) + else -> FfiException(code, text) + } + } + + /** The message a failed callback reports to the producer (code -4). */ + @JvmStatic + fun describe(error: Throwable): ByteArray = encodeUtf8(error.toString()) + + @JvmStatic external fun debug_live(kind: Int): Long + @JvmStatic external fun cancel_token_create(): Long + @JvmStatic external fun cancel_token_cancel(token: Long) + @JvmStatic external fun cancel_token_destroy(token: Long) + @JvmStatic external fun shared_ping(): ByteArray + @JvmStatic external fun kitchen_Gadget_clone(_self: Long): Long + @JvmStatic external fun kitchen_Gadget_destroy(_self: Long) + @JvmStatic external fun kitchen_bool_id(x: Boolean): Boolean + @JvmStatic external fun kitchen_i32_id(x: Int): Int + @JvmStatic external fun kitchen_u32_id(x: Long): Long + @JvmStatic external fun kitchen_i64_id(x: Long): Long + @JvmStatic external fun kitchen_f64_id(x: Double): Double + @JvmStatic external fun kitchen_echo_string(s: ByteArray): ByteArray + @JvmStatic external fun kitchen_echo_bytes(b: ByteArray): ByteArray + @JvmStatic external fun kitchen_subscribe(listener: ReadyListener): Int + @JvmStatic external fun kitchen_maybe_gadget(fallback: Long): Long + @JvmStatic external fun kitchen_gadgets(): ByteArray + @JvmStatic external fun kitchen_stream_gadgets(): Long + @JvmStatic external fun kitchen_StreamGadgetsIterator_next(_iter: Long, _out: LongArray): Boolean + @JvmStatic external fun kitchen_StreamGadgetsIterator_destroy(_iter: Long) + @JvmStatic external fun kitchen_maybe_item(id: Long): ByteArray + @JvmStatic external fun kitchen_list_items(): ByteArray + @JvmStatic external fun kitchen_get_attrs(): ByteArray + @JvmStatic external fun kitchen_stream_items(): Long + @JvmStatic external fun kitchen_StreamItemsIterator_next(_iter: Long): ByteArray? + @JvmStatic external fun kitchen_StreamItemsIterator_destroy(_iter: Long) + @JvmStatic external fun kitchen_cross_module_token(): ByteArray + @JvmStatic external fun kitchen_do_async(input: ByteArray, _done: NativeCompletion<*>) + @JvmStatic external fun kitchen_do_cancellable(input: ByteArray, _token: Long, _done: NativeCompletion<*>) + @JvmStatic external fun kitchen_legacy_op(): Int + @JvmStatic external fun kitchen_new_op(): Int + @JvmStatic external fun kitchen_Gadget_new(id: Long): Long + @JvmStatic external fun kitchen_Gadget_describe(_self: Long): ByteArray + @JvmStatic external fun kitchen_Gadget_poke(_self: Long, times: Int): Int + @JvmStatic external fun kitchen_Gadget_version(): ByteArray + @JvmStatic external fun kitchen_nested_hello(): ByteArray + + @JvmStatic + fun kitchen_ReadyListener_on_ready(_impl: ReadyListener, code: Int, msg: ByteArray) { _impl.onReady(code, decodeUtf8(msg)) } + + @JvmStatic + fun kitchen_ReadyListener_on_item(_impl: ReadyListener, item: ByteArray, gadget: Long): Boolean = _impl.onItem(decodeBuffer(item) { _r -> unpackItem(_r) }, Gadget.fromHandle(gadget)) +} + +// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Kitchen_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Kitchen_kt.snap new file mode 100644 index 00000000..68db0534 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Kitchen_kt.snap @@ -0,0 +1,215 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +/** Errors of the `KitchenErrors` domain. */ +sealed class KitchenErrorsException(code: Int, message: String) : FfiException(code, message) { + class NotFound(message: String = "Item not found") : KitchenErrorsException(1, message) + class InvalidInput(message: String = "Invalid input") : KitchenErrorsException(2, message) + + internal companion object { + /** The exception for a raw code and its serialized payload. */ + fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) { + 1 -> NotFound(message) + 2 -> InvalidInput(message) + else -> FfiException(code, message) + } + } +} + +/** Task priority levels */ +enum class Priority(val value: Int) { + /** Low priority */ + Low(0), + /** Normal priority */ + Normal(1), + /** High priority */ + High(2); + + companion object { + /** The variant whose ABI value is [value]. */ + fun fromValue(value: Int): Priority = entries.firstOrNull { it.value == value } + ?: throw FfiException(-3, "unknown Priority value $value") + } +} + +/** Kitchen sink struct exercising every field feature */ +data class Item( + /** Stable identifier */ + val id: Long, + /** Display name */ + val name: String, + /** Initial count */ + val count: Int, + /** Whether the item is enabled */ + val enabled: Boolean, + /** Scaling ratio */ + val ratio: Double, + val nick: Long, + val payload: ByteArray, + val tags: List, + val attrs: Map, + val parent: Long?, + /** Cross-module struct reference */ + val token: Token, + val priority: Priority, + /** An optional object inside a record */ + val gadget: Gadget?, +) + +internal fun packItem(_w: BufferWriter, _v: Item) { + _w.writeI64(_v.id) + _w.writeString(_v.name) + _w.writeI32(_v.count) + _w.writeBool(_v.enabled) + _w.writeF64(_v.ratio) + _w.writeU32(_v.nick) + _w.writeBytes(_v.payload) + _w.writeList(_v.tags) { _v0 -> _w.writeString(_v0) } + _w.writeMap(_v.attrs, { _k0 -> _w.writeString(_k0) }, { _v0 -> _w.writeString(_v0) }) + _w.writeOptional(_v.parent) { _v0 -> _w.writeI64(_v0) } + packToken(_w, _v.token) + _w.writeI32(_v.priority.value) + _w.writeOptional(_v.gadget) { _v0 -> _w.writeObject(_v0.cloneHandle(), JniBridge::kitchen_Gadget_destroy) } +} + +internal fun unpackItem(_r: BufferReader): Item = Item( + _r.readI64(), + _r.readString(), + _r.readI32(), + _r.readBool(), + _r.readF64(), + _r.readU32(), + _r.readBytes(), + _r.readList { _r.readString() }, + _r.readMap({ _r.readString() }, { _r.readString() }), + _r.readOptional { _r.readI64() }, + unpackToken(_r), + Priority.fromValue(_r.readI32()), + _r.readOptional { Gadget.fromHandle(_r.readObject()) }, +) + +/** Consumer-implemented observer of ready items */ +interface ReadyListener { + /** Fires when an item is ready */ + fun onReady(code: Int, msg: String) + + /** Receives the item itself and says whether to keep listening */ + fun onItem(item: Item, gadget: Gadget): Boolean +} + +/** A pocket-sized interface exercising the object surface */ +class Gadget private constructor(address: Long) : AutoCloseable { + internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::kitchen_Gadget_destroy)) + + /** A new strong reference to this object, as written into a value buffer. */ + internal fun cloneHandle(): Long = handle.borrow { JniBridge.kitchen_Gadget_clone(it) } + + /** Render the gadget as a human-readable string */ + fun describe(): String = handle.borrow { _self -> + decodeUtf8(JniBridge.kitchen_Gadget_describe(_self)) + } + + /** Poke the gadget a number of times, failing on a negative count */ + fun poke(times: Int): Int = handle.borrow { _self -> + JniBridge.kitchen_Gadget_poke(_self, times) + } + + /** Releases this wrapper's native reference; safe to call more than once. */ + override fun close() = handle.close() + + companion object { + /** Adopts one strong reference (a return value or a buffer token). */ + internal fun fromHandle(address: Long): Gadget = Gadget(address) + + /** [fromHandle], with address 0 meaning no object. */ + internal fun fromHandleOrNull(address: Long): Gadget? = if (address == 0L) null else Gadget(address) + + /** Create a gadget with the given id */ + operator fun invoke(id: Long): Gadget = Gadget.fromHandle(JniBridge.kitchen_Gadget_new(id)) + + /** The gadget subsystem version string */ + fun version(): String = decodeUtf8(JniBridge.kitchen_Gadget_version()) + } +} + +object Kitchen { + /** Identity for bool */ + fun boolId(x: Boolean): Boolean = JniBridge.kitchen_bool_id(x) + + fun i32Id(x: Int): Int = JniBridge.kitchen_i32_id(x) + + fun u32Id(x: Long): Long = JniBridge.kitchen_u32_id(x) + + fun i64Id(x: Long): Long = JniBridge.kitchen_i64_id(x) + + fun f64Id(x: Double): Double = JniBridge.kitchen_f64_id(x) + + fun echoString(s: String): String = decodeUtf8(JniBridge.kitchen_echo_string(encodeUtf8(s))) + + fun echoBytes(b: ByteArray): ByteArray = JniBridge.kitchen_echo_bytes(b) + + /** Register a listener, returning how many are attached */ + fun subscribe(listener: ReadyListener): Int = JniBridge.kitchen_subscribe(listener) + + /** Optional object in both directions */ + fun maybeGadget(fallback: Gadget?): Gadget? = fallback?.handle.borrowOrNull { _h0 -> + Gadget.fromHandleOrNull(JniBridge.kitchen_maybe_gadget(_h0)) + } + + /** A list of objects */ + fun gadgets(): List = decodeBuffer(JniBridge.kitchen_gadgets()) { _r -> _r.readList { Gadget.fromHandle(_r.readObject()) } } + + /** An iterator over objects */ + fun streamGadgets(): NativeIterator = NativeIterator(JniBridge.kitchen_stream_gadgets(), JniBridge::kitchen_StreamGadgetsIterator_destroy) { _it -> + val _slot = LongArray(1) + if (JniBridge.kitchen_StreamGadgetsIterator_next(_it, _slot)) Gadget.fromHandle(_slot[0]) else NativeIterator.DONE + } + + /** Optional struct return */ + fun maybeItem(id: Long): Item? = decodeBuffer(JniBridge.kitchen_maybe_item(id)) { _r -> _r.readOptional { unpackItem(_r) } } + + /** List of structs */ + fun listItems(): List = decodeBuffer(JniBridge.kitchen_list_items()) { _r -> _r.readList { unpackItem(_r) } } + + /** Map return type */ + fun getAttrs(): Map = decodeBuffer(JniBridge.kitchen_get_attrs()) { _r -> _r.readMap({ _r.readString() }, { _r.readI32() }) } + + /** Iterator return type */ + fun streamItems(): NativeIterator = NativeIterator(JniBridge.kitchen_stream_items(), JniBridge::kitchen_StreamItemsIterator_destroy) { _it -> + val _item = JniBridge.kitchen_StreamItemsIterator_next(_it) + if (_item == null) NativeIterator.DONE else decodeBuffer(_item) { _r -> unpackItem(_r) } + } + + /** Returns the shared Token type from another module */ + fun crossModuleToken(): Token = decodeBuffer(JniBridge.kitchen_cross_module_token()) { _r -> unpackToken(_r) } + + /** Async operation */ + suspend fun doAsync(input: String): String = awaitNative(false, 0, { decodeUtf8(it as ByteArray) }) { _, _done -> + JniBridge.kitchen_do_async(encodeUtf8(input), _done) + } + + /** Cancellable async operation */ + suspend fun doCancellable(input: String): String = awaitNative(true, 0, { decodeUtf8(it as ByteArray) }) { _token, _done -> + JniBridge.kitchen_do_cancellable(encodeUtf8(input), _token, _done) + } + + /** Legacy operation kept for compatibility */ + @Deprecated("Use new_op instead") + fun legacyOp(): Int = JniBridge.kitchen_legacy_op() + + /** Replacement for legacy_op */ + fun newOp(): Int = JniBridge.kitchen_new_op() + + object Nested { + /** Trivial nested-module function */ + fun hello(): String = decodeUtf8(JniBridge.kitchen_nested_hello()) + } +} + +// END Kitchen.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Runtime_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Runtime_kt.snap new file mode 100644 index 00000000..37419793 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Runtime_kt.snap @@ -0,0 +1,212 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * A failure reported by the native library: a producer panic (code -2), a + * marshalling failure (-3), a callback implementation that threw (-4), or any + * code these bindings don't know. Typed error domains subclass it. + */ +open class FfiException(val code: Int, message: String) : Exception(message) + +/** + * A lazy sequence streamed from the native library, one element per step. + * The native iterator is released when the sequence is exhausted, when + * [close] is called, or once this object becomes unreachable, whichever + * comes first. + */ +class NativeIterator internal constructor( + address: Long, + release: (Long) -> Unit, + private val step: (Long) -> Any?, +) : Iterator, AutoCloseable { + private val handle = NativeCleaner.register(this, NativeHandle(address, release)) + private var lookahead: Any? = EMPTY + + override fun hasNext(): Boolean { + if (lookahead !== EMPTY) return true + if (handle.isClosed) return false + val item = handle.borrow(step) + if (item === DONE) { + close() + return false + } + lookahead = item + return true + } + + override fun next(): T { + if (!hasNext()) throw NoSuchElementException() + val item = lookahead + lookahead = EMPTY + @Suppress("UNCHECKED_CAST") + return item as T + } + + /** Releases the native iterator; safe to call more than once. */ + override fun close() = handle.close() + + internal companion object { + /** What a step function returns once the native sequence is exhausted. */ + val DONE = Any() + private val EMPTY = Any() + } +} + +/** Loads the producer library and then the JNI shim, exactly once. */ +internal object NativeLibrary { + private const val LIBRARY = "kitchen_sink" + private const val JNI_LIBRARY = "kitchen_sink_jni" + + init { + val explicit = System.getenv("KITCHEN_SINK_LIBRARY") + if (!explicit.isNullOrEmpty()) { + System.load(explicit) + } else if (!loadBundled(LIBRARY)) { + // The shim's own link dependency usually finds the producer when + // it isn't on java.library.path, so a miss here is only fatal if + // loading the shim fails too. + try { + System.loadLibrary(LIBRARY) + } catch (ignored: UnsatisfiedLinkError) { + } + } + if (!loadBundled(JNI_LIBRARY)) System.loadLibrary(JNI_LIBRARY) + } + + fun ensureLoaded() {} + + /** + * Loads `base` from the `natives/-/` classpath resources a + * packaged desktop build bundles; false when it isn't bundled. + */ + private fun loadBundled(base: String): Boolean { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + val osId = when { + os.contains("mac") || os.contains("darwin") -> "darwin" + os.contains("win") -> "windows" + else -> "linux" + } + val archId = if (arch == "aarch64" || arch == "arm64") "arm64" else "x64" + val file = when (osId) { + "darwin" -> "lib$base.dylib" + "windows" -> "$base.dll" + else -> "lib$base.so" + } + val stream = NativeLibrary::class.java.getResourceAsStream("/natives/$osId-$archId/$file") ?: return false + val dir = java.nio.file.Files.createTempDirectory("$base-natives") + val target = dir.resolve(file) + stream.use { java.nio.file.Files.copy(it, target) } + target.toFile().deleteOnExit() + dir.toFile().deleteOnExit() + System.load(target.toAbsolutePath().toString()) + return true + } +} + +/** + * One strong reference to a native object, released exactly once: by + * [close], or by [NativeCleaner] once the owning wrapper is unreachable. + * Every call borrows the address for its duration, and a close that races an + * in-flight call defers the release until the last borrow ends, so the object + * is never freed while native code is using it. + */ +internal class NativeHandle(val address: Long, private val release: (Long) -> Unit) { + // Bit 0 is the closed flag; the remaining bits count in-flight borrows. + private val state = AtomicLong(0L) + + val isClosed: Boolean get() = state.get() and 1L != 0L + + fun acquire(): Long { + while (true) { + val s = state.get() + check(s and 1L == 0L) { "native object used after close()" } + if (state.compareAndSet(s, s + 2L)) return address + } + } + + fun unacquire() { + if (state.addAndGet(-2L) == 1L) release(address) + } + + fun close() { + while (true) { + val s = state.get() + if (s and 1L != 0L) return + if (state.compareAndSet(s, s or 1L)) { + if (s == 0L) release(address) + return + } + } + } +} + +/** Runs [block] with the native address borrowed for its duration. */ +internal inline fun NativeHandle.borrow(block: (Long) -> R): R { + val address = acquire() + try { + return block(address) + } finally { + unacquire() + } +} + +/** [borrow] for an optional object: a missing object lends address 0. */ +internal inline fun NativeHandle?.borrowOrNull(block: (Long) -> R): R = + if (this == null) block(0L) else borrow(block) + +/** + * Releases the native reference of every wrapper that becomes unreachable + * without being closed. A daemon thread drains a phantom-reference queue, so + * this works on every Android API level and every JVM. + */ +internal object NativeCleaner { + private val queue = ReferenceQueue() + private val pending: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) + + private class Entry(owner: Any, val handle: NativeHandle, queue: ReferenceQueue) : + PhantomReference(owner, queue) + + init { + val thread = Thread({ drain() }, "kitchen_sink-cleaner") + thread.isDaemon = true + thread.start() + } + + /** Releases [handle] once [owner] is unreachable; returns [handle]. */ + fun register(owner: Any, handle: NativeHandle): NativeHandle { + pending.add(Entry(owner, handle, queue)) + return handle + } + + private fun drain() { + while (true) { + val entry = queue.remove() as Entry + pending.remove(entry) + try { + entry.handle.close() + } catch (_: Throwable) { + } + } + } +} + +/** UTF-8 bytes of [value], for a string crossing into native code. */ +internal fun encodeUtf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8) + +/** The string a native UTF-8 byte sequence spells. */ +internal fun decodeUtf8(bytes: ByteArray): String = String(bytes, Charsets.UTF_8) + +// END Runtime.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Shared_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Shared_kt.snap new file mode 100644 index 00000000..252e2fbf --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_kitchen_sink__src_main_kotlin_kitchen_sink_Shared_kt.snap @@ -0,0 +1,28 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package kitchen_sink + +/** A reusable token referenced across modules */ +data class Token(val id: Long, val label: String) + +internal fun packToken(_w: BufferWriter, _v: Token) { + _w.writeI64(_v.id) + _w.writeString(_v.label) +} + +internal fun unpackToken(_r: BufferReader): Token = Token( + _r.readI64(), + _r.readString(), +) + +object Shared { + /** Trivial cross-module helper */ + fun ping(): String = decodeUtf8(JniBridge.shared_ping()) +} + +// END Shared.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__build_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__build_gradle_kts.snap index 374b107b..fd4f8cd8 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__build_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__build_gradle_kts.snap @@ -1,36 +1,59 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") version "1.9.22" + id("com.android.library") version "8.7.3" + id("org.jetbrains.kotlin.android") version "2.0.21" + `maven-publish` } +group = "nested_modules" +version = "0.1.0" + android { - namespace = "com.weaveffi" - compileSdk = 34 + namespace = "nested_modules" + compileSdk = 35 defaultConfig { - // java.lang.ref.Cleaner, which backs every wrapper's disposal, is API 33. - minSdk = 33 - externalNativeBuild { - cmake { - cppFlags("") - } - } + minSdk = 21 + consumerProguardFiles("consumer-rules.pro") } + // Builds libnested_modules_jni.so per ABI, linked against the producer library in + // src/main/jniLibs// (see src/main/cpp/CMakeLists.txt). externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } } -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + } +} + +publishing { + publications { + register("release") { + artifactId = "nested_modules" + afterEvaluate { + from(components["release"]) + } + } + } } // END build.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__consumer_rules_pro.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__consumer_rules_pro.snap new file mode 100644 index 00000000..ec1ab254 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__consumer_rules_pro.snap @@ -0,0 +1,11 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# The JNI shim looks classes and methods up by name. +-keep class nested_modules.** { *; } + +# END consumer-rules.pro diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__settings_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__settings_gradle_kts.snap index 69449d7c..a5caa7f2 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__settings_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__settings_gradle_kts.snap @@ -1,10 +1,25 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -rootProject.name = "weaveffi" +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "nested_modules" // END settings.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_CMakeLists_txt.snap index 32fa1b44..ad4cf329 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_CMakeLists_txt.snap @@ -1,13 +1,34 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -cmake_minimum_required(VERSION 3.22) -project(weaveffi_jni C) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) +cmake_minimum_required(VERSION 3.18) +project(nested_modules_jni C) + +# The directory holding the producer library (libnested_modules). Android builds +# default to the per-ABI copy under src/main/jniLibs/. +set(NESTED_MODULES_LIBRARY_DIR "" CACHE PATH "Directory holding the nested_modules producer library") +if(NOT NESTED_MODULES_LIBRARY_DIR AND ANDROID) + set(NESTED_MODULES_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}") +endif() +if(NESTED_MODULES_LIBRARY_DIR) + find_library(NESTED_MODULES_PRODUCER NAMES nested_modules PATHS "${NESTED_MODULES_LIBRARY_DIR}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED) +else() + set(CMAKE_FIND_FRAMEWORK NEVER) + find_library(NESTED_MODULES_PRODUCER NAMES nested_modules REQUIRED) +endif() + +add_library(nested_modules_jni SHARED nested_modules_jni.c) +target_include_directories(nested_modules_jni PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(nested_modules_jni PRIVATE "${NESTED_MODULES_PRODUCER}") +if(NOT ANDROID) + find_package(JNI REQUIRED) + find_package(Threads REQUIRED) + target_include_directories(nested_modules_jni PRIVATE ${JNI_INCLUDE_DIRS}) + target_link_libraries(nested_modules_jni PRIVATE Threads::Threads) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_h.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_h.snap new file mode 100644 index 00000000..27c97f2b --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_h.snap @@ -0,0 +1,144 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NESTED_MODULES_H +#define NESTED_MODULES_H + +#include +#include +#include + +#ifndef NESTED_MODULES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef NESTED_MODULES_BUILD +# define NESTED_MODULES_API __declspec(dllexport) +# else +# define NESTED_MODULES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define NESTED_MODULES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define NESTED_MODULES_API __attribute__((visibility("default"))) +# else +# define NESTED_MODULES_API +# endif +#endif + +#ifndef NESTED_MODULES_DEPRECATED +# if defined(_MSC_VER) +# define NESTED_MODULES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define NESTED_MODULES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define NESTED_MODULES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports nested_modules_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define NESTED_MODULES_ABI_VERSION 3u +NESTED_MODULES_API uint32_t nested_modules_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by nested_modules_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct nested_modules_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} nested_modules_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +NESTED_MODULES_API void nested_modules_error_set(nested_modules_error* err, int32_t code, const char* message); +NESTED_MODULES_API void nested_modules_error_clear(nested_modules_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + nested_modules_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +NESTED_MODULES_API void nested_modules_error_free(nested_modules_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +NESTED_MODULES_API void nested_modules_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct nested_modules_cancel_token nested_modules_cancel_token; +NESTED_MODULES_API nested_modules_cancel_token* nested_modules_cancel_token_create(void); +NESTED_MODULES_API void nested_modules_cancel_token_cancel(nested_modules_cancel_token* token); +NESTED_MODULES_API bool nested_modules_cancel_token_is_cancelled(const nested_modules_cancel_token* token); +NESTED_MODULES_API void nested_modules_cancel_token_destroy(nested_modules_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +NESTED_MODULES_API uint8_t* nested_modules_alloc(uint32_t size); +NESTED_MODULES_API void nested_modules_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +NESTED_MODULES_API uint64_t nested_modules_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define NESTED_MODULES_GRAPHICS_CHECKSUM 0x44d31b5b10fdb90dull +NESTED_MODULES_API uint64_t nested_modules_graphics_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with nested_modules_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * nested_modules_buffer.h declares a C struct and codec for each. + */ + +/** A measurement unit shared with nested submodules. */ +typedef enum { nested_modules_graphics_Unit_pixels = 0, nested_modules_graphics_Unit_points = 1 } nested_modules_graphics_Unit; +/** Error codes reported by throwing functions in the `graphics` module tree. */ +typedef enum { nested_modules_graphics_GraphicsError_OutOfBounds = 1 } nested_modules_graphics_GraphicsError; + +// Module: graphics +/** Construct a Canvas. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_new_canvas(int32_t width, size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes +/** Construct a Shape. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_make_shape(int32_t sides, size_t* out_len, nested_modules_error* out_err); +/** Nested function returning a struct owned by an ancestor module. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_describe(size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes_geometry +/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, nested_modules_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // NESTED_MODULES_H + +// END nested_modules.h diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_jni_c.snap new file mode 100644 index 00000000..ce7b8576 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_nested_modules_jni_c.snap @@ -0,0 +1,196 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#include +#include +#include +#include +#include +#include + +/* The shim forwards deprecated functions too, so it doesn't warn about them. */ +#define NESTED_MODULES_DEPRECATED(msg) +#include "nested_modules.h" + +/* Every static name in this file starts with `Jni_`, which no C ABI symbol + (all lowercase `nested_modules_...`) and no JNI export (`Java_...`) can + spell. */ + +static JavaVM* Jni_vm = NULL; +static jclass Jni_bridge = NULL; +static jmethodID Jni_error = NULL; + +/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value + buffers all cross as one. */ +typedef struct { + jbyteArray array; + jbyte* elems; + const uint8_t* ptr; + size_t len; +} Jni_bytes; + +static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { + Jni_bytes b = {array, NULL, NULL, 0}; + if (array == NULL) { + return b; + } + b.len = (size_t)(*env)->GetArrayLength(env, array); + b.elems = (*env)->GetByteArrayElements(env, array, NULL); + b.ptr = (const uint8_t*)b.elems; + if (b.elems == NULL) { + b.len = 0; + } + return b; +} + +static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { + if (b->elems != NULL) { + (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); + } +} + +/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid + input, so NULL + 0 is the empty array). */ +static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = (*env)->NewByteArray(env, (jsize)len); + if (out != NULL && len > 0 && ptr != NULL) { + (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); + } + return out; +} + +/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ +static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = Jni_new_bytes(env, ptr, len); + if (ptr != NULL) { + nested_modules_free_bytes((uint8_t*)ptr, len); + } + return out; +} + +/* Throws the Kotlin exception for `err` (mapped through domain `domain`, + 0 for the generic one) and clears `err`. */ +static inline void Jni_throw(JNIEnv* env, nested_modules_error* err, jint domain) { + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + nested_modules_error_clear(err); + if ((*env)->ExceptionCheck(env)) { + return; + } + jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); + if (ex != NULL && !(*env)->ExceptionCheck(env)) { + (*env)->Throw(env, ex); + } +} + +static jint Jni_load_error(JNIEnv* env, const char* message) { + jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); + if (cls != NULL) { + (*env)->ThrowNew(env, cls, message); + } + return JNI_ERR; +} + +/* Generated below: the contract checks and the method IDs the shim caches. */ +static jint Jni_load(JNIEnv* env); + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env = NULL; + (void)reserved; + Jni_vm = vm; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + if (nested_modules_abi_version() != 3u) { + return Jni_load_error(env, "nested_modules: the native library implements a different C ABI revision than these bindings (expected 3)"); + } + jclass bridge = (*env)->FindClass(env, "nested_modules/JniBridge"); + if (bridge == NULL) { + return JNI_ERR; + } + Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); + Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Lnested_modules/FfiException;"); + if (Jni_error == NULL) { + return JNI_ERR; + } + if (Jni_load(env) != JNI_OK) { + return JNI_ERR; + } + return JNI_VERSION_1_6; +} + +JNIEXPORT jlong JNICALL Java_nested_1modules_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { + (void)env; + (void)cls; + return (jlong)nested_modules_debug_live((int32_t)kind); +} + +static jint Jni_load(JNIEnv* env) { + (void)env; + if (nested_modules_graphics_checksum() != UINT64_C(0x44d31b5b10fdb90d)) { + return Jni_load_error(env, "nested_modules: module 'graphics' does not match these bindings (its contract checksum differs); regenerate the bindings or rebuild the library"); + } + return JNI_OK; +} + +JNIEXPORT jbyteArray JNICALL Java_nested_1modules_JniBridge_graphics_1new_1canvas(JNIEnv* env, jclass cls, jint p_width) { + nested_modules_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = nested_modules_graphics_new_canvas((int32_t)p_width, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_nested_1modules_JniBridge_graphics_1shapes_1make_1shape(JNIEnv* env, jclass cls, jint p_sides) { + nested_modules_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = nested_modules_graphics_shapes_make_shape((int32_t)p_sides, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_nested_1modules_JniBridge_graphics_1shapes_1describe(JNIEnv* env, jclass cls) { + nested_modules_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = nested_modules_graphics_shapes_describe(&out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_nested_1modules_JniBridge_graphics_1shapes_1geometry_1compute(JNIEnv* env, jclass cls, jbyteArray p_shape) { + nested_modules_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_shape_b = Jni_borrow_bytes(env, p_shape); + size_t out_len = 0; + const uint8_t* rv = nested_modules_graphics_shapes_geometry_compute(p_shape_b.ptr, p_shape_b.len, &out_len, &err); + Jni_release_bytes(env, &p_shape_b); + if (err.code != 0) { + Jni_throw(env, &err, 1); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + + +// END nested_modules_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index ef2bda33..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,193 +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" - -/* Copies `s` as a NUL-terminated standard UTF-8 string (JNI's modified - UTF-8 would encode supplementary characters as surrogate pairs the - producer rejects). Lone surrogates become U+FFFD. Release with free(). - Returns NULL for a NULL string, for a string holding U+0000 (which no - C string can carry), or on allocation failure; the producer then - reports the null argument through its marshalling error. */ -static char* weaveffi_jni_string_to_utf8(JNIEnv* env, jstring s) { - if (s == NULL) { return NULL; } - jsize n = (*env)->GetStringLength(env, s); - const jchar* u = (*env)->GetStringChars(env, s, NULL); - if (u == NULL) { return NULL; } - char* out = (char*)malloc((size_t)n * 4u + 1u); - if (out == NULL) { (*env)->ReleaseStringChars(env, s, u); return NULL; } - size_t o = 0; - for (jsize i = 0; i < n; i++) { - uint32_t c = u[i]; - if (c == 0u) { free(out); (*env)->ReleaseStringChars(env, s, u); return NULL; } - if (c >= 0xD800u && c <= 0xDBFFu && i + 1 < n && u[i + 1] >= 0xDC00u && u[i + 1] <= 0xDFFFu) { - c = 0x10000u + ((c - 0xD800u) << 10) + ((uint32_t)u[i + 1] - 0xDC00u); - i++; - } - else if (c >= 0xD800u && c <= 0xDFFFu) { c = 0xFFFDu; } - if (c < 0x80u) { out[o++] = (char)c; } - else if (c < 0x800u) { out[o++] = (char)(0xC0u | (c >> 6)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else if (c < 0x10000u) { out[o++] = (char)(0xE0u | (c >> 12)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else { out[o++] = (char)(0xF0u | (c >> 18)); out[o++] = (char)(0x80u | ((c >> 12) & 0x3Fu)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - } - out[o] = 0; - (*env)->ReleaseStringChars(env, s, u); - return out; -} - -/* Builds a jstring from standard UTF-8 (NewStringUTF expects modified UTF-8 - and mangles supplementary characters). NULL yields the empty string and - a malformed sequence yields U+FFFD, so this never fails on producer - output; it returns NULL only when the JVM could not allocate. */ -static jstring weaveffi_jni_utf8_to_string(JNIEnv* env, const char* s) { - if (s == NULL) { s = ""; } - size_t n = strlen(s); - jchar* buf = (jchar*)malloc((n + 1u) * sizeof(jchar)); - if (buf == NULL) { return NULL; } - size_t i = 0, o = 0; - while (i < n) { - unsigned char b = (unsigned char)s[i]; - uint32_t c; size_t len; - if (b < 0x80u) { c = b; len = 1; } - else if ((b & 0xE0u) == 0xC0u) { c = b & 0x1Fu; len = 2; } - else if ((b & 0xF0u) == 0xE0u) { c = b & 0x0Fu; len = 3; } - else if ((b & 0xF8u) == 0xF0u) { c = b & 0x07u; len = 4; } - else { c = 0xFFFDu; len = 1; } - if (i + len > n) { c = 0xFFFDu; len = 1; } - for (size_t k = 1; k < len; k++) { - unsigned char cb = (unsigned char)s[i + k]; - if ((cb & 0xC0u) != 0x80u) { c = 0xFFFDu; len = k; break; } - c = (c << 6) | (cb & 0x3Fu); - } - i += len; - if (c > 0x10FFFFu) { c = 0xFFFDu; } - if (c >= 0x10000u) { c -= 0x10000u; buf[o++] = (jchar)(0xD800u + (c >> 10)); buf[o++] = (jchar)(0xDC00u + (c & 0x3FFu)); } - else { buf[o++] = (jchar)c; } - } - jstring out = (*env)->NewString(env, buf, (jsize)o); - free(buf); - return out; -} - -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 = weaveffi_jni_utf8_to_string(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_graphics_GraphicsError(JNIEnv* env, weaveffi_error* err) { - jclass exClass = (*env)->FindClass(env, "com/weaveffi/GraphicsException"); - 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 = weaveffi_jni_utf8_to_string(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 JavaVM* weaveffi_jni_vm = NULL; - -static jint weaveffi_jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { (*env)->ThrowNew(env, cls, message); } - return JNI_ERR; -} - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - (void)reserved; - weaveffi_jni_vm = vm; - JNIEnv* env = NULL; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - if (weaveffi_abi_version() != 2u) { - return weaveffi_jni_load_error(env, "WeaveFFI: the native library implements a different C ABI revision than these bindings (expected 2)"); - } - return JNI_VERSION_1_6; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_newCanvasJni(JNIEnv* env, jclass clazz, jint width) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_graphics_new_canvas((int32_t)width, &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 jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_makeShapeJni(JNIEnv* env, jclass clazz, jint sides) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_graphics_shapes_make_shape((int32_t)sides, &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 jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_describeJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_graphics_shapes_describe(&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 jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_computeJni(JNIEnv* env, jclass clazz, jbyteArray shape) { - weaveffi_error err = {0, NULL, NULL, 0}; - jbyte* shape_elems = (*env)->GetByteArrayElements(env, shape, NULL); - jsize shape_len = (*env)->GetArrayLength(env, shape); - size_t out_len = 0; - const uint8_t* rv = weaveffi_graphics_shapes_geometry_compute((const uint8_t*)shape_elems, (size_t)shape_len, &out_len, &err); - (*env)->ReleaseByteArrayElements(env, shape, shape_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_graphics_GraphicsError(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; -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index f24ca48e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_com_weaveffi_WeaveFFI_kt.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 - -package com.weaveffi - -class WeaveFFI { - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - /** - * Construct a Canvas. - */ - @JvmStatic private external fun newCanvasJni(width: Int): ByteArray - @JvmStatic fun newCanvas(width: Int): Canvas = weaveDecode(newCanvasJni(width)) { r -> unpackCanvas(r) } - /** - * Construct a Shape. - */ - @JvmStatic private external fun makeShapeJni(sides: Int): ByteArray - @JvmStatic fun makeShape(sides: Int): Shape = weaveDecode(makeShapeJni(sides)) { r -> unpackShape(r) } - /** - * Nested function returning a struct owned by an ancestor module. - */ - @JvmStatic private external fun describeJni(): ByteArray - @JvmStatic fun describe(): Canvas = weaveDecode(describeJni()) { r -> unpackCanvas(r) } - /** - * Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - */ - @JvmStatic private external fun computeJni(shape: ByteArray): ByteArray - @JvmStatic fun compute(shape: Shape): Detail = weaveDecode(computeJni(weaveEncode { w -> packShape(w, shape) })) { r -> unpackDetail(r) } - } -} - -/** A measurement unit shared with nested submodules. */ -enum class Unit(val value: Int) { - pixels(0), - points(1); - - companion object { - fun fromValue(value: Int): Unit = entries.first { it.value == value } - } -} - -/** Top-level struct that references a struct defined one level deeper. */ -data class Canvas( - val width: Int, - /** Reference down into a nested module */ - val root: Shape, -) - -internal fun packCanvas(w: WeaveBufferWriter, v: Canvas) { - w.writeI32(v.width) - packShape(w, v.root) -} - -internal fun unpackCanvas(r: WeaveBufferReader): Canvas = Canvas( - r.readI32(), - unpackShape(r), -) - -/** A struct one level deep that references both an ancestor enum and a deeper struct. */ -data class Shape( - val sides: Int, - /** Reference up to the parent module's enum */ - val unit: Unit, - /** Reference down into a deeper module */ - val detail: Detail, -) - -internal fun packShape(w: WeaveBufferWriter, v: Shape) { - w.writeI32(v.sides) - w.writeI32(v.unit.value) - packDetail(w, v.detail) -} - -internal fun unpackShape(r: WeaveBufferReader): Shape = Shape( - r.readI32(), - Unit.fromValue(r.readI32()), - unpackDetail(r), -) - -/** A struct two levels deep. */ -data class Detail(val area: Double) - -internal fun packDetail(w: WeaveBufferWriter, v: Detail) { - w.writeF64(v.area) -} - -internal fun unpackDetail(r: WeaveBufferReader): Detail = Detail( - r.readF64(), -) - -/** Generic WeaveFFI failure: panics, marshalling errors, callback implementation failures (code -4), and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Typed error domain `GraphicsError` declared by module `graphics`. */ -sealed class GraphicsException(code: Int, message: String) : WeaveFFIException(code, message) { - class OutOfBounds(message: String = "coordinate out of bounds") : GraphicsException(1, message) - - companion object { - /** Map a raw `GraphicsError` 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 -> OutOfBounds(message) - else -> WeaveFFIException(code, message) - } - } -} - -/** Loads the native library once; every companion `init` calls [ensureLoaded]. */ -internal object WeaveNativeLibrary { - init { System.loadLibrary("weaveffi") } - - fun ensureLoaded() {} -} - -/** 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()) - /** An object token: one strong reference the caller adopts. Zero is never valid here. */ - fun readObject(): Long { - val token = readI64() - if (token == 0L) malformed("null object token") - return token - } - 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/kotlin_nested_modules__src_main_kotlin_nested_modules_Buffers_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Buffers_kt.snap new file mode 100644 index 00000000..3a1a17e4 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Buffers_kt.snap @@ -0,0 +1,255 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package nested_modules + +/** + * Growable little-endian writer for the value-buffer wire format that + * records, rich enums, optionals, lists, and maps cross the ABI in. + */ +internal class BufferWriter { + private var buf = ByteArray(64) + private var len = 0 + private var minted: ArrayList Unit>>? = null + + private fun reserve(extra: Int) { + if (len + extra <= buf.size) return + var cap = buf.size * 2 + while (cap < len + extra) cap *= 2 + buf = buf.copyOf(cap) + } + + fun toByteArray(): ByteArray = buf.copyOf(len) + + fun writeBool(v: Boolean) { + reserve(1) + buf[len++] = if (v) 1 else 0 + } + + fun writeI8(v: Byte) { + reserve(1) + buf[len++] = v + } + + fun writeU8(v: Byte) = writeI8(v) + + fun writeI16(v: Short) { + reserve(2) + val b = v.toInt() + buf[len++] = b.toByte() + buf[len++] = (b shr 8).toByte() + } + + fun writeU16(v: Short) = writeI16(v) + + fun writeI32(v: Int) { + reserve(4) + buf[len++] = v.toByte() + buf[len++] = (v shr 8).toByte() + buf[len++] = (v shr 16).toByte() + buf[len++] = (v shr 24).toByte() + } + + fun writeU32(v: Long) = writeI32(v.toInt()) + + fun writeI64(v: Long) { + writeI32(v.toInt()) + writeI32((v ushr 32).toInt()) + } + + fun writeU64(v: Long) = writeI64(v) + + fun writeF32(v: Float) = writeI32(v.toRawBits()) + + fun writeF64(v: Double) = writeI64(v.toRawBits()) + + fun writeString(v: String) = writeBytes(encodeUtf8(v)) + + fun writeBytes(v: ByteArray) { + writeI32(v.size) + reserve(v.size) + v.copyInto(buf, len) + len += v.size + } + + /** + * An object token: one new strong reference the reader adopts, which + * [abandon] releases with [release] if the encoding fails. + */ + fun writeObject(token: Long, release: (Long) -> Unit) { + val list = minted ?: ArrayList Unit>>().also { minted = it } + list.add(token to release) + writeI64(token) + } + + /** Releases every object token written so far, for an encoding that failed midway. */ + fun abandon() { + minted?.forEach { (token, release) -> release(token) } + minted = null + } + + inline fun writeOptional(v: T?, write: (T) -> Unit) { + if (v == null) { + writeBool(false) + } else { + writeBool(true) + write(v) + } + } + + inline fun writeList(v: List, write: (T) -> Unit) { + writeI32(v.size) + for (e in v) write(e) + } + + inline fun writeMap(v: Map, writeKey: (K) -> Unit, writeValue: (V) -> Unit) { + writeI32(v.size) + for ((k, e) in v) { + writeKey(k) + writeValue(e) + } + } +} + +/** + * Validating little-endian reader for the value-buffer wire format. A + * truncated buffer, an invalid flag byte, a zero object token, an oversized + * length, or trailing bytes raise [FfiException] code -3. + */ +internal class BufferReader(private val buf: ByteArray) { + private var pos = 0 + + private fun malformed(detail: String): Nothing = + throw FfiException(-3, "malformed value buffer: $detail") + + private fun take(n: Int): Int { + if (n > buf.size - pos) malformed("truncated buffer") + val at = pos + pos += n + return at + } + + fun readBool(): Boolean = when (buf[take(1)].toInt()) { + 0 -> false + 1 -> true + else -> malformed("invalid bool byte") + } + + fun readI8(): Byte = buf[take(1)] + + fun readU8(): Byte = readI8() + + fun readI16(): Short { + val at = take(2) + return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() + } + + fun readU16(): Short = readI16() + + 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 readU64(): Long = readI64() + + fun readF32(): Float = Float.fromBits(readI32()) + + fun readF64(): Double = Double.fromBits(readI64()) + + private fun readLen(): Int { + val n = readI32() + if (n < 0 || n > buf.size - pos) malformed("length prefix exceeds remaining bytes") + return n + } + + fun readString(): String { + val n = readLen() + val at = take(n) + return try { + Charsets.UTF_8.newDecoder().decode(java.nio.ByteBuffer.wrap(buf, at, n)).toString() + } catch (e: java.nio.charset.CharacterCodingException) { + malformed("string is not valid UTF-8") + } + } + + fun readBytes(): ByteArray { + val n = readLen() + val at = take(n) + return buf.copyOfRange(at, at + n) + } + + /** An object token: one strong reference the caller adopts. */ + fun readObject(): Long { + val token = readI64() + if (token == 0L) malformed("null object token") + return token + } + + inline fun readOptional(read: () -> T): T? = if (readBool()) read() else null + + inline fun readList(read: () -> T): List { + val n = readCount() + val out = ArrayList(minOf(n, 4096)) + repeat(n) { out.add(read()) } + return out + } + + inline fun readMap(readKey: () -> K, readValue: () -> V): Map { + val n = readCount() + val out = LinkedHashMap(minOf(n, 4096)) + repeat(n) { + val k = readKey() + out[k] = readValue() + } + return out + } + + /** An element count; elements may encode to zero bytes, so only the sign is checked. */ + fun readCount(): Int { + val n = readI32() + if (n < 0) malformed("negative element count") + return n + } + + fun expectEnd() { + if (pos != buf.size) malformed("trailing bytes after value") + } +} + +/** Encodes one value into a fresh value buffer. */ +internal inline fun encodeBuffer(write: (BufferWriter) -> Unit): ByteArray { + val w = BufferWriter() + try { + write(w) + } catch (t: Throwable) { + w.abandon() + throw t + } + return w.toByteArray() +} + +/** Decodes one value from [bytes], rejecting malformed or trailing bytes. */ +internal inline fun decodeBuffer(bytes: ByteArray, read: (BufferReader) -> T): T { + val r = BufferReader(bytes) + val v = read(r) + r.expectEnd() + return v +} + +// END Buffers.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Graphics_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Graphics_kt.snap new file mode 100644 index 00000000..fdd70d6d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Graphics_kt.snap @@ -0,0 +1,102 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package nested_modules + +/** Errors of the `GraphicsError` domain. */ +sealed class GraphicsException(code: Int, message: String) : FfiException(code, message) { + class OutOfBounds(message: String = "coordinate out of bounds") : GraphicsException(1, message) + + internal companion object { + /** The exception for a raw code and its serialized payload. */ + fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) { + 1 -> OutOfBounds(message) + else -> FfiException(code, message) + } + } +} + +/** A measurement unit shared with nested submodules. */ +enum class Unit_(val value: Int) { + pixels(0), + points(1); + + companion object { + /** The variant whose ABI value is [value]. */ + fun fromValue(value: Int): Unit_ = entries.firstOrNull { it.value == value } + ?: throw FfiException(-3, "unknown Unit_ value $value") + } +} + +/** Top-level struct that references a struct defined one level deeper. */ +data class Canvas( + val width: Int, + /** Reference down into a nested module */ + val root: Shape, +) + +internal fun packCanvas(_w: BufferWriter, _v: Canvas) { + _w.writeI32(_v.width) + packShape(_w, _v.root) +} + +internal fun unpackCanvas(_r: BufferReader): Canvas = Canvas( + _r.readI32(), + unpackShape(_r), +) + +/** A struct one level deep that references both an ancestor enum and a deeper struct. */ +data class Shape( + val sides: Int, + /** Reference up to the parent module's enum */ + val unit: Unit_, + /** Reference down into a deeper module */ + val detail: Detail, +) + +internal fun packShape(_w: BufferWriter, _v: Shape) { + _w.writeI32(_v.sides) + _w.writeI32(_v.unit.value) + packDetail(_w, _v.detail) +} + +internal fun unpackShape(_r: BufferReader): Shape = Shape( + _r.readI32(), + Unit_.fromValue(_r.readI32()), + unpackDetail(_r), +) + +/** A struct two levels deep. */ +data class Detail(val area: Double) + +internal fun packDetail(_w: BufferWriter, _v: Detail) { + _w.writeF64(_v.area) +} + +internal fun unpackDetail(_r: BufferReader): Detail = Detail( + _r.readF64(), +) + +object Graphics { + /** Construct a Canvas. */ + fun newCanvas(width: Int): Canvas = decodeBuffer(JniBridge.graphics_new_canvas(width)) { _r -> unpackCanvas(_r) } + + object Shapes { + /** Construct a Shape. */ + fun makeShape(sides: Int): Shape = decodeBuffer(JniBridge.graphics_shapes_make_shape(sides)) { _r -> unpackShape(_r) } + + /** Nested function returning a struct owned by an ancestor module. */ + fun describe(): Canvas = decodeBuffer(JniBridge.graphics_shapes_describe()) { _r -> unpackCanvas(_r) } + + object Geometry { + /** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ + fun compute(shape: Shape): Detail = decodeBuffer(JniBridge.graphics_shapes_geometry_compute(encodeBuffer { _w -> packShape(_w, shape) })) { _r -> unpackDetail(_r) } + } + } +} + +// END Graphics.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_JniBridge_kt.snap new file mode 100644 index 00000000..ab685c90 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_JniBridge_kt.snap @@ -0,0 +1,38 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package nested_modules + +/** + * The JNI entry points of `libnested_modules_jni`: one native per C symbol, named after + * it, plus the hooks the shim calls back into. Internal plumbing for the + * public wrappers. + */ +internal object JniBridge { + init { + NativeLibrary.ensureLoaded() + } + + /** The exception for a native failure; domain 0 is the generic one. */ + @JvmStatic + @Suppress("UNUSED_PARAMETER") + fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): FfiException { + val text = decodeUtf8(message) + return when (domain) { + 1 -> GraphicsException.fromCode(code, text, payload) + else -> FfiException(code, text) + } + } + + @JvmStatic external fun debug_live(kind: Int): Long + @JvmStatic external fun graphics_new_canvas(width: Int): ByteArray + @JvmStatic external fun graphics_shapes_make_shape(sides: Int): ByteArray + @JvmStatic external fun graphics_shapes_describe(): ByteArray + @JvmStatic external fun graphics_shapes_geometry_compute(shape: ByteArray): ByteArray +} + +// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Runtime_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Runtime_kt.snap new file mode 100644 index 00000000..aa07de7d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_nested_modules__src_main_kotlin_nested_modules_Runtime_kt.snap @@ -0,0 +1,212 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package nested_modules + +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * A failure reported by the native library: a producer panic (code -2), a + * marshalling failure (-3), a callback implementation that threw (-4), or any + * code these bindings don't know. Typed error domains subclass it. + */ +open class FfiException(val code: Int, message: String) : Exception(message) + +/** + * A lazy sequence streamed from the native library, one element per step. + * The native iterator is released when the sequence is exhausted, when + * [close] is called, or once this object becomes unreachable, whichever + * comes first. + */ +class NativeIterator internal constructor( + address: Long, + release: (Long) -> Unit, + private val step: (Long) -> Any?, +) : Iterator, AutoCloseable { + private val handle = NativeCleaner.register(this, NativeHandle(address, release)) + private var lookahead: Any? = EMPTY + + override fun hasNext(): Boolean { + if (lookahead !== EMPTY) return true + if (handle.isClosed) return false + val item = handle.borrow(step) + if (item === DONE) { + close() + return false + } + lookahead = item + return true + } + + override fun next(): T { + if (!hasNext()) throw NoSuchElementException() + val item = lookahead + lookahead = EMPTY + @Suppress("UNCHECKED_CAST") + return item as T + } + + /** Releases the native iterator; safe to call more than once. */ + override fun close() = handle.close() + + internal companion object { + /** What a step function returns once the native sequence is exhausted. */ + val DONE = Any() + private val EMPTY = Any() + } +} + +/** Loads the producer library and then the JNI shim, exactly once. */ +internal object NativeLibrary { + private const val LIBRARY = "nested_modules" + private const val JNI_LIBRARY = "nested_modules_jni" + + init { + val explicit = System.getenv("NESTED_MODULES_LIBRARY") + if (!explicit.isNullOrEmpty()) { + System.load(explicit) + } else if (!loadBundled(LIBRARY)) { + // The shim's own link dependency usually finds the producer when + // it isn't on java.library.path, so a miss here is only fatal if + // loading the shim fails too. + try { + System.loadLibrary(LIBRARY) + } catch (ignored: UnsatisfiedLinkError) { + } + } + if (!loadBundled(JNI_LIBRARY)) System.loadLibrary(JNI_LIBRARY) + } + + fun ensureLoaded() {} + + /** + * Loads `base` from the `natives/-/` classpath resources a + * packaged desktop build bundles; false when it isn't bundled. + */ + private fun loadBundled(base: String): Boolean { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + val osId = when { + os.contains("mac") || os.contains("darwin") -> "darwin" + os.contains("win") -> "windows" + else -> "linux" + } + val archId = if (arch == "aarch64" || arch == "arm64") "arm64" else "x64" + val file = when (osId) { + "darwin" -> "lib$base.dylib" + "windows" -> "$base.dll" + else -> "lib$base.so" + } + val stream = NativeLibrary::class.java.getResourceAsStream("/natives/$osId-$archId/$file") ?: return false + val dir = java.nio.file.Files.createTempDirectory("$base-natives") + val target = dir.resolve(file) + stream.use { java.nio.file.Files.copy(it, target) } + target.toFile().deleteOnExit() + dir.toFile().deleteOnExit() + System.load(target.toAbsolutePath().toString()) + return true + } +} + +/** + * One strong reference to a native object, released exactly once: by + * [close], or by [NativeCleaner] once the owning wrapper is unreachable. + * Every call borrows the address for its duration, and a close that races an + * in-flight call defers the release until the last borrow ends, so the object + * is never freed while native code is using it. + */ +internal class NativeHandle(val address: Long, private val release: (Long) -> Unit) { + // Bit 0 is the closed flag; the remaining bits count in-flight borrows. + private val state = AtomicLong(0L) + + val isClosed: Boolean get() = state.get() and 1L != 0L + + fun acquire(): Long { + while (true) { + val s = state.get() + check(s and 1L == 0L) { "native object used after close()" } + if (state.compareAndSet(s, s + 2L)) return address + } + } + + fun unacquire() { + if (state.addAndGet(-2L) == 1L) release(address) + } + + fun close() { + while (true) { + val s = state.get() + if (s and 1L != 0L) return + if (state.compareAndSet(s, s or 1L)) { + if (s == 0L) release(address) + return + } + } + } +} + +/** Runs [block] with the native address borrowed for its duration. */ +internal inline fun NativeHandle.borrow(block: (Long) -> R): R { + val address = acquire() + try { + return block(address) + } finally { + unacquire() + } +} + +/** [borrow] for an optional object: a missing object lends address 0. */ +internal inline fun NativeHandle?.borrowOrNull(block: (Long) -> R): R = + if (this == null) block(0L) else borrow(block) + +/** + * Releases the native reference of every wrapper that becomes unreachable + * without being closed. A daemon thread drains a phantom-reference queue, so + * this works on every Android API level and every JVM. + */ +internal object NativeCleaner { + private val queue = ReferenceQueue() + private val pending: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) + + private class Entry(owner: Any, val handle: NativeHandle, queue: ReferenceQueue) : + PhantomReference(owner, queue) + + init { + val thread = Thread({ drain() }, "nested_modules-cleaner") + thread.isDaemon = true + thread.start() + } + + /** Releases [handle] once [owner] is unreachable; returns [handle]. */ + fun register(owner: Any, handle: NativeHandle): NativeHandle { + pending.add(Entry(owner, handle, queue)) + return handle + } + + private fun drain() { + while (true) { + val entry = queue.remove() as Entry + pending.remove(entry) + try { + entry.handle.close() + } catch (_: Throwable) { + } + } + } +} + +/** UTF-8 bytes of [value], for a string crossing into native code. */ +internal fun encodeUtf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8) + +/** The string a native UTF-8 byte sequence spells. */ +internal fun decodeUtf8(bytes: ByteArray): String = String(bytes, Charsets.UTF_8) + +// END Runtime.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__build_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__build_gradle_kts.snap index 374b107b..339be7e1 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__build_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__build_gradle_kts.snap @@ -1,36 +1,59 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") version "1.9.22" + id("com.android.library") version "8.7.3" + id("org.jetbrains.kotlin.android") version "2.0.21" + `maven-publish` } +group = "shapes" +version = "0.1.0" + android { - namespace = "com.weaveffi" - compileSdk = 34 + namespace = "shapes" + compileSdk = 35 defaultConfig { - // java.lang.ref.Cleaner, which backs every wrapper's disposal, is API 33. - minSdk = 33 - externalNativeBuild { - cmake { - cppFlags("") - } - } + minSdk = 21 + consumerProguardFiles("consumer-rules.pro") } + // Builds libshapes_jni.so per ABI, linked against the producer library in + // src/main/jniLibs// (see src/main/cpp/CMakeLists.txt). externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } } -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + } +} + +publishing { + publications { + register("release") { + artifactId = "shapes" + afterEvaluate { + from(components["release"]) + } + } + } } // END build.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__consumer_rules_pro.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__consumer_rules_pro.snap new file mode 100644 index 00000000..049667cd --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__consumer_rules_pro.snap @@ -0,0 +1,11 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# The JNI shim looks classes and methods up by name. +-keep class shapes.** { *; } + +# END consumer-rules.pro diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__settings_gradle_kts.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__settings_gradle_kts.snap index 69449d7c..78bc6bdf 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__settings_gradle_kts.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__settings_gradle_kts.snap @@ -1,10 +1,25 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o -rootProject.name = "weaveffi" +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "shapes" // END settings.gradle.kts diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_CMakeLists_txt.snap index 32fa1b44..8621c5ca 100644 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_CMakeLists_txt.snap +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_CMakeLists_txt.snap @@ -1,13 +1,34 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o -cmake_minimum_required(VERSION 3.22) -project(weaveffi_jni C) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) +cmake_minimum_required(VERSION 3.18) +project(shapes_jni C) + +# The directory holding the producer library (libshapes). Android builds +# default to the per-ABI copy under src/main/jniLibs/. +set(SHAPES_LIBRARY_DIR "" CACHE PATH "Directory holding the shapes producer library") +if(NOT SHAPES_LIBRARY_DIR AND ANDROID) + set(SHAPES_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}") +endif() +if(SHAPES_LIBRARY_DIR) + find_library(SHAPES_PRODUCER NAMES shapes PATHS "${SHAPES_LIBRARY_DIR}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED) +else() + set(CMAKE_FIND_FRAMEWORK NEVER) + find_library(SHAPES_PRODUCER NAMES shapes REQUIRED) +endif() + +add_library(shapes_jni SHARED shapes_jni.c) +target_include_directories(shapes_jni PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(shapes_jni PRIVATE "${SHAPES_PRODUCER}") +if(NOT ANDROID) + find_package(JNI REQUIRED) + find_package(Threads REQUIRED) + target_include_directories(shapes_jni PRIVATE ${JNI_INCLUDE_DIRS}) + target_link_libraries(shapes_jni PRIVATE Threads::Threads) +endif() # END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_h.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_h.snap new file mode 100644 index 00000000..df20c3f5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_h.snap @@ -0,0 +1,151 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef SHAPES_H +#define SHAPES_H + +#include +#include +#include + +#ifndef SHAPES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef SHAPES_BUILD +# define SHAPES_API __declspec(dllexport) +# else +# define SHAPES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define SHAPES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define SHAPES_API __attribute__((visibility("default"))) +# else +# define SHAPES_API +# endif +#endif + +#ifndef SHAPES_DEPRECATED +# if defined(_MSC_VER) +# define SHAPES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define SHAPES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define SHAPES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports shapes_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define SHAPES_ABI_VERSION 3u +SHAPES_API uint32_t shapes_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by shapes_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct shapes_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} shapes_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +SHAPES_API void shapes_error_set(shapes_error* err, int32_t code, const char* message); +SHAPES_API void shapes_error_clear(shapes_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + shapes_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +SHAPES_API void shapes_error_free(shapes_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +SHAPES_API void shapes_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct shapes_cancel_token shapes_cancel_token; +SHAPES_API shapes_cancel_token* shapes_cancel_token_create(void); +SHAPES_API void shapes_cancel_token_cancel(shapes_cancel_token* token); +SHAPES_API bool shapes_cancel_token_is_cancelled(const shapes_cancel_token* token); +SHAPES_API void shapes_cancel_token_destroy(shapes_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +SHAPES_API uint8_t* shapes_alloc(uint32_t size); +SHAPES_API void shapes_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +SHAPES_API uint64_t shapes_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define SHAPES_SHAPES_CHECKSUM 0x06acf433ffa2a0f0ull +SHAPES_API uint64_t shapes_shapes_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with shapes_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * shapes_buffer.h declares a C struct and codec for each. + */ + +/** An algebraic shape (sum type with associated data) */ +typedef enum { + /** The empty shape (a unit variant) */ + shapes_shapes_Shape_Empty = 0, + /** A circle with a radius */ + shapes_shapes_Shape_Circle = 1, + /** An axis-aligned rectangle */ + shapes_shapes_Shape_Rectangle = 2, + /** An integer-bounded shape (exercises narrow + wide ints) */ + shapes_shapes_Shape_Bounded = 3, + /** A labeled shape with a small count */ + shapes_shapes_Shape_Labeled = 4 +} shapes_shapes_Shape_Tag; +/** A plain C-style enum (no payloads) */ +typedef enum { shapes_shapes_Channel_Red = 0, shapes_shapes_Channel_Green = 1, shapes_shapes_Channel_Blue = 2 } shapes_shapes_Channel; + +// Module: shapes +/** Render a shape to a string */ +SHAPES_API const uint8_t* shapes_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, shapes_error* out_err); +/** Scale a shape by a factor, returning a new shape */ +SHAPES_API const uint8_t* shapes_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, shapes_error* out_err); +/** Sum a list of bytes into a wide integer (numerics smoke) */ +SHAPES_API uint64_t shapes_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, shapes_error* out_err); +/** Build a Numerics struct from every numeric primitive */ +SHAPES_API const uint8_t* shapes_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, shapes_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // SHAPES_H + +// END shapes.h diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_jni_c.snap new file mode 100644 index 00000000..21717afb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_shapes_jni_c.snap @@ -0,0 +1,199 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#include +#include +#include +#include +#include +#include + +/* The shim forwards deprecated functions too, so it doesn't warn about them. */ +#define SHAPES_DEPRECATED(msg) +#include "shapes.h" + +/* Every static name in this file starts with `Jni_`, which no C ABI symbol + (all lowercase `shapes_...`) and no JNI export (`Java_...`) can + spell. */ + +static JavaVM* Jni_vm = NULL; +static jclass Jni_bridge = NULL; +static jmethodID Jni_error = NULL; + +/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value + buffers all cross as one. */ +typedef struct { + jbyteArray array; + jbyte* elems; + const uint8_t* ptr; + size_t len; +} Jni_bytes; + +static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { + Jni_bytes b = {array, NULL, NULL, 0}; + if (array == NULL) { + return b; + } + b.len = (size_t)(*env)->GetArrayLength(env, array); + b.elems = (*env)->GetByteArrayElements(env, array, NULL); + b.ptr = (const uint8_t*)b.elems; + if (b.elems == NULL) { + b.len = 0; + } + return b; +} + +static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { + if (b->elems != NULL) { + (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); + } +} + +/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid + input, so NULL + 0 is the empty array). */ +static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = (*env)->NewByteArray(env, (jsize)len); + if (out != NULL && len > 0 && ptr != NULL) { + (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); + } + return out; +} + +/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ +static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = Jni_new_bytes(env, ptr, len); + if (ptr != NULL) { + shapes_free_bytes((uint8_t*)ptr, len); + } + return out; +} + +/* Throws the Kotlin exception for `err` (mapped through domain `domain`, + 0 for the generic one) and clears `err`. */ +static inline void Jni_throw(JNIEnv* env, shapes_error* err, jint domain) { + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + shapes_error_clear(err); + if ((*env)->ExceptionCheck(env)) { + return; + } + jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); + if (ex != NULL && !(*env)->ExceptionCheck(env)) { + (*env)->Throw(env, ex); + } +} + +static jint Jni_load_error(JNIEnv* env, const char* message) { + jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); + if (cls != NULL) { + (*env)->ThrowNew(env, cls, message); + } + return JNI_ERR; +} + +/* Generated below: the contract checks and the method IDs the shim caches. */ +static jint Jni_load(JNIEnv* env); + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env = NULL; + (void)reserved; + Jni_vm = vm; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + if (shapes_abi_version() != 3u) { + return Jni_load_error(env, "shapes: the native library implements a different C ABI revision than these bindings (expected 3)"); + } + jclass bridge = (*env)->FindClass(env, "shapes/JniBridge"); + if (bridge == NULL) { + return JNI_ERR; + } + Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); + Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)Lshapes/FfiException;"); + if (Jni_error == NULL) { + return JNI_ERR; + } + if (Jni_load(env) != JNI_OK) { + return JNI_ERR; + } + return JNI_VERSION_1_6; +} + +JNIEXPORT jlong JNICALL Java_shapes_JniBridge_debug_1live(JNIEnv* env, jclass cls, jint kind) { + (void)env; + (void)cls; + return (jlong)shapes_debug_live((int32_t)kind); +} + +static jint Jni_load(JNIEnv* env) { + (void)env; + if (shapes_shapes_checksum() != UINT64_C(0x06acf433ffa2a0f0)) { + return Jni_load_error(env, "shapes: module 'shapes' does not match these bindings (its contract checksum differs); regenerate the bindings or rebuild the library"); + } + return JNI_OK; +} + +JNIEXPORT jbyteArray JNICALL Java_shapes_JniBridge_shapes_1describe(JNIEnv* env, jclass cls, jbyteArray p_shape) { + shapes_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_shape_b = Jni_borrow_bytes(env, p_shape); + size_t out_len = 0; + const uint8_t* rv = shapes_shapes_describe(p_shape_b.ptr, p_shape_b.len, &out_len, &err); + Jni_release_bytes(env, &p_shape_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jbyteArray JNICALL Java_shapes_JniBridge_shapes_1scale(JNIEnv* env, jclass cls, jbyteArray p_shape, jdouble p_factor) { + shapes_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_shape_b = Jni_borrow_bytes(env, p_shape); + size_t out_len = 0; + const uint8_t* rv = shapes_shapes_scale(p_shape_b.ptr, p_shape_b.len, (double)p_factor, &out_len, &err); + Jni_release_bytes(env, &p_shape_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + +JNIEXPORT jlong JNICALL Java_shapes_JniBridge_shapes_1sum_1bytes(JNIEnv* env, jclass cls, jbyteArray p_values) { + shapes_error err = {0, NULL, NULL, 0}; + (void)cls; + Jni_bytes p_values_b = Jni_borrow_bytes(env, p_values); + uint64_t rv = shapes_shapes_sum_bytes(p_values_b.ptr, p_values_b.len, &err); + Jni_release_bytes(env, &p_values_b); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return 0; + } + return (jlong)rv; +} + +JNIEXPORT jbyteArray JNICALL Java_shapes_JniBridge_shapes_1make_1numerics(JNIEnv* env, jclass cls, jbyte p_a, jshort p_b, jint p_c, jlong p_d, jbyte p_e, jshort p_f, jlong p_g, jlong p_h, jfloat p_i, jdouble p_j) { + shapes_error err = {0, NULL, NULL, 0}; + (void)cls; + size_t out_len = 0; + const uint8_t* rv = shapes_shapes_make_numerics((int8_t)p_a, (int16_t)p_b, (int32_t)p_c, (int64_t)p_d, (uint8_t)p_e, (uint16_t)p_f, (uint32_t)p_g, (uint64_t)p_h, (float)p_i, (double)p_j, &out_len, &err); + if (err.code != 0) { + Jni_throw(env, &err, 0); + return NULL; + } + return Jni_take_bytes(env, rv, out_len); +} + + +// END shapes_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index a3d43bd8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,174 +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" - -/* Copies `s` as a NUL-terminated standard UTF-8 string (JNI's modified - UTF-8 would encode supplementary characters as surrogate pairs the - producer rejects). Lone surrogates become U+FFFD. Release with free(). - Returns NULL for a NULL string, for a string holding U+0000 (which no - C string can carry), or on allocation failure; the producer then - reports the null argument through its marshalling error. */ -static char* weaveffi_jni_string_to_utf8(JNIEnv* env, jstring s) { - if (s == NULL) { return NULL; } - jsize n = (*env)->GetStringLength(env, s); - const jchar* u = (*env)->GetStringChars(env, s, NULL); - if (u == NULL) { return NULL; } - char* out = (char*)malloc((size_t)n * 4u + 1u); - if (out == NULL) { (*env)->ReleaseStringChars(env, s, u); return NULL; } - size_t o = 0; - for (jsize i = 0; i < n; i++) { - uint32_t c = u[i]; - if (c == 0u) { free(out); (*env)->ReleaseStringChars(env, s, u); return NULL; } - if (c >= 0xD800u && c <= 0xDBFFu && i + 1 < n && u[i + 1] >= 0xDC00u && u[i + 1] <= 0xDFFFu) { - c = 0x10000u + ((c - 0xD800u) << 10) + ((uint32_t)u[i + 1] - 0xDC00u); - i++; - } - else if (c >= 0xD800u && c <= 0xDFFFu) { c = 0xFFFDu; } - if (c < 0x80u) { out[o++] = (char)c; } - else if (c < 0x800u) { out[o++] = (char)(0xC0u | (c >> 6)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else if (c < 0x10000u) { out[o++] = (char)(0xE0u | (c >> 12)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - else { out[o++] = (char)(0xF0u | (c >> 18)); out[o++] = (char)(0x80u | ((c >> 12) & 0x3Fu)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); } - } - out[o] = 0; - (*env)->ReleaseStringChars(env, s, u); - return out; -} - -/* Builds a jstring from standard UTF-8 (NewStringUTF expects modified UTF-8 - and mangles supplementary characters). NULL yields the empty string and - a malformed sequence yields U+FFFD, so this never fails on producer - output; it returns NULL only when the JVM could not allocate. */ -static jstring weaveffi_jni_utf8_to_string(JNIEnv* env, const char* s) { - if (s == NULL) { s = ""; } - size_t n = strlen(s); - jchar* buf = (jchar*)malloc((n + 1u) * sizeof(jchar)); - if (buf == NULL) { return NULL; } - size_t i = 0, o = 0; - while (i < n) { - unsigned char b = (unsigned char)s[i]; - uint32_t c; size_t len; - if (b < 0x80u) { c = b; len = 1; } - else if ((b & 0xE0u) == 0xC0u) { c = b & 0x1Fu; len = 2; } - else if ((b & 0xF0u) == 0xE0u) { c = b & 0x0Fu; len = 3; } - else if ((b & 0xF8u) == 0xF0u) { c = b & 0x07u; len = 4; } - else { c = 0xFFFDu; len = 1; } - if (i + len > n) { c = 0xFFFDu; len = 1; } - for (size_t k = 1; k < len; k++) { - unsigned char cb = (unsigned char)s[i + k]; - if ((cb & 0xC0u) != 0x80u) { c = 0xFFFDu; len = k; break; } - c = (c << 6) | (cb & 0x3Fu); - } - i += len; - if (c > 0x10FFFFu) { c = 0xFFFDu; } - if (c >= 0x10000u) { c -= 0x10000u; buf[o++] = (jchar)(0xD800u + (c >> 10)); buf[o++] = (jchar)(0xDC00u + (c & 0x3FFu)); } - else { buf[o++] = (jchar)c; } - } - jstring out = (*env)->NewString(env, buf, (jsize)o); - free(buf); - return out; -} - -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 = weaveffi_jni_utf8_to_string(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 JavaVM* weaveffi_jni_vm = NULL; - -static jint weaveffi_jni_load_error(JNIEnv* env, const char* message) { - jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); - if (cls != NULL) { (*env)->ThrowNew(env, cls, message); } - return JNI_ERR; -} - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - (void)reserved; - weaveffi_jni_vm = vm; - JNIEnv* env = NULL; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - if (weaveffi_abi_version() != 2u) { - return weaveffi_jni_load_error(env, "WeaveFFI: the native library implements a different C ABI revision than these bindings (expected 2)"); - } - return JNI_VERSION_1_6; -} - -JNIEXPORT jstring JNICALL Java_com_weaveffi_WeaveFFI_describeJni(JNIEnv* env, jclass clazz, jbyteArray shape) { - weaveffi_error err = {0, NULL, NULL, 0}; - jbyte* shape_elems = (*env)->GetByteArrayElements(env, shape, NULL); - jsize shape_len = (*env)->GetArrayLength(env, shape); - const char* rv = weaveffi_shapes_describe((const uint8_t*)shape_elems, (size_t)shape_len, &err); - (*env)->ReleaseByteArrayElements(env, shape, shape_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jstring out = weaveffi_jni_utf8_to_string(env, rv); - weaveffi_free_string(rv); - return out; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_scaleJni(JNIEnv* env, jclass clazz, jbyteArray shape, jdouble factor) { - weaveffi_error err = {0, NULL, NULL, 0}; - jbyte* shape_elems = (*env)->GetByteArrayElements(env, shape, NULL); - jsize shape_len = (*env)->GetArrayLength(env, shape); - size_t out_len = 0; - const uint8_t* rv = weaveffi_shapes_scale((const uint8_t*)shape_elems, (size_t)shape_len, (double)factor, &out_len, &err); - (*env)->ReleaseByteArrayElements(env, shape, shape_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 jlong JNICALL Java_com_weaveffi_WeaveFFI_sumBytes(JNIEnv* env, jclass clazz, jbyteArray values) { - weaveffi_error err = {0, NULL, NULL, 0}; - jboolean values_is_copy = 0; - jbyte* values_elems = (*env)->GetByteArrayElements(env, values, &values_is_copy); - jsize values_len = (*env)->GetArrayLength(env, values); - uint64_t rv = weaveffi_shapes_sum_bytes((const uint8_t*)values_elems, (size_t)values_len, &err); - (*env)->ReleaseByteArrayElements(env, values, values_elems, 0); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong) rv; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_makeNumericsJni(JNIEnv* env, jclass clazz, jbyte a, jshort b, jint c, jlong d, jbyte e, jshort f, jlong g, jlong h, jfloat i, jdouble j) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_shapes_make_numerics((int8_t)a, (int16_t)b, (int32_t)c, (int64_t)d, (uint8_t)e, (uint16_t)f, (uint32_t)g, (uint64_t)h, (float)i, (double)j, &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; -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index c8ea8a98..00000000 --- a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,289 +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 { WeaveNativeLibrary.ensureLoaded() } - - /** - * Render a shape to a string - */ - @JvmStatic private external fun describeJni(shape: ByteArray): String - @JvmStatic fun describe(shape: Shape): String = describeJni(weaveEncode { w -> packShape(w, shape) }) - /** - * Scale a shape by a factor, returning a new shape - */ - @JvmStatic private external fun scaleJni(shape: ByteArray, factor: Double): ByteArray - @JvmStatic fun scale(shape: Shape, factor: Double): Shape = weaveDecode(scaleJni(weaveEncode { w -> packShape(w, shape) }, factor)) { r -> unpackShape(r) } - /** - * Sum a list of bytes into a wide integer (numerics smoke) - */ - @JvmStatic external fun sumBytes(values: ByteArray): Long - /** - * Build a Numerics struct from every numeric primitive - */ - @JvmStatic private external fun makeNumericsJni(a: Byte, b: Short, c: Int, d: Long, e: Byte, f: Short, g: Long, h: Long, i: Float, j: Double): ByteArray - @JvmStatic fun makeNumerics(a: Byte, b: Short, c: Int, d: Long, e: Byte, f: Short, g: Long, h: Long, i: Float, j: Double): Numerics = weaveDecode(makeNumericsJni(a, b, c, d, e, f, g, h, i, j)) { r -> unpackNumerics(r) } - } -} - -/** An algebraic shape (sum type with associated data) */ -sealed class Shape { - /** The empty shape (a unit variant) */ - object Empty : Shape() - /** A circle with a radius */ - data class Circle( - /** Radius in points */ - val radius: Double, - ) : Shape() - /** An axis-aligned rectangle */ - data class Rectangle(val width: Float, val height: Float) : Shape() - /** An integer-bounded shape (exercises narrow + wide ints) */ - data class Bounded(val min: Short, val max: Short, val weight: Long) : Shape() - /** A labeled shape with a small count */ - data class Labeled(val label: String, val count: Byte) : Shape() -} - -internal fun packShape(w: WeaveBufferWriter, v: Shape) { - when (v) { - is Shape.Empty -> w.writeI32(0) - is Shape.Circle -> { - w.writeI32(1) - w.writeF64(v.radius) - } - is Shape.Rectangle -> { - w.writeI32(2) - w.writeF32(v.width) - w.writeF32(v.height) - } - is Shape.Bounded -> { - w.writeI32(3) - w.writeI16(v.min) - w.writeI16(v.max) - w.writeI64(v.weight) - } - is Shape.Labeled -> { - w.writeI32(4) - w.writeString(v.label) - w.writeI8(v.count) - } - } -} - -internal fun unpackShape(r: WeaveBufferReader): Shape = when (val tag = r.readI32()) { - 0 -> Shape.Empty - 1 -> Shape.Circle(r.readF64()) - 2 -> Shape.Rectangle(r.readF32(), r.readF32()) - 3 -> Shape.Bounded(r.readI16(), r.readI16(), r.readI64()) - 4 -> Shape.Labeled(r.readString(), r.readI8()) - else -> throw WeaveFFIException(-2, "malformed WeaveFFI value buffer: unknown Shape tag $tag") -} - -/** A plain C-style enum (no payloads) */ -enum class Channel(val value: Int) { - Red(0), - Green(1), - Blue(2); - - companion object { - fun fromValue(value: Int): Channel = entries.first { it.value == value } - } -} - -/** One field per supported numeric primitive */ -data class Numerics(val a: Byte, val b: Short, val c: Int, val d: Long, val e: Byte, val f: Short, val g: Long, val h: Long, val i: Float, val j: Double) - -internal fun packNumerics(w: WeaveBufferWriter, v: Numerics) { - w.writeI8(v.a) - w.writeI16(v.b) - w.writeI32(v.c) - w.writeI64(v.d) - w.writeI8(v.e) - w.writeI16(v.f) - w.writeU32(v.g) - w.writeI64(v.h) - w.writeF32(v.i) - w.writeF64(v.j) -} - -internal fun unpackNumerics(r: WeaveBufferReader): Numerics = Numerics( - r.readI8(), - r.readI16(), - r.readI32(), - r.readI64(), - r.readI8(), - r.readI16(), - r.readU32(), - r.readI64(), - r.readF32(), - r.readF64(), -) - -/** Generic WeaveFFI failure: panics, marshalling errors, callback implementation failures (code -4), and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Loads the native library once; every companion `init` calls [ensureLoaded]. */ -internal object WeaveNativeLibrary { - init { System.loadLibrary("weaveffi") } - - fun ensureLoaded() {} -} - -/** 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()) - /** An object token: one strong reference the caller adopts. Zero is never valid here. */ - fun readObject(): Long { - val token = readI64() - if (token == 0L) malformed("null object token") - return token - } - 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/kotlin_shapes__src_main_kotlin_shapes_Buffers_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Buffers_kt.snap new file mode 100644 index 00000000..92661754 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Buffers_kt.snap @@ -0,0 +1,255 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package shapes + +/** + * Growable little-endian writer for the value-buffer wire format that + * records, rich enums, optionals, lists, and maps cross the ABI in. + */ +internal class BufferWriter { + private var buf = ByteArray(64) + private var len = 0 + private var minted: ArrayList Unit>>? = null + + private fun reserve(extra: Int) { + if (len + extra <= buf.size) return + var cap = buf.size * 2 + while (cap < len + extra) cap *= 2 + buf = buf.copyOf(cap) + } + + fun toByteArray(): ByteArray = buf.copyOf(len) + + fun writeBool(v: Boolean) { + reserve(1) + buf[len++] = if (v) 1 else 0 + } + + fun writeI8(v: Byte) { + reserve(1) + buf[len++] = v + } + + fun writeU8(v: Byte) = writeI8(v) + + fun writeI16(v: Short) { + reserve(2) + val b = v.toInt() + buf[len++] = b.toByte() + buf[len++] = (b shr 8).toByte() + } + + fun writeU16(v: Short) = writeI16(v) + + fun writeI32(v: Int) { + reserve(4) + buf[len++] = v.toByte() + buf[len++] = (v shr 8).toByte() + buf[len++] = (v shr 16).toByte() + buf[len++] = (v shr 24).toByte() + } + + fun writeU32(v: Long) = writeI32(v.toInt()) + + fun writeI64(v: Long) { + writeI32(v.toInt()) + writeI32((v ushr 32).toInt()) + } + + fun writeU64(v: Long) = writeI64(v) + + fun writeF32(v: Float) = writeI32(v.toRawBits()) + + fun writeF64(v: Double) = writeI64(v.toRawBits()) + + fun writeString(v: String) = writeBytes(encodeUtf8(v)) + + fun writeBytes(v: ByteArray) { + writeI32(v.size) + reserve(v.size) + v.copyInto(buf, len) + len += v.size + } + + /** + * An object token: one new strong reference the reader adopts, which + * [abandon] releases with [release] if the encoding fails. + */ + fun writeObject(token: Long, release: (Long) -> Unit) { + val list = minted ?: ArrayList Unit>>().also { minted = it } + list.add(token to release) + writeI64(token) + } + + /** Releases every object token written so far, for an encoding that failed midway. */ + fun abandon() { + minted?.forEach { (token, release) -> release(token) } + minted = null + } + + inline fun writeOptional(v: T?, write: (T) -> Unit) { + if (v == null) { + writeBool(false) + } else { + writeBool(true) + write(v) + } + } + + inline fun writeList(v: List, write: (T) -> Unit) { + writeI32(v.size) + for (e in v) write(e) + } + + inline fun writeMap(v: Map, writeKey: (K) -> Unit, writeValue: (V) -> Unit) { + writeI32(v.size) + for ((k, e) in v) { + writeKey(k) + writeValue(e) + } + } +} + +/** + * Validating little-endian reader for the value-buffer wire format. A + * truncated buffer, an invalid flag byte, a zero object token, an oversized + * length, or trailing bytes raise [FfiException] code -3. + */ +internal class BufferReader(private val buf: ByteArray) { + private var pos = 0 + + private fun malformed(detail: String): Nothing = + throw FfiException(-3, "malformed value buffer: $detail") + + private fun take(n: Int): Int { + if (n > buf.size - pos) malformed("truncated buffer") + val at = pos + pos += n + return at + } + + fun readBool(): Boolean = when (buf[take(1)].toInt()) { + 0 -> false + 1 -> true + else -> malformed("invalid bool byte") + } + + fun readI8(): Byte = buf[take(1)] + + fun readU8(): Byte = readI8() + + fun readI16(): Short { + val at = take(2) + return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() + } + + fun readU16(): Short = readI16() + + 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 readU64(): Long = readI64() + + fun readF32(): Float = Float.fromBits(readI32()) + + fun readF64(): Double = Double.fromBits(readI64()) + + private fun readLen(): Int { + val n = readI32() + if (n < 0 || n > buf.size - pos) malformed("length prefix exceeds remaining bytes") + return n + } + + fun readString(): String { + val n = readLen() + val at = take(n) + return try { + Charsets.UTF_8.newDecoder().decode(java.nio.ByteBuffer.wrap(buf, at, n)).toString() + } catch (e: java.nio.charset.CharacterCodingException) { + malformed("string is not valid UTF-8") + } + } + + fun readBytes(): ByteArray { + val n = readLen() + val at = take(n) + return buf.copyOfRange(at, at + n) + } + + /** An object token: one strong reference the caller adopts. */ + fun readObject(): Long { + val token = readI64() + if (token == 0L) malformed("null object token") + return token + } + + inline fun readOptional(read: () -> T): T? = if (readBool()) read() else null + + inline fun readList(read: () -> T): List { + val n = readCount() + val out = ArrayList(minOf(n, 4096)) + repeat(n) { out.add(read()) } + return out + } + + inline fun readMap(readKey: () -> K, readValue: () -> V): Map { + val n = readCount() + val out = LinkedHashMap(minOf(n, 4096)) + repeat(n) { + val k = readKey() + out[k] = readValue() + } + return out + } + + /** An element count; elements may encode to zero bytes, so only the sign is checked. */ + fun readCount(): Int { + val n = readI32() + if (n < 0) malformed("negative element count") + return n + } + + fun expectEnd() { + if (pos != buf.size) malformed("trailing bytes after value") + } +} + +/** Encodes one value into a fresh value buffer. */ +internal inline fun encodeBuffer(write: (BufferWriter) -> Unit): ByteArray { + val w = BufferWriter() + try { + write(w) + } catch (t: Throwable) { + w.abandon() + throw t + } + return w.toByteArray() +} + +/** Decodes one value from [bytes], rejecting malformed or trailing bytes. */ +internal inline fun decodeBuffer(bytes: ByteArray, read: (BufferReader) -> T): T { + val r = BufferReader(bytes) + val v = read(r) + r.expectEnd() + return v +} + +// END Buffers.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_JniBridge_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_JniBridge_kt.snap new file mode 100644 index 00000000..9ddcaa17 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_JniBridge_kt.snap @@ -0,0 +1,37 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package shapes + +/** + * The JNI entry points of `libshapes_jni`: one native per C symbol, named after + * it, plus the hooks the shim calls back into. Internal plumbing for the + * public wrappers. + */ +internal object JniBridge { + init { + NativeLibrary.ensureLoaded() + } + + /** The exception for a native failure; domain 0 is the generic one. */ + @JvmStatic + @Suppress("UNUSED_PARAMETER") + fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): FfiException { + val text = decodeUtf8(message) + return when (domain) { + else -> FfiException(code, text) + } + } + + @JvmStatic external fun debug_live(kind: Int): Long + @JvmStatic external fun shapes_describe(shape: ByteArray): ByteArray + @JvmStatic external fun shapes_scale(shape: ByteArray, factor: Double): ByteArray + @JvmStatic external fun shapes_sum_bytes(values: ByteArray): Long + @JvmStatic external fun shapes_make_numerics(a: Byte, b: Short, c: Int, d: Long, e: Byte, f: Short, g: Long, h: Long, i: Float, j: Double): ByteArray +} + +// END JniBridge.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Runtime_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Runtime_kt.snap new file mode 100644 index 00000000..e2942e6d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Runtime_kt.snap @@ -0,0 +1,212 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package shapes + +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * A failure reported by the native library: a producer panic (code -2), a + * marshalling failure (-3), a callback implementation that threw (-4), or any + * code these bindings don't know. Typed error domains subclass it. + */ +open class FfiException(val code: Int, message: String) : Exception(message) + +/** + * A lazy sequence streamed from the native library, one element per step. + * The native iterator is released when the sequence is exhausted, when + * [close] is called, or once this object becomes unreachable, whichever + * comes first. + */ +class NativeIterator internal constructor( + address: Long, + release: (Long) -> Unit, + private val step: (Long) -> Any?, +) : Iterator, AutoCloseable { + private val handle = NativeCleaner.register(this, NativeHandle(address, release)) + private var lookahead: Any? = EMPTY + + override fun hasNext(): Boolean { + if (lookahead !== EMPTY) return true + if (handle.isClosed) return false + val item = handle.borrow(step) + if (item === DONE) { + close() + return false + } + lookahead = item + return true + } + + override fun next(): T { + if (!hasNext()) throw NoSuchElementException() + val item = lookahead + lookahead = EMPTY + @Suppress("UNCHECKED_CAST") + return item as T + } + + /** Releases the native iterator; safe to call more than once. */ + override fun close() = handle.close() + + internal companion object { + /** What a step function returns once the native sequence is exhausted. */ + val DONE = Any() + private val EMPTY = Any() + } +} + +/** Loads the producer library and then the JNI shim, exactly once. */ +internal object NativeLibrary { + private const val LIBRARY = "shapes" + private const val JNI_LIBRARY = "shapes_jni" + + init { + val explicit = System.getenv("SHAPES_LIBRARY") + if (!explicit.isNullOrEmpty()) { + System.load(explicit) + } else if (!loadBundled(LIBRARY)) { + // The shim's own link dependency usually finds the producer when + // it isn't on java.library.path, so a miss here is only fatal if + // loading the shim fails too. + try { + System.loadLibrary(LIBRARY) + } catch (ignored: UnsatisfiedLinkError) { + } + } + if (!loadBundled(JNI_LIBRARY)) System.loadLibrary(JNI_LIBRARY) + } + + fun ensureLoaded() {} + + /** + * Loads `base` from the `natives/-/` classpath resources a + * packaged desktop build bundles; false when it isn't bundled. + */ + private fun loadBundled(base: String): Boolean { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + val osId = when { + os.contains("mac") || os.contains("darwin") -> "darwin" + os.contains("win") -> "windows" + else -> "linux" + } + val archId = if (arch == "aarch64" || arch == "arm64") "arm64" else "x64" + val file = when (osId) { + "darwin" -> "lib$base.dylib" + "windows" -> "$base.dll" + else -> "lib$base.so" + } + val stream = NativeLibrary::class.java.getResourceAsStream("/natives/$osId-$archId/$file") ?: return false + val dir = java.nio.file.Files.createTempDirectory("$base-natives") + val target = dir.resolve(file) + stream.use { java.nio.file.Files.copy(it, target) } + target.toFile().deleteOnExit() + dir.toFile().deleteOnExit() + System.load(target.toAbsolutePath().toString()) + return true + } +} + +/** + * One strong reference to a native object, released exactly once: by + * [close], or by [NativeCleaner] once the owning wrapper is unreachable. + * Every call borrows the address for its duration, and a close that races an + * in-flight call defers the release until the last borrow ends, so the object + * is never freed while native code is using it. + */ +internal class NativeHandle(val address: Long, private val release: (Long) -> Unit) { + // Bit 0 is the closed flag; the remaining bits count in-flight borrows. + private val state = AtomicLong(0L) + + val isClosed: Boolean get() = state.get() and 1L != 0L + + fun acquire(): Long { + while (true) { + val s = state.get() + check(s and 1L == 0L) { "native object used after close()" } + if (state.compareAndSet(s, s + 2L)) return address + } + } + + fun unacquire() { + if (state.addAndGet(-2L) == 1L) release(address) + } + + fun close() { + while (true) { + val s = state.get() + if (s and 1L != 0L) return + if (state.compareAndSet(s, s or 1L)) { + if (s == 0L) release(address) + return + } + } + } +} + +/** Runs [block] with the native address borrowed for its duration. */ +internal inline fun NativeHandle.borrow(block: (Long) -> R): R { + val address = acquire() + try { + return block(address) + } finally { + unacquire() + } +} + +/** [borrow] for an optional object: a missing object lends address 0. */ +internal inline fun NativeHandle?.borrowOrNull(block: (Long) -> R): R = + if (this == null) block(0L) else borrow(block) + +/** + * Releases the native reference of every wrapper that becomes unreachable + * without being closed. A daemon thread drains a phantom-reference queue, so + * this works on every Android API level and every JVM. + */ +internal object NativeCleaner { + private val queue = ReferenceQueue() + private val pending: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) + + private class Entry(owner: Any, val handle: NativeHandle, queue: ReferenceQueue) : + PhantomReference(owner, queue) + + init { + val thread = Thread({ drain() }, "shapes-cleaner") + thread.isDaemon = true + thread.start() + } + + /** Releases [handle] once [owner] is unreachable; returns [handle]. */ + fun register(owner: Any, handle: NativeHandle): NativeHandle { + pending.add(Entry(owner, handle, queue)) + return handle + } + + private fun drain() { + while (true) { + val entry = queue.remove() as Entry + pending.remove(entry) + try { + entry.handle.close() + } catch (_: Throwable) { + } + } + } +} + +/** UTF-8 bytes of [value], for a string crossing into native code. */ +internal fun encodeUtf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8) + +/** The string a native UTF-8 byte sequence spells. */ +internal fun decodeUtf8(bytes: ByteArray): String = String(bytes, Charsets.UTF_8) + +// END Runtime.kt diff --git a/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Shapes_kt.snap b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Shapes_kt.snap new file mode 100644 index 00000000..0e03c6b8 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/kotlin_shapes__src_main_kotlin_shapes_Shapes_kt.snap @@ -0,0 +1,119 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +package shapes + +/** An algebraic shape (sum type with associated data) */ +sealed class Shape { + /** The empty shape (a unit variant) */ + object Empty : Shape() + /** A circle with a radius */ + data class Circle( + /** Radius in points */ + val radius: Double, + ) : Shape() + /** An axis-aligned rectangle */ + data class Rectangle(val width: Float, val height: Float) : Shape() + /** An integer-bounded shape (exercises narrow + wide ints) */ + data class Bounded(val min: Short, val max: Short, val weight: Long) : Shape() + /** A labeled shape with a small count */ + data class Labeled(val label: String, val count: Byte) : Shape() +} + +internal fun packShape(_w: BufferWriter, _v: Shape) { + when (_v) { + is Shape.Empty -> _w.writeI32(0) + is Shape.Circle -> { + _w.writeI32(1) + _w.writeF64(_v.radius) + } + is Shape.Rectangle -> { + _w.writeI32(2) + _w.writeF32(_v.width) + _w.writeF32(_v.height) + } + is Shape.Bounded -> { + _w.writeI32(3) + _w.writeI16(_v.min) + _w.writeI16(_v.max) + _w.writeU64(_v.weight) + } + is Shape.Labeled -> { + _w.writeI32(4) + _w.writeString(_v.label) + _w.writeU8(_v.count) + } + } +} + +internal fun unpackShape(_r: BufferReader): Shape = when (val _tag = _r.readI32()) { + 0 -> Shape.Empty + 1 -> Shape.Circle(_r.readF64()) + 2 -> Shape.Rectangle(_r.readF32(), _r.readF32()) + 3 -> Shape.Bounded(_r.readI16(), _r.readI16(), _r.readU64()) + 4 -> Shape.Labeled(_r.readString(), _r.readU8()) + else -> throw FfiException(-3, "malformed value buffer: unknown Shape tag $_tag") +} + +/** A plain C-style enum (no payloads) */ +enum class Channel(val value: Int) { + Red(0), + Green(1), + Blue(2); + + companion object { + /** The variant whose ABI value is [value]. */ + fun fromValue(value: Int): Channel = entries.firstOrNull { it.value == value } + ?: throw FfiException(-3, "unknown Channel value $value") + } +} + +/** One field per supported numeric primitive */ +data class Numerics(val a: Byte, val b: Short, val c: Int, val d: Long, val e: Byte, val f: Short, val g: Long, val h: Long, val i: Float, val j: Double) + +internal fun packNumerics(_w: BufferWriter, _v: Numerics) { + _w.writeI8(_v.a) + _w.writeI16(_v.b) + _w.writeI32(_v.c) + _w.writeI64(_v.d) + _w.writeU8(_v.e) + _w.writeU16(_v.f) + _w.writeU32(_v.g) + _w.writeU64(_v.h) + _w.writeF32(_v.i) + _w.writeF64(_v.j) +} + +internal fun unpackNumerics(_r: BufferReader): Numerics = Numerics( + _r.readI8(), + _r.readI16(), + _r.readI32(), + _r.readI64(), + _r.readU8(), + _r.readU16(), + _r.readU32(), + _r.readU64(), + _r.readF32(), + _r.readF64(), +) + +/** Rich (algebraic) enums and the expanded numeric primitives. */ +object Shapes { + /** Render a shape to a string */ + fun describe(shape: Shape): String = decodeUtf8(JniBridge.shapes_describe(encodeBuffer { _w -> packShape(_w, shape) })) + + /** Scale a shape by a factor, returning a new shape */ + fun scale(shape: Shape, factor: Double): Shape = decodeBuffer(JniBridge.shapes_scale(encodeBuffer { _w -> packShape(_w, shape) }, factor)) { _r -> unpackShape(_r) } + + /** Sum a list of bytes into a wide integer (numerics smoke) */ + fun sumBytes(values: ByteArray): Long = JniBridge.shapes_sum_bytes(values) + + /** Build a Numerics struct from every numeric primitive */ + fun makeNumerics(a: Byte, b: Short, c: Int, d: Long, e: Byte, f: Short, g: Long, h: Long, i: Float, j: Double): Numerics = decodeBuffer(JniBridge.shapes_make_numerics(a, b, c, d, e, f, g, h, i, j)) { _r -> unpackNumerics(_r) } +} + +// END Shapes.kt diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__README_md.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__README_md.snap new file mode 100644 index 00000000..ae0e76ff --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__README_md.snap @@ -0,0 +1,24 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + +# docs_everywhere + +Native bindings for docs_everywhere + +Node.js bindings for the `docs_everywhere` native library: an ES module over a small N-API addon (`docs_everywhere_node.c`) that `npm install` compiles with node-gyp. The addon links `docs_everywhere`; set `DOCS_EVERYWHERE_LIBRARY` to the library file (or its directory) when installing, or pass `--docs_everywhere-library=` to npm. + +```sh +DOCS_EVERYWHERE_LIBRARY=/path/to/libdocs_everywhere.so npm install docs_everywhere +``` + +Each top-level module of the API is a namespace export: + +```js +import { docs } from 'docs_everywhere'; +``` + + diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__binding_gyp.snap index 3997b5bb..318fe42a 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__binding_gyp.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__binding_gyp.snap @@ -1,17 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o { + "variables": { + "native_dir%": " { const path = require('path'); const p = process.env.DOCS_EVERYWHERE_LIBRARY || process.env.npm_config_docs_everywhere_library; if (!p) return path.resolve('.'); return require('fs').statSync(p).isFile() ? path.dirname(path.resolve(p)) : path.resolve(p); })()\")" + }, "targets": [ { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] + "target_name": "docs_everywhere_node", + "sources": [ + "docs_everywhere_node.c" + ], + "include_dirs": [ + "." + ], + "library_dirs": [ + "<(native_dir)" + ], + "libraries": [ + "-ldocs_everywhere" + ], + "conditions": [ + ["OS=='mac'", { "xcode_settings": { "OTHER_LDFLAGS": ["-Wl,-rpath,@loader_path", "-Wl,-rpath,<(native_dir)"] } }], + ["OS=='linux'", { "ldflags": ["-Wl,-rpath,'$$ORIGIN'", "-Wl,-rpath,<(native_dir)"] }] + ] } ] } diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_h.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_h.snap new file mode 100644 index 00000000..31ab85e2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_h.snap @@ -0,0 +1,196 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef DOCS_EVERYWHERE_H +#define DOCS_EVERYWHERE_H + +#include +#include +#include + +#ifndef DOCS_EVERYWHERE_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef DOCS_EVERYWHERE_BUILD +# define DOCS_EVERYWHERE_API __declspec(dllexport) +# else +# define DOCS_EVERYWHERE_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define DOCS_EVERYWHERE_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define DOCS_EVERYWHERE_API __attribute__((visibility("default"))) +# else +# define DOCS_EVERYWHERE_API +# endif +#endif + +#ifndef DOCS_EVERYWHERE_DEPRECATED +# if defined(_MSC_VER) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define DOCS_EVERYWHERE_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports docs_everywhere_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define DOCS_EVERYWHERE_ABI_VERSION 3u +DOCS_EVERYWHERE_API uint32_t docs_everywhere_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by docs_everywhere_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct docs_everywhere_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} docs_everywhere_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_set(docs_everywhere_error* err, int32_t code, const char* message); +DOCS_EVERYWHERE_API void docs_everywhere_error_clear(docs_everywhere_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + docs_everywhere_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_free(docs_everywhere_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +DOCS_EVERYWHERE_API void docs_everywhere_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct docs_everywhere_cancel_token docs_everywhere_cancel_token; +DOCS_EVERYWHERE_API docs_everywhere_cancel_token* docs_everywhere_cancel_token_create(void); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_cancel(docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API bool docs_everywhere_cancel_token_is_cancelled(const docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_destroy(docs_everywhere_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +DOCS_EVERYWHERE_API uint8_t* docs_everywhere_alloc(uint32_t size); +DOCS_EVERYWHERE_API void docs_everywhere_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +DOCS_EVERYWHERE_API uint64_t docs_everywhere_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define DOCS_EVERYWHERE_DOCS_CHECKSUM 0x7950eec1e9cfc819ull +DOCS_EVERYWHERE_API uint64_t docs_everywhere_docs_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with docs_everywhere_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * docs_everywhere_buffer.h declares a C struct and codec for each. + */ + +/** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ +typedef enum { + /** Document is being edited */ + docs_everywhere_docs_Status_Draft = 0, + /** Document is publicly visible */ + docs_everywhere_docs_Status_Published = 1, + /** Document is hidden from listings */ + docs_everywhere_docs_Status_Archived = 2 +} docs_everywhere_docs_Status; +/** Error codes reported by throwing functions in the `docs` module tree. */ +typedef enum { + /** Returned when no document matches the requested id */ + docs_everywhere_docs_DocsErrors_not_found = 1, + /** Raised when attempting to move into a forbidden state */ + docs_everywhere_docs_DocsErrors_invalid_status = 2 +} docs_everywhere_docs_DocsErrors; +typedef struct docs_everywhere_docs_Notebook docs_everywhere_docs_Notebook; +/** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + * + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct docs_everywhere_docs_SavedListener_vtable { + /** Fires after a document has been persisted successfully */ + void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); + void (*free)(void* ctx); +} docs_everywhere_docs_SavedListener_vtable; +typedef void (*docs_everywhere_docs_publish_async_callback)(void* context, docs_everywhere_error* err, bool result); + +// Module: docs +/** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ +/** Create an empty notebook */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_new(docs_everywhere_error* out_err); +/** The largest number of documents one notebook will hold */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_max_documents(docs_everywhere_error* out_err); +/** Add a document, returning its position in the notebook */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_insert(const docs_everywhere_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, docs_everywhere_error* out_err); +/** Look up a document by identifier, failing when none matches */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_Notebook_find(const docs_everywhere_docs_Notebook* self, int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** The number of documents currently held */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_size(const docs_everywhere_docs_Notebook* self, docs_everywhere_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_clone(const docs_everywhere_docs_Notebook* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_Notebook_destroy(docs_everywhere_docs_Notebook* self); + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_create_document(const uint8_t* title_ptr, size_t title_len, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, docs_everywhere_error* out_err); +/** Look up a document by identifier */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_get_document(int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** Publish a document in the background */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_publish_async(int64_t id, docs_everywhere_docs_publish_async_callback callback, void* context); +/** Register a listener for saved notifications */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_watch(void* listener_ctx, const docs_everywhere_docs_SavedListener_vtable* listener_vtable, docs_everywhere_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // DOCS_EVERYWHERE_H + +// END docs_everywhere.h diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_node_c.snap new file mode 100644 index 00000000..ae260e29 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__docs_everywhere_node_c.snap @@ -0,0 +1,896 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NAPI_VERSION +#define NAPI_VERSION 8 +#endif +#include +#include +#include +#include +#include +#include + +/* The addon wraps deprecated functions too; the JavaScript API carries the + * deprecation instead. */ +#define DOCS_EVERYWHERE_DEPRECATED(msg) +#include "docs_everywhere.h" + +/* + * The N-API transport of the JavaScript bindings. Every C symbol of the + * library is exported to `index.js` under its own name and called with the + * raw convention the shared JavaScript layer documents: direct values as + * numbers, bigints, and booleans; strings as strings; bytes and value + * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint + * handles (null when absent); callback interfaces as adapter objects. A + * failure throws an instance of the runtime's `$Fault` class. + */ + +typedef docs_everywhere_error js_error; + +/* Helpers an API may not need. */ +#if defined(__GNUC__) || defined(__clang__) +#define JS_HELPER static inline __attribute__((unused)) +#else +#define JS_HELPER static inline +#endif + +/* Per-environment state (one per main thread or worker): the `$Fault` + * constructor `index.js` registers through `$setup`. */ +typedef struct { + napi_ref fault; +} js_env; + +static void js_env_free(napi_env env, void* data, void* hint) { + (void)hint; + js_env* e = (js_env*)data; + if (e->fault != NULL) napi_delete_reference(env, e->fault); + free(e); +} + +JS_HELPER napi_value js_undefined(napi_env env) { + napi_value v; + napi_get_undefined(env, &v); + return v; +} + +static napi_value js_setup(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e->fault != NULL) napi_delete_reference(env, e->fault); + napi_create_reference(env, argv[0], 1, &e->fault); + return js_undefined(env); +} + +/* --------------------------------------------------------------------------- + * Value conversions + * ------------------------------------------------------------------------- */ + +JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value ab, out; + void* data = NULL; + napi_create_arraybuffer(env, len, &data, &ab); + if (len > 0) memcpy(data, ptr, len); + napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); + return out; +} + +JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out; + napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); + return out; +} + +JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { + napi_value out; + napi_create_int32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { + napi_value out; + napi_create_uint32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { + napi_value out; + napi_create_bigint_int64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { + napi_value out; + napi_create_bigint_uint64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_f64(napi_env env, double v) { + napi_value out; + napi_create_double(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_bool(napi_env env, bool v) { + napi_value out; + napi_get_boolean(env, v, &out); + return out; +} + +/* An object, iterator, or cancel token handle: the pointer as a bigint, or + * null for NULL. */ +JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { + napi_value out; + if (p == NULL) { + napi_get_null(env, &out); + } else { + napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); + } + return out; +} + +/* Returned strings and buffers: convert, then release the producer's copy. */ +JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_str(env, ptr, len); + if (ptr != NULL) docs_everywhere_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_bytes(env, ptr, len); + if (ptr != NULL) docs_everywhere_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, + const uint8_t* payload, size_t payload_len) { + napi_value argv[3], ctor, out = NULL; + napi_create_int32(env, code, &argv[0]); + napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); + if (payload != NULL) { + argv[2] = js_new_bytes(env, payload, payload_len); + } else { + napi_get_null(env, &argv[2]); + } + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok || + ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { + napi_create_error(env, NULL, argv[1], &out); + napi_set_named_property(env, out, "code", argv[0]); + } + return out; +} + +/* Throw the error a call reported, then release it. */ +JS_HELPER napi_value js_throw(napi_env env, js_error* err) { + napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); + docs_everywhere_error_clear(err); + return NULL; +} + +JS_HELPER bool js_type_error(napi_env env, const char* expected) { + bool pending = false; + napi_is_exception_pending(env, &pending); + if (!pending) napi_throw_type_error(env, NULL, expected); + return false; +} + +JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { + return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { + return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int8_t)t; + return true; +} + +JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int16_t)t; + return true; +} + +JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint8_t)t; + return true; +} + +JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint16_t)t; + return true; +} + +/* 64-bit integers: a bigint that fits, or an integral number. */ +JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_int64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { + *out = (int64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_uint64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && + d == (double)(uint64_t)d) { + *out = (uint64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { + return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { + double d = 0; + if (!js_arg_f64(env, v, &d)) return false; + *out = (float)d; + return true; +} + +JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { + return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); +} + +/* A handle argument; null or undefined is NULL when `nullable`. */ +JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_null || t == napi_undefined) { + *out = NULL; + return nullable || js_type_error(env, "expected an object handle"); + } + uint64_t raw = 0; + bool lossless = false; + if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || + raw == 0) { + return js_type_error(env, "expected an object handle"); + } + *out = (void*)(uintptr_t)raw; + return true; +} + +/* A borrowed byte run: a Uint8Array (or Buffer). */ +JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { + bool is_typed = false; + napi_is_typedarray(env, v, &is_typed); + if (is_typed) { + napi_typedarray_type type; + void* data = NULL; + napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); + if (type == napi_uint8_array || type == napi_uint8_clamped_array) { + *ptr = (const uint8_t*)data; + return true; + } + } + return js_type_error(env, "expected a Uint8Array"); +} + +/* A string argument encoded as UTF-8: short strings live in the inline + * buffer, longer ones on the heap until js_str_free. */ +typedef struct { + char* ptr; + size_t len; + char inline_buf[256]; +} js_str; + +#define JS_STR_INIT {NULL, 0, {0}} + +JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { + size_t len = 0; + if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { + return js_type_error(env, "expected a string"); + } + s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); + napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); + return true; +} + +JS_HELPER void js_str_free(js_str* s) { + if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); + s->ptr = NULL; +} + +#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) + +/* Read up to `n` arguments; missing ones are undefined. */ +#define JS_ARGS(n) \ + size_t argc = (n); \ + napi_value argv[(n) + 1]; \ + napi_get_cb_info(env, info, &argc, argv, NULL, NULL) + +/* --------------------------------------------------------------------------- + * Async calls: the completion may run on any thread, so it only records the + * result and queues the settlement on the JavaScript thread through a + * thread-safe function (which also keeps the event loop alive meanwhile). + * ------------------------------------------------------------------------- */ + +typedef enum { + JS_R_VOID, + JS_R_I32, + JS_R_U32, + JS_R_I64, + JS_R_U64, + JS_R_F64, + JS_R_BOOL, + JS_R_STR, + JS_R_BYTES, + JS_R_HANDLE +} js_kind; + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + js_kind kind; + js_error* err; + union { + int64_t i; + uint64_t u; + double f; + bool b; + const void* p; + } v; + size_t len; +} js_async; + +JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { + (void)cb; + (void)context; + js_async* a = (js_async*)data; + bool failed = a->err != NULL && a->err->code != 0; + if (env != NULL && failed) { + napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, + a->err->payload_ptr, a->err->payload_len)); + } else if (env != NULL) { + napi_value v = NULL; + switch (a->kind) { + case JS_R_VOID: v = js_undefined(env); break; + case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; + case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; + case JS_R_I64: v = js_new_i64(env, a->v.i); break; + case JS_R_U64: v = js_new_u64(env, a->v.u); break; + case JS_R_F64: v = js_new_f64(env, a->v.f); break; + case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; + case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; + } + napi_resolve_deferred(env, a->deferred, v); + } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { + /* The environment is shutting down: just release the result. */ + docs_everywhere_free_bytes((uint8_t*)a->v.p, a->len); + } + docs_everywhere_error_free(a->err); + napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); + free(a); +} + +/* Create the promise and its settlement queue for one async call. */ +JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { + js_async* a = (js_async*)calloc(1, sizeof *a); + a->kind = kind; + napi_create_promise(env, &a->deferred, promise); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, + js_async_settle, &a->tsfn); + return a; +} + +/* Called by each completion once it has recorded its result. */ +JS_HELPER void js_async_done(js_async* a, js_error* err) { + a->err = err; + napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); +} + +/* --------------------------------------------------------------------------- + * Callback interfaces. One registration per implementation passed to the + * producer: a reference to the adapter object, the JavaScript thread it + * belongs to, and a thread-safe function that runs calls made from any other + * thread on that thread while the caller waits. + * ------------------------------------------------------------------------- */ + +typedef struct { + napi_env env; + napi_ref ref; + napi_threadsafe_function tsfn; + uv_thread_t js_thread; +} js_cb; + +/* Every callback frame starts with this header so the dispatcher can reach + * `out_err` without knowing the method. */ +typedef struct { + js_error* out_err; +} js_cb_frame; + +/* A call queued from another thread: `method` is the vtable index, or -1 to + * release the registration. */ +typedef struct { + js_cb* cb; + int method; + void* frame; + uv_mutex_t mu; + uv_cond_t cv; + bool done; +} js_cb_req; + +JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { + uv_thread_t self = uv_thread_self(); + return uv_thread_equal(&self, &cb->js_thread) != 0; +} + +JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, + napi_threadsafe_function_call_js dispatch) { + js_cb* cb = (js_cb*)calloc(1, sizeof *cb); + cb->env = env; + cb->js_thread = uv_thread_self(); + napi_create_reference(env, adapter, 1, &cb->ref); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, + &cb->tsfn); + /* A live implementation must not keep the process alive by itself. */ + napi_unref_threadsafe_function(env, cb->tsfn); + return cb; +} + +JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { + if (env != NULL) napi_delete_reference(env, cb->ref); + napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); + free(cb); +} + +/* A callback-interface argument: the adapter object `index.js` built. */ +JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, + napi_threadsafe_function_call_js dispatch, js_cb** out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); + *out = js_cb_register(env, v, name, dispatch); + return true; +} + +/* The vtable's `free`: the producer is done with the implementation. The + * reference can only be deleted on its own thread. */ +JS_HELPER void js_cb_free(void* ctx) { + js_cb* cb = (js_cb*)ctx; + if (cb == NULL) return; + if (js_cb_on_js_thread(cb)) { + js_cb_release(cb->env, cb); + return; + } + js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); + req->cb = cb; + req->method = -1; + if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); +} + +/* Run `req` on the JavaScript thread and wait for it. */ +JS_HELPER void js_cb_hop(js_cb_req* req) { + uv_mutex_init(&req->mu); + uv_cond_init(&req->cv); + req->done = false; + if (napi_call_threadsafe_function(req->cb->tsfn, req, napi_tsfn_blocking) == napi_ok) { + uv_mutex_lock(&req->mu); + while (!req->done) uv_cond_wait(&req->cv, &req->mu); + uv_mutex_unlock(&req->mu); + } else { + docs_everywhere_error_set(((js_cb_frame*)req->frame)->out_err, -4, + "the callback implementation is no longer reachable"); + } + uv_cond_destroy(&req->cv); + uv_mutex_destroy(&req->mu); +} + +JS_HELPER void js_cb_finish(js_cb_req* req) { + uv_mutex_lock(&req->mu); + req->done = true; + uv_cond_signal(&req->cv); + uv_mutex_unlock(&req->mu); +} + +/* Report the pending JavaScript exception (or `fallback`) through `out_err` + * as a foreign failure (code -4); nothing unwinds through the C frame. */ +JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { + char msg[512]; + msg[0] = 0; + bool pending = false; + napi_is_exception_pending(env, &pending); + if (pending) { + napi_value exc, text, str; + napi_get_and_clear_last_exception(env, &exc); + napi_valuetype t; + napi_typeof(env, exc, &t); + text = exc; + if (t == napi_object) napi_get_named_property(env, exc, "message", &text); + if (napi_coerce_to_string(env, text, &str) == napi_ok) { + napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); + } + napi_is_exception_pending(env, &pending); + if (pending) napi_get_and_clear_last_exception(env, &exc); + } + docs_everywhere_error_set(out_err, -4, msg[0] != 0 ? msg : fallback); +} + +/* Look up and call `method` on the adapter; false when it threw. */ +JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, + const napi_value* argv, napi_value* result) { + napi_value adapter, fn; + napi_get_reference_value(env, cb->ref, &adapter); + napi_get_named_property(env, adapter, method, &fn); + return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; +} + +/* --------------------------------------------------------------------------- + * The runtime symbols every library exports. + * ------------------------------------------------------------------------- */ + +static napi_value js_abi_version(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u32(env, docs_everywhere_abi_version()); +} + +static napi_value js_debug_live(napi_env env, napi_callback_info info) { + JS_ARGS(1); + int32_t kind = 0; + if (!js_arg_i32(env, argv[0], &kind)) return NULL; + return js_new_u64(env, docs_everywhere_debug_live(kind)); +} + +static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { + (void)info; + return js_new_handle(env, docs_everywhere_cancel_token_create()); +} + +static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + docs_everywhere_cancel_token_cancel((docs_everywhere_cancel_token*)token); + return js_undefined(env); +} + +static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + docs_everywhere_cancel_token_destroy((docs_everywhere_cancel_token*)token); + return js_undefined(env); +} + +#define JS_RUNTIME_EXPORTS \ + {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ + {"docs_everywhere_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ + {"docs_everywhere_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ + {"docs_everywhere_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"docs_everywhere_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"docs_everywhere_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ + napi_default, NULL} + +static napi_value nx_docs_everywhere_docs_checksum(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u64(env, docs_everywhere_docs_checksum()); +} + +typedef struct { + js_cb_frame hdr; + int64_t p0; + int32_t p1; +} frame_docs_everywhere_docs_SavedListener_on_saved; + +static void invoke_docs_everywhere_docs_SavedListener_on_saved(napi_env env, js_cb* cb, frame_docs_everywhere_docs_SavedListener_on_saved* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[3]; + argv[0] = js_new_i64(env, f->p0); + argv[1] = js_new_i32(env, (int32_t)f->p1); + napi_value result; + if (!js_cb_call(env, cb, "on_saved", 2, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "SavedListener.on_saved failed"); + } + napi_close_handle_scope(env, scope); +} + +static void tramp_docs_everywhere_docs_SavedListener_on_saved(void* ctx, int64_t p0, int32_t p1, docs_everywhere_error* out_err) { + frame_docs_everywhere_docs_SavedListener_on_saved f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + f.p0 = p0; + f.p1 = p1; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_docs_everywhere_docs_SavedListener_on_saved(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 0; + req.frame = &f; + js_cb_hop(&req); + } +} + +static void dispatch_docs_everywhere_docs_SavedListener(napi_env env, napi_value fn, void* context, void* data) { + (void)fn; + (void)context; + js_cb_req* req = (js_cb_req*)data; + if (req->method < 0) { + js_cb_release(env, req->cb); + free(req); + return; + } + if (env == NULL) { + docs_everywhere_error_set(((js_cb_frame*)req->frame)->out_err, -4, "the JavaScript environment is shutting down"); + } else { + switch (req->method) { + case 0: invoke_docs_everywhere_docs_SavedListener_on_saved(env, req->cb, (frame_docs_everywhere_docs_SavedListener_on_saved*)req->frame); break; + default: break; + } + } + js_cb_finish(req); +} + +static const docs_everywhere_docs_SavedListener_vtable vtable_docs_everywhere_docs_SavedListener = {tramp_docs_everywhere_docs_SavedListener_on_saved, js_cb_free}; + +static napi_value nx_docs_everywhere_docs_Notebook_clone(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + return js_new_handle(env, docs_everywhere_docs_Notebook_clone((const docs_everywhere_docs_Notebook*)h)); +} + +static napi_value nx_docs_everywhere_docs_Notebook_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + docs_everywhere_docs_Notebook_destroy((docs_everywhere_docs_Notebook*)h); + return js_undefined(env); +} + +static napi_value nx_docs_everywhere_docs_create_document(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + const uint8_t* a1 = NULL; + size_t a1_len = 0; + if (!js_arg_str(env, argv[0], &a0)) goto done; + if (!js_arg_bytes(env, argv[1], &a1, &a1_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = docs_everywhere_docs_create_document(JS_STR_PTR(a0), a0.len, a1, a1_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static napi_value nx_docs_everywhere_docs_get_document(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int64_t a0 = 0; + if (!js_arg_i64(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = docs_everywhere_docs_get_document(a0, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static void done_docs_everywhere_docs_publish_async(void* context, docs_everywhere_error* err, bool result) { + js_async* a = (js_async*)context; + a->v.b = result; + js_async_done(a, err); +} + +static napi_value nx_docs_everywhere_docs_publish_async(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int64_t a0 = 0; + if (!js_arg_i64(env, argv[0], &a0)) goto done; + { + js_async* a = js_async_begin(env, JS_R_BOOL, "docs_everywhere_docs_publish_async", &ret); + docs_everywhere_docs_publish_async(a0, done_docs_everywhere_docs_publish_async, a); + } + goto done; +done: + return ret; +} + +static napi_value nx_docs_everywhere_docs_watch(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + js_cb* a0 = NULL; + if (!js_arg_cb(env, argv[0], "docs_everywhere_docs_SavedListener", dispatch_docs_everywhere_docs_SavedListener, &a0)) goto done; + { + js_error err = {0}; + docs_everywhere_docs_watch((void*)a0, &vtable_docs_everywhere_docs_SavedListener, &err); + a0 = NULL; + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_undefined(env); + } + goto done; +done: + if (a0 != NULL) js_cb_release(env, a0); + return ret; +} + +static napi_value nx_docs_everywhere_docs_Notebook_new(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + docs_everywhere_docs_Notebook* r = docs_everywhere_docs_Notebook_new(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_docs_everywhere_docs_Notebook_insert(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + void* self_h = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + if (!js_arg_bytes(env, argv[1], &a0, &a0_len)) goto done; + { + js_error err = {0}; + int32_t r = docs_everywhere_docs_Notebook_insert((const docs_everywhere_docs_Notebook*)self_h, a0, a0_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_docs_everywhere_docs_Notebook_find(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + void* self_h = NULL; + int64_t a0 = 0; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + if (!js_arg_i64(env, argv[1], &a0)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = docs_everywhere_docs_Notebook_find((const docs_everywhere_docs_Notebook*)self_h, a0, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_docs_everywhere_docs_Notebook_size(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + void* self_h = NULL; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + { + js_error err = {0}; + int32_t r = docs_everywhere_docs_Notebook_size((const docs_everywhere_docs_Notebook*)self_h, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_docs_everywhere_docs_Notebook_max_documents(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + int32_t r = docs_everywhere_docs_Notebook_max_documents(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + return ret; +} + +NAPI_MODULE_INIT() { + js_env* e = (js_env*)calloc(1, sizeof *e); + napi_set_instance_data(env, e, js_env_free, NULL); + napi_property_descriptor props[] = { + JS_RUNTIME_EXPORTS, + {"docs_everywhere_docs_checksum", NULL, nx_docs_everywhere_docs_checksum, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_Notebook_clone", NULL, nx_docs_everywhere_docs_Notebook_clone, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_Notebook_destroy", NULL, nx_docs_everywhere_docs_Notebook_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_create_document", NULL, nx_docs_everywhere_docs_create_document, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_get_document", NULL, nx_docs_everywhere_docs_get_document, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_publish_async", NULL, nx_docs_everywhere_docs_publish_async, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_watch", NULL, nx_docs_everywhere_docs_watch, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_Notebook_new", NULL, nx_docs_everywhere_docs_Notebook_new, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_Notebook_insert", NULL, nx_docs_everywhere_docs_Notebook_insert, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_Notebook_find", NULL, nx_docs_everywhere_docs_Notebook_find, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_Notebook_size", NULL, nx_docs_everywhere_docs_Notebook_size, NULL, NULL, NULL, napi_default, NULL}, + {"docs_everywhere_docs_Notebook_max_documents", NULL, nx_docs_everywhere_docs_Notebook_max_documents, NULL, NULL, NULL, napi_default, NULL}, + }; + napi_define_properties(env, exports, sizeof props / sizeof props[0], props); + return exports; +} + +// END docs_everywhere_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_d_ts.snap new file mode 100644 index 00000000..0be2d718 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_d_ts.snap @@ -0,0 +1,153 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class DocsEverywhereError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends DocsEverywhereError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof DocsEverywhereError; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +export declare namespace docs { + /** The errors of the `docs` error domain. */ + export class DocsErrorsError extends $Error { + constructor(code: number, message?: string); + } + /** Returned when no document matches the requested id */ + export class NotFoundError extends docs.DocsErrorsError { + static readonly CODE: 1; + constructor(message?: string); + } + /** Raised when attempting to move into a forbidden state */ + export class InvalidStatusError extends docs.DocsErrorsError { + static readonly CODE: 2; + constructor(message?: string); + } + /** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ + export enum Status { + /** Document is being edited */ + Draft = 0, + /** Document is publicly visible */ + Published = 1, + /** Document is hidden from listings */ + Archived = 2, + } + /** + * Represents a single document tracked by the system. + * + * Documents are persisted to disk and exposed via the public API. + */ + export interface Document { + /** Stable opaque identifier */ + id: bigint; + /** Human-readable title shown in lists */ + title: string; + /** Current lifecycle status */ + status: docs.Status; + /** Free-form tags for grouping */ + tags: string[]; + } + /** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + */ + export interface SavedListener { + /** + * Fires after a document has been persisted successfully + * @param docId Identifier of the saved document + * @param revision Monotonically increasing revision + */ + onSaved(docId: bigint, revision: number): void; + } + /** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ + export class Notebook { + /** Create an empty notebook */ + constructor(); + /** + * Add a document, returning its position in the notebook + * @param document The document to add + */ + insert(document: docs.Document): number; + /** + * Look up a document by identifier, failing when none matches + * @param id Identifier of the document to find + * @throws {docs.DocsErrorsError} + */ + find(id: bigint): docs.Document; + /** The number of documents currently held */ + size(): number; + /** The largest number of documents one notebook will hold */ + static maxDocuments(): number; + /** + * Release this wrapper's reference to the native object. Safe to call + * more than once; a wrapper that is never closed is released when it is + * garbage collected. Using the wrapper after `close()` throws. + */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; + } + /** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + * @param title Human-readable title for the new document + * @param tags Initial set of tags (may be empty) + */ + export function createDocument(title: string, tags: string[]): docs.Document; + /** + * Look up a document by identifier + * @param id Identifier returned from create_document + */ + export function getDocument(id: bigint): docs.Document | null; + /** + * Publish a document in the background + * @param id Identifier of the document to publish + */ + export function publishAsync(id: bigint): Promise; + /** + * Register a listener for saved notifications + * @param listener Receiver of saved notifications + */ + export function watch(listener: docs.SavedListener): void; +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_js.snap index 339c9956..755a173d 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__index_js.snap @@ -1,240 +1,92 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.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'); - } -} +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + DocsEverywhereError as $Error, +} from './runtime.js'; +export { DocsEverywhereError, CancelledError } from './runtime.js'; -// 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]; -} +import { createRequire } from 'node:module'; -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); - }); -} - -// Borrow a live object wrapper's native handle for one call. The wrapper -// keeps its own reference; the producer clones if it retains the object. -function __borrow(o, cls) { - if (!(o instanceof cls)) { - throw new WeaveFFIError(-3, 'expected an instance of ' + cls.name); - } - if (!o._handle) { - throw new WeaveFFIError(-3, cls.name + ' used after close()'); - } - return o._handle; -} - -// --- 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'); +// The native entry points: the N-API addon `npm install` builds from +// docs_everywhere_node.c (with node-gyp), or a prebuilt copy next to this file. +function $loadAddon(name) { + const require = createRequire(import.meta.url); + const tried = []; + for (const dir of ['./build/Release/', './build/Debug/', './']) { try { - return __utf8.decode(this._buf.subarray(at, at + n)); + return require(dir + name); } catch (e) { - throw __bufferError('string is not valid UTF-8'); + if (e.code !== 'MODULE_NOT_FOUND') throw e; + tried.push(dir + name); } } - 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; + throw new Error( + `the native addon ${name} was not found (tried ${tried.join(', ')}); ` + + 'build it with `npm install` or `node-gyp rebuild` in this package', + ); } -function __packDocument(w, v) { - w.i64(v.id); - w.str(v.title); - w.i32(v.status); - __wList(w, v.tags, (w, v) => w.str(v)); -} -function __unpackDocument(r) { - return { - id: r.i64(), - title: r.str(), - status: r.i32(), - tags: __rList(r, (r) => r.str()), - }; -} +const $raw = $loadAddon('docs_everywhere_node.node'); +$raw.$setup($Fault); -class DocsErrorsError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'DocsErrorsError'; - } +// Object tokens in value buffers are already bigint handles here. +const $token = (t) => t; + +$verify($raw, 'docs_everywhere', 'docs_everywhere', 3, [['docs', 0x7950eec1e9cfc819n]]); + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.docs_everywhere_debug_live(kind); } -wv.DocsErrorsError = DocsErrorsError; -class NotFoundError extends DocsErrorsError { - constructor(message) { - super(1, message || 'Document not found'); - this.name = 'NotFoundError'; + +const docs$DocsErrorsError = class DocsErrorsError extends $Error {}; +const docs$NotFoundError = class NotFoundError extends docs$DocsErrorsError { + static CODE = 1; + constructor(message = 'Document not found') { + super(1, message); } -} -NotFoundError.CODE = 1; -wv.NotFoundError = NotFoundError; -class InvalidStatusError extends DocsErrorsError { - constructor(message) { - super(2, message || 'Invalid status transition'); - this.name = 'InvalidStatusError'; +}; +const docs$InvalidStatusError = class InvalidStatusError extends docs$DocsErrorsError { + static CODE = 2; + constructor(message = 'Invalid status transition') { + super(2, message); } -} -InvalidStatusError.CODE = 2; -wv.InvalidStatusError = InvalidStatusError; -const __docsErrorCodes = Object.freeze({ 1: NotFoundError, 2: InvalidStatusError }); -function __docsErrorFrom(code, message, payload) { - const _cls = __docsErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; +}; +const $codes$docs$DocsErrors = new Map([[1, docs$NotFoundError], [2, docs$InvalidStatusError]]); +function $from$docs$DocsErrors(e) { + return $domain(e, $codes$docs$DocsErrors); } -wv.Status = Object.freeze({ +const docs$Status = Object.freeze({ Draft: 0, Published: 1, Archived: 2, @@ -242,75 +94,127 @@ wv.Status = Object.freeze({ 1: 'Published', 2: 'Archived', }); -function __adaptSavedListener(impl) { - if (impl === null || impl === undefined) { - throw new WeaveFFIError(-3, 'SavedListener implementation must be an object'); - } + +function $w$docs$Document(w, v) { + w.writeI64(v.id); + w.writeString(v.title); + w.writeI32(v.status); + w.writeList(v.tags, $W.String); +} +function $r$docs$Document(r) { + return { + id: r.readI64(), + title: r.readString(), + status: r.readI32(), + tags: r.readList($R.String), + }; +} + +function $adapt$docs$SavedListener(impl) { + $impl(impl, 'SavedListener'); return { - on_saved(docId, revision) { - return impl.onSaved(docId, revision); + on_saved($a0, $a1) { + impl.onSaved($a0, $a1); }, }; } -class Notebook { +const docs$Notebook = class Notebook extends $Object { constructor() { - this._handle = __invoke(addon.Notebook_new, [], __generic); - Notebook._cleanup.register(this, this._handle, this); + super(); + try { + $own(this, $raw.docs_everywhere_docs_Notebook_new()); + } catch ($e) { + throw $fault($e); + } + } + static $destroy(h) { + $raw.docs_everywhere_docs_Notebook_destroy(h); + } + static $clone(h) { + return $raw.docs_everywhere_docs_Notebook_clone(h); } insert(document) { - return __invoke(addon.Notebook_insert, [__borrow(this, Notebook), __encode(__packDocument, document)], __generic); + const $self = $lend(this, docs$Notebook); + try { + return $raw.docs_everywhere_docs_Notebook_insert($self, $encode(document, $w$docs$Document)); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } } find(id) { - const _r = __invoke(addon.Notebook_find, [__borrow(this, Notebook), id], __docsErrorFrom); - return __decode(__unpackDocument, _r); + const $self = $lend(this, docs$Notebook); + try { + return $decode($raw.docs_everywhere_docs_Notebook_find($self, id), $r$docs$Document); + } catch ($e) { + throw $from$docs$DocsErrors($e); + } finally { + $unlend(this); + } } size() { - return __invoke(addon.Notebook_size, [__borrow(this, Notebook)], __generic); + const $self = $lend(this, docs$Notebook); + try { + return $raw.docs_everywhere_docs_Notebook_size($self); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } } static maxDocuments() { - return __invoke(addon.Notebook_max_documents, [], __generic); - } - close() { - if (this._handle) { - Notebook._cleanup.unregister(this); - addon.Notebook__destroy(this._handle); - this._handle = 0n; + try { + return $raw.docs_everywhere_docs_Notebook_max_documents(); + } catch ($e) { + throw $fault($e); } } - _cloneHandle() { - return addon.Notebook__clone(__borrow(this, Notebook)); - } -} -Notebook._adopt = function (handle) { - const _o = Object.create(Notebook.prototype); - _o._handle = handle; - Notebook._cleanup.register(_o, handle, _o); - return _o; }; -Notebook._cleanup = new FinalizationRegistry((handle) => { - if (handle) { addon.Notebook__destroy(handle); } -}); -if (typeof Symbol.dispose === 'symbol') { - Notebook.prototype[Symbol.dispose] = Notebook.prototype.close; -} -wv.Notebook = Notebook; -wv.createDocument = function (title, tags) { - const _r = __invoke(addon.createDocument, [title, __encode((w, v) => __wList(w, v, (w, v) => w.str(v)), tags)], __generic); - return __decode(__unpackDocument, _r); +const docs$createDocument = function createDocument(title, tags) { + try { + return $decode($raw.docs_everywhere_docs_create_document(title, $encode(tags, (w, v) => w.writeList(v, $W.String))), $r$docs$Document); + } catch ($e) { + throw $fault($e); + } }; -wv.getDocument = function (id) { - const _r = __invoke(addon.getDocument, [id], __generic); - return __decode((r) => __rOpt(r, __unpackDocument), _r); + +const docs$getDocument = function getDocument(id) { + try { + return $decode($raw.docs_everywhere_docs_get_document(id), (r) => r.readOpt($r$docs$Document)); + } catch ($e) { + throw $fault($e); + } }; -wv.publishAsync = function (id) { - return __invokeAsync(addon.publishAsync, [id], __generic); + +const docs$publishAsync = async function publishAsync(id) { + try { + return await $raw.docs_everywhere_docs_publish_async(id); + } catch ($e) { + throw $fault($e); + } }; -wv.watch = function (listener) { - return __invoke(addon.watch, [__adaptSavedListener(listener)], __generic); + +const docs$watch = function watch(listener) { + try { + $raw.docs_everywhere_docs_watch($adapt$docs$SavedListener(listener)); + } catch ($e) { + throw $fault($e); + } }; -module.exports = wv; +export const docs = Object.freeze({ + DocsErrorsError: docs$DocsErrorsError, + NotFoundError: docs$NotFoundError, + InvalidStatusError: docs$InvalidStatusError, + Status: docs$Status, + Notebook: docs$Notebook, + createDocument: docs$createDocument, + getDocument: docs$getDocument, + publishAsync: docs$publishAsync, + watch: docs$watch, +}); // END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__package_json.snap index 6fd366d2..e88b4546 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__package_json.snap @@ -2,16 +2,34 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "docs_everywhere", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for docs_everywhere", + "type": "module", "main": "index.js", - "types": "types.d.ts", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "binding.gyp", + "docs_everywhere_node.c", + "docs_everywhere.h" + ], "gypfile": true, "scripts": { "install": "node-gyp rebuild" + }, + "engines": { + "node": ">=18" } } diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__runtime_js.snap new file mode 100644 index 00000000..75fa389b --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class DocsEverywhereError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends DocsEverywhereError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new DocsEverywhereError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new DocsEverywhereError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new DocsEverywhereError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new DocsEverywhereError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__types_d_ts.snap deleted file mode 100644 index 4206845a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__types_d_ts.snap +++ /dev/null @@ -1,151 +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 runtime traps (code -2 a - * producer panic, -3 a marshalling failure, -4 a callback interface - * implementation that threw); throwing functions surface a module - * domain subclass for their declared codes. - */ -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 `docs` module's throwing functions. */ -export class DocsErrorsError extends WeaveFFIError { - constructor(code: number, message: string); -} -/** Returned when no document matches the requested id */ -export class NotFoundError extends DocsErrorsError { - static readonly CODE: 1; - constructor(message?: string); -} -/** Raised when attempting to move into a forbidden state */ -export class InvalidStatusError extends DocsErrorsError { - static readonly CODE: 2; - constructor(message?: string); -} -/** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ -export interface Document { - /** Stable opaque identifier */ - id: bigint; - /** Human-readable title shown in lists */ - title: string; - /** Current lifecycle status */ - status: Status; - /** Free-form tags for grouping */ - tags: string[]; -} -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -export enum Status { - /** Document is being edited */ - Draft = 0, - /** Document is publicly visible */ - Published = 1, - /** Document is hidden from listings */ - Archived = 2, -} -// module docs -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - */ -export interface SavedListener { - /** - * Fires after a document has been persisted successfully - * @param docId Identifier of the saved document - * @param revision Monotonically increasing revision - */ - onSaved(docId: bigint, revision: number): void; -} -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - */ -export class Notebook { - /** - * Create an empty notebook - * Maps to C function: weaveffi_docs_Notebook_new - */ - constructor(); - /** - * Add a document, returning its position in the notebook - * @param document The document to add - * Maps to C function: weaveffi_docs_Notebook_insert - */ - insert(document: Document): number; - /** - * Look up a document by identifier, failing when none matches - * @param id Identifier of the document to find - * Maps to C function: weaveffi_docs_Notebook_find - * @throws {DocsErrorsError} - */ - find(id: bigint): Document; - /** - * The number of documents currently held - * Maps to C function: weaveffi_docs_Notebook_size - */ - size(): number; - /** - * The largest number of documents one notebook will hold - * Maps to C function: weaveffi_docs_Notebook_max_documents - */ - static maxDocuments(): number; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; -} -/** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - * @param title Human-readable title for the new document - * @param tags Initial set of tags (may be empty) - * Maps to C function: weaveffi_docs_create_document - */ -export function createDocument(title: string, tags: string[]): Document -/** - * Look up a document by identifier - * @param id Identifier returned from create_document - * Maps to C function: weaveffi_docs_get_document - */ -export function getDocument(id: bigint): Document | null -/** - * Publish a document in the background - * @param id Identifier of the document to publish - * Maps to C function: weaveffi_docs_publish_async - */ -export function publishAsync(id: bigint): Promise -/** - * Register a listener for saved notifications - * @param listener Receiver of saved notifications - * Maps to C function: weaveffi_docs_watch - */ -export function watch(listener: SavedListener): void - -// END types.d.ts 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 deleted file mode 100644 index a320ed5e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap +++ /dev/null @@ -1,626 +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 NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include "weaveffi.h" -#include -#include -#include -#include -#include -#include - -static inline napi_status weaveffi_napi_get_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_int64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - return napi_get_value_int64(env, v, out); - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_get_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - double d = 0; - napi_status s = napi_get_value_double(env, v, &d); - if (s == napi_ok && (d < 0 || d != d)) { - napi_throw_range_error(env, NULL, "negative number for an unsigned 64-bit integer"); - return napi_generic_failure; - } - *out = (uint64_t)d; - return s; - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -// An object handle is the pointer value carried as a bigint. null and -// undefined read as NULL (the absent case of a nullable object). -static inline napi_status weaveffi_napi_get_handle(napi_env env, napi_value v, void** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return napi_ok; - } - if (t == napi_bigint) { - uint64_t raw = 0; - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, &raw, &lossless); - *out = (void*)(uintptr_t)raw; - return s; - } - napi_throw_type_error(env, NULL, "expected an object handle"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_make_handle(napi_env env, const void* p, napi_value* out) { - if (p == NULL) { - return napi_get_null(env, out); - } - return napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, out); -} - -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 uv_thread_t weaveffi_napi_js_thread; - -static inline bool weaveffi_napi_on_js_thread(void) { - uv_thread_t self = uv_thread_self(); - return uv_thread_equal(&self, &weaveffi_napi_js_thread) != 0; -} - -// One handle-table entry per registered callback-interface implementation; -// its address is the `ctx` the producer passes back to every method. -typedef struct { - napi_env env; - napi_ref ref; - napi_threadsafe_function tsfn; -} weaveffi_napi_cb_ctx; - -// Every method frame starts with this header so the dispatcher can reach -// `out_err` without knowing the method. -typedef struct { - weaveffi_error* out_err; -} weaveffi_napi_cb_frame_hdr; - -// A request hopped from a producer thread to the JS thread. `method` is the -// vtable index to run, or -1 to release the context. -typedef struct { - weaveffi_napi_cb_ctx* ctx; - int method; - void* frame; - uv_mutex_t mu; - uv_cond_t cv; - bool done; -} weaveffi_napi_cb_req; - -static weaveffi_napi_cb_ctx* weaveffi_napi_cb_register(napi_env env, napi_value target, const char* name, napi_threadsafe_function_call_js dispatch) { - weaveffi_napi_cb_ctx* ctx = (weaveffi_napi_cb_ctx*)calloc(1, sizeof(weaveffi_napi_cb_ctx)); - ctx->env = env; - napi_create_reference(env, target, 1, &ctx->ref); - napi_value resource_name; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, dispatch, &ctx->tsfn); - // A live implementation must not pin the event loop by itself. - napi_unref_threadsafe_function(env, ctx->tsfn); - return ctx; -} - -// Release one entry on the JS thread (env is NULL only during teardown, when -// the reference is already gone). -static void weaveffi_napi_cb_release(napi_env env, weaveffi_napi_cb_ctx* ctx) { - if (env != NULL) { - napi_delete_reference(env, ctx->ref); - } - napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); - free(ctx); -} - -// The vtable's `free` entry: the producer is done with `ctx`. -static void weaveffi_napi_cb_free(void* ctx_) { - weaveffi_napi_cb_ctx* ctx = (weaveffi_napi_cb_ctx*)ctx_; - if (ctx == NULL) { - return; - } - if (weaveffi_napi_on_js_thread()) { - weaveffi_napi_cb_release(ctx->env, ctx); - return; - } - weaveffi_napi_cb_req* req = (weaveffi_napi_cb_req*)calloc(1, sizeof(weaveffi_napi_cb_req)); - req->ctx = ctx; - req->method = -1; - if (napi_call_threadsafe_function(ctx->tsfn, req, napi_tsfn_nonblocking) != napi_ok) { - free(req); - } -} - -// Run `req` on the JS thread and block until it has completed. -static void weaveffi_napi_cb_hop(weaveffi_napi_cb_req* req) { - uv_mutex_init(&req->mu); - uv_cond_init(&req->cv); - req->done = false; - if (napi_call_threadsafe_function(req->ctx->tsfn, req, napi_tsfn_blocking) == napi_ok) { - uv_mutex_lock(&req->mu); - while (!req->done) { - uv_cond_wait(&req->cv, &req->mu); - } - uv_mutex_unlock(&req->mu); - } else { - weaveffi_error_set(((weaveffi_napi_cb_frame_hdr*)req->frame)->out_err, -4, "callback interface implementation is no longer reachable"); - } - uv_cond_destroy(&req->cv); - uv_mutex_destroy(&req->mu); -} - -static void weaveffi_napi_cb_finish(weaveffi_napi_cb_req* req) { - uv_mutex_lock(&req->mu); - req->done = true; - uv_cond_signal(&req->cv); - uv_mutex_unlock(&req->mu); -} - -// Report a JS exception raised by a callback implementation (or a return -// value of the wrong type) through `out_err` as a foreign error, clearing -// the pending exception so nothing unwinds through the C frame. -static void weaveffi_napi_cb_report(napi_env env, weaveffi_error* out_err, const char* fallback) { - char msg[512]; - msg[0] = 0; - bool pending = false; - napi_is_exception_pending(env, &pending); - if (pending) { - napi_value exc; - napi_get_and_clear_last_exception(env, &exc); - napi_valuetype t; - napi_typeof(env, exc, &t); - napi_value text = exc; - if (t == napi_object) { - napi_get_named_property(env, exc, "message", &text); - } - napi_value str; - if (napi_coerce_to_string(env, text, &str) == napi_ok) { - napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); - } - napi_is_exception_pending(env, &pending); - if (pending) { - napi_get_and_clear_last_exception(env, &exc); - } - } - weaveffi_error_set(out_err, -4, msg[0] ? msg : fallback); -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - int64_t doc_id; - int32_t revision; -} weaveffi_docs_SavedListener_on_saved_frame; - -static void weaveffi_docs_SavedListener_on_saved_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_docs_SavedListener_on_saved_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "on_saved", &fn); - napi_value argv[2]; - napi_create_bigint_int64(env, f->doc_id, &argv[0]); - napi_create_int32(env, f->revision, &argv[1]); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "SavedListener implementation has no on_saved method"); - } else if (napi_call_function(env, target, fn, 2, argv, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "SavedListener.on_saved threw"); - } - napi_close_handle_scope(env, scope); -} - -static void weaveffi_docs_SavedListener_on_saved_tramp(void* ctx, int64_t doc_id, int32_t revision, weaveffi_error* out_err) { - weaveffi_docs_SavedListener_on_saved_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - f.doc_id = doc_id; - f.revision = revision; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_docs_SavedListener_on_saved_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 0; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } -} - -static void weaveffi_docs_SavedListener_napi_dispatch(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_napi_cb_req* req = (weaveffi_napi_cb_req*)data; - if (req->method < 0) { - weaveffi_napi_cb_release(env, req->ctx); - free(req); - return; - } - if (env == NULL) { - weaveffi_error_set(((weaveffi_napi_cb_frame_hdr*)req->frame)->out_err, -4, "Node environment is shutting down"); - } else { - switch (req->method) { - case 0: weaveffi_docs_SavedListener_on_saved_invoke(env, req->ctx, (weaveffi_docs_SavedListener_on_saved_frame*)req->frame); break; - default: break; - } - } - weaveffi_napi_cb_finish(req); -} - -static const weaveffi_docs_SavedListener_vtable weaveffi_docs_SavedListener_napi_vtable = { weaveffi_docs_SavedListener_on_saved_tramp, weaveffi_napi_cb_free }; - -static napi_value Napi_weaveffi_docs_Notebook_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_docs_Notebook* result = weaveffi_docs_Notebook_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; - weaveffi_napi_make_handle(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_docs_Notebook_max_documents(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}; - int32_t result = weaveffi_docs_Notebook_max_documents(&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_docs_Notebook_insert(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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - void* document_raw = NULL; - size_t document_len = 0; - napi_get_buffer_info(env, args[1], &document_raw, &document_len); - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - int32_t result = weaveffi_docs_Notebook_insert((const weaveffi_docs_Notebook*)self_raw, (const uint8_t*)document_raw, document_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_int32(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_docs_Notebook_find(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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - int64_t id = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[1], &id); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_docs_Notebook_find((const weaveffi_docs_Notebook*)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_docs_Notebook_size(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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - int32_t result = weaveffi_docs_Notebook_size((const weaveffi_docs_Notebook*)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_docs_Notebook_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* self_raw = NULL; - if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) { - return NULL; - } - weaveffi_docs_Notebook* self = (weaveffi_docs_Notebook*)self_raw; - weaveffi_docs_Notebook_destroy(self); - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_docs_Notebook_clone(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* self_raw = NULL; - if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) { - return NULL; - } - weaveffi_docs_Notebook* self = (weaveffi_docs_Notebook*)self_raw; - weaveffi_docs_Notebook* cloned = weaveffi_docs_Notebook_clone(self); - napi_value ret; - weaveffi_napi_make_handle(env, cloned, &ret); - return ret; -} - -static napi_value Napi_weaveffi_docs_create_document(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 title_len = 0; - napi_get_value_string_utf8(env, args[0], NULL, 0, &title_len); - char* title = (char*)malloc(title_len + 1); - napi_get_value_string_utf8(env, args[0], title, title_len + 1, &title_len); - void* tags_raw = NULL; - size_t tags_len = 0; - napi_get_buffer_info(env, args[1], &tags_raw, &tags_len); - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_docs_create_document(title, (const uint8_t*)tags_raw, tags_len, &out_len, &err); - free(title); - 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_docs_get_document(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); - napi_status arg_status = napi_ok; - int64_t id = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[0], &id); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_docs_get_document(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; -} - -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; - bool result; -} weaveffi_docs_publish_async_napi_actx; - -static void weaveffi_docs_publish_async_napi_cb(void* context, weaveffi_error* err, bool result) { - weaveffi_docs_publish_async_napi_actx* ctx = (weaveffi_docs_publish_async_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_docs_publish_async_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_docs_publish_async_napi_actx* ctx = (weaveffi_docs_publish_async_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_boolean(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_docs_publish_async(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); - napi_status arg_status = napi_ok; - int64_t id = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[0], &id); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_docs_publish_async_napi_actx* ctx = (weaveffi_docs_publish_async_napi_actx*)calloc(1, sizeof(weaveffi_docs_publish_async_napi_actx)); - napi_value promise; - napi_create_promise(env, &ctx->deferred, &promise); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_docs_publish_async", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_docs_publish_async_napi_settle, &ctx->tsfn); - weaveffi_docs_publish_async_async(id, weaveffi_docs_publish_async_napi_cb, ctx); - return promise; -} - -static napi_value Napi_weaveffi_docs_watch(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); - napi_status arg_status = napi_ok; - napi_valuetype listener_type; - napi_typeof(env, args[0], &listener_type); - weaveffi_napi_cb_ctx* listener_ctx = NULL; - if (listener_type == napi_object || listener_type == napi_function) { - listener_ctx = weaveffi_napi_cb_register(env, args[0], "weaveffi_docs_SavedListener", weaveffi_docs_SavedListener_napi_dispatch); - } else { - napi_throw_type_error(env, NULL, "expected a SavedListener implementation"); - arg_status = napi_object_expected; - } - if (arg_status != napi_ok) { - if (listener_ctx != NULL) weaveffi_napi_cb_release(env, listener_ctx); - return NULL; - } - weaveffi_error err = {0}; - weaveffi_docs_watch((void*)listener_ctx, &weaveffi_docs_SavedListener_napi_vtable, &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 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; - } - } - weaveffi_napi_js_thread = uv_thread_self(); - 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 }, - { "Notebook_insert", NULL, Napi_weaveffi_docs_Notebook_insert, NULL, NULL, NULL, napi_default, NULL }, - { "Notebook_find", NULL, Napi_weaveffi_docs_Notebook_find, NULL, NULL, NULL, napi_default, NULL }, - { "Notebook_size", NULL, Napi_weaveffi_docs_Notebook_size, NULL, NULL, NULL, napi_default, NULL }, - { "Notebook__destroy", NULL, Napi_weaveffi_docs_Notebook_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "Notebook__clone", NULL, Napi_weaveffi_docs_Notebook_clone, NULL, NULL, NULL, napi_default, NULL }, - { "createDocument", NULL, Napi_weaveffi_docs_create_document, NULL, NULL, NULL, napi_default, NULL }, - { "getDocument", NULL, Napi_weaveffi_docs_get_document, NULL, NULL, NULL, napi_default, NULL }, - { "publishAsync", NULL, Napi_weaveffi_docs_publish_async, NULL, NULL, NULL, napi_default, NULL }, - { "watch", NULL, Napi_weaveffi_docs_watch, 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_edge_cases__README_md.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__README_md.snap new file mode 100644 index 00000000..288dfe80 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__README_md.snap @@ -0,0 +1,24 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + +# edge_cases + +Native bindings for edge_cases + +Node.js bindings for the `edge_cases` native library: an ES module over a small N-API addon (`edge_cases_node.c`) that `npm install` compiles with node-gyp. The addon links `edge_cases`; set `EDGE_CASES_LIBRARY` to the library file (or its directory) when installing, or pass `--edge_cases-library=` to npm. + +```sh +EDGE_CASES_LIBRARY=/path/to/libedge_cases.so npm install edge_cases +``` + +Each top-level module of the API is a namespace export: + +```js +import { edge_cases } from 'edge_cases'; +``` + + diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__binding_gyp.snap index 3997b5bb..ae606f1a 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__binding_gyp.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__binding_gyp.snap @@ -1,17 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o { + "variables": { + "native_dir%": " { const path = require('path'); const p = process.env.EDGE_CASES_LIBRARY || process.env.npm_config_edge_cases_library; if (!p) return path.resolve('.'); return require('fs').statSync(p).isFile() ? path.dirname(path.resolve(p)) : path.resolve(p); })()\")" + }, "targets": [ { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] + "target_name": "edge_cases_node", + "sources": [ + "edge_cases_node.c" + ], + "include_dirs": [ + "." + ], + "library_dirs": [ + "<(native_dir)" + ], + "libraries": [ + "-ledge_cases" + ], + "conditions": [ + ["OS=='mac'", { "xcode_settings": { "OTHER_LDFLAGS": ["-Wl,-rpath,@loader_path", "-Wl,-rpath,<(native_dir)"] } }], + ["OS=='linux'", { "ldflags": ["-Wl,-rpath,'$$ORIGIN'", "-Wl,-rpath,<(native_dir)"] }] + ] } ] } diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_h.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_h.snap new file mode 100644 index 00000000..94864086 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_h.snap @@ -0,0 +1,245 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef EDGE_CASES_H +#define EDGE_CASES_H + +#include +#include +#include + +#ifndef EDGE_CASES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef EDGE_CASES_BUILD +# define EDGE_CASES_API __declspec(dllexport) +# else +# define EDGE_CASES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define EDGE_CASES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define EDGE_CASES_API __attribute__((visibility("default"))) +# else +# define EDGE_CASES_API +# endif +#endif + +#ifndef EDGE_CASES_DEPRECATED +# if defined(_MSC_VER) +# define EDGE_CASES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define EDGE_CASES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define EDGE_CASES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports edge_cases_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define EDGE_CASES_ABI_VERSION 3u +EDGE_CASES_API uint32_t edge_cases_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by edge_cases_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct edge_cases_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} edge_cases_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +EDGE_CASES_API void edge_cases_error_set(edge_cases_error* err, int32_t code, const char* message); +EDGE_CASES_API void edge_cases_error_clear(edge_cases_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + edge_cases_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +EDGE_CASES_API void edge_cases_error_free(edge_cases_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +EDGE_CASES_API void edge_cases_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct edge_cases_cancel_token edge_cases_cancel_token; +EDGE_CASES_API edge_cases_cancel_token* edge_cases_cancel_token_create(void); +EDGE_CASES_API void edge_cases_cancel_token_cancel(edge_cases_cancel_token* token); +EDGE_CASES_API bool edge_cases_cancel_token_is_cancelled(const edge_cases_cancel_token* token); +EDGE_CASES_API void edge_cases_cancel_token_destroy(edge_cases_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +EDGE_CASES_API uint8_t* edge_cases_alloc(uint32_t size); +EDGE_CASES_API void edge_cases_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +EDGE_CASES_API uint64_t edge_cases_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define EDGE_CASES_EDGE_CASES_CHECKSUM 0x1567f441073bd4aaull +EDGE_CASES_API uint64_t edge_cases_edge_cases_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with edge_cases_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * edge_cases_buffer.h declares a C struct and codec for each. + */ + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +typedef enum { edge_cases_edge_cases_Legacy_None = 0, edge_cases_edge_cases_Legacy_Default = 5, edge_cases_edge_cases_Legacy_Class = 10 } edge_cases_edge_cases_Legacy; +typedef enum { edge_cases_edge_cases_Mode_Fast = 1, edge_cases_edge_cases_Mode_Safe = 2 } edge_cases_edge_cases_Mode; +/** Error codes reported by throwing functions in the `edge_cases` module tree. */ +typedef enum { edge_cases_edge_cases_EdgeErrors_Busy = 7 } edge_cases_edge_cases_EdgeErrors; +typedef struct edge_cases_edge_cases_Widget edge_cases_edge_cases_Widget; +typedef struct edge_cases_edge_cases_Vault edge_cases_edge_cases_Vault; +typedef struct edge_cases_edge_cases_WidgetStreamIterator edge_cases_edge_cases_WidgetStreamIterator; +typedef struct edge_cases_edge_cases_StringStreamIterator edge_cases_edge_cases_StringStreamIterator; +typedef struct edge_cases_edge_cases_ScalarStreamIterator edge_cases_edge_cases_ScalarStreamIterator; +typedef struct edge_cases_edge_cases_ModeStreamIterator edge_cases_edge_cases_ModeStreamIterator; +/** + * Callback interface carrying composite payloads and objects + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_BatchListener_vtable { + /** Composite payloads cross as value buffers */ + void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); + /** Objects cross as strong references the consumer must release */ + void (*on_widget)(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); + void (*on_tick)(void* ctx, edge_cases_error* out_err); + /** Scalar return travels back through the vtable */ + bool (*should_continue)(void* ctx, int32_t class_, edge_cases_error* out_err); + /** Enum return travels back as its integer discriminant */ + edge_cases_edge_cases_Mode (*pick_mode)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_BatchListener_vtable; +/** + * Callback interface whose name and methods are keywords + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_Keyword_vtable { + void (*delete_)(void* ctx, int32_t self, edge_cases_error* out_err); + int32_t (*import)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_Keyword_vtable; +typedef void (*edge_cases_edge_cases_async_widget_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result); +typedef void (*edge_cases_edge_cases_async_void_callback)(void* context, edge_cases_error* err); +typedef void (*edge_cases_edge_cases_async_bytes_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_struct_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_list_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_optional_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_enum_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result); +typedef void (*edge_cases_edge_cases_async_keywords_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_Widget_fetch_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: edge_cases +/** Interface with no constructor; instances come from make_widget */ +/** Void method that can fail */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_refresh(const edge_cases_edge_cases_Widget* self, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_rename(const edge_cases_edge_cases_Widget* self, const uint8_t* class_ptr, size_t class_len, const uint8_t* import_ptr, size_t import_len, size_t* out_len, edge_cases_error* out_err); +/** Async method with a struct result */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_fetch(const edge_cases_edge_cases_Widget* self, int64_t id, edge_cases_edge_cases_Widget_fetch_callback callback, void* context); +EDGE_CASES_DEPRECATED("Use rename instead") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_old_name(const edge_cases_edge_cases_Widget* self, size_t* out_len, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Widget_clone(const edge_cases_edge_cases_Widget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_destroy(edge_cases_edge_cases_Widget* self); + +/** Deprecated interface with a throwing constructor */ +/** Fails when the vault is busy */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_open(const uint8_t* path_ptr, size_t path_len, edge_cases_error* out_err); +/** Throwing static */ +EDGE_CASES_API bool edge_cases_edge_cases_Vault_probe(edge_cases_error* out_err); +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Vault_peek(const edge_cases_edge_cases_Vault* self, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_clone(const edge_cases_edge_cases_Vault* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Vault_destroy(edge_cases_edge_cases_Vault* self); + +/** No parameters and no result */ +EDGE_CASES_API void edge_cases_edge_cases_noop(edge_cases_error* out_err); +/** Function named after a keyword whose parameters are all keywords */ +EDGE_CASES_API int32_t edge_cases_edge_cases_select(int32_t class_, const uint8_t* from_ptr, size_t from_len, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, edge_cases_error* out_err); +/** Optional list of optional structs, the deepest legal nesting */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_optional_params(const uint8_t* text_ptr, size_t text_len, const uint8_t* count_ptr, size_t count_len, const uint8_t* outer_ptr, size_t outer_len, const uint8_t* tags_ptr, size_t tags_len, const uint8_t* blob_ptr, size_t blob_len, const uint8_t* mode_ptr, size_t mode_len, size_t* out_len, edge_cases_error* out_err); +/** Two callback interface parameters on one function */ +EDGE_CASES_API void edge_cases_edge_cases_register_batch(void* listener_ctx, const edge_cases_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const edge_cases_edge_cases_Keyword_vtable* keyword_vtable, edge_cases_error* out_err); +/** Objects in every buffered position */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, edge_cases_error* out_err); +/** Iterator of objects */ +EDGE_CASES_API edge_cases_edge_cases_WidgetStreamIterator* edge_cases_edge_cases_widget_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_WidgetStreamIterator_next(edge_cases_edge_cases_WidgetStreamIterator* iter, edge_cases_edge_cases_Widget** out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_WidgetStreamIterator_destroy(edge_cases_edge_cases_WidgetStreamIterator* iter); +/** Async returning an object */ +EDGE_CASES_API void edge_cases_edge_cases_async_widget(edge_cases_edge_cases_async_widget_callback callback, void* context); +/** Factory for an interface with no constructor */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_make_widget(const uint8_t* label_ptr, size_t label_len, edge_cases_error* out_err); +/** Optional interface return */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_find_widget(int64_t id, edge_cases_error* out_err); +/** Interface parameter, including an optional one */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_inspect_widget(const edge_cases_edge_cases_Widget* widget, const edge_cases_edge_cases_Widget* fallback, size_t* out_len, edge_cases_error* out_err); +/** Iterator of strings */ +EDGE_CASES_API edge_cases_edge_cases_StringStreamIterator* edge_cases_edge_cases_string_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_StringStreamIterator_next(edge_cases_edge_cases_StringStreamIterator* iter, const uint8_t** out_item, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_StringStreamIterator_destroy(edge_cases_edge_cases_StringStreamIterator* iter); +/** Iterator of scalars */ +EDGE_CASES_API edge_cases_edge_cases_ScalarStreamIterator* edge_cases_edge_cases_scalar_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ScalarStreamIterator_next(edge_cases_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ScalarStreamIterator_destroy(edge_cases_edge_cases_ScalarStreamIterator* iter); +/** Iterator of enums */ +EDGE_CASES_API edge_cases_edge_cases_ModeStreamIterator* edge_cases_edge_cases_mode_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ModeStreamIterator_next(edge_cases_edge_cases_ModeStreamIterator* iter, edge_cases_edge_cases_Mode* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ModeStreamIterator_destroy(edge_cases_edge_cases_ModeStreamIterator* iter); +/** Async with no result */ +EDGE_CASES_API void edge_cases_edge_cases_async_void(edge_cases_edge_cases_async_void_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_bytes(uint32_t size, edge_cases_edge_cases_async_bytes_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_struct(edge_cases_edge_cases_async_struct_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_list(edge_cases_cancel_token* cancel_token, edge_cases_edge_cases_async_list_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_optional(edge_cases_edge_cases_async_optional_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_enum(edge_cases_edge_cases_async_enum_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_keywords(const uint8_t* class_ptr, size_t class_len, int32_t import, edge_cases_edge_cases_async_keywords_callback callback, void* context); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_DEPRECATED("Use Mode-based APIs") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy value, size_t* out_len, edge_cases_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // EDGE_CASES_H + +// END edge_cases.h diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_node_c.snap new file mode 100644 index 00000000..62982286 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__edge_cases_node_c.snap @@ -0,0 +1,1756 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NAPI_VERSION +#define NAPI_VERSION 8 +#endif +#include +#include +#include +#include +#include +#include + +/* The addon wraps deprecated functions too; the JavaScript API carries the + * deprecation instead. */ +#define EDGE_CASES_DEPRECATED(msg) +#include "edge_cases.h" + +/* + * The N-API transport of the JavaScript bindings. Every C symbol of the + * library is exported to `index.js` under its own name and called with the + * raw convention the shared JavaScript layer documents: direct values as + * numbers, bigints, and booleans; strings as strings; bytes and value + * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint + * handles (null when absent); callback interfaces as adapter objects. A + * failure throws an instance of the runtime's `$Fault` class. + */ + +typedef edge_cases_error js_error; + +/* Helpers an API may not need. */ +#if defined(__GNUC__) || defined(__clang__) +#define JS_HELPER static inline __attribute__((unused)) +#else +#define JS_HELPER static inline +#endif + +/* Per-environment state (one per main thread or worker): the `$Fault` + * constructor `index.js` registers through `$setup`. */ +typedef struct { + napi_ref fault; +} js_env; + +static void js_env_free(napi_env env, void* data, void* hint) { + (void)hint; + js_env* e = (js_env*)data; + if (e->fault != NULL) napi_delete_reference(env, e->fault); + free(e); +} + +JS_HELPER napi_value js_undefined(napi_env env) { + napi_value v; + napi_get_undefined(env, &v); + return v; +} + +static napi_value js_setup(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e->fault != NULL) napi_delete_reference(env, e->fault); + napi_create_reference(env, argv[0], 1, &e->fault); + return js_undefined(env); +} + +/* --------------------------------------------------------------------------- + * Value conversions + * ------------------------------------------------------------------------- */ + +JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value ab, out; + void* data = NULL; + napi_create_arraybuffer(env, len, &data, &ab); + if (len > 0) memcpy(data, ptr, len); + napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); + return out; +} + +JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out; + napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); + return out; +} + +JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { + napi_value out; + napi_create_int32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { + napi_value out; + napi_create_uint32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { + napi_value out; + napi_create_bigint_int64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { + napi_value out; + napi_create_bigint_uint64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_f64(napi_env env, double v) { + napi_value out; + napi_create_double(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_bool(napi_env env, bool v) { + napi_value out; + napi_get_boolean(env, v, &out); + return out; +} + +/* An object, iterator, or cancel token handle: the pointer as a bigint, or + * null for NULL. */ +JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { + napi_value out; + if (p == NULL) { + napi_get_null(env, &out); + } else { + napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); + } + return out; +} + +/* Returned strings and buffers: convert, then release the producer's copy. */ +JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_str(env, ptr, len); + if (ptr != NULL) edge_cases_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_bytes(env, ptr, len); + if (ptr != NULL) edge_cases_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, + const uint8_t* payload, size_t payload_len) { + napi_value argv[3], ctor, out = NULL; + napi_create_int32(env, code, &argv[0]); + napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); + if (payload != NULL) { + argv[2] = js_new_bytes(env, payload, payload_len); + } else { + napi_get_null(env, &argv[2]); + } + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok || + ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { + napi_create_error(env, NULL, argv[1], &out); + napi_set_named_property(env, out, "code", argv[0]); + } + return out; +} + +/* Throw the error a call reported, then release it. */ +JS_HELPER napi_value js_throw(napi_env env, js_error* err) { + napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); + edge_cases_error_clear(err); + return NULL; +} + +JS_HELPER bool js_type_error(napi_env env, const char* expected) { + bool pending = false; + napi_is_exception_pending(env, &pending); + if (!pending) napi_throw_type_error(env, NULL, expected); + return false; +} + +JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { + return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { + return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int8_t)t; + return true; +} + +JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int16_t)t; + return true; +} + +JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint8_t)t; + return true; +} + +JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint16_t)t; + return true; +} + +/* 64-bit integers: a bigint that fits, or an integral number. */ +JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_int64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { + *out = (int64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_uint64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && + d == (double)(uint64_t)d) { + *out = (uint64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { + return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { + double d = 0; + if (!js_arg_f64(env, v, &d)) return false; + *out = (float)d; + return true; +} + +JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { + return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); +} + +/* A handle argument; null or undefined is NULL when `nullable`. */ +JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_null || t == napi_undefined) { + *out = NULL; + return nullable || js_type_error(env, "expected an object handle"); + } + uint64_t raw = 0; + bool lossless = false; + if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || + raw == 0) { + return js_type_error(env, "expected an object handle"); + } + *out = (void*)(uintptr_t)raw; + return true; +} + +/* A borrowed byte run: a Uint8Array (or Buffer). */ +JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { + bool is_typed = false; + napi_is_typedarray(env, v, &is_typed); + if (is_typed) { + napi_typedarray_type type; + void* data = NULL; + napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); + if (type == napi_uint8_array || type == napi_uint8_clamped_array) { + *ptr = (const uint8_t*)data; + return true; + } + } + return js_type_error(env, "expected a Uint8Array"); +} + +/* A string argument encoded as UTF-8: short strings live in the inline + * buffer, longer ones on the heap until js_str_free. */ +typedef struct { + char* ptr; + size_t len; + char inline_buf[256]; +} js_str; + +#define JS_STR_INIT {NULL, 0, {0}} + +JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { + size_t len = 0; + if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { + return js_type_error(env, "expected a string"); + } + s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); + napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); + return true; +} + +JS_HELPER void js_str_free(js_str* s) { + if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); + s->ptr = NULL; +} + +#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) + +/* Read up to `n` arguments; missing ones are undefined. */ +#define JS_ARGS(n) \ + size_t argc = (n); \ + napi_value argv[(n) + 1]; \ + napi_get_cb_info(env, info, &argc, argv, NULL, NULL) + +/* --------------------------------------------------------------------------- + * Async calls: the completion may run on any thread, so it only records the + * result and queues the settlement on the JavaScript thread through a + * thread-safe function (which also keeps the event loop alive meanwhile). + * ------------------------------------------------------------------------- */ + +typedef enum { + JS_R_VOID, + JS_R_I32, + JS_R_U32, + JS_R_I64, + JS_R_U64, + JS_R_F64, + JS_R_BOOL, + JS_R_STR, + JS_R_BYTES, + JS_R_HANDLE +} js_kind; + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + js_kind kind; + js_error* err; + union { + int64_t i; + uint64_t u; + double f; + bool b; + const void* p; + } v; + size_t len; +} js_async; + +JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { + (void)cb; + (void)context; + js_async* a = (js_async*)data; + bool failed = a->err != NULL && a->err->code != 0; + if (env != NULL && failed) { + napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, + a->err->payload_ptr, a->err->payload_len)); + } else if (env != NULL) { + napi_value v = NULL; + switch (a->kind) { + case JS_R_VOID: v = js_undefined(env); break; + case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; + case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; + case JS_R_I64: v = js_new_i64(env, a->v.i); break; + case JS_R_U64: v = js_new_u64(env, a->v.u); break; + case JS_R_F64: v = js_new_f64(env, a->v.f); break; + case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; + case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; + } + napi_resolve_deferred(env, a->deferred, v); + } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { + /* The environment is shutting down: just release the result. */ + edge_cases_free_bytes((uint8_t*)a->v.p, a->len); + } + edge_cases_error_free(a->err); + napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); + free(a); +} + +/* Create the promise and its settlement queue for one async call. */ +JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { + js_async* a = (js_async*)calloc(1, sizeof *a); + a->kind = kind; + napi_create_promise(env, &a->deferred, promise); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, + js_async_settle, &a->tsfn); + return a; +} + +/* Called by each completion once it has recorded its result. */ +JS_HELPER void js_async_done(js_async* a, js_error* err) { + a->err = err; + napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); +} + +/* --------------------------------------------------------------------------- + * Callback interfaces. One registration per implementation passed to the + * producer: a reference to the adapter object, the JavaScript thread it + * belongs to, and a thread-safe function that runs calls made from any other + * thread on that thread while the caller waits. + * ------------------------------------------------------------------------- */ + +typedef struct { + napi_env env; + napi_ref ref; + napi_threadsafe_function tsfn; + uv_thread_t js_thread; +} js_cb; + +/* Every callback frame starts with this header so the dispatcher can reach + * `out_err` without knowing the method. */ +typedef struct { + js_error* out_err; +} js_cb_frame; + +/* A call queued from another thread: `method` is the vtable index, or -1 to + * release the registration. */ +typedef struct { + js_cb* cb; + int method; + void* frame; + uv_mutex_t mu; + uv_cond_t cv; + bool done; +} js_cb_req; + +JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { + uv_thread_t self = uv_thread_self(); + return uv_thread_equal(&self, &cb->js_thread) != 0; +} + +JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, + napi_threadsafe_function_call_js dispatch) { + js_cb* cb = (js_cb*)calloc(1, sizeof *cb); + cb->env = env; + cb->js_thread = uv_thread_self(); + napi_create_reference(env, adapter, 1, &cb->ref); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, + &cb->tsfn); + /* A live implementation must not keep the process alive by itself. */ + napi_unref_threadsafe_function(env, cb->tsfn); + return cb; +} + +JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { + if (env != NULL) napi_delete_reference(env, cb->ref); + napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); + free(cb); +} + +/* A callback-interface argument: the adapter object `index.js` built. */ +JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, + napi_threadsafe_function_call_js dispatch, js_cb** out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); + *out = js_cb_register(env, v, name, dispatch); + return true; +} + +/* The vtable's `free`: the producer is done with the implementation. The + * reference can only be deleted on its own thread. */ +JS_HELPER void js_cb_free(void* ctx) { + js_cb* cb = (js_cb*)ctx; + if (cb == NULL) return; + if (js_cb_on_js_thread(cb)) { + js_cb_release(cb->env, cb); + return; + } + js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); + req->cb = cb; + req->method = -1; + if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); +} + +/* Run `req` on the JavaScript thread and wait for it. */ +JS_HELPER void js_cb_hop(js_cb_req* req) { + uv_mutex_init(&req->mu); + uv_cond_init(&req->cv); + req->done = false; + if (napi_call_threadsafe_function(req->cb->tsfn, req, napi_tsfn_blocking) == napi_ok) { + uv_mutex_lock(&req->mu); + while (!req->done) uv_cond_wait(&req->cv, &req->mu); + uv_mutex_unlock(&req->mu); + } else { + edge_cases_error_set(((js_cb_frame*)req->frame)->out_err, -4, + "the callback implementation is no longer reachable"); + } + uv_cond_destroy(&req->cv); + uv_mutex_destroy(&req->mu); +} + +JS_HELPER void js_cb_finish(js_cb_req* req) { + uv_mutex_lock(&req->mu); + req->done = true; + uv_cond_signal(&req->cv); + uv_mutex_unlock(&req->mu); +} + +/* Report the pending JavaScript exception (or `fallback`) through `out_err` + * as a foreign failure (code -4); nothing unwinds through the C frame. */ +JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { + char msg[512]; + msg[0] = 0; + bool pending = false; + napi_is_exception_pending(env, &pending); + if (pending) { + napi_value exc, text, str; + napi_get_and_clear_last_exception(env, &exc); + napi_valuetype t; + napi_typeof(env, exc, &t); + text = exc; + if (t == napi_object) napi_get_named_property(env, exc, "message", &text); + if (napi_coerce_to_string(env, text, &str) == napi_ok) { + napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); + } + napi_is_exception_pending(env, &pending); + if (pending) napi_get_and_clear_last_exception(env, &exc); + } + edge_cases_error_set(out_err, -4, msg[0] != 0 ? msg : fallback); +} + +/* Look up and call `method` on the adapter; false when it threw. */ +JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, + const napi_value* argv, napi_value* result) { + napi_value adapter, fn; + napi_get_reference_value(env, cb->ref, &adapter); + napi_get_named_property(env, adapter, method, &fn); + return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; +} + +/* --------------------------------------------------------------------------- + * The runtime symbols every library exports. + * ------------------------------------------------------------------------- */ + +static napi_value js_abi_version(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u32(env, edge_cases_abi_version()); +} + +static napi_value js_debug_live(napi_env env, napi_callback_info info) { + JS_ARGS(1); + int32_t kind = 0; + if (!js_arg_i32(env, argv[0], &kind)) return NULL; + return js_new_u64(env, edge_cases_debug_live(kind)); +} + +static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { + (void)info; + return js_new_handle(env, edge_cases_cancel_token_create()); +} + +static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + edge_cases_cancel_token_cancel((edge_cases_cancel_token*)token); + return js_undefined(env); +} + +static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + edge_cases_cancel_token_destroy((edge_cases_cancel_token*)token); + return js_undefined(env); +} + +#define JS_RUNTIME_EXPORTS \ + {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ + {"edge_cases_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ + {"edge_cases_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ + {"edge_cases_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"edge_cases_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"edge_cases_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ + napi_default, NULL} + +static napi_value nx_edge_cases_edge_cases_checksum(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u64(env, edge_cases_edge_cases_checksum()); +} + +typedef struct { + js_cb_frame hdr; + const uint8_t* p0; + size_t p1; + const uint8_t* p2; + size_t p3; + const uint8_t* p4; + size_t p5; + edge_cases_edge_cases_Mode p6; +} frame_edge_cases_edge_cases_BatchListener_on_batch; + +static void invoke_edge_cases_edge_cases_BatchListener_on_batch(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_on_batch* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[5]; + argv[0] = js_new_bytes(env, f->p0, f->p1); + argv[1] = js_new_bytes(env, f->p2, f->p3); + argv[2] = js_new_bytes(env, f->p4, f->p5); + argv[3] = js_new_i32(env, (int32_t)f->p6); + napi_value result; + if (!js_cb_call(env, cb, "on_batch", 4, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "BatchListener.on_batch failed"); + } + napi_close_handle_scope(env, scope); +} + +static void tramp_edge_cases_edge_cases_BatchListener_on_batch(void* ctx, const uint8_t* p0, size_t p1, const uint8_t* p2, size_t p3, const uint8_t* p4, size_t p5, edge_cases_edge_cases_Mode p6, edge_cases_error* out_err) { + frame_edge_cases_edge_cases_BatchListener_on_batch f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + f.p0 = p0; + f.p1 = p1; + f.p2 = p2; + f.p3 = p3; + f.p4 = p4; + f.p5 = p5; + f.p6 = p6; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_edge_cases_edge_cases_BatchListener_on_batch(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 0; + req.frame = &f; + js_cb_hop(&req); + } +} + +typedef struct { + js_cb_frame hdr; + edge_cases_edge_cases_Widget* p0; + edge_cases_edge_cases_Widget* p1; + const uint8_t* p2; + size_t p3; +} frame_edge_cases_edge_cases_BatchListener_on_widget; + +static void invoke_edge_cases_edge_cases_BatchListener_on_widget(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_on_widget* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[4]; + argv[0] = js_new_handle(env, f->p0); + argv[1] = js_new_handle(env, f->p1); + argv[2] = js_new_bytes(env, f->p2, f->p3); + napi_value result; + if (!js_cb_call(env, cb, "on_widget", 3, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "BatchListener.on_widget failed"); + } + napi_close_handle_scope(env, scope); +} + +static void tramp_edge_cases_edge_cases_BatchListener_on_widget(void* ctx, edge_cases_edge_cases_Widget* p0, edge_cases_edge_cases_Widget* p1, const uint8_t* p2, size_t p3, edge_cases_error* out_err) { + frame_edge_cases_edge_cases_BatchListener_on_widget f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + f.p0 = p0; + f.p1 = p1; + f.p2 = p2; + f.p3 = p3; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_edge_cases_edge_cases_BatchListener_on_widget(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 1; + req.frame = &f; + js_cb_hop(&req); + } +} + +typedef struct { + js_cb_frame hdr; +} frame_edge_cases_edge_cases_BatchListener_on_tick; + +static void invoke_edge_cases_edge_cases_BatchListener_on_tick(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_on_tick* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[1]; + napi_value result; + if (!js_cb_call(env, cb, "on_tick", 0, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "BatchListener.on_tick failed"); + } + napi_close_handle_scope(env, scope); +} + +static void tramp_edge_cases_edge_cases_BatchListener_on_tick(void* ctx, edge_cases_error* out_err) { + frame_edge_cases_edge_cases_BatchListener_on_tick f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_edge_cases_edge_cases_BatchListener_on_tick(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 2; + req.frame = &f; + js_cb_hop(&req); + } +} + +typedef struct { + js_cb_frame hdr; + int32_t p0; + bool result; +} frame_edge_cases_edge_cases_BatchListener_should_continue; + +static void invoke_edge_cases_edge_cases_BatchListener_should_continue(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_should_continue* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[2]; + argv[0] = js_new_i32(env, (int32_t)f->p0); + napi_value result; + if (!js_cb_call(env, cb, "should_continue", 1, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "BatchListener.should_continue failed"); + } else { + bool v = 0; + if (js_arg_bool(env, result, &v)) { + f->result = (bool)v; + } else { + js_cb_report(env, f->hdr.out_err, "BatchListener.should_continue returned a value of the wrong type"); + } + } + napi_close_handle_scope(env, scope); +} + +static bool tramp_edge_cases_edge_cases_BatchListener_should_continue(void* ctx, int32_t p0, edge_cases_error* out_err) { + frame_edge_cases_edge_cases_BatchListener_should_continue f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + f.p0 = p0; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_edge_cases_edge_cases_BatchListener_should_continue(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 3; + req.frame = &f; + js_cb_hop(&req); + } + return f.result; +} + +typedef struct { + js_cb_frame hdr; + edge_cases_edge_cases_Mode result; +} frame_edge_cases_edge_cases_BatchListener_pick_mode; + +static void invoke_edge_cases_edge_cases_BatchListener_pick_mode(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_BatchListener_pick_mode* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[1]; + napi_value result; + if (!js_cb_call(env, cb, "pick_mode", 0, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "BatchListener.pick_mode failed"); + } else { + int32_t v = 0; + if (js_arg_i32(env, result, &v)) { + f->result = (edge_cases_edge_cases_Mode)v; + } else { + js_cb_report(env, f->hdr.out_err, "BatchListener.pick_mode returned a value of the wrong type"); + } + } + napi_close_handle_scope(env, scope); +} + +static edge_cases_edge_cases_Mode tramp_edge_cases_edge_cases_BatchListener_pick_mode(void* ctx, edge_cases_error* out_err) { + frame_edge_cases_edge_cases_BatchListener_pick_mode f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_edge_cases_edge_cases_BatchListener_pick_mode(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 4; + req.frame = &f; + js_cb_hop(&req); + } + return f.result; +} + +static void dispatch_edge_cases_edge_cases_BatchListener(napi_env env, napi_value fn, void* context, void* data) { + (void)fn; + (void)context; + js_cb_req* req = (js_cb_req*)data; + if (req->method < 0) { + js_cb_release(env, req->cb); + free(req); + return; + } + if (env == NULL) { + edge_cases_error_set(((js_cb_frame*)req->frame)->out_err, -4, "the JavaScript environment is shutting down"); + } else { + switch (req->method) { + case 0: invoke_edge_cases_edge_cases_BatchListener_on_batch(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_on_batch*)req->frame); break; + case 1: invoke_edge_cases_edge_cases_BatchListener_on_widget(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_on_widget*)req->frame); break; + case 2: invoke_edge_cases_edge_cases_BatchListener_on_tick(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_on_tick*)req->frame); break; + case 3: invoke_edge_cases_edge_cases_BatchListener_should_continue(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_should_continue*)req->frame); break; + case 4: invoke_edge_cases_edge_cases_BatchListener_pick_mode(env, req->cb, (frame_edge_cases_edge_cases_BatchListener_pick_mode*)req->frame); break; + default: break; + } + } + js_cb_finish(req); +} + +static const edge_cases_edge_cases_BatchListener_vtable vtable_edge_cases_edge_cases_BatchListener = {tramp_edge_cases_edge_cases_BatchListener_on_batch, tramp_edge_cases_edge_cases_BatchListener_on_widget, tramp_edge_cases_edge_cases_BatchListener_on_tick, tramp_edge_cases_edge_cases_BatchListener_should_continue, tramp_edge_cases_edge_cases_BatchListener_pick_mode, js_cb_free}; + +typedef struct { + js_cb_frame hdr; + int32_t p0; +} frame_edge_cases_edge_cases_Keyword_delete; + +static void invoke_edge_cases_edge_cases_Keyword_delete(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_Keyword_delete* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[2]; + argv[0] = js_new_i32(env, (int32_t)f->p0); + napi_value result; + if (!js_cb_call(env, cb, "delete", 1, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "Keyword.delete failed"); + } + napi_close_handle_scope(env, scope); +} + +static void tramp_edge_cases_edge_cases_Keyword_delete(void* ctx, int32_t p0, edge_cases_error* out_err) { + frame_edge_cases_edge_cases_Keyword_delete f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + f.p0 = p0; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_edge_cases_edge_cases_Keyword_delete(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 0; + req.frame = &f; + js_cb_hop(&req); + } +} + +typedef struct { + js_cb_frame hdr; + int32_t result; +} frame_edge_cases_edge_cases_Keyword_import; + +static void invoke_edge_cases_edge_cases_Keyword_import(napi_env env, js_cb* cb, frame_edge_cases_edge_cases_Keyword_import* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[1]; + napi_value result; + if (!js_cb_call(env, cb, "import", 0, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "Keyword.import failed"); + } else { + int32_t v = 0; + if (js_arg_i32(env, result, &v)) { + f->result = (int32_t)v; + } else { + js_cb_report(env, f->hdr.out_err, "Keyword.import returned a value of the wrong type"); + } + } + napi_close_handle_scope(env, scope); +} + +static int32_t tramp_edge_cases_edge_cases_Keyword_import(void* ctx, edge_cases_error* out_err) { + frame_edge_cases_edge_cases_Keyword_import f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_edge_cases_edge_cases_Keyword_import(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 1; + req.frame = &f; + js_cb_hop(&req); + } + return f.result; +} + +static void dispatch_edge_cases_edge_cases_Keyword(napi_env env, napi_value fn, void* context, void* data) { + (void)fn; + (void)context; + js_cb_req* req = (js_cb_req*)data; + if (req->method < 0) { + js_cb_release(env, req->cb); + free(req); + return; + } + if (env == NULL) { + edge_cases_error_set(((js_cb_frame*)req->frame)->out_err, -4, "the JavaScript environment is shutting down"); + } else { + switch (req->method) { + case 0: invoke_edge_cases_edge_cases_Keyword_delete(env, req->cb, (frame_edge_cases_edge_cases_Keyword_delete*)req->frame); break; + case 1: invoke_edge_cases_edge_cases_Keyword_import(env, req->cb, (frame_edge_cases_edge_cases_Keyword_import*)req->frame); break; + default: break; + } + } + js_cb_finish(req); +} + +static const edge_cases_edge_cases_Keyword_vtable vtable_edge_cases_edge_cases_Keyword = {tramp_edge_cases_edge_cases_Keyword_delete, tramp_edge_cases_edge_cases_Keyword_import, js_cb_free}; + +static napi_value nx_edge_cases_edge_cases_Widget_clone(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + return js_new_handle(env, edge_cases_edge_cases_Widget_clone((const edge_cases_edge_cases_Widget*)h)); +} + +static napi_value nx_edge_cases_edge_cases_Widget_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + edge_cases_edge_cases_Widget_destroy((edge_cases_edge_cases_Widget*)h); + return js_undefined(env); +} + +static napi_value nx_edge_cases_edge_cases_Vault_clone(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + return js_new_handle(env, edge_cases_edge_cases_Vault_clone((const edge_cases_edge_cases_Vault*)h)); +} + +static napi_value nx_edge_cases_edge_cases_Vault_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + edge_cases_edge_cases_Vault_destroy((edge_cases_edge_cases_Vault*)h); + return js_undefined(env); +} + +static napi_value nx_edge_cases_edge_cases_noop(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + edge_cases_edge_cases_noop(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_undefined(env); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_select(napi_env env, napi_callback_info info) { + JS_ARGS(5); + napi_value ret = NULL; + int32_t a0 = 0; + js_str a1 = JS_STR_INIT; + const uint8_t* a2 = NULL; + size_t a2_len = 0; + const uint8_t* a3 = NULL; + size_t a3_len = 0; + bool a4 = 0; + if (!js_arg_i32(env, argv[0], &a0)) goto done; + if (!js_arg_str(env, argv[1], &a1)) goto done; + if (!js_arg_bytes(env, argv[2], &a2, &a2_len)) goto done; + if (!js_arg_bytes(env, argv[3], &a3, &a3_len)) goto done; + if (!js_arg_bool(env, argv[4], &a4)) goto done; + { + js_error err = {0}; + int32_t r = edge_cases_edge_cases_select(a0, JS_STR_PTR(a1), a1.len, a2, a2_len, a3, a3_len, a4, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + js_str_free(&a1); + return ret; +} + +static napi_value nx_edge_cases_edge_cases_nested_optional(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = edge_cases_edge_cases_nested_optional(a0, a0_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_optional_params(napi_env env, napi_callback_info info) { + JS_ARGS(6); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + const uint8_t* a1 = NULL; + size_t a1_len = 0; + const uint8_t* a2 = NULL; + size_t a2_len = 0; + const uint8_t* a3 = NULL; + size_t a3_len = 0; + const uint8_t* a4 = NULL; + size_t a4_len = 0; + const uint8_t* a5 = NULL; + size_t a5_len = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + if (!js_arg_bytes(env, argv[1], &a1, &a1_len)) goto done; + if (!js_arg_bytes(env, argv[2], &a2, &a2_len)) goto done; + if (!js_arg_bytes(env, argv[3], &a3, &a3_len)) goto done; + if (!js_arg_bytes(env, argv[4], &a4, &a4_len)) goto done; + if (!js_arg_bytes(env, argv[5], &a5, &a5_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = edge_cases_edge_cases_optional_params(a0, a0_len, a1, a1_len, a2, a2_len, a3, a3_len, a4, a4_len, a5, a5_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_register_batch(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + js_cb* a0 = NULL; + js_cb* a1 = NULL; + if (!js_arg_cb(env, argv[0], "edge_cases_edge_cases_BatchListener", dispatch_edge_cases_edge_cases_BatchListener, &a0)) goto done; + if (!js_arg_cb(env, argv[1], "edge_cases_edge_cases_Keyword", dispatch_edge_cases_edge_cases_Keyword, &a1)) goto done; + { + js_error err = {0}; + edge_cases_edge_cases_register_batch((void*)a0, &vtable_edge_cases_edge_cases_BatchListener, (void*)a1, &vtable_edge_cases_edge_cases_Keyword, &err); + a0 = NULL; + a1 = NULL; + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_undefined(env); + } + goto done; +done: + if (a0 != NULL) js_cb_release(env, a0); + if (a1 != NULL) js_cb_release(env, a1); + return ret; +} + +static napi_value nx_edge_cases_edge_cases_widgets_everywhere(napi_env env, napi_callback_info info) { + JS_ARGS(4); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + const uint8_t* a1 = NULL; + size_t a1_len = 0; + const uint8_t* a2 = NULL; + size_t a2_len = 0; + const uint8_t* a3 = NULL; + size_t a3_len = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + if (!js_arg_bytes(env, argv[1], &a1, &a1_len)) goto done; + if (!js_arg_bytes(env, argv[2], &a2, &a2_len)) goto done; + if (!js_arg_bytes(env, argv[3], &a3, &a3_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = edge_cases_edge_cases_widgets_everywhere(a0, a0_len, a1, a1_len, a2, a2_len, a3, a3_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_widget_stream(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + edge_cases_edge_cases_WidgetStreamIterator* r = edge_cases_edge_cases_widget_stream(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_WidgetStreamIterator_next(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + edge_cases_edge_cases_Widget* item; + memset(&item, 0, sizeof item); + size_t item_len = 0; + js_error err = {0}; + int32_t has = edge_cases_edge_cases_WidgetStreamIterator_next((edge_cases_edge_cases_WidgetStreamIterator*)h, &item, &err); + (void)item_len; + if (err.code != 0) return js_throw(env, &err); + if (has == 0) return js_undefined(env); + return js_new_handle(env, item); +} + +static napi_value nx_edge_cases_edge_cases_WidgetStreamIterator_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + edge_cases_edge_cases_WidgetStreamIterator_destroy((edge_cases_edge_cases_WidgetStreamIterator*)h); + return js_undefined(env); +} + +static void done_edge_cases_edge_cases_async_widget(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result) { + js_async* a = (js_async*)context; + a->v.p = result; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_async_widget(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_async* a = js_async_begin(env, JS_R_HANDLE, "edge_cases_edge_cases_async_widget", &ret); + edge_cases_edge_cases_async_widget(done_edge_cases_edge_cases_async_widget, a); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_make_widget(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + if (!js_arg_str(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + edge_cases_edge_cases_Widget* r = edge_cases_edge_cases_make_widget(JS_STR_PTR(a0), a0.len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static napi_value nx_edge_cases_edge_cases_find_widget(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int64_t a0 = 0; + if (!js_arg_i64(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + edge_cases_edge_cases_Widget* r = edge_cases_edge_cases_find_widget(a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_inspect_widget(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + void* a0 = NULL; + void* a1 = NULL; + if (!js_arg_handle(env, argv[0], &a0, false)) goto done; + if (!js_arg_handle(env, argv[1], &a1, true)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = edge_cases_edge_cases_inspect_widget((const edge_cases_edge_cases_Widget*)a0, (const edge_cases_edge_cases_Widget*)a1, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_string_stream(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + edge_cases_edge_cases_StringStreamIterator* r = edge_cases_edge_cases_string_stream(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_StringStreamIterator_next(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + const uint8_t* item; + memset(&item, 0, sizeof item); + size_t item_len = 0; + js_error err = {0}; + int32_t has = edge_cases_edge_cases_StringStreamIterator_next((edge_cases_edge_cases_StringStreamIterator*)h, &item, &item_len, &err); + if (err.code != 0) return js_throw(env, &err); + if (has == 0) return js_undefined(env); + return js_take_str(env, item, item_len); +} + +static napi_value nx_edge_cases_edge_cases_StringStreamIterator_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + edge_cases_edge_cases_StringStreamIterator_destroy((edge_cases_edge_cases_StringStreamIterator*)h); + return js_undefined(env); +} + +static napi_value nx_edge_cases_edge_cases_scalar_stream(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + edge_cases_edge_cases_ScalarStreamIterator* r = edge_cases_edge_cases_scalar_stream(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_ScalarStreamIterator_next(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + int32_t item; + memset(&item, 0, sizeof item); + size_t item_len = 0; + js_error err = {0}; + int32_t has = edge_cases_edge_cases_ScalarStreamIterator_next((edge_cases_edge_cases_ScalarStreamIterator*)h, &item, &err); + (void)item_len; + if (err.code != 0) return js_throw(env, &err); + if (has == 0) return js_undefined(env); + return js_new_i32(env, (int32_t)item); +} + +static napi_value nx_edge_cases_edge_cases_ScalarStreamIterator_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + edge_cases_edge_cases_ScalarStreamIterator_destroy((edge_cases_edge_cases_ScalarStreamIterator*)h); + return js_undefined(env); +} + +static napi_value nx_edge_cases_edge_cases_mode_stream(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + edge_cases_edge_cases_ModeStreamIterator* r = edge_cases_edge_cases_mode_stream(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_ModeStreamIterator_next(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + edge_cases_edge_cases_Mode item; + memset(&item, 0, sizeof item); + size_t item_len = 0; + js_error err = {0}; + int32_t has = edge_cases_edge_cases_ModeStreamIterator_next((edge_cases_edge_cases_ModeStreamIterator*)h, &item, &err); + (void)item_len; + if (err.code != 0) return js_throw(env, &err); + if (has == 0) return js_undefined(env); + return js_new_i32(env, (int32_t)item); +} + +static napi_value nx_edge_cases_edge_cases_ModeStreamIterator_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + edge_cases_edge_cases_ModeStreamIterator_destroy((edge_cases_edge_cases_ModeStreamIterator*)h); + return js_undefined(env); +} + +static void done_edge_cases_edge_cases_async_void(void* context, edge_cases_error* err) { + js_async* a = (js_async*)context; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_async_void(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_async* a = js_async_begin(env, JS_R_VOID, "edge_cases_edge_cases_async_void", &ret); + edge_cases_edge_cases_async_void(done_edge_cases_edge_cases_async_void, a); + } + goto done; +done: + return ret; +} + +static void done_edge_cases_edge_cases_async_bytes(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_async_bytes(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + uint32_t a0 = 0; + if (!js_arg_u32(env, argv[0], &a0)) goto done; + { + js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_async_bytes", &ret); + edge_cases_edge_cases_async_bytes(a0, done_edge_cases_edge_cases_async_bytes, a); + } + goto done; +done: + return ret; +} + +static void done_edge_cases_edge_cases_async_struct(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_async_struct(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_async_struct", &ret); + edge_cases_edge_cases_async_struct(done_edge_cases_edge_cases_async_struct, a); + } + goto done; +done: + return ret; +} + +static void done_edge_cases_edge_cases_async_list(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_async_list(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, true)) goto done; + { + js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_async_list", &ret); + edge_cases_edge_cases_async_list((edge_cases_cancel_token*)token, done_edge_cases_edge_cases_async_list, a); + } + goto done; +done: + return ret; +} + +static void done_edge_cases_edge_cases_async_optional(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_async_optional(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_async_optional", &ret); + edge_cases_edge_cases_async_optional(done_edge_cases_edge_cases_async_optional, a); + } + goto done; +done: + return ret; +} + +static void done_edge_cases_edge_cases_async_enum(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result) { + js_async* a = (js_async*)context; + a->v.i = result; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_async_enum(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_async* a = js_async_begin(env, JS_R_I32, "edge_cases_edge_cases_async_enum", &ret); + edge_cases_edge_cases_async_enum(done_edge_cases_edge_cases_async_enum, a); + } + goto done; +done: + return ret; +} + +static void done_edge_cases_edge_cases_async_keywords(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_async_keywords(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + int32_t a1 = 0; + if (!js_arg_str(env, argv[0], &a0)) goto done; + if (!js_arg_i32(env, argv[1], &a1)) goto done; + { + js_async* a = js_async_begin(env, JS_R_STR, "edge_cases_edge_cases_async_keywords", &ret); + edge_cases_edge_cases_async_keywords(JS_STR_PTR(a0), a0.len, a1, done_edge_cases_edge_cases_async_keywords, a); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static napi_value nx_edge_cases_edge_cases_keywords_roundtrip(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = edge_cases_edge_cases_keywords_roundtrip(a0, a0_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_legacy_value(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int32_t a0 = 0; + if (!js_arg_i32(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = edge_cases_edge_cases_legacy_value((edge_cases_edge_cases_Legacy)a0, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_Widget_refresh(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + void* self_h = NULL; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + { + js_error err = {0}; + edge_cases_edge_cases_Widget_refresh((const edge_cases_edge_cases_Widget*)self_h, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_undefined(env); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_Widget_rename(napi_env env, napi_callback_info info) { + JS_ARGS(3); + napi_value ret = NULL; + void* self_h = NULL; + js_str a0 = JS_STR_INIT; + const uint8_t* a1 = NULL; + size_t a1_len = 0; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + if (!js_arg_str(env, argv[1], &a0)) goto done; + if (!js_arg_bytes(env, argv[2], &a1, &a1_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = edge_cases_edge_cases_Widget_rename((const edge_cases_edge_cases_Widget*)self_h, JS_STR_PTR(a0), a0.len, a1, a1_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static void done_edge_cases_edge_cases_Widget_fetch(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + js_async_done(a, err); +} + +static napi_value nx_edge_cases_edge_cases_Widget_fetch(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + void* self_h = NULL; + int64_t a0 = 0; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + if (!js_arg_i64(env, argv[1], &a0)) goto done; + { + js_async* a = js_async_begin(env, JS_R_BYTES, "edge_cases_edge_cases_Widget_fetch", &ret); + edge_cases_edge_cases_Widget_fetch((const edge_cases_edge_cases_Widget*)self_h, a0, done_edge_cases_edge_cases_Widget_fetch, a); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_Widget_old_name(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + void* self_h = NULL; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = edge_cases_edge_cases_Widget_old_name((const edge_cases_edge_cases_Widget*)self_h, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_Vault_open(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + if (!js_arg_str(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + edge_cases_edge_cases_Vault* r = edge_cases_edge_cases_Vault_open(JS_STR_PTR(a0), a0.len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static napi_value nx_edge_cases_edge_cases_Vault_peek(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + void* self_h = NULL; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + { + js_error err = {0}; + edge_cases_edge_cases_Widget* r = edge_cases_edge_cases_Vault_peek((const edge_cases_edge_cases_Vault*)self_h, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_edge_cases_edge_cases_Vault_probe(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + bool r = edge_cases_edge_cases_Vault_probe(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_bool(env, r); + } + goto done; +done: + return ret; +} + +NAPI_MODULE_INIT() { + js_env* e = (js_env*)calloc(1, sizeof *e); + napi_set_instance_data(env, e, js_env_free, NULL); + napi_property_descriptor props[] = { + JS_RUNTIME_EXPORTS, + {"edge_cases_edge_cases_checksum", NULL, nx_edge_cases_edge_cases_checksum, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Widget_clone", NULL, nx_edge_cases_edge_cases_Widget_clone, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Widget_destroy", NULL, nx_edge_cases_edge_cases_Widget_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Vault_clone", NULL, nx_edge_cases_edge_cases_Vault_clone, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Vault_destroy", NULL, nx_edge_cases_edge_cases_Vault_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_noop", NULL, nx_edge_cases_edge_cases_noop, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_select", NULL, nx_edge_cases_edge_cases_select, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_nested_optional", NULL, nx_edge_cases_edge_cases_nested_optional, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_optional_params", NULL, nx_edge_cases_edge_cases_optional_params, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_register_batch", NULL, nx_edge_cases_edge_cases_register_batch, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_widgets_everywhere", NULL, nx_edge_cases_edge_cases_widgets_everywhere, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_widget_stream", NULL, nx_edge_cases_edge_cases_widget_stream, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_WidgetStreamIterator_next", NULL, nx_edge_cases_edge_cases_WidgetStreamIterator_next, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_WidgetStreamIterator_destroy", NULL, nx_edge_cases_edge_cases_WidgetStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_async_widget", NULL, nx_edge_cases_edge_cases_async_widget, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_make_widget", NULL, nx_edge_cases_edge_cases_make_widget, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_find_widget", NULL, nx_edge_cases_edge_cases_find_widget, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_inspect_widget", NULL, nx_edge_cases_edge_cases_inspect_widget, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_string_stream", NULL, nx_edge_cases_edge_cases_string_stream, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_StringStreamIterator_next", NULL, nx_edge_cases_edge_cases_StringStreamIterator_next, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_StringStreamIterator_destroy", NULL, nx_edge_cases_edge_cases_StringStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_scalar_stream", NULL, nx_edge_cases_edge_cases_scalar_stream, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_ScalarStreamIterator_next", NULL, nx_edge_cases_edge_cases_ScalarStreamIterator_next, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_ScalarStreamIterator_destroy", NULL, nx_edge_cases_edge_cases_ScalarStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_mode_stream", NULL, nx_edge_cases_edge_cases_mode_stream, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_ModeStreamIterator_next", NULL, nx_edge_cases_edge_cases_ModeStreamIterator_next, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_ModeStreamIterator_destroy", NULL, nx_edge_cases_edge_cases_ModeStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_async_void", NULL, nx_edge_cases_edge_cases_async_void, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_async_bytes", NULL, nx_edge_cases_edge_cases_async_bytes, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_async_struct", NULL, nx_edge_cases_edge_cases_async_struct, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_async_list", NULL, nx_edge_cases_edge_cases_async_list, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_async_optional", NULL, nx_edge_cases_edge_cases_async_optional, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_async_enum", NULL, nx_edge_cases_edge_cases_async_enum, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_async_keywords", NULL, nx_edge_cases_edge_cases_async_keywords, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_keywords_roundtrip", NULL, nx_edge_cases_edge_cases_keywords_roundtrip, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_legacy_value", NULL, nx_edge_cases_edge_cases_legacy_value, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Widget_refresh", NULL, nx_edge_cases_edge_cases_Widget_refresh, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Widget_rename", NULL, nx_edge_cases_edge_cases_Widget_rename, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Widget_fetch", NULL, nx_edge_cases_edge_cases_Widget_fetch, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Widget_old_name", NULL, nx_edge_cases_edge_cases_Widget_old_name, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Vault_open", NULL, nx_edge_cases_edge_cases_Vault_open, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Vault_peek", NULL, nx_edge_cases_edge_cases_Vault_peek, NULL, NULL, NULL, napi_default, NULL}, + {"edge_cases_edge_cases_Vault_probe", NULL, nx_edge_cases_edge_cases_Vault_probe, NULL, NULL, NULL, napi_default, NULL}, + }; + napi_define_properties(env, exports, sizeof props / sizeof props[0], props); + return exports; +} + +// END edge_cases_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_d_ts.snap new file mode 100644 index 00000000..f7a0d645 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_d_ts.snap @@ -0,0 +1,270 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class EdgeCasesError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends EdgeCasesError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof EdgeCasesError; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** No parameters and no result */ +export declare namespace edge_cases { + /** The errors of the `edge_cases` error domain. */ + export class EdgeErrorsError extends $Error { + constructor(code: number, message?: string); + } + /** Resource is busy */ + export class BusyError extends edge_cases.EdgeErrorsError { + static readonly CODE: 7; + constructor(message?: string); + } + /** + * Deprecated enum with non-contiguous values and keyword-like variants + * @deprecated Use Mode instead + */ + export enum Legacy { + None = 0, + Default = 5, + Class = 10, + } + export enum Mode { + Fast = 1, + Safe = 2, + } + export interface Leaf { + weight: number; + } + /** Middle layer of a three-deep struct nesting */ + export interface Inner { + leaf: edge_cases.Leaf; + leaves: edge_cases.Leaf[]; + } + export interface Outer { + inner: edge_cases.Inner; + maybe_inner: edge_cases.Inner | null; + by_name: Record; + modes: edge_cases.Mode[]; + matrix: number[][]; + sparse: (bigint | null)[]; + blob: Uint8Array | null; + note: string | null; + by_id: Record; + _hidden: boolean; + v2: number; + } + /** Every field is a reserved word somewhere */ + export interface Keywords { + class: number; + def: number; + import: number; + from: number; + func: number; + range: number; + register: number; + volatile: number; + new: number; + default: number; + end: number; + in: number; + is: number; + int: number; + var: number; + val: number; + self: number; + this: number; + object: number; + string: number; + case: number; + select: number; + switch: number; + void: number; + union: number; + namespace: number; + operator: number; + template: number; + package: number; + protocol: number; + guard: number; + defer: number; + inout: number; + internal: number; + when: number; + fun: number; + data: number; + late: number; + part: number; + out: number; + ref: number; + event: number; + base: number; + lambda: number; + yield: number; + nil: number; + null: number; + begin: number; + rescue: number; + module: number; + chan: number; + go: number; + let: number; + export: number; + delete: number; + typedef: number; + sizeof: number; + static: number; + super: number; + } + /** + * Deprecated struct + * @deprecated Use Outer instead + */ + export interface Retired { + id: bigint; + } + /** Record carrying objects in every buffered position */ + export interface WidgetHolder { + primary: edge_cases.Widget; + fallback: edge_cases.Widget | null; + extras: edge_cases.Widget[]; + by_name: Record; + } + /** Callback interface carrying composite payloads and objects */ + export interface BatchListener { + /** Composite payloads cross as value buffers */ + onBatch(items: edge_cases.Outer[], raw: Uint8Array, first: edge_cases.Outer | null, mode: edge_cases.Mode): void; + /** Objects cross as strong references the consumer must release */ + onWidget(widget: edge_cases.Widget, fallback: edge_cases.Widget | null, children: edge_cases.Widget[]): void; + onTick(): void; + /** Scalar return travels back through the vtable */ + shouldContinue(class_: number): boolean; + /** Enum return travels back as its integer discriminant */ + pickMode(): edge_cases.Mode; + } + /** Callback interface whose name and methods are keywords */ + export interface Keyword { + delete(self: number): void; + import(): number; + } + /** Interface with no constructor; instances come from make_widget */ + export class Widget { + private constructor(); + /** + * Void method that can fail + * @throws {edge_cases.EdgeErrorsError} + */ + refresh(): void; + rename(class_: string, import_: string | null): string; + /** + * Async method with a struct result + * @throws {edge_cases.EdgeErrorsError} + */ + fetch(id: bigint): Promise; + /** @deprecated Use rename instead */ + oldName(): string; + /** + * Release this wrapper's reference to the native object. Safe to call + * more than once; a wrapper that is never closed is released when it is + * garbage collected. Using the wrapper after `close()` throws. + */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; + } + /** + * Deprecated interface with a throwing constructor + * @deprecated Use Widget instead + */ + export class Vault { + private constructor(); + /** + * Fails when the vault is busy + * @throws {edge_cases.EdgeErrorsError} + */ + static open(path: string): edge_cases.Vault; + peek(): edge_cases.Widget | null; + /** + * Throwing static + * @throws {edge_cases.EdgeErrorsError} + */ + static probe(): boolean; + /** + * Release this wrapper's reference to the native object. Safe to call + * more than once; a wrapper that is never closed is released when it is + * garbage collected. Using the wrapper after `close()` throws. + */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; + } + /** No parameters and no result */ + export function noop(): void; + /** Function named after a keyword whose parameters are all keywords */ + export function select(class_: number, from: string, func: string[], range: number | null, type: boolean): number; + /** Optional list of optional structs, the deepest legal nesting */ + export function nestedOptional(items: (edge_cases.Outer | null)[] | null): (edge_cases.Outer | null)[] | null; + export function optionalParams(text: string | null, count: number | null, outer: edge_cases.Outer | null, tags: string[] | null, blob: Uint8Array | null, mode: edge_cases.Mode | null): edge_cases.Mode | null; + /** Two callback interface parameters on one function */ + export function registerBatch(listener: edge_cases.BatchListener, keyword: edge_cases.Keyword): void; + /** Objects in every buffered position */ + export function widgetsEverywhere(list: edge_cases.Widget[], byName: Record, sparse: (edge_cases.Widget | null)[], holder: edge_cases.WidgetHolder): edge_cases.WidgetHolder; + /** Iterator of objects */ + export function widgetStream(): IterableIterator; + /** + * Async returning an object + * @throws {edge_cases.EdgeErrorsError} + */ + export function asyncWidget(): Promise; + /** Factory for an interface with no constructor */ + export function makeWidget(label: string): edge_cases.Widget; + /** + * Optional interface return + * @throws {edge_cases.EdgeErrorsError} + */ + export function findWidget(id: bigint): edge_cases.Widget | null; + /** Interface parameter, including an optional one */ + export function inspectWidget(widget: edge_cases.Widget, fallback: edge_cases.Widget | null): string; + /** Iterator of strings */ + export function stringStream(): IterableIterator; + /** Iterator of scalars */ + export function scalarStream(): IterableIterator; + /** Iterator of enums */ + export function modeStream(): IterableIterator; + /** Async with no result */ + export function asyncVoid(): Promise; + export function asyncBytes(size: number): Promise; + /** @throws {edge_cases.EdgeErrorsError} */ + export function asyncStruct(): Promise; + /** @throws {edge_cases.EdgeErrorsError} */ + export function asyncList(options?: { signal?: AbortSignal }): Promise; + export function asyncOptional(): Promise; + export function asyncEnum(): Promise; + export function asyncKeywords(class_: string, import_: number): Promise; + export function keywordsRoundtrip(value: edge_cases.Keywords): edge_cases.Keywords; + /** @deprecated Use Mode-based APIs */ + export function legacyValue(value: edge_cases.Legacy): edge_cases.Retired; +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_js.snap index 8047997a..a3eb9a21 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_js.snap @@ -1,651 +1,670 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.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'); - } -} +// To regenerate: weaveffi generate api.yml -o -// 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]; -} +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + EdgeCasesError as $Error, +} from './runtime.js'; +export { EdgeCasesError, CancelledError } from './runtime.js'; -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); - }); -} +import { createRequire } from 'node:module'; -// Borrow a live object wrapper's native handle for one call. The wrapper -// keeps its own reference; the producer clones if it retains the object. -function __borrow(o, cls) { - if (!(o instanceof cls)) { - throw new WeaveFFIError(-3, 'expected an instance of ' + cls.name); - } - if (!o._handle) { - throw new WeaveFFIError(-3, cls.name + ' used after close()'); - } - return o._handle; -} - -// --- 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'); +// The native entry points: the N-API addon `npm install` builds from +// edge_cases_node.c (with node-gyp), or a prebuilt copy next to this file. +function $loadAddon(name) { + const require = createRequire(import.meta.url); + const tried = []; + for (const dir of ['./build/Release/', './build/Debug/', './']) { try { - return __utf8.decode(this._buf.subarray(at, at + n)); + return require(dir + name); } catch (e) { - throw __bufferError('string is not valid UTF-8'); + if (e.code !== 'MODULE_NOT_FOUND') throw e; + tried.push(dir + name); } } - 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'); - } + throw new Error( + `the native addon ${name} was not found (tried ${tried.join(', ')}); ` + + 'build it with `npm install` or `node-gyp rebuild` in this package', + ); } -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]); } + +const $raw = $loadAddon('edge_cases_node.node'); +$raw.$setup($Fault); + +// Object tokens in value buffers are already bigint handles here. +const $token = (t) => t; + +$verify($raw, 'edge_cases', 'edge_cases', 3, [['edge_cases', 0x1567f441073bd4aan]]); + +// The native cancel tokens behind `AbortSignal`s. +const $tokens = { + create: () => $raw.edge_cases_cancel_token_create(), + cancel: (t) => $raw.edge_cases_cancel_token_cancel(t), + destroy: (t) => $raw.edge_cases_cancel_token_destroy(t), +}; + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.edge_cases_debug_live(kind); } -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; + +const edge_cases$EdgeErrorsError = class EdgeErrorsError extends $Error {}; +const edge_cases$BusyError = class BusyError extends edge_cases$EdgeErrorsError { + static CODE = 7; + constructor(message = 'Resource is busy') { + super(7, message); + } +}; +const $codes$edge_cases$EdgeErrors = new Map([[7, edge_cases$BusyError]]); +function $from$edge_cases$EdgeErrors(e) { + return $domain(e, $codes$edge_cases$EdgeErrors); } -function __packLeaf(w, v) { - w.f32(v.weight); +const edge_cases$Legacy = Object.freeze({ + None: 0, + Default: 5, + Class: 10, + 0: 'None', + 5: 'Default', + 10: 'Class', +}); + +const edge_cases$Mode = Object.freeze({ + Fast: 1, + Safe: 2, + 1: 'Fast', + 2: 'Safe', +}); + +function $w$edge_cases$Leaf(w, v) { + w.writeF32(v.weight); } -function __unpackLeaf(r) { +function $r$edge_cases$Leaf(r) { return { - weight: r.f32(), + weight: r.readF32(), }; } -function __packInner(w, v) { - __packLeaf(w, v.leaf); - __wList(w, v.leaves, __packLeaf); +function $w$edge_cases$Inner(w, v) { + $w$edge_cases$Leaf(w, v.leaf); + w.writeList(v.leaves, $w$edge_cases$Leaf); } -function __unpackInner(r) { +function $r$edge_cases$Inner(r) { return { - leaf: __unpackLeaf(r), - leaves: __rList(r, __unpackLeaf), + leaf: $r$edge_cases$Leaf(r), + leaves: r.readList($r$edge_cases$Leaf), }; } -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) { +function $w$edge_cases$Outer(w, v) { + $w$edge_cases$Inner(w, v.inner); + w.writeOpt(v.maybe_inner, $w$edge_cases$Inner); + w.writeMap(v.by_name, $W.String, $w$edge_cases$Inner); + w.writeList(v.modes, $W.I32); + w.writeList(v.matrix, (w, v) => w.writeList(v, $W.I32)); + w.writeList(v.sparse, (w, v) => w.writeOpt(v, $W.I64)); + w.writeOpt(v.blob, $W.Bytes); + w.writeOpt(v.note, $W.String); + w.writeMap(v.by_id, (w, k) => w.writeI64(BigInt(k)), $W.String); + w.writeBool(v._hidden); + w.writeU8(v.v2); +} +function $r$edge_cases$Outer(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) => r.i64())), - blob: __rOpt(r, (r) => r.bytes()), - note: __rOpt(r, (r) => r.str()), - by_id: __rMap(r, (r) => r.i64(), (r) => r.str()), - _hidden: r.bool(), - v2: r.u8(), + inner: $r$edge_cases$Inner(r), + maybe_inner: r.readOpt($r$edge_cases$Inner), + by_name: r.readMap($R.String, $r$edge_cases$Inner), + modes: r.readList($R.I32), + matrix: r.readList((r) => r.readList($R.I32)), + sparse: r.readList((r) => r.readOpt($R.I64)), + blob: r.readOpt($R.Bytes), + note: r.readOpt($R.String), + by_id: r.readMap($R.I64, $R.String), + _hidden: r.readBool(), + v2: r.readU8(), }; } -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) { +function $w$edge_cases$Keywords(w, v) { + w.writeI32(v.class); + w.writeI32(v.def); + w.writeI32(v.import); + w.writeI32(v.from); + w.writeI32(v.func); + w.writeI32(v.range); + w.writeI32(v.register); + w.writeI32(v.volatile); + w.writeI32(v.new); + w.writeI32(v.default); + w.writeI32(v.end); + w.writeI32(v.in); + w.writeI32(v.is); + w.writeI32(v.int); + w.writeI32(v.var); + w.writeI32(v.val); + w.writeI32(v.self); + w.writeI32(v.this); + w.writeI32(v.object); + w.writeI32(v.string); + w.writeI32(v.case); + w.writeI32(v.select); + w.writeI32(v.switch); + w.writeI32(v.void); + w.writeI32(v.union); + w.writeI32(v.namespace); + w.writeI32(v.operator); + w.writeI32(v.template); + w.writeI32(v.package); + w.writeI32(v.protocol); + w.writeI32(v.guard); + w.writeI32(v.defer); + w.writeI32(v.inout); + w.writeI32(v.internal); + w.writeI32(v.when); + w.writeI32(v.fun); + w.writeI32(v.data); + w.writeI32(v.late); + w.writeI32(v.part); + w.writeI32(v.out); + w.writeI32(v.ref); + w.writeI32(v.event); + w.writeI32(v.base); + w.writeI32(v.lambda); + w.writeI32(v.yield); + w.writeI32(v.nil); + w.writeI32(v.null); + w.writeI32(v.begin); + w.writeI32(v.rescue); + w.writeI32(v.module); + w.writeI32(v.chan); + w.writeI32(v.go); + w.writeI32(v.let); + w.writeI32(v.export); + w.writeI32(v.delete); + w.writeI32(v.typedef); + w.writeI32(v.sizeof); + w.writeI32(v.static); + w.writeI32(v.super); +} +function $r$edge_cases$Keywords(r) { return { - class: r.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(), + class: r.readI32(), + def: r.readI32(), + import: r.readI32(), + from: r.readI32(), + func: r.readI32(), + range: r.readI32(), + register: r.readI32(), + volatile: r.readI32(), + new: r.readI32(), + default: r.readI32(), + end: r.readI32(), + in: r.readI32(), + is: r.readI32(), + int: r.readI32(), + var: r.readI32(), + val: r.readI32(), + self: r.readI32(), + this: r.readI32(), + object: r.readI32(), + string: r.readI32(), + case: r.readI32(), + select: r.readI32(), + switch: r.readI32(), + void: r.readI32(), + union: r.readI32(), + namespace: r.readI32(), + operator: r.readI32(), + template: r.readI32(), + package: r.readI32(), + protocol: r.readI32(), + guard: r.readI32(), + defer: r.readI32(), + inout: r.readI32(), + internal: r.readI32(), + when: r.readI32(), + fun: r.readI32(), + data: r.readI32(), + late: r.readI32(), + part: r.readI32(), + out: r.readI32(), + ref: r.readI32(), + event: r.readI32(), + base: r.readI32(), + lambda: r.readI32(), + yield: r.readI32(), + nil: r.readI32(), + null: r.readI32(), + begin: r.readI32(), + rescue: r.readI32(), + module: r.readI32(), + chan: r.readI32(), + go: r.readI32(), + let: r.readI32(), + export: r.readI32(), + delete: r.readI32(), + typedef: r.readI32(), + sizeof: r.readI32(), + static: r.readI32(), + super: r.readI32(), }; } -function __packRetired(w, v) { - w.i64(v.id); +function $w$edge_cases$Retired(w, v) { + w.writeI64(v.id); } -function __unpackRetired(r) { +function $r$edge_cases$Retired(r) { return { - id: r.i64(), + id: r.readI64(), }; } -function __packWidgetHolder(w, v) { - w.u64(v.primary._cloneHandle()); - __wOpt(w, v.fallback, (w, v) => w.u64(v._cloneHandle())); - __wList(w, v.extras, (w, v) => w.u64(v._cloneHandle())); - __wMap(w, v.by_name, (w, k) => w.str(k), (w, v) => w.u64(v._cloneHandle())); +function $w$edge_cases$WidgetHolder(w, v) { + w.writeU64(BigInt($clone(v.primary, edge_cases$Widget))); + w.writeOpt(v.fallback, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget)))); + w.writeList(v.extras, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget)))); + w.writeMap(v.by_name, $W.String, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget)))); } -function __unpackWidgetHolder(r) { +function $r$edge_cases$WidgetHolder(r) { return { - primary: Widget._adopt(r.u64()), - fallback: __rOpt(r, (r) => Widget._adopt(r.u64())), - extras: __rList(r, (r) => Widget._adopt(r.u64())), - by_name: __rMap(r, (r) => r.str(), (r) => Widget._adopt(r.u64())), + primary: $adopt(edge_cases$Widget, $token(r.readU64())), + fallback: r.readOpt((r) => $adopt(edge_cases$Widget, $token(r.readU64()))), + extras: r.readList((r) => $adopt(edge_cases$Widget, $token(r.readU64()))), + by_name: r.readMap($R.String, (r) => $adopt(edge_cases$Widget, $token(r.readU64()))), }; } -// 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; -} - -wv.Legacy = Object.freeze({ - None: 0, - Default: 5, - Class: 10, - 0: 'None', - 5: 'Default', - 10: 'Class', -}); -wv.Mode = Object.freeze({ - Fast: 1, - Safe: 2, - 1: 'Fast', - 2: 'Safe', -}); -function __adaptBatchListener(impl) { - if (impl === null || impl === undefined) { - throw new WeaveFFIError(-3, 'BatchListener implementation must be an object'); - } +function $adapt$edge_cases$BatchListener(impl) { + $impl(impl, 'BatchListener'); return { - on_batch(items, raw, first, mode) { - return impl.onBatch(__decode((r) => __rList(r, __unpackOuter), items), raw, __decode((r) => __rOpt(r, __unpackOuter), first), mode); + on_batch($a0, $a1, $a2, $a3) { + impl.onBatch($decode($a0, (r) => r.readList($r$edge_cases$Outer)), $a1, $decode($a2, (r) => r.readOpt($r$edge_cases$Outer)), $a3); }, - on_widget(widget, fallback, children) { - return impl.onWidget(Widget._adopt(widget), (fallback == null ? null : Widget._adopt(fallback)), __decode((r) => __rList(r, (r) => Widget._adopt(r.u64())), children)); + on_widget($a0, $a1, $a2) { + impl.onWidget($adopt(edge_cases$Widget, $a0), $adoptOpt(edge_cases$Widget, $a1), $decode($a2, (r) => r.readList((r) => $adopt(edge_cases$Widget, $token(r.readU64()))))); }, on_tick() { - return impl.onTick(); + impl.onTick(); }, - should_continue(class_) { - return impl.shouldContinue(class_); + should_continue($a0) { + return $ret.Bool(impl.shouldContinue($a0), 'BatchListener.shouldContinue'); }, pick_mode() { - return impl.pickMode(); + return $ret.I32(impl.pickMode(), 'BatchListener.pickMode'); }, }; } -function __adaptKeyword(impl) { - if (impl === null || impl === undefined) { - throw new WeaveFFIError(-3, 'Keyword implementation must be an object'); - } +function $adapt$edge_cases$Keyword(impl) { + $impl(impl, 'Keyword'); return { - delete(self) { - return impl.delete(self); + delete($a0) { + impl.delete($a0); }, import() { - return impl.import(); + return $ret.I32(impl.import(), 'Keyword.import'); }, }; } -class Widget { +const edge_cases$Widget = class Widget extends $Object { + constructor() { + super(); + throw new TypeError('Widget has no public constructor; use its factory functions'); + } + static $destroy(h) { + $raw.edge_cases_edge_cases_Widget_destroy(h); + } + static $clone(h) { + return $raw.edge_cases_edge_cases_Widget_clone(h); + } refresh() { - return __invoke(addon.Widget_refresh, [__borrow(this, Widget)], __edgeCasesErrorFrom); + const $self = $lend(this, edge_cases$Widget); + try { + $raw.edge_cases_edge_cases_Widget_refresh($self); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } finally { + $unlend(this); + } } rename(class_, import_) { - return __invoke(addon.Widget_rename, [__borrow(this, Widget), class_, __encode((w, v) => __wOpt(w, v, (w, v) => w.str(v)), import_)], __generic); + const $self = $lend(this, edge_cases$Widget); + try { + return $raw.edge_cases_edge_cases_Widget_rename($self, class_, $encode(import_, (w, v) => w.writeOpt(v, $W.String))); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } } - fetch(id) { - return __invokeAsync(addon.Widget_fetch, [__borrow(this, Widget), id], __edgeCasesErrorFrom).then((_r) => __decode(__unpackOuter, _r)); + async fetch(id) { + const $self = $lend(this, edge_cases$Widget); + try { + return $decode(await $raw.edge_cases_edge_cases_Widget_fetch($self, id), $r$edge_cases$Outer); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } finally { + $unlend(this); + } } oldName() { - return __invoke(addon.Widget_old_name, [__borrow(this, Widget)], __generic); - } - close() { - if (this._handle) { - Widget._cleanup.unregister(this); - addon.Widget__destroy(this._handle); - this._handle = 0n; + const $self = $lend(this, edge_cases$Widget); + try { + return $raw.edge_cases_edge_cases_Widget_old_name($self); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); } } - _cloneHandle() { - return addon.Widget__clone(__borrow(this, Widget)); - } -} -Widget._adopt = 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); } -}); -if (typeof Symbol.dispose === 'symbol') { - Widget.prototype[Symbol.dispose] = Widget.prototype.close; -} -wv.Widget = Widget; -class Vault { +const edge_cases$Vault = class Vault extends $Object { + constructor() { + super(); + throw new TypeError('Vault has no public constructor; use its factory functions'); + } + static $destroy(h) { + $raw.edge_cases_edge_cases_Vault_destroy(h); + } + static $clone(h) { + return $raw.edge_cases_edge_cases_Vault_clone(h); + } static open(path) { - const _r = __invoke(addon.Vault_open, [path], __edgeCasesErrorFrom); - return Vault._adopt(_r); + try { + return $adopt(edge_cases$Vault, $raw.edge_cases_edge_cases_Vault_open(path)); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } } peek() { - const _r = __invoke(addon.Vault_peek, [__borrow(this, Vault)], __generic); - return (_r == null ? null : Widget._adopt(_r)); + const $self = $lend(this, edge_cases$Vault); + try { + return $adoptOpt(edge_cases$Widget, $raw.edge_cases_edge_cases_Vault_peek($self)); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } } static probe() { - return __invoke(addon.Vault_probe, [], __edgeCasesErrorFrom); - } - close() { - if (this._handle) { - Vault._cleanup.unregister(this); - addon.Vault__destroy(this._handle); - this._handle = 0n; + try { + return $raw.edge_cases_edge_cases_Vault_probe(); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); } } - _cloneHandle() { - return addon.Vault__clone(__borrow(this, Vault)); +}; + +const edge_cases$noop = function noop() { + try { + $raw.edge_cases_edge_cases_noop(); + } catch ($e) { + throw $fault($e); } -} -Vault._adopt = 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); } -}); -if (typeof Symbol.dispose === 'symbol') { - Vault.prototype[Symbol.dispose] = Vault.prototype.close; -} -wv.Vault = Vault; -wv.noop = function () { - return __invoke(addon.noop, [], __generic); +const edge_cases$select = function select(class_, from, func, range, type) { + try { + return $raw.edge_cases_edge_cases_select(class_, from, $encode(func, (w, v) => w.writeList(v, $W.String)), $encode(range, (w, v) => w.writeOpt(v, $W.I32)), type); + } catch ($e) { + throw $fault($e); + } }; -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); + +const edge_cases$nestedOptional = function nestedOptional(items) { + try { + return $decode($raw.edge_cases_edge_cases_nested_optional($encode(items, (w, v) => w.writeOpt(v, (w, v) => w.writeList(v, (w, v) => w.writeOpt(v, $w$edge_cases$Outer))))), (r) => r.readOpt((r) => r.readList((r) => r.readOpt($r$edge_cases$Outer)))); + } catch ($e) { + throw $fault($e); + } }; -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); + +const edge_cases$optionalParams = function optionalParams(text, count, outer, tags, blob, mode) { + try { + return $decode($raw.edge_cases_edge_cases_optional_params($encode(text, (w, v) => w.writeOpt(v, $W.String)), $encode(count, (w, v) => w.writeOpt(v, $W.I32)), $encode(outer, (w, v) => w.writeOpt(v, $w$edge_cases$Outer)), $encode(tags, (w, v) => w.writeOpt(v, (w, v) => w.writeList(v, $W.String))), $encode(blob, (w, v) => w.writeOpt(v, $W.Bytes)), $encode(mode, (w, v) => w.writeOpt(v, $W.I32))), (r) => r.readOpt($R.I32)); + } catch ($e) { + throw $fault($e); + } }; -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); + +const edge_cases$registerBatch = function registerBatch(listener, keyword) { + try { + $raw.edge_cases_edge_cases_register_batch($adapt$edge_cases$BatchListener(listener), $adapt$edge_cases$Keyword(keyword)); + } catch ($e) { + throw $fault($e); + } }; -wv.registerBatch = function (listener, keyword) { - return __invoke(addon.registerBatch, [__adaptBatchListener(listener), __adaptKeyword(keyword)], __generic); + +const edge_cases$widgetsEverywhere = function widgetsEverywhere(list, byName, sparse, holder) { + try { + return $decode($raw.edge_cases_edge_cases_widgets_everywhere($encode(list, (w, v) => w.writeList(v, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget))))), $encode(byName, (w, v) => w.writeMap(v, $W.String, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget))))), $encode(sparse, (w, v) => w.writeList(v, (w, v) => w.writeOpt(v, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget)))))), $encode(holder, $w$edge_cases$WidgetHolder)), $r$edge_cases$WidgetHolder); + } catch ($e) { + throw $fault($e); + } }; -wv.widgetsEverywhere = function (list, byName, sparse, holder) { - const _r = __invoke(addon.widgetsEverywhere, [__encode((w, v) => __wList(w, v, (w, v) => w.u64(v._cloneHandle())), list), __encode((w, v) => __wMap(w, v, (w, k) => w.str(k), (w, v) => w.u64(v._cloneHandle())), byName), __encode((w, v) => __wList(w, v, (w, v) => __wOpt(w, v, (w, v) => w.u64(v._cloneHandle()))), sparse), __encode(__packWidgetHolder, holder)], __generic); - return __decode(__unpackWidgetHolder, _r); + +const $it$edge_cases$widget_stream = { + next: (h) => $raw.edge_cases_edge_cases_WidgetStreamIterator_next(h), + destroy: (h) => $raw.edge_cases_edge_cases_WidgetStreamIterator_destroy(h), + map: $fault, + convert: (v) => $adopt(edge_cases$Widget, v), }; -wv.widgetStream = function () { - const _it = __invoke(addon.widgetStream, [], __generic); - return new WeaveFFIIterator(_it, addon.widgetStream_iterNext, addon.widgetStream_iterDestroy, __generic, (_e) => Widget._adopt(_e)); +const edge_cases$widgetStream = function widgetStream() { + try { + return new $Iterator($raw.edge_cases_edge_cases_widget_stream(), $it$edge_cases$widget_stream); + } catch ($e) { + throw $fault($e); + } }; -wv.asyncWidget = function () { - return __invokeAsync(addon.asyncWidget, [], __edgeCasesErrorFrom).then((_r) => Widget._adopt(_r)); + +const edge_cases$asyncWidget = async function asyncWidget() { + try { + return $adopt(edge_cases$Widget, await $raw.edge_cases_edge_cases_async_widget()); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } }; -wv.makeWidget = function (label) { - const _r = __invoke(addon.makeWidget, [label], __generic); - return Widget._adopt(_r); + +const edge_cases$makeWidget = function makeWidget(label) { + try { + return $adopt(edge_cases$Widget, $raw.edge_cases_edge_cases_make_widget(label)); + } catch ($e) { + throw $fault($e); + } }; -wv.findWidget = function (id) { - const _r = __invoke(addon.findWidget, [id], __edgeCasesErrorFrom); - return (_r == null ? null : Widget._adopt(_r)); + +const edge_cases$findWidget = function findWidget(id) { + try { + return $adoptOpt(edge_cases$Widget, $raw.edge_cases_edge_cases_find_widget(id)); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } }; -wv.inspectWidget = function (widget, fallback) { - return __invoke(addon.inspectWidget, [__borrow(widget, Widget), (fallback == null ? null : __borrow(fallback, Widget))], __generic); + +const edge_cases$inspectWidget = function inspectWidget(widget, fallback) { + const $o0 = $lend(widget, edge_cases$Widget); + try { + const $o1 = $lendOpt(fallback, edge_cases$Widget); + try { + return $raw.edge_cases_edge_cases_inspect_widget($o0, $o1); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(fallback); + } + } finally { + $unlend(widget); + } +}; + +const $it$edge_cases$string_stream = { + next: (h) => $raw.edge_cases_edge_cases_StringStreamIterator_next(h), + destroy: (h) => $raw.edge_cases_edge_cases_StringStreamIterator_destroy(h), + map: $fault, + convert: null, +}; +const edge_cases$stringStream = function stringStream() { + try { + return new $Iterator($raw.edge_cases_edge_cases_string_stream(), $it$edge_cases$string_stream); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$edge_cases$scalar_stream = { + next: (h) => $raw.edge_cases_edge_cases_ScalarStreamIterator_next(h), + destroy: (h) => $raw.edge_cases_edge_cases_ScalarStreamIterator_destroy(h), + map: $fault, + convert: null, }; -wv.stringStream = function () { - const _it = __invoke(addon.stringStream, [], __generic); - return new WeaveFFIIterator(_it, addon.stringStream_iterNext, addon.stringStream_iterDestroy, __generic, null); +const edge_cases$scalarStream = function scalarStream() { + try { + return new $Iterator($raw.edge_cases_edge_cases_scalar_stream(), $it$edge_cases$scalar_stream); + } catch ($e) { + throw $fault($e); + } }; -wv.scalarStream = function () { - const _it = __invoke(addon.scalarStream, [], __generic); - return new WeaveFFIIterator(_it, addon.scalarStream_iterNext, addon.scalarStream_iterDestroy, __generic, null); + +const $it$edge_cases$mode_stream = { + next: (h) => $raw.edge_cases_edge_cases_ModeStreamIterator_next(h), + destroy: (h) => $raw.edge_cases_edge_cases_ModeStreamIterator_destroy(h), + map: $fault, + convert: null, }; -wv.modeStream = function () { - const _it = __invoke(addon.modeStream, [], __generic); - return new WeaveFFIIterator(_it, addon.modeStream_iterNext, addon.modeStream_iterDestroy, __generic, null); +const edge_cases$modeStream = function modeStream() { + try { + return new $Iterator($raw.edge_cases_edge_cases_mode_stream(), $it$edge_cases$mode_stream); + } catch ($e) { + throw $fault($e); + } }; -wv.asyncVoid = function () { - return __invokeAsync(addon.asyncVoid, [], __generic); + +const edge_cases$asyncVoid = async function asyncVoid() { + try { + await $raw.edge_cases_edge_cases_async_void(); + } catch ($e) { + throw $fault($e); + } }; -wv.asyncBytes = function (size) { - return __invokeAsync(addon.asyncBytes, [size], __generic); + +const edge_cases$asyncBytes = async function asyncBytes(size) { + try { + return await $raw.edge_cases_edge_cases_async_bytes(size); + } catch ($e) { + throw $fault($e); + } }; -wv.asyncStruct = function () { - return __invokeAsync(addon.asyncStruct, [], __edgeCasesErrorFrom).then((_r) => __decode(__unpackOuter, _r)); + +const edge_cases$asyncStruct = async function asyncStruct() { + try { + return $decode(await $raw.edge_cases_edge_cases_async_struct(), $r$edge_cases$Outer); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } }; -wv.asyncList = function () { - return __invokeAsync(addon.asyncList, [], __edgeCasesErrorFrom).then((_r) => __decode((r) => __rList(r, __unpackOuter), _r)); + +const edge_cases$asyncList = async function asyncList($options) { + try { + return $decode(await $cancellable($options?.signal, $tokens, ($t) => $raw.edge_cases_edge_cases_async_list($t)), (r) => r.readList($r$edge_cases$Outer)); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } }; -wv.asyncOptional = function () { - return __invokeAsync(addon.asyncOptional, [], __generic).then((_r) => __decode((r) => __rOpt(r, (r) => r.i64()), _r)); + +const edge_cases$asyncOptional = async function asyncOptional() { + try { + return $decode(await $raw.edge_cases_edge_cases_async_optional(), (r) => r.readOpt($R.I64)); + } catch ($e) { + throw $fault($e); + } }; -wv.asyncEnum = function () { - return __invokeAsync(addon.asyncEnum, [], __generic); + +const edge_cases$asyncEnum = async function asyncEnum() { + try { + return await $raw.edge_cases_edge_cases_async_enum(); + } catch ($e) { + throw $fault($e); + } }; -wv.asyncKeywords = function (class_, import_) { - return __invokeAsync(addon.asyncKeywords, [class_, import_], __generic); + +const edge_cases$asyncKeywords = async function asyncKeywords(class_, import_) { + try { + return await $raw.edge_cases_edge_cases_async_keywords(class_, import_); + } catch ($e) { + throw $fault($e); + } }; -wv.keywordsRoundtrip = function (value) { - const _r = __invoke(addon.keywordsRoundtrip, [__encode(__packKeywords, value)], __generic); - return __decode(__unpackKeywords, _r); + +const edge_cases$keywordsRoundtrip = function keywordsRoundtrip(value) { + try { + return $decode($raw.edge_cases_edge_cases_keywords_roundtrip($encode(value, $w$edge_cases$Keywords)), $r$edge_cases$Keywords); + } catch ($e) { + throw $fault($e); + } }; -wv.legacyValue = function (value) { - const _r = __invoke(addon.legacyValue, [value], __generic); - return __decode(__unpackRetired, _r); + +const edge_cases$legacyValue = function legacyValue(value) { + try { + return $decode($raw.edge_cases_edge_cases_legacy_value(value), $r$edge_cases$Retired); + } catch ($e) { + throw $fault($e); + } }; -module.exports = wv; +export const edge_cases = Object.freeze({ + EdgeErrorsError: edge_cases$EdgeErrorsError, + BusyError: edge_cases$BusyError, + Legacy: edge_cases$Legacy, + Mode: edge_cases$Mode, + Widget: edge_cases$Widget, + Vault: edge_cases$Vault, + noop: edge_cases$noop, + select: edge_cases$select, + nestedOptional: edge_cases$nestedOptional, + optionalParams: edge_cases$optionalParams, + registerBatch: edge_cases$registerBatch, + widgetsEverywhere: edge_cases$widgetsEverywhere, + widgetStream: edge_cases$widgetStream, + asyncWidget: edge_cases$asyncWidget, + makeWidget: edge_cases$makeWidget, + findWidget: edge_cases$findWidget, + inspectWidget: edge_cases$inspectWidget, + stringStream: edge_cases$stringStream, + scalarStream: edge_cases$scalarStream, + modeStream: edge_cases$modeStream, + asyncVoid: edge_cases$asyncVoid, + asyncBytes: edge_cases$asyncBytes, + asyncStruct: edge_cases$asyncStruct, + asyncList: edge_cases$asyncList, + asyncOptional: edge_cases$asyncOptional, + asyncEnum: edge_cases$asyncEnum, + asyncKeywords: edge_cases$asyncKeywords, + keywordsRoundtrip: edge_cases$keywordsRoundtrip, + legacyValue: edge_cases$legacyValue, +}); // END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__package_json.snap index 6fd366d2..a8924019 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__package_json.snap @@ -2,16 +2,34 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "edge_cases", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for edge_cases", + "type": "module", "main": "index.js", - "types": "types.d.ts", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "binding.gyp", + "edge_cases_node.c", + "edge_cases.h" + ], "gypfile": true, "scripts": { "install": "node-gyp rebuild" + }, + "engines": { + "node": ">=18" } } diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__runtime_js.snap new file mode 100644 index 00000000..d7fbec01 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class EdgeCasesError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends EdgeCasesError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new EdgeCasesError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new EdgeCasesError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new EdgeCasesError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new EdgeCasesError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js 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 deleted file mode 100644 index e4e4828f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__types_d_ts.snap +++ /dev/null @@ -1,332 +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 runtime traps (code -2 a - * producer panic, -3 a marshalling failure, -4 a callback interface - * implementation that threw); throwing functions surface a module - * domain subclass for their declared codes. - */ -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: (bigint | 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: bigint; -} -/** Record carrying objects in every buffered position */ -export interface WidgetHolder { - primary: Widget; - fallback: Widget | null; - extras: Widget[]; - by_name: Record; -} -/** 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 -/** Callback interface carrying composite payloads and objects */ -export interface BatchListener { - /** - * Composite payloads cross as value buffers - */ - onBatch(items: Outer[], raw: Buffer, first: Outer | null, mode: Mode): void; - /** - * Objects cross as strong references the consumer must release - */ - onWidget(widget: Widget, fallback: Widget | null, children: Widget[]): void; - onTick(): void; - /** - * Scalar return travels back through the vtable - */ - shouldContinue(class_: number): boolean; - /** - * Enum return travels back as its integer discriminant - */ - pickMode(): Mode; -} -/** Callback interface whose name and methods are keywords */ -export interface Keyword { - delete(self: number): void; - import(): number; -} -/** Interface with no constructor; instances come from make_widget */ -export class Widget { - /** - * 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: bigint): Promise; - /** - * Maps to C function: weaveffi_edge_cases_Widget_old_name - * @deprecated Use rename instead - */ - oldName(): string; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; -} -/** 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; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; -} -/** - * No parameters and no result - * 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 -/** - * Two callback interface parameters on one function - * Maps to C function: weaveffi_edge_cases_register_batch - */ -export function registerBatch(listener: BatchListener, keyword: Keyword): void -/** - * Objects in every buffered position - * Maps to C function: weaveffi_edge_cases_widgets_everywhere - */ -export function widgetsEverywhere(list: Widget[], byName: Record, sparse: (Widget | null)[], holder: WidgetHolder): WidgetHolder -/** - * Iterator of objects - * Maps to C function: weaveffi_edge_cases_widget_stream - */ -export function widgetStream(): IterableIterator -/** - * Async returning an object - * Maps to C function: weaveffi_edge_cases_async_widget - * @throws {EdgeErrorsError} - */ -export function asyncWidget(): Promise -/** - * 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: bigint): 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 -/** - * 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 deleted file mode 100644 index 56a32fa3..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__weaveffi_addon_c.snap +++ /dev/null @@ -1,2058 +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 NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include "weaveffi.h" -#include -#include -#include -#include -#include -#include - -static inline napi_status weaveffi_napi_get_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_int64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - return napi_get_value_int64(env, v, out); - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_get_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - double d = 0; - napi_status s = napi_get_value_double(env, v, &d); - if (s == napi_ok && (d < 0 || d != d)) { - napi_throw_range_error(env, NULL, "negative number for an unsigned 64-bit integer"); - return napi_generic_failure; - } - *out = (uint64_t)d; - return s; - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -// An object handle is the pointer value carried as a bigint. null and -// undefined read as NULL (the absent case of a nullable object). -static inline napi_status weaveffi_napi_get_handle(napi_env env, napi_value v, void** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return napi_ok; - } - if (t == napi_bigint) { - uint64_t raw = 0; - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, &raw, &lossless); - *out = (void*)(uintptr_t)raw; - return s; - } - napi_throw_type_error(env, NULL, "expected an object handle"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_make_handle(napi_env env, const void* p, napi_value* out) { - if (p == NULL) { - return napi_get_null(env, out); - } - return napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, out); -} - -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; - -static uv_thread_t weaveffi_napi_js_thread; - -static inline bool weaveffi_napi_on_js_thread(void) { - uv_thread_t self = uv_thread_self(); - return uv_thread_equal(&self, &weaveffi_napi_js_thread) != 0; -} - -// One handle-table entry per registered callback-interface implementation; -// its address is the `ctx` the producer passes back to every method. -typedef struct { - napi_env env; - napi_ref ref; - napi_threadsafe_function tsfn; -} weaveffi_napi_cb_ctx; - -// Every method frame starts with this header so the dispatcher can reach -// `out_err` without knowing the method. -typedef struct { - weaveffi_error* out_err; -} weaveffi_napi_cb_frame_hdr; - -// A request hopped from a producer thread to the JS thread. `method` is the -// vtable index to run, or -1 to release the context. -typedef struct { - weaveffi_napi_cb_ctx* ctx; - int method; - void* frame; - uv_mutex_t mu; - uv_cond_t cv; - bool done; -} weaveffi_napi_cb_req; - -static weaveffi_napi_cb_ctx* weaveffi_napi_cb_register(napi_env env, napi_value target, const char* name, napi_threadsafe_function_call_js dispatch) { - weaveffi_napi_cb_ctx* ctx = (weaveffi_napi_cb_ctx*)calloc(1, sizeof(weaveffi_napi_cb_ctx)); - ctx->env = env; - napi_create_reference(env, target, 1, &ctx->ref); - napi_value resource_name; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, dispatch, &ctx->tsfn); - // A live implementation must not pin the event loop by itself. - napi_unref_threadsafe_function(env, ctx->tsfn); - return ctx; -} - -// Release one entry on the JS thread (env is NULL only during teardown, when -// the reference is already gone). -static void weaveffi_napi_cb_release(napi_env env, weaveffi_napi_cb_ctx* ctx) { - if (env != NULL) { - napi_delete_reference(env, ctx->ref); - } - napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); - free(ctx); -} - -// The vtable's `free` entry: the producer is done with `ctx`. -static void weaveffi_napi_cb_free(void* ctx_) { - weaveffi_napi_cb_ctx* ctx = (weaveffi_napi_cb_ctx*)ctx_; - if (ctx == NULL) { - return; - } - if (weaveffi_napi_on_js_thread()) { - weaveffi_napi_cb_release(ctx->env, ctx); - return; - } - weaveffi_napi_cb_req* req = (weaveffi_napi_cb_req*)calloc(1, sizeof(weaveffi_napi_cb_req)); - req->ctx = ctx; - req->method = -1; - if (napi_call_threadsafe_function(ctx->tsfn, req, napi_tsfn_nonblocking) != napi_ok) { - free(req); - } -} - -// Run `req` on the JS thread and block until it has completed. -static void weaveffi_napi_cb_hop(weaveffi_napi_cb_req* req) { - uv_mutex_init(&req->mu); - uv_cond_init(&req->cv); - req->done = false; - if (napi_call_threadsafe_function(req->ctx->tsfn, req, napi_tsfn_blocking) == napi_ok) { - uv_mutex_lock(&req->mu); - while (!req->done) { - uv_cond_wait(&req->cv, &req->mu); - } - uv_mutex_unlock(&req->mu); - } else { - weaveffi_error_set(((weaveffi_napi_cb_frame_hdr*)req->frame)->out_err, -4, "callback interface implementation is no longer reachable"); - } - uv_cond_destroy(&req->cv); - uv_mutex_destroy(&req->mu); -} - -static void weaveffi_napi_cb_finish(weaveffi_napi_cb_req* req) { - uv_mutex_lock(&req->mu); - req->done = true; - uv_cond_signal(&req->cv); - uv_mutex_unlock(&req->mu); -} - -// Report a JS exception raised by a callback implementation (or a return -// value of the wrong type) through `out_err` as a foreign error, clearing -// the pending exception so nothing unwinds through the C frame. -static void weaveffi_napi_cb_report(napi_env env, weaveffi_error* out_err, const char* fallback) { - char msg[512]; - msg[0] = 0; - bool pending = false; - napi_is_exception_pending(env, &pending); - if (pending) { - napi_value exc; - napi_get_and_clear_last_exception(env, &exc); - napi_valuetype t; - napi_typeof(env, exc, &t); - napi_value text = exc; - if (t == napi_object) { - napi_get_named_property(env, exc, "message", &text); - } - napi_value str; - if (napi_coerce_to_string(env, text, &str) == napi_ok) { - napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); - } - napi_is_exception_pending(env, &pending); - if (pending) { - napi_get_and_clear_last_exception(env, &exc); - } - } - weaveffi_error_set(out_err, -4, msg[0] ? msg : fallback); -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - 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; -} weaveffi_edge_cases_BatchListener_on_batch_frame; - -static void weaveffi_edge_cases_BatchListener_on_batch_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_edge_cases_BatchListener_on_batch_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "on_batch", &fn); - napi_value argv[4]; - napi_create_buffer_copy(env, f->items_len, f->items_ptr ? (const void*)f->items_ptr : (const void*)"", NULL, &argv[0]); - napi_create_buffer_copy(env, f->raw_len, f->raw_ptr ? (const void*)f->raw_ptr : (const void*)"", NULL, &argv[1]); - napi_create_buffer_copy(env, f->first_len, f->first_ptr ? (const void*)f->first_ptr : (const void*)"", NULL, &argv[2]); - napi_create_int32(env, (int32_t)(f->mode), &argv[3]); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener implementation has no on_batch method"); - } else if (napi_call_function(env, target, fn, 4, argv, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener.on_batch threw"); - } - napi_close_handle_scope(env, scope); -} - -static void weaveffi_edge_cases_BatchListener_on_batch_tramp(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, weaveffi_edge_cases_Mode mode, weaveffi_error* out_err) { - weaveffi_edge_cases_BatchListener_on_batch_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - f.items_ptr = items_ptr; - f.items_len = items_len; - f.raw_ptr = raw_ptr; - f.raw_len = raw_len; - f.first_ptr = first_ptr; - f.first_len = first_len; - f.mode = mode; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_edge_cases_BatchListener_on_batch_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 0; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - weaveffi_edge_cases_Widget* widget; - weaveffi_edge_cases_Widget* fallback; - const uint8_t* children_ptr; - size_t children_len; -} weaveffi_edge_cases_BatchListener_on_widget_frame; - -static void weaveffi_edge_cases_BatchListener_on_widget_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_edge_cases_BatchListener_on_widget_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "on_widget", &fn); - napi_value argv[3]; - weaveffi_napi_make_handle(env, f->widget, &argv[0]); - weaveffi_napi_make_handle(env, f->fallback, &argv[1]); - napi_create_buffer_copy(env, f->children_len, f->children_ptr ? (const void*)f->children_ptr : (const void*)"", NULL, &argv[2]); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener implementation has no on_widget method"); - } else if (napi_call_function(env, target, fn, 3, argv, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener.on_widget threw"); - } - napi_close_handle_scope(env, scope); -} - -static void weaveffi_edge_cases_BatchListener_on_widget_tramp(void* ctx, weaveffi_edge_cases_Widget* widget, weaveffi_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, weaveffi_error* out_err) { - weaveffi_edge_cases_BatchListener_on_widget_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - f.widget = widget; - f.fallback = fallback; - f.children_ptr = children_ptr; - f.children_len = children_len; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_edge_cases_BatchListener_on_widget_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 1; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; -} weaveffi_edge_cases_BatchListener_on_tick_frame; - -static void weaveffi_edge_cases_BatchListener_on_tick_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_edge_cases_BatchListener_on_tick_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "on_tick", &fn); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener implementation has no on_tick method"); - } else if (napi_call_function(env, target, fn, 0, NULL, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener.on_tick threw"); - } - napi_close_handle_scope(env, scope); -} - -static void weaveffi_edge_cases_BatchListener_on_tick_tramp(void* ctx, weaveffi_error* out_err) { - weaveffi_edge_cases_BatchListener_on_tick_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_edge_cases_BatchListener_on_tick_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 2; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - int32_t class; - bool result; -} weaveffi_edge_cases_BatchListener_should_continue_frame; - -static void weaveffi_edge_cases_BatchListener_should_continue_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_edge_cases_BatchListener_should_continue_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "should_continue", &fn); - napi_value argv[1]; - napi_create_int32(env, f->class, &argv[0]); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener implementation has no should_continue method"); - } else if (napi_call_function(env, target, fn, 1, argv, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener.should_continue threw"); - } else { - napi_status rs; - rs = napi_get_value_bool(env, result, &f->result); - if (rs != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener.should_continue returned a value of the wrong type"); - } - } - napi_close_handle_scope(env, scope); -} - -static bool weaveffi_edge_cases_BatchListener_should_continue_tramp(void* ctx, int32_t class, weaveffi_error* out_err) { - weaveffi_edge_cases_BatchListener_should_continue_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - f.class = class; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_edge_cases_BatchListener_should_continue_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 3; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } - return f.result; -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - weaveffi_edge_cases_Mode result; -} weaveffi_edge_cases_BatchListener_pick_mode_frame; - -static void weaveffi_edge_cases_BatchListener_pick_mode_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_edge_cases_BatchListener_pick_mode_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "pick_mode", &fn); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener implementation has no pick_mode method"); - } else if (napi_call_function(env, target, fn, 0, NULL, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener.pick_mode threw"); - } else { - napi_status rs; - int32_t f__result_tmp = 0; - rs = napi_get_value_int32(env, result, &f__result_tmp); - f->result = f__result_tmp; - if (rs != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "BatchListener.pick_mode returned a value of the wrong type"); - } - } - napi_close_handle_scope(env, scope); -} - -static weaveffi_edge_cases_Mode weaveffi_edge_cases_BatchListener_pick_mode_tramp(void* ctx, weaveffi_error* out_err) { - weaveffi_edge_cases_BatchListener_pick_mode_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_edge_cases_BatchListener_pick_mode_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 4; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } - return f.result; -} - -static void weaveffi_edge_cases_BatchListener_napi_dispatch(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_napi_cb_req* req = (weaveffi_napi_cb_req*)data; - if (req->method < 0) { - weaveffi_napi_cb_release(env, req->ctx); - free(req); - return; - } - if (env == NULL) { - weaveffi_error_set(((weaveffi_napi_cb_frame_hdr*)req->frame)->out_err, -4, "Node environment is shutting down"); - } else { - switch (req->method) { - case 0: weaveffi_edge_cases_BatchListener_on_batch_invoke(env, req->ctx, (weaveffi_edge_cases_BatchListener_on_batch_frame*)req->frame); break; - case 1: weaveffi_edge_cases_BatchListener_on_widget_invoke(env, req->ctx, (weaveffi_edge_cases_BatchListener_on_widget_frame*)req->frame); break; - case 2: weaveffi_edge_cases_BatchListener_on_tick_invoke(env, req->ctx, (weaveffi_edge_cases_BatchListener_on_tick_frame*)req->frame); break; - case 3: weaveffi_edge_cases_BatchListener_should_continue_invoke(env, req->ctx, (weaveffi_edge_cases_BatchListener_should_continue_frame*)req->frame); break; - case 4: weaveffi_edge_cases_BatchListener_pick_mode_invoke(env, req->ctx, (weaveffi_edge_cases_BatchListener_pick_mode_frame*)req->frame); break; - default: break; - } - } - weaveffi_napi_cb_finish(req); -} - -static const weaveffi_edge_cases_BatchListener_vtable weaveffi_edge_cases_BatchListener_napi_vtable = { weaveffi_edge_cases_BatchListener_on_batch_tramp, weaveffi_edge_cases_BatchListener_on_widget_tramp, weaveffi_edge_cases_BatchListener_on_tick_tramp, weaveffi_edge_cases_BatchListener_should_continue_tramp, weaveffi_edge_cases_BatchListener_pick_mode_tramp, weaveffi_napi_cb_free }; - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - int32_t self; -} weaveffi_edge_cases_Keyword_delete_frame; - -static void weaveffi_edge_cases_Keyword_delete_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_edge_cases_Keyword_delete_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "delete", &fn); - napi_value argv[1]; - napi_create_int32(env, f->self, &argv[0]); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "Keyword implementation has no delete method"); - } else if (napi_call_function(env, target, fn, 1, argv, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "Keyword.delete threw"); - } - napi_close_handle_scope(env, scope); -} - -static void weaveffi_edge_cases_Keyword_delete_tramp(void* ctx, int32_t self, weaveffi_error* out_err) { - weaveffi_edge_cases_Keyword_delete_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - f.self = self; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_edge_cases_Keyword_delete_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 0; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - int32_t result; -} weaveffi_edge_cases_Keyword_import_frame; - -static void weaveffi_edge_cases_Keyword_import_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_edge_cases_Keyword_import_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "import", &fn); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "Keyword implementation has no import method"); - } else if (napi_call_function(env, target, fn, 0, NULL, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "Keyword.import threw"); - } else { - napi_status rs; - rs = napi_get_value_int32(env, result, &f->result); - if (rs != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "Keyword.import returned a value of the wrong type"); - } - } - napi_close_handle_scope(env, scope); -} - -static int32_t weaveffi_edge_cases_Keyword_import_tramp(void* ctx, weaveffi_error* out_err) { - weaveffi_edge_cases_Keyword_import_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_edge_cases_Keyword_import_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 1; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } - return f.result; -} - -static void weaveffi_edge_cases_Keyword_napi_dispatch(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_napi_cb_req* req = (weaveffi_napi_cb_req*)data; - if (req->method < 0) { - weaveffi_napi_cb_release(env, req->ctx); - free(req); - return; - } - if (env == NULL) { - weaveffi_error_set(((weaveffi_napi_cb_frame_hdr*)req->frame)->out_err, -4, "Node environment is shutting down"); - } else { - switch (req->method) { - case 0: weaveffi_edge_cases_Keyword_delete_invoke(env, req->ctx, (weaveffi_edge_cases_Keyword_delete_frame*)req->frame); break; - case 1: weaveffi_edge_cases_Keyword_import_invoke(env, req->ctx, (weaveffi_edge_cases_Keyword_import_frame*)req->frame); break; - default: break; - } - } - weaveffi_napi_cb_finish(req); -} - -static const weaveffi_edge_cases_Keyword_vtable weaveffi_edge_cases_Keyword_napi_vtable = { weaveffi_edge_cases_Keyword_delete_tramp, weaveffi_edge_cases_Keyword_import_tramp, weaveffi_napi_cb_free }; - -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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - weaveffi_edge_cases_Widget_refresh((const weaveffi_edge_cases_Widget*)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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - size_t class_len = 0; - 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 = NULL; - size_t import_len = 0; - napi_get_buffer_info(env, args[2], &import_raw, &import_len); - if (arg_status != napi_ok) { - free(class); - return NULL; - } - weaveffi_error err = {0}; - const char* result = weaveffi_edge_cases_Widget_rename((const weaveffi_edge_cases_Widget*)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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - int64_t id = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[1], &id); - } - if (arg_status != napi_ok) { - return NULL; - } - 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*)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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - const char* result = weaveffi_edge_cases_Widget_old_name((const weaveffi_edge_cases_Widget*)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); - void* self_raw = NULL; - if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) { - return NULL; - } - weaveffi_edge_cases_Widget* self = (weaveffi_edge_cases_Widget*)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_Widget_clone(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* self_raw = NULL; - if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) { - return NULL; - } - weaveffi_edge_cases_Widget* self = (weaveffi_edge_cases_Widget*)self_raw; - weaveffi_edge_cases_Widget* cloned = weaveffi_edge_cases_Widget_clone(self); - napi_value ret; - weaveffi_napi_make_handle(env, cloned, &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 = 0; - 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; - weaveffi_napi_make_handle(env, 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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - weaveffi_edge_cases_Widget* result = weaveffi_edge_cases_Vault_peek((const weaveffi_edge_cases_Vault*)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; - weaveffi_napi_make_handle(env, 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); - void* self_raw = NULL; - if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) { - return NULL; - } - weaveffi_edge_cases_Vault* self = (weaveffi_edge_cases_Vault*)self_raw; - weaveffi_edge_cases_Vault_destroy(self); - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_edge_cases_Vault_clone(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* self_raw = NULL; - if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) { - return NULL; - } - weaveffi_edge_cases_Vault* self = (weaveffi_edge_cases_Vault*)self_raw; - weaveffi_edge_cases_Vault* cloned = weaveffi_edge_cases_Vault_clone(self); - napi_value ret; - weaveffi_napi_make_handle(env, cloned, &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); - napi_status arg_status = napi_ok; - int32_t class = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_int32(env, args[0], &class); - } - size_t from_len = 0; - 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 = NULL; - size_t func_len = 0; - napi_get_buffer_info(env, args[2], &func_raw, &func_len); - void* range_raw = NULL; - size_t range_len = 0; - napi_get_buffer_info(env, args[3], &range_raw, &range_len); - bool type_ = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_bool(env, args[4], &type_); - } - if (arg_status != napi_ok) { - free(from); - return NULL; - } - 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 = NULL; - size_t items_len = 0; - napi_get_buffer_info(env, args[0], &items_raw, &items_len); - weaveffi_error err = {0}; - size_t out_len = 0; - 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 = NULL; - size_t text_len = 0; - napi_get_buffer_info(env, args[0], &text_raw, &text_len); - void* count_raw = NULL; - size_t count_len = 0; - napi_get_buffer_info(env, args[1], &count_raw, &count_len); - void* outer_raw = NULL; - size_t outer_len = 0; - napi_get_buffer_info(env, args[2], &outer_raw, &outer_len); - void* tags_raw = NULL; - size_t tags_len = 0; - napi_get_buffer_info(env, args[3], &tags_raw, &tags_len); - void* blob_raw = NULL; - size_t blob_len = 0; - napi_get_buffer_info(env, args[4], &blob_raw, &blob_len); - void* mode_raw = NULL; - size_t mode_len = 0; - napi_get_buffer_info(env, args[5], &mode_raw, &mode_len); - weaveffi_error err = {0}; - size_t out_len = 0; - 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_register_batch(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); - napi_status arg_status = napi_ok; - napi_valuetype listener_type; - napi_typeof(env, args[0], &listener_type); - weaveffi_napi_cb_ctx* listener_ctx = NULL; - if (listener_type == napi_object || listener_type == napi_function) { - listener_ctx = weaveffi_napi_cb_register(env, args[0], "weaveffi_edge_cases_BatchListener", weaveffi_edge_cases_BatchListener_napi_dispatch); - } else { - napi_throw_type_error(env, NULL, "expected a BatchListener implementation"); - arg_status = napi_object_expected; - } - napi_valuetype keyword_type; - napi_typeof(env, args[1], &keyword_type); - weaveffi_napi_cb_ctx* keyword_ctx = NULL; - if (keyword_type == napi_object || keyword_type == napi_function) { - keyword_ctx = weaveffi_napi_cb_register(env, args[1], "weaveffi_edge_cases_Keyword", weaveffi_edge_cases_Keyword_napi_dispatch); - } else { - napi_throw_type_error(env, NULL, "expected a Keyword implementation"); - arg_status = napi_object_expected; - } - if (arg_status != napi_ok) { - if (listener_ctx != NULL) weaveffi_napi_cb_release(env, listener_ctx); - if (keyword_ctx != NULL) weaveffi_napi_cb_release(env, keyword_ctx); - return NULL; - } - weaveffi_error err = {0}; - weaveffi_edge_cases_register_batch((void*)listener_ctx, &weaveffi_edge_cases_BatchListener_napi_vtable, (void*)keyword_ctx, &weaveffi_edge_cases_Keyword_napi_vtable, &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_widgets_everywhere(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); - void* list_raw = NULL; - size_t list_len = 0; - napi_get_buffer_info(env, args[0], &list_raw, &list_len); - void* by_name_raw = NULL; - size_t by_name_len = 0; - napi_get_buffer_info(env, args[1], &by_name_raw, &by_name_len); - void* sparse_raw = NULL; - size_t sparse_len = 0; - napi_get_buffer_info(env, args[2], &sparse_raw, &sparse_len); - void* holder_raw = NULL; - size_t holder_len = 0; - napi_get_buffer_info(env, args[3], &holder_raw, &holder_len); - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_edge_cases_widgets_everywhere((const uint8_t*)list_raw, list_len, (const uint8_t*)by_name_raw, by_name_len, (const uint8_t*)sparse_raw, sparse_len, (const uint8_t*)holder_raw, holder_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 void weaveffi_edge_cases_widget_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_WidgetStreamIterator_destroy((weaveffi_edge_cases_WidgetStreamIterator*)state->iter); - state->iter = NULL; - } - free(state); -} - -static napi_value Napi_weaveffi_edge_cases_WidgetStreamIterator_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_Widget* iter_item; - weaveffi_error iter_err = {0}; - if (!weaveffi_edge_cases_WidgetStreamIterator_next((weaveffi_edge_cases_WidgetStreamIterator*)state->iter, &iter_item, &iter_err)) { - weaveffi_edge_cases_WidgetStreamIterator_destroy((weaveffi_edge_cases_WidgetStreamIterator*)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; - } - weaveffi_napi_make_handle(env, iter_item, &ret); - return ret; -} - -static napi_value Napi_weaveffi_edge_cases_WidgetStreamIterator_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_WidgetStreamIterator_destroy((weaveffi_edge_cases_WidgetStreamIterator*)state->iter); - state->iter = NULL; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_edge_cases_widget_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_widget_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_widget_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; - void* result; -} weaveffi_edge_cases_async_widget_napi_actx; - -static void weaveffi_edge_cases_async_widget_napi_cb(void* context, weaveffi_error* err, weaveffi_edge_cases_Widget* result) { - weaveffi_edge_cases_async_widget_napi_actx* ctx = (weaveffi_edge_cases_async_widget_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 = (void*)result; - } - weaveffi_error_free(err); - napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); -} - -static void weaveffi_edge_cases_async_widget_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_edge_cases_async_widget_napi_actx* ctx = (weaveffi_edge_cases_async_widget_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; - weaveffi_napi_make_handle(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_edge_cases_async_widget(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_widget_napi_actx* ctx = (weaveffi_edge_cases_async_widget_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_async_widget_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_widget", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_async_widget_napi_settle, &ctx->tsfn); - weaveffi_edge_cases_async_widget_async(weaveffi_edge_cases_async_widget_napi_cb, ctx); - return promise; -} - -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 = 0; - 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; - weaveffi_napi_make_handle(env, 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); - napi_status arg_status = napi_ok; - int64_t id = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[0], &id); - } - if (arg_status != napi_ok) { - return NULL; - } - 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; - weaveffi_napi_make_handle(env, 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); - napi_status arg_status = napi_ok; - void* widget_raw = NULL; - if (arg_status == napi_ok) arg_status = weaveffi_napi_get_handle(env, args[0], &widget_raw); - void* fallback_raw = NULL; - if (arg_status == napi_ok) arg_status = weaveffi_napi_get_handle(env, args[1], &fallback_raw); - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - const char* result = weaveffi_edge_cases_inspect_widget((const weaveffi_edge_cases_Widget*)widget_raw, (const weaveffi_edge_cases_Widget*)fallback_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 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); - napi_status arg_status = napi_ok; - uint32_t size = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_uint32(env, args[0], &size); - } - if (arg_status != napi_ok) { - return NULL; - } - 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); - napi_status arg_status = napi_ok; - size_t class_len = 0; - 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 = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_int32(env, args[1], &import); - } - if (arg_status != napi_ok) { - free(class); - return NULL; - } - 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 = NULL; - size_t value_len = 0; - napi_get_buffer_info(env, args[0], &value_raw, &value_len); - weaveffi_error err = {0}; - size_t out_len = 0; - 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); - napi_status arg_status = napi_ok; - int32_t value = 0; - if (arg_status == napi_ok) { - int32_t value_tmp = 0; - arg_status = napi_get_value_int32(env, args[0], &value_tmp); - value = value_tmp; - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - size_t out_len = 0; - 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; - } - } - weaveffi_napi_js_thread = uv_thread_self(); - 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 }, - { "Widget__clone", NULL, Napi_weaveffi_edge_cases_Widget_clone, 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 }, - { "Vault__clone", NULL, Napi_weaveffi_edge_cases_Vault_clone, 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 }, - { "registerBatch", NULL, Napi_weaveffi_edge_cases_register_batch, NULL, NULL, NULL, napi_default, NULL }, - { "widgetsEverywhere", NULL, Napi_weaveffi_edge_cases_widgets_everywhere, NULL, NULL, NULL, napi_default, NULL }, - { "widgetStream_iterNext", NULL, Napi_weaveffi_edge_cases_WidgetStreamIterator_next, NULL, NULL, NULL, napi_default, NULL }, - { "widgetStream_iterDestroy", NULL, Napi_weaveffi_edge_cases_WidgetStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "widgetStream", NULL, Napi_weaveffi_edge_cases_widget_stream, NULL, NULL, NULL, napi_default, NULL }, - { "asyncWidget", NULL, Napi_weaveffi_edge_cases_async_widget, 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 }, - { "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, 42, 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__README_md.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__README_md.snap new file mode 100644 index 00000000..4aff8ddc --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__README_md.snap @@ -0,0 +1,24 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + +# kitchen_sink + +Native bindings for kitchen_sink + +Node.js bindings for the `kitchen_sink` native library: an ES module over a small N-API addon (`kitchen_sink_node.c`) that `npm install` compiles with node-gyp. The addon links `kitchen_sink`; set `KITCHEN_SINK_LIBRARY` to the library file (or its directory) when installing, or pass `--kitchen_sink-library=` to npm. + +```sh +KITCHEN_SINK_LIBRARY=/path/to/libkitchen_sink.so npm install kitchen_sink +``` + +Each top-level module of the API is a namespace export: + +```js +import { shared, kitchen } from 'kitchen_sink'; +``` + + diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__binding_gyp.snap index 3997b5bb..78061921 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__binding_gyp.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__binding_gyp.snap @@ -1,17 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o { + "variables": { + "native_dir%": " { const path = require('path'); const p = process.env.KITCHEN_SINK_LIBRARY || process.env.npm_config_kitchen_sink_library; if (!p) return path.resolve('.'); return require('fs').statSync(p).isFile() ? path.dirname(path.resolve(p)) : path.resolve(p); })()\")" + }, "targets": [ { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] + "target_name": "kitchen_sink_node", + "sources": [ + "kitchen_sink_node.c" + ], + "include_dirs": [ + "." + ], + "library_dirs": [ + "<(native_dir)" + ], + "libraries": [ + "-lkitchen_sink" + ], + "conditions": [ + ["OS=='mac'", { "xcode_settings": { "OTHER_LDFLAGS": ["-Wl,-rpath,@loader_path", "-Wl,-rpath,<(native_dir)"] } }], + ["OS=='linux'", { "ldflags": ["-Wl,-rpath,'$$ORIGIN'", "-Wl,-rpath,<(native_dir)"] }] + ] } ] } diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_d_ts.snap new file mode 100644 index 00000000..edc3eb0e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_d_ts.snap @@ -0,0 +1,168 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class KitchenSinkError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends KitchenSinkError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof KitchenSinkError; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** Trivial cross-module helper */ +export declare namespace shared { + /** A reusable token referenced across modules */ + export interface Token { + id: bigint; + label: string; + } + /** Trivial cross-module helper */ + export function ping(): string; +} + +/** Identity for bool */ +export declare namespace kitchen { + /** The errors of the `kitchen` error domain. */ + export class KitchenErrorsError extends $Error { + constructor(code: number, message?: string); + } + /** Item not found */ + export class NotFoundError extends kitchen.KitchenErrorsError { + static readonly CODE: 1; + constructor(message?: string); + } + /** Invalid input */ + export class InvalidInputError extends kitchen.KitchenErrorsError { + static readonly CODE: 2; + constructor(message?: string); + } + /** Task priority levels */ + export enum Priority { + /** Low priority */ + Low = 0, + /** Normal priority */ + Normal = 1, + /** High priority */ + High = 2, + } + /** Kitchen sink struct exercising every field feature */ + export interface Item { + /** Stable identifier */ + id: bigint; + /** Display name */ + name: string; + /** Initial count */ + count: number; + /** Whether the item is enabled */ + enabled: boolean; + /** Scaling ratio */ + ratio: number; + nick: number; + payload: Uint8Array; + tags: string[]; + attrs: Record; + parent: bigint | null; + /** Cross-module struct reference */ + token: shared.Token; + priority: kitchen.Priority; + /** An optional object inside a record */ + gadget: kitchen.Gadget | null; + } + /** Consumer-implemented observer of ready items */ + export interface ReadyListener { + /** Fires when an item is ready */ + onReady(code: number, msg: string): void; + /** Receives the item itself and says whether to keep listening */ + onItem(item: kitchen.Item, gadget: kitchen.Gadget): boolean; + } + /** A pocket-sized interface exercising the object surface */ + export class Gadget { + /** Create a gadget with the given id */ + constructor(id: bigint); + /** Render the gadget as a human-readable string */ + describe(): string; + /** + * Poke the gadget a number of times, failing on a negative count + * @throws {kitchen.KitchenErrorsError} + */ + poke(times: number): number; + /** The gadget subsystem version string */ + static version(): string; + /** + * Release this wrapper's reference to the native object. Safe to call + * more than once; a wrapper that is never closed is released when it is + * garbage collected. Using the wrapper after `close()` throws. + */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; + } + /** Identity for bool */ + export function boolId(x: boolean): boolean; + export function i32Id(x: number): number; + export function u32Id(x: number): number; + export function i64Id(x: bigint): bigint; + export function f64Id(x: number): number; + export function echoString(s: string): string; + export function echoBytes(b: Uint8Array): Uint8Array; + /** Register a listener, returning how many are attached */ + export function subscribe(listener: kitchen.ReadyListener): number; + /** Optional object in both directions */ + export function maybeGadget(fallback: kitchen.Gadget | null): kitchen.Gadget | null; + /** A list of objects */ + export function gadgets(): kitchen.Gadget[]; + /** An iterator over objects */ + export function streamGadgets(): IterableIterator; + /** + * Optional struct return + * @throws {kitchen.KitchenErrorsError} + */ + export function maybeItem(id: bigint): kitchen.Item | null; + /** List of structs */ + export function listItems(): kitchen.Item[]; + /** Map return type */ + export function getAttrs(): Record; + /** Iterator return type */ + export function streamItems(): IterableIterator; + /** Returns the shared Token type from another module */ + export function crossModuleToken(): shared.Token; + /** Async operation */ + export function doAsync(input: string): Promise; + /** Cancellable async operation */ + export function doCancellable(input: string, options?: { signal?: AbortSignal }): Promise; + /** + * Legacy operation kept for compatibility + * @deprecated Use new_op instead + */ + export function legacyOp(): number; + /** Replacement for legacy_op */ + export function newOp(): number; + /** Trivial nested-module function */ + export namespace nested { + /** Trivial nested-module function */ + export function hello(): string; + } +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_js.snap index 95c217c6..78e08876 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__index_js.snap @@ -1,304 +1,99 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.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'); - } -} +// To regenerate: weaveffi generate api.yml -o -// 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]; -} +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + KitchenSinkError as $Error, +} from './runtime.js'; +export { KitchenSinkError, CancelledError } from './runtime.js'; -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); - }); -} +import { createRequire } from 'node:module'; -// Borrow a live object wrapper's native handle for one call. The wrapper -// keeps its own reference; the producer clones if it retains the object. -function __borrow(o, cls) { - if (!(o instanceof cls)) { - throw new WeaveFFIError(-3, 'expected an instance of ' + cls.name); - } - if (!o._handle) { - throw new WeaveFFIError(-3, cls.name + ' used after close()'); - } - return o._handle; -} - -// --- 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'); +// The native entry points: the N-API addon `npm install` builds from +// kitchen_sink_node.c (with node-gyp), or a prebuilt copy next to this file. +function $loadAddon(name) { + const require = createRequire(import.meta.url); + const tried = []; + for (const dir of ['./build/Release/', './build/Debug/', './']) { try { - return __utf8.decode(this._buf.subarray(at, at + n)); + return require(dir + name); } catch (e) { - throw __bufferError('string is not valid UTF-8'); + if (e.code !== 'MODULE_NOT_FOUND') throw e; + tried.push(dir + name); } } - 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; + throw new Error( + `the native addon ${name} was not found (tried ${tried.join(', ')}); ` + + 'build it with `npm install` or `node-gyp rebuild` in this package', + ); } -function __packToken(w, v) { - w.i64(v.id); - w.str(v.label); -} -function __unpackToken(r) { - return { - id: r.i64(), - label: r.str(), - }; -} +const $raw = $loadAddon('kitchen_sink_node.node'); +$raw.$setup($Fault); -function __packItem(w, v) { - w.i64(v.id); - w.str(v.name); - w.i32(v.count); - w.bool(v.enabled); - w.f64(v.ratio); - w.u32(v.nick); - w.bytes(v.payload); - __wList(w, v.tags, (w, v) => w.str(v)); - __wMap(w, v.attrs, (w, k) => w.str(k), (w, v) => w.str(v)); - __wOpt(w, v.parent, (w, v) => w.i64(v)); - __packToken(w, v.token); - w.i32(v.priority); - __wOpt(w, v.gadget, (w, v) => w.u64(v._cloneHandle())); -} -function __unpackItem(r) { - return { - id: r.i64(), - name: r.str(), - count: r.i32(), - enabled: r.bool(), - ratio: r.f64(), - nick: r.u32(), - payload: r.bytes(), - tags: __rList(r, (r) => r.str()), - attrs: __rMap(r, (r) => r.str(), (r) => r.str()), - parent: __rOpt(r, (r) => r.i64()), - token: __unpackToken(r), - priority: r.i32(), - gadget: __rOpt(r, (r) => Gadget._adopt(r.u64())), - }; -} +// Object tokens in value buffers are already bigint handles here. +const $token = (t) => t; -// 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; - } -} +$verify($raw, 'kitchen_sink', 'kitchen_sink', 3, [['shared', 0x42c4ce2c8d0af052n], ['kitchen', 0xd83da75b24b545ffn]]); -class KitchenErrorsError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'KitchenErrorsError'; - } +// The native cancel tokens behind `AbortSignal`s. +const $tokens = { + create: () => $raw.kitchen_sink_cancel_token_create(), + cancel: (t) => $raw.kitchen_sink_cancel_token_cancel(t), + destroy: (t) => $raw.kitchen_sink_cancel_token_destroy(t), +}; + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.kitchen_sink_debug_live(kind); } -wv.KitchenErrorsError = KitchenErrorsError; -class NotFoundError extends KitchenErrorsError { - constructor(message) { - super(1, message || 'Item not found'); - this.name = 'NotFoundError'; + +const kitchen$KitchenErrorsError = class KitchenErrorsError extends $Error {}; +const kitchen$NotFoundError = class NotFoundError extends kitchen$KitchenErrorsError { + static CODE = 1; + constructor(message = 'Item not found') { + super(1, message); } -} -NotFoundError.CODE = 1; -wv.NotFoundError = NotFoundError; -class InvalidInputError extends KitchenErrorsError { - constructor(message) { - super(2, message || 'Invalid input'); - this.name = 'InvalidInputError'; +}; +const kitchen$InvalidInputError = class InvalidInputError extends kitchen$KitchenErrorsError { + static CODE = 2; + constructor(message = 'Invalid input') { + super(2, message); } -} -InvalidInputError.CODE = 2; -wv.InvalidInputError = InvalidInputError; -const __kitchenErrorCodes = Object.freeze({ 1: NotFoundError, 2: InvalidInputError }); -function __kitchenErrorFrom(code, message, payload) { - const _cls = __kitchenErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; +}; +const $codes$kitchen$KitchenErrors = new Map([[1, kitchen$NotFoundError], [2, kitchen$InvalidInputError]]); +function $from$kitchen$KitchenErrors(e) { + return $domain(e, $codes$kitchen$KitchenErrors); } -wv.Priority = Object.freeze({ +const kitchen$Priority = Object.freeze({ Low: 0, Normal: 1, High: 2, @@ -306,134 +101,330 @@ wv.Priority = Object.freeze({ 1: 'Normal', 2: 'High', }); -function __adaptReadyListener(impl) { - if (impl === null || impl === undefined) { - throw new WeaveFFIError(-3, 'ReadyListener implementation must be an object'); + +function $w$shared$Token(w, v) { + w.writeI64(v.id); + w.writeString(v.label); +} +function $r$shared$Token(r) { + return { + id: r.readI64(), + label: r.readString(), + }; +} +function $w$kitchen$Item(w, v) { + w.writeI64(v.id); + w.writeString(v.name); + w.writeI32(v.count); + w.writeBool(v.enabled); + w.writeF64(v.ratio); + w.writeU32(v.nick); + w.writeBytes(v.payload); + w.writeList(v.tags, $W.String); + w.writeMap(v.attrs, $W.String, $W.String); + w.writeOpt(v.parent, $W.I64); + $w$shared$Token(w, v.token); + w.writeI32(v.priority); + w.writeOpt(v.gadget, (w, v) => w.writeU64(BigInt($clone(v, kitchen$Gadget)))); +} +function $r$kitchen$Item(r) { + return { + id: r.readI64(), + name: r.readString(), + count: r.readI32(), + enabled: r.readBool(), + ratio: r.readF64(), + nick: r.readU32(), + payload: r.readBytes(), + tags: r.readList($R.String), + attrs: r.readMap($R.String, $R.String), + parent: r.readOpt($R.I64), + token: $r$shared$Token(r), + priority: r.readI32(), + gadget: r.readOpt((r) => $adopt(kitchen$Gadget, $token(r.readU64()))), + }; +} + +const shared$ping = function ping() { + try { + return $raw.kitchen_sink_shared_ping(); + } catch ($e) { + throw $fault($e); } +}; + +function $adapt$kitchen$ReadyListener(impl) { + $impl(impl, 'ReadyListener'); return { - on_ready(code, msg) { - return impl.onReady(code, msg); + on_ready($a0, $a1) { + impl.onReady($a0, $a1); }, - on_item(item, gadget) { - return impl.onItem(__decode(__unpackItem, item), Gadget._adopt(gadget)); + on_item($a0, $a1) { + return $ret.Bool(impl.onItem($decode($a0, $r$kitchen$Item), $adopt(kitchen$Gadget, $a1)), 'ReadyListener.onItem'); }, }; } -class Gadget { +const kitchen$Gadget = class Gadget extends $Object { constructor(id) { - this._handle = __invoke(addon.Gadget_new, [id], __generic); - Gadget._cleanup.register(this, this._handle, this); + super(); + try { + $own(this, $raw.kitchen_sink_kitchen_Gadget_new(id)); + } catch ($e) { + throw $fault($e); + } + } + static $destroy(h) { + $raw.kitchen_sink_kitchen_Gadget_destroy(h); + } + static $clone(h) { + return $raw.kitchen_sink_kitchen_Gadget_clone(h); } describe() { - return __invoke(addon.Gadget_describe, [__borrow(this, Gadget)], __generic); + const $self = $lend(this, kitchen$Gadget); + try { + return $raw.kitchen_sink_kitchen_Gadget_describe($self); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } } poke(times) { - return __invoke(addon.Gadget_poke, [__borrow(this, Gadget), times], __kitchenErrorFrom); + const $self = $lend(this, kitchen$Gadget); + try { + return $raw.kitchen_sink_kitchen_Gadget_poke($self, times); + } catch ($e) { + throw $from$kitchen$KitchenErrors($e); + } finally { + $unlend(this); + } } static version() { - return __invoke(addon.Gadget_version, [], __generic); - } - close() { - if (this._handle) { - Gadget._cleanup.unregister(this); - addon.Gadget__destroy(this._handle); - this._handle = 0n; + try { + return $raw.kitchen_sink_kitchen_Gadget_version(); + } catch ($e) { + throw $fault($e); } } - _cloneHandle() { - return addon.Gadget__clone(__borrow(this, Gadget)); +}; + +const kitchen$boolId = function boolId(x) { + try { + return $raw.kitchen_sink_kitchen_bool_id(x); + } catch ($e) { + throw $fault($e); } -} -Gadget._adopt = function (handle) { - const _o = Object.create(Gadget.prototype); - _o._handle = handle; - Gadget._cleanup.register(_o, handle, _o); - return _o; }; -Gadget._cleanup = new FinalizationRegistry((handle) => { - if (handle) { addon.Gadget__destroy(handle); } -}); -if (typeof Symbol.dispose === 'symbol') { - Gadget.prototype[Symbol.dispose] = Gadget.prototype.close; -} -wv.Gadget = Gadget; -wv.ping = function () { - return __invoke(addon.ping, [], __generic); +const kitchen$i32Id = function i32Id(x) { + try { + return $raw.kitchen_sink_kitchen_i32_id(x); + } catch ($e) { + throw $fault($e); + } }; -wv.boolId = function (x) { - return __invoke(addon.boolId, [x], __generic); + +const kitchen$u32Id = function u32Id(x) { + try { + return $raw.kitchen_sink_kitchen_u32_id(x); + } catch ($e) { + throw $fault($e); + } }; -wv.i32Id = function (x) { - return __invoke(addon.i32Id, [x], __generic); + +const kitchen$i64Id = function i64Id(x) { + try { + return $raw.kitchen_sink_kitchen_i64_id(x); + } catch ($e) { + throw $fault($e); + } }; -wv.u32Id = function (x) { - return __invoke(addon.u32Id, [x], __generic); + +const kitchen$f64Id = function f64Id(x) { + try { + return $raw.kitchen_sink_kitchen_f64_id(x); + } catch ($e) { + throw $fault($e); + } }; -wv.i64Id = function (x) { - return __invoke(addon.i64Id, [x], __generic); + +const kitchen$echoString = function echoString(s) { + try { + return $raw.kitchen_sink_kitchen_echo_string(s); + } catch ($e) { + throw $fault($e); + } }; -wv.f64Id = function (x) { - return __invoke(addon.f64Id, [x], __generic); + +const kitchen$echoBytes = function echoBytes(b) { + try { + return $raw.kitchen_sink_kitchen_echo_bytes(b); + } catch ($e) { + throw $fault($e); + } }; -wv.echoString = function (s) { - return __invoke(addon.echoString, [s], __generic); + +const kitchen$subscribe = function subscribe(listener) { + try { + return $raw.kitchen_sink_kitchen_subscribe($adapt$kitchen$ReadyListener(listener)); + } catch ($e) { + throw $fault($e); + } }; -wv.echoBytes = function (b) { - return __invoke(addon.echoBytes, [b], __generic); + +const kitchen$maybeGadget = function maybeGadget(fallback) { + const $o0 = $lendOpt(fallback, kitchen$Gadget); + try { + return $adoptOpt(kitchen$Gadget, $raw.kitchen_sink_kitchen_maybe_gadget($o0)); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(fallback); + } }; -wv.subscribe = function (listener) { - return __invoke(addon.subscribe, [__adaptReadyListener(listener)], __generic); + +const kitchen$gadgets = function gadgets() { + try { + return $decode($raw.kitchen_sink_kitchen_gadgets(), (r) => r.readList((r) => $adopt(kitchen$Gadget, $token(r.readU64())))); + } catch ($e) { + throw $fault($e); + } }; -wv.maybeGadget = function (fallback) { - const _r = __invoke(addon.maybeGadget, [(fallback == null ? null : __borrow(fallback, Gadget))], __generic); - return (_r == null ? null : Gadget._adopt(_r)); + +const $it$kitchen$stream_gadgets = { + next: (h) => $raw.kitchen_sink_kitchen_StreamGadgetsIterator_next(h), + destroy: (h) => $raw.kitchen_sink_kitchen_StreamGadgetsIterator_destroy(h), + map: $fault, + convert: (v) => $adopt(kitchen$Gadget, v), }; -wv.gadgets = function () { - const _r = __invoke(addon.gadgets, [], __generic); - return __decode((r) => __rList(r, (r) => Gadget._adopt(r.u64())), _r); +const kitchen$streamGadgets = function streamGadgets() { + try { + return new $Iterator($raw.kitchen_sink_kitchen_stream_gadgets(), $it$kitchen$stream_gadgets); + } catch ($e) { + throw $fault($e); + } }; -wv.streamGadgets = function () { - const _it = __invoke(addon.streamGadgets, [], __generic); - return new WeaveFFIIterator(_it, addon.streamGadgets_iterNext, addon.streamGadgets_iterDestroy, __generic, (_e) => Gadget._adopt(_e)); + +const kitchen$maybeItem = function maybeItem(id) { + try { + return $decode($raw.kitchen_sink_kitchen_maybe_item(id), (r) => r.readOpt($r$kitchen$Item)); + } catch ($e) { + throw $from$kitchen$KitchenErrors($e); + } }; -wv.maybeItem = function (id) { - const _r = __invoke(addon.maybeItem, [id], __kitchenErrorFrom); - return __decode((r) => __rOpt(r, __unpackItem), _r); + +const kitchen$listItems = function listItems() { + try { + return $decode($raw.kitchen_sink_kitchen_list_items(), (r) => r.readList($r$kitchen$Item)); + } catch ($e) { + throw $fault($e); + } }; -wv.listItems = function () { - const _r = __invoke(addon.listItems, [], __generic); - return __decode((r) => __rList(r, __unpackItem), _r); + +const kitchen$getAttrs = function getAttrs() { + try { + return $decode($raw.kitchen_sink_kitchen_get_attrs(), (r) => r.readMap($R.String, $R.I32)); + } catch ($e) { + throw $fault($e); + } }; -wv.getAttrs = function () { - const _r = __invoke(addon.getAttrs, [], __generic); - return __decode((r) => __rMap(r, (r) => r.str(), (r) => r.i32()), _r); + +const $it$kitchen$stream_items = { + next: (h) => $raw.kitchen_sink_kitchen_StreamItemsIterator_next(h), + destroy: (h) => $raw.kitchen_sink_kitchen_StreamItemsIterator_destroy(h), + map: $fault, + convert: (v) => $decode(v, $r$kitchen$Item), }; -wv.streamItems = function () { - const _it = __invoke(addon.streamItems, [], __generic); - return new WeaveFFIIterator(_it, addon.streamItems_iterNext, addon.streamItems_iterDestroy, __generic, (_e) => __decode(__unpackItem, _e)); +const kitchen$streamItems = function streamItems() { + try { + return new $Iterator($raw.kitchen_sink_kitchen_stream_items(), $it$kitchen$stream_items); + } catch ($e) { + throw $fault($e); + } }; -wv.crossModuleToken = function () { - const _r = __invoke(addon.crossModuleToken, [], __generic); - return __decode(__unpackToken, _r); + +const kitchen$crossModuleToken = function crossModuleToken() { + try { + return $decode($raw.kitchen_sink_kitchen_cross_module_token(), $r$shared$Token); + } catch ($e) { + throw $fault($e); + } }; -wv.doAsync = function (input) { - return __invokeAsync(addon.doAsync, [input], __generic); + +const kitchen$doAsync = async function doAsync(input) { + try { + return await $raw.kitchen_sink_kitchen_do_async(input); + } catch ($e) { + throw $fault($e); + } }; -wv.doCancellable = function (input) { - return __invokeAsync(addon.doCancellable, [input], __generic); + +const kitchen$doCancellable = async function doCancellable(input, $options) { + try { + return await $cancellable($options?.signal, $tokens, ($t) => $raw.kitchen_sink_kitchen_do_cancellable(input, $t)); + } catch ($e) { + throw $fault($e); + } }; -wv.legacyOp = function () { - return __invoke(addon.legacyOp, [], __generic); + +const kitchen$legacyOp = function legacyOp() { + try { + return $raw.kitchen_sink_kitchen_legacy_op(); + } catch ($e) { + throw $fault($e); + } }; -wv.newOp = function () { - return __invoke(addon.newOp, [], __generic); + +const kitchen$newOp = function newOp() { + try { + return $raw.kitchen_sink_kitchen_new_op(); + } catch ($e) { + throw $fault($e); + } }; -wv.hello = function () { - return __invoke(addon.hello, [], __generic); + +const kitchen$nested$hello = function hello() { + try { + return $raw.kitchen_sink_kitchen_nested_hello(); + } catch ($e) { + throw $fault($e); + } }; -module.exports = wv; +export const shared = Object.freeze({ + ping: shared$ping, +}); + +export const kitchen = Object.freeze({ + KitchenErrorsError: kitchen$KitchenErrorsError, + NotFoundError: kitchen$NotFoundError, + InvalidInputError: kitchen$InvalidInputError, + Priority: kitchen$Priority, + Gadget: kitchen$Gadget, + boolId: kitchen$boolId, + i32Id: kitchen$i32Id, + u32Id: kitchen$u32Id, + i64Id: kitchen$i64Id, + f64Id: kitchen$f64Id, + echoString: kitchen$echoString, + echoBytes: kitchen$echoBytes, + subscribe: kitchen$subscribe, + maybeGadget: kitchen$maybeGadget, + gadgets: kitchen$gadgets, + streamGadgets: kitchen$streamGadgets, + maybeItem: kitchen$maybeItem, + listItems: kitchen$listItems, + getAttrs: kitchen$getAttrs, + streamItems: kitchen$streamItems, + crossModuleToken: kitchen$crossModuleToken, + doAsync: kitchen$doAsync, + doCancellable: kitchen$doCancellable, + legacyOp: kitchen$legacyOp, + newOp: kitchen$newOp, + nested: Object.freeze({ + hello: kitchen$nested$hello, + }), +}); // END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_h.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_h.snap new file mode 100644 index 00000000..14faeffb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_h.snap @@ -0,0 +1,219 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef KITCHEN_SINK_H +#define KITCHEN_SINK_H + +#include +#include +#include + +#ifndef KITCHEN_SINK_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef KITCHEN_SINK_BUILD +# define KITCHEN_SINK_API __declspec(dllexport) +# else +# define KITCHEN_SINK_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define KITCHEN_SINK_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define KITCHEN_SINK_API __attribute__((visibility("default"))) +# else +# define KITCHEN_SINK_API +# endif +#endif + +#ifndef KITCHEN_SINK_DEPRECATED +# if defined(_MSC_VER) +# define KITCHEN_SINK_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define KITCHEN_SINK_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define KITCHEN_SINK_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports kitchen_sink_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define KITCHEN_SINK_ABI_VERSION 3u +KITCHEN_SINK_API uint32_t kitchen_sink_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by kitchen_sink_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct kitchen_sink_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} kitchen_sink_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +KITCHEN_SINK_API void kitchen_sink_error_set(kitchen_sink_error* err, int32_t code, const char* message); +KITCHEN_SINK_API void kitchen_sink_error_clear(kitchen_sink_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + kitchen_sink_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_error_free(kitchen_sink_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +KITCHEN_SINK_API void kitchen_sink_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct kitchen_sink_cancel_token kitchen_sink_cancel_token; +KITCHEN_SINK_API kitchen_sink_cancel_token* kitchen_sink_cancel_token_create(void); +KITCHEN_SINK_API void kitchen_sink_cancel_token_cancel(kitchen_sink_cancel_token* token); +KITCHEN_SINK_API bool kitchen_sink_cancel_token_is_cancelled(const kitchen_sink_cancel_token* token); +KITCHEN_SINK_API void kitchen_sink_cancel_token_destroy(kitchen_sink_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +KITCHEN_SINK_API uint8_t* kitchen_sink_alloc(uint32_t size); +KITCHEN_SINK_API void kitchen_sink_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +KITCHEN_SINK_API uint64_t kitchen_sink_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define KITCHEN_SINK_SHARED_CHECKSUM 0x42c4ce2c8d0af052ull +KITCHEN_SINK_API uint64_t kitchen_sink_shared_checksum(void); +#define KITCHEN_SINK_KITCHEN_CHECKSUM 0xd83da75b24b545ffull +KITCHEN_SINK_API uint64_t kitchen_sink_kitchen_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with kitchen_sink_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * kitchen_sink_buffer.h declares a C struct and codec for each. + */ + +/** Task priority levels */ +typedef enum { + /** Low priority */ + kitchen_sink_kitchen_Priority_Low = 0, + /** Normal priority */ + kitchen_sink_kitchen_Priority_Normal = 1, + /** High priority */ + kitchen_sink_kitchen_Priority_High = 2 +} kitchen_sink_kitchen_Priority; +/** Error codes reported by throwing functions in the `kitchen` module tree. */ +typedef enum { kitchen_sink_kitchen_KitchenErrors_NotFound = 1, kitchen_sink_kitchen_KitchenErrors_InvalidInput = 2 } kitchen_sink_kitchen_KitchenErrors; +typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget; +typedef struct kitchen_sink_kitchen_StreamGadgetsIterator kitchen_sink_kitchen_StreamGadgetsIterator; +typedef struct kitchen_sink_kitchen_StreamItemsIterator kitchen_sink_kitchen_StreamItemsIterator; +/** + * Consumer-implemented observer of ready items + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct kitchen_sink_kitchen_ReadyListener_vtable { + /** Fires when an item is ready */ + void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); + /** Receives the item itself and says whether to keep listening */ + bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); + void (*free)(void* ctx); +} kitchen_sink_kitchen_ReadyListener_vtable; +typedef void (*kitchen_sink_kitchen_do_async_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*kitchen_sink_kitchen_do_cancellable_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: shared +/** Trivial cross-module helper */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_shared_ping(size_t* out_len, kitchen_sink_error* out_err); + +// Module: kitchen +/** A pocket-sized interface exercising the object surface */ +/** Create a gadget with the given id */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(int64_t id, kitchen_sink_error* out_err); +/** The gadget subsystem version string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_version(size_t* out_len, kitchen_sink_error* out_err); +/** Render the gadget as a human-readable string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err); +/** Poke the gadget a number of times, failing on a negative count */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_Gadget_poke(const kitchen_sink_kitchen_Gadget* self, int32_t times, kitchen_sink_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_kitchen_Gadget_destroy(kitchen_sink_kitchen_Gadget* self); + +/** Identity for bool */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_bool_id(bool x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_i32_id(int32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API uint32_t kitchen_sink_kitchen_u32_id(uint32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int64_t kitchen_sink_kitchen_i64_id(int64_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API double kitchen_sink_kitchen_f64_id(double x, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_string(const uint8_t* s_ptr, size_t s_len, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_bytes(const uint8_t* b_ptr, size_t b_len, size_t* out_len, kitchen_sink_error* out_err); +/** Register a listener, returning how many are attached */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err); +/** Optional object in both directions */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_maybe_gadget(const kitchen_sink_kitchen_Gadget* fallback, kitchen_sink_error* out_err); +/** A list of objects */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_gadgets(size_t* out_len, kitchen_sink_error* out_err); +/** An iterator over objects */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamGadgetsIterator* kitchen_sink_kitchen_stream_gadgets(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamGadgetsIterator_next(kitchen_sink_kitchen_StreamGadgetsIterator* iter, kitchen_sink_kitchen_Gadget** out_item, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamGadgetsIterator_destroy(kitchen_sink_kitchen_StreamGadgetsIterator* iter); +/** Optional struct return */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_maybe_item(int64_t id, size_t* out_len, kitchen_sink_error* out_err); +/** List of structs */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_list_items(size_t* out_len, kitchen_sink_error* out_err); +/** Map return type */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_get_attrs(size_t* out_len, kitchen_sink_error* out_err); +/** Iterator return type */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamItemsIterator* kitchen_sink_kitchen_stream_items(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamItemsIterator_next(kitchen_sink_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamItemsIterator_destroy(kitchen_sink_kitchen_StreamItemsIterator* iter); +/** Returns the shared Token type from another module */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_cross_module_token(size_t* out_len, kitchen_sink_error* out_err); +/** Async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_async(const uint8_t* input_ptr, size_t input_len, kitchen_sink_kitchen_do_async_callback callback, void* context); +/** Cancellable async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_ptr, size_t input_len, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_do_cancellable_callback callback, void* context); +/** Legacy operation kept for compatibility */ +KITCHEN_SINK_DEPRECATED("Use new_op instead") +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_legacy_op(kitchen_sink_error* out_err); +/** Replacement for legacy_op */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_new_op(kitchen_sink_error* out_err); + +// Module: kitchen_nested +/** Trivial nested-module function */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_nested_hello(size_t* out_len, kitchen_sink_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // KITCHEN_SINK_H + +// END kitchen_sink.h diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_node_c.snap new file mode 100644 index 00000000..1fe6f418 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__kitchen_sink_node_c.snap @@ -0,0 +1,1335 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NAPI_VERSION +#define NAPI_VERSION 8 +#endif +#include +#include +#include +#include +#include +#include + +/* The addon wraps deprecated functions too; the JavaScript API carries the + * deprecation instead. */ +#define KITCHEN_SINK_DEPRECATED(msg) +#include "kitchen_sink.h" + +/* + * The N-API transport of the JavaScript bindings. Every C symbol of the + * library is exported to `index.js` under its own name and called with the + * raw convention the shared JavaScript layer documents: direct values as + * numbers, bigints, and booleans; strings as strings; bytes and value + * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint + * handles (null when absent); callback interfaces as adapter objects. A + * failure throws an instance of the runtime's `$Fault` class. + */ + +typedef kitchen_sink_error js_error; + +/* Helpers an API may not need. */ +#if defined(__GNUC__) || defined(__clang__) +#define JS_HELPER static inline __attribute__((unused)) +#else +#define JS_HELPER static inline +#endif + +/* Per-environment state (one per main thread or worker): the `$Fault` + * constructor `index.js` registers through `$setup`. */ +typedef struct { + napi_ref fault; +} js_env; + +static void js_env_free(napi_env env, void* data, void* hint) { + (void)hint; + js_env* e = (js_env*)data; + if (e->fault != NULL) napi_delete_reference(env, e->fault); + free(e); +} + +JS_HELPER napi_value js_undefined(napi_env env) { + napi_value v; + napi_get_undefined(env, &v); + return v; +} + +static napi_value js_setup(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e->fault != NULL) napi_delete_reference(env, e->fault); + napi_create_reference(env, argv[0], 1, &e->fault); + return js_undefined(env); +} + +/* --------------------------------------------------------------------------- + * Value conversions + * ------------------------------------------------------------------------- */ + +JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value ab, out; + void* data = NULL; + napi_create_arraybuffer(env, len, &data, &ab); + if (len > 0) memcpy(data, ptr, len); + napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); + return out; +} + +JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out; + napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); + return out; +} + +JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { + napi_value out; + napi_create_int32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { + napi_value out; + napi_create_uint32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { + napi_value out; + napi_create_bigint_int64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { + napi_value out; + napi_create_bigint_uint64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_f64(napi_env env, double v) { + napi_value out; + napi_create_double(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_bool(napi_env env, bool v) { + napi_value out; + napi_get_boolean(env, v, &out); + return out; +} + +/* An object, iterator, or cancel token handle: the pointer as a bigint, or + * null for NULL. */ +JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { + napi_value out; + if (p == NULL) { + napi_get_null(env, &out); + } else { + napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); + } + return out; +} + +/* Returned strings and buffers: convert, then release the producer's copy. */ +JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_str(env, ptr, len); + if (ptr != NULL) kitchen_sink_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_bytes(env, ptr, len); + if (ptr != NULL) kitchen_sink_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, + const uint8_t* payload, size_t payload_len) { + napi_value argv[3], ctor, out = NULL; + napi_create_int32(env, code, &argv[0]); + napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); + if (payload != NULL) { + argv[2] = js_new_bytes(env, payload, payload_len); + } else { + napi_get_null(env, &argv[2]); + } + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok || + ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { + napi_create_error(env, NULL, argv[1], &out); + napi_set_named_property(env, out, "code", argv[0]); + } + return out; +} + +/* Throw the error a call reported, then release it. */ +JS_HELPER napi_value js_throw(napi_env env, js_error* err) { + napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); + kitchen_sink_error_clear(err); + return NULL; +} + +JS_HELPER bool js_type_error(napi_env env, const char* expected) { + bool pending = false; + napi_is_exception_pending(env, &pending); + if (!pending) napi_throw_type_error(env, NULL, expected); + return false; +} + +JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { + return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { + return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int8_t)t; + return true; +} + +JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int16_t)t; + return true; +} + +JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint8_t)t; + return true; +} + +JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint16_t)t; + return true; +} + +/* 64-bit integers: a bigint that fits, or an integral number. */ +JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_int64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { + *out = (int64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_uint64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && + d == (double)(uint64_t)d) { + *out = (uint64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { + return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { + double d = 0; + if (!js_arg_f64(env, v, &d)) return false; + *out = (float)d; + return true; +} + +JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { + return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); +} + +/* A handle argument; null or undefined is NULL when `nullable`. */ +JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_null || t == napi_undefined) { + *out = NULL; + return nullable || js_type_error(env, "expected an object handle"); + } + uint64_t raw = 0; + bool lossless = false; + if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || + raw == 0) { + return js_type_error(env, "expected an object handle"); + } + *out = (void*)(uintptr_t)raw; + return true; +} + +/* A borrowed byte run: a Uint8Array (or Buffer). */ +JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { + bool is_typed = false; + napi_is_typedarray(env, v, &is_typed); + if (is_typed) { + napi_typedarray_type type; + void* data = NULL; + napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); + if (type == napi_uint8_array || type == napi_uint8_clamped_array) { + *ptr = (const uint8_t*)data; + return true; + } + } + return js_type_error(env, "expected a Uint8Array"); +} + +/* A string argument encoded as UTF-8: short strings live in the inline + * buffer, longer ones on the heap until js_str_free. */ +typedef struct { + char* ptr; + size_t len; + char inline_buf[256]; +} js_str; + +#define JS_STR_INIT {NULL, 0, {0}} + +JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { + size_t len = 0; + if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { + return js_type_error(env, "expected a string"); + } + s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); + napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); + return true; +} + +JS_HELPER void js_str_free(js_str* s) { + if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); + s->ptr = NULL; +} + +#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) + +/* Read up to `n` arguments; missing ones are undefined. */ +#define JS_ARGS(n) \ + size_t argc = (n); \ + napi_value argv[(n) + 1]; \ + napi_get_cb_info(env, info, &argc, argv, NULL, NULL) + +/* --------------------------------------------------------------------------- + * Async calls: the completion may run on any thread, so it only records the + * result and queues the settlement on the JavaScript thread through a + * thread-safe function (which also keeps the event loop alive meanwhile). + * ------------------------------------------------------------------------- */ + +typedef enum { + JS_R_VOID, + JS_R_I32, + JS_R_U32, + JS_R_I64, + JS_R_U64, + JS_R_F64, + JS_R_BOOL, + JS_R_STR, + JS_R_BYTES, + JS_R_HANDLE +} js_kind; + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + js_kind kind; + js_error* err; + union { + int64_t i; + uint64_t u; + double f; + bool b; + const void* p; + } v; + size_t len; +} js_async; + +JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { + (void)cb; + (void)context; + js_async* a = (js_async*)data; + bool failed = a->err != NULL && a->err->code != 0; + if (env != NULL && failed) { + napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, + a->err->payload_ptr, a->err->payload_len)); + } else if (env != NULL) { + napi_value v = NULL; + switch (a->kind) { + case JS_R_VOID: v = js_undefined(env); break; + case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; + case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; + case JS_R_I64: v = js_new_i64(env, a->v.i); break; + case JS_R_U64: v = js_new_u64(env, a->v.u); break; + case JS_R_F64: v = js_new_f64(env, a->v.f); break; + case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; + case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; + } + napi_resolve_deferred(env, a->deferred, v); + } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { + /* The environment is shutting down: just release the result. */ + kitchen_sink_free_bytes((uint8_t*)a->v.p, a->len); + } + kitchen_sink_error_free(a->err); + napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); + free(a); +} + +/* Create the promise and its settlement queue for one async call. */ +JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { + js_async* a = (js_async*)calloc(1, sizeof *a); + a->kind = kind; + napi_create_promise(env, &a->deferred, promise); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, + js_async_settle, &a->tsfn); + return a; +} + +/* Called by each completion once it has recorded its result. */ +JS_HELPER void js_async_done(js_async* a, js_error* err) { + a->err = err; + napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); +} + +/* --------------------------------------------------------------------------- + * Callback interfaces. One registration per implementation passed to the + * producer: a reference to the adapter object, the JavaScript thread it + * belongs to, and a thread-safe function that runs calls made from any other + * thread on that thread while the caller waits. + * ------------------------------------------------------------------------- */ + +typedef struct { + napi_env env; + napi_ref ref; + napi_threadsafe_function tsfn; + uv_thread_t js_thread; +} js_cb; + +/* Every callback frame starts with this header so the dispatcher can reach + * `out_err` without knowing the method. */ +typedef struct { + js_error* out_err; +} js_cb_frame; + +/* A call queued from another thread: `method` is the vtable index, or -1 to + * release the registration. */ +typedef struct { + js_cb* cb; + int method; + void* frame; + uv_mutex_t mu; + uv_cond_t cv; + bool done; +} js_cb_req; + +JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { + uv_thread_t self = uv_thread_self(); + return uv_thread_equal(&self, &cb->js_thread) != 0; +} + +JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, + napi_threadsafe_function_call_js dispatch) { + js_cb* cb = (js_cb*)calloc(1, sizeof *cb); + cb->env = env; + cb->js_thread = uv_thread_self(); + napi_create_reference(env, adapter, 1, &cb->ref); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, + &cb->tsfn); + /* A live implementation must not keep the process alive by itself. */ + napi_unref_threadsafe_function(env, cb->tsfn); + return cb; +} + +JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { + if (env != NULL) napi_delete_reference(env, cb->ref); + napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); + free(cb); +} + +/* A callback-interface argument: the adapter object `index.js` built. */ +JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, + napi_threadsafe_function_call_js dispatch, js_cb** out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); + *out = js_cb_register(env, v, name, dispatch); + return true; +} + +/* The vtable's `free`: the producer is done with the implementation. The + * reference can only be deleted on its own thread. */ +JS_HELPER void js_cb_free(void* ctx) { + js_cb* cb = (js_cb*)ctx; + if (cb == NULL) return; + if (js_cb_on_js_thread(cb)) { + js_cb_release(cb->env, cb); + return; + } + js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); + req->cb = cb; + req->method = -1; + if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); +} + +/* Run `req` on the JavaScript thread and wait for it. */ +JS_HELPER void js_cb_hop(js_cb_req* req) { + uv_mutex_init(&req->mu); + uv_cond_init(&req->cv); + req->done = false; + if (napi_call_threadsafe_function(req->cb->tsfn, req, napi_tsfn_blocking) == napi_ok) { + uv_mutex_lock(&req->mu); + while (!req->done) uv_cond_wait(&req->cv, &req->mu); + uv_mutex_unlock(&req->mu); + } else { + kitchen_sink_error_set(((js_cb_frame*)req->frame)->out_err, -4, + "the callback implementation is no longer reachable"); + } + uv_cond_destroy(&req->cv); + uv_mutex_destroy(&req->mu); +} + +JS_HELPER void js_cb_finish(js_cb_req* req) { + uv_mutex_lock(&req->mu); + req->done = true; + uv_cond_signal(&req->cv); + uv_mutex_unlock(&req->mu); +} + +/* Report the pending JavaScript exception (or `fallback`) through `out_err` + * as a foreign failure (code -4); nothing unwinds through the C frame. */ +JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { + char msg[512]; + msg[0] = 0; + bool pending = false; + napi_is_exception_pending(env, &pending); + if (pending) { + napi_value exc, text, str; + napi_get_and_clear_last_exception(env, &exc); + napi_valuetype t; + napi_typeof(env, exc, &t); + text = exc; + if (t == napi_object) napi_get_named_property(env, exc, "message", &text); + if (napi_coerce_to_string(env, text, &str) == napi_ok) { + napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); + } + napi_is_exception_pending(env, &pending); + if (pending) napi_get_and_clear_last_exception(env, &exc); + } + kitchen_sink_error_set(out_err, -4, msg[0] != 0 ? msg : fallback); +} + +/* Look up and call `method` on the adapter; false when it threw. */ +JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, + const napi_value* argv, napi_value* result) { + napi_value adapter, fn; + napi_get_reference_value(env, cb->ref, &adapter); + napi_get_named_property(env, adapter, method, &fn); + return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; +} + +/* --------------------------------------------------------------------------- + * The runtime symbols every library exports. + * ------------------------------------------------------------------------- */ + +static napi_value js_abi_version(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u32(env, kitchen_sink_abi_version()); +} + +static napi_value js_debug_live(napi_env env, napi_callback_info info) { + JS_ARGS(1); + int32_t kind = 0; + if (!js_arg_i32(env, argv[0], &kind)) return NULL; + return js_new_u64(env, kitchen_sink_debug_live(kind)); +} + +static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { + (void)info; + return js_new_handle(env, kitchen_sink_cancel_token_create()); +} + +static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + kitchen_sink_cancel_token_cancel((kitchen_sink_cancel_token*)token); + return js_undefined(env); +} + +static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + kitchen_sink_cancel_token_destroy((kitchen_sink_cancel_token*)token); + return js_undefined(env); +} + +#define JS_RUNTIME_EXPORTS \ + {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ + {"kitchen_sink_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ + {"kitchen_sink_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ + {"kitchen_sink_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"kitchen_sink_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"kitchen_sink_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ + napi_default, NULL} + +static napi_value nx_kitchen_sink_shared_checksum(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u64(env, kitchen_sink_shared_checksum()); +} + +static napi_value nx_kitchen_sink_kitchen_checksum(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u64(env, kitchen_sink_kitchen_checksum()); +} + +typedef struct { + js_cb_frame hdr; + int32_t p0; + const uint8_t* p1; + size_t p2; +} frame_kitchen_sink_kitchen_ReadyListener_on_ready; + +static void invoke_kitchen_sink_kitchen_ReadyListener_on_ready(napi_env env, js_cb* cb, frame_kitchen_sink_kitchen_ReadyListener_on_ready* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[3]; + argv[0] = js_new_i32(env, (int32_t)f->p0); + argv[1] = js_new_str(env, f->p1, f->p2); + napi_value result; + if (!js_cb_call(env, cb, "on_ready", 2, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "ReadyListener.on_ready failed"); + } + napi_close_handle_scope(env, scope); +} + +static void tramp_kitchen_sink_kitchen_ReadyListener_on_ready(void* ctx, int32_t p0, const uint8_t* p1, size_t p2, kitchen_sink_error* out_err) { + frame_kitchen_sink_kitchen_ReadyListener_on_ready f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + f.p0 = p0; + f.p1 = p1; + f.p2 = p2; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_kitchen_sink_kitchen_ReadyListener_on_ready(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 0; + req.frame = &f; + js_cb_hop(&req); + } +} + +typedef struct { + js_cb_frame hdr; + const uint8_t* p0; + size_t p1; + kitchen_sink_kitchen_Gadget* p2; + bool result; +} frame_kitchen_sink_kitchen_ReadyListener_on_item; + +static void invoke_kitchen_sink_kitchen_ReadyListener_on_item(napi_env env, js_cb* cb, frame_kitchen_sink_kitchen_ReadyListener_on_item* f) { + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value argv[3]; + argv[0] = js_new_bytes(env, f->p0, f->p1); + argv[1] = js_new_handle(env, f->p2); + napi_value result; + if (!js_cb_call(env, cb, "on_item", 2, argv, &result)) { + js_cb_report(env, f->hdr.out_err, "ReadyListener.on_item failed"); + } else { + bool v = 0; + if (js_arg_bool(env, result, &v)) { + f->result = (bool)v; + } else { + js_cb_report(env, f->hdr.out_err, "ReadyListener.on_item returned a value of the wrong type"); + } + } + napi_close_handle_scope(env, scope); +} + +static bool tramp_kitchen_sink_kitchen_ReadyListener_on_item(void* ctx, const uint8_t* p0, size_t p1, kitchen_sink_kitchen_Gadget* p2, kitchen_sink_error* out_err) { + frame_kitchen_sink_kitchen_ReadyListener_on_item f; + memset(&f, 0, sizeof f); + f.hdr.out_err = out_err; + f.p0 = p0; + f.p1 = p1; + f.p2 = p2; + js_cb* cb = (js_cb*)ctx; + if (js_cb_on_js_thread(cb)) { + invoke_kitchen_sink_kitchen_ReadyListener_on_item(cb->env, cb, &f); + } else { + js_cb_req req; + memset(&req, 0, sizeof req); + req.cb = cb; + req.method = 1; + req.frame = &f; + js_cb_hop(&req); + } + return f.result; +} + +static void dispatch_kitchen_sink_kitchen_ReadyListener(napi_env env, napi_value fn, void* context, void* data) { + (void)fn; + (void)context; + js_cb_req* req = (js_cb_req*)data; + if (req->method < 0) { + js_cb_release(env, req->cb); + free(req); + return; + } + if (env == NULL) { + kitchen_sink_error_set(((js_cb_frame*)req->frame)->out_err, -4, "the JavaScript environment is shutting down"); + } else { + switch (req->method) { + case 0: invoke_kitchen_sink_kitchen_ReadyListener_on_ready(env, req->cb, (frame_kitchen_sink_kitchen_ReadyListener_on_ready*)req->frame); break; + case 1: invoke_kitchen_sink_kitchen_ReadyListener_on_item(env, req->cb, (frame_kitchen_sink_kitchen_ReadyListener_on_item*)req->frame); break; + default: break; + } + } + js_cb_finish(req); +} + +static const kitchen_sink_kitchen_ReadyListener_vtable vtable_kitchen_sink_kitchen_ReadyListener = {tramp_kitchen_sink_kitchen_ReadyListener_on_ready, tramp_kitchen_sink_kitchen_ReadyListener_on_item, js_cb_free}; + +static napi_value nx_kitchen_sink_shared_ping(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_shared_ping(&out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_Gadget_clone(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + return js_new_handle(env, kitchen_sink_kitchen_Gadget_clone((const kitchen_sink_kitchen_Gadget*)h)); +} + +static napi_value nx_kitchen_sink_kitchen_Gadget_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + kitchen_sink_kitchen_Gadget_destroy((kitchen_sink_kitchen_Gadget*)h); + return js_undefined(env); +} + +static napi_value nx_kitchen_sink_kitchen_bool_id(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + bool a0 = 0; + if (!js_arg_bool(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + bool r = kitchen_sink_kitchen_bool_id(a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_bool(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_i32_id(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int32_t a0 = 0; + if (!js_arg_i32(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + int32_t r = kitchen_sink_kitchen_i32_id(a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_u32_id(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + uint32_t a0 = 0; + if (!js_arg_u32(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + uint32_t r = kitchen_sink_kitchen_u32_id(a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_u32(env, (uint32_t)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_i64_id(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int64_t a0 = 0; + if (!js_arg_i64(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + int64_t r = kitchen_sink_kitchen_i64_id(a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i64(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_f64_id(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + double a0 = 0; + if (!js_arg_f64(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + double r = kitchen_sink_kitchen_f64_id(a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_f64(env, (double)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_echo_string(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + if (!js_arg_str(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_echo_string(JS_STR_PTR(a0), a0.len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_echo_bytes(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_echo_bytes(a0, a0_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_subscribe(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + js_cb* a0 = NULL; + if (!js_arg_cb(env, argv[0], "kitchen_sink_kitchen_ReadyListener", dispatch_kitchen_sink_kitchen_ReadyListener, &a0)) goto done; + { + js_error err = {0}; + int32_t r = kitchen_sink_kitchen_subscribe((void*)a0, &vtable_kitchen_sink_kitchen_ReadyListener, &err); + a0 = NULL; + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + if (a0 != NULL) js_cb_release(env, a0); + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_maybe_gadget(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + void* a0 = NULL; + if (!js_arg_handle(env, argv[0], &a0, true)) goto done; + { + js_error err = {0}; + kitchen_sink_kitchen_Gadget* r = kitchen_sink_kitchen_maybe_gadget((const kitchen_sink_kitchen_Gadget*)a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_gadgets(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_gadgets(&out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_stream_gadgets(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + kitchen_sink_kitchen_StreamGadgetsIterator* r = kitchen_sink_kitchen_stream_gadgets(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_StreamGadgetsIterator_next(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + kitchen_sink_kitchen_Gadget* item; + memset(&item, 0, sizeof item); + size_t item_len = 0; + js_error err = {0}; + int32_t has = kitchen_sink_kitchen_StreamGadgetsIterator_next((kitchen_sink_kitchen_StreamGadgetsIterator*)h, &item, &err); + (void)item_len; + if (err.code != 0) return js_throw(env, &err); + if (has == 0) return js_undefined(env); + return js_new_handle(env, item); +} + +static napi_value nx_kitchen_sink_kitchen_StreamGadgetsIterator_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + kitchen_sink_kitchen_StreamGadgetsIterator_destroy((kitchen_sink_kitchen_StreamGadgetsIterator*)h); + return js_undefined(env); +} + +static napi_value nx_kitchen_sink_kitchen_maybe_item(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int64_t a0 = 0; + if (!js_arg_i64(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_maybe_item(a0, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_list_items(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_list_items(&out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_get_attrs(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_get_attrs(&out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_stream_items(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + kitchen_sink_kitchen_StreamItemsIterator* r = kitchen_sink_kitchen_stream_items(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_StreamItemsIterator_next(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + const uint8_t* item; + memset(&item, 0, sizeof item); + size_t item_len = 0; + js_error err = {0}; + int32_t has = kitchen_sink_kitchen_StreamItemsIterator_next((kitchen_sink_kitchen_StreamItemsIterator*)h, &item, &item_len, &err); + if (err.code != 0) return js_throw(env, &err); + if (has == 0) return js_undefined(env); + return js_take_bytes(env, item, item_len); +} + +static napi_value nx_kitchen_sink_kitchen_StreamItemsIterator_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* h = NULL; + if (!js_arg_handle(env, argv[0], &h, false)) return NULL; + kitchen_sink_kitchen_StreamItemsIterator_destroy((kitchen_sink_kitchen_StreamItemsIterator*)h); + return js_undefined(env); +} + +static napi_value nx_kitchen_sink_kitchen_cross_module_token(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_cross_module_token(&out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static void done_kitchen_sink_kitchen_do_async(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + js_async_done(a, err); +} + +static napi_value nx_kitchen_sink_kitchen_do_async(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + if (!js_arg_str(env, argv[0], &a0)) goto done; + { + js_async* a = js_async_begin(env, JS_R_STR, "kitchen_sink_kitchen_do_async", &ret); + kitchen_sink_kitchen_do_async(JS_STR_PTR(a0), a0.len, done_kitchen_sink_kitchen_do_async, a); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static void done_kitchen_sink_kitchen_do_cancellable(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len) { + js_async* a = (js_async*)context; + a->v.p = result_ptr; + a->len = result_len; + js_async_done(a, err); +} + +static napi_value nx_kitchen_sink_kitchen_do_cancellable(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + js_str a0 = JS_STR_INIT; + void* token = NULL; + if (!js_arg_str(env, argv[0], &a0)) goto done; + if (!js_arg_handle(env, argv[1], &token, true)) goto done; + { + js_async* a = js_async_begin(env, JS_R_STR, "kitchen_sink_kitchen_do_cancellable", &ret); + kitchen_sink_kitchen_do_cancellable(JS_STR_PTR(a0), a0.len, (kitchen_sink_cancel_token*)token, done_kitchen_sink_kitchen_do_cancellable, a); + } + goto done; +done: + js_str_free(&a0); + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_legacy_op(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + int32_t r = kitchen_sink_kitchen_legacy_op(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_new_op(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + int32_t r = kitchen_sink_kitchen_new_op(&err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_Gadget_new(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int64_t a0 = 0; + if (!js_arg_i64(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + kitchen_sink_kitchen_Gadget* r = kitchen_sink_kitchen_Gadget_new(a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_handle(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_Gadget_describe(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + void* self_h = NULL; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_Gadget_describe((const kitchen_sink_kitchen_Gadget*)self_h, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_Gadget_poke(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + void* self_h = NULL; + int32_t a0 = 0; + if (!js_arg_handle(env, argv[0], &self_h, false)) goto done; + if (!js_arg_i32(env, argv[1], &a0)) goto done; + { + js_error err = {0}; + int32_t r = kitchen_sink_kitchen_Gadget_poke((const kitchen_sink_kitchen_Gadget*)self_h, a0, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_i32(env, (int32_t)r); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_Gadget_version(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_Gadget_version(&out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_kitchen_sink_kitchen_nested_hello(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = kitchen_sink_kitchen_nested_hello(&out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + return ret; +} + +NAPI_MODULE_INIT() { + js_env* e = (js_env*)calloc(1, sizeof *e); + napi_set_instance_data(env, e, js_env_free, NULL); + napi_property_descriptor props[] = { + JS_RUNTIME_EXPORTS, + {"kitchen_sink_shared_checksum", NULL, nx_kitchen_sink_shared_checksum, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_checksum", NULL, nx_kitchen_sink_kitchen_checksum, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_shared_ping", NULL, nx_kitchen_sink_shared_ping, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_Gadget_clone", NULL, nx_kitchen_sink_kitchen_Gadget_clone, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_Gadget_destroy", NULL, nx_kitchen_sink_kitchen_Gadget_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_bool_id", NULL, nx_kitchen_sink_kitchen_bool_id, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_i32_id", NULL, nx_kitchen_sink_kitchen_i32_id, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_u32_id", NULL, nx_kitchen_sink_kitchen_u32_id, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_i64_id", NULL, nx_kitchen_sink_kitchen_i64_id, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_f64_id", NULL, nx_kitchen_sink_kitchen_f64_id, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_echo_string", NULL, nx_kitchen_sink_kitchen_echo_string, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_echo_bytes", NULL, nx_kitchen_sink_kitchen_echo_bytes, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_subscribe", NULL, nx_kitchen_sink_kitchen_subscribe, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_maybe_gadget", NULL, nx_kitchen_sink_kitchen_maybe_gadget, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_gadgets", NULL, nx_kitchen_sink_kitchen_gadgets, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_stream_gadgets", NULL, nx_kitchen_sink_kitchen_stream_gadgets, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_StreamGadgetsIterator_next", NULL, nx_kitchen_sink_kitchen_StreamGadgetsIterator_next, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_StreamGadgetsIterator_destroy", NULL, nx_kitchen_sink_kitchen_StreamGadgetsIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_maybe_item", NULL, nx_kitchen_sink_kitchen_maybe_item, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_list_items", NULL, nx_kitchen_sink_kitchen_list_items, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_get_attrs", NULL, nx_kitchen_sink_kitchen_get_attrs, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_stream_items", NULL, nx_kitchen_sink_kitchen_stream_items, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_StreamItemsIterator_next", NULL, nx_kitchen_sink_kitchen_StreamItemsIterator_next, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_StreamItemsIterator_destroy", NULL, nx_kitchen_sink_kitchen_StreamItemsIterator_destroy, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_cross_module_token", NULL, nx_kitchen_sink_kitchen_cross_module_token, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_do_async", NULL, nx_kitchen_sink_kitchen_do_async, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_do_cancellable", NULL, nx_kitchen_sink_kitchen_do_cancellable, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_legacy_op", NULL, nx_kitchen_sink_kitchen_legacy_op, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_new_op", NULL, nx_kitchen_sink_kitchen_new_op, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_Gadget_new", NULL, nx_kitchen_sink_kitchen_Gadget_new, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_Gadget_describe", NULL, nx_kitchen_sink_kitchen_Gadget_describe, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_Gadget_poke", NULL, nx_kitchen_sink_kitchen_Gadget_poke, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_Gadget_version", NULL, nx_kitchen_sink_kitchen_Gadget_version, NULL, NULL, NULL, napi_default, NULL}, + {"kitchen_sink_kitchen_nested_hello", NULL, nx_kitchen_sink_kitchen_nested_hello, NULL, NULL, NULL, napi_default, NULL}, + }; + napi_define_properties(env, exports, sizeof props / sizeof props[0], props); + return exports; +} + +// END kitchen_sink_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__package_json.snap index 6fd366d2..ed61743e 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__package_json.snap @@ -2,16 +2,34 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "kitchen_sink", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for kitchen_sink", + "type": "module", "main": "index.js", - "types": "types.d.ts", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "binding.gyp", + "kitchen_sink_node.c", + "kitchen_sink.h" + ], "gypfile": true, "scripts": { "install": "node-gyp rebuild" + }, + "engines": { + "node": ">=18" } } diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__runtime_js.snap new file mode 100644 index 00000000..e11da31e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class KitchenSinkError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends KitchenSinkError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new KitchenSinkError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new KitchenSinkError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new KitchenSinkError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new KitchenSinkError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__types_d_ts.snap deleted file mode 100644 index 76640c64..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__types_d_ts.snap +++ /dev/null @@ -1,225 +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 runtime traps (code -2 a - * producer panic, -3 a marshalling failure, -4 a callback interface - * implementation that threw); throwing functions surface a module - * domain subclass for their declared codes. - */ -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); -} -/** A reusable token referenced across modules */ -export interface Token { - id: bigint; - label: string; -} -// module shared -/** - * Trivial cross-module helper - * Maps to C function: weaveffi_shared_ping - */ -export function ping(): string -/** Typed errors reported by the `kitchen` module's throwing functions. */ -export class KitchenErrorsError extends WeaveFFIError { - constructor(code: number, message: string); -} -export class NotFoundError extends KitchenErrorsError { - static readonly CODE: 1; - constructor(message?: string); -} -export class InvalidInputError extends KitchenErrorsError { - static readonly CODE: 2; - constructor(message?: string); -} -/** Kitchen sink struct exercising every field feature */ -export interface Item { - /** Stable identifier */ - id: bigint; - /** Display name */ - name: string; - /** Initial count */ - count: number; - /** Whether the item is enabled */ - enabled: boolean; - /** Scaling ratio */ - ratio: number; - nick: number; - payload: Buffer; - tags: string[]; - attrs: Record; - parent: bigint | null; - /** Cross-module struct reference */ - token: Token; - priority: Priority; - /** An optional object inside a record */ - gadget: Gadget | null; -} -/** Task priority levels */ -export enum Priority { - /** Low priority */ - Low = 0, - /** Normal priority */ - Normal = 1, - /** High priority */ - High = 2, -} -// module kitchen -/** Consumer-implemented observer of ready items */ -export interface ReadyListener { - /** - * Fires when an item is ready - */ - onReady(code: number, msg: string): void; - /** - * Receives the item itself and says whether to keep listening - */ - onItem(item: Item, gadget: Gadget): boolean; -} -/** A pocket-sized interface exercising the object surface */ -export class Gadget { - /** - * Create a gadget with the given id - * Maps to C function: weaveffi_kitchen_Gadget_new - */ - constructor(id: bigint); - /** - * Render the gadget as a human-readable string - * Maps to C function: weaveffi_kitchen_Gadget_describe - */ - describe(): string; - /** - * Poke the gadget a number of times, failing on a negative count - * Maps to C function: weaveffi_kitchen_Gadget_poke - * @throws {KitchenErrorsError} - */ - poke(times: number): number; - /** - * The gadget subsystem version string - * Maps to C function: weaveffi_kitchen_Gadget_version - */ - static version(): string; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; -} -/** - * Identity for bool - * Maps to C function: weaveffi_kitchen_bool_id - */ -export function boolId(x: boolean): boolean -/** - * Maps to C function: weaveffi_kitchen_i32_id - */ -export function i32Id(x: number): number -/** - * Maps to C function: weaveffi_kitchen_u32_id - */ -export function u32Id(x: number): number -/** - * Maps to C function: weaveffi_kitchen_i64_id - */ -export function i64Id(x: bigint): bigint -/** - * Maps to C function: weaveffi_kitchen_f64_id - */ -export function f64Id(x: number): number -/** - * Maps to C function: weaveffi_kitchen_echo_string - */ -export function echoString(s: string): string -/** - * Maps to C function: weaveffi_kitchen_echo_bytes - */ -export function echoBytes(b: Buffer): Buffer -/** - * Register a listener, returning how many are attached - * Maps to C function: weaveffi_kitchen_subscribe - */ -export function subscribe(listener: ReadyListener): number -/** - * Optional object in both directions - * Maps to C function: weaveffi_kitchen_maybe_gadget - */ -export function maybeGadget(fallback: Gadget | null): Gadget | null -/** - * A list of objects - * Maps to C function: weaveffi_kitchen_gadgets - */ -export function gadgets(): Gadget[] -/** - * An iterator over objects - * Maps to C function: weaveffi_kitchen_stream_gadgets - */ -export function streamGadgets(): IterableIterator -/** - * Optional struct return - * Maps to C function: weaveffi_kitchen_maybe_item - * @throws {KitchenErrorsError} - */ -export function maybeItem(id: bigint): Item | null -/** - * List of structs - * Maps to C function: weaveffi_kitchen_list_items - */ -export function listItems(): Item[] -/** - * Map return type - * Maps to C function: weaveffi_kitchen_get_attrs - */ -export function getAttrs(): Record -/** - * Iterator return type - * Maps to C function: weaveffi_kitchen_stream_items - */ -export function streamItems(): IterableIterator -/** - * Returns the shared Token type from another module - * Maps to C function: weaveffi_kitchen_cross_module_token - */ -export function crossModuleToken(): Token -/** - * Async operation - * Maps to C function: weaveffi_kitchen_do_async - */ -export function doAsync(input: string): Promise -/** - * Cancellable async operation - * Maps to C function: weaveffi_kitchen_do_cancellable - */ -export function doCancellable(input: string): Promise -/** - * Legacy operation kept for compatibility - * Maps to C function: weaveffi_kitchen_legacy_op - * @deprecated Use new_op instead - */ -export function legacyOp(): number -/** - * Replacement for legacy_op - * Maps to C function: weaveffi_kitchen_new_op - */ -export function newOp(): number -// module kitchen_nested -/** - * Trivial nested-module function - * Maps to C function: weaveffi_kitchen_nested_hello - */ -export function hello(): string - -// END types.d.ts 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 deleted file mode 100644 index 0be6a8e5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap +++ /dev/null @@ -1,1192 +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 NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include "weaveffi.h" -#include -#include -#include -#include -#include -#include - -static inline napi_status weaveffi_napi_get_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_int64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - return napi_get_value_int64(env, v, out); - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_get_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - double d = 0; - napi_status s = napi_get_value_double(env, v, &d); - if (s == napi_ok && (d < 0 || d != d)) { - napi_throw_range_error(env, NULL, "negative number for an unsigned 64-bit integer"); - return napi_generic_failure; - } - *out = (uint64_t)d; - return s; - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -// An object handle is the pointer value carried as a bigint. null and -// undefined read as NULL (the absent case of a nullable object). -static inline napi_status weaveffi_napi_get_handle(napi_env env, napi_value v, void** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return napi_ok; - } - if (t == napi_bigint) { - uint64_t raw = 0; - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, &raw, &lossless); - *out = (void*)(uintptr_t)raw; - return s; - } - napi_throw_type_error(env, NULL, "expected an object handle"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_make_handle(napi_env env, const void* p, napi_value* out) { - if (p == NULL) { - return napi_get_null(env, out); - } - return napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, out); -} - -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; - -static uv_thread_t weaveffi_napi_js_thread; - -static inline bool weaveffi_napi_on_js_thread(void) { - uv_thread_t self = uv_thread_self(); - return uv_thread_equal(&self, &weaveffi_napi_js_thread) != 0; -} - -// One handle-table entry per registered callback-interface implementation; -// its address is the `ctx` the producer passes back to every method. -typedef struct { - napi_env env; - napi_ref ref; - napi_threadsafe_function tsfn; -} weaveffi_napi_cb_ctx; - -// Every method frame starts with this header so the dispatcher can reach -// `out_err` without knowing the method. -typedef struct { - weaveffi_error* out_err; -} weaveffi_napi_cb_frame_hdr; - -// A request hopped from a producer thread to the JS thread. `method` is the -// vtable index to run, or -1 to release the context. -typedef struct { - weaveffi_napi_cb_ctx* ctx; - int method; - void* frame; - uv_mutex_t mu; - uv_cond_t cv; - bool done; -} weaveffi_napi_cb_req; - -static weaveffi_napi_cb_ctx* weaveffi_napi_cb_register(napi_env env, napi_value target, const char* name, napi_threadsafe_function_call_js dispatch) { - weaveffi_napi_cb_ctx* ctx = (weaveffi_napi_cb_ctx*)calloc(1, sizeof(weaveffi_napi_cb_ctx)); - ctx->env = env; - napi_create_reference(env, target, 1, &ctx->ref); - napi_value resource_name; - napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, dispatch, &ctx->tsfn); - // A live implementation must not pin the event loop by itself. - napi_unref_threadsafe_function(env, ctx->tsfn); - return ctx; -} - -// Release one entry on the JS thread (env is NULL only during teardown, when -// the reference is already gone). -static void weaveffi_napi_cb_release(napi_env env, weaveffi_napi_cb_ctx* ctx) { - if (env != NULL) { - napi_delete_reference(env, ctx->ref); - } - napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); - free(ctx); -} - -// The vtable's `free` entry: the producer is done with `ctx`. -static void weaveffi_napi_cb_free(void* ctx_) { - weaveffi_napi_cb_ctx* ctx = (weaveffi_napi_cb_ctx*)ctx_; - if (ctx == NULL) { - return; - } - if (weaveffi_napi_on_js_thread()) { - weaveffi_napi_cb_release(ctx->env, ctx); - return; - } - weaveffi_napi_cb_req* req = (weaveffi_napi_cb_req*)calloc(1, sizeof(weaveffi_napi_cb_req)); - req->ctx = ctx; - req->method = -1; - if (napi_call_threadsafe_function(ctx->tsfn, req, napi_tsfn_nonblocking) != napi_ok) { - free(req); - } -} - -// Run `req` on the JS thread and block until it has completed. -static void weaveffi_napi_cb_hop(weaveffi_napi_cb_req* req) { - uv_mutex_init(&req->mu); - uv_cond_init(&req->cv); - req->done = false; - if (napi_call_threadsafe_function(req->ctx->tsfn, req, napi_tsfn_blocking) == napi_ok) { - uv_mutex_lock(&req->mu); - while (!req->done) { - uv_cond_wait(&req->cv, &req->mu); - } - uv_mutex_unlock(&req->mu); - } else { - weaveffi_error_set(((weaveffi_napi_cb_frame_hdr*)req->frame)->out_err, -4, "callback interface implementation is no longer reachable"); - } - uv_cond_destroy(&req->cv); - uv_mutex_destroy(&req->mu); -} - -static void weaveffi_napi_cb_finish(weaveffi_napi_cb_req* req) { - uv_mutex_lock(&req->mu); - req->done = true; - uv_cond_signal(&req->cv); - uv_mutex_unlock(&req->mu); -} - -// Report a JS exception raised by a callback implementation (or a return -// value of the wrong type) through `out_err` as a foreign error, clearing -// the pending exception so nothing unwinds through the C frame. -static void weaveffi_napi_cb_report(napi_env env, weaveffi_error* out_err, const char* fallback) { - char msg[512]; - msg[0] = 0; - bool pending = false; - napi_is_exception_pending(env, &pending); - if (pending) { - napi_value exc; - napi_get_and_clear_last_exception(env, &exc); - napi_valuetype t; - napi_typeof(env, exc, &t); - napi_value text = exc; - if (t == napi_object) { - napi_get_named_property(env, exc, "message", &text); - } - napi_value str; - if (napi_coerce_to_string(env, text, &str) == napi_ok) { - napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); - } - napi_is_exception_pending(env, &pending); - if (pending) { - napi_get_and_clear_last_exception(env, &exc); - } - } - weaveffi_error_set(out_err, -4, msg[0] ? msg : fallback); -} - -static napi_value Napi_weaveffi_shared_ping(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}; - const char* result = weaveffi_shared_ping(&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; -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - int32_t code; - const char* msg; -} weaveffi_kitchen_ReadyListener_on_ready_frame; - -static void weaveffi_kitchen_ReadyListener_on_ready_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_kitchen_ReadyListener_on_ready_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "on_ready", &fn); - napi_value argv[2]; - napi_create_int32(env, f->code, &argv[0]); - napi_create_string_utf8(env, f->msg ? f->msg : "", NAPI_AUTO_LENGTH, &argv[1]); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "ReadyListener implementation has no on_ready method"); - } else if (napi_call_function(env, target, fn, 2, argv, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "ReadyListener.on_ready threw"); - } - napi_close_handle_scope(env, scope); -} - -static void weaveffi_kitchen_ReadyListener_on_ready_tramp(void* ctx, int32_t code, const char* msg, weaveffi_error* out_err) { - weaveffi_kitchen_ReadyListener_on_ready_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - f.code = code; - f.msg = msg; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_kitchen_ReadyListener_on_ready_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 0; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } -} - -typedef struct { - weaveffi_napi_cb_frame_hdr hdr; - const uint8_t* item_ptr; - size_t item_len; - weaveffi_kitchen_Gadget* gadget; - bool result; -} weaveffi_kitchen_ReadyListener_on_item_frame; - -static void weaveffi_kitchen_ReadyListener_on_item_invoke(napi_env env, weaveffi_napi_cb_ctx* ctx, weaveffi_kitchen_ReadyListener_on_item_frame* f) { - napi_handle_scope scope; - napi_open_handle_scope(env, &scope); - napi_value target; - napi_get_reference_value(env, ctx->ref, &target); - napi_value fn; - napi_get_named_property(env, target, "on_item", &fn); - napi_value argv[2]; - napi_create_buffer_copy(env, f->item_len, f->item_ptr ? (const void*)f->item_ptr : (const void*)"", NULL, &argv[0]); - weaveffi_napi_make_handle(env, f->gadget, &argv[1]); - napi_value result; - napi_valuetype fn_type; - napi_typeof(env, fn, &fn_type); - if (fn_type != napi_function) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "ReadyListener implementation has no on_item method"); - } else if (napi_call_function(env, target, fn, 2, argv, &result) != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "ReadyListener.on_item threw"); - } else { - napi_status rs; - rs = napi_get_value_bool(env, result, &f->result); - if (rs != napi_ok) { - weaveffi_napi_cb_report(env, f->hdr.out_err, "ReadyListener.on_item returned a value of the wrong type"); - } - } - napi_close_handle_scope(env, scope); -} - -static bool weaveffi_kitchen_ReadyListener_on_item_tramp(void* ctx, const uint8_t* item_ptr, size_t item_len, weaveffi_kitchen_Gadget* gadget, weaveffi_error* out_err) { - weaveffi_kitchen_ReadyListener_on_item_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - f.item_ptr = item_ptr; - f.item_len = item_len; - f.gadget = gadget; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_kitchen_ReadyListener_on_item_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 1; - req.frame = &f; - weaveffi_napi_cb_hop(&req); - } - return f.result; -} - -static void weaveffi_kitchen_ReadyListener_napi_dispatch(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_napi_cb_req* req = (weaveffi_napi_cb_req*)data; - if (req->method < 0) { - weaveffi_napi_cb_release(env, req->ctx); - free(req); - return; - } - if (env == NULL) { - weaveffi_error_set(((weaveffi_napi_cb_frame_hdr*)req->frame)->out_err, -4, "Node environment is shutting down"); - } else { - switch (req->method) { - case 0: weaveffi_kitchen_ReadyListener_on_ready_invoke(env, req->ctx, (weaveffi_kitchen_ReadyListener_on_ready_frame*)req->frame); break; - case 1: weaveffi_kitchen_ReadyListener_on_item_invoke(env, req->ctx, (weaveffi_kitchen_ReadyListener_on_item_frame*)req->frame); break; - default: break; - } - } - weaveffi_napi_cb_finish(req); -} - -static const weaveffi_kitchen_ReadyListener_vtable weaveffi_kitchen_ReadyListener_napi_vtable = { weaveffi_kitchen_ReadyListener_on_ready_tramp, weaveffi_kitchen_ReadyListener_on_item_tramp, weaveffi_napi_cb_free }; - -static napi_value Napi_weaveffi_kitchen_Gadget_new(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); - napi_status arg_status = napi_ok; - int64_t id = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[0], &id); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - weaveffi_kitchen_Gadget* result = weaveffi_kitchen_Gadget_new(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; - weaveffi_napi_make_handle(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_Gadget_version(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}; - const char* result = weaveffi_kitchen_Gadget_version(&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_kitchen_Gadget_describe(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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - const char* result = weaveffi_kitchen_Gadget_describe((const weaveffi_kitchen_Gadget*)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_kitchen_Gadget_poke(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); - napi_status arg_status = napi_ok; - void* self_raw = NULL; - arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw); - if (arg_status == napi_ok && self_raw == NULL) { - napi_throw_type_error(env, NULL, "object used after close()"); - arg_status = napi_invalid_arg; - } - int32_t times = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_int32(env, args[1], ×); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - int32_t result = weaveffi_kitchen_Gadget_poke((const weaveffi_kitchen_Gadget*)self_raw, times, &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_kitchen_Gadget_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* self_raw = NULL; - if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) { - return NULL; - } - weaveffi_kitchen_Gadget* self = (weaveffi_kitchen_Gadget*)self_raw; - weaveffi_kitchen_Gadget_destroy(self); - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_Gadget_clone(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* self_raw = NULL; - if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) { - return NULL; - } - weaveffi_kitchen_Gadget* self = (weaveffi_kitchen_Gadget*)self_raw; - weaveffi_kitchen_Gadget* cloned = weaveffi_kitchen_Gadget_clone(self); - napi_value ret; - weaveffi_napi_make_handle(env, cloned, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_bool_id(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); - napi_status arg_status = napi_ok; - bool x = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_bool(env, args[0], &x); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - bool result = weaveffi_kitchen_bool_id(x, &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_kitchen_i32_id(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); - napi_status arg_status = napi_ok; - int32_t x = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_int32(env, args[0], &x); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - int32_t result = weaveffi_kitchen_i32_id(x, &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_kitchen_u32_id(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); - napi_status arg_status = napi_ok; - uint32_t x = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_uint32(env, args[0], &x); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - uint32_t result = weaveffi_kitchen_u32_id(x, &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_uint32(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_i64_id(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); - napi_status arg_status = napi_ok; - int64_t x = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[0], &x); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - int64_t result = weaveffi_kitchen_i64_id(x, &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_bigint_int64(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_f64_id(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); - napi_status arg_status = napi_ok; - double x = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_double(env, args[0], &x); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - double result = weaveffi_kitchen_f64_id(x, &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_double(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_echo_string(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 = 0; - 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_kitchen_echo_string(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 Napi_weaveffi_kitchen_echo_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); - void* b_raw = NULL; - size_t b_len = 0; - napi_get_buffer_info(env, args[0], &b_raw, &b_len); - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_kitchen_echo_bytes((const uint8_t*)b_raw, b_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_kitchen_subscribe(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); - napi_status arg_status = napi_ok; - napi_valuetype listener_type; - napi_typeof(env, args[0], &listener_type); - weaveffi_napi_cb_ctx* listener_ctx = NULL; - if (listener_type == napi_object || listener_type == napi_function) { - listener_ctx = weaveffi_napi_cb_register(env, args[0], "weaveffi_kitchen_ReadyListener", weaveffi_kitchen_ReadyListener_napi_dispatch); - } else { - napi_throw_type_error(env, NULL, "expected a ReadyListener implementation"); - arg_status = napi_object_expected; - } - if (arg_status != napi_ok) { - if (listener_ctx != NULL) weaveffi_napi_cb_release(env, listener_ctx); - return NULL; - } - weaveffi_error err = {0}; - int32_t result = weaveffi_kitchen_subscribe((void*)listener_ctx, &weaveffi_kitchen_ReadyListener_napi_vtable, &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_kitchen_maybe_gadget(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); - napi_status arg_status = napi_ok; - void* fallback_raw = NULL; - if (arg_status == napi_ok) arg_status = weaveffi_napi_get_handle(env, args[0], &fallback_raw); - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - weaveffi_kitchen_Gadget* result = weaveffi_kitchen_maybe_gadget((const weaveffi_kitchen_Gadget*)fallback_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; - weaveffi_napi_make_handle(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_gadgets(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}; - size_t out_len = 0; - const uint8_t* result = weaveffi_kitchen_gadgets(&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 void weaveffi_kitchen_stream_gadgets_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_kitchen_StreamGadgetsIterator_destroy((weaveffi_kitchen_StreamGadgetsIterator*)state->iter); - state->iter = NULL; - } - free(state); -} - -static napi_value Napi_weaveffi_kitchen_StreamGadgetsIterator_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_kitchen_Gadget* iter_item; - weaveffi_error iter_err = {0}; - if (!weaveffi_kitchen_StreamGadgetsIterator_next((weaveffi_kitchen_StreamGadgetsIterator*)state->iter, &iter_item, &iter_err)) { - weaveffi_kitchen_StreamGadgetsIterator_destroy((weaveffi_kitchen_StreamGadgetsIterator*)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; - } - weaveffi_napi_make_handle(env, iter_item, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_StreamGadgetsIterator_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_kitchen_StreamGadgetsIterator_destroy((weaveffi_kitchen_StreamGadgetsIterator*)state->iter); - state->iter = NULL; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_stream_gadgets(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_kitchen_stream_gadgets(&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_kitchen_stream_gadgets_napi_iter_finalize, NULL, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_maybe_item(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); - napi_status arg_status = napi_ok; - int64_t id = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[0], &id); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_kitchen_maybe_item(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_kitchen_list_items(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}; - size_t out_len = 0; - const uint8_t* result = weaveffi_kitchen_list_items(&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_kitchen_get_attrs(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}; - size_t out_len = 0; - const uint8_t* result = weaveffi_kitchen_get_attrs(&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 void weaveffi_kitchen_stream_items_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_kitchen_StreamItemsIterator_destroy((weaveffi_kitchen_StreamItemsIterator*)state->iter); - state->iter = NULL; - } - free(state); -} - -static napi_value Napi_weaveffi_kitchen_StreamItemsIterator_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 uint8_t* iter_item; - size_t iter_item_len = 0; - weaveffi_error iter_err = {0}; - if (!weaveffi_kitchen_StreamItemsIterator_next((weaveffi_kitchen_StreamItemsIterator*)state->iter, &iter_item, &iter_item_len, &iter_err)) { - weaveffi_kitchen_StreamItemsIterator_destroy((weaveffi_kitchen_StreamItemsIterator*)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_buffer_copy(env, iter_item_len, iter_item, NULL, &ret); - weaveffi_free_bytes((uint8_t*)iter_item, iter_item_len); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_StreamItemsIterator_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_kitchen_StreamItemsIterator_destroy((weaveffi_kitchen_StreamItemsIterator*)state->iter); - state->iter = NULL; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_stream_items(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_kitchen_stream_items(&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_kitchen_stream_items_napi_iter_finalize, NULL, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kitchen_cross_module_token(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}; - size_t out_len = 0; - const uint8_t* result = weaveffi_kitchen_cross_module_token(&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; -} - -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_kitchen_do_async_napi_actx; - -static void weaveffi_kitchen_do_async_napi_cb(void* context, weaveffi_error* err, const char* result) { - weaveffi_kitchen_do_async_napi_actx* ctx = (weaveffi_kitchen_do_async_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_kitchen_do_async_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_kitchen_do_async_napi_actx* ctx = (weaveffi_kitchen_do_async_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_kitchen_do_async(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 input_len = 0; - napi_get_value_string_utf8(env, args[0], NULL, 0, &input_len); - char* input = (char*)malloc(input_len + 1); - napi_get_value_string_utf8(env, args[0], input, input_len + 1, &input_len); - weaveffi_kitchen_do_async_napi_actx* ctx = (weaveffi_kitchen_do_async_napi_actx*)calloc(1, sizeof(weaveffi_kitchen_do_async_napi_actx)); - napi_value promise; - napi_create_promise(env, &ctx->deferred, &promise); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_kitchen_do_async", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_kitchen_do_async_napi_settle, &ctx->tsfn); - weaveffi_kitchen_do_async_async(input, weaveffi_kitchen_do_async_napi_cb, ctx); - free(input); - 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_kitchen_do_cancellable_napi_actx; - -static void weaveffi_kitchen_do_cancellable_napi_cb(void* context, weaveffi_error* err, const char* result) { - weaveffi_kitchen_do_cancellable_napi_actx* ctx = (weaveffi_kitchen_do_cancellable_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_kitchen_do_cancellable_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_kitchen_do_cancellable_napi_actx* ctx = (weaveffi_kitchen_do_cancellable_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_kitchen_do_cancellable(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 input_len = 0; - napi_get_value_string_utf8(env, args[0], NULL, 0, &input_len); - char* input = (char*)malloc(input_len + 1); - napi_get_value_string_utf8(env, args[0], input, input_len + 1, &input_len); - weaveffi_kitchen_do_cancellable_napi_actx* ctx = (weaveffi_kitchen_do_cancellable_napi_actx*)calloc(1, sizeof(weaveffi_kitchen_do_cancellable_napi_actx)); - napi_value promise; - napi_create_promise(env, &ctx->deferred, &promise); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_kitchen_do_cancellable", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_kitchen_do_cancellable_napi_settle, &ctx->tsfn); - weaveffi_kitchen_do_cancellable_async(input, NULL, weaveffi_kitchen_do_cancellable_napi_cb, ctx); - free(input); - return promise; -} - -static napi_value Napi_weaveffi_kitchen_legacy_op(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}; - int32_t result = weaveffi_kitchen_legacy_op(&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_kitchen_new_op(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}; - int32_t result = weaveffi_kitchen_new_op(&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_kitchen_nested_hello(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}; - const char* result = weaveffi_kitchen_nested_hello(&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 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; - } - } - weaveffi_napi_js_thread = uv_thread_self(); - 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 }, - { "Gadget_version", NULL, Napi_weaveffi_kitchen_Gadget_version, NULL, NULL, NULL, napi_default, NULL }, - { "Gadget_describe", NULL, Napi_weaveffi_kitchen_Gadget_describe, NULL, NULL, NULL, napi_default, NULL }, - { "Gadget_poke", NULL, Napi_weaveffi_kitchen_Gadget_poke, NULL, NULL, NULL, napi_default, NULL }, - { "Gadget__destroy", NULL, Napi_weaveffi_kitchen_Gadget_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "Gadget__clone", NULL, Napi_weaveffi_kitchen_Gadget_clone, NULL, NULL, NULL, napi_default, NULL }, - { "boolId", NULL, Napi_weaveffi_kitchen_bool_id, NULL, NULL, NULL, napi_default, NULL }, - { "i32Id", NULL, Napi_weaveffi_kitchen_i32_id, NULL, NULL, NULL, napi_default, NULL }, - { "u32Id", NULL, Napi_weaveffi_kitchen_u32_id, NULL, NULL, NULL, napi_default, NULL }, - { "i64Id", NULL, Napi_weaveffi_kitchen_i64_id, NULL, NULL, NULL, napi_default, NULL }, - { "f64Id", NULL, Napi_weaveffi_kitchen_f64_id, NULL, NULL, NULL, napi_default, NULL }, - { "echoString", NULL, Napi_weaveffi_kitchen_echo_string, NULL, NULL, NULL, napi_default, NULL }, - { "echoBytes", NULL, Napi_weaveffi_kitchen_echo_bytes, NULL, NULL, NULL, napi_default, NULL }, - { "subscribe", NULL, Napi_weaveffi_kitchen_subscribe, NULL, NULL, NULL, napi_default, NULL }, - { "maybeGadget", NULL, Napi_weaveffi_kitchen_maybe_gadget, NULL, NULL, NULL, napi_default, NULL }, - { "gadgets", NULL, Napi_weaveffi_kitchen_gadgets, NULL, NULL, NULL, napi_default, NULL }, - { "streamGadgets_iterNext", NULL, Napi_weaveffi_kitchen_StreamGadgetsIterator_next, NULL, NULL, NULL, napi_default, NULL }, - { "streamGadgets_iterDestroy", NULL, Napi_weaveffi_kitchen_StreamGadgetsIterator_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "streamGadgets", NULL, Napi_weaveffi_kitchen_stream_gadgets, NULL, NULL, NULL, napi_default, NULL }, - { "maybeItem", NULL, Napi_weaveffi_kitchen_maybe_item, NULL, NULL, NULL, napi_default, NULL }, - { "listItems", NULL, Napi_weaveffi_kitchen_list_items, NULL, NULL, NULL, napi_default, NULL }, - { "getAttrs", NULL, Napi_weaveffi_kitchen_get_attrs, NULL, NULL, NULL, napi_default, NULL }, - { "streamItems_iterNext", NULL, Napi_weaveffi_kitchen_StreamItemsIterator_next, NULL, NULL, NULL, napi_default, NULL }, - { "streamItems_iterDestroy", NULL, Napi_weaveffi_kitchen_StreamItemsIterator_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "streamItems", NULL, Napi_weaveffi_kitchen_stream_items, NULL, NULL, NULL, napi_default, NULL }, - { "crossModuleToken", NULL, Napi_weaveffi_kitchen_cross_module_token, NULL, NULL, NULL, napi_default, NULL }, - { "doAsync", NULL, Napi_weaveffi_kitchen_do_async, NULL, NULL, NULL, napi_default, NULL }, - { "doCancellable", NULL, Napi_weaveffi_kitchen_do_cancellable, NULL, NULL, NULL, napi_default, NULL }, - { "legacyOp", NULL, Napi_weaveffi_kitchen_legacy_op, NULL, NULL, NULL, napi_default, NULL }, - { "newOp", NULL, Napi_weaveffi_kitchen_new_op, NULL, NULL, NULL, napi_default, NULL }, - { "hello", NULL, Napi_weaveffi_kitchen_nested_hello, NULL, NULL, NULL, napi_default, NULL }, - }; - napi_define_properties(env, exports, 32, 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__README_md.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__README_md.snap new file mode 100644 index 00000000..152f77c4 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__README_md.snap @@ -0,0 +1,24 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + +# nested_modules + +Native bindings for nested_modules + +Node.js bindings for the `nested_modules` native library: an ES module over a small N-API addon (`nested_modules_node.c`) that `npm install` compiles with node-gyp. The addon links `nested_modules`; set `NESTED_MODULES_LIBRARY` to the library file (or its directory) when installing, or pass `--nested_modules-library=` to npm. + +```sh +NESTED_MODULES_LIBRARY=/path/to/libnested_modules.so npm install nested_modules +``` + +Each top-level module of the API is a namespace export: + +```js +import { graphics } from 'nested_modules'; +``` + + diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__binding_gyp.snap index 3997b5bb..f9dd2600 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__binding_gyp.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__binding_gyp.snap @@ -1,17 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o { + "variables": { + "native_dir%": " { const path = require('path'); const p = process.env.NESTED_MODULES_LIBRARY || process.env.npm_config_nested_modules_library; if (!p) return path.resolve('.'); return require('fs').statSync(p).isFile() ? path.dirname(path.resolve(p)) : path.resolve(p); })()\")" + }, "targets": [ { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] + "target_name": "nested_modules_node", + "sources": [ + "nested_modules_node.c" + ], + "include_dirs": [ + "." + ], + "library_dirs": [ + "<(native_dir)" + ], + "libraries": [ + "-lnested_modules" + ], + "conditions": [ + ["OS=='mac'", { "xcode_settings": { "OTHER_LDFLAGS": ["-Wl,-rpath,@loader_path", "-Wl,-rpath,<(native_dir)"] } }], + ["OS=='linux'", { "ldflags": ["-Wl,-rpath,'$$ORIGIN'", "-Wl,-rpath,<(native_dir)"] }] + ] } ] } diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_d_ts.snap new file mode 100644 index 00000000..9df384d2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_d_ts.snap @@ -0,0 +1,86 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class NestedModulesError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends NestedModulesError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof NestedModulesError; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** Construct a Canvas. */ +export declare namespace graphics { + /** The errors of the `graphics` error domain. */ + export class GraphicsError extends $Error { + constructor(code: number, message?: string); + } + /** coordinate out of bounds */ + export class OutOfBoundsError extends graphics.GraphicsError { + static readonly CODE: 1; + constructor(message?: string); + } + /** A measurement unit shared with nested submodules. */ + export enum Unit { + pixels = 0, + points = 1, + } + /** Top-level struct that references a struct defined one level deeper. */ + export interface Canvas { + width: number; + /** Reference down into a nested module */ + root: graphics.shapes.Shape; + } + /** Construct a Canvas. */ + export function newCanvas(width: number): graphics.Canvas; + /** Construct a Shape. */ + export namespace shapes { + /** A struct one level deep that references both an ancestor enum and a deeper struct. */ + export interface Shape { + sides: number; + /** Reference up to the parent module's enum */ + unit: graphics.Unit; + /** Reference down into a deeper module */ + detail: graphics.shapes.geometry.Detail; + } + /** Construct a Shape. */ + export function makeShape(sides: number): graphics.shapes.Shape; + /** Nested function returning a struct owned by an ancestor module. */ + export function describe(): graphics.Canvas; + /** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ + export namespace geometry { + /** A struct two levels deep. */ + export interface Detail { + area: number; + } + /** + * Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. + * @throws {graphics.GraphicsError} + */ + export function compute(shape: graphics.shapes.Shape): graphics.shapes.geometry.Detail; + } + } +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_js.snap index 3be4f1fc..b9a47871 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__index_js.snap @@ -1,260 +1,167 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.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]; -} +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + NestedModulesError as $Error, +} from './runtime.js'; +export { NestedModulesError, CancelledError } from './runtime.js'; -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); - }); -} +import { createRequire } from 'node:module'; -// --- 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'); +// The native entry points: the N-API addon `npm install` builds from +// nested_modules_node.c (with node-gyp), or a prebuilt copy next to this file. +function $loadAddon(name) { + const require = createRequire(import.meta.url); + const tried = []; + for (const dir of ['./build/Release/', './build/Debug/', './']) { try { - return __utf8.decode(this._buf.subarray(at, at + n)); + return require(dir + name); } catch (e) { - throw __bufferError('string is not valid UTF-8'); + if (e.code !== 'MODULE_NOT_FOUND') throw e; + tried.push(dir + name); } } - 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; + throw new Error( + `the native addon ${name} was not found (tried ${tried.join(', ')}); ` + + 'build it with `npm install` or `node-gyp rebuild` in this package', + ); } -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]); } + +const $raw = $loadAddon('nested_modules_node.node'); +$raw.$setup($Fault); + +// Object tokens in value buffers are already bigint handles here. +const $token = (t) => t; + +$verify($raw, 'nested_modules', 'nested_modules', 3, [['graphics', 0x44d31b5b10fdb90dn]]); + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.nested_modules_debug_live(kind); } -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); + +const graphics$GraphicsError = class GraphicsError extends $Error {}; +const graphics$OutOfBoundsError = class OutOfBoundsError extends graphics$GraphicsError { + static CODE = 1; + constructor(message = 'coordinate out of bounds') { + super(1, message); } - return out; +}; +const $codes$graphics$GraphicsError = new Map([[1, graphics$OutOfBoundsError]]); +function $from$graphics$GraphicsError(e) { + return $domain(e, $codes$graphics$GraphicsError); } -function __packCanvas(w, v) { - w.i32(v.width); - __packShape(w, v.root); +const graphics$Unit = Object.freeze({ + pixels: 0, + points: 1, + 0: 'pixels', + 1: 'points', +}); + +function $w$graphics$Canvas(w, v) { + w.writeI32(v.width); + $w$graphics$shapes$Shape(w, v.root); } -function __unpackCanvas(r) { +function $r$graphics$Canvas(r) { return { - width: r.i32(), - root: __unpackShape(r), + width: r.readI32(), + root: $r$graphics$shapes$Shape(r), }; } - -function __packShape(w, v) { - w.i32(v.sides); - w.i32(v.unit); - __packDetail(w, v.detail); +function $w$graphics$shapes$Shape(w, v) { + w.writeI32(v.sides); + w.writeI32(v.unit); + $w$graphics$shapes$geometry$Detail(w, v.detail); } -function __unpackShape(r) { +function $r$graphics$shapes$Shape(r) { return { - sides: r.i32(), - unit: r.i32(), - detail: __unpackDetail(r), + sides: r.readI32(), + unit: r.readI32(), + detail: $r$graphics$shapes$geometry$Detail(r), }; } - -function __packDetail(w, v) { - w.f64(v.area); +function $w$graphics$shapes$geometry$Detail(w, v) { + w.writeF64(v.area); } -function __unpackDetail(r) { +function $r$graphics$shapes$geometry$Detail(r) { return { - area: r.f64(), + area: r.readF64(), }; } -class GraphicsError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'GraphicsError'; - } -} -wv.GraphicsError = GraphicsError; -class OutOfBoundsError extends GraphicsError { - constructor(message) { - super(1, message || 'coordinate out of bounds'); - this.name = 'OutOfBoundsError'; +const graphics$newCanvas = function newCanvas(width) { + try { + return $decode($raw.nested_modules_graphics_new_canvas(width), $r$graphics$Canvas); + } catch ($e) { + throw $fault($e); } -} -OutOfBoundsError.CODE = 1; -wv.OutOfBoundsError = OutOfBoundsError; -const __graphicsErrorCodes = Object.freeze({ 1: OutOfBoundsError }); -function __graphicsErrorFrom(code, message, payload) { - const _cls = __graphicsErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; -} - -wv.Unit = Object.freeze({ - pixels: 0, - points: 1, - 0: 'pixels', - 1: 'points', -}); -wv.newCanvas = function (width) { - const _r = __invoke(addon.newCanvas, [width], __generic); - return __decode(__unpackCanvas, _r); }; -wv.makeShape = function (sides) { - const _r = __invoke(addon.makeShape, [sides], __generic); - return __decode(__unpackShape, _r); + +const graphics$shapes$makeShape = function makeShape(sides) { + try { + return $decode($raw.nested_modules_graphics_shapes_make_shape(sides), $r$graphics$shapes$Shape); + } catch ($e) { + throw $fault($e); + } }; -wv.describe = function () { - const _r = __invoke(addon.describe, [], __generic); - return __decode(__unpackCanvas, _r); + +const graphics$shapes$describe = function describe() { + try { + return $decode($raw.nested_modules_graphics_shapes_describe(), $r$graphics$Canvas); + } catch ($e) { + throw $fault($e); + } }; -wv.compute = function (shape) { - const _r = __invoke(addon.compute, [__encode(__packShape, shape)], __graphicsErrorFrom); - return __decode(__unpackDetail, _r); + +const graphics$shapes$geometry$compute = function compute(shape) { + try { + return $decode($raw.nested_modules_graphics_shapes_geometry_compute($encode(shape, $w$graphics$shapes$Shape)), $r$graphics$shapes$geometry$Detail); + } catch ($e) { + throw $from$graphics$GraphicsError($e); + } }; -module.exports = wv; +export const graphics = Object.freeze({ + GraphicsError: graphics$GraphicsError, + OutOfBoundsError: graphics$OutOfBoundsError, + Unit: graphics$Unit, + newCanvas: graphics$newCanvas, + shapes: Object.freeze({ + makeShape: graphics$shapes$makeShape, + describe: graphics$shapes$describe, + geometry: Object.freeze({ + compute: graphics$shapes$geometry$compute, + }), + }), +}); // END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_h.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_h.snap new file mode 100644 index 00000000..27c97f2b --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_h.snap @@ -0,0 +1,144 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NESTED_MODULES_H +#define NESTED_MODULES_H + +#include +#include +#include + +#ifndef NESTED_MODULES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef NESTED_MODULES_BUILD +# define NESTED_MODULES_API __declspec(dllexport) +# else +# define NESTED_MODULES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define NESTED_MODULES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define NESTED_MODULES_API __attribute__((visibility("default"))) +# else +# define NESTED_MODULES_API +# endif +#endif + +#ifndef NESTED_MODULES_DEPRECATED +# if defined(_MSC_VER) +# define NESTED_MODULES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define NESTED_MODULES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define NESTED_MODULES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports nested_modules_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define NESTED_MODULES_ABI_VERSION 3u +NESTED_MODULES_API uint32_t nested_modules_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by nested_modules_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct nested_modules_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} nested_modules_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +NESTED_MODULES_API void nested_modules_error_set(nested_modules_error* err, int32_t code, const char* message); +NESTED_MODULES_API void nested_modules_error_clear(nested_modules_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + nested_modules_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +NESTED_MODULES_API void nested_modules_error_free(nested_modules_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +NESTED_MODULES_API void nested_modules_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct nested_modules_cancel_token nested_modules_cancel_token; +NESTED_MODULES_API nested_modules_cancel_token* nested_modules_cancel_token_create(void); +NESTED_MODULES_API void nested_modules_cancel_token_cancel(nested_modules_cancel_token* token); +NESTED_MODULES_API bool nested_modules_cancel_token_is_cancelled(const nested_modules_cancel_token* token); +NESTED_MODULES_API void nested_modules_cancel_token_destroy(nested_modules_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +NESTED_MODULES_API uint8_t* nested_modules_alloc(uint32_t size); +NESTED_MODULES_API void nested_modules_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +NESTED_MODULES_API uint64_t nested_modules_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define NESTED_MODULES_GRAPHICS_CHECKSUM 0x44d31b5b10fdb90dull +NESTED_MODULES_API uint64_t nested_modules_graphics_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with nested_modules_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * nested_modules_buffer.h declares a C struct and codec for each. + */ + +/** A measurement unit shared with nested submodules. */ +typedef enum { nested_modules_graphics_Unit_pixels = 0, nested_modules_graphics_Unit_points = 1 } nested_modules_graphics_Unit; +/** Error codes reported by throwing functions in the `graphics` module tree. */ +typedef enum { nested_modules_graphics_GraphicsError_OutOfBounds = 1 } nested_modules_graphics_GraphicsError; + +// Module: graphics +/** Construct a Canvas. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_new_canvas(int32_t width, size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes +/** Construct a Shape. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_make_shape(int32_t sides, size_t* out_len, nested_modules_error* out_err); +/** Nested function returning a struct owned by an ancestor module. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_describe(size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes_geometry +/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, nested_modules_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // NESTED_MODULES_H + +// END nested_modules.h diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_node_c.snap new file mode 100644 index 00000000..789f9ee7 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__nested_modules_node_c.snap @@ -0,0 +1,711 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NAPI_VERSION +#define NAPI_VERSION 8 +#endif +#include +#include +#include +#include +#include +#include + +/* The addon wraps deprecated functions too; the JavaScript API carries the + * deprecation instead. */ +#define NESTED_MODULES_DEPRECATED(msg) +#include "nested_modules.h" + +/* + * The N-API transport of the JavaScript bindings. Every C symbol of the + * library is exported to `index.js` under its own name and called with the + * raw convention the shared JavaScript layer documents: direct values as + * numbers, bigints, and booleans; strings as strings; bytes and value + * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint + * handles (null when absent); callback interfaces as adapter objects. A + * failure throws an instance of the runtime's `$Fault` class. + */ + +typedef nested_modules_error js_error; + +/* Helpers an API may not need. */ +#if defined(__GNUC__) || defined(__clang__) +#define JS_HELPER static inline __attribute__((unused)) +#else +#define JS_HELPER static inline +#endif + +/* Per-environment state (one per main thread or worker): the `$Fault` + * constructor `index.js` registers through `$setup`. */ +typedef struct { + napi_ref fault; +} js_env; + +static void js_env_free(napi_env env, void* data, void* hint) { + (void)hint; + js_env* e = (js_env*)data; + if (e->fault != NULL) napi_delete_reference(env, e->fault); + free(e); +} + +JS_HELPER napi_value js_undefined(napi_env env) { + napi_value v; + napi_get_undefined(env, &v); + return v; +} + +static napi_value js_setup(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e->fault != NULL) napi_delete_reference(env, e->fault); + napi_create_reference(env, argv[0], 1, &e->fault); + return js_undefined(env); +} + +/* --------------------------------------------------------------------------- + * Value conversions + * ------------------------------------------------------------------------- */ + +JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value ab, out; + void* data = NULL; + napi_create_arraybuffer(env, len, &data, &ab); + if (len > 0) memcpy(data, ptr, len); + napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); + return out; +} + +JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out; + napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); + return out; +} + +JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { + napi_value out; + napi_create_int32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { + napi_value out; + napi_create_uint32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { + napi_value out; + napi_create_bigint_int64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { + napi_value out; + napi_create_bigint_uint64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_f64(napi_env env, double v) { + napi_value out; + napi_create_double(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_bool(napi_env env, bool v) { + napi_value out; + napi_get_boolean(env, v, &out); + return out; +} + +/* An object, iterator, or cancel token handle: the pointer as a bigint, or + * null for NULL. */ +JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { + napi_value out; + if (p == NULL) { + napi_get_null(env, &out); + } else { + napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); + } + return out; +} + +/* Returned strings and buffers: convert, then release the producer's copy. */ +JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_str(env, ptr, len); + if (ptr != NULL) nested_modules_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_bytes(env, ptr, len); + if (ptr != NULL) nested_modules_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, + const uint8_t* payload, size_t payload_len) { + napi_value argv[3], ctor, out = NULL; + napi_create_int32(env, code, &argv[0]); + napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); + if (payload != NULL) { + argv[2] = js_new_bytes(env, payload, payload_len); + } else { + napi_get_null(env, &argv[2]); + } + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok || + ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { + napi_create_error(env, NULL, argv[1], &out); + napi_set_named_property(env, out, "code", argv[0]); + } + return out; +} + +/* Throw the error a call reported, then release it. */ +JS_HELPER napi_value js_throw(napi_env env, js_error* err) { + napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); + nested_modules_error_clear(err); + return NULL; +} + +JS_HELPER bool js_type_error(napi_env env, const char* expected) { + bool pending = false; + napi_is_exception_pending(env, &pending); + if (!pending) napi_throw_type_error(env, NULL, expected); + return false; +} + +JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { + return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { + return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int8_t)t; + return true; +} + +JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int16_t)t; + return true; +} + +JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint8_t)t; + return true; +} + +JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint16_t)t; + return true; +} + +/* 64-bit integers: a bigint that fits, or an integral number. */ +JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_int64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { + *out = (int64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_uint64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && + d == (double)(uint64_t)d) { + *out = (uint64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { + return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { + double d = 0; + if (!js_arg_f64(env, v, &d)) return false; + *out = (float)d; + return true; +} + +JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { + return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); +} + +/* A handle argument; null or undefined is NULL when `nullable`. */ +JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_null || t == napi_undefined) { + *out = NULL; + return nullable || js_type_error(env, "expected an object handle"); + } + uint64_t raw = 0; + bool lossless = false; + if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || + raw == 0) { + return js_type_error(env, "expected an object handle"); + } + *out = (void*)(uintptr_t)raw; + return true; +} + +/* A borrowed byte run: a Uint8Array (or Buffer). */ +JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { + bool is_typed = false; + napi_is_typedarray(env, v, &is_typed); + if (is_typed) { + napi_typedarray_type type; + void* data = NULL; + napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); + if (type == napi_uint8_array || type == napi_uint8_clamped_array) { + *ptr = (const uint8_t*)data; + return true; + } + } + return js_type_error(env, "expected a Uint8Array"); +} + +/* A string argument encoded as UTF-8: short strings live in the inline + * buffer, longer ones on the heap until js_str_free. */ +typedef struct { + char* ptr; + size_t len; + char inline_buf[256]; +} js_str; + +#define JS_STR_INIT {NULL, 0, {0}} + +JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { + size_t len = 0; + if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { + return js_type_error(env, "expected a string"); + } + s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); + napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); + return true; +} + +JS_HELPER void js_str_free(js_str* s) { + if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); + s->ptr = NULL; +} + +#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) + +/* Read up to `n` arguments; missing ones are undefined. */ +#define JS_ARGS(n) \ + size_t argc = (n); \ + napi_value argv[(n) + 1]; \ + napi_get_cb_info(env, info, &argc, argv, NULL, NULL) + +/* --------------------------------------------------------------------------- + * Async calls: the completion may run on any thread, so it only records the + * result and queues the settlement on the JavaScript thread through a + * thread-safe function (which also keeps the event loop alive meanwhile). + * ------------------------------------------------------------------------- */ + +typedef enum { + JS_R_VOID, + JS_R_I32, + JS_R_U32, + JS_R_I64, + JS_R_U64, + JS_R_F64, + JS_R_BOOL, + JS_R_STR, + JS_R_BYTES, + JS_R_HANDLE +} js_kind; + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + js_kind kind; + js_error* err; + union { + int64_t i; + uint64_t u; + double f; + bool b; + const void* p; + } v; + size_t len; +} js_async; + +JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { + (void)cb; + (void)context; + js_async* a = (js_async*)data; + bool failed = a->err != NULL && a->err->code != 0; + if (env != NULL && failed) { + napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, + a->err->payload_ptr, a->err->payload_len)); + } else if (env != NULL) { + napi_value v = NULL; + switch (a->kind) { + case JS_R_VOID: v = js_undefined(env); break; + case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; + case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; + case JS_R_I64: v = js_new_i64(env, a->v.i); break; + case JS_R_U64: v = js_new_u64(env, a->v.u); break; + case JS_R_F64: v = js_new_f64(env, a->v.f); break; + case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; + case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; + } + napi_resolve_deferred(env, a->deferred, v); + } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { + /* The environment is shutting down: just release the result. */ + nested_modules_free_bytes((uint8_t*)a->v.p, a->len); + } + nested_modules_error_free(a->err); + napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); + free(a); +} + +/* Create the promise and its settlement queue for one async call. */ +JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { + js_async* a = (js_async*)calloc(1, sizeof *a); + a->kind = kind; + napi_create_promise(env, &a->deferred, promise); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, + js_async_settle, &a->tsfn); + return a; +} + +/* Called by each completion once it has recorded its result. */ +JS_HELPER void js_async_done(js_async* a, js_error* err) { + a->err = err; + napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); +} + +/* --------------------------------------------------------------------------- + * Callback interfaces. One registration per implementation passed to the + * producer: a reference to the adapter object, the JavaScript thread it + * belongs to, and a thread-safe function that runs calls made from any other + * thread on that thread while the caller waits. + * ------------------------------------------------------------------------- */ + +typedef struct { + napi_env env; + napi_ref ref; + napi_threadsafe_function tsfn; + uv_thread_t js_thread; +} js_cb; + +/* Every callback frame starts with this header so the dispatcher can reach + * `out_err` without knowing the method. */ +typedef struct { + js_error* out_err; +} js_cb_frame; + +/* A call queued from another thread: `method` is the vtable index, or -1 to + * release the registration. */ +typedef struct { + js_cb* cb; + int method; + void* frame; + uv_mutex_t mu; + uv_cond_t cv; + bool done; +} js_cb_req; + +JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { + uv_thread_t self = uv_thread_self(); + return uv_thread_equal(&self, &cb->js_thread) != 0; +} + +JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, + napi_threadsafe_function_call_js dispatch) { + js_cb* cb = (js_cb*)calloc(1, sizeof *cb); + cb->env = env; + cb->js_thread = uv_thread_self(); + napi_create_reference(env, adapter, 1, &cb->ref); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, + &cb->tsfn); + /* A live implementation must not keep the process alive by itself. */ + napi_unref_threadsafe_function(env, cb->tsfn); + return cb; +} + +JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { + if (env != NULL) napi_delete_reference(env, cb->ref); + napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); + free(cb); +} + +/* A callback-interface argument: the adapter object `index.js` built. */ +JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, + napi_threadsafe_function_call_js dispatch, js_cb** out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); + *out = js_cb_register(env, v, name, dispatch); + return true; +} + +/* The vtable's `free`: the producer is done with the implementation. The + * reference can only be deleted on its own thread. */ +JS_HELPER void js_cb_free(void* ctx) { + js_cb* cb = (js_cb*)ctx; + if (cb == NULL) return; + if (js_cb_on_js_thread(cb)) { + js_cb_release(cb->env, cb); + return; + } + js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); + req->cb = cb; + req->method = -1; + if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); +} + +/* Run `req` on the JavaScript thread and wait for it. */ +JS_HELPER void js_cb_hop(js_cb_req* req) { + uv_mutex_init(&req->mu); + uv_cond_init(&req->cv); + req->done = false; + if (napi_call_threadsafe_function(req->cb->tsfn, req, napi_tsfn_blocking) == napi_ok) { + uv_mutex_lock(&req->mu); + while (!req->done) uv_cond_wait(&req->cv, &req->mu); + uv_mutex_unlock(&req->mu); + } else { + nested_modules_error_set(((js_cb_frame*)req->frame)->out_err, -4, + "the callback implementation is no longer reachable"); + } + uv_cond_destroy(&req->cv); + uv_mutex_destroy(&req->mu); +} + +JS_HELPER void js_cb_finish(js_cb_req* req) { + uv_mutex_lock(&req->mu); + req->done = true; + uv_cond_signal(&req->cv); + uv_mutex_unlock(&req->mu); +} + +/* Report the pending JavaScript exception (or `fallback`) through `out_err` + * as a foreign failure (code -4); nothing unwinds through the C frame. */ +JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { + char msg[512]; + msg[0] = 0; + bool pending = false; + napi_is_exception_pending(env, &pending); + if (pending) { + napi_value exc, text, str; + napi_get_and_clear_last_exception(env, &exc); + napi_valuetype t; + napi_typeof(env, exc, &t); + text = exc; + if (t == napi_object) napi_get_named_property(env, exc, "message", &text); + if (napi_coerce_to_string(env, text, &str) == napi_ok) { + napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); + } + napi_is_exception_pending(env, &pending); + if (pending) napi_get_and_clear_last_exception(env, &exc); + } + nested_modules_error_set(out_err, -4, msg[0] != 0 ? msg : fallback); +} + +/* Look up and call `method` on the adapter; false when it threw. */ +JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, + const napi_value* argv, napi_value* result) { + napi_value adapter, fn; + napi_get_reference_value(env, cb->ref, &adapter); + napi_get_named_property(env, adapter, method, &fn); + return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; +} + +/* --------------------------------------------------------------------------- + * The runtime symbols every library exports. + * ------------------------------------------------------------------------- */ + +static napi_value js_abi_version(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u32(env, nested_modules_abi_version()); +} + +static napi_value js_debug_live(napi_env env, napi_callback_info info) { + JS_ARGS(1); + int32_t kind = 0; + if (!js_arg_i32(env, argv[0], &kind)) return NULL; + return js_new_u64(env, nested_modules_debug_live(kind)); +} + +static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { + (void)info; + return js_new_handle(env, nested_modules_cancel_token_create()); +} + +static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + nested_modules_cancel_token_cancel((nested_modules_cancel_token*)token); + return js_undefined(env); +} + +static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + nested_modules_cancel_token_destroy((nested_modules_cancel_token*)token); + return js_undefined(env); +} + +#define JS_RUNTIME_EXPORTS \ + {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ + {"nested_modules_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ + {"nested_modules_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ + {"nested_modules_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"nested_modules_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"nested_modules_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ + napi_default, NULL} + +static napi_value nx_nested_modules_graphics_checksum(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u64(env, nested_modules_graphics_checksum()); +} + +static napi_value nx_nested_modules_graphics_new_canvas(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int32_t a0 = 0; + if (!js_arg_i32(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = nested_modules_graphics_new_canvas(a0, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_nested_modules_graphics_shapes_make_shape(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + int32_t a0 = 0; + if (!js_arg_i32(env, argv[0], &a0)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = nested_modules_graphics_shapes_make_shape(a0, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_nested_modules_graphics_shapes_describe(napi_env env, napi_callback_info info) { + JS_ARGS(0); + napi_value ret = NULL; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = nested_modules_graphics_shapes_describe(&out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_nested_modules_graphics_shapes_geometry_compute(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = nested_modules_graphics_shapes_geometry_compute(a0, a0_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +NAPI_MODULE_INIT() { + js_env* e = (js_env*)calloc(1, sizeof *e); + napi_set_instance_data(env, e, js_env_free, NULL); + napi_property_descriptor props[] = { + JS_RUNTIME_EXPORTS, + {"nested_modules_graphics_checksum", NULL, nx_nested_modules_graphics_checksum, NULL, NULL, NULL, napi_default, NULL}, + {"nested_modules_graphics_new_canvas", NULL, nx_nested_modules_graphics_new_canvas, NULL, NULL, NULL, napi_default, NULL}, + {"nested_modules_graphics_shapes_make_shape", NULL, nx_nested_modules_graphics_shapes_make_shape, NULL, NULL, NULL, napi_default, NULL}, + {"nested_modules_graphics_shapes_describe", NULL, nx_nested_modules_graphics_shapes_describe, NULL, NULL, NULL, napi_default, NULL}, + {"nested_modules_graphics_shapes_geometry_compute", NULL, nx_nested_modules_graphics_shapes_geometry_compute, NULL, NULL, NULL, napi_default, NULL}, + }; + napi_define_properties(env, exports, sizeof props / sizeof props[0], props); + return exports; +} + +// END nested_modules_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__package_json.snap index 6fd366d2..9ed96de0 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__package_json.snap @@ -2,16 +2,34 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "nested_modules", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for nested_modules", + "type": "module", "main": "index.js", - "types": "types.d.ts", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "binding.gyp", + "nested_modules_node.c", + "nested_modules.h" + ], "gypfile": true, "scripts": { "install": "node-gyp rebuild" + }, + "engines": { + "node": ">=18" } } diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__runtime_js.snap new file mode 100644 index 00000000..97a25292 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class NestedModulesError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends NestedModulesError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new NestedModulesError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new NestedModulesError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new NestedModulesError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new NestedModulesError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__types_d_ts.snap deleted file mode 100644 index 3c88f26a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__types_d_ts.snap +++ /dev/null @@ -1,79 +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 runtime traps (code -2 a - * producer panic, -3 a marshalling failure, -4 a callback interface - * implementation that threw); throwing functions surface a module - * domain subclass for their declared codes. - */ -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 `graphics` module's throwing functions. */ -export class GraphicsError extends WeaveFFIError { - constructor(code: number, message: string); -} -export class OutOfBoundsError extends GraphicsError { - static readonly CODE: 1; - constructor(message?: string); -} -/** Top-level struct that references a struct defined one level deeper. */ -export interface Canvas { - width: number; - /** Reference down into a nested module */ - root: Shape; -} -/** A measurement unit shared with nested submodules. */ -export enum Unit { - pixels = 0, - points = 1, -} -// module graphics -/** - * Construct a Canvas. - * Maps to C function: weaveffi_graphics_new_canvas - */ -export function newCanvas(width: number): Canvas -/** A struct one level deep that references both an ancestor enum and a deeper struct. */ -export interface Shape { - sides: number; - /** Reference up to the parent module's enum */ - unit: Unit; - /** Reference down into a deeper module */ - detail: Detail; -} -// module graphics_shapes -/** - * Construct a Shape. - * Maps to C function: weaveffi_graphics_shapes_make_shape - */ -export function makeShape(sides: number): Shape -/** - * Nested function returning a struct owned by an ancestor module. - * Maps to C function: weaveffi_graphics_shapes_describe - */ -export function describe(): Canvas -/** A struct two levels deep. */ -export interface Detail { - area: number; -} -// module graphics_shapes_geometry -/** - * Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - * Maps to C function: weaveffi_graphics_shapes_geometry_compute - * @throws {GraphicsError} - */ -export function compute(shape: Shape): Detail - -// END types.d.ts 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 deleted file mode 100644 index 4f89c1f2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__weaveffi_addon_c.snap +++ /dev/null @@ -1,219 +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 NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include "weaveffi.h" -#include -#include -#include -#include -#include - -static inline napi_status weaveffi_napi_get_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_int64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - return napi_get_value_int64(env, v, out); - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_get_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - double d = 0; - napi_status s = napi_get_value_double(env, v, &d); - if (s == napi_ok && (d < 0 || d != d)) { - napi_throw_range_error(env, NULL, "negative number for an unsigned 64-bit integer"); - return napi_generic_failure; - } - *out = (uint64_t)d; - return s; - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -// An object handle is the pointer value carried as a bigint. null and -// undefined read as NULL (the absent case of a nullable object). -static inline napi_status weaveffi_napi_get_handle(napi_env env, napi_value v, void** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return napi_ok; - } - if (t == napi_bigint) { - uint64_t raw = 0; - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, &raw, &lossless); - *out = (void*)(uintptr_t)raw; - return s; - } - napi_throw_type_error(env, NULL, "expected an object handle"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_make_handle(napi_env env, const void* p, napi_value* out) { - if (p == NULL) { - return napi_get_null(env, out); - } - return napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, out); -} - -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_graphics_new_canvas(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); - napi_status arg_status = napi_ok; - int32_t width = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_int32(env, args[0], &width); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_graphics_new_canvas(width, &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_graphics_shapes_make_shape(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); - napi_status arg_status = napi_ok; - int32_t sides = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_int32(env, args[0], &sides); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_graphics_shapes_make_shape(sides, &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_graphics_shapes_describe(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}; - size_t out_len = 0; - const uint8_t* result = weaveffi_graphics_shapes_describe(&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_graphics_shapes_geometry_compute(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* shape_raw = NULL; - size_t shape_len = 0; - napi_get_buffer_info(env, args[0], &shape_raw, &shape_len); - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_graphics_shapes_geometry_compute((const uint8_t*)shape_raw, shape_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 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 }, - { "describe", NULL, Napi_weaveffi_graphics_shapes_describe, NULL, NULL, NULL, napi_default, NULL }, - { "compute", NULL, Napi_weaveffi_graphics_shapes_geometry_compute, 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_shapes__README_md.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__README_md.snap new file mode 100644 index 00000000..1519f91c --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__README_md.snap @@ -0,0 +1,24 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + +# shapes + +Native bindings for shapes + +Node.js bindings for the `shapes` native library: an ES module over a small N-API addon (`shapes_node.c`) that `npm install` compiles with node-gyp. The addon links `shapes`; set `SHAPES_LIBRARY` to the library file (or its directory) when installing, or pass `--shapes-library=` to npm. + +```sh +SHAPES_LIBRARY=/path/to/libshapes.so npm install shapes +``` + +Each top-level module of the API is a namespace export: + +```js +import { shapes } from 'shapes'; +``` + + diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__binding_gyp.snap index 3997b5bb..2bfd672c 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__binding_gyp.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__binding_gyp.snap @@ -1,17 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o { + "variables": { + "native_dir%": " { const path = require('path'); const p = process.env.SHAPES_LIBRARY || process.env.npm_config_shapes_library; if (!p) return path.resolve('.'); return require('fs').statSync(p).isFile() ? path.dirname(path.resolve(p)) : path.resolve(p); })()\")" + }, "targets": [ { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] + "target_name": "shapes_node", + "sources": [ + "shapes_node.c" + ], + "include_dirs": [ + "." + ], + "library_dirs": [ + "<(native_dir)" + ], + "libraries": [ + "-lshapes" + ], + "conditions": [ + ["OS=='mac'", { "xcode_settings": { "OTHER_LDFLAGS": ["-Wl,-rpath,@loader_path", "-Wl,-rpath,<(native_dir)"] } }], + ["OS=='linux'", { "ldflags": ["-Wl,-rpath,'$$ORIGIN'", "-Wl,-rpath,<(native_dir)"] }] + ] } ] } diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__index_d_ts.snap new file mode 100644 index 00000000..504ddfa2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__index_d_ts.snap @@ -0,0 +1,76 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class ShapesError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends ShapesError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof ShapesError; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** Rich (algebraic) enums and the expanded numeric primitives. */ +export declare namespace shapes { + /** An algebraic shape (sum type with associated data) */ + export type Shape = + /** The empty shape (a unit variant) */ + | { tag: 'Empty' } + /** A circle with a radius */ + | { tag: 'Circle'; radius: number } + /** An axis-aligned rectangle */ + | { tag: 'Rectangle'; width: number; height: number } + /** An integer-bounded shape (exercises narrow + wide ints) */ + | { tag: 'Bounded'; min: number; max: number; weight: bigint } + /** A labeled shape with a small count */ + | { tag: 'Labeled'; label: string; count: number }; + /** A plain C-style enum (no payloads) */ + export enum Channel { + Red = 0, + Green = 1, + Blue = 2, + } + /** One field per supported numeric primitive */ + export interface Numerics { + a: number; + b: number; + c: number; + d: bigint; + e: number; + f: number; + g: number; + h: bigint; + i: number; + j: number; + } + /** Render a shape to a string */ + export function describe(shape: shapes.Shape): string; + /** Scale a shape by a factor, returning a new shape */ + export function scale(shape: shapes.Shape, factor: number): shapes.Shape; + /** Sum a list of bytes into a wide integer (numerics smoke) */ + export function sumBytes(values: Uint8Array): bigint; + /** Build a Numerics struct from every numeric primitive */ + export function makeNumerics(a: number, b: number, c: number, d: bigint, e: number, f: number, g: number, h: bigint, i: number, j: number): shapes.Numerics; +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__index_js.snap index 1585a234..60553893 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__index_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__index_js.snap @@ -1,272 +1,188 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.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]; -} +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + ShapesError as $Error, +} from './runtime.js'; +export { ShapesError, CancelledError } from './runtime.js'; -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); - }); -} +import { createRequire } from 'node:module'; -// --- 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'); +// The native entry points: the N-API addon `npm install` builds from +// shapes_node.c (with node-gyp), or a prebuilt copy next to this file. +function $loadAddon(name) { + const require = createRequire(import.meta.url); + const tried = []; + for (const dir of ['./build/Release/', './build/Debug/', './']) { try { - return __utf8.decode(this._buf.subarray(at, at + n)); + return require(dir + name); } catch (e) { - throw __bufferError('string is not valid UTF-8'); + if (e.code !== 'MODULE_NOT_FOUND') throw e; + tried.push(dir + name); } } - 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; + throw new Error( + `the native addon ${name} was not found (tried ${tried.join(', ')}); ` + + 'build it with `npm install` or `node-gyp rebuild` in this package', + ); } -function __packNumerics(w, v) { - w.i8(v.a); - w.i16(v.b); - w.i32(v.c); - w.i64(v.d); - w.u8(v.e); - w.u16(v.f); - w.u32(v.g); - w.u64(v.h); - w.f32(v.i); - w.f64(v.j); +const $raw = $loadAddon('shapes_node.node'); +$raw.$setup($Fault); + +// Object tokens in value buffers are already bigint handles here. +const $token = (t) => t; + +$verify($raw, 'shapes', 'shapes', 3, [['shapes', 0x06acf433ffa2a0f0n]]); + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.shapes_debug_live(kind); } -function __unpackNumerics(r) { + +const shapes$Channel = Object.freeze({ + Red: 0, + Green: 1, + Blue: 2, + 0: 'Red', + 1: 'Green', + 2: 'Blue', +}); + +function $w$shapes$Numerics(w, v) { + w.writeI8(v.a); + w.writeI16(v.b); + w.writeI32(v.c); + w.writeI64(v.d); + w.writeU8(v.e); + w.writeU16(v.f); + w.writeU32(v.g); + w.writeU64(v.h); + w.writeF32(v.i); + w.writeF64(v.j); +} +function $r$shapes$Numerics(r) { return { - a: r.i8(), - b: r.i16(), - c: r.i32(), - d: r.i64(), - e: r.u8(), - f: r.u16(), - g: r.u32(), - h: r.u64(), - i: r.f32(), - j: r.f64(), + a: r.readI8(), + b: r.readI16(), + c: r.readI32(), + d: r.readI64(), + e: r.readU8(), + f: r.readU16(), + g: r.readU32(), + h: r.readU64(), + i: r.readF32(), + j: r.readF64(), }; } -function __packShape(w, v) { +function $w$shapes$Shape(w, v) { switch (v.tag) { case 'Empty': - w.i32(0); - break; + w.writeI32(0); + return; case 'Circle': - w.i32(1); - w.f64(v.radius); - break; + w.writeI32(1); + w.writeF64(v.radius); + return; case 'Rectangle': - w.i32(2); - w.f32(v.width); - w.f32(v.height); - break; + w.writeI32(2); + w.writeF32(v.width); + w.writeF32(v.height); + return; case 'Bounded': - w.i32(3); - w.i16(v.min); - w.i16(v.max); - w.u64(v.weight); - break; + w.writeI32(3); + w.writeI16(v.min); + w.writeI16(v.max); + w.writeU64(v.weight); + return; case 'Labeled': - w.i32(4); - w.str(v.label); - w.u8(v.count); - break; + w.writeI32(4); + w.writeString(v.label); + w.writeU8(v.count); + return; default: - throw new WeaveFFIError(-2, 'unknown Shape tag: ' + (v && v.tag)); + throw new TypeError('unknown Shape tag: ' + (v && v.tag)); } } -function __unpackShape(r) { - const tag = r.i32(); +function $r$shapes$Shape(r) { + const tag = r.readI32(); switch (tag) { case 0: return { tag: 'Empty' }; - case 1: return { tag: 'Circle', radius: r.f64() }; - case 2: return { tag: 'Rectangle', width: r.f32(), height: r.f32() }; - case 3: return { tag: 'Bounded', min: r.i16(), max: r.i16(), weight: r.u64() }; - case 4: return { tag: 'Labeled', label: r.str(), count: r.u8() }; - default: throw new WeaveFFIError(-2, 'unknown Shape tag: ' + tag); + case 1: return { tag: 'Circle', radius: r.readF64() }; + case 2: return { tag: 'Rectangle', width: r.readF32(), height: r.readF32() }; + case 3: return { tag: 'Bounded', min: r.readI16(), max: r.readI16(), weight: r.readU64() }; + case 4: return { tag: 'Labeled', label: r.readString(), count: r.readU8() }; + default: + throw new $Error(-3, 'unknown Shape tag: ' + tag); } } -wv.Channel = Object.freeze({ - Red: 0, - Green: 1, - Blue: 2, - 0: 'Red', - 1: 'Green', - 2: 'Blue', -}); -wv.describe = function (shape) { - return __invoke(addon.describe, [__encode(__packShape, shape)], __generic); +const shapes$describe = function describe(shape) { + try { + return $raw.shapes_shapes_describe($encode(shape, $w$shapes$Shape)); + } catch ($e) { + throw $fault($e); + } }; -wv.scale = function (shape, factor) { - const _r = __invoke(addon.scale, [__encode(__packShape, shape), factor], __generic); - return __decode(__unpackShape, _r); + +const shapes$scale = function scale(shape, factor) { + try { + return $decode($raw.shapes_shapes_scale($encode(shape, $w$shapes$Shape), factor), $r$shapes$Shape); + } catch ($e) { + throw $fault($e); + } }; -wv.sumBytes = function (values) { - return __invoke(addon.sumBytes, [values], __generic); + +const shapes$sumBytes = function sumBytes(values) { + try { + return $raw.shapes_shapes_sum_bytes(values); + } catch ($e) { + throw $fault($e); + } }; -wv.makeNumerics = function (a, b, c, d, e, f, g, h, i, j) { - const _r = __invoke(addon.makeNumerics, [a, b, c, d, e, f, g, h, i, j], __generic); - return __decode(__unpackNumerics, _r); + +const shapes$makeNumerics = function makeNumerics(a, b, c, d, e, f, g, h, i, j) { + try { + return $decode($raw.shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j), $r$shapes$Numerics); + } catch ($e) { + throw $fault($e); + } }; -module.exports = wv; +export const shapes = Object.freeze({ + Channel: shapes$Channel, + describe: shapes$describe, + scale: shapes$scale, + sumBytes: shapes$sumBytes, + makeNumerics: shapes$makeNumerics, +}); // END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__package_json.snap index 6fd366d2..6487043e 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__package_json.snap @@ -2,16 +2,34 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "shapes", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for shapes", + "type": "module", "main": "index.js", - "types": "types.d.ts", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "binding.gyp", + "shapes_node.c", + "shapes.h" + ], "gypfile": true, "scripts": { "install": "node-gyp rebuild" + }, + "engines": { + "node": ">=18" } } diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__runtime_js.snap new file mode 100644 index 00000000..323cda6a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class ShapesError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends ShapesError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new ShapesError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new ShapesError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new ShapesError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new ShapesError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_h.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_h.snap new file mode 100644 index 00000000..df20c3f5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_h.snap @@ -0,0 +1,151 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef SHAPES_H +#define SHAPES_H + +#include +#include +#include + +#ifndef SHAPES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef SHAPES_BUILD +# define SHAPES_API __declspec(dllexport) +# else +# define SHAPES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define SHAPES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define SHAPES_API __attribute__((visibility("default"))) +# else +# define SHAPES_API +# endif +#endif + +#ifndef SHAPES_DEPRECATED +# if defined(_MSC_VER) +# define SHAPES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define SHAPES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define SHAPES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports shapes_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define SHAPES_ABI_VERSION 3u +SHAPES_API uint32_t shapes_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by shapes_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct shapes_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} shapes_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +SHAPES_API void shapes_error_set(shapes_error* err, int32_t code, const char* message); +SHAPES_API void shapes_error_clear(shapes_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + shapes_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +SHAPES_API void shapes_error_free(shapes_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +SHAPES_API void shapes_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct shapes_cancel_token shapes_cancel_token; +SHAPES_API shapes_cancel_token* shapes_cancel_token_create(void); +SHAPES_API void shapes_cancel_token_cancel(shapes_cancel_token* token); +SHAPES_API bool shapes_cancel_token_is_cancelled(const shapes_cancel_token* token); +SHAPES_API void shapes_cancel_token_destroy(shapes_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +SHAPES_API uint8_t* shapes_alloc(uint32_t size); +SHAPES_API void shapes_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +SHAPES_API uint64_t shapes_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define SHAPES_SHAPES_CHECKSUM 0x06acf433ffa2a0f0ull +SHAPES_API uint64_t shapes_shapes_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with shapes_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * shapes_buffer.h declares a C struct and codec for each. + */ + +/** An algebraic shape (sum type with associated data) */ +typedef enum { + /** The empty shape (a unit variant) */ + shapes_shapes_Shape_Empty = 0, + /** A circle with a radius */ + shapes_shapes_Shape_Circle = 1, + /** An axis-aligned rectangle */ + shapes_shapes_Shape_Rectangle = 2, + /** An integer-bounded shape (exercises narrow + wide ints) */ + shapes_shapes_Shape_Bounded = 3, + /** A labeled shape with a small count */ + shapes_shapes_Shape_Labeled = 4 +} shapes_shapes_Shape_Tag; +/** A plain C-style enum (no payloads) */ +typedef enum { shapes_shapes_Channel_Red = 0, shapes_shapes_Channel_Green = 1, shapes_shapes_Channel_Blue = 2 } shapes_shapes_Channel; + +// Module: shapes +/** Render a shape to a string */ +SHAPES_API const uint8_t* shapes_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, shapes_error* out_err); +/** Scale a shape by a factor, returning a new shape */ +SHAPES_API const uint8_t* shapes_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, shapes_error* out_err); +/** Sum a list of bytes into a wide integer (numerics smoke) */ +SHAPES_API uint64_t shapes_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, shapes_error* out_err); +/** Build a Numerics struct from every numeric primitive */ +SHAPES_API const uint8_t* shapes_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, shapes_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // SHAPES_H + +// END shapes.h diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_node_c.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_node_c.snap new file mode 100644 index 00000000..06f456da --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__shapes_node_c.snap @@ -0,0 +1,734 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NAPI_VERSION +#define NAPI_VERSION 8 +#endif +#include +#include +#include +#include +#include +#include + +/* The addon wraps deprecated functions too; the JavaScript API carries the + * deprecation instead. */ +#define SHAPES_DEPRECATED(msg) +#include "shapes.h" + +/* + * The N-API transport of the JavaScript bindings. Every C symbol of the + * library is exported to `index.js` under its own name and called with the + * raw convention the shared JavaScript layer documents: direct values as + * numbers, bigints, and booleans; strings as strings; bytes and value + * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint + * handles (null when absent); callback interfaces as adapter objects. A + * failure throws an instance of the runtime's `$Fault` class. + */ + +typedef shapes_error js_error; + +/* Helpers an API may not need. */ +#if defined(__GNUC__) || defined(__clang__) +#define JS_HELPER static inline __attribute__((unused)) +#else +#define JS_HELPER static inline +#endif + +/* Per-environment state (one per main thread or worker): the `$Fault` + * constructor `index.js` registers through `$setup`. */ +typedef struct { + napi_ref fault; +} js_env; + +static void js_env_free(napi_env env, void* data, void* hint) { + (void)hint; + js_env* e = (js_env*)data; + if (e->fault != NULL) napi_delete_reference(env, e->fault); + free(e); +} + +JS_HELPER napi_value js_undefined(napi_env env) { + napi_value v; + napi_get_undefined(env, &v); + return v; +} + +static napi_value js_setup(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e->fault != NULL) napi_delete_reference(env, e->fault); + napi_create_reference(env, argv[0], 1, &e->fault); + return js_undefined(env); +} + +/* --------------------------------------------------------------------------- + * Value conversions + * ------------------------------------------------------------------------- */ + +JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value ab, out; + void* data = NULL; + napi_create_arraybuffer(env, len, &data, &ab); + if (len > 0) memcpy(data, ptr, len); + napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); + return out; +} + +JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out; + napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); + return out; +} + +JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { + napi_value out; + napi_create_int32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { + napi_value out; + napi_create_uint32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { + napi_value out; + napi_create_bigint_int64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { + napi_value out; + napi_create_bigint_uint64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_f64(napi_env env, double v) { + napi_value out; + napi_create_double(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_bool(napi_env env, bool v) { + napi_value out; + napi_get_boolean(env, v, &out); + return out; +} + +/* An object, iterator, or cancel token handle: the pointer as a bigint, or + * null for NULL. */ +JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { + napi_value out; + if (p == NULL) { + napi_get_null(env, &out); + } else { + napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); + } + return out; +} + +/* Returned strings and buffers: convert, then release the producer's copy. */ +JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_str(env, ptr, len); + if (ptr != NULL) shapes_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_bytes(env, ptr, len); + if (ptr != NULL) shapes_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, + const uint8_t* payload, size_t payload_len) { + napi_value argv[3], ctor, out = NULL; + napi_create_int32(env, code, &argv[0]); + napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); + if (payload != NULL) { + argv[2] = js_new_bytes(env, payload, payload_len); + } else { + napi_get_null(env, &argv[2]); + } + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok || + ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { + napi_create_error(env, NULL, argv[1], &out); + napi_set_named_property(env, out, "code", argv[0]); + } + return out; +} + +/* Throw the error a call reported, then release it. */ +JS_HELPER napi_value js_throw(napi_env env, js_error* err) { + napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); + shapes_error_clear(err); + return NULL; +} + +JS_HELPER bool js_type_error(napi_env env, const char* expected) { + bool pending = false; + napi_is_exception_pending(env, &pending); + if (!pending) napi_throw_type_error(env, NULL, expected); + return false; +} + +JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { + return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { + return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int8_t)t; + return true; +} + +JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int16_t)t; + return true; +} + +JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint8_t)t; + return true; +} + +JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint16_t)t; + return true; +} + +/* 64-bit integers: a bigint that fits, or an integral number. */ +JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_int64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { + *out = (int64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_uint64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && + d == (double)(uint64_t)d) { + *out = (uint64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { + return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { + double d = 0; + if (!js_arg_f64(env, v, &d)) return false; + *out = (float)d; + return true; +} + +JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { + return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); +} + +/* A handle argument; null or undefined is NULL when `nullable`. */ +JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_null || t == napi_undefined) { + *out = NULL; + return nullable || js_type_error(env, "expected an object handle"); + } + uint64_t raw = 0; + bool lossless = false; + if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || + raw == 0) { + return js_type_error(env, "expected an object handle"); + } + *out = (void*)(uintptr_t)raw; + return true; +} + +/* A borrowed byte run: a Uint8Array (or Buffer). */ +JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { + bool is_typed = false; + napi_is_typedarray(env, v, &is_typed); + if (is_typed) { + napi_typedarray_type type; + void* data = NULL; + napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); + if (type == napi_uint8_array || type == napi_uint8_clamped_array) { + *ptr = (const uint8_t*)data; + return true; + } + } + return js_type_error(env, "expected a Uint8Array"); +} + +/* A string argument encoded as UTF-8: short strings live in the inline + * buffer, longer ones on the heap until js_str_free. */ +typedef struct { + char* ptr; + size_t len; + char inline_buf[256]; +} js_str; + +#define JS_STR_INIT {NULL, 0, {0}} + +JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { + size_t len = 0; + if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { + return js_type_error(env, "expected a string"); + } + s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); + napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); + return true; +} + +JS_HELPER void js_str_free(js_str* s) { + if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); + s->ptr = NULL; +} + +#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) + +/* Read up to `n` arguments; missing ones are undefined. */ +#define JS_ARGS(n) \ + size_t argc = (n); \ + napi_value argv[(n) + 1]; \ + napi_get_cb_info(env, info, &argc, argv, NULL, NULL) + +/* --------------------------------------------------------------------------- + * Async calls: the completion may run on any thread, so it only records the + * result and queues the settlement on the JavaScript thread through a + * thread-safe function (which also keeps the event loop alive meanwhile). + * ------------------------------------------------------------------------- */ + +typedef enum { + JS_R_VOID, + JS_R_I32, + JS_R_U32, + JS_R_I64, + JS_R_U64, + JS_R_F64, + JS_R_BOOL, + JS_R_STR, + JS_R_BYTES, + JS_R_HANDLE +} js_kind; + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + js_kind kind; + js_error* err; + union { + int64_t i; + uint64_t u; + double f; + bool b; + const void* p; + } v; + size_t len; +} js_async; + +JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { + (void)cb; + (void)context; + js_async* a = (js_async*)data; + bool failed = a->err != NULL && a->err->code != 0; + if (env != NULL && failed) { + napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, + a->err->payload_ptr, a->err->payload_len)); + } else if (env != NULL) { + napi_value v = NULL; + switch (a->kind) { + case JS_R_VOID: v = js_undefined(env); break; + case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; + case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; + case JS_R_I64: v = js_new_i64(env, a->v.i); break; + case JS_R_U64: v = js_new_u64(env, a->v.u); break; + case JS_R_F64: v = js_new_f64(env, a->v.f); break; + case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; + case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; + } + napi_resolve_deferred(env, a->deferred, v); + } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { + /* The environment is shutting down: just release the result. */ + shapes_free_bytes((uint8_t*)a->v.p, a->len); + } + shapes_error_free(a->err); + napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); + free(a); +} + +/* Create the promise and its settlement queue for one async call. */ +JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { + js_async* a = (js_async*)calloc(1, sizeof *a); + a->kind = kind; + napi_create_promise(env, &a->deferred, promise); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, + js_async_settle, &a->tsfn); + return a; +} + +/* Called by each completion once it has recorded its result. */ +JS_HELPER void js_async_done(js_async* a, js_error* err) { + a->err = err; + napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); +} + +/* --------------------------------------------------------------------------- + * Callback interfaces. One registration per implementation passed to the + * producer: a reference to the adapter object, the JavaScript thread it + * belongs to, and a thread-safe function that runs calls made from any other + * thread on that thread while the caller waits. + * ------------------------------------------------------------------------- */ + +typedef struct { + napi_env env; + napi_ref ref; + napi_threadsafe_function tsfn; + uv_thread_t js_thread; +} js_cb; + +/* Every callback frame starts with this header so the dispatcher can reach + * `out_err` without knowing the method. */ +typedef struct { + js_error* out_err; +} js_cb_frame; + +/* A call queued from another thread: `method` is the vtable index, or -1 to + * release the registration. */ +typedef struct { + js_cb* cb; + int method; + void* frame; + uv_mutex_t mu; + uv_cond_t cv; + bool done; +} js_cb_req; + +JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { + uv_thread_t self = uv_thread_self(); + return uv_thread_equal(&self, &cb->js_thread) != 0; +} + +JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, + napi_threadsafe_function_call_js dispatch) { + js_cb* cb = (js_cb*)calloc(1, sizeof *cb); + cb->env = env; + cb->js_thread = uv_thread_self(); + napi_create_reference(env, adapter, 1, &cb->ref); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, + &cb->tsfn); + /* A live implementation must not keep the process alive by itself. */ + napi_unref_threadsafe_function(env, cb->tsfn); + return cb; +} + +JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { + if (env != NULL) napi_delete_reference(env, cb->ref); + napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); + free(cb); +} + +/* A callback-interface argument: the adapter object `index.js` built. */ +JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, + napi_threadsafe_function_call_js dispatch, js_cb** out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); + *out = js_cb_register(env, v, name, dispatch); + return true; +} + +/* The vtable's `free`: the producer is done with the implementation. The + * reference can only be deleted on its own thread. */ +JS_HELPER void js_cb_free(void* ctx) { + js_cb* cb = (js_cb*)ctx; + if (cb == NULL) return; + if (js_cb_on_js_thread(cb)) { + js_cb_release(cb->env, cb); + return; + } + js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); + req->cb = cb; + req->method = -1; + if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); +} + +/* Run `req` on the JavaScript thread and wait for it. */ +JS_HELPER void js_cb_hop(js_cb_req* req) { + uv_mutex_init(&req->mu); + uv_cond_init(&req->cv); + req->done = false; + if (napi_call_threadsafe_function(req->cb->tsfn, req, napi_tsfn_blocking) == napi_ok) { + uv_mutex_lock(&req->mu); + while (!req->done) uv_cond_wait(&req->cv, &req->mu); + uv_mutex_unlock(&req->mu); + } else { + shapes_error_set(((js_cb_frame*)req->frame)->out_err, -4, + "the callback implementation is no longer reachable"); + } + uv_cond_destroy(&req->cv); + uv_mutex_destroy(&req->mu); +} + +JS_HELPER void js_cb_finish(js_cb_req* req) { + uv_mutex_lock(&req->mu); + req->done = true; + uv_cond_signal(&req->cv); + uv_mutex_unlock(&req->mu); +} + +/* Report the pending JavaScript exception (or `fallback`) through `out_err` + * as a foreign failure (code -4); nothing unwinds through the C frame. */ +JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { + char msg[512]; + msg[0] = 0; + bool pending = false; + napi_is_exception_pending(env, &pending); + if (pending) { + napi_value exc, text, str; + napi_get_and_clear_last_exception(env, &exc); + napi_valuetype t; + napi_typeof(env, exc, &t); + text = exc; + if (t == napi_object) napi_get_named_property(env, exc, "message", &text); + if (napi_coerce_to_string(env, text, &str) == napi_ok) { + napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); + } + napi_is_exception_pending(env, &pending); + if (pending) napi_get_and_clear_last_exception(env, &exc); + } + shapes_error_set(out_err, -4, msg[0] != 0 ? msg : fallback); +} + +/* Look up and call `method` on the adapter; false when it threw. */ +JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, + const napi_value* argv, napi_value* result) { + napi_value adapter, fn; + napi_get_reference_value(env, cb->ref, &adapter); + napi_get_named_property(env, adapter, method, &fn); + return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; +} + +/* --------------------------------------------------------------------------- + * The runtime symbols every library exports. + * ------------------------------------------------------------------------- */ + +static napi_value js_abi_version(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u32(env, shapes_abi_version()); +} + +static napi_value js_debug_live(napi_env env, napi_callback_info info) { + JS_ARGS(1); + int32_t kind = 0; + if (!js_arg_i32(env, argv[0], &kind)) return NULL; + return js_new_u64(env, shapes_debug_live(kind)); +} + +static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { + (void)info; + return js_new_handle(env, shapes_cancel_token_create()); +} + +static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + shapes_cancel_token_cancel((shapes_cancel_token*)token); + return js_undefined(env); +} + +static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + shapes_cancel_token_destroy((shapes_cancel_token*)token); + return js_undefined(env); +} + +#define JS_RUNTIME_EXPORTS \ + {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ + {"shapes_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ + {"shapes_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ + {"shapes_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"shapes_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"shapes_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ + napi_default, NULL} + +static napi_value nx_shapes_shapes_checksum(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u64(env, shapes_shapes_checksum()); +} + +static napi_value nx_shapes_shapes_describe(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = shapes_shapes_describe(a0, a0_len, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_str(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_shapes_shapes_scale(napi_env env, napi_callback_info info) { + JS_ARGS(2); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + double a1 = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + if (!js_arg_f64(env, argv[1], &a1)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = shapes_shapes_scale(a0, a0_len, a1, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +static napi_value nx_shapes_shapes_sum_bytes(napi_env env, napi_callback_info info) { + JS_ARGS(1); + napi_value ret = NULL; + const uint8_t* a0 = NULL; + size_t a0_len = 0; + if (!js_arg_bytes(env, argv[0], &a0, &a0_len)) goto done; + { + js_error err = {0}; + uint64_t r = shapes_shapes_sum_bytes(a0, a0_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_new_u64(env, r); + } + goto done; +done: + return ret; +} + +static napi_value nx_shapes_shapes_make_numerics(napi_env env, napi_callback_info info) { + JS_ARGS(10); + napi_value ret = NULL; + int8_t a0 = 0; + int16_t a1 = 0; + int32_t a2 = 0; + int64_t a3 = 0; + uint8_t a4 = 0; + uint16_t a5 = 0; + uint32_t a6 = 0; + uint64_t a7 = 0; + float a8 = 0; + double a9 = 0; + if (!js_arg_i8(env, argv[0], &a0)) goto done; + if (!js_arg_i16(env, argv[1], &a1)) goto done; + if (!js_arg_i32(env, argv[2], &a2)) goto done; + if (!js_arg_i64(env, argv[3], &a3)) goto done; + if (!js_arg_u8(env, argv[4], &a4)) goto done; + if (!js_arg_u16(env, argv[5], &a5)) goto done; + if (!js_arg_u32(env, argv[6], &a6)) goto done; + if (!js_arg_u64(env, argv[7], &a7)) goto done; + if (!js_arg_f32(env, argv[8], &a8)) goto done; + if (!js_arg_f64(env, argv[9], &a9)) goto done; + { + js_error err = {0}; + size_t out_len = 0; + const uint8_t* r = shapes_shapes_make_numerics(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, &out_len, &err); + if (err.code != 0) { + js_throw(env, &err); + goto done; + } + ret = js_take_bytes(env, r, out_len); + } + goto done; +done: + return ret; +} + +NAPI_MODULE_INIT() { + js_env* e = (js_env*)calloc(1, sizeof *e); + napi_set_instance_data(env, e, js_env_free, NULL); + napi_property_descriptor props[] = { + JS_RUNTIME_EXPORTS, + {"shapes_shapes_checksum", NULL, nx_shapes_shapes_checksum, NULL, NULL, NULL, napi_default, NULL}, + {"shapes_shapes_describe", NULL, nx_shapes_shapes_describe, NULL, NULL, NULL, napi_default, NULL}, + {"shapes_shapes_scale", NULL, nx_shapes_shapes_scale, NULL, NULL, NULL, napi_default, NULL}, + {"shapes_shapes_sum_bytes", NULL, nx_shapes_shapes_sum_bytes, NULL, NULL, NULL, napi_default, NULL}, + {"shapes_shapes_make_numerics", NULL, nx_shapes_shapes_make_numerics, NULL, NULL, NULL, napi_default, NULL}, + }; + napi_define_properties(env, exports, sizeof props / sizeof props[0], props); + return exports; +} + +// END shapes_node.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__types_d_ts.snap deleted file mode 100644 index 7da37c22..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__types_d_ts.snap +++ /dev/null @@ -1,76 +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 runtime traps (code -2 a - * producer panic, -3 a marshalling failure, -4 a callback interface - * implementation that threw); throwing functions surface a module - * domain subclass for their declared codes. - */ -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); -} -/** One field per supported numeric primitive */ -export interface Numerics { - a: number; - b: number; - c: number; - d: bigint; - e: number; - f: number; - g: number; - h: bigint; - i: number; - j: number; -} -/** An algebraic shape (sum type with associated data) */ -export type Shape = - /** The empty shape (a unit variant) */ - | { tag: 'Empty' } - /** A circle with a radius */ - | { tag: 'Circle'; radius: number } - /** An axis-aligned rectangle */ - | { tag: 'Rectangle'; width: number; height: number } - /** An integer-bounded shape (exercises narrow + wide ints) */ - | { tag: 'Bounded'; min: number; max: number; weight: bigint } - /** A labeled shape with a small count */ - | { tag: 'Labeled'; label: string; count: number }; -/** A plain C-style enum (no payloads) */ -export enum Channel { - Red = 0, - Green = 1, - Blue = 2, -} -// module shapes -/** - * Render a shape to a string - * Maps to C function: weaveffi_shapes_describe - */ -export function describe(shape: Shape): string -/** - * Scale a shape by a factor, returning a new shape - * Maps to C function: weaveffi_shapes_scale - */ -export function scale(shape: Shape, factor: number): Shape -/** - * Sum a list of bytes into a wide integer (numerics smoke) - * Maps to C function: weaveffi_shapes_sum_bytes - */ -export function sumBytes(values: Buffer): bigint -/** - * Build a Numerics struct from every numeric primitive - * Maps to C function: weaveffi_shapes_make_numerics - */ -export function makeNumerics(a: number, b: number, c: number, d: bigint, e: number, f: number, g: number, h: bigint, i: number, j: number): Numerics - -// END types.d.ts 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 deleted file mode 100644 index e151188d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__weaveffi_addon_c.snap +++ /dev/null @@ -1,269 +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 NAPI_VERSION -#define NAPI_VERSION 8 -#endif -#include -#include "weaveffi.h" -#include -#include -#include -#include -#include - -static inline napi_status weaveffi_napi_get_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_int64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - return napi_get_value_int64(env, v, out); - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_get_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - double d = 0; - napi_status s = napi_get_value_double(env, v, &d); - if (s == napi_ok && (d < 0 || d != d)) { - napi_throw_range_error(env, NULL, "negative number for an unsigned 64-bit integer"); - return napi_generic_failure; - } - *out = (uint64_t)d; - return s; - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -// An object handle is the pointer value carried as a bigint. null and -// undefined read as NULL (the absent case of a nullable object). -static inline napi_status weaveffi_napi_get_handle(napi_env env, napi_value v, void** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return napi_ok; - } - if (t == napi_bigint) { - uint64_t raw = 0; - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, &raw, &lossless); - *out = (void*)(uintptr_t)raw; - return s; - } - napi_throw_type_error(env, NULL, "expected an object handle"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_make_handle(napi_env env, const void* p, napi_value* out) { - if (p == NULL) { - return napi_get_null(env, out); - } - return napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, out); -} - -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_shapes_describe(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* shape_raw = NULL; - size_t shape_len = 0; - napi_get_buffer_info(env, args[0], &shape_raw, &shape_len); - weaveffi_error err = {0}; - const char* result = weaveffi_shapes_describe((const uint8_t*)shape_raw, shape_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_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret); - weaveffi_free_string(result); - return ret; -} - -static napi_value Napi_weaveffi_shapes_scale(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); - napi_status arg_status = napi_ok; - void* shape_raw = NULL; - size_t shape_len = 0; - napi_get_buffer_info(env, args[0], &shape_raw, &shape_len); - double factor = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_double(env, args[1], &factor); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_shapes_scale((const uint8_t*)shape_raw, shape_len, factor, &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_shapes_sum_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); - void* values_raw = NULL; - size_t values_len = 0; - napi_get_buffer_info(env, args[0], &values_raw, &values_len); - weaveffi_error err = {0}; - uint64_t result = weaveffi_shapes_sum_bytes((const uint8_t*)values_raw, values_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_bigint_uint64(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_shapes_make_numerics(napi_env env, napi_callback_info info) { - size_t argc = 10; - napi_value args[10]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - napi_status arg_status = napi_ok; - int8_t a = 0; - if (arg_status == napi_ok) { - int32_t a_tmp = 0; - arg_status = napi_get_value_int32(env, args[0], &a_tmp); - a = (int8_t)a_tmp; - } - int16_t b = 0; - if (arg_status == napi_ok) { - int32_t b_tmp = 0; - arg_status = napi_get_value_int32(env, args[1], &b_tmp); - b = (int16_t)b_tmp; - } - int32_t c = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_int32(env, args[2], &c); - } - int64_t d = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_i64(env, args[3], &d); - } - uint8_t e = 0; - if (arg_status == napi_ok) { - uint32_t e_tmp = 0; - arg_status = napi_get_value_uint32(env, args[4], &e_tmp); - e = (uint8_t)e_tmp; - } - uint16_t f = 0; - if (arg_status == napi_ok) { - uint32_t f_tmp = 0; - arg_status = napi_get_value_uint32(env, args[5], &f_tmp); - f = (uint16_t)f_tmp; - } - uint32_t g = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_uint32(env, args[6], &g); - } - uint64_t h = 0; - if (arg_status == napi_ok) { - arg_status = weaveffi_napi_get_u64(env, args[7], &h); - } - float i = 0; - if (arg_status == napi_ok) { - double i_tmp = 0; - arg_status = napi_get_value_double(env, args[8], &i_tmp); - i = (float)i_tmp; - } - double j = 0; - if (arg_status == napi_ok) { - arg_status = napi_get_value_double(env, args[9], &j); - } - if (arg_status != napi_ok) { - return NULL; - } - weaveffi_error err = {0}; - size_t out_len = 0; - const uint8_t* result = weaveffi_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &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[] = { - { "describe", NULL, Napi_weaveffi_shapes_describe, NULL, NULL, NULL, napi_default, NULL }, - { "scale", NULL, Napi_weaveffi_shapes_scale, NULL, NULL, NULL, napi_default, NULL }, - { "sumBytes", NULL, Napi_weaveffi_shapes_sum_bytes, NULL, NULL, NULL, napi_default, NULL }, - { "makeNumerics", NULL, Napi_weaveffi_shapes_make_numerics, 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/python_docs_everywhere__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__README_md.snap index e3b8a7b9..89f0942d 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__README_md.snap @@ -1,18 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Python) +# docs_everywhere (Python) -Auto-generated Python bindings using ctypes. +Python bindings for the `docs_everywhere` native library, built on `ctypes`. -## Prerequisites +## Requirements -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. +- Python 3.9 or later +- The native library (`libdocs_everywhere.so`, `libdocs_everywhere.dylib`, or `docs_everywhere.dll`) ## Install @@ -20,16 +20,16 @@ Auto-generated Python bindings using ctypes. pip install . ``` -## Development install +## Loading the native library -```bash -pip install -e . -``` +The bindings load the library from `DOCS_EVERYWHERE_LIBRARY` when it is set (a full path), +then from inside the installed package, then from the system loader's search +path. ## Usage ```python -from weaveffi import * +import docs_everywhere ``` diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere___init___py.snap new file mode 100644 index 00000000..ede48601 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere___init___py.snap @@ -0,0 +1,10 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +from .docs_everywhere import * # noqa: F401,F403 + +# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_py.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_py.snap new file mode 100644 index 00000000..5a8ae4bc --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_py.snap @@ -0,0 +1,976 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +"""ctypes bindings for the docs_everywhere native library.""" +import abc +import asyncio +import builtins +import ctypes +import os +import platform +import struct +import threading +import warnings +from dataclasses import dataclass +from enum import IntEnum +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type + + +class Error(Exception): + """An error reported by the native library. + + `code` is positive for one of the API's declared error codes (raised as + the declaring module's typed subclasses) and negative for a runtime trap: + a generic failure (-1), a producer panic (-2), a marshalling failure + (-3), or an exception raised by a callback-interface implementation + (-4). A cancelled async call (-5) raises `asyncio.CancelledError` + instead. + """ + + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + FOREIGN_ERROR_CODE = -4 + CANCELLED_ERROR_CODE = -5 + + def __init__(self, code: int, message: str) -> None: + super().__init__(f"({code}) {message}") + self.code = code + self.message = message + + +class _ErrorStruct(ctypes.Structure): + """The C `docs_everywhere_error` out-parameter every call reports through.""" + + _fields_ = [ + ("code", ctypes.c_int32), + ("message", ctypes.c_char_p), + ("payload_ptr", ctypes.c_void_p), + ("payload_len", ctypes.c_size_t), + ] + + +def _load_library() -> ctypes.CDLL: + # An explicit path wins; then a library bundled inside this package (a + # platform wheel); then the system loader's search path. + override = os.environ.get("DOCS_EVERYWHERE_LIBRARY") + if override: + return ctypes.CDLL(override) + system = platform.system() + if system == "Darwin": + name = "libdocs_everywhere.dylib" + elif system == "Windows": + name = "docs_everywhere.dll" + else: + name = "libdocs_everywhere.so" + bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) + if os.path.exists(bundled): + return ctypes.CDLL(bundled) + try: + return ctypes.CDLL(name) + except OSError as exc: + raise ImportError( + f"cannot load the native library {name!r} ({exc}); " + "set DOCS_EVERYWHERE_LIBRARY to its path" + ) from exc + + +_lib = _load_library() + + +def _bind(name: str, restype: Any, *argtypes: Any) -> Any: + """Look up one exported symbol and fix its C signature. Every prototype + is bound once, at import time.""" + fn = getattr(_lib, name) + fn.restype = restype + fn.argtypes = argtypes + return fn + + +# The C ABI revision these bindings were generated against, and the contract +# checksum of every top-level module: (module, checksum symbol, value). +_ABI_VERSION = 3 +_CHECKSUMS: List[Tuple[str, str, int]] = [ + ("docs", "docs_everywhere_docs_checksum", 0x7950eec1e9cfc819), +] + + +def _check_contract() -> None: + """Refuse to load a library built for a different ABI revision or a + different version of any module's API, before any other symbol is + bound.""" + try: + abi_version = _bind("docs_everywhere_abi_version", ctypes.c_uint32) + except AttributeError: + raise ImportError( + f"{_lib._name} does not export docs_everywhere_abi_version; " + "it is not the library these bindings were generated for" + ) from None + found = abi_version() + if found != _ABI_VERSION: + raise ImportError( + f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " + f"but {_lib._name} reports revision {found}" + ) + for module, symbol, expected in _CHECKSUMS: + try: + checksum = _bind(symbol, ctypes.c_uint64) + except AttributeError: + raise ImportError( + f"module {module!r}: {_lib._name} does not export {symbol}" + ) from None + found = checksum() + if found != expected: + raise ImportError( + f"module {module!r}: the native library's contract checksum " + f"{found:#018x} does not match these bindings ({expected:#018x}); " + "regenerate the bindings from the library's current API" + ) + + +_check_contract() + +_error_clear = _bind("docs_everywhere_error_clear", None, ctypes.POINTER(_ErrorStruct)) +_free_bytes = _bind("docs_everywhere_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) + + +def _debug_live(kind: int) -> int: + """The producer's live-allocation counter `kind` (0 objects, 1 callbacks, + 2 iterators, 3 cancel tokens, 4 returned allocations). Always 0 unless + the producer was built with its leak counters enabled.""" + return _bind("docs_everywhere_debug_live", ctypes.c_uint64, ctypes.c_int32)(kind) + + +def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: + """Copy a filled out-err slot's code, message, and payload, then release + them.""" + code = err.code + message = err.message.decode("utf-8", "replace") if err.message else "" + payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + _error_clear(ctypes.byref(err)) + return code, message, payload + + +def _error_from(code: int, message: str, payload: bytes = b"") -> Error: + """The exception for a code no declared error domain claims.""" + return Error(code, message) + + +def _check_error(err: _ErrorStruct) -> None: + """Raise for a non-zero out-err slot of a call that declares no errors: + only runtime traps (a panic, a marshalling failure, a failed callback) + land here.""" + if err.code: + raise _error_from(*_read_error(err)) + + +def _take_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a producer-owned byte run (a string, bytes, or value buffer) and + release it with docs_everywhere_free_bytes.""" + if not ptr: + return b"" + try: + return ctypes.string_at(ptr, length) + finally: + _free_bytes(ptr, length) + + +def _take_str(ptr: Optional[int], length: int) -> str: + """Copy and release a producer-owned UTF-8 string.""" + return _take_bytes(ptr, length).decode("utf-8") + + +def _peek_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a byte run the producer only lends for the current call.""" + return ctypes.string_at(ptr, length) if ptr and length else b"" + + +class _Handle: + """Owns one native reference and releases it exactly once. + + Calls lend the pointer through `_acquire`/`_release`, so `close()` from + another thread while a call is in flight defers the release until the + last such call returns instead of freeing the object mid-call. + """ + + # The native destroy (or iterator release) function, set per subclass. + _destroy: Callable[[int], None] + + def _init_handle(self, ptr: int) -> None: + self._ptr: Optional[int] = ptr + self._calls = 0 + self._closing = False + self._lock = threading.Lock() + + @classmethod + def _adopt(cls, ptr: int) -> Any: + """Wrap one strong reference the producer handed over.""" + obj = cls.__new__(cls) + obj._init_handle(ptr) + return obj + + def _acquire(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise Error(-1, f"{type(self).__name__} used after close()") + self._calls += 1 + return ptr + + def _release(self) -> None: + with self._lock: + self._calls -= 1 + if self._calls or not self._closing: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def close(self) -> None: + """Release this wrapper's native reference. Idempotent; the wrapper + is unusable afterwards.""" + lock = getattr(self, "_lock", None) + if lock is None: + return + with lock: + if self._closing: + return + self._closing = True + if self._calls: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def __enter__(self) -> Any: + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def __del__(self) -> None: + # An unreachable wrapper has no call in flight: release directly. + ptr = getattr(self, "_ptr", None) + if ptr is not None: + self._ptr = None + type(self)._destroy(ptr) + + +class _Object(_Handle): + """Base of every interface wrapper: a reference-counted native object.""" + + # The native clone function, set per subclass. + _clone: Callable[[int], int] + + def _clone_ref(self) -> int: + """A new strong reference to the same object, for a receiver that + adopts it (an object token inside a value buffer).""" + ptr = self._acquire() + try: + return type(self)._clone(ptr) + finally: + self._release() + + +class _Iterator(_Handle): + """Base of every lazy iterator over a producer stream: one native `next` + call per step; the handle is released on exhaustion, `close()`, or + garbage collection.""" + + def __iter__(self) -> Any: + return self + + def _step(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise StopIteration + self._calls += 1 + return ptr + + +def _required(ptr: Optional[int]) -> int: + """A non-null object pointer the producer returned.""" + if not ptr: + raise Error(-1, "the producer returned a null object") + return ptr + + +def _lend(obj: Any, cls: "Type[_Object]") -> int: + """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + return obj._acquire() + + +def _lend_opt(obj: Any, cls: "Type[_Object]") -> Optional[int]: + """`_lend` for an optional object: None lends a null pointer.""" + return None if obj is None else _lend(obj, cls) + + +def _release_opt(obj: Any) -> None: + if obj is not None: + obj._release() + + +_I8 = struct.Struct(" Error: + return Error(-3, f"malformed value buffer: {what}") + + +class _Writer: + """Encodes values in the value-buffer wire format: little-endian, + packed, no alignment.""" + + __slots__ = ("_buf", "_objects") + + def __init__(self) -> None: + self._buf = bytearray() + self._objects: List[Tuple[int, _Object]] = [] + + def finish(self) -> bytes: + # Object tokens are minted last, so an encoding that fails part-way + # never leaks a strong reference. + minted: List[Tuple[_Object, int]] = [] + try: + for pos, obj in self._objects: + ptr = obj._clone_ref() + minted.append((obj, ptr)) + _U64.pack_into(self._buf, pos, ptr) + except BaseException: + for obj, ptr in minted: + type(obj)._destroy(ptr) + raise + self._objects = [] + return bytes(self._buf) + + def write_bool(self, v: bool) -> None: + self._buf.append(1 if v else 0) + + def write_i8(self, v: int) -> None: + self._buf += _I8.pack(v) + + def write_u8(self, v: int) -> None: + self._buf += _U8.pack(v) + + def write_i16(self, v: int) -> None: + self._buf += _I16.pack(v) + + def write_u16(self, v: int) -> None: + self._buf += _U16.pack(v) + + def write_i32(self, v: int) -> None: + self._buf += _I32.pack(v) + + def write_u32(self, v: int) -> None: + self._buf += _U32.pack(v) + + def write_i64(self, v: int) -> None: + self._buf += _I64.pack(v) + + def write_u64(self, v: int) -> None: + self._buf += _U64.pack(v) + + def write_f32(self, v: float) -> None: + self._buf += _F32.pack(v) + + def write_f64(self, v: float) -> None: + self._buf += _F64.pack(v) + + def write_count(self, n: int) -> None: + self._buf += _U32.pack(n) + + def write_flag(self, present: bool) -> None: + self._buf.append(1 if present else 0) + + def write_string(self, v: str) -> None: + data = v.encode("utf-8") + self._buf += _U32.pack(len(data)) + self._buf += data + + def write_bytes(self, v: bytes) -> None: + self._buf += _U32.pack(len(v)) + self._buf += v + + def write_numbers(self, code: str, values: List[Any]) -> None: + """A list of one fixed-width numeric primitive (`code` is its struct + format character), packed in one call.""" + self._buf += _U32.pack(len(values)) + self._buf += struct.pack(f"<{len(values)}{code}", *values) + + def write_object(self, obj: Any, cls: "Type[_Object]") -> None: + """Reserve an object token: one strong reference to `obj` the reader + adopts, cloned in `finish()`. A closed wrapper is rejected now.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + obj._acquire() + obj._release() + self._objects.append((len(self._buf), obj)) + self._buf += bytes(8) + + +class _Reader: + """Decodes values from the value-buffer wire format, rejecting truncated + buffers, invalid flag bytes, invalid UTF-8, and trailing data.""" + + __slots__ = ("_data", "_pos") + + def __init__(self, data: bytes) -> None: + self._data = data + self._pos = 0 + + def _take(self, n: int, what: str) -> int: + pos = self._pos + if len(self._data) - pos < n: + raise _malformed(f"truncated {what}") + self._pos = pos + n + return pos + + def read_bool(self) -> bool: + b = self._data[self._take(1, "bool")] + if b > 1: + raise _malformed("invalid bool byte") + return b == 1 + + def read_i8(self) -> int: + return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) + + def read_u8(self) -> int: + return self._data[self._take(1, "u8")] + + def read_i16(self) -> int: + return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) + + def read_u16(self) -> int: + return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) + + def read_i32(self) -> int: + return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) + + def read_u32(self) -> int: + return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) + + def read_i64(self) -> int: + return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) + + def read_u64(self) -> int: + return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) + + def read_f32(self) -> float: + return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) + + def read_f64(self) -> float: + return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) + + def read_count(self) -> int: + # An element count, not a byte length: elements can encode to zero + # bytes, so it is not bounded by the remaining data. + return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) + + def read_flag(self) -> bool: + b = self._data[self._take(1, "option flag")] + if b > 1: + raise _malformed("invalid option flag") + return b == 1 + + def read_bytes(self) -> bytes: + n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) + pos = self._take(n, "bytes") + return self._data[pos:pos + n] + + def read_string(self) -> str: + try: + return self.read_bytes().decode("utf-8") + except UnicodeDecodeError: + raise _malformed("string is not valid UTF-8") from None + + def read_numbers(self, code: str, size: int) -> List[Any]: + """A list of one fixed-width numeric primitive, unpacked in one call.""" + n = self.read_count() + pos = self._take(n * size, "list") + return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) + + def read_object(self) -> int: + """An object token: one strong reference the caller adopts.""" + ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) + if ptr == 0: + raise _malformed("null object token") + return ptr + + def expect_end(self) -> None: + if self._pos != len(self._data): + raise _malformed("trailing bytes") + + +def _decode(data: bytes, read: Callable[[_Reader], Any]) -> Any: + """Decode exactly one value from `data`.""" + r = _Reader(data) + value = read(r) + r.expect_end() + return value + + +# Async completions. A call registers its future under an integer key, passes +# the key as the launcher's `context`, and the function's static completion +# trampoline settles it from whichever producer thread completes the call. +# The trampolines live at module scope, so nothing a pending call needs can +# be garbage collected before the producer fires the completion, even when +# the awaiting coroutine is gone. +_pending: Dict[int, Tuple[asyncio.AbstractEventLoop, "asyncio.Future[Any]"]] = {} +_pending_lock = threading.Lock() +_pending_next = 0 + +_error_free = _bind("docs_everywhere_error_free", None, ctypes.POINTER(_ErrorStruct)) + + +def _async_begin() -> Tuple[int, "asyncio.Future[Any]"]: + """Register a new pending call on the running event loop.""" + global _pending_next + loop = asyncio.get_running_loop() + future = loop.create_future() + with _pending_lock: + _pending_next += 1 + key = _pending_next + _pending[key] = (loop, future) + return key, future + + +def _async_abandon(key: int) -> None: + """Forget a call whose launcher never ran.""" + with _pending_lock: + _pending.pop(key, None) + + +def _async_error(err: Any, factory: Callable[[int, str, bytes], BaseException]) -> BaseException: + """Copy and release a heap-boxed completion error, then build the + exception it reports. The cancelled code maps to CancelledError.""" + e = err.contents + code = e.code + message = e.message.decode("utf-8", "replace") if e.message else "" + payload = ctypes.string_at(e.payload_ptr, e.payload_len) if e.payload_ptr else b"" + _error_free(err) + if code == Error.CANCELLED_ERROR_CODE: + return asyncio.CancelledError(message) + return factory(code, message, payload) + + +def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) -> None: + """Hand a completion back to the event loop that awaits it.""" + with _pending_lock: + entry = _pending.pop(key or 0, None) + if entry is None: + return + loop, future = entry + try: + loop.call_soon_threadsafe(_async_resolve, future, exc, value) + except RuntimeError: + # The loop is closed, so nothing awaits the result any more. + pass + + +def _async_resolve(future: "asyncio.Future[Any]", exc: Optional[BaseException], value: Any) -> None: + if future.done(): + return + if isinstance(exc, asyncio.CancelledError): + future.cancel() + elif exc is not None: + future.set_exception(exc) + else: + future.set_result(value) + + +# Callback-interface implementations the producer holds, keyed by the integer +# it receives as `ctx`. An entry is added when an implementation is passed to +# a call and removed when the producer calls the vtable's `free(ctx)`, so the +# implementation lives exactly as long as the producer may call it and the +# producer never holds a raw reference to a Python object. ctypes acquires +# the GIL on entry to a trampoline, so the producer may call from any thread. +_callbacks: Dict[int, Any] = {} +_callbacks_lock = threading.Lock() +_callbacks_next = 0 + +_error_set = _bind( + "docs_everywhere_error_set", None, ctypes.POINTER(_ErrorStruct), ctypes.c_int32, ctypes.c_char_p +) + + +def _callback_register(impl: Any, cls: type) -> int: + """Register an implementation for one call; returns its `ctx` key.""" + global _callbacks_next + if not isinstance(impl, cls): + raise TypeError(f"expected {cls.__name__}, got {type(impl).__name__}") + with _callbacks_lock: + _callbacks_next += 1 + ctx = _callbacks_next + _callbacks[ctx] = impl + return ctx + + +def _callback_get(ctx: Optional[int]) -> Any: + return _callbacks[ctx or 0] + + +def _callback_free(ctx: Optional[int]) -> None: + # The producer's last reference is gone; it never passes `ctx` again. + with _callbacks_lock: + _callbacks.pop(ctx or 0, None) + + +def _callback_fail(out_err: Any, exc: BaseException) -> None: + """Report an implementation's exception to the producer, which aborts + its call with the foreign error code. Nothing unwinds through C.""" + message = str(exc) or type(exc).__name__ + _error_set(out_err, Error.FOREIGN_ERROR_CODE, message.encode("utf-8", "replace")) + + +# === Module: docs === + + +class DocsErrorsError(Error): + """Base exception for the `docs` module's error domain.""" + + NotFound: "Type[NotFound]" + InvalidStatus: "Type[InvalidStatus]" + + +class NotFound(DocsErrorsError): + """Returned when no document matches the requested id""" + + CODE = 1 + + def __init__(self, message: str = "Document not found") -> None: + super().__init__(1, message) + + +class InvalidStatus(DocsErrorsError): + """Raised when attempting to move into a forbidden state""" + + CODE = 2 + + def __init__(self, message: str = "Invalid status transition") -> None: + super().__init__(2, message) + + +DocsErrorsError.NotFound = NotFound +DocsErrorsError.InvalidStatus = InvalidStatus + + +_DOCS_ERRORS_ERROR_CODES: Dict[int, Any] = { + 1: NotFound, + 2: InvalidStatus, +} + + +def _docs_errors_error_from(code: int, message: str, payload: bytes = b"") -> Error: + """Build the DocsErrorsError subclass matching `code`, or the plain + Error for codes outside the domain (panics, marshalling).""" + cls = _DOCS_ERRORS_ERROR_CODES.get(code) + if cls is None: + return Error(code, message) + exc: Any = cls(message) if message else cls() + return exc + + +def _check_docs_errors_error(err: _ErrorStruct) -> None: + if err.code: + raise _docs_errors_error_from(*_read_error(err)) + + +class Status(IntEnum): + """ + Lifecycle status of a document. + + Used to drive UI badges and filters. + """ + # Document is being edited + Draft = 0 + # Document is publicly visible + Published = 1 + # Document is hidden from listings + Archived = 2 + + +@dataclass +class Document: + """ + Represents a single document tracked by the system. + + Documents are persisted to disk and exposed via the public API. + """ + + # Stable opaque identifier + id: int + # Human-readable title shown in lists + title: str + # Current lifecycle status + status: "Status" + # Free-form tags for grouping + tags: List[str] + + +def _write_Document(_w: _Writer, value: "Document") -> None: + _w.write_i64(value.id) + _w.write_string(value.title) + _w.write_i32(value.status) + _w.write_count(len(value.tags)) + for _e0 in value.tags: + _w.write_string(_e0) + + +def _read_Document(_r: _Reader) -> "Document": + return Document( + id=_r.read_i64(), + title=_r.read_string(), + status=Status(_r.read_i32()), + tags=[_r.read_string() for _i0 in range(_r.read_count())], + ) + + +class SavedListener(abc.ABC): + """ + Consumer-implemented observer of document-saved notifications. + + Implementations must be safe to call from any thread. + + Consumer-implemented callback interface. Subclass it, implement every + abstract method, and pass an instance wherever the API takes a `SavedListener`; + the producer may call the methods from any thread until it releases the + instance. An exception raised by a method is reported to the producer, + which fails the call in progress with Error.FOREIGN_ERROR_CODE (-4). + """ + + @abc.abstractmethod + def on_saved(self, doc_id: int, revision: int) -> None: + """Fires after a document has been persisted successfully + + Parameters + ---------- + doc_id : Identifier of the saved document + revision : Monotonically increasing revision + """ + + +_SavedListener_on_saved_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int64, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) +_SavedListener_free_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p) + + +class _SavedListenerVtable(ctypes.Structure): + """The C vtable `docs_everywhere_docs_SavedListener_vtable`.""" + + _fields_ = [ + ("on_saved", _SavedListener_on_saved_t), + ("free", _SavedListener_free_t), + ] + + +def _SavedListener_on_saved(ctx, doc_id, revision, out_err) -> None: + try: + _callback_get(ctx).on_saved(doc_id, revision) + except BaseException as exc: + _callback_fail(out_err, exc) + + +_SavedListener_vtable = _SavedListenerVtable( + _SavedListener_on_saved_t(_SavedListener_on_saved), + _SavedListener_free_t(_callback_free), +) + + +_c_docs_Notebook_new = _bind("docs_everywhere_docs_Notebook_new", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_docs_Notebook_insert = _bind("docs_everywhere_docs_Notebook_insert", ctypes.c_int32, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) +_c_docs_Notebook_find = _bind("docs_everywhere_docs_Notebook_find", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) +_c_docs_Notebook_size = _bind("docs_everywhere_docs_Notebook_size", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_docs_Notebook_max_documents = _bind("docs_everywhere_docs_Notebook_max_documents", ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) +_c_docs_Notebook_clone = _bind("docs_everywhere_docs_Notebook_clone", ctypes.c_void_p, ctypes.c_void_p) +_c_docs_Notebook_destroy = _bind("docs_everywhere_docs_Notebook_destroy", None, ctypes.c_void_p) + + +class Notebook(_Object): + """ + An ordered collection of documents. + + Notebooks own their documents and release them when destroyed. + """ + + _destroy = staticmethod(_c_docs_Notebook_destroy) + _clone = staticmethod(_c_docs_Notebook_clone) + + def __init__(self) -> None: + """Create an empty notebook""" + _err = _ErrorStruct() + _ret = _c_docs_Notebook_new(ctypes.byref(_err)) + _check_error(_err) + self._init_handle(_required(_ret)) + + def insert(self, document: "Document") -> int: + """Add a document, returning its position in the notebook + + Parameters + ---------- + document : The document to add + """ + _document_w = _Writer() + _write_Document(_document_w, document) + _err = _ErrorStruct() + _self_p = self._acquire() + try: + _document_b = _document_w.finish() + _ret = _c_docs_Notebook_insert(_self_p, _document_b, len(_document_b), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + return _ret + + def find(self, id: int) -> "Document": + """Look up a document by identifier, failing when none matches + + Parameters + ---------- + id : Identifier of the document to find + + Raises + ------ + DocsErrorsError + If the call reports one of the domain's error codes. + """ + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _self_p = self._acquire() + try: + _ret = _c_docs_Notebook_find(_self_p, id, ctypes.byref(_out_len), ctypes.byref(_err)) + finally: + self._release() + _check_docs_errors_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Document) + + def size(self) -> int: + """The number of documents currently held""" + _err = _ErrorStruct() + _self_p = self._acquire() + try: + _ret = _c_docs_Notebook_size(_self_p, ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + return _ret + + @staticmethod + def max_documents() -> int: + """The largest number of documents one notebook will hold""" + _err = _ErrorStruct() + _ret = _c_docs_Notebook_max_documents(ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_docs_create_document = _bind("docs_everywhere_docs_create_document", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def create_document(title: str, tags: List[str]) -> "Document": + """ + Create a brand new document. + + The document starts in `Draft` status; callers must publish + it explicitly to make it visible. + + Parameters + ---------- + title : Human-readable title for the new document + tags : Initial set of tags (may be empty) + """ + _title_b = title.encode("utf-8") + _tags_w = _Writer() + _tags_w.write_count(len(tags)) + for _e0 in tags: + _tags_w.write_string(_e0) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _tags_b = _tags_w.finish() + _ret = _c_docs_create_document(_title_b, len(_title_b), _tags_b, len(_tags_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Document) + + +_c_docs_get_document = _bind("docs_everywhere_docs_get_document", ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def get_document(id: int) -> Optional["Document"]: + """Look up a document by identifier + + Parameters + ---------- + id : Identifier returned from create_document + """ + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_docs_get_document(id, ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), lambda _r: (_read_Document(_r) if _r.read_flag() else None)) + + +_c_docs_publish_async_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_bool) +_c_docs_publish_async = _bind("docs_everywhere_docs_publish_async", None, ctypes.c_int64, _c_docs_publish_async_callback, ctypes.c_void_p) + + +def _docs_publish_async_complete(context, err, result) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _error_from), None) + else: + _async_settle(context, None, result) + except BaseException as exc: + _async_settle(context, exc, None) + + +_docs_publish_async_completion = _c_docs_publish_async_callback(_docs_publish_async_complete) + + +async def publish_async(id: int) -> bool: + """Publish a document in the background + + Parameters + ---------- + id : Identifier of the document to publish + """ + _call, _future = _async_begin() + try: + _c_docs_publish_async(id, _docs_publish_async_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_docs_watch = _bind("docs_everywhere_docs_watch", None, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) + + +def watch(listener: "SavedListener") -> None: + """Register a listener for saved notifications + + Parameters + ---------- + listener : Receiver of saved notifications + """ + _err = _ErrorStruct() + _listener_ctx = _callback_register(listener, SavedListener) + _c_docs_watch(_listener_ctx, ctypes.addressof(_SavedListener_vtable), ctypes.byref(_err)) + _check_error(_err) + +# END docs_everywhere.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_pyi.snap similarity index 87% rename from crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_pyi.snap rename to crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_pyi.snap index d6971ba8..19038d31 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_pyi.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_docs_everywhere_pyi.snap @@ -1,24 +1,26 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o +import builtins from abc import ABC, abstractmethod from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type +from typing import Dict, Iterator, List, Optional, Type -class WeaveFFIError(Exception): +class Error(Exception): GENERIC_ERROR_CODE: int PANIC_ERROR_CODE: int MARSHAL_ERROR_CODE: int FOREIGN_ERROR_CODE: int + CANCELLED_ERROR_CODE: int code: int message: str def __init__(self, code: int, message: str) -> None: ... -class DocsErrorsError(WeaveFFIError): +class DocsErrorsError(Error): NotFound: Type["NotFound"] InvalidStatus: Type["InvalidStatus"] def __init__(self, code: int, message: str) -> None: ... @@ -38,11 +40,11 @@ class InvalidStatus(DocsErrorsError): # Used to drive UI badges and filters. class Status(IntEnum): # Document is being edited - Draft: int + Draft = 0 # Document is publicly visible - Published: int + Published = 1 # Document is hidden from listings - Archived: int + Archived = 2 # Represents a single document tracked by the system. # @@ -73,7 +75,7 @@ class Notebook: def __init__(self) -> None: ... def close(self) -> None: ... def __enter__(self) -> "Notebook": ... - def __exit__(self, *exc: object) -> bool: ... + def __exit__(self, *exc: object) -> None: ... def insert(self, document: "Document") -> int: ... def find(self, id: int) -> "Document": ... def size(self) -> int: ... @@ -95,4 +97,4 @@ async def publish_async(id: int) -> bool: ... # Register a listener for saved notifications def watch(listener: "SavedListener") -> None: ... -# END weaveffi.pyi +# END docs_everywhere.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_py_typed.snap new file mode 100644 index 00000000..f8cd271e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__docs_everywhere_py_typed.snap @@ -0,0 +1,6 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__pyproject_toml.snap index 244fb84d..40c0948b 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__pyproject_toml.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__pyproject_toml.snap @@ -1,24 +1,24 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "weaveffi" +name = "docs_everywhere" version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" +description = "Native bindings for docs_everywhere" +requires-python = ">=3.9" [tool.setuptools] -packages = ["weaveffi"] +packages = ["docs_everywhere"] [tool.setuptools.package-data] -"weaveffi" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] +"docs_everywhere" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] # END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__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_docs_everywhere__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__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_docs_everywhere__weaveffi_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_py_typed.snap deleted file mode 100644 index 501a8090..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_py_typed.snap +++ /dev/null @@ -1,6 +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 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 deleted file mode 100644 index 15f821b9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,790 +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): - """An error reported through the C ABI. - - Positive codes are a module's declared error codes (raised as the - module's typed subclasses); negative codes are runtime traps: a generic - producer error (-1), a producer panic (-2), a marshalling failure (-3), - or an exception raised by a callback-interface implementation (-4). - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - - 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 = 2 - - -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) -# Callback-interface trampolines report a failed implementation through -# weaveffi_error_set, which copies the message with the producer's allocator. -_lib.weaveffi_error_set.argtypes = [ - ctypes.POINTER(_WeaveFFIErrorStruct), - ctypes.c_int32, - ctypes.c_char_p, -] -_lib.weaveffi_error_set.restype = None -_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 - - -def _borrow(obj): - """The raw pointer of a live object wrapper, lent to the producer for the - duration of one call. Raises if the wrapper was already closed.""" - _p = obj._ptr - if _p is None: - raise WeaveFFIError(-1, f"{type(obj).__name__} used after close()") - return _p - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - self._objects = [] - - def finish(self) -> bytes: - # Mint the object tokens last: an encoding that raised part-way - # (a closed wrapper, a field of the wrong type) never reaches here, - # so it leaks no strong references. - for _pos, _obj in self._objects: - struct.pack_into(" 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) - - def write_object(self, obj) -> None: - """Write an object token: one strong reference to `obj` the reader - will adopt. The wrapper is checked to be live now (a closed one - raises), but the reference itself is cloned in finish().""" - _borrow(obj) - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -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 read_object(self) -> int: - """Read an object token: one strong reference the caller adopts into - a wrapper. Zero never appears in a non-optional position.""" - _p = struct.unpack(" 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 abc -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 - - -# Live callback-interface implementations, keyed by the integer the producer -# receives as `ctx`. An entry is added when an implementation is passed to a -# call and removed when the producer invokes the vtable's `free(ctx)`, so the -# implementation stays alive exactly as long as the producer may call it and -# the producer never holds a raw reference to a Python object. -_cb_impls: Dict[int, object] = {} -_cb_lock = threading.Lock() -_cb_next_ctx = 0 - - -def _cb_register(impl) -> int: - global _cb_next_ctx - with _cb_lock: - _cb_next_ctx += 1 - _ctx = _cb_next_ctx - _cb_impls[_ctx] = impl - return _ctx - - -# === Module: docs === - -class DocsErrorsError(WeaveFFIError): - """Base exception for the `docs` module's error domain.""" - - -class NotFound(DocsErrorsError): - """Returned when no document matches the requested id""" - - CODE = 1 - - def __init__(self, message: str = "Document not found") -> None: - super().__init__(1, message) - - -class InvalidStatus(DocsErrorsError): - """Raised when attempting to move into a forbidden state""" - - CODE = 2 - - def __init__(self, message: str = "Invalid status transition") -> None: - super().__init__(2, message) - - -DocsErrorsError.NotFound = NotFound -DocsErrorsError.InvalidStatus = InvalidStatus - - -_DOCS_ERRORS_ERROR_CODES: Dict[int, type] = { - 1: NotFound, - 2: InvalidStatus, -} - - -def _docs_errors_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the DocsErrorsError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _DOCS_ERRORS_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_docs_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 _docs_errors_error_from(code, message, payload) - - -class Status(IntEnum): - """ - Lifecycle status of a document. - - Used to drive UI badges and filters. - """ - # Document is being edited - Draft = 0 - # Document is publicly visible - Published = 1 - # Document is hidden from listings - Archived = 2 - - -@dataclass -class Document: - """ - Represents a single document tracked by the system. - - Documents are persisted to disk and exposed via the public API. - """ - - # Stable opaque identifier - id: int - # Human-readable title shown in lists - title: str - # Current lifecycle status - status: "Status" - # Free-form tags for grouping - tags: List[str] - - -def _write_Document(_w: _BufferWriter, value: "Document") -> None: - _w.write_i64(value.id) - _w.write_string(value.title) - _w.write_i32(value.status) - _w.write_len(len(value.tags)) - for _e0 in value.tags: - _w.write_string(_e0) - - -def _read_Document(_r: _BufferReader) -> "Document": - return Document( - id=_r.read_i64(), - title=_r.read_string(), - status=Status(_r.read_i32()), - tags=[_r.read_string() for _i0 in range(_r.read_len())], - ) - - -def _pack_Document(value: "Document") -> bytes: - _w = _BufferWriter() - _write_Document(_w, value) - return _w.finish() - - -def _unpack_Document(data: bytes) -> "Document": - return _decode_buffer(data, _read_Document) - - -class SavedListener(abc.ABC): - """ - Consumer-implemented observer of document-saved notifications. - - Implementations must be safe to call from any thread. - - Consumer-implemented callback interface. Subclass it, implement every abstract method, and pass an instance wherever the API takes a `SavedListener`; the producer may call the methods from any thread until it releases the instance. An exception raised by a method is reported to the producer as WeaveFFIError.FOREIGN_ERROR_CODE (-4) and aborts the call that was in progress. - """ - - @abc.abstractmethod - def on_saved(self, doc_id: int, revision: int) -> None: - """Fires after a document has been persisted successfully - - Parameters - ---------- - doc_id : Identifier of the saved document - revision : Monotonically increasing revision - """ - - -_SavedListener_on_saved_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int64, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)) -_SavedListener_vtable_free_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p) - - -class _SavedListenerVtable(ctypes.Structure): - """The C vtable `weaveffi_docs_SavedListener_vtable`.""" - - _fields_ = [ - ("on_saved", _SavedListener_on_saved_cfunctype), - ("free", _SavedListener_vtable_free_cfunctype), - ] - - -def _SavedListener_on_saved_trampoline(ctx, doc_id, revision, out_err): - try: - _impl = _cb_impls[ctx] - _impl.on_saved(doc_id, revision) - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - - -def _SavedListener_vtable_free_trampoline(ctx): - # The producer's last reference is gone; it never touches `ctx` again. - _cb_impls.pop(ctx, None) - - -# The one process-wide vtable for SavedListener. The CFUNCTYPE objects are held at -# module scope so their function pointers stay valid for the process lifetime. -_SavedListener_on_saved_cfunc = _SavedListener_on_saved_cfunctype(_SavedListener_on_saved_trampoline) -_SavedListener_vtable_free_cfunc = _SavedListener_vtable_free_cfunctype(_SavedListener_vtable_free_trampoline) -_SavedListener_vtable = _SavedListenerVtable(_SavedListener_on_saved_cfunc, _SavedListener_vtable_free_cfunc) - - -_lib.weaveffi_docs_Notebook_clone.argtypes = [ctypes.c_void_p] -_lib.weaveffi_docs_Notebook_clone.restype = ctypes.c_void_p -_lib.weaveffi_docs_Notebook_destroy.argtypes = [ctypes.c_void_p] -_lib.weaveffi_docs_Notebook_destroy.restype = None - - -class Notebook: - """ - An ordered collection of documents. - - Notebooks own their documents and release them when destroyed. - """ - - @classmethod - def _from_ptr(cls, ptr) -> "Notebook": - """Adopt one strong reference the producer handed over.""" - _obj = cls.__new__(cls) - _obj._ptr = ptr - return _obj - - def __init__(self) -> None: - """Create an empty notebook - """ - self._ptr = None - _fn = _lib.weaveffi_docs_Notebook_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 _clone_ptr(self): - """A new strong reference to the same object (a raw pointer the - receiver must eventually release), leaving this wrapper's own - reference untouched.""" - return _lib.weaveffi_docs_Notebook_clone(_borrow(self)) - - def close(self) -> None: - """Release this wrapper's reference. The object itself is dropped - when its last reference (here or in the producer) is released. - Idempotent; the wrapper is unusable afterwards.""" - _p = getattr(self, "_ptr", None) - self._ptr = None - if _p is not None: - _lib.weaveffi_docs_Notebook_destroy(_p) - - def __enter__(self) -> "Notebook": - return self - - def __exit__(self, *exc) -> bool: - self.close() - return False - - def __del__(self) -> None: - self.close() - - def insert(self, document: "Document") -> int: - """Add a document, returning its position in the notebook - - Parameters - ---------- - document : The document to add - """ - _fn = _lib.weaveffi_docs_Notebook_insert - _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _document_buf = _pack_Document(document) - _err = _WeaveFFIErrorStruct() - _result = _fn(_borrow(self), _document_buf, len(_document_buf), ctypes.byref(_err)) - _check_error(_err) - return _result - - - def find(self, id: int) -> "Document": - """Look up a document by identifier, failing when none matches - - Parameters - ---------- - id : Identifier of the document to find - - Raises - ------ - DocsErrorsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_docs_Notebook_find - _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(_borrow(self), id, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_docs_errors_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Document(_data) - - - def size(self) -> int: - """The number of documents currently held - """ - _fn = _lib.weaveffi_docs_Notebook_size - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(_borrow(self), ctypes.byref(_err)) - _check_error(_err) - return _result - - - @staticmethod - def max_documents() -> int: - """The largest number of documents one notebook will hold - """ - _fn = _lib.weaveffi_docs_Notebook_max_documents - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _result - - - -def create_document(title: str, tags: List[str]) -> "Document": - """ - Create a brand new document. - - The document starts in `Draft` status; callers must publish - it explicitly to make it visible. - - Parameters - ---------- - title : Human-readable title for the new document - tags : Initial set of tags (may be empty) - """ - _fn = _lib.weaveffi_docs_create_document - _fn.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _tags_w = _BufferWriter() - _tags_w.write_len(len(tags)) - for _e0 in tags: - _tags_w.write_string(_e0) - _tags_buf = _tags_w.finish() - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(_string_to_bytes(title), _tags_buf, len(_tags_buf), ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Document(_data) - - -def get_document(id: int) -> Optional["Document"]: - """Look up a document by identifier - - Parameters - ---------- - id : Identifier returned from create_document - """ - _fn = _lib.weaveffi_docs_get_document - _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_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _decode_buffer(_data, lambda _r: (_read_Document(_r) if _r.read_option_flag() else None)) - - -async def publish_async(id: int) -> bool: - """Publish a document in the background - - Parameters - ---------- - id : Identifier of the document to publish - """ - _fn = _lib.weaveffi_docs_publish_async_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"] = bool(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_int64, _cb_type, ctypes.c_void_p] - _fn.restype = None - _fn(id, _cb, None) - return await _fut - - -def watch(listener: "SavedListener") -> None: - """Register a listener for saved notifications - - Parameters - ---------- - listener : Receiver of saved notifications - """ - _fn = _lib.weaveffi_docs_watch - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_SavedListenerVtable), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = None - _listener_ctx = _cb_register(listener) - _err = _WeaveFFIErrorStruct() - _fn(_listener_ctx, ctypes.byref(_SavedListener_vtable), ctypes.byref(_err)) - _check_error(_err) - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__README_md.snap index e3b8a7b9..a2344bbc 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__README_md.snap @@ -1,18 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Python) +# edge_cases (Python) -Auto-generated Python bindings using ctypes. +Python bindings for the `edge_cases` native library, built on `ctypes`. -## Prerequisites +## Requirements -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. +- Python 3.9 or later +- The native library (`libedge_cases.so`, `libedge_cases.dylib`, or `edge_cases.dll`) ## Install @@ -20,16 +20,16 @@ Auto-generated Python bindings using ctypes. pip install . ``` -## Development install +## Loading the native library -```bash -pip install -e . -``` +The bindings load the library from `EDGE_CASES_LIBRARY` when it is set (a full path), +then from inside the installed package, then from the system loader's search +path. ## Usage ```python -from weaveffi import * +import edge_cases ``` diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases___init___py.snap new file mode 100644 index 00000000..3b6fde75 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases___init___py.snap @@ -0,0 +1,10 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +from .edge_cases import * # noqa: F401,F403 + +# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_edge_cases_py.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_edge_cases_py.snap new file mode 100644 index 00000000..e7a587f3 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_edge_cases_py.snap @@ -0,0 +1,2042 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +"""ctypes bindings for the edge_cases native library.""" +import abc +import asyncio +import builtins +import ctypes +import os +import platform +import struct +import threading +import warnings +from dataclasses import dataclass +from enum import IntEnum +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type + + +class Error(Exception): + """An error reported by the native library. + + `code` is positive for one of the API's declared error codes (raised as + the declaring module's typed subclasses) and negative for a runtime trap: + a generic failure (-1), a producer panic (-2), a marshalling failure + (-3), or an exception raised by a callback-interface implementation + (-4). A cancelled async call (-5) raises `asyncio.CancelledError` + instead. + """ + + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + FOREIGN_ERROR_CODE = -4 + CANCELLED_ERROR_CODE = -5 + + def __init__(self, code: int, message: str) -> None: + super().__init__(f"({code}) {message}") + self.code = code + self.message = message + + +class _ErrorStruct(ctypes.Structure): + """The C `edge_cases_error` out-parameter every call reports through.""" + + _fields_ = [ + ("code", ctypes.c_int32), + ("message", ctypes.c_char_p), + ("payload_ptr", ctypes.c_void_p), + ("payload_len", ctypes.c_size_t), + ] + + +def _load_library() -> ctypes.CDLL: + # An explicit path wins; then a library bundled inside this package (a + # platform wheel); then the system loader's search path. + override = os.environ.get("EDGE_CASES_LIBRARY") + if override: + return ctypes.CDLL(override) + system = platform.system() + if system == "Darwin": + name = "libedge_cases.dylib" + elif system == "Windows": + name = "edge_cases.dll" + else: + name = "libedge_cases.so" + bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) + if os.path.exists(bundled): + return ctypes.CDLL(bundled) + try: + return ctypes.CDLL(name) + except OSError as exc: + raise ImportError( + f"cannot load the native library {name!r} ({exc}); " + "set EDGE_CASES_LIBRARY to its path" + ) from exc + + +_lib = _load_library() + + +def _bind(name: str, restype: Any, *argtypes: Any) -> Any: + """Look up one exported symbol and fix its C signature. Every prototype + is bound once, at import time.""" + fn = getattr(_lib, name) + fn.restype = restype + fn.argtypes = argtypes + return fn + + +# The C ABI revision these bindings were generated against, and the contract +# checksum of every top-level module: (module, checksum symbol, value). +_ABI_VERSION = 3 +_CHECKSUMS: List[Tuple[str, str, int]] = [ + ("edge_cases", "edge_cases_edge_cases_checksum", 0x1567f441073bd4aa), +] + + +def _check_contract() -> None: + """Refuse to load a library built for a different ABI revision or a + different version of any module's API, before any other symbol is + bound.""" + try: + abi_version = _bind("edge_cases_abi_version", ctypes.c_uint32) + except AttributeError: + raise ImportError( + f"{_lib._name} does not export edge_cases_abi_version; " + "it is not the library these bindings were generated for" + ) from None + found = abi_version() + if found != _ABI_VERSION: + raise ImportError( + f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " + f"but {_lib._name} reports revision {found}" + ) + for module, symbol, expected in _CHECKSUMS: + try: + checksum = _bind(symbol, ctypes.c_uint64) + except AttributeError: + raise ImportError( + f"module {module!r}: {_lib._name} does not export {symbol}" + ) from None + found = checksum() + if found != expected: + raise ImportError( + f"module {module!r}: the native library's contract checksum " + f"{found:#018x} does not match these bindings ({expected:#018x}); " + "regenerate the bindings from the library's current API" + ) + + +_check_contract() + +_error_clear = _bind("edge_cases_error_clear", None, ctypes.POINTER(_ErrorStruct)) +_free_bytes = _bind("edge_cases_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) + + +def _debug_live(kind: int) -> int: + """The producer's live-allocation counter `kind` (0 objects, 1 callbacks, + 2 iterators, 3 cancel tokens, 4 returned allocations). Always 0 unless + the producer was built with its leak counters enabled.""" + return _bind("edge_cases_debug_live", ctypes.c_uint64, ctypes.c_int32)(kind) + + +def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: + """Copy a filled out-err slot's code, message, and payload, then release + them.""" + code = err.code + message = err.message.decode("utf-8", "replace") if err.message else "" + payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + _error_clear(ctypes.byref(err)) + return code, message, payload + + +def _error_from(code: int, message: str, payload: bytes = b"") -> Error: + """The exception for a code no declared error domain claims.""" + return Error(code, message) + + +def _check_error(err: _ErrorStruct) -> None: + """Raise for a non-zero out-err slot of a call that declares no errors: + only runtime traps (a panic, a marshalling failure, a failed callback) + land here.""" + if err.code: + raise _error_from(*_read_error(err)) + + +def _take_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a producer-owned byte run (a string, bytes, or value buffer) and + release it with edge_cases_free_bytes.""" + if not ptr: + return b"" + try: + return ctypes.string_at(ptr, length) + finally: + _free_bytes(ptr, length) + + +def _take_str(ptr: Optional[int], length: int) -> str: + """Copy and release a producer-owned UTF-8 string.""" + return _take_bytes(ptr, length).decode("utf-8") + + +def _peek_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a byte run the producer only lends for the current call.""" + return ctypes.string_at(ptr, length) if ptr and length else b"" + + +class _Handle: + """Owns one native reference and releases it exactly once. + + Calls lend the pointer through `_acquire`/`_release`, so `close()` from + another thread while a call is in flight defers the release until the + last such call returns instead of freeing the object mid-call. + """ + + # The native destroy (or iterator release) function, set per subclass. + _destroy: Callable[[int], None] + + def _init_handle(self, ptr: int) -> None: + self._ptr: Optional[int] = ptr + self._calls = 0 + self._closing = False + self._lock = threading.Lock() + + @classmethod + def _adopt(cls, ptr: int) -> Any: + """Wrap one strong reference the producer handed over.""" + obj = cls.__new__(cls) + obj._init_handle(ptr) + return obj + + def _acquire(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise Error(-1, f"{type(self).__name__} used after close()") + self._calls += 1 + return ptr + + def _release(self) -> None: + with self._lock: + self._calls -= 1 + if self._calls or not self._closing: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def close(self) -> None: + """Release this wrapper's native reference. Idempotent; the wrapper + is unusable afterwards.""" + lock = getattr(self, "_lock", None) + if lock is None: + return + with lock: + if self._closing: + return + self._closing = True + if self._calls: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def __enter__(self) -> Any: + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def __del__(self) -> None: + # An unreachable wrapper has no call in flight: release directly. + ptr = getattr(self, "_ptr", None) + if ptr is not None: + self._ptr = None + type(self)._destroy(ptr) + + +class _Object(_Handle): + """Base of every interface wrapper: a reference-counted native object.""" + + # The native clone function, set per subclass. + _clone: Callable[[int], int] + + def _clone_ref(self) -> int: + """A new strong reference to the same object, for a receiver that + adopts it (an object token inside a value buffer).""" + ptr = self._acquire() + try: + return type(self)._clone(ptr) + finally: + self._release() + + +class _Iterator(_Handle): + """Base of every lazy iterator over a producer stream: one native `next` + call per step; the handle is released on exhaustion, `close()`, or + garbage collection.""" + + def __iter__(self) -> Any: + return self + + def _step(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise StopIteration + self._calls += 1 + return ptr + + +def _required(ptr: Optional[int]) -> int: + """A non-null object pointer the producer returned.""" + if not ptr: + raise Error(-1, "the producer returned a null object") + return ptr + + +def _lend(obj: Any, cls: "Type[_Object]") -> int: + """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + return obj._acquire() + + +def _lend_opt(obj: Any, cls: "Type[_Object]") -> Optional[int]: + """`_lend` for an optional object: None lends a null pointer.""" + return None if obj is None else _lend(obj, cls) + + +def _release_opt(obj: Any) -> None: + if obj is not None: + obj._release() + + +_I8 = struct.Struct(" Error: + return Error(-3, f"malformed value buffer: {what}") + + +class _Writer: + """Encodes values in the value-buffer wire format: little-endian, + packed, no alignment.""" + + __slots__ = ("_buf", "_objects") + + def __init__(self) -> None: + self._buf = bytearray() + self._objects: List[Tuple[int, _Object]] = [] + + def finish(self) -> bytes: + # Object tokens are minted last, so an encoding that fails part-way + # never leaks a strong reference. + minted: List[Tuple[_Object, int]] = [] + try: + for pos, obj in self._objects: + ptr = obj._clone_ref() + minted.append((obj, ptr)) + _U64.pack_into(self._buf, pos, ptr) + except BaseException: + for obj, ptr in minted: + type(obj)._destroy(ptr) + raise + self._objects = [] + return bytes(self._buf) + + def write_bool(self, v: bool) -> None: + self._buf.append(1 if v else 0) + + def write_i8(self, v: int) -> None: + self._buf += _I8.pack(v) + + def write_u8(self, v: int) -> None: + self._buf += _U8.pack(v) + + def write_i16(self, v: int) -> None: + self._buf += _I16.pack(v) + + def write_u16(self, v: int) -> None: + self._buf += _U16.pack(v) + + def write_i32(self, v: int) -> None: + self._buf += _I32.pack(v) + + def write_u32(self, v: int) -> None: + self._buf += _U32.pack(v) + + def write_i64(self, v: int) -> None: + self._buf += _I64.pack(v) + + def write_u64(self, v: int) -> None: + self._buf += _U64.pack(v) + + def write_f32(self, v: float) -> None: + self._buf += _F32.pack(v) + + def write_f64(self, v: float) -> None: + self._buf += _F64.pack(v) + + def write_count(self, n: int) -> None: + self._buf += _U32.pack(n) + + def write_flag(self, present: bool) -> None: + self._buf.append(1 if present else 0) + + def write_string(self, v: str) -> None: + data = v.encode("utf-8") + self._buf += _U32.pack(len(data)) + self._buf += data + + def write_bytes(self, v: bytes) -> None: + self._buf += _U32.pack(len(v)) + self._buf += v + + def write_numbers(self, code: str, values: List[Any]) -> None: + """A list of one fixed-width numeric primitive (`code` is its struct + format character), packed in one call.""" + self._buf += _U32.pack(len(values)) + self._buf += struct.pack(f"<{len(values)}{code}", *values) + + def write_object(self, obj: Any, cls: "Type[_Object]") -> None: + """Reserve an object token: one strong reference to `obj` the reader + adopts, cloned in `finish()`. A closed wrapper is rejected now.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + obj._acquire() + obj._release() + self._objects.append((len(self._buf), obj)) + self._buf += bytes(8) + + +class _Reader: + """Decodes values from the value-buffer wire format, rejecting truncated + buffers, invalid flag bytes, invalid UTF-8, and trailing data.""" + + __slots__ = ("_data", "_pos") + + def __init__(self, data: bytes) -> None: + self._data = data + self._pos = 0 + + def _take(self, n: int, what: str) -> int: + pos = self._pos + if len(self._data) - pos < n: + raise _malformed(f"truncated {what}") + self._pos = pos + n + return pos + + def read_bool(self) -> bool: + b = self._data[self._take(1, "bool")] + if b > 1: + raise _malformed("invalid bool byte") + return b == 1 + + def read_i8(self) -> int: + return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) + + def read_u8(self) -> int: + return self._data[self._take(1, "u8")] + + def read_i16(self) -> int: + return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) + + def read_u16(self) -> int: + return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) + + def read_i32(self) -> int: + return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) + + def read_u32(self) -> int: + return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) + + def read_i64(self) -> int: + return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) + + def read_u64(self) -> int: + return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) + + def read_f32(self) -> float: + return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) + + def read_f64(self) -> float: + return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) + + def read_count(self) -> int: + # An element count, not a byte length: elements can encode to zero + # bytes, so it is not bounded by the remaining data. + return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) + + def read_flag(self) -> bool: + b = self._data[self._take(1, "option flag")] + if b > 1: + raise _malformed("invalid option flag") + return b == 1 + + def read_bytes(self) -> bytes: + n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) + pos = self._take(n, "bytes") + return self._data[pos:pos + n] + + def read_string(self) -> str: + try: + return self.read_bytes().decode("utf-8") + except UnicodeDecodeError: + raise _malformed("string is not valid UTF-8") from None + + def read_numbers(self, code: str, size: int) -> List[Any]: + """A list of one fixed-width numeric primitive, unpacked in one call.""" + n = self.read_count() + pos = self._take(n * size, "list") + return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) + + def read_object(self) -> int: + """An object token: one strong reference the caller adopts.""" + ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) + if ptr == 0: + raise _malformed("null object token") + return ptr + + def expect_end(self) -> None: + if self._pos != len(self._data): + raise _malformed("trailing bytes") + + +def _decode(data: bytes, read: Callable[[_Reader], Any]) -> Any: + """Decode exactly one value from `data`.""" + r = _Reader(data) + value = read(r) + r.expect_end() + return value + + +# Async completions. A call registers its future under an integer key, passes +# the key as the launcher's `context`, and the function's static completion +# trampoline settles it from whichever producer thread completes the call. +# The trampolines live at module scope, so nothing a pending call needs can +# be garbage collected before the producer fires the completion, even when +# the awaiting coroutine is gone. +_pending: Dict[int, Tuple[asyncio.AbstractEventLoop, "asyncio.Future[Any]"]] = {} +_pending_lock = threading.Lock() +_pending_next = 0 + +_error_free = _bind("edge_cases_error_free", None, ctypes.POINTER(_ErrorStruct)) + + +def _async_begin() -> Tuple[int, "asyncio.Future[Any]"]: + """Register a new pending call on the running event loop.""" + global _pending_next + loop = asyncio.get_running_loop() + future = loop.create_future() + with _pending_lock: + _pending_next += 1 + key = _pending_next + _pending[key] = (loop, future) + return key, future + + +def _async_abandon(key: int) -> None: + """Forget a call whose launcher never ran.""" + with _pending_lock: + _pending.pop(key, None) + + +def _async_error(err: Any, factory: Callable[[int, str, bytes], BaseException]) -> BaseException: + """Copy and release a heap-boxed completion error, then build the + exception it reports. The cancelled code maps to CancelledError.""" + e = err.contents + code = e.code + message = e.message.decode("utf-8", "replace") if e.message else "" + payload = ctypes.string_at(e.payload_ptr, e.payload_len) if e.payload_ptr else b"" + _error_free(err) + if code == Error.CANCELLED_ERROR_CODE: + return asyncio.CancelledError(message) + return factory(code, message, payload) + + +def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) -> None: + """Hand a completion back to the event loop that awaits it.""" + with _pending_lock: + entry = _pending.pop(key or 0, None) + if entry is None: + return + loop, future = entry + try: + loop.call_soon_threadsafe(_async_resolve, future, exc, value) + except RuntimeError: + # The loop is closed, so nothing awaits the result any more. + pass + + +def _async_resolve(future: "asyncio.Future[Any]", exc: Optional[BaseException], value: Any) -> None: + if future.done(): + return + if isinstance(exc, asyncio.CancelledError): + future.cancel() + elif exc is not None: + future.set_exception(exc) + else: + future.set_result(value) + + +# Cancellation for `cancellable` async functions. Each call creates a token, +# the producer takes its own reference at launch, and cancelling the awaiting +# task cancels the token; the producer then completes the call with the +# cancelled code. +_cancel_token_create = _bind("edge_cases_cancel_token_create", ctypes.c_void_p) +_cancel_token_cancel = _bind("edge_cases_cancel_token_cancel", None, ctypes.c_void_p) +_cancel_token_destroy = _bind("edge_cases_cancel_token_destroy", None, ctypes.c_void_p) + + +async def _async_wait_cancellable(future: "asyncio.Future[Any]", token: int) -> Any: + """Await a cancellable call. Cancelling the awaiting task cancels the + native call and waits for its (cancelled) completion before raising, so + nothing outlives the call.""" + try: + return await asyncio.shield(future) + except asyncio.CancelledError: + _cancel_token_cancel(token) + try: + await future + except BaseException: + pass + raise + finally: + _cancel_token_destroy(token) + + +# Callback-interface implementations the producer holds, keyed by the integer +# it receives as `ctx`. An entry is added when an implementation is passed to +# a call and removed when the producer calls the vtable's `free(ctx)`, so the +# implementation lives exactly as long as the producer may call it and the +# producer never holds a raw reference to a Python object. ctypes acquires +# the GIL on entry to a trampoline, so the producer may call from any thread. +_callbacks: Dict[int, Any] = {} +_callbacks_lock = threading.Lock() +_callbacks_next = 0 + +_error_set = _bind( + "edge_cases_error_set", None, ctypes.POINTER(_ErrorStruct), ctypes.c_int32, ctypes.c_char_p +) + + +def _callback_register(impl: Any, cls: type) -> int: + """Register an implementation for one call; returns its `ctx` key.""" + global _callbacks_next + if not isinstance(impl, cls): + raise TypeError(f"expected {cls.__name__}, got {type(impl).__name__}") + with _callbacks_lock: + _callbacks_next += 1 + ctx = _callbacks_next + _callbacks[ctx] = impl + return ctx + + +def _callback_get(ctx: Optional[int]) -> Any: + return _callbacks[ctx or 0] + + +def _callback_free(ctx: Optional[int]) -> None: + # The producer's last reference is gone; it never passes `ctx` again. + with _callbacks_lock: + _callbacks.pop(ctx or 0, None) + + +def _callback_fail(out_err: Any, exc: BaseException) -> None: + """Report an implementation's exception to the producer, which aborts + its call with the foreign error code. Nothing unwinds through C.""" + message = str(exc) or type(exc).__name__ + _error_set(out_err, Error.FOREIGN_ERROR_CODE, message.encode("utf-8", "replace")) + + +# === Module: edge_cases === + + +class EdgeErrorsError(Error): + """Base exception for the `edge_cases` module's error domain.""" + + Busy: "Type[Busy]" + + +class Busy(EdgeErrorsError): + """Resource is busy""" + + CODE = 7 + + def __init__(self, message: str = "Resource is busy") -> None: + super().__init__(7, message) + + +EdgeErrorsError.Busy = Busy + + +_EDGE_ERRORS_ERROR_CODES: Dict[int, Any] = { + 7: Busy, +} + + +def _edge_errors_error_from(code: int, message: str, payload: bytes = b"") -> Error: + """Build the EdgeErrorsError subclass matching `code`, or the plain + Error for codes outside the domain (panics, marshalling).""" + cls = _EDGE_ERRORS_ERROR_CODES.get(code) + if cls is None: + return Error(code, message) + exc: Any = cls(message) if message else cls() + return exc + + +def _check_edge_errors_error(err: _ErrorStruct) -> None: + if err.code: + raise _edge_errors_error_from(*_read_error(err)) + + +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: _Writer, value: "Leaf") -> None: + _w.write_f32(value.weight) + + +def _read_Leaf(_r: _Reader) -> "Leaf": + return Leaf( + weight=_r.read_f32(), + ) + + +@dataclass +class Inner: + """Middle layer of a three-deep struct nesting""" + + leaf: "Leaf" + leaves: List["Leaf"] + + +def _write_Inner(_w: _Writer, value: "Inner") -> None: + _write_Leaf(_w, value.leaf) + _w.write_count(len(value.leaves)) + for _e0 in value.leaves: + _write_Leaf(_w, _e0) + + +def _read_Inner(_r: _Reader) -> "Inner": + return Inner( + leaf=_read_Leaf(_r), + leaves=[_read_Leaf(_r) for _i0 in range(_r.read_count())], + ) + + +@dataclass +class Outer: + inner: "Inner" + maybe_inner: Optional["Inner"] + by_name: Dict[str, "Inner"] + modes: List["Mode"] + matrix: List[List[int]] + sparse: List[Optional[int]] + blob: Optional[bytes] + note: Optional[str] + by_id: Dict[int, str] + _hidden: bool + v2: int + + +def _write_Outer(_w: _Writer, value: "Outer") -> None: + _write_Inner(_w, value.inner) + if value.maybe_inner is None: + _w.write_flag(False) + else: + _w.write_flag(True) + _write_Inner(_w, value.maybe_inner) + _w.write_count(len(value.by_name)) + for _k0, _v0 in value.by_name.items(): + _w.write_string(_k0) + _write_Inner(_w, _v0) + _w.write_count(len(value.modes)) + for _e1 in value.modes: + _w.write_i32(_e1) + _w.write_count(len(value.matrix)) + for _e2 in value.matrix: + _w.write_numbers("i", _e2) + _w.write_count(len(value.sparse)) + for _e3 in value.sparse: + if _e3 is None: + _w.write_flag(False) + else: + _w.write_flag(True) + _w.write_i64(_e3) + if value.blob is None: + _w.write_flag(False) + else: + _w.write_flag(True) + _w.write_bytes(value.blob) + if value.note is None: + _w.write_flag(False) + else: + _w.write_flag(True) + _w.write_string(value.note) + _w.write_count(len(value.by_id)) + for _k4, _v4 in value.by_id.items(): + _w.write_i64(_k4) + _w.write_string(_v4) + _w.write_bool(value._hidden) + _w.write_u8(value.v2) + + +def _read_Outer(_r: _Reader) -> "Outer": + return Outer( + inner=_read_Inner(_r), + maybe_inner=(_read_Inner(_r) if _r.read_flag() else None), + by_name=dict((_r.read_string(), _read_Inner(_r)) for _i0 in range(_r.read_count())), + modes=[Mode(_r.read_i32()) for _i0 in range(_r.read_count())], + matrix=[_r.read_numbers("i", 4) for _i0 in range(_r.read_count())], + sparse=[(_r.read_i64() if _r.read_flag() else None) for _i0 in range(_r.read_count())], + blob=(_r.read_bytes() if _r.read_flag() else None), + note=(_r.read_string() if _r.read_flag() else None), + by_id=dict((_r.read_i64(), _r.read_string()) for _i0 in range(_r.read_count())), + _hidden=_r.read_bool(), + v2=_r.read_u8(), + ) + + +@dataclass +class Keywords: + """Every field is a reserved word somewhere""" + + class_: builtins.int + def_: builtins.int + import_: builtins.int + from_: builtins.int + func: builtins.int + range: builtins.int + register: builtins.int + volatile: builtins.int + new: builtins.int + default: builtins.int + end: builtins.int + in_: builtins.int + is_: builtins.int + int: builtins.int + var: builtins.int + val: builtins.int + self: builtins.int + this: builtins.int + object: builtins.int + string: builtins.int + case: builtins.int + select: builtins.int + switch: builtins.int + void: builtins.int + union: builtins.int + namespace: builtins.int + operator: builtins.int + template: builtins.int + package: builtins.int + protocol: builtins.int + guard: builtins.int + defer: builtins.int + inout: builtins.int + internal: builtins.int + when: builtins.int + fun: builtins.int + data: builtins.int + late: builtins.int + part: builtins.int + out: builtins.int + ref: builtins.int + event: builtins.int + base: builtins.int + lambda_: builtins.int + yield_: builtins.int + nil: builtins.int + null: builtins.int + begin: builtins.int + rescue: builtins.int + module: builtins.int + chan: builtins.int + go: builtins.int + let: builtins.int + export: builtins.int + delete: builtins.int + typedef: builtins.int + sizeof: builtins.int + static: builtins.int + super: builtins.int + + +def _write_Keywords(_w: _Writer, value: "Keywords") -> None: + _w.write_i32(value.class_) + _w.write_i32(value.def_) + _w.write_i32(value.import_) + _w.write_i32(value.from_) + _w.write_i32(value.func) + _w.write_i32(value.range) + _w.write_i32(value.register) + _w.write_i32(value.volatile) + _w.write_i32(value.new) + _w.write_i32(value.default) + _w.write_i32(value.end) + _w.write_i32(value.in_) + _w.write_i32(value.is_) + _w.write_i32(value.int) + _w.write_i32(value.var) + _w.write_i32(value.val) + _w.write_i32(value.self) + _w.write_i32(value.this) + _w.write_i32(value.object) + _w.write_i32(value.string) + _w.write_i32(value.case) + _w.write_i32(value.select) + _w.write_i32(value.switch) + _w.write_i32(value.void) + _w.write_i32(value.union) + _w.write_i32(value.namespace) + _w.write_i32(value.operator) + _w.write_i32(value.template) + _w.write_i32(value.package) + _w.write_i32(value.protocol) + _w.write_i32(value.guard) + _w.write_i32(value.defer) + _w.write_i32(value.inout) + _w.write_i32(value.internal) + _w.write_i32(value.when) + _w.write_i32(value.fun) + _w.write_i32(value.data) + _w.write_i32(value.late) + _w.write_i32(value.part) + _w.write_i32(value.out) + _w.write_i32(value.ref) + _w.write_i32(value.event) + _w.write_i32(value.base) + _w.write_i32(value.lambda_) + _w.write_i32(value.yield_) + _w.write_i32(value.nil) + _w.write_i32(value.null) + _w.write_i32(value.begin) + _w.write_i32(value.rescue) + _w.write_i32(value.module) + _w.write_i32(value.chan) + _w.write_i32(value.go) + _w.write_i32(value.let) + _w.write_i32(value.export) + _w.write_i32(value.delete) + _w.write_i32(value.typedef) + _w.write_i32(value.sizeof) + _w.write_i32(value.static) + _w.write_i32(value.super) + + +def _read_Keywords(_r: _Reader) -> "Keywords": + return Keywords( + class_=_r.read_i32(), + def_=_r.read_i32(), + import_=_r.read_i32(), + from_=_r.read_i32(), + func=_r.read_i32(), + range=_r.read_i32(), + register=_r.read_i32(), + volatile=_r.read_i32(), + new=_r.read_i32(), + default=_r.read_i32(), + end=_r.read_i32(), + in_=_r.read_i32(), + is_=_r.read_i32(), + int=_r.read_i32(), + var=_r.read_i32(), + val=_r.read_i32(), + self=_r.read_i32(), + this=_r.read_i32(), + object=_r.read_i32(), + string=_r.read_i32(), + case=_r.read_i32(), + select=_r.read_i32(), + switch=_r.read_i32(), + void=_r.read_i32(), + union=_r.read_i32(), + namespace=_r.read_i32(), + operator=_r.read_i32(), + template=_r.read_i32(), + package=_r.read_i32(), + protocol=_r.read_i32(), + guard=_r.read_i32(), + defer=_r.read_i32(), + inout=_r.read_i32(), + internal=_r.read_i32(), + when=_r.read_i32(), + fun=_r.read_i32(), + data=_r.read_i32(), + late=_r.read_i32(), + part=_r.read_i32(), + out=_r.read_i32(), + ref=_r.read_i32(), + event=_r.read_i32(), + base=_r.read_i32(), + lambda_=_r.read_i32(), + yield_=_r.read_i32(), + nil=_r.read_i32(), + null=_r.read_i32(), + begin=_r.read_i32(), + rescue=_r.read_i32(), + module=_r.read_i32(), + chan=_r.read_i32(), + go=_r.read_i32(), + let=_r.read_i32(), + export=_r.read_i32(), + delete=_r.read_i32(), + typedef=_r.read_i32(), + sizeof=_r.read_i32(), + static=_r.read_i32(), + super=_r.read_i32(), + ) + + +@dataclass +class Retired: + """Deprecated struct""" + + id: int + + +def _write_Retired(_w: _Writer, value: "Retired") -> None: + _w.write_i64(value.id) + + +def _read_Retired(_r: _Reader) -> "Retired": + return Retired( + id=_r.read_i64(), + ) + + +@dataclass +class WidgetHolder: + """Record carrying objects in every buffered position""" + + primary: "Widget" + fallback: Optional["Widget"] + extras: List["Widget"] + by_name: Dict[str, "Widget"] + + +def _write_WidgetHolder(_w: _Writer, value: "WidgetHolder") -> None: + _w.write_object(value.primary, Widget) + if value.fallback is None: + _w.write_flag(False) + else: + _w.write_flag(True) + _w.write_object(value.fallback, Widget) + _w.write_count(len(value.extras)) + for _e0 in value.extras: + _w.write_object(_e0, Widget) + _w.write_count(len(value.by_name)) + for _k1, _v1 in value.by_name.items(): + _w.write_string(_k1) + _w.write_object(_v1, Widget) + + +def _read_WidgetHolder(_r: _Reader) -> "WidgetHolder": + return WidgetHolder( + primary=Widget._adopt(_r.read_object()), + fallback=(Widget._adopt(_r.read_object()) if _r.read_flag() else None), + extras=[Widget._adopt(_r.read_object()) for _i0 in range(_r.read_count())], + by_name=dict((_r.read_string(), Widget._adopt(_r.read_object())) for _i0 in range(_r.read_count())), + ) + + +class BatchListener(abc.ABC): + """ + Callback interface carrying composite payloads and objects + + Consumer-implemented callback interface. Subclass it, implement every + abstract method, and pass an instance wherever the API takes a `BatchListener`; + the producer may call the methods from any thread until it releases the + instance. An exception raised by a method is reported to the producer, + which fails the call in progress with Error.FOREIGN_ERROR_CODE (-4). + """ + + @abc.abstractmethod + def on_batch(self, items: List["Outer"], raw: bytes, first: Optional["Outer"], mode: "Mode") -> None: + """Composite payloads cross as value buffers""" + + @abc.abstractmethod + def on_widget(self, widget: "Widget", fallback: Optional["Widget"], children: List["Widget"]) -> None: + """Objects cross as strong references the consumer must release""" + + @abc.abstractmethod + def on_tick(self) -> None: + ... + + @abc.abstractmethod + def should_continue(self, class_: int) -> bool: + """Scalar return travels back through the vtable""" + + @abc.abstractmethod + def pick_mode(self) -> "Mode": + """Enum return travels back as its integer discriminant""" + + +_BatchListener_on_batch_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) +_BatchListener_on_widget_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) +_BatchListener_on_tick_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_BatchListener_should_continue_t = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) +_BatchListener_pick_mode_t = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_BatchListener_free_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p) + + +class _BatchListenerVtable(ctypes.Structure): + """The C vtable `edge_cases_edge_cases_BatchListener_vtable`.""" + + _fields_ = [ + ("on_batch", _BatchListener_on_batch_t), + ("on_widget", _BatchListener_on_widget_t), + ("on_tick", _BatchListener_on_tick_t), + ("should_continue", _BatchListener_should_continue_t), + ("pick_mode", _BatchListener_pick_mode_t), + ("free", _BatchListener_free_t), + ] + + +def _BatchListener_on_batch(ctx, items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, out_err) -> None: + try: + _callback_get(ctx).on_batch(_decode(_peek_bytes(items_ptr, items_len), lambda _r: [_read_Outer(_r) for _i0 in range(_r.read_count())]), _peek_bytes(raw_ptr, raw_len), _decode(_peek_bytes(first_ptr, first_len), lambda _r: (_read_Outer(_r) if _r.read_flag() else None)), Mode(mode)) + except BaseException as exc: + _callback_fail(out_err, exc) + + +def _BatchListener_on_widget(ctx, widget, fallback, children_ptr, children_len, out_err) -> None: + try: + _callback_get(ctx).on_widget(Widget._adopt(_required(widget)), (Widget._adopt(fallback) if fallback else None), _decode(_peek_bytes(children_ptr, children_len), lambda _r: [Widget._adopt(_r.read_object()) for _i0 in range(_r.read_count())])) + except BaseException as exc: + _callback_fail(out_err, exc) + + +def _BatchListener_on_tick(ctx, out_err) -> None: + try: + _callback_get(ctx).on_tick() + except BaseException as exc: + _callback_fail(out_err, exc) + + +def _BatchListener_should_continue(ctx, class_, out_err) -> bool: + try: + return bool(_callback_get(ctx).should_continue(class_)) + except BaseException as exc: + _callback_fail(out_err, exc) + return False + + +def _BatchListener_pick_mode(ctx, out_err) -> int: + try: + return int(_callback_get(ctx).pick_mode()) + except BaseException as exc: + _callback_fail(out_err, exc) + return 0 + + +_BatchListener_vtable = _BatchListenerVtable( + _BatchListener_on_batch_t(_BatchListener_on_batch), + _BatchListener_on_widget_t(_BatchListener_on_widget), + _BatchListener_on_tick_t(_BatchListener_on_tick), + _BatchListener_should_continue_t(_BatchListener_should_continue), + _BatchListener_pick_mode_t(_BatchListener_pick_mode), + _BatchListener_free_t(_callback_free), +) + + +class Keyword(abc.ABC): + """ + Callback interface whose name and methods are keywords + + Consumer-implemented callback interface. Subclass it, implement every + abstract method, and pass an instance wherever the API takes a `Keyword`; + the producer may call the methods from any thread until it releases the + instance. An exception raised by a method is reported to the producer, + which fails the call in progress with Error.FOREIGN_ERROR_CODE (-4). + """ + + @abc.abstractmethod + def delete(self, self_: int) -> None: + ... + + @abc.abstractmethod + def import_(self) -> int: + ... + + +_Keyword_delete_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) +_Keyword_import_t = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_Keyword_free_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p) + + +class _KeywordVtable(ctypes.Structure): + """The C vtable `edge_cases_edge_cases_Keyword_vtable`.""" + + _fields_ = [ + ("delete", _Keyword_delete_t), + ("import", _Keyword_import_t), + ("free", _Keyword_free_t), + ] + + +def _Keyword_delete(ctx, self_, out_err) -> None: + try: + _callback_get(ctx).delete(self_) + except BaseException as exc: + _callback_fail(out_err, exc) + + +def _Keyword_import(ctx, out_err) -> int: + try: + return int(_callback_get(ctx).import_()) + except BaseException as exc: + _callback_fail(out_err, exc) + return 0 + + +_Keyword_vtable = _KeywordVtable( + _Keyword_delete_t(_Keyword_delete), + _Keyword_import_t(_Keyword_import), + _Keyword_free_t(_callback_free), +) + + +_c_edge_cases_Widget_refresh = _bind("edge_cases_edge_cases_Widget_refresh", None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_Widget_rename = _bind("edge_cases_edge_cases_Widget_rename", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +_c_edge_cases_Widget_fetch_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_edge_cases_Widget_fetch = _bind("edge_cases_edge_cases_Widget_fetch", None, ctypes.c_void_p, ctypes.c_int64, _c_edge_cases_Widget_fetch_callback, ctypes.c_void_p) + + +def _edge_cases_Widget_fetch_complete(context, err, result_ptr, result_len) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _edge_errors_error_from), None) + else: + _async_settle(context, None, _decode(_take_bytes(result_ptr, result_len), _read_Outer)) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_Widget_fetch_completion = _c_edge_cases_Widget_fetch_callback(_edge_cases_Widget_fetch_complete) + + +_c_edge_cases_Widget_old_name = _bind("edge_cases_edge_cases_Widget_old_name", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_Widget_clone = _bind("edge_cases_edge_cases_Widget_clone", ctypes.c_void_p, ctypes.c_void_p) +_c_edge_cases_Widget_destroy = _bind("edge_cases_edge_cases_Widget_destroy", None, ctypes.c_void_p) + + +class Widget(_Object): + """Interface with no constructor; instances come from make_widget""" + + _destroy = staticmethod(_c_edge_cases_Widget_destroy) + _clone = staticmethod(_c_edge_cases_Widget_clone) + + def __init__(self) -> None: + raise TypeError("Widget cannot be instantiated directly") + + def refresh(self) -> None: + """Void method that can fail + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _err = _ErrorStruct() + _self_p = self._acquire() + try: + _c_edge_cases_Widget_refresh(_self_p, ctypes.byref(_err)) + finally: + self._release() + _check_edge_errors_error(_err) + + def rename(self, class_: str, import_: Optional[str]) -> str: + _class__b = class_.encode("utf-8") + _import__w = _Writer() + if import_ is None: + _import__w.write_flag(False) + else: + _import__w.write_flag(True) + _import__w.write_string(import_) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _self_p = self._acquire() + try: + _import__b = _import__w.finish() + _ret = _c_edge_cases_Widget_rename(_self_p, _class__b, len(_class__b), _import__b, len(_import__b), ctypes.byref(_out_len), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + return _take_str(_ret, _out_len.value) + + async def fetch(self, id: int) -> "Outer": + """Async method with a struct result + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _call, _future = _async_begin() + try: + _self_p = self._acquire() + try: + _c_edge_cases_Widget_fetch(_self_p, id, _edge_cases_Widget_fetch_completion, _call) + finally: + self._release() + except BaseException: + _async_abandon(_call) + raise + return await _future + + def old_name(self) -> str: + warnings.warn("Use rename instead", DeprecationWarning, stacklevel=2) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _self_p = self._acquire() + try: + _ret = _c_edge_cases_Widget_old_name(_self_p, ctypes.byref(_out_len), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + return _take_str(_ret, _out_len.value) + + +_c_edge_cases_Vault_open = _bind("edge_cases_edge_cases_Vault_open", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_Vault_peek = _bind("edge_cases_edge_cases_Vault_peek", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_Vault_probe = _bind("edge_cases_edge_cases_Vault_probe", ctypes.c_bool, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_Vault_clone = _bind("edge_cases_edge_cases_Vault_clone", ctypes.c_void_p, ctypes.c_void_p) +_c_edge_cases_Vault_destroy = _bind("edge_cases_edge_cases_Vault_destroy", None, ctypes.c_void_p) + + +class Vault(_Object): + """Deprecated interface with a throwing constructor""" + + _destroy = staticmethod(_c_edge_cases_Vault_destroy) + _clone = staticmethod(_c_edge_cases_Vault_clone) + + def __init__(self) -> None: + raise TypeError("Vault cannot be instantiated directly") + + @classmethod + def open(cls, path: str) -> "Vault": + """Fails when the vault is busy + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _path_b = path.encode("utf-8") + _err = _ErrorStruct() + _ret = _c_edge_cases_Vault_open(_path_b, len(_path_b), ctypes.byref(_err)) + _check_edge_errors_error(_err) + return cls._adopt(_required(_ret)) + + def peek(self) -> Optional["Widget"]: + _err = _ErrorStruct() + _self_p = self._acquire() + try: + _ret = _c_edge_cases_Vault_peek(_self_p, ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + return (Widget._adopt(_ret) if _ret else None) + + @staticmethod + def probe() -> bool: + """Throwing static + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _err = _ErrorStruct() + _ret = _c_edge_cases_Vault_probe(ctypes.byref(_err)) + _check_edge_errors_error(_err) + return _ret + + +_c_edge_cases_noop = _bind("edge_cases_edge_cases_noop", None, ctypes.POINTER(_ErrorStruct)) + + +def noop() -> None: + """No parameters and no result""" + _err = _ErrorStruct() + _c_edge_cases_noop(ctypes.byref(_err)) + _check_error(_err) + + +_c_edge_cases_select = _bind("edge_cases_edge_cases_select", ctypes.c_int32, ctypes.c_int32, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool, ctypes.POINTER(_ErrorStruct)) + + +def select(class_: int, from_: str, func: List[str], range_: Optional[int], type: bool) -> int: + """Function named after a keyword whose parameters are all keywords""" + _from__b = from_.encode("utf-8") + _func_w = _Writer() + _func_w.write_count(len(func)) + for _e0 in func: + _func_w.write_string(_e0) + _range__w = _Writer() + if range_ is None: + _range__w.write_flag(False) + else: + _range__w.write_flag(True) + _range__w.write_i32(range_) + _err = _ErrorStruct() + _func_b = _func_w.finish() + _range__b = _range__w.finish() + _ret = _c_edge_cases_select(class_, _from__b, len(_from__b), _func_b, len(_func_b), _range__b, len(_range__b), type, ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_edge_cases_nested_optional = _bind("edge_cases_edge_cases_nested_optional", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def nested_optional(items: Optional[List[Optional["Outer"]]]) -> Optional[List[Optional["Outer"]]]: + """Optional list of optional structs, the deepest legal nesting""" + _items_w = _Writer() + if items is None: + _items_w.write_flag(False) + else: + _items_w.write_flag(True) + _items_w.write_count(len(items)) + for _e0 in items: + if _e0 is None: + _items_w.write_flag(False) + else: + _items_w.write_flag(True) + _write_Outer(_items_w, _e0) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _items_b = _items_w.finish() + _ret = _c_edge_cases_nested_optional(_items_b, len(_items_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), lambda _r: ([(_read_Outer(_r) if _r.read_flag() else None) for _i0 in range(_r.read_count())] if _r.read_flag() else None)) + + +_c_edge_cases_optional_params = _bind("edge_cases_edge_cases_optional_params", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def optional_params(text: Optional[str], count: Optional[int], outer: Optional["Outer"], tags: Optional[List[str]], blob: Optional[bytes], mode: Optional["Mode"]) -> Optional["Mode"]: + _text_w = _Writer() + if text is None: + _text_w.write_flag(False) + else: + _text_w.write_flag(True) + _text_w.write_string(text) + _count_w = _Writer() + if count is None: + _count_w.write_flag(False) + else: + _count_w.write_flag(True) + _count_w.write_i32(count) + _outer_w = _Writer() + if outer is None: + _outer_w.write_flag(False) + else: + _outer_w.write_flag(True) + _write_Outer(_outer_w, outer) + _tags_w = _Writer() + if tags is None: + _tags_w.write_flag(False) + else: + _tags_w.write_flag(True) + _tags_w.write_count(len(tags)) + for _e0 in tags: + _tags_w.write_string(_e0) + _blob_w = _Writer() + if blob is None: + _blob_w.write_flag(False) + else: + _blob_w.write_flag(True) + _blob_w.write_bytes(blob) + _mode_w = _Writer() + if mode is None: + _mode_w.write_flag(False) + else: + _mode_w.write_flag(True) + _mode_w.write_i32(mode) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _text_b = _text_w.finish() + _count_b = _count_w.finish() + _outer_b = _outer_w.finish() + _tags_b = _tags_w.finish() + _blob_b = _blob_w.finish() + _mode_b = _mode_w.finish() + _ret = _c_edge_cases_optional_params(_text_b, len(_text_b), _count_b, len(_count_b), _outer_b, len(_outer_b), _tags_b, len(_tags_b), _blob_b, len(_blob_b), _mode_b, len(_mode_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), lambda _r: (Mode(_r.read_i32()) if _r.read_flag() else None)) + + +_c_edge_cases_register_batch = _bind("edge_cases_edge_cases_register_batch", None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) + + +def register_batch(listener: "BatchListener", keyword: "Keyword") -> None: + """Two callback interface parameters on one function""" + _err = _ErrorStruct() + _listener_ctx = _callback_register(listener, BatchListener) + _keyword_ctx = _callback_register(keyword, Keyword) + _c_edge_cases_register_batch(_listener_ctx, ctypes.addressof(_BatchListener_vtable), _keyword_ctx, ctypes.addressof(_Keyword_vtable), ctypes.byref(_err)) + _check_error(_err) + + +_c_edge_cases_widgets_everywhere = _bind("edge_cases_edge_cases_widgets_everywhere", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def widgets_everywhere(list: List["Widget"], by_name: Dict[str, "Widget"], sparse: List[Optional["Widget"]], holder: "WidgetHolder") -> "WidgetHolder": + """Objects in every buffered position""" + _list_w = _Writer() + _list_w.write_count(len(list)) + for _e0 in list: + _list_w.write_object(_e0, Widget) + _by_name_w = _Writer() + _by_name_w.write_count(len(by_name)) + for _k1, _v1 in by_name.items(): + _by_name_w.write_string(_k1) + _by_name_w.write_object(_v1, Widget) + _sparse_w = _Writer() + _sparse_w.write_count(len(sparse)) + for _e2 in sparse: + if _e2 is None: + _sparse_w.write_flag(False) + else: + _sparse_w.write_flag(True) + _sparse_w.write_object(_e2, Widget) + _holder_w = _Writer() + _write_WidgetHolder(_holder_w, holder) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _list_b = _list_w.finish() + _by_name_b = _by_name_w.finish() + _sparse_b = _sparse_w.finish() + _holder_b = _holder_w.finish() + _ret = _c_edge_cases_widgets_everywhere(_list_b, len(_list_b), _by_name_b, len(_by_name_b), _sparse_b, len(_sparse_b), _holder_b, len(_holder_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_WidgetHolder) + + +_c_edge_cases_widget_stream = _bind("edge_cases_edge_cases_widget_stream", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_WidgetStreamIterator_next = _bind("edge_cases_edge_cases_WidgetStreamIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_WidgetStreamIterator_destroy = _bind("edge_cases_edge_cases_WidgetStreamIterator_destroy", None, ctypes.c_void_p) + + +class _edge_cases_WidgetStreamIterator(_Iterator): + """The lazy iterator `widget_stream` returns: each step pulls one element + from the producer.""" + + _destroy = staticmethod(_c_edge_cases_WidgetStreamIterator_destroy) + + def __next__(self) -> "Widget": + _item = ctypes.c_void_p() + _err = _ErrorStruct() + _p = self._step() + try: + _more = _c_edge_cases_WidgetStreamIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + if not _more: + self.close() + raise StopIteration + return Widget._adopt(_required(_item.value)) + + +def widget_stream() -> Iterator["Widget"]: + """ + Iterator of objects + + Returns a lazy iterator: each step pulls one element from the + producer. Exhaust or close() the iterator to release its native + handle (garbage collection also releases it). + """ + _err = _ErrorStruct() + _ret = _c_edge_cases_widget_stream(ctypes.byref(_err)) + _check_error(_err) + return _edge_cases_WidgetStreamIterator._adopt(_ret) + + +_c_edge_cases_async_widget_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p) +_c_edge_cases_async_widget = _bind("edge_cases_edge_cases_async_widget", None, _c_edge_cases_async_widget_callback, ctypes.c_void_p) + + +def _edge_cases_async_widget_complete(context, err, result) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _edge_errors_error_from), None) + else: + _async_settle(context, None, Widget._adopt(_required(result))) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_async_widget_completion = _c_edge_cases_async_widget_callback(_edge_cases_async_widget_complete) + + +async def async_widget() -> "Widget": + """Async returning an object + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _call, _future = _async_begin() + try: + _c_edge_cases_async_widget(_edge_cases_async_widget_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_edge_cases_make_widget = _bind("edge_cases_edge_cases_make_widget", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) + + +def make_widget(label: str) -> "Widget": + """Factory for an interface with no constructor""" + _label_b = label.encode("utf-8") + _err = _ErrorStruct() + _ret = _c_edge_cases_make_widget(_label_b, len(_label_b), ctypes.byref(_err)) + _check_error(_err) + return Widget._adopt(_required(_ret)) + + +_c_edge_cases_find_widget = _bind("edge_cases_edge_cases_find_widget", ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(_ErrorStruct)) + + +def find_widget(id: int) -> Optional["Widget"]: + """Optional interface return + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _err = _ErrorStruct() + _ret = _c_edge_cases_find_widget(id, ctypes.byref(_err)) + _check_edge_errors_error(_err) + return (Widget._adopt(_ret) if _ret else None) + + +_c_edge_cases_inspect_widget = _bind("edge_cases_edge_cases_inspect_widget", ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def inspect_widget(widget: "Widget", fallback: Optional["Widget"]) -> str: + """Interface parameter, including an optional one""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _widget_p = _lend(widget, Widget) + try: + _fallback_p = _lend_opt(fallback, Widget) + try: + _ret = _c_edge_cases_inspect_widget(_widget_p, _fallback_p, ctypes.byref(_out_len), ctypes.byref(_err)) + finally: + _release_opt(fallback) + finally: + widget._release() + _check_error(_err) + return _take_str(_ret, _out_len.value) + + +_c_edge_cases_string_stream = _bind("edge_cases_edge_cases_string_stream", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_StringStreamIterator_next = _bind("edge_cases_edge_cases_StringStreamIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_StringStreamIterator_destroy = _bind("edge_cases_edge_cases_StringStreamIterator_destroy", None, ctypes.c_void_p) + + +class _edge_cases_StringStreamIterator(_Iterator): + """The lazy iterator `string_stream` returns: each step pulls one element + from the producer.""" + + _destroy = staticmethod(_c_edge_cases_StringStreamIterator_destroy) + + def __next__(self) -> str: + _item = ctypes.c_void_p() + _len = ctypes.c_size_t() + _err = _ErrorStruct() + _p = self._step() + try: + _more = _c_edge_cases_StringStreamIterator_next(_p, ctypes.byref(_item), ctypes.byref(_len), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + if not _more: + self.close() + raise StopIteration + return _take_str(_item.value, _len.value) + + +def string_stream() -> Iterator[str]: + """ + Iterator of strings + + Returns a lazy iterator: each step pulls one element from the + producer. Exhaust or close() the iterator to release its native + handle (garbage collection also releases it). + """ + _err = _ErrorStruct() + _ret = _c_edge_cases_string_stream(ctypes.byref(_err)) + _check_error(_err) + return _edge_cases_StringStreamIterator._adopt(_ret) + + +_c_edge_cases_scalar_stream = _bind("edge_cases_edge_cases_scalar_stream", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_ScalarStreamIterator_next = _bind("edge_cases_edge_cases_ScalarStreamIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_ScalarStreamIterator_destroy = _bind("edge_cases_edge_cases_ScalarStreamIterator_destroy", None, ctypes.c_void_p) + + +class _edge_cases_ScalarStreamIterator(_Iterator): + """The lazy iterator `scalar_stream` returns: each step pulls one element + from the producer.""" + + _destroy = staticmethod(_c_edge_cases_ScalarStreamIterator_destroy) + + def __next__(self) -> int: + _item = ctypes.c_int32() + _err = _ErrorStruct() + _p = self._step() + try: + _more = _c_edge_cases_ScalarStreamIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + if not _more: + self.close() + raise StopIteration + return _item.value + + +def scalar_stream() -> Iterator[int]: + """ + Iterator of scalars + + Returns a lazy iterator: each step pulls one element from the + producer. Exhaust or close() the iterator to release its native + handle (garbage collection also releases it). + """ + _err = _ErrorStruct() + _ret = _c_edge_cases_scalar_stream(ctypes.byref(_err)) + _check_error(_err) + return _edge_cases_ScalarStreamIterator._adopt(_ret) + + +_c_edge_cases_mode_stream = _bind("edge_cases_edge_cases_mode_stream", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_ModeStreamIterator_next = _bind("edge_cases_edge_cases_ModeStreamIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_ModeStreamIterator_destroy = _bind("edge_cases_edge_cases_ModeStreamIterator_destroy", None, ctypes.c_void_p) + + +class _edge_cases_ModeStreamIterator(_Iterator): + """The lazy iterator `mode_stream` returns: each step pulls one element + from the producer.""" + + _destroy = staticmethod(_c_edge_cases_ModeStreamIterator_destroy) + + def __next__(self) -> "Mode": + _item = ctypes.c_int32() + _err = _ErrorStruct() + _p = self._step() + try: + _more = _c_edge_cases_ModeStreamIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + if not _more: + self.close() + raise StopIteration + return Mode(_item.value) + + +def mode_stream() -> Iterator["Mode"]: + """ + Iterator of enums + + Returns a lazy iterator: each step pulls one element from the + producer. Exhaust or close() the iterator to release its native + handle (garbage collection also releases it). + """ + _err = _ErrorStruct() + _ret = _c_edge_cases_mode_stream(ctypes.byref(_err)) + _check_error(_err) + return _edge_cases_ModeStreamIterator._adopt(_ret) + + +_c_edge_cases_async_void_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_edge_cases_async_void = _bind("edge_cases_edge_cases_async_void", None, _c_edge_cases_async_void_callback, ctypes.c_void_p) + + +def _edge_cases_async_void_complete(context, err) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _error_from), None) + else: + _async_settle(context, None, None) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_async_void_completion = _c_edge_cases_async_void_callback(_edge_cases_async_void_complete) + + +async def async_void() -> None: + """Async with no result""" + _call, _future = _async_begin() + try: + _c_edge_cases_async_void(_edge_cases_async_void_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_edge_cases_async_bytes_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_edge_cases_async_bytes = _bind("edge_cases_edge_cases_async_bytes", None, ctypes.c_uint32, _c_edge_cases_async_bytes_callback, ctypes.c_void_p) + + +def _edge_cases_async_bytes_complete(context, err, result_ptr, result_len) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _error_from), None) + else: + _async_settle(context, None, _take_bytes(result_ptr, result_len)) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_async_bytes_completion = _c_edge_cases_async_bytes_callback(_edge_cases_async_bytes_complete) + + +async def async_bytes(size: int) -> bytes: + _call, _future = _async_begin() + try: + _c_edge_cases_async_bytes(size, _edge_cases_async_bytes_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_edge_cases_async_struct_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_edge_cases_async_struct = _bind("edge_cases_edge_cases_async_struct", None, _c_edge_cases_async_struct_callback, ctypes.c_void_p) + + +def _edge_cases_async_struct_complete(context, err, result_ptr, result_len) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _edge_errors_error_from), None) + else: + _async_settle(context, None, _decode(_take_bytes(result_ptr, result_len), _read_Outer)) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_async_struct_completion = _c_edge_cases_async_struct_callback(_edge_cases_async_struct_complete) + + +async def async_struct() -> "Outer": + """ + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _call, _future = _async_begin() + try: + _c_edge_cases_async_struct(_edge_cases_async_struct_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_edge_cases_async_list_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_edge_cases_async_list = _bind("edge_cases_edge_cases_async_list", None, ctypes.c_void_p, _c_edge_cases_async_list_callback, ctypes.c_void_p) + + +def _edge_cases_async_list_complete(context, err, result_ptr, result_len) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _edge_errors_error_from), None) + else: + _async_settle(context, None, _decode(_take_bytes(result_ptr, result_len), lambda _r: [_read_Outer(_r) for _i0 in range(_r.read_count())])) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_async_list_completion = _c_edge_cases_async_list_callback(_edge_cases_async_list_complete) + + +async def async_list() -> List["Outer"]: + """ + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _call, _future = _async_begin() + _token = _cancel_token_create() + try: + _c_edge_cases_async_list(_token, _edge_cases_async_list_completion, _call) + except BaseException: + _async_abandon(_call) + _cancel_token_destroy(_token) + raise + return await _async_wait_cancellable(_future, _token) + + +_c_edge_cases_async_optional_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_edge_cases_async_optional = _bind("edge_cases_edge_cases_async_optional", None, _c_edge_cases_async_optional_callback, ctypes.c_void_p) + + +def _edge_cases_async_optional_complete(context, err, result_ptr, result_len) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _error_from), None) + else: + _async_settle(context, None, _decode(_take_bytes(result_ptr, result_len), lambda _r: (_r.read_i64() if _r.read_flag() else None))) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_async_optional_completion = _c_edge_cases_async_optional_callback(_edge_cases_async_optional_complete) + + +async def async_optional() -> Optional[int]: + _call, _future = _async_begin() + try: + _c_edge_cases_async_optional(_edge_cases_async_optional_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_edge_cases_async_enum_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_int32) +_c_edge_cases_async_enum = _bind("edge_cases_edge_cases_async_enum", None, _c_edge_cases_async_enum_callback, ctypes.c_void_p) + + +def _edge_cases_async_enum_complete(context, err, result) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _error_from), None) + else: + _async_settle(context, None, Mode(result)) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_async_enum_completion = _c_edge_cases_async_enum_callback(_edge_cases_async_enum_complete) + + +async def async_enum() -> "Mode": + _call, _future = _async_begin() + try: + _c_edge_cases_async_enum(_edge_cases_async_enum_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_edge_cases_async_keywords_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_edge_cases_async_keywords = _bind("edge_cases_edge_cases_async_keywords", None, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int32, _c_edge_cases_async_keywords_callback, ctypes.c_void_p) + + +def _edge_cases_async_keywords_complete(context, err, result_ptr, result_len) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _error_from), None) + else: + _async_settle(context, None, _take_str(result_ptr, result_len)) + except BaseException as exc: + _async_settle(context, exc, None) + + +_edge_cases_async_keywords_completion = _c_edge_cases_async_keywords_callback(_edge_cases_async_keywords_complete) + + +async def async_keywords(class_: str, import_: int) -> str: + _class__b = class_.encode("utf-8") + _call, _future = _async_begin() + try: + _c_edge_cases_async_keywords(_class__b, len(_class__b), import_, _edge_cases_async_keywords_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_edge_cases_keywords_roundtrip = _bind("edge_cases_edge_cases_keywords_roundtrip", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def keywords_roundtrip(value: "Keywords") -> "Keywords": + _value_w = _Writer() + _write_Keywords(_value_w, value) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _value_b = _value_w.finish() + _ret = _c_edge_cases_keywords_roundtrip(_value_b, len(_value_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Keywords) + + +_c_edge_cases_legacy_value = _bind("edge_cases_edge_cases_legacy_value", ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def legacy_value(value: "Legacy") -> "Retired": + warnings.warn("Use Mode-based APIs", DeprecationWarning, stacklevel=2) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_edge_cases_legacy_value(value, ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Retired) + +# END edge_cases.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__edge_cases_edge_cases_pyi.snap similarity index 62% rename from crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_pyi.snap rename to crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_edge_cases_pyi.snap index a91ed3c9..5677e104 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_pyi.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_edge_cases_pyi.snap @@ -1,24 +1,26 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o +import builtins from abc import ABC, abstractmethod from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type +from typing import Dict, Iterator, List, Optional, Type -class WeaveFFIError(Exception): +class Error(Exception): GENERIC_ERROR_CODE: int PANIC_ERROR_CODE: int MARSHAL_ERROR_CODE: int FOREIGN_ERROR_CODE: int + CANCELLED_ERROR_CODE: int code: int message: str def __init__(self, code: int, message: str) -> None: ... -class EdgeErrorsError(WeaveFFIError): +class EdgeErrorsError(Error): Busy: Type["Busy"] def __init__(self, code: int, message: str) -> None: ... @@ -28,13 +30,13 @@ class Busy(EdgeErrorsError): # Deprecated enum with non-contiguous values and keyword-like variants class Legacy(IntEnum): - None_: int - Default: int - Class: int + None_ = 0 + Default = 5 + Class = 10 class Mode(IntEnum): - Fast: int - Safe: int + Fast = 1 + Safe = 2 class Leaf: weight: float @@ -62,66 +64,66 @@ class Outer: # 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: ... + class_: builtins.int + def_: builtins.int + import_: builtins.int + from_: builtins.int + func: builtins.int + range: builtins.int + register: builtins.int + volatile: builtins.int + new: builtins.int + default: builtins.int + end: builtins.int + in_: builtins.int + is_: builtins.int + int: builtins.int + var: builtins.int + val: builtins.int + self: builtins.int + this: builtins.int + object: builtins.int + string: builtins.int + case: builtins.int + select: builtins.int + switch: builtins.int + void: builtins.int + union: builtins.int + namespace: builtins.int + operator: builtins.int + template: builtins.int + package: builtins.int + protocol: builtins.int + guard: builtins.int + defer: builtins.int + inout: builtins.int + internal: builtins.int + when: builtins.int + fun: builtins.int + data: builtins.int + late: builtins.int + part: builtins.int + out: builtins.int + ref: builtins.int + event: builtins.int + base: builtins.int + lambda_: builtins.int + yield_: builtins.int + nil: builtins.int + null: builtins.int + begin: builtins.int + rescue: builtins.int + module: builtins.int + chan: builtins.int + go: builtins.int + let: builtins.int + export: builtins.int + delete: builtins.int + typedef: builtins.int + sizeof: builtins.int + static: builtins.int + super: builtins.int + def __init__(__dataclass_self__, class_: builtins.int, def_: builtins.int, import_: builtins.int, from_: builtins.int, func: builtins.int, range: builtins.int, register: builtins.int, volatile: builtins.int, new: builtins.int, default: builtins.int, end: builtins.int, in_: builtins.int, is_: builtins.int, int: builtins.int, var: builtins.int, val: builtins.int, self: builtins.int, this: builtins.int, object: builtins.int, string: builtins.int, case: builtins.int, select: builtins.int, switch: builtins.int, void: builtins.int, union: builtins.int, namespace: builtins.int, operator: builtins.int, template: builtins.int, package: builtins.int, protocol: builtins.int, guard: builtins.int, defer: builtins.int, inout: builtins.int, internal: builtins.int, when: builtins.int, fun: builtins.int, data: builtins.int, late: builtins.int, part: builtins.int, out: builtins.int, ref: builtins.int, event: builtins.int, base: builtins.int, lambda_: builtins.int, yield_: builtins.int, nil: builtins.int, null: builtins.int, begin: builtins.int, rescue: builtins.int, module: builtins.int, chan: builtins.int, go: builtins.int, let: builtins.int, export: builtins.int, delete: builtins.int, typedef: builtins.int, sizeof: builtins.int, static: builtins.int, super: builtins.int) -> None: ... # Deprecated struct class Retired: @@ -156,7 +158,7 @@ class BatchListener(ABC): # Callback interface whose name and methods are keywords class Keyword(ABC): @abstractmethod - def delete(self, self: int) -> None: ... + def delete(self, self_: int) -> None: ... @abstractmethod def import_(self) -> int: ... @@ -164,7 +166,7 @@ class Keyword(ABC): class Widget: def close(self) -> None: ... def __enter__(self) -> "Widget": ... - def __exit__(self, *exc: object) -> bool: ... + def __exit__(self, *exc: object) -> None: ... def refresh(self) -> None: ... def rename(self, class_: str, import_: Optional[str]) -> str: ... async def fetch(self, id: int) -> "Outer": ... @@ -174,7 +176,7 @@ class Widget: class Vault: def close(self) -> None: ... def __enter__(self) -> "Vault": ... - def __exit__(self, *exc: object) -> bool: ... + def __exit__(self, *exc: object) -> None: ... @classmethod def open(cls, path: str) -> "Vault": ... def peek(self) -> Optional["Widget"]: ... @@ -185,7 +187,7 @@ class Vault: 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: ... +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"]]]: ... @@ -241,4 +243,4 @@ def keywords_roundtrip(value: "Keywords") -> "Keywords": ... def legacy_value(value: "Legacy") -> "Retired": ... -# END weaveffi.pyi +# END edge_cases.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_py_typed.snap new file mode 100644 index 00000000..f8cd271e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__edge_cases_py_typed.snap @@ -0,0 +1,6 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__pyproject_toml.snap index 244fb84d..6db35d80 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__pyproject_toml.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__pyproject_toml.snap @@ -1,24 +1,24 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "weaveffi" +name = "edge_cases" version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" +description = "Native bindings for edge_cases" +requires-python = ">=3.9" [tool.setuptools] -packages = ["weaveffi"] +packages = ["edge_cases"] [tool.setuptools.package-data] -"weaveffi" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] +"edge_cases" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] # END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__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_edge_cases__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__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_edge_cases__weaveffi_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_py_typed.snap deleted file mode 100644 index 501a8090..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_py_typed.snap +++ /dev/null @@ -1,6 +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 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 deleted file mode 100644 index 05ddd67c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,2118 +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): - """An error reported through the C ABI. - - Positive codes are a module's declared error codes (raised as the - module's typed subclasses); negative codes are runtime traps: a generic - producer error (-1), a producer panic (-2), a marshalling failure (-3), - or an exception raised by a callback-interface implementation (-4). - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - - 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 = 2 - - -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) -# Callback-interface trampolines report a failed implementation through -# weaveffi_error_set, which copies the message with the producer's allocator. -_lib.weaveffi_error_set.argtypes = [ - ctypes.POINTER(_WeaveFFIErrorStruct), - ctypes.c_int32, - ctypes.c_char_p, -] -_lib.weaveffi_error_set.restype = None -_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 - - -def _borrow(obj): - """The raw pointer of a live object wrapper, lent to the producer for the - duration of one call. Raises if the wrapper was already closed.""" - _p = obj._ptr - if _p is None: - raise WeaveFFIError(-1, f"{type(obj).__name__} used after close()") - return _p - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - self._objects = [] - - def finish(self) -> bytes: - # Mint the object tokens last: an encoding that raised part-way - # (a closed wrapper, a field of the wrong type) never reaches here, - # so it leaks no strong references. - for _pos, _obj in self._objects: - struct.pack_into(" 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) - - def write_object(self, obj) -> None: - """Write an object token: one strong reference to `obj` the reader - will adopt. The wrapper is checked to be live now (a closed one - raises), but the reference itself is cloned in finish().""" - _borrow(obj) - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -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 read_object(self) -> int: - """Read an object token: one strong reference the caller adopts into - a wrapper. Zero never appears in a non-optional position.""" - _p = struct.unpack(" 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 abc -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 - - -# Live callback-interface implementations, keyed by the integer the producer -# receives as `ctx`. An entry is added when an implementation is passed to a -# call and removed when the producer invokes the vtable's `free(ctx)`, so the -# implementation stays alive exactly as long as the producer may call it and -# the producer never holds a raw reference to a Python object. -_cb_impls: Dict[int, object] = {} -_cb_lock = threading.Lock() -_cb_next_ctx = 0 - - -def _cb_register(impl) -> int: - global _cb_next_ctx - with _cb_lock: - _cb_next_ctx += 1 - _ctx = _cb_next_ctx - _cb_impls[_ctx] = impl - return _ctx - - -# === 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) - - -@dataclass -class WidgetHolder: - """Record carrying objects in every buffered position""" - - primary: "Widget" - fallback: Optional["Widget"] - extras: List["Widget"] - by_name: Dict[str, "Widget"] - - -def _write_WidgetHolder(_w: _BufferWriter, value: "WidgetHolder") -> None: - _w.write_object(value.primary) - if value.fallback is None: - _w.write_option_flag(False) - else: - _w.write_option_flag(True) - _w.write_object(value.fallback) - _w.write_len(len(value.extras)) - for _e0 in value.extras: - _w.write_object(_e0) - _w.write_len(len(value.by_name)) - for _k0, _v0 in value.by_name.items(): - _w.write_string(_k0) - _w.write_object(_v0) - - -def _read_WidgetHolder(_r: _BufferReader) -> "WidgetHolder": - return WidgetHolder( - primary=Widget._from_ptr(_r.read_object()), - fallback=(Widget._from_ptr(_r.read_object()) if _r.read_option_flag() else None), - extras=[Widget._from_ptr(_r.read_object()) for _i0 in range(_r.read_len())], - by_name=dict((_r.read_string(), Widget._from_ptr(_r.read_object())) for _i0 in range(_r.read_len())), - ) - - -def _pack_WidgetHolder(value: "WidgetHolder") -> bytes: - _w = _BufferWriter() - _write_WidgetHolder(_w, value) - return _w.finish() - - -def _unpack_WidgetHolder(data: bytes) -> "WidgetHolder": - return _decode_buffer(data, _read_WidgetHolder) - - -class BatchListener(abc.ABC): - """ - Callback interface carrying composite payloads and objects - - Consumer-implemented callback interface. Subclass it, implement every abstract method, and pass an instance wherever the API takes a `BatchListener`; the producer may call the methods from any thread until it releases the instance. An exception raised by a method is reported to the producer as WeaveFFIError.FOREIGN_ERROR_CODE (-4) and aborts the call that was in progress. - """ - - @abc.abstractmethod - def on_batch(self, items: List["Outer"], raw: bytes, first: Optional["Outer"], mode: "Mode") -> None: - """Composite payloads cross as value buffers - """ - - @abc.abstractmethod - def on_widget(self, widget: "Widget", fallback: Optional["Widget"], children: List["Widget"]) -> None: - """Objects cross as strong references the consumer must release - """ - - @abc.abstractmethod - def on_tick(self) -> None: - ... - - @abc.abstractmethod - def should_continue(self, class_: int) -> bool: - """Scalar return travels back through the vtable - """ - - @abc.abstractmethod - def pick_mode(self) -> "Mode": - """Enum return travels back as its integer discriminant - """ - - -_BatchListener_on_batch_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, 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.POINTER(_WeaveFFIErrorStruct)) -_BatchListener_on_widget_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)) -_BatchListener_on_tick_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)) -_BatchListener_should_continue_cfunctype = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)) -_BatchListener_pick_mode_cfunctype = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)) -_BatchListener_vtable_free_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p) - - -class _BatchListenerVtable(ctypes.Structure): - """The C vtable `weaveffi_edge_cases_BatchListener_vtable`.""" - - _fields_ = [ - ("on_batch", _BatchListener_on_batch_cfunctype), - ("on_widget", _BatchListener_on_widget_cfunctype), - ("on_tick", _BatchListener_on_tick_cfunctype), - ("should_continue", _BatchListener_should_continue_cfunctype), - ("pick_mode", _BatchListener_pick_mode_cfunctype), - ("free", _BatchListener_vtable_free_cfunctype), - ] - - -def _BatchListener_on_batch_trampoline(ctx, items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, out_err): - try: - _impl = _cb_impls[ctx] - _impl.on_batch(_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())]), (ctypes.string_at(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)) - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - - -def _BatchListener_on_widget_trampoline(ctx, widget, fallback, children_ptr, children_len, out_err): - try: - _impl = _cb_impls[ctx] - _impl.on_widget(Widget._from_ptr(widget), (Widget._from_ptr(fallback) if fallback else None), _decode_buffer(ctypes.string_at(children_ptr, children_len) if children_ptr else b"", lambda _r: [Widget._from_ptr(_r.read_object()) for _i0 in range(_r.read_len())])) - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - - -def _BatchListener_on_tick_trampoline(ctx, out_err): - try: - _impl = _cb_impls[ctx] - _impl.on_tick() - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - - -def _BatchListener_should_continue_trampoline(ctx, class_, out_err): - try: - _impl = _cb_impls[ctx] - _ret = _impl.should_continue(class_) - return 1 if _ret else 0 - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - return 0 - - -def _BatchListener_pick_mode_trampoline(ctx, out_err): - try: - _impl = _cb_impls[ctx] - _ret = _impl.pick_mode() - return int(_ret) - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - return 0 - - -def _BatchListener_vtable_free_trampoline(ctx): - # The producer's last reference is gone; it never touches `ctx` again. - _cb_impls.pop(ctx, None) - - -# The one process-wide vtable for BatchListener. The CFUNCTYPE objects are held at -# module scope so their function pointers stay valid for the process lifetime. -_BatchListener_on_batch_cfunc = _BatchListener_on_batch_cfunctype(_BatchListener_on_batch_trampoline) -_BatchListener_on_widget_cfunc = _BatchListener_on_widget_cfunctype(_BatchListener_on_widget_trampoline) -_BatchListener_on_tick_cfunc = _BatchListener_on_tick_cfunctype(_BatchListener_on_tick_trampoline) -_BatchListener_should_continue_cfunc = _BatchListener_should_continue_cfunctype(_BatchListener_should_continue_trampoline) -_BatchListener_pick_mode_cfunc = _BatchListener_pick_mode_cfunctype(_BatchListener_pick_mode_trampoline) -_BatchListener_vtable_free_cfunc = _BatchListener_vtable_free_cfunctype(_BatchListener_vtable_free_trampoline) -_BatchListener_vtable = _BatchListenerVtable(_BatchListener_on_batch_cfunc, _BatchListener_on_widget_cfunc, _BatchListener_on_tick_cfunc, _BatchListener_should_continue_cfunc, _BatchListener_pick_mode_cfunc, _BatchListener_vtable_free_cfunc) - - -class Keyword(abc.ABC): - """ - Callback interface whose name and methods are keywords - - Consumer-implemented callback interface. Subclass it, implement every abstract method, and pass an instance wherever the API takes a `Keyword`; the producer may call the methods from any thread until it releases the instance. An exception raised by a method is reported to the producer as WeaveFFIError.FOREIGN_ERROR_CODE (-4) and aborts the call that was in progress. - """ - - @abc.abstractmethod - def delete(self, self: int) -> None: - ... - - @abc.abstractmethod - def import_(self) -> int: - ... - - -_Keyword_delete_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)) -_Keyword_import_cfunctype = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)) -_Keyword_vtable_free_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p) - - -class _KeywordVtable(ctypes.Structure): - """The C vtable `weaveffi_edge_cases_Keyword_vtable`.""" - - _fields_ = [ - ("delete", _Keyword_delete_cfunctype), - ("import", _Keyword_import_cfunctype), - ("free", _Keyword_vtable_free_cfunctype), - ] - - -def _Keyword_delete_trampoline(ctx, self, out_err): - try: - _impl = _cb_impls[ctx] - _impl.delete(self) - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - - -def _Keyword_import_trampoline(ctx, out_err): - try: - _impl = _cb_impls[ctx] - _ret = _impl.import_() - return int(_ret) - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - return 0 - - -def _Keyword_vtable_free_trampoline(ctx): - # The producer's last reference is gone; it never touches `ctx` again. - _cb_impls.pop(ctx, None) - - -# The one process-wide vtable for Keyword. The CFUNCTYPE objects are held at -# module scope so their function pointers stay valid for the process lifetime. -_Keyword_delete_cfunc = _Keyword_delete_cfunctype(_Keyword_delete_trampoline) -_Keyword_import_cfunc = _Keyword_import_cfunctype(_Keyword_import_trampoline) -_Keyword_vtable_free_cfunc = _Keyword_vtable_free_cfunctype(_Keyword_vtable_free_trampoline) -_Keyword_vtable = _KeywordVtable(_Keyword_delete_cfunc, _Keyword_import_cfunc, _Keyword_vtable_free_cfunc) - - -_lib.weaveffi_edge_cases_Widget_clone.argtypes = [ctypes.c_void_p] -_lib.weaveffi_edge_cases_Widget_clone.restype = ctypes.c_void_p -_lib.weaveffi_edge_cases_Widget_destroy.argtypes = [ctypes.c_void_p] -_lib.weaveffi_edge_cases_Widget_destroy.restype = None - - -class Widget: - """Interface with no constructor; instances come from make_widget""" - - @classmethod - def _from_ptr(cls, ptr) -> "Widget": - """Adopt one strong reference the producer handed over.""" - _obj = cls.__new__(cls) - _obj._ptr = ptr - return _obj - - def __init__(self) -> None: - raise TypeError("Widget cannot be instantiated directly") - - - def _clone_ptr(self): - """A new strong reference to the same object (a raw pointer the - receiver must eventually release), leaving this wrapper's own - reference untouched.""" - return _lib.weaveffi_edge_cases_Widget_clone(_borrow(self)) - - def close(self) -> None: - """Release this wrapper's reference. The object itself is dropped - when its last reference (here or in the producer) is released. - Idempotent; the wrapper is unusable afterwards.""" - _p = getattr(self, "_ptr", None) - self._ptr = None - if _p is not None: - _lib.weaveffi_edge_cases_Widget_destroy(_p) - - def __enter__(self) -> "Widget": - return self - - def __exit__(self, *exc) -> bool: - self.close() - return False - - def __del__(self) -> None: - self.close() - - 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(_borrow(self), 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(_borrow(self), _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(_borrow(self), 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(_borrow(self), ctypes.byref(_err)) - _check_error(_err) - return _take_string(_result) or "" - - - -_lib.weaveffi_edge_cases_Vault_clone.argtypes = [ctypes.c_void_p] -_lib.weaveffi_edge_cases_Vault_clone.restype = ctypes.c_void_p -_lib.weaveffi_edge_cases_Vault_destroy.argtypes = [ctypes.c_void_p] -_lib.weaveffi_edge_cases_Vault_destroy.restype = None - - -class Vault: - """Deprecated interface with a throwing constructor""" - - @classmethod - def _from_ptr(cls, ptr) -> "Vault": - """Adopt one strong reference the producer handed over.""" - _obj = cls.__new__(cls) - _obj._ptr = ptr - return _obj - - def __init__(self) -> None: - raise TypeError("Vault cannot be instantiated directly") - - - def _clone_ptr(self): - """A new strong reference to the same object (a raw pointer the - receiver must eventually release), leaving this wrapper's own - reference untouched.""" - return _lib.weaveffi_edge_cases_Vault_clone(_borrow(self)) - - def close(self) -> None: - """Release this wrapper's reference. The object itself is dropped - when its last reference (here or in the producer) is released. - Idempotent; the wrapper is unusable afterwards.""" - _p = getattr(self, "_ptr", None) - self._ptr = None - if _p is not None: - _lib.weaveffi_edge_cases_Vault_destroy(_p) - - def __enter__(self) -> "Vault": - return self - - def __exit__(self, *exc) -> bool: - self.close() - return False - - def __del__(self) -> None: - self.close() - - @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(_borrow(self), 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) - - - -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 register_batch(listener: "BatchListener", keyword: "Keyword") -> None: - """Two callback interface parameters on one function - """ - _fn = _lib.weaveffi_edge_cases_register_batch - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_BatchListenerVtable), ctypes.c_void_p, ctypes.POINTER(_KeywordVtable), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = None - _listener_ctx = _cb_register(listener) - _keyword_ctx = _cb_register(keyword) - _err = _WeaveFFIErrorStruct() - _fn(_listener_ctx, ctypes.byref(_BatchListener_vtable), _keyword_ctx, ctypes.byref(_Keyword_vtable), ctypes.byref(_err)) - _check_error(_err) - - -def widgets_everywhere(list: List["Widget"], by_name: Dict[str, "Widget"], sparse: List[Optional["Widget"]], holder: "WidgetHolder") -> "WidgetHolder": - """Objects in every buffered position - """ - _fn = _lib.weaveffi_edge_cases_widgets_everywhere - _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.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _list_w = _BufferWriter() - _list_w.write_len(len(list)) - for _e0 in list: - _list_w.write_object(_e0) - _list_buf = _list_w.finish() - _by_name_w = _BufferWriter() - _by_name_w.write_len(len(by_name)) - for _k0, _v0 in by_name.items(): - _by_name_w.write_string(_k0) - _by_name_w.write_object(_v0) - _by_name_buf = _by_name_w.finish() - _sparse_w = _BufferWriter() - _sparse_w.write_len(len(sparse)) - for _e0 in sparse: - if _e0 is None: - _sparse_w.write_option_flag(False) - else: - _sparse_w.write_option_flag(True) - _sparse_w.write_object(_e0) - _sparse_buf = _sparse_w.finish() - _holder_buf = _pack_WidgetHolder(holder) - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(_list_buf, len(_list_buf), _by_name_buf, len(_by_name_buf), _sparse_buf, len(_sparse_buf), _holder_buf, len(_holder_buf), ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_WidgetHolder(_data) - - -class _WidgetStreamIterator: - """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_WidgetStreamIterator_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 Widget._from_ptr(_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_WidgetStreamIterator_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 widget_stream() -> Iterator["Widget"]: - """ - Iterator of objects - - Returns a lazy iterator: each step pulls one element from the producer. Exhaust or close() the iterator to release its native handle (garbage collection also releases it). - """ - _fn = _lib.weaveffi_edge_cases_widget_stream - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _WidgetStreamIterator(_result) - - -async def async_widget() -> "Widget": - """Async returning an object - - Raises - ------ - EdgeErrorsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_edge_cases_async_widget_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"] = _edge_errors_error_from(_code, _msg, _payload) - else: - try: - if result is None: - _state["err"] = WeaveFFIError(-1, "null pointer") - else: - _state["val"] = Widget._from_ptr(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_void_p) - _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 - - -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(_borrow(widget), (_borrow(fallback) if fallback is not None else None), ctypes.byref(_err)) - _check_error(_err) - return _take_string(_result) or "" - - -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_kitchen_sink__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__README_md.snap index e3b8a7b9..ca5362cf 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__README_md.snap @@ -1,18 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Python) +# kitchen_sink (Python) -Auto-generated Python bindings using ctypes. +Python bindings for the `kitchen_sink` native library, built on `ctypes`. -## Prerequisites +## Requirements -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. +- Python 3.9 or later +- The native library (`libkitchen_sink.so`, `libkitchen_sink.dylib`, or `kitchen_sink.dll`) ## Install @@ -20,16 +20,16 @@ Auto-generated Python bindings using ctypes. pip install . ``` -## Development install +## Loading the native library -```bash -pip install -e . -``` +The bindings load the library from `KITCHEN_SINK_LIBRARY` when it is set (a full path), +then from inside the installed package, then from the system loader's search +path. ## Usage ```python -from weaveffi import * +import kitchen_sink ``` diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink___init___py.snap new file mode 100644 index 00000000..14ddd591 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink___init___py.snap @@ -0,0 +1,10 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +from .kitchen_sink import * # noqa: F401,F403 + +# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_py.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_py.snap new file mode 100644 index 00000000..56c40319 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_py.snap @@ -0,0 +1,1306 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +"""ctypes bindings for the kitchen_sink native library.""" +import abc +import asyncio +import builtins +import ctypes +import os +import platform +import struct +import threading +import warnings +from dataclasses import dataclass +from enum import IntEnum +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type + + +class Error(Exception): + """An error reported by the native library. + + `code` is positive for one of the API's declared error codes (raised as + the declaring module's typed subclasses) and negative for a runtime trap: + a generic failure (-1), a producer panic (-2), a marshalling failure + (-3), or an exception raised by a callback-interface implementation + (-4). A cancelled async call (-5) raises `asyncio.CancelledError` + instead. + """ + + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + FOREIGN_ERROR_CODE = -4 + CANCELLED_ERROR_CODE = -5 + + def __init__(self, code: int, message: str) -> None: + super().__init__(f"({code}) {message}") + self.code = code + self.message = message + + +class _ErrorStruct(ctypes.Structure): + """The C `kitchen_sink_error` out-parameter every call reports through.""" + + _fields_ = [ + ("code", ctypes.c_int32), + ("message", ctypes.c_char_p), + ("payload_ptr", ctypes.c_void_p), + ("payload_len", ctypes.c_size_t), + ] + + +def _load_library() -> ctypes.CDLL: + # An explicit path wins; then a library bundled inside this package (a + # platform wheel); then the system loader's search path. + override = os.environ.get("KITCHEN_SINK_LIBRARY") + if override: + return ctypes.CDLL(override) + system = platform.system() + if system == "Darwin": + name = "libkitchen_sink.dylib" + elif system == "Windows": + name = "kitchen_sink.dll" + else: + name = "libkitchen_sink.so" + bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) + if os.path.exists(bundled): + return ctypes.CDLL(bundled) + try: + return ctypes.CDLL(name) + except OSError as exc: + raise ImportError( + f"cannot load the native library {name!r} ({exc}); " + "set KITCHEN_SINK_LIBRARY to its path" + ) from exc + + +_lib = _load_library() + + +def _bind(name: str, restype: Any, *argtypes: Any) -> Any: + """Look up one exported symbol and fix its C signature. Every prototype + is bound once, at import time.""" + fn = getattr(_lib, name) + fn.restype = restype + fn.argtypes = argtypes + return fn + + +# The C ABI revision these bindings were generated against, and the contract +# checksum of every top-level module: (module, checksum symbol, value). +_ABI_VERSION = 3 +_CHECKSUMS: List[Tuple[str, str, int]] = [ + ("shared", "kitchen_sink_shared_checksum", 0x42c4ce2c8d0af052), + ("kitchen", "kitchen_sink_kitchen_checksum", 0xd83da75b24b545ff), +] + + +def _check_contract() -> None: + """Refuse to load a library built for a different ABI revision or a + different version of any module's API, before any other symbol is + bound.""" + try: + abi_version = _bind("kitchen_sink_abi_version", ctypes.c_uint32) + except AttributeError: + raise ImportError( + f"{_lib._name} does not export kitchen_sink_abi_version; " + "it is not the library these bindings were generated for" + ) from None + found = abi_version() + if found != _ABI_VERSION: + raise ImportError( + f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " + f"but {_lib._name} reports revision {found}" + ) + for module, symbol, expected in _CHECKSUMS: + try: + checksum = _bind(symbol, ctypes.c_uint64) + except AttributeError: + raise ImportError( + f"module {module!r}: {_lib._name} does not export {symbol}" + ) from None + found = checksum() + if found != expected: + raise ImportError( + f"module {module!r}: the native library's contract checksum " + f"{found:#018x} does not match these bindings ({expected:#018x}); " + "regenerate the bindings from the library's current API" + ) + + +_check_contract() + +_error_clear = _bind("kitchen_sink_error_clear", None, ctypes.POINTER(_ErrorStruct)) +_free_bytes = _bind("kitchen_sink_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) + + +def _debug_live(kind: int) -> int: + """The producer's live-allocation counter `kind` (0 objects, 1 callbacks, + 2 iterators, 3 cancel tokens, 4 returned allocations). Always 0 unless + the producer was built with its leak counters enabled.""" + return _bind("kitchen_sink_debug_live", ctypes.c_uint64, ctypes.c_int32)(kind) + + +def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: + """Copy a filled out-err slot's code, message, and payload, then release + them.""" + code = err.code + message = err.message.decode("utf-8", "replace") if err.message else "" + payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + _error_clear(ctypes.byref(err)) + return code, message, payload + + +def _error_from(code: int, message: str, payload: bytes = b"") -> Error: + """The exception for a code no declared error domain claims.""" + return Error(code, message) + + +def _check_error(err: _ErrorStruct) -> None: + """Raise for a non-zero out-err slot of a call that declares no errors: + only runtime traps (a panic, a marshalling failure, a failed callback) + land here.""" + if err.code: + raise _error_from(*_read_error(err)) + + +def _take_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a producer-owned byte run (a string, bytes, or value buffer) and + release it with kitchen_sink_free_bytes.""" + if not ptr: + return b"" + try: + return ctypes.string_at(ptr, length) + finally: + _free_bytes(ptr, length) + + +def _take_str(ptr: Optional[int], length: int) -> str: + """Copy and release a producer-owned UTF-8 string.""" + return _take_bytes(ptr, length).decode("utf-8") + + +def _peek_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a byte run the producer only lends for the current call.""" + return ctypes.string_at(ptr, length) if ptr and length else b"" + + +class _Handle: + """Owns one native reference and releases it exactly once. + + Calls lend the pointer through `_acquire`/`_release`, so `close()` from + another thread while a call is in flight defers the release until the + last such call returns instead of freeing the object mid-call. + """ + + # The native destroy (or iterator release) function, set per subclass. + _destroy: Callable[[int], None] + + def _init_handle(self, ptr: int) -> None: + self._ptr: Optional[int] = ptr + self._calls = 0 + self._closing = False + self._lock = threading.Lock() + + @classmethod + def _adopt(cls, ptr: int) -> Any: + """Wrap one strong reference the producer handed over.""" + obj = cls.__new__(cls) + obj._init_handle(ptr) + return obj + + def _acquire(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise Error(-1, f"{type(self).__name__} used after close()") + self._calls += 1 + return ptr + + def _release(self) -> None: + with self._lock: + self._calls -= 1 + if self._calls or not self._closing: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def close(self) -> None: + """Release this wrapper's native reference. Idempotent; the wrapper + is unusable afterwards.""" + lock = getattr(self, "_lock", None) + if lock is None: + return + with lock: + if self._closing: + return + self._closing = True + if self._calls: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def __enter__(self) -> Any: + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def __del__(self) -> None: + # An unreachable wrapper has no call in flight: release directly. + ptr = getattr(self, "_ptr", None) + if ptr is not None: + self._ptr = None + type(self)._destroy(ptr) + + +class _Object(_Handle): + """Base of every interface wrapper: a reference-counted native object.""" + + # The native clone function, set per subclass. + _clone: Callable[[int], int] + + def _clone_ref(self) -> int: + """A new strong reference to the same object, for a receiver that + adopts it (an object token inside a value buffer).""" + ptr = self._acquire() + try: + return type(self)._clone(ptr) + finally: + self._release() + + +class _Iterator(_Handle): + """Base of every lazy iterator over a producer stream: one native `next` + call per step; the handle is released on exhaustion, `close()`, or + garbage collection.""" + + def __iter__(self) -> Any: + return self + + def _step(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise StopIteration + self._calls += 1 + return ptr + + +def _required(ptr: Optional[int]) -> int: + """A non-null object pointer the producer returned.""" + if not ptr: + raise Error(-1, "the producer returned a null object") + return ptr + + +def _lend(obj: Any, cls: "Type[_Object]") -> int: + """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + return obj._acquire() + + +def _lend_opt(obj: Any, cls: "Type[_Object]") -> Optional[int]: + """`_lend` for an optional object: None lends a null pointer.""" + return None if obj is None else _lend(obj, cls) + + +def _release_opt(obj: Any) -> None: + if obj is not None: + obj._release() + + +_I8 = struct.Struct(" Error: + return Error(-3, f"malformed value buffer: {what}") + + +class _Writer: + """Encodes values in the value-buffer wire format: little-endian, + packed, no alignment.""" + + __slots__ = ("_buf", "_objects") + + def __init__(self) -> None: + self._buf = bytearray() + self._objects: List[Tuple[int, _Object]] = [] + + def finish(self) -> bytes: + # Object tokens are minted last, so an encoding that fails part-way + # never leaks a strong reference. + minted: List[Tuple[_Object, int]] = [] + try: + for pos, obj in self._objects: + ptr = obj._clone_ref() + minted.append((obj, ptr)) + _U64.pack_into(self._buf, pos, ptr) + except BaseException: + for obj, ptr in minted: + type(obj)._destroy(ptr) + raise + self._objects = [] + return bytes(self._buf) + + def write_bool(self, v: bool) -> None: + self._buf.append(1 if v else 0) + + def write_i8(self, v: int) -> None: + self._buf += _I8.pack(v) + + def write_u8(self, v: int) -> None: + self._buf += _U8.pack(v) + + def write_i16(self, v: int) -> None: + self._buf += _I16.pack(v) + + def write_u16(self, v: int) -> None: + self._buf += _U16.pack(v) + + def write_i32(self, v: int) -> None: + self._buf += _I32.pack(v) + + def write_u32(self, v: int) -> None: + self._buf += _U32.pack(v) + + def write_i64(self, v: int) -> None: + self._buf += _I64.pack(v) + + def write_u64(self, v: int) -> None: + self._buf += _U64.pack(v) + + def write_f32(self, v: float) -> None: + self._buf += _F32.pack(v) + + def write_f64(self, v: float) -> None: + self._buf += _F64.pack(v) + + def write_count(self, n: int) -> None: + self._buf += _U32.pack(n) + + def write_flag(self, present: bool) -> None: + self._buf.append(1 if present else 0) + + def write_string(self, v: str) -> None: + data = v.encode("utf-8") + self._buf += _U32.pack(len(data)) + self._buf += data + + def write_bytes(self, v: bytes) -> None: + self._buf += _U32.pack(len(v)) + self._buf += v + + def write_numbers(self, code: str, values: List[Any]) -> None: + """A list of one fixed-width numeric primitive (`code` is its struct + format character), packed in one call.""" + self._buf += _U32.pack(len(values)) + self._buf += struct.pack(f"<{len(values)}{code}", *values) + + def write_object(self, obj: Any, cls: "Type[_Object]") -> None: + """Reserve an object token: one strong reference to `obj` the reader + adopts, cloned in `finish()`. A closed wrapper is rejected now.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + obj._acquire() + obj._release() + self._objects.append((len(self._buf), obj)) + self._buf += bytes(8) + + +class _Reader: + """Decodes values from the value-buffer wire format, rejecting truncated + buffers, invalid flag bytes, invalid UTF-8, and trailing data.""" + + __slots__ = ("_data", "_pos") + + def __init__(self, data: bytes) -> None: + self._data = data + self._pos = 0 + + def _take(self, n: int, what: str) -> int: + pos = self._pos + if len(self._data) - pos < n: + raise _malformed(f"truncated {what}") + self._pos = pos + n + return pos + + def read_bool(self) -> bool: + b = self._data[self._take(1, "bool")] + if b > 1: + raise _malformed("invalid bool byte") + return b == 1 + + def read_i8(self) -> int: + return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) + + def read_u8(self) -> int: + return self._data[self._take(1, "u8")] + + def read_i16(self) -> int: + return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) + + def read_u16(self) -> int: + return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) + + def read_i32(self) -> int: + return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) + + def read_u32(self) -> int: + return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) + + def read_i64(self) -> int: + return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) + + def read_u64(self) -> int: + return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) + + def read_f32(self) -> float: + return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) + + def read_f64(self) -> float: + return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) + + def read_count(self) -> int: + # An element count, not a byte length: elements can encode to zero + # bytes, so it is not bounded by the remaining data. + return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) + + def read_flag(self) -> bool: + b = self._data[self._take(1, "option flag")] + if b > 1: + raise _malformed("invalid option flag") + return b == 1 + + def read_bytes(self) -> bytes: + n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) + pos = self._take(n, "bytes") + return self._data[pos:pos + n] + + def read_string(self) -> str: + try: + return self.read_bytes().decode("utf-8") + except UnicodeDecodeError: + raise _malformed("string is not valid UTF-8") from None + + def read_numbers(self, code: str, size: int) -> List[Any]: + """A list of one fixed-width numeric primitive, unpacked in one call.""" + n = self.read_count() + pos = self._take(n * size, "list") + return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) + + def read_object(self) -> int: + """An object token: one strong reference the caller adopts.""" + ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) + if ptr == 0: + raise _malformed("null object token") + return ptr + + def expect_end(self) -> None: + if self._pos != len(self._data): + raise _malformed("trailing bytes") + + +def _decode(data: bytes, read: Callable[[_Reader], Any]) -> Any: + """Decode exactly one value from `data`.""" + r = _Reader(data) + value = read(r) + r.expect_end() + return value + + +# Async completions. A call registers its future under an integer key, passes +# the key as the launcher's `context`, and the function's static completion +# trampoline settles it from whichever producer thread completes the call. +# The trampolines live at module scope, so nothing a pending call needs can +# be garbage collected before the producer fires the completion, even when +# the awaiting coroutine is gone. +_pending: Dict[int, Tuple[asyncio.AbstractEventLoop, "asyncio.Future[Any]"]] = {} +_pending_lock = threading.Lock() +_pending_next = 0 + +_error_free = _bind("kitchen_sink_error_free", None, ctypes.POINTER(_ErrorStruct)) + + +def _async_begin() -> Tuple[int, "asyncio.Future[Any]"]: + """Register a new pending call on the running event loop.""" + global _pending_next + loop = asyncio.get_running_loop() + future = loop.create_future() + with _pending_lock: + _pending_next += 1 + key = _pending_next + _pending[key] = (loop, future) + return key, future + + +def _async_abandon(key: int) -> None: + """Forget a call whose launcher never ran.""" + with _pending_lock: + _pending.pop(key, None) + + +def _async_error(err: Any, factory: Callable[[int, str, bytes], BaseException]) -> BaseException: + """Copy and release a heap-boxed completion error, then build the + exception it reports. The cancelled code maps to CancelledError.""" + e = err.contents + code = e.code + message = e.message.decode("utf-8", "replace") if e.message else "" + payload = ctypes.string_at(e.payload_ptr, e.payload_len) if e.payload_ptr else b"" + _error_free(err) + if code == Error.CANCELLED_ERROR_CODE: + return asyncio.CancelledError(message) + return factory(code, message, payload) + + +def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) -> None: + """Hand a completion back to the event loop that awaits it.""" + with _pending_lock: + entry = _pending.pop(key or 0, None) + if entry is None: + return + loop, future = entry + try: + loop.call_soon_threadsafe(_async_resolve, future, exc, value) + except RuntimeError: + # The loop is closed, so nothing awaits the result any more. + pass + + +def _async_resolve(future: "asyncio.Future[Any]", exc: Optional[BaseException], value: Any) -> None: + if future.done(): + return + if isinstance(exc, asyncio.CancelledError): + future.cancel() + elif exc is not None: + future.set_exception(exc) + else: + future.set_result(value) + + +# Cancellation for `cancellable` async functions. Each call creates a token, +# the producer takes its own reference at launch, and cancelling the awaiting +# task cancels the token; the producer then completes the call with the +# cancelled code. +_cancel_token_create = _bind("kitchen_sink_cancel_token_create", ctypes.c_void_p) +_cancel_token_cancel = _bind("kitchen_sink_cancel_token_cancel", None, ctypes.c_void_p) +_cancel_token_destroy = _bind("kitchen_sink_cancel_token_destroy", None, ctypes.c_void_p) + + +async def _async_wait_cancellable(future: "asyncio.Future[Any]", token: int) -> Any: + """Await a cancellable call. Cancelling the awaiting task cancels the + native call and waits for its (cancelled) completion before raising, so + nothing outlives the call.""" + try: + return await asyncio.shield(future) + except asyncio.CancelledError: + _cancel_token_cancel(token) + try: + await future + except BaseException: + pass + raise + finally: + _cancel_token_destroy(token) + + +# Callback-interface implementations the producer holds, keyed by the integer +# it receives as `ctx`. An entry is added when an implementation is passed to +# a call and removed when the producer calls the vtable's `free(ctx)`, so the +# implementation lives exactly as long as the producer may call it and the +# producer never holds a raw reference to a Python object. ctypes acquires +# the GIL on entry to a trampoline, so the producer may call from any thread. +_callbacks: Dict[int, Any] = {} +_callbacks_lock = threading.Lock() +_callbacks_next = 0 + +_error_set = _bind( + "kitchen_sink_error_set", None, ctypes.POINTER(_ErrorStruct), ctypes.c_int32, ctypes.c_char_p +) + + +def _callback_register(impl: Any, cls: type) -> int: + """Register an implementation for one call; returns its `ctx` key.""" + global _callbacks_next + if not isinstance(impl, cls): + raise TypeError(f"expected {cls.__name__}, got {type(impl).__name__}") + with _callbacks_lock: + _callbacks_next += 1 + ctx = _callbacks_next + _callbacks[ctx] = impl + return ctx + + +def _callback_get(ctx: Optional[int]) -> Any: + return _callbacks[ctx or 0] + + +def _callback_free(ctx: Optional[int]) -> None: + # The producer's last reference is gone; it never passes `ctx` again. + with _callbacks_lock: + _callbacks.pop(ctx or 0, None) + + +def _callback_fail(out_err: Any, exc: BaseException) -> None: + """Report an implementation's exception to the producer, which aborts + its call with the foreign error code. Nothing unwinds through C.""" + message = str(exc) or type(exc).__name__ + _error_set(out_err, Error.FOREIGN_ERROR_CODE, message.encode("utf-8", "replace")) + + +# === Module: shared === + + +@dataclass +class Token: + """A reusable token referenced across modules""" + + id: int + label: str + + +def _write_Token(_w: _Writer, value: "Token") -> None: + _w.write_i64(value.id) + _w.write_string(value.label) + + +def _read_Token(_r: _Reader) -> "Token": + return Token( + id=_r.read_i64(), + label=_r.read_string(), + ) + + +_c_shared_ping = _bind("kitchen_sink_shared_ping", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def ping() -> str: + """Trivial cross-module helper""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_shared_ping(ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _take_str(_ret, _out_len.value) + + +# === Module: kitchen === + + +class KitchenErrorsError(Error): + """Base exception for the `kitchen` module's error domain.""" + + NotFound: "Type[NotFound]" + InvalidInput: "Type[InvalidInput]" + + +class NotFound(KitchenErrorsError): + """Item not found""" + + CODE = 1 + + def __init__(self, message: str = "Item not found") -> None: + super().__init__(1, message) + + +class InvalidInput(KitchenErrorsError): + """Invalid input""" + + CODE = 2 + + def __init__(self, message: str = "Invalid input") -> None: + super().__init__(2, message) + + +KitchenErrorsError.NotFound = NotFound +KitchenErrorsError.InvalidInput = InvalidInput + + +_KITCHEN_ERRORS_ERROR_CODES: Dict[int, Any] = { + 1: NotFound, + 2: InvalidInput, +} + + +def _kitchen_errors_error_from(code: int, message: str, payload: bytes = b"") -> Error: + """Build the KitchenErrorsError subclass matching `code`, or the plain + Error for codes outside the domain (panics, marshalling).""" + cls = _KITCHEN_ERRORS_ERROR_CODES.get(code) + if cls is None: + return Error(code, message) + exc: Any = cls(message) if message else cls() + return exc + + +def _check_kitchen_errors_error(err: _ErrorStruct) -> None: + if err.code: + raise _kitchen_errors_error_from(*_read_error(err)) + + +class Priority(IntEnum): + """Task priority levels""" + # Low priority + Low = 0 + # Normal priority + Normal = 1 + # High priority + High = 2 + + +@dataclass +class Item: + """Kitchen sink struct exercising every field feature""" + + # Stable identifier + id: int + # Display name + name: str + # Initial count + count: int + # Whether the item is enabled + enabled: bool + # Scaling ratio + ratio: float + nick: int + payload: bytes + tags: List[str] + attrs: Dict[str, str] + parent: Optional[int] + # Cross-module struct reference + token: "Token" + priority: "Priority" + # An optional object inside a record + gadget: Optional["Gadget"] + + +def _write_Item(_w: _Writer, value: "Item") -> None: + _w.write_i64(value.id) + _w.write_string(value.name) + _w.write_i32(value.count) + _w.write_bool(value.enabled) + _w.write_f64(value.ratio) + _w.write_u32(value.nick) + _w.write_bytes(value.payload) + _w.write_count(len(value.tags)) + for _e0 in value.tags: + _w.write_string(_e0) + _w.write_count(len(value.attrs)) + for _k1, _v1 in value.attrs.items(): + _w.write_string(_k1) + _w.write_string(_v1) + if value.parent is None: + _w.write_flag(False) + else: + _w.write_flag(True) + _w.write_i64(value.parent) + _write_Token(_w, value.token) + _w.write_i32(value.priority) + if value.gadget is None: + _w.write_flag(False) + else: + _w.write_flag(True) + _w.write_object(value.gadget, Gadget) + + +def _read_Item(_r: _Reader) -> "Item": + return Item( + id=_r.read_i64(), + name=_r.read_string(), + count=_r.read_i32(), + enabled=_r.read_bool(), + ratio=_r.read_f64(), + nick=_r.read_u32(), + payload=_r.read_bytes(), + tags=[_r.read_string() for _i0 in range(_r.read_count())], + attrs=dict((_r.read_string(), _r.read_string()) for _i0 in range(_r.read_count())), + parent=(_r.read_i64() if _r.read_flag() else None), + token=_read_Token(_r), + priority=Priority(_r.read_i32()), + gadget=(Gadget._adopt(_r.read_object()) if _r.read_flag() else None), + ) + + +class ReadyListener(abc.ABC): + """ + Consumer-implemented observer of ready items + + Consumer-implemented callback interface. Subclass it, implement every + abstract method, and pass an instance wherever the API takes a `ReadyListener`; + the producer may call the methods from any thread until it releases the + instance. An exception raised by a method is reported to the producer, + which fails the call in progress with Error.FOREIGN_ERROR_CODE (-4). + """ + + @abc.abstractmethod + def on_ready(self, code: int, msg: str) -> None: + """Fires when an item is ready""" + + @abc.abstractmethod + def on_item(self, item: "Item", gadget: "Gadget") -> bool: + """Receives the item itself and says whether to keep listening""" + + +_ReadyListener_on_ready_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int32, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) +_ReadyListener_on_item_t = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_ReadyListener_free_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p) + + +class _ReadyListenerVtable(ctypes.Structure): + """The C vtable `kitchen_sink_kitchen_ReadyListener_vtable`.""" + + _fields_ = [ + ("on_ready", _ReadyListener_on_ready_t), + ("on_item", _ReadyListener_on_item_t), + ("free", _ReadyListener_free_t), + ] + + +def _ReadyListener_on_ready(ctx, code, msg_ptr, msg_len, out_err) -> None: + try: + _callback_get(ctx).on_ready(code, _peek_bytes(msg_ptr, msg_len).decode("utf-8")) + except BaseException as exc: + _callback_fail(out_err, exc) + + +def _ReadyListener_on_item(ctx, item_ptr, item_len, gadget, out_err) -> bool: + try: + return bool(_callback_get(ctx).on_item(_decode(_peek_bytes(item_ptr, item_len), _read_Item), Gadget._adopt(_required(gadget)))) + except BaseException as exc: + _callback_fail(out_err, exc) + return False + + +_ReadyListener_vtable = _ReadyListenerVtable( + _ReadyListener_on_ready_t(_ReadyListener_on_ready), + _ReadyListener_on_item_t(_ReadyListener_on_item), + _ReadyListener_free_t(_callback_free), +) + + +_c_kitchen_Gadget_new = _bind("kitchen_sink_kitchen_Gadget_new", ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(_ErrorStruct)) +_c_kitchen_Gadget_describe = _bind("kitchen_sink_kitchen_Gadget_describe", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) +_c_kitchen_Gadget_poke = _bind("kitchen_sink_kitchen_Gadget_poke", ctypes.c_int32, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) +_c_kitchen_Gadget_version = _bind("kitchen_sink_kitchen_Gadget_version", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) +_c_kitchen_Gadget_clone = _bind("kitchen_sink_kitchen_Gadget_clone", ctypes.c_void_p, ctypes.c_void_p) +_c_kitchen_Gadget_destroy = _bind("kitchen_sink_kitchen_Gadget_destroy", None, ctypes.c_void_p) + + +class Gadget(_Object): + """A pocket-sized interface exercising the object surface""" + + _destroy = staticmethod(_c_kitchen_Gadget_destroy) + _clone = staticmethod(_c_kitchen_Gadget_clone) + + def __init__(self, id: int) -> None: + """Create a gadget with the given id""" + _err = _ErrorStruct() + _ret = _c_kitchen_Gadget_new(id, ctypes.byref(_err)) + _check_error(_err) + self._init_handle(_required(_ret)) + + def describe(self) -> str: + """Render the gadget as a human-readable string""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _self_p = self._acquire() + try: + _ret = _c_kitchen_Gadget_describe(_self_p, ctypes.byref(_out_len), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + return _take_str(_ret, _out_len.value) + + def poke(self, times: int) -> int: + """Poke the gadget a number of times, failing on a negative count + + Raises + ------ + KitchenErrorsError + If the call reports one of the domain's error codes. + """ + _err = _ErrorStruct() + _self_p = self._acquire() + try: + _ret = _c_kitchen_Gadget_poke(_self_p, times, ctypes.byref(_err)) + finally: + self._release() + _check_kitchen_errors_error(_err) + return _ret + + @staticmethod + def version() -> str: + """The gadget subsystem version string""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_Gadget_version(ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _take_str(_ret, _out_len.value) + + +_c_kitchen_bool_id = _bind("kitchen_sink_kitchen_bool_id", ctypes.c_bool, ctypes.c_bool, ctypes.POINTER(_ErrorStruct)) + + +def bool_id(x: bool) -> bool: + """Identity for bool""" + _err = _ErrorStruct() + _ret = _c_kitchen_bool_id(x, ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_kitchen_i32_id = _bind("kitchen_sink_kitchen_i32_id", ctypes.c_int32, ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) + + +def i32_id(x: int) -> int: + _err = _ErrorStruct() + _ret = _c_kitchen_i32_id(x, ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_kitchen_u32_id = _bind("kitchen_sink_kitchen_u32_id", ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(_ErrorStruct)) + + +def u32_id(x: int) -> int: + _err = _ErrorStruct() + _ret = _c_kitchen_u32_id(x, ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_kitchen_i64_id = _bind("kitchen_sink_kitchen_i64_id", ctypes.c_int64, ctypes.c_int64, ctypes.POINTER(_ErrorStruct)) + + +def i64_id(x: int) -> int: + _err = _ErrorStruct() + _ret = _c_kitchen_i64_id(x, ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_kitchen_f64_id = _bind("kitchen_sink_kitchen_f64_id", ctypes.c_double, ctypes.c_double, ctypes.POINTER(_ErrorStruct)) + + +def f64_id(x: float) -> float: + _err = _ErrorStruct() + _ret = _c_kitchen_f64_id(x, ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_kitchen_echo_string = _bind("kitchen_sink_kitchen_echo_string", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def echo_string(s: str) -> str: + _s_b = s.encode("utf-8") + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_echo_string(_s_b, len(_s_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _take_str(_ret, _out_len.value) + + +_c_kitchen_echo_bytes = _bind("kitchen_sink_kitchen_echo_bytes", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def echo_bytes(b: bytes) -> bytes: + _b_b = bytes(b) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_echo_bytes(_b_b, len(_b_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _take_bytes(_ret, _out_len.value) + + +_c_kitchen_subscribe = _bind("kitchen_sink_kitchen_subscribe", ctypes.c_int32, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) + + +def subscribe(listener: "ReadyListener") -> int: + """Register a listener, returning how many are attached""" + _err = _ErrorStruct() + _listener_ctx = _callback_register(listener, ReadyListener) + _ret = _c_kitchen_subscribe(_listener_ctx, ctypes.addressof(_ReadyListener_vtable), ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_kitchen_maybe_gadget = _bind("kitchen_sink_kitchen_maybe_gadget", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) + + +def maybe_gadget(fallback: Optional["Gadget"]) -> Optional["Gadget"]: + """Optional object in both directions""" + _err = _ErrorStruct() + _fallback_p = _lend_opt(fallback, Gadget) + try: + _ret = _c_kitchen_maybe_gadget(_fallback_p, ctypes.byref(_err)) + finally: + _release_opt(fallback) + _check_error(_err) + return (Gadget._adopt(_ret) if _ret else None) + + +_c_kitchen_gadgets = _bind("kitchen_sink_kitchen_gadgets", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def gadgets() -> List["Gadget"]: + """A list of objects""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_gadgets(ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), lambda _r: [Gadget._adopt(_r.read_object()) for _i0 in range(_r.read_count())]) + + +_c_kitchen_stream_gadgets = _bind("kitchen_sink_kitchen_stream_gadgets", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_kitchen_StreamGadgetsIterator_next = _bind("kitchen_sink_kitchen_StreamGadgetsIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_ErrorStruct)) +_c_kitchen_StreamGadgetsIterator_destroy = _bind("kitchen_sink_kitchen_StreamGadgetsIterator_destroy", None, ctypes.c_void_p) + + +class _kitchen_StreamGadgetsIterator(_Iterator): + """The lazy iterator `stream_gadgets` returns: each step pulls one element + from the producer.""" + + _destroy = staticmethod(_c_kitchen_StreamGadgetsIterator_destroy) + + def __next__(self) -> "Gadget": + _item = ctypes.c_void_p() + _err = _ErrorStruct() + _p = self._step() + try: + _more = _c_kitchen_StreamGadgetsIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + if not _more: + self.close() + raise StopIteration + return Gadget._adopt(_required(_item.value)) + + +def stream_gadgets() -> Iterator["Gadget"]: + """ + An iterator over objects + + Returns a lazy iterator: each step pulls one element from the + producer. Exhaust or close() the iterator to release its native + handle (garbage collection also releases it). + """ + _err = _ErrorStruct() + _ret = _c_kitchen_stream_gadgets(ctypes.byref(_err)) + _check_error(_err) + return _kitchen_StreamGadgetsIterator._adopt(_ret) + + +_c_kitchen_maybe_item = _bind("kitchen_sink_kitchen_maybe_item", ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def maybe_item(id: int) -> Optional["Item"]: + """Optional struct return + + Raises + ------ + KitchenErrorsError + If the call reports one of the domain's error codes. + """ + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_maybe_item(id, ctypes.byref(_out_len), ctypes.byref(_err)) + _check_kitchen_errors_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), lambda _r: (_read_Item(_r) if _r.read_flag() else None)) + + +_c_kitchen_list_items = _bind("kitchen_sink_kitchen_list_items", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def list_items() -> List["Item"]: + """List of structs""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_list_items(ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), lambda _r: [_read_Item(_r) for _i0 in range(_r.read_count())]) + + +_c_kitchen_get_attrs = _bind("kitchen_sink_kitchen_get_attrs", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def get_attrs() -> Dict[str, int]: + """Map return type""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_get_attrs(ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), lambda _r: dict((_r.read_string(), _r.read_i32()) for _i0 in range(_r.read_count()))) + + +_c_kitchen_stream_items = _bind("kitchen_sink_kitchen_stream_items", ctypes.c_void_p, ctypes.POINTER(_ErrorStruct)) +_c_kitchen_StreamItemsIterator_next = _bind("kitchen_sink_kitchen_StreamItemsIterator_next", ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) +_c_kitchen_StreamItemsIterator_destroy = _bind("kitchen_sink_kitchen_StreamItemsIterator_destroy", None, ctypes.c_void_p) + + +class _kitchen_StreamItemsIterator(_Iterator): + """The lazy iterator `stream_items` returns: each step pulls one element + from the producer.""" + + _destroy = staticmethod(_c_kitchen_StreamItemsIterator_destroy) + + def __next__(self) -> "Item": + _item = ctypes.c_void_p() + _len = ctypes.c_size_t() + _err = _ErrorStruct() + _p = self._step() + try: + _more = _c_kitchen_StreamItemsIterator_next(_p, ctypes.byref(_item), ctypes.byref(_len), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + if not _more: + self.close() + raise StopIteration + return _decode(_take_bytes(_item.value, _len.value), _read_Item) + + +def stream_items() -> Iterator["Item"]: + """ + Iterator return type + + Returns a lazy iterator: each step pulls one element from the + producer. Exhaust or close() the iterator to release its native + handle (garbage collection also releases it). + """ + _err = _ErrorStruct() + _ret = _c_kitchen_stream_items(ctypes.byref(_err)) + _check_error(_err) + return _kitchen_StreamItemsIterator._adopt(_ret) + + +_c_kitchen_cross_module_token = _bind("kitchen_sink_kitchen_cross_module_token", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def cross_module_token() -> "Token": + """Returns the shared Token type from another module""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_cross_module_token(ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Token) + + +_c_kitchen_do_async_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_kitchen_do_async = _bind("kitchen_sink_kitchen_do_async", None, ctypes.c_char_p, ctypes.c_size_t, _c_kitchen_do_async_callback, ctypes.c_void_p) + + +def _kitchen_do_async_complete(context, err, result_ptr, result_len) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _error_from), None) + else: + _async_settle(context, None, _take_str(result_ptr, result_len)) + except BaseException as exc: + _async_settle(context, exc, None) + + +_kitchen_do_async_completion = _c_kitchen_do_async_callback(_kitchen_do_async_complete) + + +async def do_async(input: str) -> str: + """Async operation""" + _input_b = input.encode("utf-8") + _call, _future = _async_begin() + try: + _c_kitchen_do_async(_input_b, len(_input_b), _kitchen_do_async_completion, _call) + except BaseException: + _async_abandon(_call) + raise + return await _future + + +_c_kitchen_do_cancellable_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_ErrorStruct), ctypes.c_void_p, ctypes.c_size_t) +_c_kitchen_do_cancellable = _bind("kitchen_sink_kitchen_do_cancellable", None, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p, _c_kitchen_do_cancellable_callback, ctypes.c_void_p) + + +def _kitchen_do_cancellable_complete(context, err, result_ptr, result_len) -> None: + # Runs once, on a producer thread: take ownership of the result + # here, then hand it to the awaiting event loop. + try: + if err: + _async_settle(context, _async_error(err, _error_from), None) + else: + _async_settle(context, None, _take_str(result_ptr, result_len)) + except BaseException as exc: + _async_settle(context, exc, None) + + +_kitchen_do_cancellable_completion = _c_kitchen_do_cancellable_callback(_kitchen_do_cancellable_complete) + + +async def do_cancellable(input: str) -> str: + """Cancellable async operation""" + _input_b = input.encode("utf-8") + _call, _future = _async_begin() + _token = _cancel_token_create() + try: + _c_kitchen_do_cancellable(_input_b, len(_input_b), _token, _kitchen_do_cancellable_completion, _call) + except BaseException: + _async_abandon(_call) + _cancel_token_destroy(_token) + raise + return await _async_wait_cancellable(_future, _token) + + +_c_kitchen_legacy_op = _bind("kitchen_sink_kitchen_legacy_op", ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) + + +def legacy_op() -> int: + """Legacy operation kept for compatibility""" + warnings.warn("Use new_op instead", DeprecationWarning, stacklevel=2) + _err = _ErrorStruct() + _ret = _c_kitchen_legacy_op(ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_kitchen_new_op = _bind("kitchen_sink_kitchen_new_op", ctypes.c_int32, ctypes.POINTER(_ErrorStruct)) + + +def new_op() -> int: + """Replacement for legacy_op""" + _err = _ErrorStruct() + _ret = _c_kitchen_new_op(ctypes.byref(_err)) + _check_error(_err) + return _ret + + +# === Module: kitchen.nested === + + +_c_kitchen_nested_hello = _bind("kitchen_sink_kitchen_nested_hello", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def hello() -> str: + """Trivial nested-module function""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_kitchen_nested_hello(ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _take_str(_ret, _out_len.value) + +# END kitchen_sink.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_pyi.snap similarity index 90% rename from crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_pyi.snap rename to crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_pyi.snap index e3eedfaf..5c24161c 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_pyi.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_kitchen_sink_pyi.snap @@ -1,19 +1,21 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o +import builtins from abc import ABC, abstractmethod from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type +from typing import Dict, Iterator, List, Optional, Type -class WeaveFFIError(Exception): +class Error(Exception): GENERIC_ERROR_CODE: int PANIC_ERROR_CODE: int MARSHAL_ERROR_CODE: int FOREIGN_ERROR_CODE: int + CANCELLED_ERROR_CODE: int code: int message: str def __init__(self, code: int, message: str) -> None: ... @@ -27,7 +29,7 @@ class Token: # Trivial cross-module helper def ping() -> str: ... -class KitchenErrorsError(WeaveFFIError): +class KitchenErrorsError(Error): NotFound: Type["NotFound"] InvalidInput: Type["InvalidInput"] def __init__(self, code: int, message: str) -> None: ... @@ -43,11 +45,11 @@ class InvalidInput(KitchenErrorsError): # Task priority levels class Priority(IntEnum): # Low priority - Low: int + Low = 0 # Normal priority - Normal: int + Normal = 1 # High priority - High: int + High = 2 # Kitchen sink struct exercising every field feature class Item: @@ -87,7 +89,7 @@ class Gadget: def __init__(self, id: int) -> None: ... def close(self) -> None: ... def __enter__(self) -> "Gadget": ... - def __exit__(self, *exc: object) -> bool: ... + def __exit__(self, *exc: object) -> None: ... def describe(self) -> str: ... def poke(self, times: int) -> int: ... @staticmethod @@ -150,4 +152,4 @@ def new_op() -> int: ... # Trivial nested-module function def hello() -> str: ... -# END weaveffi.pyi +# END kitchen_sink.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_py_typed.snap new file mode 100644 index 00000000..f8cd271e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__kitchen_sink_py_typed.snap @@ -0,0 +1,6 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__pyproject_toml.snap index 244fb84d..2a35fd2b 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__pyproject_toml.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__pyproject_toml.snap @@ -1,24 +1,24 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "weaveffi" +name = "kitchen_sink" version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" +description = "Native bindings for kitchen_sink" +requires-python = ">=3.9" [tool.setuptools] -packages = ["weaveffi"] +packages = ["kitchen_sink"] [tool.setuptools.package-data] -"weaveffi" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] +"kitchen_sink" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] # END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__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_kitchen_sink__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__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_kitchen_sink__weaveffi_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_py_typed.snap deleted file mode 100644 index 501a8090..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_py_typed.snap +++ /dev/null @@ -1,6 +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 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 deleted file mode 100644 index 71ecd2a7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,1171 +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): - """An error reported through the C ABI. - - Positive codes are a module's declared error codes (raised as the - module's typed subclasses); negative codes are runtime traps: a generic - producer error (-1), a producer panic (-2), a marshalling failure (-3), - or an exception raised by a callback-interface implementation (-4). - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - - 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 = 2 - - -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) -# Callback-interface trampolines report a failed implementation through -# weaveffi_error_set, which copies the message with the producer's allocator. -_lib.weaveffi_error_set.argtypes = [ - ctypes.POINTER(_WeaveFFIErrorStruct), - ctypes.c_int32, - ctypes.c_char_p, -] -_lib.weaveffi_error_set.restype = None -_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 - - -def _borrow(obj): - """The raw pointer of a live object wrapper, lent to the producer for the - duration of one call. Raises if the wrapper was already closed.""" - _p = obj._ptr - if _p is None: - raise WeaveFFIError(-1, f"{type(obj).__name__} used after close()") - return _p - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - self._objects = [] - - def finish(self) -> bytes: - # Mint the object tokens last: an encoding that raised part-way - # (a closed wrapper, a field of the wrong type) never reaches here, - # so it leaks no strong references. - for _pos, _obj in self._objects: - struct.pack_into(" 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) - - def write_object(self, obj) -> None: - """Write an object token: one strong reference to `obj` the reader - will adopt. The wrapper is checked to be live now (a closed one - raises), but the reference itself is cloned in finish().""" - _borrow(obj) - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -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 read_object(self) -> int: - """Read an object token: one strong reference the caller adopts into - a wrapper. Zero never appears in a non-optional position.""" - _p = struct.unpack(" 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 abc -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 - - -# Live callback-interface implementations, keyed by the integer the producer -# receives as `ctx`. An entry is added when an implementation is passed to a -# call and removed when the producer invokes the vtable's `free(ctx)`, so the -# implementation stays alive exactly as long as the producer may call it and -# the producer never holds a raw reference to a Python object. -_cb_impls: Dict[int, object] = {} -_cb_lock = threading.Lock() -_cb_next_ctx = 0 - - -def _cb_register(impl) -> int: - global _cb_next_ctx - with _cb_lock: - _cb_next_ctx += 1 - _ctx = _cb_next_ctx - _cb_impls[_ctx] = impl - return _ctx - - -# === Module: shared === - -@dataclass -class Token: - """A reusable token referenced across modules""" - - id: int - label: str - - -def _write_Token(_w: _BufferWriter, value: "Token") -> None: - _w.write_i64(value.id) - _w.write_string(value.label) - - -def _read_Token(_r: _BufferReader) -> "Token": - return Token( - id=_r.read_i64(), - label=_r.read_string(), - ) - - -def _pack_Token(value: "Token") -> bytes: - _w = _BufferWriter() - _write_Token(_w, value) - return _w.finish() - - -def _unpack_Token(data: bytes) -> "Token": - return _decode_buffer(data, _read_Token) - - -def ping() -> str: - """Trivial cross-module helper - """ - _fn = _lib.weaveffi_shared_ping - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _take_string(_result) or "" - - -# === Module: kitchen === - -class KitchenErrorsError(WeaveFFIError): - """Base exception for the `kitchen` module's error domain.""" - - -class NotFound(KitchenErrorsError): - """Item not found""" - - CODE = 1 - - def __init__(self, message: str = "Item not found") -> None: - super().__init__(1, message) - - -class InvalidInput(KitchenErrorsError): - """Invalid input""" - - CODE = 2 - - def __init__(self, message: str = "Invalid input") -> None: - super().__init__(2, message) - - -KitchenErrorsError.NotFound = NotFound -KitchenErrorsError.InvalidInput = InvalidInput - - -_KITCHEN_ERRORS_ERROR_CODES: Dict[int, type] = { - 1: NotFound, - 2: InvalidInput, -} - - -def _kitchen_errors_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the KitchenErrorsError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _KITCHEN_ERRORS_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_kitchen_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 _kitchen_errors_error_from(code, message, payload) - - -class Priority(IntEnum): - """Task priority levels""" - # Low priority - Low = 0 - # Normal priority - Normal = 1 - # High priority - High = 2 - - -@dataclass -class Item: - """Kitchen sink struct exercising every field feature""" - - # Stable identifier - id: int - # Display name - name: str - # Initial count - count: int - # Whether the item is enabled - enabled: bool - # Scaling ratio - ratio: float - nick: int - payload: bytes - tags: List[str] - attrs: Dict[str, str] - parent: Optional[int] - # Cross-module struct reference - token: "Token" - priority: "Priority" - # An optional object inside a record - gadget: Optional["Gadget"] - - -def _write_Item(_w: _BufferWriter, value: "Item") -> None: - _w.write_i64(value.id) - _w.write_string(value.name) - _w.write_i32(value.count) - _w.write_bool(value.enabled) - _w.write_f64(value.ratio) - _w.write_u32(value.nick) - _w.write_bytes(value.payload) - _w.write_len(len(value.tags)) - for _e0 in value.tags: - _w.write_string(_e0) - _w.write_len(len(value.attrs)) - for _k0, _v0 in value.attrs.items(): - _w.write_string(_k0) - _w.write_string(_v0) - if value.parent is None: - _w.write_option_flag(False) - else: - _w.write_option_flag(True) - _w.write_i64(value.parent) - _write_Token(_w, value.token) - _w.write_i32(value.priority) - if value.gadget is None: - _w.write_option_flag(False) - else: - _w.write_option_flag(True) - _w.write_object(value.gadget) - - -def _read_Item(_r: _BufferReader) -> "Item": - return Item( - id=_r.read_i64(), - name=_r.read_string(), - count=_r.read_i32(), - enabled=_r.read_bool(), - ratio=_r.read_f64(), - nick=_r.read_u32(), - payload=_r.read_bytes(), - tags=[_r.read_string() for _i0 in range(_r.read_len())], - attrs=dict((_r.read_string(), _r.read_string()) for _i0 in range(_r.read_len())), - parent=(_r.read_i64() if _r.read_option_flag() else None), - token=_read_Token(_r), - priority=Priority(_r.read_i32()), - gadget=(Gadget._from_ptr(_r.read_object()) if _r.read_option_flag() else None), - ) - - -def _pack_Item(value: "Item") -> bytes: - _w = _BufferWriter() - _write_Item(_w, value) - return _w.finish() - - -def _unpack_Item(data: bytes) -> "Item": - return _decode_buffer(data, _read_Item) - - -class ReadyListener(abc.ABC): - """ - Consumer-implemented observer of ready items - - Consumer-implemented callback interface. Subclass it, implement every abstract method, and pass an instance wherever the API takes a `ReadyListener`; the producer may call the methods from any thread until it releases the instance. An exception raised by a method is reported to the producer as WeaveFFIError.FOREIGN_ERROR_CODE (-4) and aborts the call that was in progress. - """ - - @abc.abstractmethod - def on_ready(self, code: int, msg: str) -> None: - """Fires when an item is ready - """ - - @abc.abstractmethod - def on_item(self, item: "Item", gadget: "Gadget") -> bool: - """Receives the item itself and says whether to keep listening - """ - - -_ReadyListener_on_ready_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int32, ctypes.c_char_p, ctypes.POINTER(_WeaveFFIErrorStruct)) -_ReadyListener_on_item_cfunctype = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)) -_ReadyListener_vtable_free_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p) - - -class _ReadyListenerVtable(ctypes.Structure): - """The C vtable `weaveffi_kitchen_ReadyListener_vtable`.""" - - _fields_ = [ - ("on_ready", _ReadyListener_on_ready_cfunctype), - ("on_item", _ReadyListener_on_item_cfunctype), - ("free", _ReadyListener_vtable_free_cfunctype), - ] - - -def _ReadyListener_on_ready_trampoline(ctx, code, msg, out_err): - try: - _impl = _cb_impls[ctx] - _impl.on_ready(code, _bytes_to_string(msg)) - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - - -def _ReadyListener_on_item_trampoline(ctx, item_ptr, item_len, gadget, out_err): - try: - _impl = _cb_impls[ctx] - _ret = _impl.on_item(_unpack_Item(ctypes.string_at(item_ptr, item_len) if item_ptr else b""), Gadget._from_ptr(gadget)) - return 1 if _ret else 0 - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - return 0 - - -def _ReadyListener_vtable_free_trampoline(ctx): - # The producer's last reference is gone; it never touches `ctx` again. - _cb_impls.pop(ctx, None) - - -# The one process-wide vtable for ReadyListener. The CFUNCTYPE objects are held at -# module scope so their function pointers stay valid for the process lifetime. -_ReadyListener_on_ready_cfunc = _ReadyListener_on_ready_cfunctype(_ReadyListener_on_ready_trampoline) -_ReadyListener_on_item_cfunc = _ReadyListener_on_item_cfunctype(_ReadyListener_on_item_trampoline) -_ReadyListener_vtable_free_cfunc = _ReadyListener_vtable_free_cfunctype(_ReadyListener_vtable_free_trampoline) -_ReadyListener_vtable = _ReadyListenerVtable(_ReadyListener_on_ready_cfunc, _ReadyListener_on_item_cfunc, _ReadyListener_vtable_free_cfunc) - - -_lib.weaveffi_kitchen_Gadget_clone.argtypes = [ctypes.c_void_p] -_lib.weaveffi_kitchen_Gadget_clone.restype = ctypes.c_void_p -_lib.weaveffi_kitchen_Gadget_destroy.argtypes = [ctypes.c_void_p] -_lib.weaveffi_kitchen_Gadget_destroy.restype = None - - -class Gadget: - """A pocket-sized interface exercising the object surface""" - - @classmethod - def _from_ptr(cls, ptr) -> "Gadget": - """Adopt one strong reference the producer handed over.""" - _obj = cls.__new__(cls) - _obj._ptr = ptr - return _obj - - def __init__(self, id: int) -> None: - """Create a gadget with the given id - """ - self._ptr = None - _fn = _lib.weaveffi_kitchen_Gadget_new - _fn.argtypes = [ctypes.c_int64, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(id, ctypes.byref(_err)) - _check_error(_err) - if _result is None: - raise WeaveFFIError(-1, "null pointer") - self._ptr = _result - - - def _clone_ptr(self): - """A new strong reference to the same object (a raw pointer the - receiver must eventually release), leaving this wrapper's own - reference untouched.""" - return _lib.weaveffi_kitchen_Gadget_clone(_borrow(self)) - - def close(self) -> None: - """Release this wrapper's reference. The object itself is dropped - when its last reference (here or in the producer) is released. - Idempotent; the wrapper is unusable afterwards.""" - _p = getattr(self, "_ptr", None) - self._ptr = None - if _p is not None: - _lib.weaveffi_kitchen_Gadget_destroy(_p) - - def __enter__(self) -> "Gadget": - return self - - def __exit__(self, *exc) -> bool: - self.close() - return False - - def __del__(self) -> None: - self.close() - - def describe(self) -> str: - """Render the gadget as a human-readable string - """ - _fn = _lib.weaveffi_kitchen_Gadget_describe - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(_borrow(self), ctypes.byref(_err)) - _check_error(_err) - return _take_string(_result) or "" - - - def poke(self, times: int) -> int: - """Poke the gadget a number of times, failing on a negative count - - Raises - ------ - KitchenErrorsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kitchen_Gadget_poke - _fn.argtypes = [ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(_borrow(self), times, ctypes.byref(_err)) - _check_kitchen_errors_error(_err) - return _result - - - @staticmethod - def version() -> str: - """The gadget subsystem version string - """ - _fn = _lib.weaveffi_kitchen_Gadget_version - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _take_string(_result) or "" - - - -def bool_id(x: bool) -> bool: - """Identity for bool - """ - _fn = _lib.weaveffi_kitchen_bool_id - _fn.argtypes = [ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(1 if x else 0, ctypes.byref(_err)) - _check_error(_err) - return bool(_result) - - -def i32_id(x: int) -> int: - _fn = _lib.weaveffi_kitchen_i32_id - _fn.argtypes = [ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(x, ctypes.byref(_err)) - _check_error(_err) - return _result - - -def u32_id(x: int) -> int: - _fn = _lib.weaveffi_kitchen_u32_id - _fn.argtypes = [ctypes.c_uint32, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_uint32 - _err = _WeaveFFIErrorStruct() - _result = _fn(x, ctypes.byref(_err)) - _check_error(_err) - return _result - - -def i64_id(x: int) -> int: - _fn = _lib.weaveffi_kitchen_i64_id - _fn.argtypes = [ctypes.c_int64, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int64 - _err = _WeaveFFIErrorStruct() - _result = _fn(x, ctypes.byref(_err)) - _check_error(_err) - return _result - - -def f64_id(x: float) -> float: - _fn = _lib.weaveffi_kitchen_f64_id - _fn.argtypes = [ctypes.c_double, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_double - _err = _WeaveFFIErrorStruct() - _result = _fn(x, ctypes.byref(_err)) - _check_error(_err) - return _result - - -def echo_string(s: str) -> str: - _fn = _lib.weaveffi_kitchen_echo_string - _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 "" - - -def echo_bytes(b: bytes) -> bytes: - _fn = _lib.weaveffi_kitchen_echo_bytes - _fn.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.POINTER(ctypes.c_uint8) - _b_arr = (ctypes.c_uint8 * len(b))(*b) - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(_b_arr, len(b), ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - if not _result: - return b"" - _val = bytes(_result[:_out_len.value]) - _lib.weaveffi_free_bytes(_result, ctypes.c_size_t(_out_len.value)) - return _val - - -def subscribe(listener: "ReadyListener") -> int: - """Register a listener, returning how many are attached - """ - _fn = _lib.weaveffi_kitchen_subscribe - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_ReadyListenerVtable), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _listener_ctx = _cb_register(listener) - _err = _WeaveFFIErrorStruct() - _result = _fn(_listener_ctx, ctypes.byref(_ReadyListener_vtable), ctypes.byref(_err)) - _check_error(_err) - return _result - - -def maybe_gadget(fallback: Optional["Gadget"]) -> Optional["Gadget"]: - """Optional object in both directions - """ - _fn = _lib.weaveffi_kitchen_maybe_gadget - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn((_borrow(fallback) if fallback is not None else None), ctypes.byref(_err)) - _check_error(_err) - if _result is None: - return None - return Gadget._from_ptr(_result) - - -def gadgets() -> List["Gadget"]: - """A list of objects - """ - _fn = _lib.weaveffi_kitchen_gadgets - _fn.argtypes = [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(ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _decode_buffer(_data, lambda _r: [Gadget._from_ptr(_r.read_object()) for _i0 in range(_r.read_len())]) - - -class _StreamGadgetsIterator: - """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_kitchen_StreamGadgetsIterator_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 Gadget._from_ptr(_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_kitchen_StreamGadgetsIterator_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 stream_gadgets() -> Iterator["Gadget"]: - """ - An iterator over objects - - Returns a lazy iterator: each step pulls one element from the producer. Exhaust or close() the iterator to release its native handle (garbage collection also releases it). - """ - _fn = _lib.weaveffi_kitchen_stream_gadgets - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _StreamGadgetsIterator(_result) - - -def maybe_item(id: int) -> Optional["Item"]: - """Optional struct return - - Raises - ------ - KitchenErrorsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kitchen_maybe_item - _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_kitchen_errors_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _decode_buffer(_data, lambda _r: (_read_Item(_r) if _r.read_option_flag() else None)) - - -def list_items() -> List["Item"]: - """List of structs - """ - _fn = _lib.weaveffi_kitchen_list_items - _fn.argtypes = [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(ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _decode_buffer(_data, lambda _r: [_read_Item(_r) for _i0 in range(_r.read_len())]) - - -def get_attrs() -> Dict[str, int]: - """Map return type - """ - _fn = _lib.weaveffi_kitchen_get_attrs - _fn.argtypes = [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(ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _decode_buffer(_data, lambda _r: dict((_r.read_string(), _r.read_i32()) for _i0 in range(_r.read_len()))) - - -class _StreamItemsIterator: - """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_kitchen_StreamItemsIterator_next - _next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _next_fn.restype = ctypes.c_int32 - _out_item = ctypes.c_void_p() - _out_len = ctypes.c_size_t(0) - _err = _WeaveFFIErrorStruct() - _has = _next_fn(self._ptr, ctypes.byref(_out_item), ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - if not _has: - self._done = True - self._destroy() - raise StopIteration - return _unpack_Item(_take_buffer(_out_item.value, _out_len.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_kitchen_StreamItemsIterator_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 stream_items() -> Iterator["Item"]: - """ - Iterator return type - - Returns a lazy iterator: each step pulls one element from the producer. Exhaust or close() the iterator to release its native handle (garbage collection also releases it). - """ - _fn = _lib.weaveffi_kitchen_stream_items - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _StreamItemsIterator(_result) - - -def cross_module_token() -> "Token": - """Returns the shared Token type from another module - """ - _fn = _lib.weaveffi_kitchen_cross_module_token - _fn.argtypes = [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(ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Token(_data) - - -async def do_async(input: str) -> str: - """Async operation - """ - _fn = _lib.weaveffi_kitchen_do_async_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, _cb_type, ctypes.c_void_p] - _fn.restype = None - _fn(_string_to_bytes(input), _cb, None) - return await _fut - - -async def do_cancellable(input: str) -> str: - """Cancellable async operation - """ - _fn = _lib.weaveffi_kitchen_do_cancellable_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_void_p, _cb_type, ctypes.c_void_p] - _fn.restype = None - _fn(_string_to_bytes(input), None, _cb, None) - return await _fut - - -def legacy_op() -> int: - """Legacy operation kept for compatibility - """ - import warnings - warnings.warn("Use new_op instead", DeprecationWarning, stacklevel=2) - _fn = _lib.weaveffi_kitchen_legacy_op - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _result - - -def new_op() -> int: - """Replacement for legacy_op - """ - _fn = _lib.weaveffi_kitchen_new_op - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _result - - -# === Module: kitchen_nested === - -def hello() -> str: - """Trivial nested-module function - """ - _fn = _lib.weaveffi_kitchen_nested_hello - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _take_string(_result) or "" - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__README_md.snap index e3b8a7b9..04250551 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__README_md.snap @@ -1,18 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Python) +# nested_modules (Python) -Auto-generated Python bindings using ctypes. +Python bindings for the `nested_modules` native library, built on `ctypes`. -## Prerequisites +## Requirements -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. +- Python 3.9 or later +- The native library (`libnested_modules.so`, `libnested_modules.dylib`, or `nested_modules.dll`) ## Install @@ -20,16 +20,16 @@ Auto-generated Python bindings using ctypes. pip install . ``` -## Development install +## Loading the native library -```bash -pip install -e . -``` +The bindings load the library from `NESTED_MODULES_LIBRARY` when it is set (a full path), +then from inside the installed package, then from the system loader's search +path. ## Usage ```python -from weaveffi import * +import nested_modules ``` diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules___init___py.snap new file mode 100644 index 00000000..03409d55 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules___init___py.snap @@ -0,0 +1,10 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +from .nested_modules import * # noqa: F401,F403 + +# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_py.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_py.snap new file mode 100644 index 00000000..9c61443a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_py.snap @@ -0,0 +1,698 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +"""ctypes bindings for the nested_modules native library.""" +import builtins +import ctypes +import os +import platform +import struct +import threading +import warnings +from dataclasses import dataclass +from enum import IntEnum +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type + + +class Error(Exception): + """An error reported by the native library. + + `code` is positive for one of the API's declared error codes (raised as + the declaring module's typed subclasses) and negative for a runtime trap: + a generic failure (-1), a producer panic (-2), a marshalling failure + (-3), or an exception raised by a callback-interface implementation + (-4). A cancelled async call (-5) raises `asyncio.CancelledError` + instead. + """ + + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + FOREIGN_ERROR_CODE = -4 + CANCELLED_ERROR_CODE = -5 + + def __init__(self, code: int, message: str) -> None: + super().__init__(f"({code}) {message}") + self.code = code + self.message = message + + +class _ErrorStruct(ctypes.Structure): + """The C `nested_modules_error` out-parameter every call reports through.""" + + _fields_ = [ + ("code", ctypes.c_int32), + ("message", ctypes.c_char_p), + ("payload_ptr", ctypes.c_void_p), + ("payload_len", ctypes.c_size_t), + ] + + +def _load_library() -> ctypes.CDLL: + # An explicit path wins; then a library bundled inside this package (a + # platform wheel); then the system loader's search path. + override = os.environ.get("NESTED_MODULES_LIBRARY") + if override: + return ctypes.CDLL(override) + system = platform.system() + if system == "Darwin": + name = "libnested_modules.dylib" + elif system == "Windows": + name = "nested_modules.dll" + else: + name = "libnested_modules.so" + bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) + if os.path.exists(bundled): + return ctypes.CDLL(bundled) + try: + return ctypes.CDLL(name) + except OSError as exc: + raise ImportError( + f"cannot load the native library {name!r} ({exc}); " + "set NESTED_MODULES_LIBRARY to its path" + ) from exc + + +_lib = _load_library() + + +def _bind(name: str, restype: Any, *argtypes: Any) -> Any: + """Look up one exported symbol and fix its C signature. Every prototype + is bound once, at import time.""" + fn = getattr(_lib, name) + fn.restype = restype + fn.argtypes = argtypes + return fn + + +# The C ABI revision these bindings were generated against, and the contract +# checksum of every top-level module: (module, checksum symbol, value). +_ABI_VERSION = 3 +_CHECKSUMS: List[Tuple[str, str, int]] = [ + ("graphics", "nested_modules_graphics_checksum", 0x44d31b5b10fdb90d), +] + + +def _check_contract() -> None: + """Refuse to load a library built for a different ABI revision or a + different version of any module's API, before any other symbol is + bound.""" + try: + abi_version = _bind("nested_modules_abi_version", ctypes.c_uint32) + except AttributeError: + raise ImportError( + f"{_lib._name} does not export nested_modules_abi_version; " + "it is not the library these bindings were generated for" + ) from None + found = abi_version() + if found != _ABI_VERSION: + raise ImportError( + f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " + f"but {_lib._name} reports revision {found}" + ) + for module, symbol, expected in _CHECKSUMS: + try: + checksum = _bind(symbol, ctypes.c_uint64) + except AttributeError: + raise ImportError( + f"module {module!r}: {_lib._name} does not export {symbol}" + ) from None + found = checksum() + if found != expected: + raise ImportError( + f"module {module!r}: the native library's contract checksum " + f"{found:#018x} does not match these bindings ({expected:#018x}); " + "regenerate the bindings from the library's current API" + ) + + +_check_contract() + +_error_clear = _bind("nested_modules_error_clear", None, ctypes.POINTER(_ErrorStruct)) +_free_bytes = _bind("nested_modules_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) + + +def _debug_live(kind: int) -> int: + """The producer's live-allocation counter `kind` (0 objects, 1 callbacks, + 2 iterators, 3 cancel tokens, 4 returned allocations). Always 0 unless + the producer was built with its leak counters enabled.""" + return _bind("nested_modules_debug_live", ctypes.c_uint64, ctypes.c_int32)(kind) + + +def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: + """Copy a filled out-err slot's code, message, and payload, then release + them.""" + code = err.code + message = err.message.decode("utf-8", "replace") if err.message else "" + payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + _error_clear(ctypes.byref(err)) + return code, message, payload + + +def _error_from(code: int, message: str, payload: bytes = b"") -> Error: + """The exception for a code no declared error domain claims.""" + return Error(code, message) + + +def _check_error(err: _ErrorStruct) -> None: + """Raise for a non-zero out-err slot of a call that declares no errors: + only runtime traps (a panic, a marshalling failure, a failed callback) + land here.""" + if err.code: + raise _error_from(*_read_error(err)) + + +def _take_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a producer-owned byte run (a string, bytes, or value buffer) and + release it with nested_modules_free_bytes.""" + if not ptr: + return b"" + try: + return ctypes.string_at(ptr, length) + finally: + _free_bytes(ptr, length) + + +def _take_str(ptr: Optional[int], length: int) -> str: + """Copy and release a producer-owned UTF-8 string.""" + return _take_bytes(ptr, length).decode("utf-8") + + +def _peek_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a byte run the producer only lends for the current call.""" + return ctypes.string_at(ptr, length) if ptr and length else b"" + + +class _Handle: + """Owns one native reference and releases it exactly once. + + Calls lend the pointer through `_acquire`/`_release`, so `close()` from + another thread while a call is in flight defers the release until the + last such call returns instead of freeing the object mid-call. + """ + + # The native destroy (or iterator release) function, set per subclass. + _destroy: Callable[[int], None] + + def _init_handle(self, ptr: int) -> None: + self._ptr: Optional[int] = ptr + self._calls = 0 + self._closing = False + self._lock = threading.Lock() + + @classmethod + def _adopt(cls, ptr: int) -> Any: + """Wrap one strong reference the producer handed over.""" + obj = cls.__new__(cls) + obj._init_handle(ptr) + return obj + + def _acquire(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise Error(-1, f"{type(self).__name__} used after close()") + self._calls += 1 + return ptr + + def _release(self) -> None: + with self._lock: + self._calls -= 1 + if self._calls or not self._closing: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def close(self) -> None: + """Release this wrapper's native reference. Idempotent; the wrapper + is unusable afterwards.""" + lock = getattr(self, "_lock", None) + if lock is None: + return + with lock: + if self._closing: + return + self._closing = True + if self._calls: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def __enter__(self) -> Any: + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def __del__(self) -> None: + # An unreachable wrapper has no call in flight: release directly. + ptr = getattr(self, "_ptr", None) + if ptr is not None: + self._ptr = None + type(self)._destroy(ptr) + + +class _Object(_Handle): + """Base of every interface wrapper: a reference-counted native object.""" + + # The native clone function, set per subclass. + _clone: Callable[[int], int] + + def _clone_ref(self) -> int: + """A new strong reference to the same object, for a receiver that + adopts it (an object token inside a value buffer).""" + ptr = self._acquire() + try: + return type(self)._clone(ptr) + finally: + self._release() + + +class _Iterator(_Handle): + """Base of every lazy iterator over a producer stream: one native `next` + call per step; the handle is released on exhaustion, `close()`, or + garbage collection.""" + + def __iter__(self) -> Any: + return self + + def _step(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise StopIteration + self._calls += 1 + return ptr + + +def _required(ptr: Optional[int]) -> int: + """A non-null object pointer the producer returned.""" + if not ptr: + raise Error(-1, "the producer returned a null object") + return ptr + + +def _lend(obj: Any, cls: "Type[_Object]") -> int: + """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + return obj._acquire() + + +def _lend_opt(obj: Any, cls: "Type[_Object]") -> Optional[int]: + """`_lend` for an optional object: None lends a null pointer.""" + return None if obj is None else _lend(obj, cls) + + +def _release_opt(obj: Any) -> None: + if obj is not None: + obj._release() + + +_I8 = struct.Struct(" Error: + return Error(-3, f"malformed value buffer: {what}") + + +class _Writer: + """Encodes values in the value-buffer wire format: little-endian, + packed, no alignment.""" + + __slots__ = ("_buf", "_objects") + + def __init__(self) -> None: + self._buf = bytearray() + self._objects: List[Tuple[int, _Object]] = [] + + def finish(self) -> bytes: + # Object tokens are minted last, so an encoding that fails part-way + # never leaks a strong reference. + minted: List[Tuple[_Object, int]] = [] + try: + for pos, obj in self._objects: + ptr = obj._clone_ref() + minted.append((obj, ptr)) + _U64.pack_into(self._buf, pos, ptr) + except BaseException: + for obj, ptr in minted: + type(obj)._destroy(ptr) + raise + self._objects = [] + return bytes(self._buf) + + def write_bool(self, v: bool) -> None: + self._buf.append(1 if v else 0) + + def write_i8(self, v: int) -> None: + self._buf += _I8.pack(v) + + def write_u8(self, v: int) -> None: + self._buf += _U8.pack(v) + + def write_i16(self, v: int) -> None: + self._buf += _I16.pack(v) + + def write_u16(self, v: int) -> None: + self._buf += _U16.pack(v) + + def write_i32(self, v: int) -> None: + self._buf += _I32.pack(v) + + def write_u32(self, v: int) -> None: + self._buf += _U32.pack(v) + + def write_i64(self, v: int) -> None: + self._buf += _I64.pack(v) + + def write_u64(self, v: int) -> None: + self._buf += _U64.pack(v) + + def write_f32(self, v: float) -> None: + self._buf += _F32.pack(v) + + def write_f64(self, v: float) -> None: + self._buf += _F64.pack(v) + + def write_count(self, n: int) -> None: + self._buf += _U32.pack(n) + + def write_flag(self, present: bool) -> None: + self._buf.append(1 if present else 0) + + def write_string(self, v: str) -> None: + data = v.encode("utf-8") + self._buf += _U32.pack(len(data)) + self._buf += data + + def write_bytes(self, v: bytes) -> None: + self._buf += _U32.pack(len(v)) + self._buf += v + + def write_numbers(self, code: str, values: List[Any]) -> None: + """A list of one fixed-width numeric primitive (`code` is its struct + format character), packed in one call.""" + self._buf += _U32.pack(len(values)) + self._buf += struct.pack(f"<{len(values)}{code}", *values) + + def write_object(self, obj: Any, cls: "Type[_Object]") -> None: + """Reserve an object token: one strong reference to `obj` the reader + adopts, cloned in `finish()`. A closed wrapper is rejected now.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + obj._acquire() + obj._release() + self._objects.append((len(self._buf), obj)) + self._buf += bytes(8) + + +class _Reader: + """Decodes values from the value-buffer wire format, rejecting truncated + buffers, invalid flag bytes, invalid UTF-8, and trailing data.""" + + __slots__ = ("_data", "_pos") + + def __init__(self, data: bytes) -> None: + self._data = data + self._pos = 0 + + def _take(self, n: int, what: str) -> int: + pos = self._pos + if len(self._data) - pos < n: + raise _malformed(f"truncated {what}") + self._pos = pos + n + return pos + + def read_bool(self) -> bool: + b = self._data[self._take(1, "bool")] + if b > 1: + raise _malformed("invalid bool byte") + return b == 1 + + def read_i8(self) -> int: + return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) + + def read_u8(self) -> int: + return self._data[self._take(1, "u8")] + + def read_i16(self) -> int: + return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) + + def read_u16(self) -> int: + return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) + + def read_i32(self) -> int: + return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) + + def read_u32(self) -> int: + return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) + + def read_i64(self) -> int: + return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) + + def read_u64(self) -> int: + return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) + + def read_f32(self) -> float: + return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) + + def read_f64(self) -> float: + return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) + + def read_count(self) -> int: + # An element count, not a byte length: elements can encode to zero + # bytes, so it is not bounded by the remaining data. + return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) + + def read_flag(self) -> bool: + b = self._data[self._take(1, "option flag")] + if b > 1: + raise _malformed("invalid option flag") + return b == 1 + + def read_bytes(self) -> bytes: + n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) + pos = self._take(n, "bytes") + return self._data[pos:pos + n] + + def read_string(self) -> str: + try: + return self.read_bytes().decode("utf-8") + except UnicodeDecodeError: + raise _malformed("string is not valid UTF-8") from None + + def read_numbers(self, code: str, size: int) -> List[Any]: + """A list of one fixed-width numeric primitive, unpacked in one call.""" + n = self.read_count() + pos = self._take(n * size, "list") + return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) + + def read_object(self) -> int: + """An object token: one strong reference the caller adopts.""" + ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) + if ptr == 0: + raise _malformed("null object token") + return ptr + + def expect_end(self) -> None: + if self._pos != len(self._data): + raise _malformed("trailing bytes") + + +def _decode(data: bytes, read: Callable[[_Reader], Any]) -> Any: + """Decode exactly one value from `data`.""" + r = _Reader(data) + value = read(r) + r.expect_end() + return value + + +# === Module: graphics === + + +class GraphicsError(Error): + """Base exception for the `graphics` module's error domain.""" + + OutOfBounds: "Type[OutOfBounds]" + + +class OutOfBounds(GraphicsError): + """coordinate out of bounds""" + + CODE = 1 + + def __init__(self, message: str = "coordinate out of bounds") -> None: + super().__init__(1, message) + + +GraphicsError.OutOfBounds = OutOfBounds + + +_GRAPHICS_ERROR_CODES: Dict[int, Any] = { + 1: OutOfBounds, +} + + +def _graphics_error_from(code: int, message: str, payload: bytes = b"") -> Error: + """Build the GraphicsError subclass matching `code`, or the plain + Error for codes outside the domain (panics, marshalling).""" + cls = _GRAPHICS_ERROR_CODES.get(code) + if cls is None: + return Error(code, message) + exc: Any = cls(message) if message else cls() + return exc + + +def _check_graphics_error(err: _ErrorStruct) -> None: + if err.code: + raise _graphics_error_from(*_read_error(err)) + + +class Unit(IntEnum): + """A measurement unit shared with nested submodules.""" + pixels = 0 + points = 1 + + +@dataclass +class Canvas: + """Top-level struct that references a struct defined one level deeper.""" + + width: int + # Reference down into a nested module + root: "Shape" + + +def _write_Canvas(_w: _Writer, value: "Canvas") -> None: + _w.write_i32(value.width) + _write_Shape(_w, value.root) + + +def _read_Canvas(_r: _Reader) -> "Canvas": + return Canvas( + width=_r.read_i32(), + root=_read_Shape(_r), + ) + + +_c_graphics_new_canvas = _bind("nested_modules_graphics_new_canvas", ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def new_canvas(width: int) -> "Canvas": + """Construct a Canvas.""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_graphics_new_canvas(width, ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Canvas) + + +# === Module: graphics.shapes === + + +@dataclass +class Shape: + """A struct one level deep that references both an ancestor enum and a deeper struct.""" + + sides: int + # Reference up to the parent module's enum + unit: "Unit" + # Reference down into a deeper module + detail: "Detail" + + +def _write_Shape(_w: _Writer, value: "Shape") -> None: + _w.write_i32(value.sides) + _w.write_i32(value.unit) + _write_Detail(_w, value.detail) + + +def _read_Shape(_r: _Reader) -> "Shape": + return Shape( + sides=_r.read_i32(), + unit=Unit(_r.read_i32()), + detail=_read_Detail(_r), + ) + + +_c_graphics_shapes_make_shape = _bind("nested_modules_graphics_shapes_make_shape", ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def make_shape(sides: int) -> "Shape": + """Construct a Shape.""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_graphics_shapes_make_shape(sides, ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Shape) + + +_c_graphics_shapes_describe = _bind("nested_modules_graphics_shapes_describe", ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def describe() -> "Canvas": + """Nested function returning a struct owned by an ancestor module.""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_graphics_shapes_describe(ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Canvas) + + +# === Module: graphics.shapes.geometry === + + +@dataclass +class Detail: + """A struct two levels deep.""" + + area: float + + +def _write_Detail(_w: _Writer, value: "Detail") -> None: + _w.write_f64(value.area) + + +def _read_Detail(_r: _Reader) -> "Detail": + return Detail( + area=_r.read_f64(), + ) + + +_c_graphics_shapes_geometry_compute = _bind("nested_modules_graphics_shapes_geometry_compute", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def compute(shape: "Shape") -> "Detail": + """Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. + + Raises + ------ + GraphicsError + If the call reports one of the domain's error codes. + """ + _shape_w = _Writer() + _write_Shape(_shape_w, shape) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _shape_b = _shape_w.finish() + _ret = _c_graphics_shapes_geometry_compute(_shape_b, len(_shape_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_graphics_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Detail) + +# END nested_modules.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_pyi.snap similarity index 84% rename from crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_pyi.snap rename to crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_pyi.snap index 2c4c152c..88754fc2 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_pyi.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_nested_modules_pyi.snap @@ -1,24 +1,26 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o +import builtins from abc import ABC, abstractmethod from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type +from typing import Dict, Iterator, List, Optional, Type -class WeaveFFIError(Exception): +class Error(Exception): GENERIC_ERROR_CODE: int PANIC_ERROR_CODE: int MARSHAL_ERROR_CODE: int FOREIGN_ERROR_CODE: int + CANCELLED_ERROR_CODE: int code: int message: str def __init__(self, code: int, message: str) -> None: ... -class GraphicsError(WeaveFFIError): +class GraphicsError(Error): OutOfBounds: Type["OutOfBounds"] def __init__(self, code: int, message: str) -> None: ... @@ -28,8 +30,8 @@ class OutOfBounds(GraphicsError): # A measurement unit shared with nested submodules. class Unit(IntEnum): - pixels: int - points: int + pixels = 0 + points = 1 # Top-level struct that references a struct defined one level deeper. class Canvas: @@ -64,4 +66,4 @@ class Detail: # Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. def compute(shape: "Shape") -> "Detail": ... -# END weaveffi.pyi +# END nested_modules.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_py_typed.snap new file mode 100644 index 00000000..f8cd271e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__nested_modules_py_typed.snap @@ -0,0 +1,6 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__pyproject_toml.snap index 244fb84d..ab68d0b3 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__pyproject_toml.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__pyproject_toml.snap @@ -1,24 +1,24 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "weaveffi" +name = "nested_modules" version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" +description = "Native bindings for nested_modules" +requires-python = ">=3.9" [tool.setuptools] -packages = ["weaveffi"] +packages = ["nested_modules"] [tool.setuptools.package-data] -"weaveffi" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] +"nested_modules" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] # END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__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_nested_modules__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__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_nested_modules__weaveffi_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_py_typed.snap deleted file mode 100644 index 501a8090..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_py_typed.snap +++ /dev/null @@ -1,6 +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 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 deleted file mode 100644 index 2e1886fe..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,553 +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): - """An error reported through the C ABI. - - Positive codes are a module's declared error codes (raised as the - module's typed subclasses); negative codes are runtime traps: a generic - producer error (-1), a producer panic (-2), a marshalling failure (-3), - or an exception raised by a callback-interface implementation (-4). - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - - 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 = 2 - - -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) -# Callback-interface trampolines report a failed implementation through -# weaveffi_error_set, which copies the message with the producer's allocator. -_lib.weaveffi_error_set.argtypes = [ - ctypes.POINTER(_WeaveFFIErrorStruct), - ctypes.c_int32, - ctypes.c_char_p, -] -_lib.weaveffi_error_set.restype = None -_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 - - -def _borrow(obj): - """The raw pointer of a live object wrapper, lent to the producer for the - duration of one call. Raises if the wrapper was already closed.""" - _p = obj._ptr - if _p is None: - raise WeaveFFIError(-1, f"{type(obj).__name__} used after close()") - return _p - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - self._objects = [] - - def finish(self) -> bytes: - # Mint the object tokens last: an encoding that raised part-way - # (a closed wrapper, a field of the wrong type) never reaches here, - # so it leaks no strong references. - for _pos, _obj in self._objects: - struct.pack_into(" 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) - - def write_object(self, obj) -> None: - """Write an object token: one strong reference to `obj` the reader - will adopt. The wrapper is checked to be live now (a closed one - raises), but the reference itself is cloned in finish().""" - _borrow(obj) - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -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 read_object(self) -> int: - """Read an object token: one strong reference the caller adopts into - a wrapper. Zero never appears in a non-optional position.""" - _p = struct.unpack(" 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: graphics === - -class GraphicsError(WeaveFFIError): - """Base exception for the `graphics` module's error domain.""" - - -class OutOfBounds(GraphicsError): - """coordinate out of bounds""" - - CODE = 1 - - def __init__(self, message: str = "coordinate out of bounds") -> None: - super().__init__(1, message) - - -GraphicsError.OutOfBounds = OutOfBounds - - -_GRAPHICS_ERROR_CODES: Dict[int, type] = { - 1: OutOfBounds, -} - - -def _graphics_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the GraphicsError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _GRAPHICS_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_graphics_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 _graphics_error_from(code, message, payload) - - -class Unit(IntEnum): - """A measurement unit shared with nested submodules.""" - pixels = 0 - points = 1 - - -@dataclass -class Canvas: - """Top-level struct that references a struct defined one level deeper.""" - - width: int - # Reference down into a nested module - root: "Shape" - - -def _write_Canvas(_w: _BufferWriter, value: "Canvas") -> None: - _w.write_i32(value.width) - _write_Shape(_w, value.root) - - -def _read_Canvas(_r: _BufferReader) -> "Canvas": - return Canvas( - width=_r.read_i32(), - root=_read_Shape(_r), - ) - - -def _pack_Canvas(value: "Canvas") -> bytes: - _w = _BufferWriter() - _write_Canvas(_w, value) - return _w.finish() - - -def _unpack_Canvas(data: bytes) -> "Canvas": - return _decode_buffer(data, _read_Canvas) - - -def new_canvas(width: int) -> "Canvas": - """Construct a Canvas. - """ - _fn = _lib.weaveffi_graphics_new_canvas - _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(width, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Canvas(_data) - - -# === Module: graphics_shapes === - -@dataclass -class Shape: - """A struct one level deep that references both an ancestor enum and a deeper struct.""" - - sides: int - # Reference up to the parent module's enum - unit: "Unit" - # Reference down into a deeper module - detail: "Detail" - - -def _write_Shape(_w: _BufferWriter, value: "Shape") -> None: - _w.write_i32(value.sides) - _w.write_i32(value.unit) - _write_Detail(_w, value.detail) - - -def _read_Shape(_r: _BufferReader) -> "Shape": - return Shape( - sides=_r.read_i32(), - unit=Unit(_r.read_i32()), - detail=_read_Detail(_r), - ) - - -def _pack_Shape(value: "Shape") -> bytes: - _w = _BufferWriter() - _write_Shape(_w, value) - return _w.finish() - - -def _unpack_Shape(data: bytes) -> "Shape": - return _decode_buffer(data, _read_Shape) - - -def make_shape(sides: int) -> "Shape": - """Construct a Shape. - """ - _fn = _lib.weaveffi_graphics_shapes_make_shape - _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(sides, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Shape(_data) - - -def describe() -> "Canvas": - """Nested function returning a struct owned by an ancestor module. - """ - _fn = _lib.weaveffi_graphics_shapes_describe - _fn.argtypes = [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(ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Canvas(_data) - - -# === Module: graphics_shapes_geometry === - -@dataclass -class Detail: - """A struct two levels deep.""" - - area: float - - -def _write_Detail(_w: _BufferWriter, value: "Detail") -> None: - _w.write_f64(value.area) - - -def _read_Detail(_r: _BufferReader) -> "Detail": - return Detail( - area=_r.read_f64(), - ) - - -def _pack_Detail(value: "Detail") -> bytes: - _w = _BufferWriter() - _write_Detail(_w, value) - return _w.finish() - - -def _unpack_Detail(data: bytes) -> "Detail": - return _decode_buffer(data, _read_Detail) - - -def compute(shape: "Shape") -> "Detail": - """Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - - Raises - ------ - GraphicsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_graphics_shapes_geometry_compute - _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 - _shape_buf = _pack_Shape(shape) - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(_shape_buf, len(_shape_buf), ctypes.byref(_out_len), ctypes.byref(_err)) - _check_graphics_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Detail(_data) - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__README_md.snap index e3b8a7b9..7c7e328e 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_shapes__README_md.snap @@ -1,18 +1,18 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Python) +# shapes (Python) -Auto-generated Python bindings using ctypes. +Python bindings for the `shapes` native library, built on `ctypes`. -## Prerequisites +## Requirements -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. +- Python 3.9 or later +- The native library (`libshapes.so`, `libshapes.dylib`, or `shapes.dll`) ## Install @@ -20,16 +20,16 @@ Auto-generated Python bindings using ctypes. pip install . ``` -## Development install +## Loading the native library -```bash -pip install -e . -``` +The bindings load the library from `SHAPES_LIBRARY` when it is set (a full path), +then from inside the installed package, then from the system loader's search +path. ## Usage ```python -from weaveffi import * +import shapes ``` diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__pyproject_toml.snap index 244fb84d..5efd79d6 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__pyproject_toml.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_shapes__pyproject_toml.snap @@ -1,24 +1,24 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "weaveffi" +name = "shapes" version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" +description = "Native bindings for shapes" +requires-python = ">=3.9" [tool.setuptools] -packages = ["weaveffi"] +packages = ["shapes"] [tool.setuptools.package-data] -"weaveffi" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] +"shapes" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] # END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__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_shapes__shapes___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes___init___py.snap new file mode 100644 index 00000000..0029ff1c --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes___init___py.snap @@ -0,0 +1,10 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +from .shapes import * # noqa: F401,F403 + +# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_py_typed.snap new file mode 100644 index 00000000..f8cd271e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_py_typed.snap @@ -0,0 +1,6 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_py.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_py.snap new file mode 100644 index 00000000..72a15b5f --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_py.snap @@ -0,0 +1,770 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +"""ctypes bindings for the shapes native library.""" +import builtins +import ctypes +import os +import platform +import struct +import threading +import warnings +from dataclasses import dataclass +from enum import IntEnum +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type + + +class Error(Exception): + """An error reported by the native library. + + `code` is positive for one of the API's declared error codes (raised as + the declaring module's typed subclasses) and negative for a runtime trap: + a generic failure (-1), a producer panic (-2), a marshalling failure + (-3), or an exception raised by a callback-interface implementation + (-4). A cancelled async call (-5) raises `asyncio.CancelledError` + instead. + """ + + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + FOREIGN_ERROR_CODE = -4 + CANCELLED_ERROR_CODE = -5 + + def __init__(self, code: int, message: str) -> None: + super().__init__(f"({code}) {message}") + self.code = code + self.message = message + + +class _ErrorStruct(ctypes.Structure): + """The C `shapes_error` out-parameter every call reports through.""" + + _fields_ = [ + ("code", ctypes.c_int32), + ("message", ctypes.c_char_p), + ("payload_ptr", ctypes.c_void_p), + ("payload_len", ctypes.c_size_t), + ] + + +def _load_library() -> ctypes.CDLL: + # An explicit path wins; then a library bundled inside this package (a + # platform wheel); then the system loader's search path. + override = os.environ.get("SHAPES_LIBRARY") + if override: + return ctypes.CDLL(override) + system = platform.system() + if system == "Darwin": + name = "libshapes.dylib" + elif system == "Windows": + name = "shapes.dll" + else: + name = "libshapes.so" + bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) + if os.path.exists(bundled): + return ctypes.CDLL(bundled) + try: + return ctypes.CDLL(name) + except OSError as exc: + raise ImportError( + f"cannot load the native library {name!r} ({exc}); " + "set SHAPES_LIBRARY to its path" + ) from exc + + +_lib = _load_library() + + +def _bind(name: str, restype: Any, *argtypes: Any) -> Any: + """Look up one exported symbol and fix its C signature. Every prototype + is bound once, at import time.""" + fn = getattr(_lib, name) + fn.restype = restype + fn.argtypes = argtypes + return fn + + +# The C ABI revision these bindings were generated against, and the contract +# checksum of every top-level module: (module, checksum symbol, value). +_ABI_VERSION = 3 +_CHECKSUMS: List[Tuple[str, str, int]] = [ + ("shapes", "shapes_shapes_checksum", 0x06acf433ffa2a0f0), +] + + +def _check_contract() -> None: + """Refuse to load a library built for a different ABI revision or a + different version of any module's API, before any other symbol is + bound.""" + try: + abi_version = _bind("shapes_abi_version", ctypes.c_uint32) + except AttributeError: + raise ImportError( + f"{_lib._name} does not export shapes_abi_version; " + "it is not the library these bindings were generated for" + ) from None + found = abi_version() + if found != _ABI_VERSION: + raise ImportError( + f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " + f"but {_lib._name} reports revision {found}" + ) + for module, symbol, expected in _CHECKSUMS: + try: + checksum = _bind(symbol, ctypes.c_uint64) + except AttributeError: + raise ImportError( + f"module {module!r}: {_lib._name} does not export {symbol}" + ) from None + found = checksum() + if found != expected: + raise ImportError( + f"module {module!r}: the native library's contract checksum " + f"{found:#018x} does not match these bindings ({expected:#018x}); " + "regenerate the bindings from the library's current API" + ) + + +_check_contract() + +_error_clear = _bind("shapes_error_clear", None, ctypes.POINTER(_ErrorStruct)) +_free_bytes = _bind("shapes_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) + + +def _debug_live(kind: int) -> int: + """The producer's live-allocation counter `kind` (0 objects, 1 callbacks, + 2 iterators, 3 cancel tokens, 4 returned allocations). Always 0 unless + the producer was built with its leak counters enabled.""" + return _bind("shapes_debug_live", ctypes.c_uint64, ctypes.c_int32)(kind) + + +def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: + """Copy a filled out-err slot's code, message, and payload, then release + them.""" + code = err.code + message = err.message.decode("utf-8", "replace") if err.message else "" + payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + _error_clear(ctypes.byref(err)) + return code, message, payload + + +def _error_from(code: int, message: str, payload: bytes = b"") -> Error: + """The exception for a code no declared error domain claims.""" + return Error(code, message) + + +def _check_error(err: _ErrorStruct) -> None: + """Raise for a non-zero out-err slot of a call that declares no errors: + only runtime traps (a panic, a marshalling failure, a failed callback) + land here.""" + if err.code: + raise _error_from(*_read_error(err)) + + +def _take_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a producer-owned byte run (a string, bytes, or value buffer) and + release it with shapes_free_bytes.""" + if not ptr: + return b"" + try: + return ctypes.string_at(ptr, length) + finally: + _free_bytes(ptr, length) + + +def _take_str(ptr: Optional[int], length: int) -> str: + """Copy and release a producer-owned UTF-8 string.""" + return _take_bytes(ptr, length).decode("utf-8") + + +def _peek_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a byte run the producer only lends for the current call.""" + return ctypes.string_at(ptr, length) if ptr and length else b"" + + +class _Handle: + """Owns one native reference and releases it exactly once. + + Calls lend the pointer through `_acquire`/`_release`, so `close()` from + another thread while a call is in flight defers the release until the + last such call returns instead of freeing the object mid-call. + """ + + # The native destroy (or iterator release) function, set per subclass. + _destroy: Callable[[int], None] + + def _init_handle(self, ptr: int) -> None: + self._ptr: Optional[int] = ptr + self._calls = 0 + self._closing = False + self._lock = threading.Lock() + + @classmethod + def _adopt(cls, ptr: int) -> Any: + """Wrap one strong reference the producer handed over.""" + obj = cls.__new__(cls) + obj._init_handle(ptr) + return obj + + def _acquire(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise Error(-1, f"{type(self).__name__} used after close()") + self._calls += 1 + return ptr + + def _release(self) -> None: + with self._lock: + self._calls -= 1 + if self._calls or not self._closing: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def close(self) -> None: + """Release this wrapper's native reference. Idempotent; the wrapper + is unusable afterwards.""" + lock = getattr(self, "_lock", None) + if lock is None: + return + with lock: + if self._closing: + return + self._closing = True + if self._calls: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def __enter__(self) -> Any: + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def __del__(self) -> None: + # An unreachable wrapper has no call in flight: release directly. + ptr = getattr(self, "_ptr", None) + if ptr is not None: + self._ptr = None + type(self)._destroy(ptr) + + +class _Object(_Handle): + """Base of every interface wrapper: a reference-counted native object.""" + + # The native clone function, set per subclass. + _clone: Callable[[int], int] + + def _clone_ref(self) -> int: + """A new strong reference to the same object, for a receiver that + adopts it (an object token inside a value buffer).""" + ptr = self._acquire() + try: + return type(self)._clone(ptr) + finally: + self._release() + + +class _Iterator(_Handle): + """Base of every lazy iterator over a producer stream: one native `next` + call per step; the handle is released on exhaustion, `close()`, or + garbage collection.""" + + def __iter__(self) -> Any: + return self + + def _step(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise StopIteration + self._calls += 1 + return ptr + + +def _required(ptr: Optional[int]) -> int: + """A non-null object pointer the producer returned.""" + if not ptr: + raise Error(-1, "the producer returned a null object") + return ptr + + +def _lend(obj: Any, cls: "Type[_Object]") -> int: + """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + return obj._acquire() + + +def _lend_opt(obj: Any, cls: "Type[_Object]") -> Optional[int]: + """`_lend` for an optional object: None lends a null pointer.""" + return None if obj is None else _lend(obj, cls) + + +def _release_opt(obj: Any) -> None: + if obj is not None: + obj._release() + + +_I8 = struct.Struct(" Error: + return Error(-3, f"malformed value buffer: {what}") + + +class _Writer: + """Encodes values in the value-buffer wire format: little-endian, + packed, no alignment.""" + + __slots__ = ("_buf", "_objects") + + def __init__(self) -> None: + self._buf = bytearray() + self._objects: List[Tuple[int, _Object]] = [] + + def finish(self) -> bytes: + # Object tokens are minted last, so an encoding that fails part-way + # never leaks a strong reference. + minted: List[Tuple[_Object, int]] = [] + try: + for pos, obj in self._objects: + ptr = obj._clone_ref() + minted.append((obj, ptr)) + _U64.pack_into(self._buf, pos, ptr) + except BaseException: + for obj, ptr in minted: + type(obj)._destroy(ptr) + raise + self._objects = [] + return bytes(self._buf) + + def write_bool(self, v: bool) -> None: + self._buf.append(1 if v else 0) + + def write_i8(self, v: int) -> None: + self._buf += _I8.pack(v) + + def write_u8(self, v: int) -> None: + self._buf += _U8.pack(v) + + def write_i16(self, v: int) -> None: + self._buf += _I16.pack(v) + + def write_u16(self, v: int) -> None: + self._buf += _U16.pack(v) + + def write_i32(self, v: int) -> None: + self._buf += _I32.pack(v) + + def write_u32(self, v: int) -> None: + self._buf += _U32.pack(v) + + def write_i64(self, v: int) -> None: + self._buf += _I64.pack(v) + + def write_u64(self, v: int) -> None: + self._buf += _U64.pack(v) + + def write_f32(self, v: float) -> None: + self._buf += _F32.pack(v) + + def write_f64(self, v: float) -> None: + self._buf += _F64.pack(v) + + def write_count(self, n: int) -> None: + self._buf += _U32.pack(n) + + def write_flag(self, present: bool) -> None: + self._buf.append(1 if present else 0) + + def write_string(self, v: str) -> None: + data = v.encode("utf-8") + self._buf += _U32.pack(len(data)) + self._buf += data + + def write_bytes(self, v: bytes) -> None: + self._buf += _U32.pack(len(v)) + self._buf += v + + def write_numbers(self, code: str, values: List[Any]) -> None: + """A list of one fixed-width numeric primitive (`code` is its struct + format character), packed in one call.""" + self._buf += _U32.pack(len(values)) + self._buf += struct.pack(f"<{len(values)}{code}", *values) + + def write_object(self, obj: Any, cls: "Type[_Object]") -> None: + """Reserve an object token: one strong reference to `obj` the reader + adopts, cloned in `finish()`. A closed wrapper is rejected now.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + obj._acquire() + obj._release() + self._objects.append((len(self._buf), obj)) + self._buf += bytes(8) + + +class _Reader: + """Decodes values from the value-buffer wire format, rejecting truncated + buffers, invalid flag bytes, invalid UTF-8, and trailing data.""" + + __slots__ = ("_data", "_pos") + + def __init__(self, data: bytes) -> None: + self._data = data + self._pos = 0 + + def _take(self, n: int, what: str) -> int: + pos = self._pos + if len(self._data) - pos < n: + raise _malformed(f"truncated {what}") + self._pos = pos + n + return pos + + def read_bool(self) -> bool: + b = self._data[self._take(1, "bool")] + if b > 1: + raise _malformed("invalid bool byte") + return b == 1 + + def read_i8(self) -> int: + return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) + + def read_u8(self) -> int: + return self._data[self._take(1, "u8")] + + def read_i16(self) -> int: + return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) + + def read_u16(self) -> int: + return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) + + def read_i32(self) -> int: + return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) + + def read_u32(self) -> int: + return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) + + def read_i64(self) -> int: + return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) + + def read_u64(self) -> int: + return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) + + def read_f32(self) -> float: + return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) + + def read_f64(self) -> float: + return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) + + def read_count(self) -> int: + # An element count, not a byte length: elements can encode to zero + # bytes, so it is not bounded by the remaining data. + return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) + + def read_flag(self) -> bool: + b = self._data[self._take(1, "option flag")] + if b > 1: + raise _malformed("invalid option flag") + return b == 1 + + def read_bytes(self) -> bytes: + n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) + pos = self._take(n, "bytes") + return self._data[pos:pos + n] + + def read_string(self) -> str: + try: + return self.read_bytes().decode("utf-8") + except UnicodeDecodeError: + raise _malformed("string is not valid UTF-8") from None + + def read_numbers(self, code: str, size: int) -> List[Any]: + """A list of one fixed-width numeric primitive, unpacked in one call.""" + n = self.read_count() + pos = self._take(n * size, "list") + return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) + + def read_object(self) -> int: + """An object token: one strong reference the caller adopts.""" + ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) + if ptr == 0: + raise _malformed("null object token") + return ptr + + def expect_end(self) -> None: + if self._pos != len(self._data): + raise _malformed("trailing bytes") + + +def _decode(data: bytes, read: Callable[[_Reader], Any]) -> Any: + """Decode exactly one value from `data`.""" + r = _Reader(data) + value = read(r) + r.expect_end() + return value + + +# === Module: shapes === + + +class Shape: + """An algebraic shape (sum type with associated data)""" + + class Tag(IntEnum): + # The empty shape (a unit variant) + Empty = 0 + # A circle with a radius + Circle = 1 + # An axis-aligned rectangle + Rectangle = 2 + # An integer-bounded shape (exercises narrow + wide ints) + Bounded = 3 + # A labeled shape with a small count + Labeled = 4 + + Empty: "Type[ShapeEmpty]" + Circle: "Type[ShapeCircle]" + Rectangle: "Type[ShapeRectangle]" + Bounded: "Type[ShapeBounded]" + Labeled: "Type[ShapeLabeled]" + TAG: "Shape.Tag" + + @property + def tag(self) -> "Shape.Tag": + """The discriminant of this value's active variant.""" + return type(self).TAG + + +@dataclass +class ShapeEmpty(Shape): + """The empty shape (a unit variant)""" + + TAG = Shape.Tag.Empty + + +@dataclass +class ShapeCircle(Shape): + """A circle with a radius""" + + TAG = Shape.Tag.Circle + + # Radius in points + radius: float + + +@dataclass +class ShapeRectangle(Shape): + """An axis-aligned rectangle""" + + TAG = Shape.Tag.Rectangle + + width: float + height: float + + +@dataclass +class ShapeBounded(Shape): + """An integer-bounded shape (exercises narrow + wide ints)""" + + TAG = Shape.Tag.Bounded + + min: int + max: int + weight: int + + +@dataclass +class ShapeLabeled(Shape): + """A labeled shape with a small count""" + + TAG = Shape.Tag.Labeled + + label: str + count: int + + +Shape.Empty = ShapeEmpty +Shape.Circle = ShapeCircle +Shape.Rectangle = ShapeRectangle +Shape.Bounded = ShapeBounded +Shape.Labeled = ShapeLabeled + + +def _write_Shape(_w: _Writer, value: "Shape") -> None: + if isinstance(value, ShapeEmpty): + _w.write_i32(0) + return + if isinstance(value, ShapeCircle): + _w.write_i32(1) + _w.write_f64(value.radius) + return + if isinstance(value, ShapeRectangle): + _w.write_i32(2) + _w.write_f32(value.width) + _w.write_f32(value.height) + return + if isinstance(value, ShapeBounded): + _w.write_i32(3) + _w.write_i16(value.min) + _w.write_i16(value.max) + _w.write_u64(value.weight) + return + if isinstance(value, ShapeLabeled): + _w.write_i32(4) + _w.write_string(value.label) + _w.write_u8(value.count) + return + raise TypeError(f"expected a Shape variant, got {type(value).__name__}") + + +def _read_Shape(_r: _Reader) -> "Shape": + _tag = _r.read_i32() + if _tag == 0: + return ShapeEmpty() + if _tag == 1: + return ShapeCircle( + radius=_r.read_f64(), + ) + if _tag == 2: + return ShapeRectangle( + width=_r.read_f32(), + height=_r.read_f32(), + ) + if _tag == 3: + return ShapeBounded( + min=_r.read_i16(), + max=_r.read_i16(), + weight=_r.read_u64(), + ) + if _tag == 4: + return ShapeLabeled( + label=_r.read_string(), + count=_r.read_u8(), + ) + raise _malformed(f"unknown Shape tag {_tag}") + + +class Channel(IntEnum): + """A plain C-style enum (no payloads)""" + Red = 0 + Green = 1 + Blue = 2 + + +@dataclass +class Numerics: + """One field per supported numeric primitive""" + + a: int + b: int + c: int + d: int + e: int + f: int + g: int + h: int + i: float + j: float + + +def _write_Numerics(_w: _Writer, value: "Numerics") -> None: + _w.write_i8(value.a) + _w.write_i16(value.b) + _w.write_i32(value.c) + _w.write_i64(value.d) + _w.write_u8(value.e) + _w.write_u16(value.f) + _w.write_u32(value.g) + _w.write_u64(value.h) + _w.write_f32(value.i) + _w.write_f64(value.j) + + +def _read_Numerics(_r: _Reader) -> "Numerics": + return Numerics( + a=_r.read_i8(), + b=_r.read_i16(), + c=_r.read_i32(), + d=_r.read_i64(), + e=_r.read_u8(), + f=_r.read_u16(), + g=_r.read_u32(), + h=_r.read_u64(), + i=_r.read_f32(), + j=_r.read_f64(), + ) + + +_c_shapes_describe = _bind("shapes_shapes_describe", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def describe(shape: "Shape") -> str: + """Render a shape to a string""" + _shape_w = _Writer() + _write_Shape(_shape_w, shape) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _shape_b = _shape_w.finish() + _ret = _c_shapes_describe(_shape_b, len(_shape_b), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _take_str(_ret, _out_len.value) + + +_c_shapes_scale = _bind("shapes_shapes_scale", ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_double, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def scale(shape: "Shape", factor: float) -> "Shape": + """Scale a shape by a factor, returning a new shape""" + _shape_w = _Writer() + _write_Shape(_shape_w, shape) + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _shape_b = _shape_w.finish() + _ret = _c_shapes_scale(_shape_b, len(_shape_b), factor, ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Shape) + + +_c_shapes_sum_bytes = _bind("shapes_shapes_sum_bytes", ctypes.c_uint64, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct)) + + +def sum_bytes(values: bytes) -> int: + """Sum a list of bytes into a wide integer (numerics smoke)""" + _values_b = bytes(values) + _err = _ErrorStruct() + _ret = _c_shapes_sum_bytes(_values_b, len(_values_b), ctypes.byref(_err)) + _check_error(_err) + return _ret + + +_c_shapes_make_numerics = _bind("shapes_shapes_make_numerics", ctypes.c_void_p, ctypes.c_int8, ctypes.c_int16, ctypes.c_int32, ctypes.c_int64, ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint32, ctypes.c_uint64, ctypes.c_float, ctypes.c_double, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) + + +def make_numerics(a: int, b: int, c: int, d: int, e: int, f: int, g: int, h: int, i: float, j: float) -> "Numerics": + """Build a Numerics struct from every numeric primitive""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _ret = _c_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + return _decode(_take_bytes(_ret, _out_len.value), _read_Numerics) + +# END shapes.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_pyi.snap similarity index 86% rename from crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_pyi.snap rename to crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_pyi.snap index bdf758b2..50f08b51 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_pyi.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_shapes__shapes_shapes_pyi.snap @@ -1,19 +1,21 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o +import builtins from abc import ABC, abstractmethod from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type +from typing import Dict, Iterator, List, Optional, Type -class WeaveFFIError(Exception): +class Error(Exception): GENERIC_ERROR_CODE: int PANIC_ERROR_CODE: int MARSHAL_ERROR_CODE: int FOREIGN_ERROR_CODE: int + CANCELLED_ERROR_CODE: int code: int message: str def __init__(self, code: int, message: str) -> None: ... @@ -22,15 +24,15 @@ class WeaveFFIError(Exception): class Shape: class Tag(IntEnum): # The empty shape (a unit variant) - Empty: int + Empty = 0 # A circle with a radius - Circle: int + Circle = 1 # An axis-aligned rectangle - Rectangle: int + Rectangle = 2 # An integer-bounded shape (exercises narrow + wide ints) - Bounded: int + Bounded = 3 # A labeled shape with a small count - Labeled: int + Labeled = 4 Empty: Type["ShapeEmpty"] Circle: Type["ShapeCircle"] Rectangle: Type["ShapeRectangle"] @@ -47,6 +49,7 @@ class ShapeEmpty(Shape): # A circle with a radius class ShapeCircle(Shape): TAG: "Shape.Tag" + # Radius in points radius: float def __init__(self, radius: float) -> None: ... @@ -74,9 +77,9 @@ class ShapeLabeled(Shape): # A plain C-style enum (no payloads) class Channel(IntEnum): - Red: int - Green: int - Blue: int + Red = 0 + Green = 1 + Blue = 2 # One field per supported numeric primitive class Numerics: @@ -104,4 +107,4 @@ def sum_bytes(values: bytes) -> int: ... # Build a Numerics struct from every numeric primitive def make_numerics(a: int, b: int, c: int, d: int, e: int, f: int, g: int, h: int, i: float, j: float) -> "Numerics": ... -# END weaveffi.pyi +# END shapes.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__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_shapes__weaveffi_py_typed.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_py_typed.snap deleted file mode 100644 index 501a8090..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_py_typed.snap +++ /dev/null @@ -1,6 +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 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 deleted file mode 100644 index 8c65ea50..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,604 +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): - """An error reported through the C ABI. - - Positive codes are a module's declared error codes (raised as the - module's typed subclasses); negative codes are runtime traps: a generic - producer error (-1), a producer panic (-2), a marshalling failure (-3), - or an exception raised by a callback-interface implementation (-4). - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - - 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 = 2 - - -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) -# Callback-interface trampolines report a failed implementation through -# weaveffi_error_set, which copies the message with the producer's allocator. -_lib.weaveffi_error_set.argtypes = [ - ctypes.POINTER(_WeaveFFIErrorStruct), - ctypes.c_int32, - ctypes.c_char_p, -] -_lib.weaveffi_error_set.restype = None -_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 - - -def _borrow(obj): - """The raw pointer of a live object wrapper, lent to the producer for the - duration of one call. Raises if the wrapper was already closed.""" - _p = obj._ptr - if _p is None: - raise WeaveFFIError(-1, f"{type(obj).__name__} used after close()") - return _p - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - self._objects = [] - - def finish(self) -> bytes: - # Mint the object tokens last: an encoding that raised part-way - # (a closed wrapper, a field of the wrong type) never reaches here, - # so it leaks no strong references. - for _pos, _obj in self._objects: - struct.pack_into(" 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) - - def write_object(self, obj) -> None: - """Write an object token: one strong reference to `obj` the reader - will adopt. The wrapper is checked to be live now (a closed one - raises), but the reference itself is cloned in finish().""" - _borrow(obj) - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -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 read_object(self) -> int: - """Read an object token: one strong reference the caller adopts into - a wrapper. Zero never appears in a non-optional position.""" - _p = struct.unpack(" 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: shapes === - -class Shape: - """An algebraic shape (sum type with associated data)""" - - class Tag(IntEnum): - # The empty shape (a unit variant) - Empty = 0 - # A circle with a radius - Circle = 1 - # An axis-aligned rectangle - Rectangle = 2 - # An integer-bounded shape (exercises narrow + wide ints) - Bounded = 3 - # A labeled shape with a small count - Labeled = 4 - - @property - def tag(self) -> "Shape.Tag": - """The discriminant of this value's active variant.""" - return type(self).TAG - - -@dataclass -class ShapeEmpty(Shape): - """The empty shape (a unit variant)""" - - TAG = Shape.Tag.Empty - - -@dataclass -class ShapeCircle(Shape): - """A circle with a radius""" - - TAG = Shape.Tag.Circle - - # Radius in points - radius: float - - -@dataclass -class ShapeRectangle(Shape): - """An axis-aligned rectangle""" - - TAG = Shape.Tag.Rectangle - - width: float - height: float - - -@dataclass -class ShapeBounded(Shape): - """An integer-bounded shape (exercises narrow + wide ints)""" - - TAG = Shape.Tag.Bounded - - min: int - max: int - weight: int - - -@dataclass -class ShapeLabeled(Shape): - """A labeled shape with a small count""" - - TAG = Shape.Tag.Labeled - - label: str - count: int - - -Shape.Empty = ShapeEmpty -Shape.Circle = ShapeCircle -Shape.Rectangle = ShapeRectangle -Shape.Bounded = ShapeBounded -Shape.Labeled = ShapeLabeled - - -def _write_Shape(_w: _BufferWriter, value: "Shape") -> None: - if isinstance(value, ShapeEmpty): - _w.write_i32(0) - return - if isinstance(value, ShapeCircle): - _w.write_i32(1) - _w.write_f64(value.radius) - return - if isinstance(value, ShapeRectangle): - _w.write_i32(2) - _w.write_f32(value.width) - _w.write_f32(value.height) - return - if isinstance(value, ShapeBounded): - _w.write_i32(3) - _w.write_i16(value.min) - _w.write_i16(value.max) - _w.write_u64(value.weight) - return - if isinstance(value, ShapeLabeled): - _w.write_i32(4) - _w.write_string(value.label) - _w.write_u8(value.count) - return - raise WeaveFFIError(-1, "unknown Shape variant") - - -def _read_Shape(_r: _BufferReader) -> "Shape": - _tag = _r.read_i32() - if _tag == 0: - return ShapeEmpty() - if _tag == 1: - return ShapeCircle( - radius=_r.read_f64(), - ) - if _tag == 2: - return ShapeRectangle( - width=_r.read_f32(), - height=_r.read_f32(), - ) - if _tag == 3: - return ShapeBounded( - min=_r.read_i16(), - max=_r.read_i16(), - weight=_r.read_u64(), - ) - if _tag == 4: - return ShapeLabeled( - label=_r.read_string(), - count=_r.read_u8(), - ) - raise WeaveFFIError(-1, f"malformed value buffer: unknown Shape tag {_tag}") - - -def _pack_Shape(value: "Shape") -> bytes: - _w = _BufferWriter() - _write_Shape(_w, value) - return _w.finish() - - -def _unpack_Shape(data: bytes) -> "Shape": - return _decode_buffer(data, _read_Shape) - - -class Channel(IntEnum): - """A plain C-style enum (no payloads)""" - Red = 0 - Green = 1 - Blue = 2 - - -@dataclass -class Numerics: - """One field per supported numeric primitive""" - - a: int - b: int - c: int - d: int - e: int - f: int - g: int - h: int - i: float - j: float - - -def _write_Numerics(_w: _BufferWriter, value: "Numerics") -> None: - _w.write_i8(value.a) - _w.write_i16(value.b) - _w.write_i32(value.c) - _w.write_i64(value.d) - _w.write_u8(value.e) - _w.write_u16(value.f) - _w.write_u32(value.g) - _w.write_u64(value.h) - _w.write_f32(value.i) - _w.write_f64(value.j) - - -def _read_Numerics(_r: _BufferReader) -> "Numerics": - return Numerics( - a=_r.read_i8(), - b=_r.read_i16(), - c=_r.read_i32(), - d=_r.read_i64(), - e=_r.read_u8(), - f=_r.read_u16(), - g=_r.read_u32(), - h=_r.read_u64(), - i=_r.read_f32(), - j=_r.read_f64(), - ) - - -def _pack_Numerics(value: "Numerics") -> bytes: - _w = _BufferWriter() - _write_Numerics(_w, value) - return _w.finish() - - -def _unpack_Numerics(data: bytes) -> "Numerics": - return _decode_buffer(data, _read_Numerics) - - -def describe(shape: "Shape") -> str: - """Render a shape to a string - """ - _fn = _lib.weaveffi_shapes_describe - _fn.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _shape_buf = _pack_Shape(shape) - _err = _WeaveFFIErrorStruct() - _result = _fn(_shape_buf, len(_shape_buf), ctypes.byref(_err)) - _check_error(_err) - return _take_string(_result) or "" - - -def scale(shape: "Shape", factor: float) -> "Shape": - """Scale a shape by a factor, returning a new shape - """ - _fn = _lib.weaveffi_shapes_scale - _fn.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_double, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _shape_buf = _pack_Shape(shape) - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(_shape_buf, len(_shape_buf), factor, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Shape(_data) - - -def sum_bytes(values: bytes) -> int: - """Sum a list of bytes into a wide integer (numerics smoke) - """ - _fn = _lib.weaveffi_shapes_sum_bytes - _fn.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_uint64 - _values_arr = (ctypes.c_uint8 * len(values))(*values) - _err = _WeaveFFIErrorStruct() - _result = _fn(_values_arr, len(values), ctypes.byref(_err)) - _check_error(_err) - return _result - - -def make_numerics(a: int, b: int, c: int, d: int, e: int, f: int, g: int, h: int, i: float, j: float) -> "Numerics": - """Build a Numerics struct from every numeric primitive - """ - _fn = _lib.weaveffi_shapes_make_numerics - _fn.argtypes = [ctypes.c_int8, ctypes.c_int16, ctypes.c_int32, ctypes.c_int64, ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint32, ctypes.c_uint64, ctypes.c_float, ctypes.c_double, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(a, b, c, d, e, f, g, h, i, j, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Numerics(_data) - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__README_md.snap index dfa27948..33cb6c5b 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__README_md.snap @@ -1,30 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Ruby) +# docs_everywhere (Ruby) -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. +Ruby bindings for `docs_everywhere` 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. +- Ruby 2.7 or newer +- The native library (`libdocs_everywhere.so`, `libdocs_everywhere.dylib`, or `docs_everywhere.dll`) on the library + search path, or its path in the `DOCS_EVERYWHERE_LIBRARY` environment variable. ## Install ```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem +gem build docs_everywhere.gemspec +gem install docs_everywhere-0.1.0.gem ``` ## Usage ```ruby -require 'weaveffi' +require 'docs_everywhere' + +DocsEverywhere::ABI_VERSION ``` diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__docs_everywhere_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__docs_everywhere_gemspec.snap new file mode 100644 index 00000000..efa508fe --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__docs_everywhere_gemspec.snap @@ -0,0 +1,20 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +Gem::Specification.new do |s| + s.name = 'docs_everywhere' + s.version = '0.1.0' + s.summary = 'Native bindings for docs_everywhere' + s.authors = ['docs_everywhere'] + s.files = Dir['lib/**/*.rb'] + s.require_paths = ['lib'] + s.required_ruby_version = '>= 2.7' + + s.add_dependency 'ffi', '~> 1.15' +end + +# END docs_everywhere.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_rb.snap new file mode 100644 index 00000000..8699cc91 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_rb.snap @@ -0,0 +1,354 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require_relative 'docs_everywhere/runtime' + +# Ruby bindings for docs_everywhere. +module DocsEverywhere + # Contract checksums of the top-level modules these bindings were + # generated from; a library built from a different API fails to load. + _wv_check_contract!( + 'docs' => [:docs_everywhere_docs_checksum, 0x7950eec1e9cfc819], + ) + + # === Module: docs === + + # Base error for the `docs` module's error domain. + class DocsErrorsError < Error + # Returned when no document matches the requested id + class NotFound < DocsErrorsError + CODE = 1 + + def initialize(message = 'Document not found') + super(1, message) + end + end + + # Raised when attempting to move into a forbidden state + class InvalidStatus < DocsErrorsError + CODE = 2 + + def initialize(message = 'Invalid status transition') + super(2, message) + end + end + end + + # Maps each ABI code of the DocsErrorsError domain to its error class. + DOCS_ERRORS_ERROR_CODES = { + 1 => DocsErrorsError::NotFound, + 2 => DocsErrorsError::InvalidStatus, + }.freeze + + # Builds the DocsErrorsError subclass matching `code`, decoding any payload + # fields the code declares, or a generic Error for codes outside + # the domain (panics, marshalling, callback failures). + def self.docs_errors_error_from(code, message, payload = nil) + cls = DOCS_ERRORS_ERROR_CODES[code] + return _wv_error(code, message) if cls.nil? + message.empty? ? cls.new : cls.new(message) + end + + # Raises the typed DocsErrorsError for a non-zero error slot. + def self.check_docs_errors_error!(err) + taken = _wv_take_error(err) + raise docs_errors_error_from(*taken) if taken + end + + # Lifecycle status of a document. + # + # Used to drive UI badges and filters. + module Status + # Document is being edited + DRAFT = 0 + # Document is publicly visible + PUBLISHED = 1 + # Document is hidden from listings + ARCHIVED = 2 + end + + # Represents a single document tracked by the system. + # + # Documents are persisted to disk and exposed via the public API. + class Document + # Stable opaque identifier + attr_reader :id + + # Human-readable title shown in lists + attr_reader :title + + # Current lifecycle status + attr_reader :status + + # Free-form tags for grouping + attr_reader :tags + + def initialize(id:, title:, status:, tags:) + @id = id + @title = title + @status = status + @tags = tags + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(Document) + return false unless id == other.id + return false unless title == other.title + return false unless status == other.status + return false unless tags == other.tags + true + end + end + + # @api private + # Packs a Document into the value-buffer wire format. + def self._wv_write_document(w, v) + w.write_i64(v.id) + w.write_string(v.title) + w.write_i32(v.status) + w.write_len(v.tags.length) + v.tags.each do |_wv_e0| + w.write_string(_wv_e0) + end + end + + # @api private + # Unpacks a Document from the value-buffer wire format. + def self._wv_read_document(r) + _wv_id = r.read_i64 + _wv_title = r.read_string + _wv_status = r.read_i32 + _wv_tags = Array.new(r.read_len) do + _wv_e0 = r.read_string + _wv_e0 + end + Document.new(id: _wv_id, title: _wv_title, status: _wv_status, tags: _wv_tags) + end + + attach_function :docs_everywhere_docs_Notebook_clone, [:pointer], :pointer + attach_function :docs_everywhere_docs_Notebook_destroy, [:pointer], :void, blocking: true + attach_function :docs_everywhere_docs_Notebook_new, [:pointer], :pointer, blocking: true + attach_function :docs_everywhere_docs_Notebook_insert, [:pointer, :pointer, :size_t, :pointer], :int32, blocking: true + attach_function :docs_everywhere_docs_Notebook_find, [:pointer, :int64, :pointer, :pointer], :pointer, blocking: true + attach_function :docs_everywhere_docs_Notebook_size, [:pointer, :pointer], :int32, blocking: true + attach_function :docs_everywhere_docs_Notebook_max_documents, [:pointer], :int32, blocking: true + attach_function :docs_everywhere_docs_create_document, [:pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :docs_everywhere_docs_get_document, [:int64, :pointer, :pointer], :pointer, blocking: true + attach_function :docs_everywhere_docs_publish_async, [:int64, :pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for publish_async. + DOCS_EVERYWHERE_DOCS_PUBLISH_ASYNC_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :bool]) do |ctx, err, result| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << _wv_error(taken[0], taken[1]) + else + value = result + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :docs_everywhere_docs_watch, [:pointer, :pointer, :pointer], :void, blocking: true + + # Consumer-implemented observer of document-saved notifications. + # + # Implementations must be safe to call from any thread. + # + # Consumer-implemented callback interface. Any object responding to the + # methods below is accepted wherever a SavedListener is expected; include + # this module to inherit NotImplementedError defaults. The producer + # may call the methods from any thread until it releases the + # implementation. + module SavedListener + # Fires after a document has been persisted successfully + # @param doc_id [Object] Identifier of the saved document + # @param revision [Object] Monotonically increasing revision + def on_saved(doc_id, revision) + raise NotImplementedError, "#{self.class}#on_saved is not implemented" + end + end + + # @api private + # The C vtable layout the producer calls a SavedListener through. + class WvSavedListenerVtable < FFI::Struct + layout :on_saved, :pointer, + :free, :pointer + end + + # @api private + # Trampoline for SavedListener#on_saved. + WV_SAVED_LISTENER_ON_SAVED = FFI::Function.new(:void, [:pointer, :int64, :int32, :pointer]) do |ctx, doc_id, revision, out_err| + begin + doc_id_v = doc_id + revision_v = revision + impl = _wv_cb_lookup(ctx) + impl.on_saved(doc_id_v, revision_v) + nil + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + nil + end + end + + # @api private + # Releases a SavedListener implementation when the producer drops its last + # reference. + WV_SAVED_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| + _wv_cb_free(ctx) + end + + # The one process-wide SavedListener vtable every registration hands the + # producer; its entries live for the process lifetime. + WV_SAVED_LISTENER_VTABLE = WvSavedListenerVtable.new + WV_SAVED_LISTENER_VTABLE[:on_saved] = WV_SAVED_LISTENER_ON_SAVED + WV_SAVED_LISTENER_VTABLE[:free] = WV_SAVED_LISTENER_FREE + + # @api private + # Owns one strong reference to a Notebook; releases it exactly once. + class NotebookPtr < FFI::AutoPointer + def self.release(ptr) + DocsEverywhere.docs_everywhere_docs_Notebook_destroy(ptr) + end + end + + # An ordered collection of documents. + # + # Notebooks own their documents and release them when destroyed. + class Notebook < WvObject + WV_PTR = NotebookPtr + + # @api private + def self._wv_clone(ptr) + DocsEverywhere.docs_everywhere_docs_Notebook_clone(ptr) + end + + # Create an empty notebook + def initialize + err = ErrorStruct.new + result = DocsEverywhere.docs_everywhere_docs_Notebook_new(err) + DocsEverywhere.check_error!(err) + raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if result.null? + _wv_init(result) + end + + # Add a document, returning its position in the notebook + # @param document [Object] The document to add + def insert(document) + document_w = WvBufferWriter.new + DocsEverywhere._wv_write_document(document_w, document) + DocsEverywhere._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + document_s = DocsEverywhere._wv_seal(document_w).first + result = DocsEverywhere.docs_everywhere_docs_Notebook_insert(_wv_self, document_s, document_s.bytesize, err) + DocsEverywhere.check_error!(err) + result + end + end + + # Look up a document by identifier, failing when none matches + # @param id [Object] Identifier of the document to find + def find(id) + DocsEverywhere._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = DocsEverywhere.docs_everywhere_docs_Notebook_find(_wv_self, id, out_len, err) + DocsEverywhere.check_docs_errors_error!(err) + DocsEverywhere._wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = DocsEverywhere._wv_read_document(_wv_r) + _wv_value + end + end + end + + # The number of documents currently held + def size + DocsEverywhere._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + result = DocsEverywhere.docs_everywhere_docs_Notebook_size(_wv_self, err) + DocsEverywhere.check_error!(err) + result + end + end + + # The largest number of documents one notebook will hold + def self.max_documents + err = ErrorStruct.new + result = DocsEverywhere.docs_everywhere_docs_Notebook_max_documents(err) + DocsEverywhere.check_error!(err) + result + end + end + + # Create a brand new document. + # + # The document starts in `Draft` status; callers must publish + # it explicitly to make it visible. + # @param title [Object] Human-readable title for the new document + # @param tags [Object] Initial set of tags (may be empty) + def self.create_document(title, tags) + title_s = _wv_str(title) + tags_w = WvBufferWriter.new + tags_w.write_len(tags.length) + tags.each do |_wv_e0| + tags_w.write_string(_wv_e0) + end + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + tags_s = _wv_seal(tags_w).first + result = docs_everywhere_docs_create_document(title_s, title_s.bytesize, tags_s, tags_s.bytesize, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_document(_wv_r) + _wv_value + end + end + + # Look up a document by identifier + # @param id [Object] Identifier returned from create_document + def self.get_document(id) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = docs_everywhere_docs_get_document(id, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + if _wv_r.read_flag + _wv_value = _wv_read_document(_wv_r) + else + _wv_value = nil + end + _wv_value + end + end + + # Publish a document in the background + # @param id [Object] Identifier of the document to publish + # Blocks until the call completes on a producer thread. + def self.publish_async(id) + ctx, queue = _wv_async_begin + docs_everywhere_docs_publish_async(id, DOCS_EVERYWHERE_DOCS_PUBLISH_ASYNC_CALLBACK, ctx) + _wv_async_wait(queue) + end + + # Register a listener for saved notifications + # @param listener [Object] Receiver of saved notifications + def self.watch(listener) + err = ErrorStruct.new + listener_ctx = _wv_cb_register(listener) + docs_everywhere_docs_watch(listener_ctx, WV_SAVED_LISTENER_VTABLE.to_ptr, err) + check_error!(err) + end +end + +# END docs_everywhere.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_runtime_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_runtime_rb.snap new file mode 100644 index 00000000..59ccee30 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_docs_everywhere_runtime_rb.snap @@ -0,0 +1,705 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require 'ffi' +require 'monitor' + +# The fixed runtime every generated DocsEverywhere binding builds on: the native +# library loader, the load-time ABI check, the error classes, the +# value-buffer reader and writer, cancel tokens, the handle table behind +# callback interfaces and async completions, and the object wrapper base. +module DocsEverywhere + extend FFI::Library + + # An explicit path in DOCS_EVERYWHERE_LIBRARY wins. Otherwise a library bundled + # under lib/native/ (a platform gem) is preferred over the system search + # path. + _wv_override = ENV['DOCS_EVERYWHERE_LIBRARY'] + if _wv_override && !_wv_override.empty? + ffi_lib _wv_override + else + _wv_name = + case FFI::Platform::OS + when /darwin/ then 'libdocs_everywhere.dylib' + when /mswin|mingw|windows/ then 'docs_everywhere.dll' + else 'libdocs_everywhere.so' + end + _wv_bundled = File.expand_path(File.join('..', 'native', _wv_name), __dir__) + ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) + end + + # The C ABI revision these bindings speak. Checked before anything else is + # attached, so a mismatched library fails at require time instead of + # misreading an error struct or a value buffer later. + ABI_VERSION = 3 + begin + attach_function :docs_everywhere_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, "the loaded docs_everywhere library does not export docs_everywhere_abi_version " \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = docs_everywhere_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "docs_everywhere ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + + # The `docs_everywhere_error` out-parameter every call reports through. + class ErrorStruct < FFI::Struct + layout :code, :int32, + :message, :pointer, + :payload_ptr, :pointer, + :payload_len, :size_t + end + + # Codes reserved by the runtime. Positive codes belong to a module's error + # domain; these negative codes are raised as a plain Error (or Cancelled) + # regardless of whether the function declares errors. + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + # A callback-interface implementation raised; the message carries the + # exception text. + FOREIGN_ERROR_CODE = -4 + # The call was cancelled through its CancelToken. + CANCELLED_ERROR_CODE = -5 + + # The base class of every error the library raises. + class Error < StandardError + # The ABI error code: positive for a domain error, negative for a runtime + # failure. + attr_reader :code + + def initialize(code, message) + @code = code + super(message) + end + end + + # Raised when a cancellable call completes because its CancelToken fired. + class Cancelled < Error + def initialize(message = 'cancelled') + super(CANCELLED_ERROR_CODE, message) + end + end + + attach_function :docs_everywhere_error_set, [:pointer, :int32, :string], :void + attach_function :docs_everywhere_error_clear, [:pointer], :void + attach_function :docs_everywhere_error_free, [:pointer], :void + attach_function :docs_everywhere_free_bytes, [:pointer, :size_t], :void + attach_function :docs_everywhere_cancel_token_create, [], :pointer + attach_function :docs_everywhere_cancel_token_cancel, [:pointer], :void + attach_function :docs_everywhere_cancel_token_is_cancelled, [:pointer], :bool + attach_function :docs_everywhere_cancel_token_destroy, [:pointer], :void + + # @api private + # The error for a code outside any declared domain. + def self._wv_error(code, message) + return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE + + Error.new(code, message) + end + + # @api private + # Copies the code, message, and payload out of a reported error and clears + # it. Returns nil when the struct reports success. + def self._wv_take_error(err) + code = err[:code] + return nil if code.zero? + + msg_ptr = err[:message] + message = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) + payload_ptr = err[:payload_ptr] + payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) + docs_everywhere_error_clear(err.to_ptr) + [code, message, payload] + end + + # @api private + # Like _wv_take_error for the heap-boxed error an async completion + # receives; the box is released with docs_everywhere_error_free. + def self._wv_take_boxed_error(err_ptr) + return nil if err_ptr.null? + + taken = _wv_take_error(ErrorStruct.new(err_ptr)) + docs_everywhere_error_free(err_ptr) + taken + end + + # @api private + # Raises the generic error for a call that declares no error domain. + def self.check_error!(err) + taken = _wv_take_error(err) + return if taken.nil? + + raise _wv_error(taken[0], taken[1]) + end + + # @api private + # The UTF-8 bytes of a String argument, as a private binary copy the + # producer borrows for the call. + def self._wv_str(value) + value.to_s.encode(Encoding::UTF_8).b + end + + # @api private + # A private binary copy of a bytes argument. + def self._wv_bytes(value) + value.to_s.b + end + + # @api private + # Copies a borrowed (ptr, len) run without releasing it. + def self._wv_borrow_bytes(ptr, len) + ptr.null? || len.zero? ? ''.b : ptr.read_string(len) + end + + # @api private + # Copies a returned (ptr, len) allocation, then releases it. + def self._wv_take_bytes(ptr, len) + return ''.b if ptr.null? + + data = len.zero? ? ''.b : ptr.read_string(len) + docs_everywhere_free_bytes(ptr, len) + data + end + + # @api private + # Copies a returned UTF-8 string, then releases it. + def self._wv_take_string(ptr, len) + _wv_take_bytes(ptr, len).force_encoding(Encoding::UTF_8) + end + + # A cancellation signal for a cancellable async call. Pass one as the + # `cancel:` keyword, then call #cancel from any thread; the call raises + # Cancelled unless it already completed. A token may be shared by several + # calls and stays cancelled once cancelled. + class CancelToken + # @api private + # Owns the consumer's reference to the native token. + class Ptr < FFI::AutoPointer + def self.release(ptr) + DocsEverywhere.docs_everywhere_cancel_token_destroy(ptr) + end + end + + def initialize + @ptr = Ptr.new(DocsEverywhere.docs_everywhere_cancel_token_create) + end + + # Requests cancellation. Idempotent. + def cancel + DocsEverywhere.docs_everywhere_cancel_token_cancel(@ptr) unless @ptr.nil? + nil + end + + # Whether #cancel has been called. + def cancelled? + !@ptr.nil? && DocsEverywhere.docs_everywhere_cancel_token_is_cancelled(@ptr) + end + + # Releases the native token now rather than at GC time. Calls already + # launched with it keep their own reference. + def close + return if @ptr.nil? + + @ptr.free + @ptr = nil + end + + # @api private + def _wv_ptr + raise Error.new(GENERIC_ERROR_CODE, 'CancelToken used after close') if @ptr.nil? + + @ptr + end + end + + # @api private + # Appends values in the value-buffer wire format: little-endian, packed, + # no alignment. + class WvBufferWriter + def initialize + @buf = +''.b + @objects = [] + end + + # Reserves an object token. The strong reference it carries is minted + # by #seal, just before the buffer is handed to the producer, so an + # encoding abandoned on the way never strands a reference. + def write_object(object) + raise TypeError, "expected a wrapper object, got #{object.class}" unless object.is_a?(WvObject) + + @objects << [@buf.bytesize, object] + write_u64(0) + end + + # Mints the reference each reserved object token carries, recording it + # in `minted`, and returns the finished encoding. + def seal(minted) + @objects.each do |offset, object| + ptr = object._wv_clone_ptr + minted << [object.class, ptr] + @buf[offset, 8] = [ptr.address].pack('Q<') + end + @buf + end + + # Every append is a binary String, so the buffer stays binary and byte + # offsets index it directly. + def write_bool(value) + @buf << [value ? 1 : 0].pack('C') + end + + def write_flag(value) + write_bool(value) + end + + def write_i8(value) + @buf << [value].pack('c') + end + + def write_u8(value) + @buf << [value].pack('C') + end + + def write_i16(value) + @buf << [value].pack('s<') + end + + def write_u16(value) + @buf << [value].pack('S<') + end + + def write_i32(value) + @buf << [value].pack('l<') + end + + def write_u32(value) + @buf << [value].pack('L<') + end + + def write_len(value) + write_u32(value) + end + + def write_i64(value) + @buf << [value].pack('q<') + end + + def write_u64(value) + @buf << [value].pack('Q<') + end + + def write_f32(value) + @buf << [value].pack('e') + end + + def write_f64(value) + @buf << [value].pack('E') + end + + def write_string(value) + write_bytes(value.to_s.encode(Encoding::UTF_8)) + end + + def write_bytes(value) + bytes = value.to_s.b + write_u32(bytes.bytesize) + @buf << bytes + end + end + + # @api private + # Reads values in the 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(count, what) + if @pos + count > @data.bytesize + raise Error.new(MARSHAL_ERROR_CODE, "malformed value buffer: truncated #{what}") + end + + bytes = @data.byteslice(@pos, count) + @pos += count + bytes + end + + def read_bool + byte = take(1, 'bool').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: bool byte out of range') if byte > 1 + + byte == 1 + end + + def read_flag + byte = take(1, 'option flag').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: option flag out of range') if byte > 1 + + byte == 1 + end + + def read_i8 + take(1, 'i8').unpack1('c') + end + + def read_u8 + take(1, 'u8').unpack1('C') + end + + def read_i16 + take(2, 'i16').unpack1('s<') + end + + def read_u16 + take(2, 'u16').unpack1('S<') + end + + def read_i32 + take(4, 'i32').unpack1('l<') + end + + def read_u32 + take(4, 'u32').unpack1('L<') + end + + # A u32 element count. Counts are not checked against the remaining + # bytes, since an element can encode to zero bytes; a short buffer still + # fails on the first truncated read. + def read_len + read_u32 + end + + def read_i64 + take(8, 'i64').unpack1('q<') + end + + def read_u64 + take(8, 'u64').unpack1('Q<') + end + + def read_f32 + take(4, 'f32').unpack1('e') + end + + def read_f64 + take(8, 'f64').unpack1('E') + end + + def read_string + text = read_bytes.force_encoding(Encoding::UTF_8) + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: string is not valid UTF-8') unless text.valid_encoding? + + text + end + + def read_bytes + take(read_u32, 'byte run') + end + + # An object token: a non-zero u64 carrying one strong reference the + # caller adopts into a wrapper. + def read_object_token + addr = read_u64 + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: null object token') if addr.zero? + + FFI::Pointer.new(addr) + end + + def expect_end! + return if @pos == @data.bytesize + + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: trailing bytes after value') + end + end + + # @api private + # Seals the writers of one call's object-carrying buffers (see + # WvBufferWriter#seal) and returns their encodings. If any reference can't + # be minted (a closed wrapper), the ones already minted are released before + # the error propagates. The producer adopts every minted reference once the + # call is made. + def self._wv_seal(*writers) + minted = [] + writers.map { |writer| writer.seal(minted) } + rescue Exception # rubocop:disable Lint/RescueException + minted.each { |cls, ptr| cls::WV_PTR.release(ptr) } + raise + end + + # @api private + # Decodes a returned value buffer: copies and releases it, yields a reader + # to the block, and checks that the block consumed every byte. + def self._wv_decode(ptr, len) + reader = WvBufferReader.new(_wv_take_bytes(ptr, len)) + value = yield reader + reader.expect_end! + value + end + + # Ruby objects the producer refers to by an integer key: callback-interface + # implementations (the key is the vtable `ctx`) and pending async + # completions (the key is the completion `context`). The producer never + # sees a Ruby object address. A Monitor rather than a Mutex, so a + # finalizer that runs while this thread holds the lock can't deadlock it. + @wv_handles = {} + @wv_next_handle = 0 + @wv_handles_lock = Monitor.new + + # @api private + def self._wv_handle_put(value) + @wv_handles_lock.synchronize do + @wv_next_handle += 1 + @wv_handles[@wv_next_handle] = value + FFI::Pointer.new(@wv_next_handle) + end + end + + # @api private + def self._wv_handle_get(key) + @wv_handles_lock.synchronize { @wv_handles[key.address] } + end + + # @api private + def self._wv_handle_take(key) + @wv_handles_lock.synchronize { @wv_handles.delete(key.address) } + end + + # @api private + # Registers a callback-interface implementation and returns its `ctx`. + # Never raises, so nothing can strand the registration between this and + # the call that hands it to the producer (an unusable implementation fails + # when the producer calls it, with FOREIGN_ERROR_CODE). + def self._wv_cb_register(impl) + _wv_handle_put([:callback, impl]) + end + + # @api private + # Resolves a trampoline's `ctx` back to the registered implementation. + def self._wv_cb_lookup(ctx) + kind, impl = _wv_handle_get(ctx) + raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') unless kind == :callback + + impl + end + + # @api private + # Drops an implementation when the producer releases its last reference. + def self._wv_cb_free(ctx) + _wv_handle_take(ctx) + nil + end + + # @api private + # Reports a callback implementation's exception through `out_err`. The + # producer copies the message. + def self._wv_cb_fail(out_err, exception) + message = exception.message.to_s + message = exception.class.name if message.empty? + message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).delete("\0") + docs_everywhere_error_set(out_err, FOREIGN_ERROR_CODE, message) + nil + end + + # @api private + # Registers a pending async call; returns its completion context and the + # queue its completion is delivered on. + def self._wv_async_begin + queue = Queue.new + [_wv_handle_put([:async, queue]), queue] + end + + # @api private + # The queue of the pending call a completion belongs to. Each context + # completes exactly once, so its entry is removed here. + def self._wv_async_finish(ctx) + kind, queue = _wv_handle_take(ctx) + kind == :async ? queue : Queue.new + end + + # @api private + # Blocks until a pending call completes, raising its error. If the wait is + # interrupted (Thread#raise, Thread#kill, Timeout), the call's token is + # cancelled before the interruption propagates. + def self._wv_async_wait(queue, token = nil) + value = + begin + queue.pop + rescue Exception # rubocop:disable Lint/RescueException + token&.cancel + raise + end + raise value if value.is_a?(Exception) + + value + end + + # @api private + # The base of every interface wrapper. A wrapper owns one strong reference + # to its native object and releases it exactly once, from #close or (as a + # backstop) from the GC finalizer of its FFI::AutoPointer. Calls borrow the + # pointer through #_wv_call, so a #close that races an in-flight call on + # another thread only releases the reference once the call returns. + class WvObject + # @api private + # Adopts one strong reference the producer handed over. + def self._from_ptr(ptr) + obj = allocate + obj._wv_init(ptr) + obj + end + + # @api private + def _wv_init(ptr) + @wv_handle = self.class::WV_PTR.new(ptr) + @wv_lock = Mutex.new + @wv_calls = 0 + @wv_closing = false + end + + # The native object pointer (borrowed; valid until #close). + def handle + handle = @wv_handle + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if handle.nil? || @wv_closing + + handle + end + + # Whether #close has released this wrapper's reference. + def closed? + @wv_handle.nil? || @wv_closing + end + + # Releases this wrapper's reference now rather than at GC time. + # Idempotent. The object itself lives until its last reference anywhere + # (another wrapper, a record field, the producer) goes. + def close + handle = @wv_lock.synchronize do + next nil if @wv_handle.nil? || @wv_closing + + if @wv_calls.zero? + taken = @wv_handle + @wv_handle = nil + taken + else + @wv_closing = true + nil + end + end + handle&.free + nil + end + + # @api private + # Yields the native pointer, keeping the reference alive until the block + # returns. + def _wv_call + handle = @wv_lock.synchronize do + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if @wv_handle.nil? || @wv_closing + + @wv_calls += 1 + @wv_handle + end + begin + yield handle + ensure + _wv_leave + end + end + + # @api private + # Mints a new strong reference the caller owns (an object written into a + # value buffer carries its own reference). + def _wv_clone_ptr + _wv_call { |ptr| self.class._wv_clone(ptr) } + end + + # dup and clone produce an independent wrapper with its own reference to + # the same object. + def initialize_copy(other) + super + _wv_init(other._wv_clone_ptr) + end + + private + + def _wv_leave + release = @wv_lock.synchronize do + @wv_calls -= 1 + next nil unless @wv_closing && @wv_calls.zero? + + taken = @wv_handle + @wv_handle = nil + @wv_closing = false + taken + end + release&.free + end + end + + # @api private + # Yields the native pointers of `objects` (NULL for nil), each kept alive + # until the block returns. + def self._wv_pin(*objects, &block) + _wv_pin_from(objects, 0, [], &block) + end + + # @api private + def self._wv_pin_from(objects, index, pointers, &block) + return yield(*pointers) if index == objects.length + + object = objects[index] + if object.nil? + _wv_pin_from(objects, index + 1, pointers + [FFI::Pointer::NULL], &block) + else + object._wv_call { |ptr| _wv_pin_from(objects, index + 1, pointers + [ptr], &block) } + end + end + + # @api private + # Checks each top-level module's contract checksum (`module name => + # [checksum function, expected value]`) against the library, so bindings + # generated from a different API fail at require time. + def self._wv_check_contract!(checksums) + checksums.each do |mod, (symbol, expected)| + begin + attach_function symbol, [], :uint64 + rescue FFI::NotFoundError + raise LoadError, "the loaded docs_everywhere library has no module '#{mod}' (#{symbol} is missing)" + end + actual = public_send(symbol) + next if actual == expected + + raise LoadError, "docs_everywhere module '#{mod}' does not match these bindings: expected contract " \ + "checksum 0x#{expected.to_s(16)}, the library reports 0x#{actual.to_s(16)}; " \ + 'regenerate the bindings' + end + end + + # @api private + # Live native resources of one kind (0 objects, 1 callbacks, 2 iterators, + # 3 cancel tokens, 4 byte allocations). Only libraries built with the + # `leak-check` feature export the counter. + def self._wv_debug_live(kind) + unless respond_to?(:docs_everywhere_debug_live) + begin + attach_function :docs_everywhere_debug_live, [:int32], :uint64 + rescue FFI::NotFoundError + raise Error.new(GENERIC_ERROR_CODE, 'docs_everywhere was built without the leak-check feature') + end + end + docs_everywhere_debug_live(kind) + end +end + +# END runtime.rb 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 deleted file mode 100644 index 9ad65da5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap +++ /dev/null @@ -1,696 +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 - - # 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 = 2 - 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 - - # The runtime-reserved error codes. Positive codes belong to a module's - # declared error domain; these negative codes are programming errors the - # wrappers raise as a plain Error regardless of the function's `throws`. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - - 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 - - # An object token: a non-zero u64 carrying one strong reference the - # caller adopts into a wrapper. - def read_object_token - addr = read_u64 - raise Error.new(-1, 'malformed value buffer: null object token') if addr.zero? - FFI::Pointer.new(addr) - 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_set, [:pointer, :int32, :string], :void - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # Live callback-interface implementations keyed by the Integer the producer - # holds as `ctx`. Keys start at 1 so a context pointer is never NULL. - @wv_cb_registry = {} - @wv_cb_next_key = 0 - @wv_cb_mutex = Mutex.new - - # @api private - # Registers `impl` and returns the `ctx` pointer to hand the producer. - def self._wv_cb_register(impl) - @wv_cb_mutex.synchronize do - @wv_cb_next_key += 1 - @wv_cb_registry[@wv_cb_next_key] = impl - FFI::Pointer.new(@wv_cb_next_key) - end - end - - # @api private - # Resolves a trampoline's `ctx` back to the registered implementation. - def self._wv_cb_lookup(ctx) - impl = @wv_cb_mutex.synchronize { @wv_cb_registry[ctx.address] } - raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') if impl.nil? - impl - end - - # @api private - # Drops the registry entry when the producer releases its last reference. - def self._wv_cb_free(ctx) - @wv_cb_mutex.synchronize { @wv_cb_registry.delete(ctx.address) } - nil - end - - # @api private - # Reports a callback implementation's exception through `out_err`. The - # producer copies the message, so the Ruby String need not outlive the call. - def self._wv_cb_fail(out_err, exception) - message = exception.message.to_s - message = exception.class.name if message.empty? - weaveffi_error_set(out_err, FOREIGN_ERROR_CODE, message) - nil - end - - # === Module: docs === - - # Base error for the `docs` module's error domain. - class DocsErrorsError < Error - # Returned when no document matches the requested id - class NotFound < DocsErrorsError - CODE = 1 - - def initialize(message = 'Document not found') - super(1, message) - end - end - - # Raised when attempting to move into a forbidden state - class InvalidStatus < DocsErrorsError - CODE = 2 - - def initialize(message = 'Invalid status transition') - super(2, message) - end - end - end - - # Maps each ABI code of the DocsErrorsError domain to its error class. - DOCS_ERRORS_ERROR_CODES = { - 1 => DocsErrorsError::NotFound, - 2 => DocsErrorsError::InvalidStatus, - }.freeze - - # Builds the DocsErrorsError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling, callback failures). - def self.docs_errors_error_from(code, message, payload = nil) - cls = DOCS_ERRORS_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed DocsErrorsError for a non-zero error slot. - def self.check_docs_errors_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise docs_errors_error_from(code, msg, payload) - end - - # Lifecycle status of a document. - # - # Used to drive UI badges and filters. - module Status - # Document is being edited - DRAFT = 0 - # Document is publicly visible - PUBLISHED = 1 - # Document is hidden from listings - ARCHIVED = 2 - end - - # Represents a single document tracked by the system. - # - # Documents are persisted to disk and exposed via the public API. - class Document - # Stable opaque identifier - attr_reader :id - - # Human-readable title shown in lists - attr_reader :title - - # Current lifecycle status - attr_reader :status - - # Free-form tags for grouping - attr_reader :tags - - def initialize(id:, title:, status:, tags:) - @id = id - @title = title - @status = status - @tags = tags - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Document) - return false unless id == other.id - return false unless title == other.title - return false unless status == other.status - return false unless tags == other.tags - true - end - end - - # @api private - # Packs a Document into the value-buffer wire format. - def self._wv_write_document(w, v) - w.write_i64(v.id) - w.write_string(v.title) - w.write_i32(v.status) - w.write_len(v.tags.length) - v.tags.each do |_wv_e0| - w.write_string(_wv_e0) - end - end - - # @api private - # Unpacks a Document from the value-buffer wire format. - def self._wv_read_document(r) - _wv_id = r.read_i64 - _wv_title = r.read_string - _wv_status = r.read_i32 - _wv_tags = Array.new(r.read_len) do - _wv_e0 = r.read_string - _wv_e0 - end - Document.new(id: _wv_id, title: _wv_title, status: _wv_status, tags: _wv_tags) - end - - attach_function :weaveffi_docs_Notebook_clone, [:pointer], :pointer - attach_function :weaveffi_docs_Notebook_destroy, [:pointer], :void - # Create an empty notebook - attach_function :weaveffi_docs_Notebook_new, [:pointer], :pointer - # Add a document, returning its position in the notebook - attach_function :weaveffi_docs_Notebook_insert, [:pointer, :pointer, :size_t, :pointer], :int32 - # Look up a document by identifier, failing when none matches - attach_function :weaveffi_docs_Notebook_find, [:pointer, :int64, :pointer, :pointer], :pointer - # The number of documents currently held - attach_function :weaveffi_docs_Notebook_size, [:pointer, :pointer], :int32 - # The largest number of documents one notebook will hold - attach_function :weaveffi_docs_Notebook_max_documents, [:pointer], :int32 - # Create a brand new document. - # - # The document starts in `Draft` status; callers must publish - # it explicitly to make it visible. - attach_function :weaveffi_docs_create_document, [:string, :pointer, :size_t, :pointer, :pointer], :pointer - # Look up a document by identifier - attach_function :weaveffi_docs_get_document, [:int64, :pointer, :pointer], :pointer - # Publish a document in the background - callback :weaveffi_docs_publish_async_callback, [:pointer, :pointer, :int32], :void - attach_function :weaveffi_docs_publish_async_async, [:int64, :weaveffi_docs_publish_async_callback, :pointer], :void - # Register a listener for saved notifications - attach_function :weaveffi_docs_watch, [:pointer, :pointer, :pointer], :void - - # Consumer-implemented observer of document-saved notifications. - # - # Implementations must be safe to call from any thread. - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a SavedListener parameter is expected; - # include this module to inherit NotImplementedError defaults. The - # producer may call the methods from any thread until it releases the - # implementation. - module SavedListener - # Fires after a document has been persisted successfully - # @param doc_id [Object] Identifier of the saved document - # @param revision [Object] Monotonically increasing revision - def on_saved(doc_id, revision) - raise NotImplementedError, "#{self.class}#on_saved is not implemented" - end - end - - # @api private - # The C vtable layout the producer calls a SavedListener through. - class WvSavedListenerVtable < FFI::Struct - layout :on_saved, :pointer, - :free, :pointer - end - - # @api private - # Trampoline for SavedListener#on_saved. - WV_SAVED_LISTENER_ON_SAVED = FFI::Function.new(:void, [:pointer, :int64, :int32, :pointer]) do |ctx, doc_id, revision, out_err| - begin - impl = _wv_cb_lookup(ctx) - doc_id_v = doc_id - revision_v = revision - impl.on_saved(doc_id_v, revision_v) - nil - rescue Exception => e - _wv_cb_fail(out_err, e) - nil - end - end - - # @api private - # Releases a SavedListener implementation when the producer drops its last - # reference. - WV_SAVED_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end - - # The one process-wide SavedListener vtable every registration hands the - # producer; its entries live for the process lifetime. - WV_SAVED_LISTENER_VTABLE = WvSavedListenerVtable.new - WV_SAVED_LISTENER_VTABLE[:on_saved] = WV_SAVED_LISTENER_ON_SAVED - WV_SAVED_LISTENER_VTABLE[:free] = WV_SAVED_LISTENER_FREE - - # @api private - # Owns one strong reference to a Notebook; releases it exactly once. - class NotebookPtr < FFI::AutoPointer - def self.release(ptr) - WeaveFFI.weaveffi_docs_Notebook_destroy(ptr) - end - end - - # An ordered collection of documents. - # - # Notebooks own their documents and release them when destroyed. - class Notebook - # @api private - # Adopts one strong reference the producer handed over, without - # re-running initialize. - def self._from_ptr(ptr) - obj = allocate - obj.instance_variable_set(:@handle, NotebookPtr.new(ptr)) - obj - end - - # The borrowed object pointer passed to producer calls. - def handle - raise Error.new(-1, 'Notebook used after close') if @handle.nil? - @handle - end - - # Whether close has released this wrapper's reference. - def closed? - @handle.nil? - end - - # Releases this wrapper's reference now rather than at GC time. - # Idempotent; the object itself is dropped when the last reference - # anywhere (another wrapper, a record field, the producer) goes. - def close - return if @handle.nil? - @handle.free - @handle = nil - end - - # @api private - # Mints a new strong reference the caller owns (used when this - # object is written into a value buffer). - def _wv_clone_ptr - WeaveFFI.weaveffi_docs_Notebook_clone(handle) - end - - # dup and clone produce an independent wrapper with its own reference. - def initialize_copy(other) - super - @handle = NotebookPtr.new(WeaveFFI.weaveffi_docs_Notebook_clone(other.handle)) - end - - # Create an empty notebook - def initialize() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_docs_Notebook_new(err) - WeaveFFI.check_error!(err) - raise Error.new(-1, 'null pointer') if result.null? - @handle = NotebookPtr.new(result) - end - - # Add a document, returning its position in the notebook - # @param document [Object] The document to add - def insert(document) - err = ErrorStruct.new - document_w = WvBufferWriter.new - WeaveFFI._wv_write_document(document_w, document) - document_data = document_w.data - document_buf = FFI::MemoryPointer.new(:uint8, document_data.bytesize) - document_buf.put_bytes(0, document_data) - result = WeaveFFI.weaveffi_docs_Notebook_insert(handle, document_buf, document_data.bytesize, err) - WeaveFFI.check_error!(err) - result - end - - # Look up a document by identifier, failing when none matches - # @param id [Object] Identifier of the document to find - def find(id) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = WeaveFFI.weaveffi_docs_Notebook_find(handle, id, out_len, err) - WeaveFFI.check_docs_errors_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_document(_wv_r) - _wv_r.expect_end! - _wv_value - end - - # The number of documents currently held - def size() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_docs_Notebook_size(handle, err) - WeaveFFI.check_error!(err) - result - end - - # The largest number of documents one notebook will hold - def self.max_documents() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_docs_Notebook_max_documents(err) - WeaveFFI.check_error!(err) - result - end - end - - # Create a brand new document. - # - # The document starts in `Draft` status; callers must publish - # it explicitly to make it visible. - # @param title [Object] Human-readable title for the new document - # @param tags [Object] Initial set of tags (may be empty) - def self.create_document(title, tags) - err = ErrorStruct.new - tags_w = WvBufferWriter.new - tags_w.write_len(tags.length) - tags.each do |_wv_e0| - tags_w.write_string(_wv_e0) - end - tags_data = tags_w.data - tags_buf = FFI::MemoryPointer.new(:uint8, tags_data.bytesize) - tags_buf.put_bytes(0, tags_data) - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_docs_create_document(title, tags_buf, tags_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_document(_wv_r) - _wv_r.expect_end! - _wv_value - end - - # Look up a document by identifier - # @param id [Object] Identifier returned from create_document - def self.get_document(id) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_docs_get_document(id, 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_read_document(_wv_r) - else - _wv_value = nil - end - _wv_r.expect_end! - _wv_value - end - - # Publish a document in the background - # Blocks the current thread until the async producer completes; the - # result (or error) is delivered through the completion callback. - def self.publish_async(id) - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_free(err_ptr) - queue << Error.new(code, msg) - else - queue << (result != 0) - end - end - weaveffi_docs_publish_async_async(id, callback, FFI::Pointer::NULL) - value = queue.pop - raise value if value.is_a?(Error) - value - end - - # Register a listener for saved notifications - # @param listener [Object] Receiver of saved notifications - def self.watch(listener) - err = ErrorStruct.new - listener_ctx = _wv_cb_register(listener) - weaveffi_docs_watch(listener_ctx, WV_SAVED_LISTENER_VTABLE.to_ptr, err) - check_error!(err) - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__README_md.snap index dfa27948..4f9ecfaa 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__README_md.snap @@ -1,30 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Ruby) +# edge_cases (Ruby) -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. +Ruby bindings for `edge_cases` 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. +- Ruby 2.7 or newer +- The native library (`libedge_cases.so`, `libedge_cases.dylib`, or `edge_cases.dll`) on the library + search path, or its path in the `EDGE_CASES_LIBRARY` environment variable. ## Install ```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem +gem build edge_cases.gemspec +gem install edge_cases-0.1.0.gem ``` ## Usage ```ruby -require 'weaveffi' +require 'edge_cases' + +EdgeCases::ABI_VERSION ``` diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__edge_cases_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__edge_cases_gemspec.snap new file mode 100644 index 00000000..99d3cbc1 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__edge_cases_gemspec.snap @@ -0,0 +1,20 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +Gem::Specification.new do |s| + s.name = 'edge_cases' + s.version = '0.1.0' + s.summary = 'Native bindings for edge_cases' + s.authors = ['edge_cases'] + s.files = Dir['lib/**/*.rb'] + s.require_paths = ['lib'] + s.required_ruby_version = '>= 2.7' + + s.add_dependency 'ffi', '~> 1.15' +end + +# END edge_cases.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_rb.snap new file mode 100644 index 00000000..18d0f8fa --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_rb.snap @@ -0,0 +1,1733 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require_relative 'edge_cases/runtime' + +# Ruby bindings for edge_cases. +module EdgeCases + # Contract checksums of the top-level modules these bindings were + # generated from; a library built from a different API fails to load. + _wv_check_contract!( + 'edge_cases' => [:edge_cases_edge_cases_checksum, 0x1567f441073bd4aa], + ) + + # === 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, callback failures). + def self.edge_errors_error_from(code, message, payload = nil) + cls = EDGE_ERRORS_ERROR_CODES[code] + return _wv_error(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) + taken = _wv_take_error(err) + raise edge_errors_error_from(*taken) if taken + 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 + + # Record carrying objects in every buffered position + class WidgetHolder + attr_reader :primary + + attr_reader :fallback + + attr_reader :extras + + attr_reader :by_name + + def initialize(primary:, fallback:, extras:, by_name:) + @primary = primary + @fallback = fallback + @extras = extras + @by_name = by_name + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(WidgetHolder) + return false unless primary == other.primary + return false unless fallback == other.fallback + return false unless extras == other.extras + return false unless by_name == other.by_name + 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 + + # @api private + # Packs a WidgetHolder into the value-buffer wire format. + def self._wv_write_widget_holder(w, v) + w.write_object(v.primary) + if v.fallback.nil? + w.write_flag(false) + else + w.write_flag(true) + w.write_object(v.fallback) + end + w.write_len(v.extras.length) + v.extras.each do |_wv_e0| + w.write_object(_wv_e0) + end + w.write_len(v.by_name.length) + v.by_name.each do |_wv_k0, _wv_v0| + w.write_string(_wv_k0) + w.write_object(_wv_v0) + end + end + + # @api private + # Unpacks a WidgetHolder from the value-buffer wire format. + def self._wv_read_widget_holder(r) + _wv_primary = Widget._from_ptr(r.read_object_token) + if r.read_flag + _wv_fallback = Widget._from_ptr(r.read_object_token) + else + _wv_fallback = nil + end + _wv_extras = Array.new(r.read_len) do + _wv_e0 = Widget._from_ptr(r.read_object_token) + _wv_e0 + end + _wv_by_name = {} + r.read_len.times do + _wv_k0 = r.read_string + _wv_v0 = Widget._from_ptr(r.read_object_token) + _wv_by_name[_wv_k0] = _wv_v0 + end + WidgetHolder.new(primary: _wv_primary, fallback: _wv_fallback, extras: _wv_extras, by_name: _wv_by_name) + end + + attach_function :edge_cases_edge_cases_Widget_clone, [:pointer], :pointer + attach_function :edge_cases_edge_cases_Widget_destroy, [:pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_Widget_refresh, [:pointer, :pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_Widget_rename, [:pointer, :pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_Widget_fetch, [:pointer, :int64, :pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for fetch. + EDGE_CASES_EDGE_CASES_WIDGET_FETCH_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << edge_errors_error_from(*taken) + else + value = _wv_decode(result_ptr, result_len) do |_wv_r| + _wv_value = _wv_read_outer(_wv_r) + _wv_value + end + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_Widget_old_name, [:pointer, :pointer, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_Vault_clone, [:pointer], :pointer + attach_function :edge_cases_edge_cases_Vault_destroy, [:pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_Vault_open, [:pointer, :size_t, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_Vault_peek, [:pointer, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_Vault_probe, [:pointer], :bool, blocking: true + attach_function :edge_cases_edge_cases_noop, [:pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_select, [:int32, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :bool, :pointer], :int32, blocking: true + attach_function :edge_cases_edge_cases_nested_optional, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_optional_params, [:pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_register_batch, [:pointer, :pointer, :pointer, :pointer, :pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_widgets_everywhere, [:pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_widget_stream, [:pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_WidgetStreamIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true + attach_function :edge_cases_edge_cases_WidgetStreamIterator_destroy, [:pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_async_widget, [:pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for async_widget. + EDGE_CASES_EDGE_CASES_ASYNC_WIDGET_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer]) do |ctx, err, result| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << edge_errors_error_from(*taken) + else + raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if result.null? + value = Widget._from_ptr(result) + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_make_widget, [:pointer, :size_t, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_find_widget, [:int64, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_inspect_widget, [:pointer, :pointer, :pointer, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_string_stream, [:pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_StringStreamIterator_next, [:pointer, :pointer, :pointer, :pointer], :int32, blocking: true + attach_function :edge_cases_edge_cases_StringStreamIterator_destroy, [:pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_scalar_stream, [:pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_ScalarStreamIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true + attach_function :edge_cases_edge_cases_ScalarStreamIterator_destroy, [:pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_mode_stream, [:pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_ModeStreamIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true + attach_function :edge_cases_edge_cases_ModeStreamIterator_destroy, [:pointer], :void, blocking: true + attach_function :edge_cases_edge_cases_async_void, [:pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for async_void. + EDGE_CASES_EDGE_CASES_ASYNC_VOID_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer]) do |ctx, err| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << _wv_error(taken[0], taken[1]) + else + value = nil + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_async_bytes, [:uint32, :pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for async_bytes. + EDGE_CASES_EDGE_CASES_ASYNC_BYTES_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << _wv_error(taken[0], taken[1]) + else + value = _wv_take_bytes(result_ptr, result_len) + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_async_struct, [:pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for async_struct. + EDGE_CASES_EDGE_CASES_ASYNC_STRUCT_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << edge_errors_error_from(*taken) + else + value = _wv_decode(result_ptr, result_len) do |_wv_r| + _wv_value = _wv_read_outer(_wv_r) + _wv_value + end + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_async_list, [:pointer, :pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for async_list. + EDGE_CASES_EDGE_CASES_ASYNC_LIST_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << edge_errors_error_from(*taken) + else + value = _wv_decode(result_ptr, result_len) do |_wv_r| + _wv_value = Array.new(_wv_r.read_len) do + _wv_e0 = _wv_read_outer(_wv_r) + _wv_e0 + end + _wv_value + end + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_async_optional, [:pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for async_optional. + EDGE_CASES_EDGE_CASES_ASYNC_OPTIONAL_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << _wv_error(taken[0], taken[1]) + else + value = _wv_decode(result_ptr, result_len) do |_wv_r| + if _wv_r.read_flag + _wv_value = _wv_r.read_i64 + else + _wv_value = nil + end + _wv_value + end + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_async_enum, [:pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for async_enum. + EDGE_CASES_EDGE_CASES_ASYNC_ENUM_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :int32]) do |ctx, err, result| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << _wv_error(taken[0], taken[1]) + else + value = result + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_async_keywords, [:pointer, :size_t, :int32, :pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for async_keywords. + EDGE_CASES_EDGE_CASES_ASYNC_KEYWORDS_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << _wv_error(taken[0], taken[1]) + else + value = _wv_take_string(result_ptr, result_len) + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :edge_cases_edge_cases_keywords_roundtrip, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :edge_cases_edge_cases_legacy_value, [:int32, :pointer, :pointer], :pointer, blocking: true + + # Callback interface carrying composite payloads and objects + # + # Consumer-implemented callback interface. Any object responding to the + # methods below is accepted wherever a BatchListener is expected; include + # this module to inherit NotImplementedError defaults. The producer + # may call the methods from any thread until it releases the + # implementation. + module BatchListener + # Composite payloads cross as value buffers + def on_batch(items, raw, first, mode) + raise NotImplementedError, "#{self.class}#on_batch is not implemented" + end + + # Objects cross as strong references the consumer must release + def on_widget(widget, fallback, children) + raise NotImplementedError, "#{self.class}#on_widget is not implemented" + end + + def on_tick + raise NotImplementedError, "#{self.class}#on_tick is not implemented" + end + + # Scalar return travels back through the vtable + # @return [Object] a bool + def should_continue(class_) + raise NotImplementedError, "#{self.class}#should_continue is not implemented" + end + + # Enum return travels back as its integer discriminant + # @return [Object] a edge_cases.Mode + def pick_mode + raise NotImplementedError, "#{self.class}#pick_mode is not implemented" + end + end + + # @api private + # The C vtable layout the producer calls a BatchListener through. + class WvBatchListenerVtable < FFI::Struct + layout :on_batch, :pointer, + :on_widget, :pointer, + :on_tick, :pointer, + :should_continue, :pointer, + :pick_mode, :pointer, + :free, :pointer + end + + # @api private + # Trampoline for BatchListener#on_batch. + WV_BATCH_LISTENER_ON_BATCH = FFI::Function.new(:void, [:pointer, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :int32, :pointer]) do |ctx, items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, out_err| + begin + items_r = WvBufferReader.new(_wv_borrow_bytes(items_ptr, 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! + raw_v = _wv_borrow_bytes(raw_ptr, raw_len) + first_r = WvBufferReader.new(_wv_borrow_bytes(first_ptr, first_len)) + if first_r.read_flag + first_v = _wv_read_outer(first_r) + else + first_v = nil + end + first_r.expect_end! + mode_v = mode + impl = _wv_cb_lookup(ctx) + impl.on_batch(items_v, raw_v, first_v, mode_v) + nil + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + nil + end + end + + # @api private + # Trampoline for BatchListener#on_widget. + WV_BATCH_LISTENER_ON_WIDGET = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :pointer, :size_t, :pointer]) do |ctx, widget, fallback, children_ptr, children_len, out_err| + begin + widget_v = Widget._from_ptr(widget) + fallback_v = fallback.null? ? nil : Widget._from_ptr(fallback) + children_r = WvBufferReader.new(_wv_borrow_bytes(children_ptr, children_len)) + children_v = Array.new(children_r.read_len) do + _wv_e0 = Widget._from_ptr(children_r.read_object_token) + _wv_e0 + end + children_r.expect_end! + impl = _wv_cb_lookup(ctx) + impl.on_widget(widget_v, fallback_v, children_v) + nil + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + nil + end + end + + # @api private + # Trampoline for BatchListener#on_tick. + WV_BATCH_LISTENER_ON_TICK = FFI::Function.new(:void, [:pointer, :pointer]) do |ctx, out_err| + begin + impl = _wv_cb_lookup(ctx) + impl.on_tick + nil + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + nil + end + end + + # @api private + # Trampoline for BatchListener#should_continue. + WV_BATCH_LISTENER_SHOULD_CONTINUE = FFI::Function.new(:bool, [:pointer, :int32, :pointer]) do |ctx, class_, out_err| + begin + class__v = class_ + impl = _wv_cb_lookup(ctx) + impl.should_continue(class__v) ? true : false + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + false + end + end + + # @api private + # Trampoline for BatchListener#pick_mode. + WV_BATCH_LISTENER_PICK_MODE = FFI::Function.new(:int32, [:pointer, :pointer]) do |ctx, out_err| + begin + impl = _wv_cb_lookup(ctx) + Integer(impl.pick_mode) + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + 0 + end + end + + # @api private + # Releases a BatchListener implementation when the producer drops its last + # reference. + WV_BATCH_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| + _wv_cb_free(ctx) + end + + # The one process-wide BatchListener vtable every registration hands the + # producer; its entries live for the process lifetime. + WV_BATCH_LISTENER_VTABLE = WvBatchListenerVtable.new + WV_BATCH_LISTENER_VTABLE[:on_batch] = WV_BATCH_LISTENER_ON_BATCH + WV_BATCH_LISTENER_VTABLE[:on_widget] = WV_BATCH_LISTENER_ON_WIDGET + WV_BATCH_LISTENER_VTABLE[:on_tick] = WV_BATCH_LISTENER_ON_TICK + WV_BATCH_LISTENER_VTABLE[:should_continue] = WV_BATCH_LISTENER_SHOULD_CONTINUE + WV_BATCH_LISTENER_VTABLE[:pick_mode] = WV_BATCH_LISTENER_PICK_MODE + WV_BATCH_LISTENER_VTABLE[:free] = WV_BATCH_LISTENER_FREE + + # Callback interface whose name and methods are keywords + # + # Consumer-implemented callback interface. Any object responding to the + # methods below is accepted wherever a Keyword is expected; include + # this module to inherit NotImplementedError defaults. The producer + # may call the methods from any thread until it releases the + # implementation. + module Keyword + def delete(self_) + raise NotImplementedError, "#{self.class}#delete is not implemented" + end + + # @return [Object] a i32 + def import + raise NotImplementedError, "#{self.class}#import is not implemented" + end + end + + # @api private + # The C vtable layout the producer calls a Keyword through. + class WvKeywordVtable < FFI::Struct + layout :delete, :pointer, + :import, :pointer, + :free, :pointer + end + + # @api private + # Trampoline for Keyword#delete. + WV_KEYWORD_DELETE = FFI::Function.new(:void, [:pointer, :int32, :pointer]) do |ctx, self_, out_err| + begin + self__v = self_ + impl = _wv_cb_lookup(ctx) + impl.delete(self__v) + nil + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + nil + end + end + + # @api private + # Trampoline for Keyword#import. + WV_KEYWORD_IMPORT = FFI::Function.new(:int32, [:pointer, :pointer]) do |ctx, out_err| + begin + impl = _wv_cb_lookup(ctx) + Integer(impl.import) + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + 0 + end + end + + # @api private + # Releases a Keyword implementation when the producer drops its last + # reference. + WV_KEYWORD_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| + _wv_cb_free(ctx) + end + + # The one process-wide Keyword vtable every registration hands the + # producer; its entries live for the process lifetime. + WV_KEYWORD_VTABLE = WvKeywordVtable.new + WV_KEYWORD_VTABLE[:delete] = WV_KEYWORD_DELETE + WV_KEYWORD_VTABLE[:import] = WV_KEYWORD_IMPORT + WV_KEYWORD_VTABLE[:free] = WV_KEYWORD_FREE + + # @api private + # Owns one strong reference to a Widget; releases it exactly once. + class WidgetPtr < FFI::AutoPointer + def self.release(ptr) + EdgeCases.edge_cases_edge_cases_Widget_destroy(ptr) + end + end + + # Interface with no constructor; instances come from make_widget + class Widget < WvObject + WV_PTR = WidgetPtr + private_class_method :new + + # @api private + def self._wv_clone(ptr) + EdgeCases.edge_cases_edge_cases_Widget_clone(ptr) + end + + # Void method that can fail + def refresh + EdgeCases._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + EdgeCases.edge_cases_edge_cases_Widget_refresh(_wv_self, err) + EdgeCases.check_edge_errors_error!(err) + end + end + + def rename(class_, import) + class__s = EdgeCases._wv_str(class_) + import_w = WvBufferWriter.new + if import.nil? + import_w.write_flag(false) + else + import_w.write_flag(true) + import_w.write_string(import) + end + EdgeCases._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + import_s = EdgeCases._wv_seal(import_w).first + result = EdgeCases.edge_cases_edge_cases_Widget_rename(_wv_self, class__s, class__s.bytesize, import_s, import_s.bytesize, out_len, err) + EdgeCases.check_error!(err) + EdgeCases._wv_take_string(result, out_len.read(:size_t)) + end + end + + # Async method with a struct result + # Blocks until the call completes on a producer thread. + def fetch(id) + queue = nil + EdgeCases._wv_pin(self) do |_wv_self| + ctx, queue = EdgeCases._wv_async_begin + EdgeCases.edge_cases_edge_cases_Widget_fetch(_wv_self, id, EDGE_CASES_EDGE_CASES_WIDGET_FETCH_CALLBACK, ctx) + end + EdgeCases._wv_async_wait(queue) + end + + # @deprecated Use rename instead + def old_name + warn '[DEPRECATED] Use rename instead' + EdgeCases._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = EdgeCases.edge_cases_edge_cases_Widget_old_name(_wv_self, out_len, err) + EdgeCases.check_error!(err) + EdgeCases._wv_take_string(result, out_len.read(:size_t)) + end + end + end + + # @api private + # Owns one strong reference to a Vault; releases it exactly once. + class VaultPtr < FFI::AutoPointer + def self.release(ptr) + EdgeCases.edge_cases_edge_cases_Vault_destroy(ptr) + end + end + + # Deprecated interface with a throwing constructor + # @deprecated Use Widget instead + class Vault < WvObject + WV_PTR = VaultPtr + private_class_method :new + + # @api private + def self._wv_clone(ptr) + EdgeCases.edge_cases_edge_cases_Vault_clone(ptr) + end + + # Fails when the vault is busy + def self.open(path) + path_s = EdgeCases._wv_str(path) + err = ErrorStruct.new + result = EdgeCases.edge_cases_edge_cases_Vault_open(path_s, path_s.bytesize, err) + EdgeCases.check_edge_errors_error!(err) + raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if result.null? + _from_ptr(result) + end + + def peek + EdgeCases._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + result = EdgeCases.edge_cases_edge_cases_Vault_peek(_wv_self, err) + EdgeCases.check_error!(err) + result.null? ? nil : Widget._from_ptr(result) + end + end + + # Throwing static + def self.probe + err = ErrorStruct.new + result = EdgeCases.edge_cases_edge_cases_Vault_probe(err) + EdgeCases.check_edge_errors_error!(err) + result + end + end + + # No parameters and no result + def self.noop + err = ErrorStruct.new + edge_cases_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) + from_s = _wv_str(from) + func_w = WvBufferWriter.new + func_w.write_len(func.length) + func.each do |_wv_e0| + func_w.write_string(_wv_e0) + end + range_w = WvBufferWriter.new + if range.nil? + range_w.write_flag(false) + else + range_w.write_flag(true) + range_w.write_i32(range) + end + err = ErrorStruct.new + func_s, range_s = _wv_seal(func_w, range_w) + result = edge_cases_edge_cases_select(class_, from_s, from_s.bytesize, func_s, func_s.bytesize, range_s, range_s.bytesize, (type ? true : false), err) + check_error!(err) + result + end + + # Optional list of optional structs, the deepest legal nesting + def self.nested_optional(items) + 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 + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + items_s = _wv_seal(items_w).first + result = edge_cases_edge_cases_nested_optional(items_s, items_s.bytesize, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + 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_value + end + end + + def self.optional_params(text, count, outer, tags, blob, mode) + text_w = WvBufferWriter.new + if text.nil? + text_w.write_flag(false) + else + text_w.write_flag(true) + text_w.write_string(text) + end + count_w = WvBufferWriter.new + if count.nil? + count_w.write_flag(false) + else + count_w.write_flag(true) + count_w.write_i32(count) + end + 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 + 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 + blob_w = WvBufferWriter.new + if blob.nil? + blob_w.write_flag(false) + else + blob_w.write_flag(true) + blob_w.write_bytes(blob) + end + mode_w = WvBufferWriter.new + if mode.nil? + mode_w.write_flag(false) + else + mode_w.write_flag(true) + mode_w.write_i32(mode) + end + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + text_s, count_s, outer_s, tags_s, blob_s, mode_s = _wv_seal(text_w, count_w, outer_w, tags_w, blob_w, mode_w) + result = edge_cases_edge_cases_optional_params(text_s, text_s.bytesize, count_s, count_s.bytesize, outer_s, outer_s.bytesize, tags_s, tags_s.bytesize, blob_s, blob_s.bytesize, mode_s, mode_s.bytesize, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + if _wv_r.read_flag + _wv_value = _wv_r.read_i32 + else + _wv_value = nil + end + _wv_value + end + end + + # Two callback interface parameters on one function + def self.register_batch(listener, keyword) + err = ErrorStruct.new + listener_ctx = _wv_cb_register(listener) + keyword_ctx = _wv_cb_register(keyword) + edge_cases_edge_cases_register_batch(listener_ctx, WV_BATCH_LISTENER_VTABLE.to_ptr, keyword_ctx, WV_KEYWORD_VTABLE.to_ptr, err) + check_error!(err) + end + + # Objects in every buffered position + def self.widgets_everywhere(list, by_name, sparse, holder) + list_w = WvBufferWriter.new + list_w.write_len(list.length) + list.each do |_wv_e0| + list_w.write_object(_wv_e0) + end + by_name_w = WvBufferWriter.new + by_name_w.write_len(by_name.length) + by_name.each do |_wv_k0, _wv_v0| + by_name_w.write_string(_wv_k0) + by_name_w.write_object(_wv_v0) + end + sparse_w = WvBufferWriter.new + sparse_w.write_len(sparse.length) + sparse.each do |_wv_e0| + if _wv_e0.nil? + sparse_w.write_flag(false) + else + sparse_w.write_flag(true) + sparse_w.write_object(_wv_e0) + end + end + holder_w = WvBufferWriter.new + _wv_write_widget_holder(holder_w, holder) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + list_s, by_name_s, sparse_s, holder_s = _wv_seal(list_w, by_name_w, sparse_w, holder_w) + result = edge_cases_edge_cases_widgets_everywhere(list_s, list_s.bytesize, by_name_s, by_name_s.bytesize, sparse_s, sparse_s.bytesize, holder_s, holder_s.bytesize, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_widget_holder(_wv_r) + _wv_value + end + end + + # Iterator of objects + # Returns a lazy Enumerator that pulls one element per step. The + # producer iterator starts on the first pull and is released when + # iteration finishes or is abandoned early. + def self.widget_stream + Enumerator.new do |y| + err = ErrorStruct.new + iter = edge_cases_edge_cases_widget_stream(err) + begin + check_error!(err) + out_item = FFI::MemoryPointer.new(:pointer) + loop do + has_item = edge_cases_edge_cases_WidgetStreamIterator_next(iter, out_item, err) + check_error!(err) + break if has_item.zero? + item = out_item.read_pointer + raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if item.null? + value = Widget._from_ptr(item) + y << value + end + ensure + edge_cases_edge_cases_WidgetStreamIterator_destroy(iter) unless iter.null? + end + end + end + + # Async returning an object + # Blocks until the call completes on a producer thread. + def self.async_widget + ctx, queue = _wv_async_begin + edge_cases_edge_cases_async_widget(EDGE_CASES_EDGE_CASES_ASYNC_WIDGET_CALLBACK, ctx) + _wv_async_wait(queue) + end + + # Factory for an interface with no constructor + def self.make_widget(label) + label_s = _wv_str(label) + err = ErrorStruct.new + result = edge_cases_edge_cases_make_widget(label_s, label_s.bytesize, err) + check_error!(err) + raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if result.null? + Widget._from_ptr(result) + end + + # Optional interface return + def self.find_widget(id) + err = ErrorStruct.new + result = edge_cases_edge_cases_find_widget(id, err) + check_edge_errors_error!(err) + result.null? ? nil : Widget._from_ptr(result) + end + + # Interface parameter, including an optional one + def self.inspect_widget(widget, fallback) + _wv_pin(widget, fallback) do |_wv_widget, _wv_fallback| + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = edge_cases_edge_cases_inspect_widget(_wv_widget, _wv_fallback, out_len, err) + check_error!(err) + _wv_take_string(result, out_len.read(:size_t)) + end + end + + # Iterator of strings + # Returns a lazy Enumerator that pulls one element per step. The + # producer iterator starts on the first pull and is released when + # iteration finishes or is abandoned early. + def self.string_stream + Enumerator.new do |y| + err = ErrorStruct.new + iter = edge_cases_edge_cases_string_stream(err) + begin + check_error!(err) + out_item = FFI::MemoryPointer.new(:pointer) + out_len = FFI::MemoryPointer.new(:size_t) + loop do + has_item = edge_cases_edge_cases_StringStreamIterator_next(iter, out_item, out_len, err) + check_error!(err) + break if has_item.zero? + item = out_item.read_pointer + value = _wv_take_string(item, out_len.read(:size_t)) + y << value + end + ensure + edge_cases_edge_cases_StringStreamIterator_destroy(iter) unless iter.null? + end + end + end + + # Iterator of scalars + # Returns a lazy Enumerator that pulls one element per step. The + # producer iterator starts on the first pull and is released when + # iteration finishes or is abandoned early. + def self.scalar_stream + Enumerator.new do |y| + err = ErrorStruct.new + iter = edge_cases_edge_cases_scalar_stream(err) + begin + check_error!(err) + out_item = FFI::MemoryPointer.new(:int32) + loop do + has_item = edge_cases_edge_cases_ScalarStreamIterator_next(iter, out_item, err) + check_error!(err) + break if has_item.zero? + item = out_item.read(:int32) + value = item + y << value + end + ensure + edge_cases_edge_cases_ScalarStreamIterator_destroy(iter) unless iter.null? + end + end + end + + # Iterator of enums + # Returns a lazy Enumerator that pulls one element per step. The + # producer iterator starts on the first pull and is released when + # iteration finishes or is abandoned early. + def self.mode_stream + Enumerator.new do |y| + err = ErrorStruct.new + iter = edge_cases_edge_cases_mode_stream(err) + begin + check_error!(err) + out_item = FFI::MemoryPointer.new(:int32) + loop do + has_item = edge_cases_edge_cases_ModeStreamIterator_next(iter, out_item, err) + check_error!(err) + break if has_item.zero? + item = out_item.read(:int32) + value = item + y << value + end + ensure + edge_cases_edge_cases_ModeStreamIterator_destroy(iter) unless iter.null? + end + end + end + + # Async with no result + # Blocks until the call completes on a producer thread. + def self.async_void + ctx, queue = _wv_async_begin + edge_cases_edge_cases_async_void(EDGE_CASES_EDGE_CASES_ASYNC_VOID_CALLBACK, ctx) + _wv_async_wait(queue) + end + + # Blocks until the call completes on a producer thread. + def self.async_bytes(size) + ctx, queue = _wv_async_begin + edge_cases_edge_cases_async_bytes(size, EDGE_CASES_EDGE_CASES_ASYNC_BYTES_CALLBACK, ctx) + _wv_async_wait(queue) + end + + # Blocks until the call completes on a producer thread. + def self.async_struct + ctx, queue = _wv_async_begin + edge_cases_edge_cases_async_struct(EDGE_CASES_EDGE_CASES_ASYNC_STRUCT_CALLBACK, ctx) + _wv_async_wait(queue) + end + + # Blocks until the call completes on a producer thread. + # @param cancel [CancelToken, nil] cancels the call; it then raises Cancelled + def self.async_list(cancel: nil) + own_token = CancelToken.new if cancel.nil? + token = cancel || own_token + ctx, queue = _wv_async_begin + edge_cases_edge_cases_async_list(token._wv_ptr, EDGE_CASES_EDGE_CASES_ASYNC_LIST_CALLBACK, ctx) + _wv_async_wait(queue, token) + ensure + own_token&.close + end + + # Blocks until the call completes on a producer thread. + def self.async_optional + ctx, queue = _wv_async_begin + edge_cases_edge_cases_async_optional(EDGE_CASES_EDGE_CASES_ASYNC_OPTIONAL_CALLBACK, ctx) + _wv_async_wait(queue) + end + + # Blocks until the call completes on a producer thread. + def self.async_enum + ctx, queue = _wv_async_begin + edge_cases_edge_cases_async_enum(EDGE_CASES_EDGE_CASES_ASYNC_ENUM_CALLBACK, ctx) + _wv_async_wait(queue) + end + + # Blocks until the call completes on a producer thread. + def self.async_keywords(class_, import) + class__s = _wv_str(class_) + ctx, queue = _wv_async_begin + edge_cases_edge_cases_async_keywords(class__s, class__s.bytesize, import, EDGE_CASES_EDGE_CASES_ASYNC_KEYWORDS_CALLBACK, ctx) + _wv_async_wait(queue) + end + + def self.keywords_roundtrip(value) + value_w = WvBufferWriter.new + _wv_write_keywords(value_w, value) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + value_s = _wv_seal(value_w).first + result = edge_cases_edge_cases_keywords_roundtrip(value_s, value_s.bytesize, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_keywords(_wv_r) + _wv_value + end + end + + # @deprecated Use Mode-based APIs + def self.legacy_value(value) + warn '[DEPRECATED] Use Mode-based APIs' + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = edge_cases_edge_cases_legacy_value(value, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_retired(_wv_r) + _wv_value + end + end +end + +# END edge_cases.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_runtime_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_runtime_rb.snap new file mode 100644 index 00000000..2ea694c4 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_edge_cases_runtime_rb.snap @@ -0,0 +1,705 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require 'ffi' +require 'monitor' + +# The fixed runtime every generated EdgeCases binding builds on: the native +# library loader, the load-time ABI check, the error classes, the +# value-buffer reader and writer, cancel tokens, the handle table behind +# callback interfaces and async completions, and the object wrapper base. +module EdgeCases + extend FFI::Library + + # An explicit path in EDGE_CASES_LIBRARY wins. Otherwise a library bundled + # under lib/native/ (a platform gem) is preferred over the system search + # path. + _wv_override = ENV['EDGE_CASES_LIBRARY'] + if _wv_override && !_wv_override.empty? + ffi_lib _wv_override + else + _wv_name = + case FFI::Platform::OS + when /darwin/ then 'libedge_cases.dylib' + when /mswin|mingw|windows/ then 'edge_cases.dll' + else 'libedge_cases.so' + end + _wv_bundled = File.expand_path(File.join('..', 'native', _wv_name), __dir__) + ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) + end + + # The C ABI revision these bindings speak. Checked before anything else is + # attached, so a mismatched library fails at require time instead of + # misreading an error struct or a value buffer later. + ABI_VERSION = 3 + begin + attach_function :edge_cases_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, "the loaded edge_cases library does not export edge_cases_abi_version " \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = edge_cases_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "edge_cases ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + + # The `edge_cases_error` out-parameter every call reports through. + class ErrorStruct < FFI::Struct + layout :code, :int32, + :message, :pointer, + :payload_ptr, :pointer, + :payload_len, :size_t + end + + # Codes reserved by the runtime. Positive codes belong to a module's error + # domain; these negative codes are raised as a plain Error (or Cancelled) + # regardless of whether the function declares errors. + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + # A callback-interface implementation raised; the message carries the + # exception text. + FOREIGN_ERROR_CODE = -4 + # The call was cancelled through its CancelToken. + CANCELLED_ERROR_CODE = -5 + + # The base class of every error the library raises. + class Error < StandardError + # The ABI error code: positive for a domain error, negative for a runtime + # failure. + attr_reader :code + + def initialize(code, message) + @code = code + super(message) + end + end + + # Raised when a cancellable call completes because its CancelToken fired. + class Cancelled < Error + def initialize(message = 'cancelled') + super(CANCELLED_ERROR_CODE, message) + end + end + + attach_function :edge_cases_error_set, [:pointer, :int32, :string], :void + attach_function :edge_cases_error_clear, [:pointer], :void + attach_function :edge_cases_error_free, [:pointer], :void + attach_function :edge_cases_free_bytes, [:pointer, :size_t], :void + attach_function :edge_cases_cancel_token_create, [], :pointer + attach_function :edge_cases_cancel_token_cancel, [:pointer], :void + attach_function :edge_cases_cancel_token_is_cancelled, [:pointer], :bool + attach_function :edge_cases_cancel_token_destroy, [:pointer], :void + + # @api private + # The error for a code outside any declared domain. + def self._wv_error(code, message) + return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE + + Error.new(code, message) + end + + # @api private + # Copies the code, message, and payload out of a reported error and clears + # it. Returns nil when the struct reports success. + def self._wv_take_error(err) + code = err[:code] + return nil if code.zero? + + msg_ptr = err[:message] + message = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) + payload_ptr = err[:payload_ptr] + payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) + edge_cases_error_clear(err.to_ptr) + [code, message, payload] + end + + # @api private + # Like _wv_take_error for the heap-boxed error an async completion + # receives; the box is released with edge_cases_error_free. + def self._wv_take_boxed_error(err_ptr) + return nil if err_ptr.null? + + taken = _wv_take_error(ErrorStruct.new(err_ptr)) + edge_cases_error_free(err_ptr) + taken + end + + # @api private + # Raises the generic error for a call that declares no error domain. + def self.check_error!(err) + taken = _wv_take_error(err) + return if taken.nil? + + raise _wv_error(taken[0], taken[1]) + end + + # @api private + # The UTF-8 bytes of a String argument, as a private binary copy the + # producer borrows for the call. + def self._wv_str(value) + value.to_s.encode(Encoding::UTF_8).b + end + + # @api private + # A private binary copy of a bytes argument. + def self._wv_bytes(value) + value.to_s.b + end + + # @api private + # Copies a borrowed (ptr, len) run without releasing it. + def self._wv_borrow_bytes(ptr, len) + ptr.null? || len.zero? ? ''.b : ptr.read_string(len) + end + + # @api private + # Copies a returned (ptr, len) allocation, then releases it. + def self._wv_take_bytes(ptr, len) + return ''.b if ptr.null? + + data = len.zero? ? ''.b : ptr.read_string(len) + edge_cases_free_bytes(ptr, len) + data + end + + # @api private + # Copies a returned UTF-8 string, then releases it. + def self._wv_take_string(ptr, len) + _wv_take_bytes(ptr, len).force_encoding(Encoding::UTF_8) + end + + # A cancellation signal for a cancellable async call. Pass one as the + # `cancel:` keyword, then call #cancel from any thread; the call raises + # Cancelled unless it already completed. A token may be shared by several + # calls and stays cancelled once cancelled. + class CancelToken + # @api private + # Owns the consumer's reference to the native token. + class Ptr < FFI::AutoPointer + def self.release(ptr) + EdgeCases.edge_cases_cancel_token_destroy(ptr) + end + end + + def initialize + @ptr = Ptr.new(EdgeCases.edge_cases_cancel_token_create) + end + + # Requests cancellation. Idempotent. + def cancel + EdgeCases.edge_cases_cancel_token_cancel(@ptr) unless @ptr.nil? + nil + end + + # Whether #cancel has been called. + def cancelled? + !@ptr.nil? && EdgeCases.edge_cases_cancel_token_is_cancelled(@ptr) + end + + # Releases the native token now rather than at GC time. Calls already + # launched with it keep their own reference. + def close + return if @ptr.nil? + + @ptr.free + @ptr = nil + end + + # @api private + def _wv_ptr + raise Error.new(GENERIC_ERROR_CODE, 'CancelToken used after close') if @ptr.nil? + + @ptr + end + end + + # @api private + # Appends values in the value-buffer wire format: little-endian, packed, + # no alignment. + class WvBufferWriter + def initialize + @buf = +''.b + @objects = [] + end + + # Reserves an object token. The strong reference it carries is minted + # by #seal, just before the buffer is handed to the producer, so an + # encoding abandoned on the way never strands a reference. + def write_object(object) + raise TypeError, "expected a wrapper object, got #{object.class}" unless object.is_a?(WvObject) + + @objects << [@buf.bytesize, object] + write_u64(0) + end + + # Mints the reference each reserved object token carries, recording it + # in `minted`, and returns the finished encoding. + def seal(minted) + @objects.each do |offset, object| + ptr = object._wv_clone_ptr + minted << [object.class, ptr] + @buf[offset, 8] = [ptr.address].pack('Q<') + end + @buf + end + + # Every append is a binary String, so the buffer stays binary and byte + # offsets index it directly. + def write_bool(value) + @buf << [value ? 1 : 0].pack('C') + end + + def write_flag(value) + write_bool(value) + end + + def write_i8(value) + @buf << [value].pack('c') + end + + def write_u8(value) + @buf << [value].pack('C') + end + + def write_i16(value) + @buf << [value].pack('s<') + end + + def write_u16(value) + @buf << [value].pack('S<') + end + + def write_i32(value) + @buf << [value].pack('l<') + end + + def write_u32(value) + @buf << [value].pack('L<') + end + + def write_len(value) + write_u32(value) + end + + def write_i64(value) + @buf << [value].pack('q<') + end + + def write_u64(value) + @buf << [value].pack('Q<') + end + + def write_f32(value) + @buf << [value].pack('e') + end + + def write_f64(value) + @buf << [value].pack('E') + end + + def write_string(value) + write_bytes(value.to_s.encode(Encoding::UTF_8)) + end + + def write_bytes(value) + bytes = value.to_s.b + write_u32(bytes.bytesize) + @buf << bytes + end + end + + # @api private + # Reads values in the 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(count, what) + if @pos + count > @data.bytesize + raise Error.new(MARSHAL_ERROR_CODE, "malformed value buffer: truncated #{what}") + end + + bytes = @data.byteslice(@pos, count) + @pos += count + bytes + end + + def read_bool + byte = take(1, 'bool').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: bool byte out of range') if byte > 1 + + byte == 1 + end + + def read_flag + byte = take(1, 'option flag').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: option flag out of range') if byte > 1 + + byte == 1 + end + + def read_i8 + take(1, 'i8').unpack1('c') + end + + def read_u8 + take(1, 'u8').unpack1('C') + end + + def read_i16 + take(2, 'i16').unpack1('s<') + end + + def read_u16 + take(2, 'u16').unpack1('S<') + end + + def read_i32 + take(4, 'i32').unpack1('l<') + end + + def read_u32 + take(4, 'u32').unpack1('L<') + end + + # A u32 element count. Counts are not checked against the remaining + # bytes, since an element can encode to zero bytes; a short buffer still + # fails on the first truncated read. + def read_len + read_u32 + end + + def read_i64 + take(8, 'i64').unpack1('q<') + end + + def read_u64 + take(8, 'u64').unpack1('Q<') + end + + def read_f32 + take(4, 'f32').unpack1('e') + end + + def read_f64 + take(8, 'f64').unpack1('E') + end + + def read_string + text = read_bytes.force_encoding(Encoding::UTF_8) + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: string is not valid UTF-8') unless text.valid_encoding? + + text + end + + def read_bytes + take(read_u32, 'byte run') + end + + # An object token: a non-zero u64 carrying one strong reference the + # caller adopts into a wrapper. + def read_object_token + addr = read_u64 + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: null object token') if addr.zero? + + FFI::Pointer.new(addr) + end + + def expect_end! + return if @pos == @data.bytesize + + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: trailing bytes after value') + end + end + + # @api private + # Seals the writers of one call's object-carrying buffers (see + # WvBufferWriter#seal) and returns their encodings. If any reference can't + # be minted (a closed wrapper), the ones already minted are released before + # the error propagates. The producer adopts every minted reference once the + # call is made. + def self._wv_seal(*writers) + minted = [] + writers.map { |writer| writer.seal(minted) } + rescue Exception # rubocop:disable Lint/RescueException + minted.each { |cls, ptr| cls::WV_PTR.release(ptr) } + raise + end + + # @api private + # Decodes a returned value buffer: copies and releases it, yields a reader + # to the block, and checks that the block consumed every byte. + def self._wv_decode(ptr, len) + reader = WvBufferReader.new(_wv_take_bytes(ptr, len)) + value = yield reader + reader.expect_end! + value + end + + # Ruby objects the producer refers to by an integer key: callback-interface + # implementations (the key is the vtable `ctx`) and pending async + # completions (the key is the completion `context`). The producer never + # sees a Ruby object address. A Monitor rather than a Mutex, so a + # finalizer that runs while this thread holds the lock can't deadlock it. + @wv_handles = {} + @wv_next_handle = 0 + @wv_handles_lock = Monitor.new + + # @api private + def self._wv_handle_put(value) + @wv_handles_lock.synchronize do + @wv_next_handle += 1 + @wv_handles[@wv_next_handle] = value + FFI::Pointer.new(@wv_next_handle) + end + end + + # @api private + def self._wv_handle_get(key) + @wv_handles_lock.synchronize { @wv_handles[key.address] } + end + + # @api private + def self._wv_handle_take(key) + @wv_handles_lock.synchronize { @wv_handles.delete(key.address) } + end + + # @api private + # Registers a callback-interface implementation and returns its `ctx`. + # Never raises, so nothing can strand the registration between this and + # the call that hands it to the producer (an unusable implementation fails + # when the producer calls it, with FOREIGN_ERROR_CODE). + def self._wv_cb_register(impl) + _wv_handle_put([:callback, impl]) + end + + # @api private + # Resolves a trampoline's `ctx` back to the registered implementation. + def self._wv_cb_lookup(ctx) + kind, impl = _wv_handle_get(ctx) + raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') unless kind == :callback + + impl + end + + # @api private + # Drops an implementation when the producer releases its last reference. + def self._wv_cb_free(ctx) + _wv_handle_take(ctx) + nil + end + + # @api private + # Reports a callback implementation's exception through `out_err`. The + # producer copies the message. + def self._wv_cb_fail(out_err, exception) + message = exception.message.to_s + message = exception.class.name if message.empty? + message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).delete("\0") + edge_cases_error_set(out_err, FOREIGN_ERROR_CODE, message) + nil + end + + # @api private + # Registers a pending async call; returns its completion context and the + # queue its completion is delivered on. + def self._wv_async_begin + queue = Queue.new + [_wv_handle_put([:async, queue]), queue] + end + + # @api private + # The queue of the pending call a completion belongs to. Each context + # completes exactly once, so its entry is removed here. + def self._wv_async_finish(ctx) + kind, queue = _wv_handle_take(ctx) + kind == :async ? queue : Queue.new + end + + # @api private + # Blocks until a pending call completes, raising its error. If the wait is + # interrupted (Thread#raise, Thread#kill, Timeout), the call's token is + # cancelled before the interruption propagates. + def self._wv_async_wait(queue, token = nil) + value = + begin + queue.pop + rescue Exception # rubocop:disable Lint/RescueException + token&.cancel + raise + end + raise value if value.is_a?(Exception) + + value + end + + # @api private + # The base of every interface wrapper. A wrapper owns one strong reference + # to its native object and releases it exactly once, from #close or (as a + # backstop) from the GC finalizer of its FFI::AutoPointer. Calls borrow the + # pointer through #_wv_call, so a #close that races an in-flight call on + # another thread only releases the reference once the call returns. + class WvObject + # @api private + # Adopts one strong reference the producer handed over. + def self._from_ptr(ptr) + obj = allocate + obj._wv_init(ptr) + obj + end + + # @api private + def _wv_init(ptr) + @wv_handle = self.class::WV_PTR.new(ptr) + @wv_lock = Mutex.new + @wv_calls = 0 + @wv_closing = false + end + + # The native object pointer (borrowed; valid until #close). + def handle + handle = @wv_handle + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if handle.nil? || @wv_closing + + handle + end + + # Whether #close has released this wrapper's reference. + def closed? + @wv_handle.nil? || @wv_closing + end + + # Releases this wrapper's reference now rather than at GC time. + # Idempotent. The object itself lives until its last reference anywhere + # (another wrapper, a record field, the producer) goes. + def close + handle = @wv_lock.synchronize do + next nil if @wv_handle.nil? || @wv_closing + + if @wv_calls.zero? + taken = @wv_handle + @wv_handle = nil + taken + else + @wv_closing = true + nil + end + end + handle&.free + nil + end + + # @api private + # Yields the native pointer, keeping the reference alive until the block + # returns. + def _wv_call + handle = @wv_lock.synchronize do + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if @wv_handle.nil? || @wv_closing + + @wv_calls += 1 + @wv_handle + end + begin + yield handle + ensure + _wv_leave + end + end + + # @api private + # Mints a new strong reference the caller owns (an object written into a + # value buffer carries its own reference). + def _wv_clone_ptr + _wv_call { |ptr| self.class._wv_clone(ptr) } + end + + # dup and clone produce an independent wrapper with its own reference to + # the same object. + def initialize_copy(other) + super + _wv_init(other._wv_clone_ptr) + end + + private + + def _wv_leave + release = @wv_lock.synchronize do + @wv_calls -= 1 + next nil unless @wv_closing && @wv_calls.zero? + + taken = @wv_handle + @wv_handle = nil + @wv_closing = false + taken + end + release&.free + end + end + + # @api private + # Yields the native pointers of `objects` (NULL for nil), each kept alive + # until the block returns. + def self._wv_pin(*objects, &block) + _wv_pin_from(objects, 0, [], &block) + end + + # @api private + def self._wv_pin_from(objects, index, pointers, &block) + return yield(*pointers) if index == objects.length + + object = objects[index] + if object.nil? + _wv_pin_from(objects, index + 1, pointers + [FFI::Pointer::NULL], &block) + else + object._wv_call { |ptr| _wv_pin_from(objects, index + 1, pointers + [ptr], &block) } + end + end + + # @api private + # Checks each top-level module's contract checksum (`module name => + # [checksum function, expected value]`) against the library, so bindings + # generated from a different API fail at require time. + def self._wv_check_contract!(checksums) + checksums.each do |mod, (symbol, expected)| + begin + attach_function symbol, [], :uint64 + rescue FFI::NotFoundError + raise LoadError, "the loaded edge_cases library has no module '#{mod}' (#{symbol} is missing)" + end + actual = public_send(symbol) + next if actual == expected + + raise LoadError, "edge_cases module '#{mod}' does not match these bindings: expected contract " \ + "checksum 0x#{expected.to_s(16)}, the library reports 0x#{actual.to_s(16)}; " \ + 'regenerate the bindings' + end + end + + # @api private + # Live native resources of one kind (0 objects, 1 callbacks, 2 iterators, + # 3 cancel tokens, 4 byte allocations). Only libraries built with the + # `leak-check` feature export the counter. + def self._wv_debug_live(kind) + unless respond_to?(:edge_cases_debug_live) + begin + attach_function :edge_cases_debug_live, [:int32], :uint64 + rescue FFI::NotFoundError + raise Error.new(GENERIC_ERROR_CODE, 'edge_cases was built without the leak-check feature') + end + end + edge_cases_debug_live(kind) + end +end + +# END runtime.rb 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 deleted file mode 100644 index 4b5209de..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_weaveffi_rb.snap +++ /dev/null @@ -1,2177 +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 - - # 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 = 2 - 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 - - # The runtime-reserved error codes. Positive codes belong to a module's - # declared error domain; these negative codes are programming errors the - # wrappers raise as a plain Error regardless of the function's `throws`. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - - 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 - - # An object token: a non-zero u64 carrying one strong reference the - # caller adopts into a wrapper. - def read_object_token - addr = read_u64 - raise Error.new(-1, 'malformed value buffer: null object token') if addr.zero? - FFI::Pointer.new(addr) - 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_set, [:pointer, :int32, :string], :void - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # Live callback-interface implementations keyed by the Integer the producer - # holds as `ctx`. Keys start at 1 so a context pointer is never NULL. - @wv_cb_registry = {} - @wv_cb_next_key = 0 - @wv_cb_mutex = Mutex.new - - # @api private - # Registers `impl` and returns the `ctx` pointer to hand the producer. - def self._wv_cb_register(impl) - @wv_cb_mutex.synchronize do - @wv_cb_next_key += 1 - @wv_cb_registry[@wv_cb_next_key] = impl - FFI::Pointer.new(@wv_cb_next_key) - end - end - - # @api private - # Resolves a trampoline's `ctx` back to the registered implementation. - def self._wv_cb_lookup(ctx) - impl = @wv_cb_mutex.synchronize { @wv_cb_registry[ctx.address] } - raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') if impl.nil? - impl - end - - # @api private - # Drops the registry entry when the producer releases its last reference. - def self._wv_cb_free(ctx) - @wv_cb_mutex.synchronize { @wv_cb_registry.delete(ctx.address) } - nil - end - - # @api private - # Reports a callback implementation's exception through `out_err`. The - # producer copies the message, so the Ruby String need not outlive the call. - def self._wv_cb_fail(out_err, exception) - message = exception.message.to_s - message = exception.class.name if message.empty? - weaveffi_error_set(out_err, FOREIGN_ERROR_CODE, message) - nil - end - - # === 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, callback failures). - 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.force_encoding(Encoding::UTF_8) - 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 - - # Record carrying objects in every buffered position - class WidgetHolder - attr_reader :primary - - attr_reader :fallback - - attr_reader :extras - - attr_reader :by_name - - def initialize(primary:, fallback:, extras:, by_name:) - @primary = primary - @fallback = fallback - @extras = extras - @by_name = by_name - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(WidgetHolder) - return false unless primary == other.primary - return false unless fallback == other.fallback - return false unless extras == other.extras - return false unless by_name == other.by_name - 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 - - # @api private - # Packs a WidgetHolder into the value-buffer wire format. - def self._wv_write_widget_holder(w, v) - w.write_u64(v.primary._wv_clone_ptr.address) - if v.fallback.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_u64(v.fallback._wv_clone_ptr.address) - end - w.write_len(v.extras.length) - v.extras.each do |_wv_e0| - w.write_u64(_wv_e0._wv_clone_ptr.address) - end - w.write_len(v.by_name.length) - v.by_name.each do |_wv_k0, _wv_v0| - w.write_string(_wv_k0) - w.write_u64(_wv_v0._wv_clone_ptr.address) - end - end - - # @api private - # Unpacks a WidgetHolder from the value-buffer wire format. - def self._wv_read_widget_holder(r) - _wv_primary = Widget._from_ptr(r.read_object_token) - if r.read_flag - _wv_fallback = Widget._from_ptr(r.read_object_token) - else - _wv_fallback = nil - end - _wv_extras = Array.new(r.read_len) do - _wv_e0 = Widget._from_ptr(r.read_object_token) - _wv_e0 - end - _wv_by_name = {} - r.read_len.times do - _wv_k0 = r.read_string - _wv_v0 = Widget._from_ptr(r.read_object_token) - _wv_by_name[_wv_k0] = _wv_v0 - end - WidgetHolder.new(primary: _wv_primary, fallback: _wv_fallback, extras: _wv_extras, by_name: _wv_by_name) - end - - attach_function :weaveffi_edge_cases_Widget_clone, [:pointer], :pointer - 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_clone, [: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 - # 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 - # Two callback interface parameters on one function - attach_function :weaveffi_edge_cases_register_batch, [:pointer, :pointer, :pointer, :pointer, :pointer], :void - # Objects in every buffered position - attach_function :weaveffi_edge_cases_widgets_everywhere, [:pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :pointer, :pointer], :pointer - # Iterator of objects - attach_function :weaveffi_edge_cases_widget_stream, [:pointer], :pointer - attach_function :weaveffi_edge_cases_WidgetStreamIterator_next, [:pointer, :pointer, :pointer], :int32 - attach_function :weaveffi_edge_cases_WidgetStreamIterator_destroy, [:pointer], :void - # Async returning an object - callback :weaveffi_edge_cases_async_widget_callback, [:pointer, :pointer, :pointer], :void - attach_function :weaveffi_edge_cases_async_widget_async, [:weaveffi_edge_cases_async_widget_callback, :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 - # 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 - - # Callback interface carrying composite payloads and objects - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a BatchListener parameter is expected; - # include this module to inherit NotImplementedError defaults. The - # producer may call the methods from any thread until it releases the - # implementation. - module BatchListener - # Composite payloads cross as value buffers - def on_batch(items, raw, first, mode) - raise NotImplementedError, "#{self.class}#on_batch is not implemented" - end - - # Objects cross as strong references the consumer must release - def on_widget(widget, fallback, children) - raise NotImplementedError, "#{self.class}#on_widget is not implemented" - end - - def on_tick - raise NotImplementedError, "#{self.class}#on_tick is not implemented" - end - - # Scalar return travels back through the vtable - # @return [Object] a bool - def should_continue(class_) - raise NotImplementedError, "#{self.class}#should_continue is not implemented" - end - - # Enum return travels back as its integer discriminant - # @return [Object] a Mode - def pick_mode - raise NotImplementedError, "#{self.class}#pick_mode is not implemented" - end - end - - # @api private - # The C vtable layout the producer calls a BatchListener through. - class WvBatchListenerVtable < FFI::Struct - layout :on_batch, :pointer, - :on_widget, :pointer, - :on_tick, :pointer, - :should_continue, :pointer, - :pick_mode, :pointer, - :free, :pointer - end - - # @api private - # Trampoline for BatchListener#on_batch. - WV_BATCH_LISTENER_ON_BATCH = FFI::Function.new(:void, [:pointer, :pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :int32, :pointer]) do |ctx, items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, out_err| - begin - impl = _wv_cb_lookup(ctx) - 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! - raw_v = raw_ptr.null? ? ''.b : raw_ptr.read_string(raw_len) - 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! - mode_v = mode - impl.on_batch(items_v, raw_v, first_v, mode_v) - nil - rescue Exception => e - _wv_cb_fail(out_err, e) - nil - end - end - - # @api private - # Trampoline for BatchListener#on_widget. - WV_BATCH_LISTENER_ON_WIDGET = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :pointer, :size_t, :pointer]) do |ctx, widget, fallback, children_ptr, children_len, out_err| - begin - impl = _wv_cb_lookup(ctx) - widget_v = Widget._from_ptr(widget) - fallback_v = fallback.null? ? nil : Widget._from_ptr(fallback) - children_r = WvBufferReader.new(children_ptr.null? ? ''.b : children_ptr.read_string(children_len)) - children_v = Array.new(children_r.read_len) do - _wv_e0 = Widget._from_ptr(children_r.read_object_token) - _wv_e0 - end - children_r.expect_end! - impl.on_widget(widget_v, fallback_v, children_v) - nil - rescue Exception => e - _wv_cb_fail(out_err, e) - nil - end - end - - # @api private - # Trampoline for BatchListener#on_tick. - WV_BATCH_LISTENER_ON_TICK = FFI::Function.new(:void, [:pointer, :pointer]) do |ctx, out_err| - begin - impl = _wv_cb_lookup(ctx) - impl.on_tick - nil - rescue Exception => e - _wv_cb_fail(out_err, e) - nil - end - end - - # @api private - # Trampoline for BatchListener#should_continue. - WV_BATCH_LISTENER_SHOULD_CONTINUE = FFI::Function.new(:int32, [:pointer, :int32, :pointer]) do |ctx, class_, out_err| - begin - impl = _wv_cb_lookup(ctx) - class__v = class_ - impl.should_continue(class__v) ? 1 : 0 - rescue Exception => e - _wv_cb_fail(out_err, e) - 0 - end - end - - # @api private - # Trampoline for BatchListener#pick_mode. - WV_BATCH_LISTENER_PICK_MODE = FFI::Function.new(:int32, [:pointer, :pointer]) do |ctx, out_err| - begin - impl = _wv_cb_lookup(ctx) - Integer(impl.pick_mode) - rescue Exception => e - _wv_cb_fail(out_err, e) - 0 - end - end - - # @api private - # Releases a BatchListener implementation when the producer drops its last - # reference. - WV_BATCH_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end - - # The one process-wide BatchListener vtable every registration hands the - # producer; its entries live for the process lifetime. - WV_BATCH_LISTENER_VTABLE = WvBatchListenerVtable.new - WV_BATCH_LISTENER_VTABLE[:on_batch] = WV_BATCH_LISTENER_ON_BATCH - WV_BATCH_LISTENER_VTABLE[:on_widget] = WV_BATCH_LISTENER_ON_WIDGET - WV_BATCH_LISTENER_VTABLE[:on_tick] = WV_BATCH_LISTENER_ON_TICK - WV_BATCH_LISTENER_VTABLE[:should_continue] = WV_BATCH_LISTENER_SHOULD_CONTINUE - WV_BATCH_LISTENER_VTABLE[:pick_mode] = WV_BATCH_LISTENER_PICK_MODE - WV_BATCH_LISTENER_VTABLE[:free] = WV_BATCH_LISTENER_FREE - - # Callback interface whose name and methods are keywords - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a Keyword parameter is expected; - # include this module to inherit NotImplementedError defaults. The - # producer may call the methods from any thread until it releases the - # implementation. - module Keyword - def delete(self_) - raise NotImplementedError, "#{self.class}#delete is not implemented" - end - - # @return [Object] a i32 - def import - raise NotImplementedError, "#{self.class}#import is not implemented" - end - end - - # @api private - # The C vtable layout the producer calls a Keyword through. - class WvKeywordVtable < FFI::Struct - layout :delete, :pointer, - :import, :pointer, - :free, :pointer - end - - # @api private - # Trampoline for Keyword#delete. - WV_KEYWORD_DELETE = FFI::Function.new(:void, [:pointer, :int32, :pointer]) do |ctx, self_, out_err| - begin - impl = _wv_cb_lookup(ctx) - self__v = self_ - impl.delete(self__v) - nil - rescue Exception => e - _wv_cb_fail(out_err, e) - nil - end - end - - # @api private - # Trampoline for Keyword#import. - WV_KEYWORD_IMPORT = FFI::Function.new(:int32, [:pointer, :pointer]) do |ctx, out_err| - begin - impl = _wv_cb_lookup(ctx) - Integer(impl.import) - rescue Exception => e - _wv_cb_fail(out_err, e) - 0 - end - end - - # @api private - # Releases a Keyword implementation when the producer drops its last - # reference. - WV_KEYWORD_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end - - # The one process-wide Keyword vtable every registration hands the - # producer; its entries live for the process lifetime. - WV_KEYWORD_VTABLE = WvKeywordVtable.new - WV_KEYWORD_VTABLE[:delete] = WV_KEYWORD_DELETE - WV_KEYWORD_VTABLE[:import] = WV_KEYWORD_IMPORT - WV_KEYWORD_VTABLE[:free] = WV_KEYWORD_FREE - - # @api private - # Owns one strong reference to a Widget; releases it exactly once. - 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 - # @api private - # Adopts one strong reference 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 - - # The borrowed object pointer passed to producer calls. - def handle - raise Error.new(-1, 'Widget used after close') if @handle.nil? - @handle - end - - # Whether close has released this wrapper's reference. - def closed? - @handle.nil? - end - - # Releases this wrapper's reference now rather than at GC time. - # Idempotent; the object itself is dropped when the last reference - # anywhere (another wrapper, a record field, the producer) goes. - def close - return if @handle.nil? - @handle.free - @handle = nil - end - - # @api private - # Mints a new strong reference the caller owns (used when this - # object is written into a value buffer). - def _wv_clone_ptr - WeaveFFI.weaveffi_edge_cases_Widget_clone(handle) - end - - # dup and clone produce an independent wrapper with its own reference. - def initialize_copy(other) - super - @handle = WidgetPtr.new(WeaveFFI.weaveffi_edge_cases_Widget_clone(other.handle)) - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - WeaveFFI.weaveffi_free_string(result) - str - end - end - - # @api private - # Owns one strong reference to a Vault; releases it exactly once. - class VaultPtr < FFI::AutoPointer - def self.release(ptr) - WeaveFFI.weaveffi_edge_cases_Vault_destroy(ptr) - end - end - - # Deprecated interface with a throwing constructor - # @deprecated Use Widget instead - class Vault - # @api private - # Adopts one strong reference 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 - - # The borrowed object pointer passed to producer calls. - def handle - raise Error.new(-1, 'Vault used after close') if @handle.nil? - @handle - end - - # Whether close has released this wrapper's reference. - def closed? - @handle.nil? - end - - # Releases this wrapper's reference now rather than at GC time. - # Idempotent; the object itself is dropped when the last reference - # anywhere (another wrapper, a record field, the producer) goes. - def close - return if @handle.nil? - @handle.free - @handle = nil - end - - # @api private - # Mints a new strong reference the caller owns (used when this - # object is written into a value buffer). - def _wv_clone_ptr - WeaveFFI.weaveffi_edge_cases_Vault_clone(handle) - end - - # dup and clone produce an independent wrapper with its own reference. - def initialize_copy(other) - super - @handle = VaultPtr.new(WeaveFFI.weaveffi_edge_cases_Vault_clone(other.handle)) - 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 - - # 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 - - # Two callback interface parameters on one function - def self.register_batch(listener, keyword) - err = ErrorStruct.new - listener_ctx = _wv_cb_register(listener) - keyword_ctx = _wv_cb_register(keyword) - weaveffi_edge_cases_register_batch(listener_ctx, WV_BATCH_LISTENER_VTABLE.to_ptr, keyword_ctx, WV_KEYWORD_VTABLE.to_ptr, err) - check_error!(err) - end - - # Objects in every buffered position - def self.widgets_everywhere(list, by_name, sparse, holder) - err = ErrorStruct.new - list_w = WvBufferWriter.new - list_w.write_len(list.length) - list.each do |_wv_e0| - list_w.write_u64(_wv_e0._wv_clone_ptr.address) - end - list_data = list_w.data - list_buf = FFI::MemoryPointer.new(:uint8, list_data.bytesize) - list_buf.put_bytes(0, list_data) - by_name_w = WvBufferWriter.new - by_name_w.write_len(by_name.length) - by_name.each do |_wv_k0, _wv_v0| - by_name_w.write_string(_wv_k0) - by_name_w.write_u64(_wv_v0._wv_clone_ptr.address) - end - by_name_data = by_name_w.data - by_name_buf = FFI::MemoryPointer.new(:uint8, by_name_data.bytesize) - by_name_buf.put_bytes(0, by_name_data) - sparse_w = WvBufferWriter.new - sparse_w.write_len(sparse.length) - sparse.each do |_wv_e0| - if _wv_e0.nil? - sparse_w.write_flag(false) - else - sparse_w.write_flag(true) - sparse_w.write_u64(_wv_e0._wv_clone_ptr.address) - end - end - sparse_data = sparse_w.data - sparse_buf = FFI::MemoryPointer.new(:uint8, sparse_data.bytesize) - sparse_buf.put_bytes(0, sparse_data) - holder_w = WvBufferWriter.new - _wv_write_widget_holder(holder_w, holder) - holder_data = holder_w.data - holder_buf = FFI::MemoryPointer.new(:uint8, holder_data.bytesize) - holder_buf.put_bytes(0, holder_data) - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_edge_cases_widgets_everywhere(list_buf, list_data.bytesize, by_name_buf, by_name_data.bytesize, sparse_buf, sparse_data.bytesize, holder_buf, holder_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_widget_holder(_wv_r) - _wv_r.expect_end! - _wv_value - end - - # Iterator of objects - # 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.widget_stream() - Enumerator.new do |y| - err = ErrorStruct.new - iter = weaveffi_edge_cases_widget_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_WidgetStreamIterator_next(iter, out_item, item_err) - check_error!(item_err) - break if has_item.zero? - item_ptr = out_item.read_pointer - raise Error.new(-1, 'null pointer') if item_ptr.null? - y << Widget._from_ptr(item_ptr) - end - end - ensure - weaveffi_edge_cases_WidgetStreamIterator_destroy(iter) unless iter.null? - end - end - end - - # Async returning an object - # Blocks the current thread until the async producer completes; the - # result (or error) is delivered through the completion callback. - def self.async_widget() - 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.force_encoding(Encoding::UTF_8) - 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 - if result.null? - queue << Error.new(-1, 'null pointer') - else - queue << Widget._from_ptr(result) - end - end - end - weaveffi_edge_cases_async_widget_async(callback, FFI::Pointer::NULL) - value = queue.pop - raise value if value.is_a?(Error) - value - 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.force_encoding(Encoding::UTF_8) - weaveffi_free_string(result) - str - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_free(err_ptr) - queue << Error.new(code, msg) - else - _wv_s = result.null? ? '' : result.read_string.force_encoding(Encoding::UTF_8) - 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_edge_cases__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__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__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__README_md.snap index dfa27948..6f0e6f67 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__README_md.snap @@ -1,30 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Ruby) +# kitchen_sink (Ruby) -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. +Ruby bindings for `kitchen_sink` 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. +- Ruby 2.7 or newer +- The native library (`libkitchen_sink.so`, `libkitchen_sink.dylib`, or `kitchen_sink.dll`) on the library + search path, or its path in the `KITCHEN_SINK_LIBRARY` environment variable. ## Install ```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem +gem build kitchen_sink.gemspec +gem install kitchen_sink-0.1.0.gem ``` ## Usage ```ruby -require 'weaveffi' +require 'kitchen_sink' + +KitchenSink::ABI_VERSION ``` diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__kitchen_sink_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__kitchen_sink_gemspec.snap new file mode 100644 index 00000000..7d8d8fcf --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__kitchen_sink_gemspec.snap @@ -0,0 +1,20 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +Gem::Specification.new do |s| + s.name = 'kitchen_sink' + s.version = '0.1.0' + s.summary = 'Native bindings for kitchen_sink' + s.authors = ['kitchen_sink'] + s.files = Dir['lib/**/*.rb'] + s.require_paths = ['lib'] + s.required_ruby_version = '>= 2.7' + + s.add_dependency 'ffi', '~> 1.15' +end + +# END kitchen_sink.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_rb.snap new file mode 100644 index 00000000..d08bb7c9 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_rb.snap @@ -0,0 +1,719 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require_relative 'kitchen_sink/runtime' + +# Ruby bindings for kitchen_sink. +module KitchenSink + # Contract checksums of the top-level modules these bindings were + # generated from; a library built from a different API fails to load. + _wv_check_contract!( + 'shared' => [:kitchen_sink_shared_checksum, 0x42c4ce2c8d0af052], + 'kitchen' => [:kitchen_sink_kitchen_checksum, 0xd83da75b24b545ff], + ) + + # === Module: shared === + + # A reusable token referenced across modules + class Token + attr_reader :id + + attr_reader :label + + def initialize(id:, label:) + @id = id + @label = label + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(Token) + return false unless id == other.id + return false unless label == other.label + true + end + end + + # @api private + # Packs a Token into the value-buffer wire format. + def self._wv_write_token(w, v) + w.write_i64(v.id) + w.write_string(v.label) + end + + # @api private + # Unpacks a Token from the value-buffer wire format. + def self._wv_read_token(r) + _wv_id = r.read_i64 + _wv_label = r.read_string + Token.new(id: _wv_id, label: _wv_label) + end + + attach_function :kitchen_sink_shared_ping, [:pointer, :pointer], :pointer, blocking: true + + # Trivial cross-module helper + def self.ping + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_shared_ping(out_len, err) + check_error!(err) + _wv_take_string(result, out_len.read(:size_t)) + end + + # === Module: kitchen === + + # Base error for the `kitchen` module's error domain. + class KitchenErrorsError < Error + # Item not found + class NotFound < KitchenErrorsError + CODE = 1 + + def initialize(message = 'Item not found') + super(1, message) + end + end + + # Invalid input + class InvalidInput < KitchenErrorsError + CODE = 2 + + def initialize(message = 'Invalid input') + super(2, message) + end + end + end + + # Maps each ABI code of the KitchenErrorsError domain to its error class. + KITCHEN_ERRORS_ERROR_CODES = { + 1 => KitchenErrorsError::NotFound, + 2 => KitchenErrorsError::InvalidInput, + }.freeze + + # Builds the KitchenErrorsError subclass matching `code`, decoding any payload + # fields the code declares, or a generic Error for codes outside + # the domain (panics, marshalling, callback failures). + def self.kitchen_errors_error_from(code, message, payload = nil) + cls = KITCHEN_ERRORS_ERROR_CODES[code] + return _wv_error(code, message) if cls.nil? + message.empty? ? cls.new : cls.new(message) + end + + # Raises the typed KitchenErrorsError for a non-zero error slot. + def self.check_kitchen_errors_error!(err) + taken = _wv_take_error(err) + raise kitchen_errors_error_from(*taken) if taken + end + + # Task priority levels + module Priority + # Low priority + LOW = 0 + # Normal priority + NORMAL = 1 + # High priority + HIGH = 2 + end + + # Kitchen sink struct exercising every field feature + class Item + # Stable identifier + attr_reader :id + + # Display name + attr_reader :name + + # Initial count + attr_reader :count + + # Whether the item is enabled + attr_reader :enabled + + # Scaling ratio + attr_reader :ratio + + attr_reader :nick + + attr_reader :payload + + attr_reader :tags + + attr_reader :attrs + + attr_reader :parent + + # Cross-module struct reference + attr_reader :token + + attr_reader :priority + + # An optional object inside a record + attr_reader :gadget + + def initialize(id:, name:, count:, enabled:, ratio:, nick:, payload:, tags:, attrs:, parent:, token:, priority:, gadget:) + @id = id + @name = name + @count = count + @enabled = enabled + @ratio = ratio + @nick = nick + @payload = payload + @tags = tags + @attrs = attrs + @parent = parent + @token = token + @priority = priority + @gadget = gadget + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(Item) + return false unless id == other.id + return false unless name == other.name + return false unless count == other.count + return false unless enabled == other.enabled + return false unless ratio == other.ratio + return false unless nick == other.nick + return false unless payload == other.payload + return false unless tags == other.tags + return false unless attrs == other.attrs + return false unless parent == other.parent + return false unless token == other.token + return false unless priority == other.priority + return false unless gadget == other.gadget + true + end + end + + # @api private + # Packs a Item into the value-buffer wire format. + def self._wv_write_item(w, v) + w.write_i64(v.id) + w.write_string(v.name) + w.write_i32(v.count) + w.write_bool(v.enabled) + w.write_f64(v.ratio) + w.write_u32(v.nick) + w.write_bytes(v.payload) + w.write_len(v.tags.length) + v.tags.each do |_wv_e0| + w.write_string(_wv_e0) + end + w.write_len(v.attrs.length) + v.attrs.each do |_wv_k0, _wv_v0| + w.write_string(_wv_k0) + w.write_string(_wv_v0) + end + if v.parent.nil? + w.write_flag(false) + else + w.write_flag(true) + w.write_i64(v.parent) + end + _wv_write_token(w, v.token) + w.write_i32(v.priority) + if v.gadget.nil? + w.write_flag(false) + else + w.write_flag(true) + w.write_object(v.gadget) + end + end + + # @api private + # Unpacks a Item from the value-buffer wire format. + def self._wv_read_item(r) + _wv_id = r.read_i64 + _wv_name = r.read_string + _wv_count = r.read_i32 + _wv_enabled = r.read_bool + _wv_ratio = r.read_f64 + _wv_nick = r.read_u32 + _wv_payload = r.read_bytes + _wv_tags = Array.new(r.read_len) do + _wv_e0 = r.read_string + _wv_e0 + end + _wv_attrs = {} + r.read_len.times do + _wv_k0 = r.read_string + _wv_v0 = r.read_string + _wv_attrs[_wv_k0] = _wv_v0 + end + if r.read_flag + _wv_parent = r.read_i64 + else + _wv_parent = nil + end + _wv_token = _wv_read_token(r) + _wv_priority = r.read_i32 + if r.read_flag + _wv_gadget = Gadget._from_ptr(r.read_object_token) + else + _wv_gadget = nil + end + Item.new(id: _wv_id, name: _wv_name, count: _wv_count, enabled: _wv_enabled, ratio: _wv_ratio, nick: _wv_nick, payload: _wv_payload, tags: _wv_tags, attrs: _wv_attrs, parent: _wv_parent, token: _wv_token, priority: _wv_priority, gadget: _wv_gadget) + end + + attach_function :kitchen_sink_kitchen_Gadget_clone, [:pointer], :pointer + attach_function :kitchen_sink_kitchen_Gadget_destroy, [:pointer], :void, blocking: true + attach_function :kitchen_sink_kitchen_Gadget_new, [:int64, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_Gadget_describe, [:pointer, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_Gadget_poke, [:pointer, :int32, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_Gadget_version, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_bool_id, [:bool, :pointer], :bool, blocking: true + attach_function :kitchen_sink_kitchen_i32_id, [:int32, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_u32_id, [:uint32, :pointer], :uint32, blocking: true + attach_function :kitchen_sink_kitchen_i64_id, [:int64, :pointer], :int64, blocking: true + attach_function :kitchen_sink_kitchen_f64_id, [:double, :pointer], :double, blocking: true + attach_function :kitchen_sink_kitchen_echo_string, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_echo_bytes, [:pointer, :size_t, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_subscribe, [:pointer, :pointer, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_maybe_gadget, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_gadgets, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_stream_gadgets, [:pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_StreamGadgetsIterator_next, [:pointer, :pointer, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_StreamGadgetsIterator_destroy, [:pointer], :void, blocking: true + attach_function :kitchen_sink_kitchen_maybe_item, [:int64, :pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_list_items, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_get_attrs, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_stream_items, [:pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_StreamItemsIterator_next, [:pointer, :pointer, :pointer, :pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_StreamItemsIterator_destroy, [:pointer], :void, blocking: true + attach_function :kitchen_sink_kitchen_cross_module_token, [:pointer, :pointer], :pointer, blocking: true + attach_function :kitchen_sink_kitchen_do_async, [:pointer, :size_t, :pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for do_async. + KITCHEN_SINK_KITCHEN_DO_ASYNC_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << _wv_error(taken[0], taken[1]) + else + value = _wv_take_string(result_ptr, result_len) + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :kitchen_sink_kitchen_do_cancellable, [:pointer, :size_t, :pointer, :pointer, :pointer], :void, blocking: true + + # @api private + # Completion trampoline for do_cancellable. + KITCHEN_SINK_KITCHEN_DO_CANCELLABLE_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |ctx, err, result_ptr, result_len| + queue = _wv_async_finish(ctx) + begin + taken = _wv_take_boxed_error(err) + if taken + queue << _wv_error(taken[0], taken[1]) + else + value = _wv_take_string(result_ptr, result_len) + queue << value + end + rescue Exception => e # rubocop:disable Lint/RescueException + queue << e + end + end + + attach_function :kitchen_sink_kitchen_legacy_op, [:pointer], :int32, blocking: true + attach_function :kitchen_sink_kitchen_new_op, [:pointer], :int32, blocking: true + + # Consumer-implemented observer of ready items + # + # Consumer-implemented callback interface. Any object responding to the + # methods below is accepted wherever a ReadyListener is expected; include + # this module to inherit NotImplementedError defaults. The producer + # may call the methods from any thread until it releases the + # implementation. + module ReadyListener + # Fires when an item is ready + def on_ready(code, msg) + raise NotImplementedError, "#{self.class}#on_ready is not implemented" + end + + # Receives the item itself and says whether to keep listening + # @return [Object] a bool + def on_item(item, gadget) + raise NotImplementedError, "#{self.class}#on_item is not implemented" + end + end + + # @api private + # The C vtable layout the producer calls a ReadyListener through. + class WvReadyListenerVtable < FFI::Struct + layout :on_ready, :pointer, + :on_item, :pointer, + :free, :pointer + end + + # @api private + # Trampoline for ReadyListener#on_ready. + WV_READY_LISTENER_ON_READY = FFI::Function.new(:void, [:pointer, :int32, :pointer, :size_t, :pointer]) do |ctx, code, msg_ptr, msg_len, out_err| + begin + code_v = code + msg_v = _wv_borrow_bytes(msg_ptr, msg_len).force_encoding(Encoding::UTF_8) + impl = _wv_cb_lookup(ctx) + impl.on_ready(code_v, msg_v) + nil + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + nil + end + end + + # @api private + # Trampoline for ReadyListener#on_item. + WV_READY_LISTENER_ON_ITEM = FFI::Function.new(:bool, [:pointer, :pointer, :size_t, :pointer, :pointer]) do |ctx, item_ptr, item_len, gadget, out_err| + begin + gadget_v = Gadget._from_ptr(gadget) + item_r = WvBufferReader.new(_wv_borrow_bytes(item_ptr, item_len)) + item_v = _wv_read_item(item_r) + item_r.expect_end! + impl = _wv_cb_lookup(ctx) + impl.on_item(item_v, gadget_v) ? true : false + rescue Exception => e # rubocop:disable Lint/RescueException + _wv_cb_fail(out_err, e) + false + end + end + + # @api private + # Releases a ReadyListener implementation when the producer drops its last + # reference. + WV_READY_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| + _wv_cb_free(ctx) + end + + # The one process-wide ReadyListener vtable every registration hands the + # producer; its entries live for the process lifetime. + WV_READY_LISTENER_VTABLE = WvReadyListenerVtable.new + WV_READY_LISTENER_VTABLE[:on_ready] = WV_READY_LISTENER_ON_READY + WV_READY_LISTENER_VTABLE[:on_item] = WV_READY_LISTENER_ON_ITEM + WV_READY_LISTENER_VTABLE[:free] = WV_READY_LISTENER_FREE + + # @api private + # Owns one strong reference to a Gadget; releases it exactly once. + class GadgetPtr < FFI::AutoPointer + def self.release(ptr) + KitchenSink.kitchen_sink_kitchen_Gadget_destroy(ptr) + end + end + + # A pocket-sized interface exercising the object surface + class Gadget < WvObject + WV_PTR = GadgetPtr + + # @api private + def self._wv_clone(ptr) + KitchenSink.kitchen_sink_kitchen_Gadget_clone(ptr) + end + + # Create a gadget with the given id + def initialize(id) + err = ErrorStruct.new + result = KitchenSink.kitchen_sink_kitchen_Gadget_new(id, err) + KitchenSink.check_error!(err) + raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if result.null? + _wv_init(result) + end + + # Render the gadget as a human-readable string + def describe + KitchenSink._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = KitchenSink.kitchen_sink_kitchen_Gadget_describe(_wv_self, out_len, err) + KitchenSink.check_error!(err) + KitchenSink._wv_take_string(result, out_len.read(:size_t)) + end + end + + # Poke the gadget a number of times, failing on a negative count + def poke(times) + KitchenSink._wv_pin(self) do |_wv_self| + err = ErrorStruct.new + result = KitchenSink.kitchen_sink_kitchen_Gadget_poke(_wv_self, times, err) + KitchenSink.check_kitchen_errors_error!(err) + result + end + end + + # The gadget subsystem version string + def self.version + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = KitchenSink.kitchen_sink_kitchen_Gadget_version(out_len, err) + KitchenSink.check_error!(err) + KitchenSink._wv_take_string(result, out_len.read(:size_t)) + end + end + + # Identity for bool + def self.bool_id(x) + err = ErrorStruct.new + result = kitchen_sink_kitchen_bool_id((x ? true : false), err) + check_error!(err) + result + end + + def self.i32_id(x) + err = ErrorStruct.new + result = kitchen_sink_kitchen_i32_id(x, err) + check_error!(err) + result + end + + def self.u32_id(x) + err = ErrorStruct.new + result = kitchen_sink_kitchen_u32_id(x, err) + check_error!(err) + result + end + + def self.i64_id(x) + err = ErrorStruct.new + result = kitchen_sink_kitchen_i64_id(x, err) + check_error!(err) + result + end + + def self.f64_id(x) + err = ErrorStruct.new + result = kitchen_sink_kitchen_f64_id(x, err) + check_error!(err) + result + end + + def self.echo_string(s) + s_s = _wv_str(s) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_kitchen_echo_string(s_s, s_s.bytesize, out_len, err) + check_error!(err) + _wv_take_string(result, out_len.read(:size_t)) + end + + def self.echo_bytes(b) + b_s = _wv_bytes(b) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_kitchen_echo_bytes(b_s, b_s.bytesize, out_len, err) + check_error!(err) + _wv_take_bytes(result, out_len.read(:size_t)) + end + + # Register a listener, returning how many are attached + def self.subscribe(listener) + err = ErrorStruct.new + listener_ctx = _wv_cb_register(listener) + result = kitchen_sink_kitchen_subscribe(listener_ctx, WV_READY_LISTENER_VTABLE.to_ptr, err) + check_error!(err) + result + end + + # Optional object in both directions + def self.maybe_gadget(fallback) + _wv_pin(fallback) do |_wv_fallback| + err = ErrorStruct.new + result = kitchen_sink_kitchen_maybe_gadget(_wv_fallback, err) + check_error!(err) + result.null? ? nil : Gadget._from_ptr(result) + end + end + + # A list of objects + def self.gadgets + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_kitchen_gadgets(out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = Array.new(_wv_r.read_len) do + _wv_e0 = Gadget._from_ptr(_wv_r.read_object_token) + _wv_e0 + end + _wv_value + end + end + + # An iterator over objects + # Returns a lazy Enumerator that pulls one element per step. The + # producer iterator starts on the first pull and is released when + # iteration finishes or is abandoned early. + def self.stream_gadgets + Enumerator.new do |y| + err = ErrorStruct.new + iter = kitchen_sink_kitchen_stream_gadgets(err) + begin + check_error!(err) + out_item = FFI::MemoryPointer.new(:pointer) + loop do + has_item = kitchen_sink_kitchen_StreamGadgetsIterator_next(iter, out_item, err) + check_error!(err) + break if has_item.zero? + item = out_item.read_pointer + raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if item.null? + value = Gadget._from_ptr(item) + y << value + end + ensure + kitchen_sink_kitchen_StreamGadgetsIterator_destroy(iter) unless iter.null? + end + end + end + + # Optional struct return + def self.maybe_item(id) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_kitchen_maybe_item(id, out_len, err) + check_kitchen_errors_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + if _wv_r.read_flag + _wv_value = _wv_read_item(_wv_r) + else + _wv_value = nil + end + _wv_value + end + end + + # List of structs + def self.list_items + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_kitchen_list_items(out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = Array.new(_wv_r.read_len) do + _wv_e0 = _wv_read_item(_wv_r) + _wv_e0 + end + _wv_value + end + end + + # Map return type + def self.get_attrs + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_kitchen_get_attrs(out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = {} + _wv_r.read_len.times do + _wv_k0 = _wv_r.read_string + _wv_v0 = _wv_r.read_i32 + _wv_value[_wv_k0] = _wv_v0 + end + _wv_value + end + end + + # Iterator return type + # Returns a lazy Enumerator that pulls one element per step. The + # producer iterator starts on the first pull and is released when + # iteration finishes or is abandoned early. + def self.stream_items + Enumerator.new do |y| + err = ErrorStruct.new + iter = kitchen_sink_kitchen_stream_items(err) + begin + check_error!(err) + out_item = FFI::MemoryPointer.new(:pointer) + out_len = FFI::MemoryPointer.new(:size_t) + loop do + has_item = kitchen_sink_kitchen_StreamItemsIterator_next(iter, out_item, out_len, err) + check_error!(err) + break if has_item.zero? + item = out_item.read_pointer + value = _wv_decode(item, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_item(_wv_r) + _wv_value + end + y << value + end + ensure + kitchen_sink_kitchen_StreamItemsIterator_destroy(iter) unless iter.null? + end + end + end + + # Returns the shared Token type from another module + def self.cross_module_token + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_kitchen_cross_module_token(out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_token(_wv_r) + _wv_value + end + end + + # Async operation + # Blocks until the call completes on a producer thread. + def self.do_async(input) + input_s = _wv_str(input) + ctx, queue = _wv_async_begin + kitchen_sink_kitchen_do_async(input_s, input_s.bytesize, KITCHEN_SINK_KITCHEN_DO_ASYNC_CALLBACK, ctx) + _wv_async_wait(queue) + end + + # Cancellable async operation + # Blocks until the call completes on a producer thread. + # @param cancel [CancelToken, nil] cancels the call; it then raises Cancelled + def self.do_cancellable(input, cancel: nil) + input_s = _wv_str(input) + own_token = CancelToken.new if cancel.nil? + token = cancel || own_token + ctx, queue = _wv_async_begin + kitchen_sink_kitchen_do_cancellable(input_s, input_s.bytesize, token._wv_ptr, KITCHEN_SINK_KITCHEN_DO_CANCELLABLE_CALLBACK, ctx) + _wv_async_wait(queue, token) + ensure + own_token&.close + end + + # Legacy operation kept for compatibility + # @deprecated Use new_op instead + def self.legacy_op + warn '[DEPRECATED] Use new_op instead' + err = ErrorStruct.new + result = kitchen_sink_kitchen_legacy_op(err) + check_error!(err) + result + end + + # Replacement for legacy_op + def self.new_op + err = ErrorStruct.new + result = kitchen_sink_kitchen_new_op(err) + check_error!(err) + result + end + + # === Module: kitchen.nested === + + attach_function :kitchen_sink_kitchen_nested_hello, [:pointer, :pointer], :pointer, blocking: true + + # Trivial nested-module function + def self.hello + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = kitchen_sink_kitchen_nested_hello(out_len, err) + check_error!(err) + _wv_take_string(result, out_len.read(:size_t)) + end +end + +# END kitchen_sink.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_runtime_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_runtime_rb.snap new file mode 100644 index 00000000..834b8f57 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_kitchen_sink_runtime_rb.snap @@ -0,0 +1,705 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require 'ffi' +require 'monitor' + +# The fixed runtime every generated KitchenSink binding builds on: the native +# library loader, the load-time ABI check, the error classes, the +# value-buffer reader and writer, cancel tokens, the handle table behind +# callback interfaces and async completions, and the object wrapper base. +module KitchenSink + extend FFI::Library + + # An explicit path in KITCHEN_SINK_LIBRARY wins. Otherwise a library bundled + # under lib/native/ (a platform gem) is preferred over the system search + # path. + _wv_override = ENV['KITCHEN_SINK_LIBRARY'] + if _wv_override && !_wv_override.empty? + ffi_lib _wv_override + else + _wv_name = + case FFI::Platform::OS + when /darwin/ then 'libkitchen_sink.dylib' + when /mswin|mingw|windows/ then 'kitchen_sink.dll' + else 'libkitchen_sink.so' + end + _wv_bundled = File.expand_path(File.join('..', 'native', _wv_name), __dir__) + ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) + end + + # The C ABI revision these bindings speak. Checked before anything else is + # attached, so a mismatched library fails at require time instead of + # misreading an error struct or a value buffer later. + ABI_VERSION = 3 + begin + attach_function :kitchen_sink_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, "the loaded kitchen_sink library does not export kitchen_sink_abi_version " \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = kitchen_sink_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "kitchen_sink ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + + # The `kitchen_sink_error` out-parameter every call reports through. + class ErrorStruct < FFI::Struct + layout :code, :int32, + :message, :pointer, + :payload_ptr, :pointer, + :payload_len, :size_t + end + + # Codes reserved by the runtime. Positive codes belong to a module's error + # domain; these negative codes are raised as a plain Error (or Cancelled) + # regardless of whether the function declares errors. + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + # A callback-interface implementation raised; the message carries the + # exception text. + FOREIGN_ERROR_CODE = -4 + # The call was cancelled through its CancelToken. + CANCELLED_ERROR_CODE = -5 + + # The base class of every error the library raises. + class Error < StandardError + # The ABI error code: positive for a domain error, negative for a runtime + # failure. + attr_reader :code + + def initialize(code, message) + @code = code + super(message) + end + end + + # Raised when a cancellable call completes because its CancelToken fired. + class Cancelled < Error + def initialize(message = 'cancelled') + super(CANCELLED_ERROR_CODE, message) + end + end + + attach_function :kitchen_sink_error_set, [:pointer, :int32, :string], :void + attach_function :kitchen_sink_error_clear, [:pointer], :void + attach_function :kitchen_sink_error_free, [:pointer], :void + attach_function :kitchen_sink_free_bytes, [:pointer, :size_t], :void + attach_function :kitchen_sink_cancel_token_create, [], :pointer + attach_function :kitchen_sink_cancel_token_cancel, [:pointer], :void + attach_function :kitchen_sink_cancel_token_is_cancelled, [:pointer], :bool + attach_function :kitchen_sink_cancel_token_destroy, [:pointer], :void + + # @api private + # The error for a code outside any declared domain. + def self._wv_error(code, message) + return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE + + Error.new(code, message) + end + + # @api private + # Copies the code, message, and payload out of a reported error and clears + # it. Returns nil when the struct reports success. + def self._wv_take_error(err) + code = err[:code] + return nil if code.zero? + + msg_ptr = err[:message] + message = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) + payload_ptr = err[:payload_ptr] + payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) + kitchen_sink_error_clear(err.to_ptr) + [code, message, payload] + end + + # @api private + # Like _wv_take_error for the heap-boxed error an async completion + # receives; the box is released with kitchen_sink_error_free. + def self._wv_take_boxed_error(err_ptr) + return nil if err_ptr.null? + + taken = _wv_take_error(ErrorStruct.new(err_ptr)) + kitchen_sink_error_free(err_ptr) + taken + end + + # @api private + # Raises the generic error for a call that declares no error domain. + def self.check_error!(err) + taken = _wv_take_error(err) + return if taken.nil? + + raise _wv_error(taken[0], taken[1]) + end + + # @api private + # The UTF-8 bytes of a String argument, as a private binary copy the + # producer borrows for the call. + def self._wv_str(value) + value.to_s.encode(Encoding::UTF_8).b + end + + # @api private + # A private binary copy of a bytes argument. + def self._wv_bytes(value) + value.to_s.b + end + + # @api private + # Copies a borrowed (ptr, len) run without releasing it. + def self._wv_borrow_bytes(ptr, len) + ptr.null? || len.zero? ? ''.b : ptr.read_string(len) + end + + # @api private + # Copies a returned (ptr, len) allocation, then releases it. + def self._wv_take_bytes(ptr, len) + return ''.b if ptr.null? + + data = len.zero? ? ''.b : ptr.read_string(len) + kitchen_sink_free_bytes(ptr, len) + data + end + + # @api private + # Copies a returned UTF-8 string, then releases it. + def self._wv_take_string(ptr, len) + _wv_take_bytes(ptr, len).force_encoding(Encoding::UTF_8) + end + + # A cancellation signal for a cancellable async call. Pass one as the + # `cancel:` keyword, then call #cancel from any thread; the call raises + # Cancelled unless it already completed. A token may be shared by several + # calls and stays cancelled once cancelled. + class CancelToken + # @api private + # Owns the consumer's reference to the native token. + class Ptr < FFI::AutoPointer + def self.release(ptr) + KitchenSink.kitchen_sink_cancel_token_destroy(ptr) + end + end + + def initialize + @ptr = Ptr.new(KitchenSink.kitchen_sink_cancel_token_create) + end + + # Requests cancellation. Idempotent. + def cancel + KitchenSink.kitchen_sink_cancel_token_cancel(@ptr) unless @ptr.nil? + nil + end + + # Whether #cancel has been called. + def cancelled? + !@ptr.nil? && KitchenSink.kitchen_sink_cancel_token_is_cancelled(@ptr) + end + + # Releases the native token now rather than at GC time. Calls already + # launched with it keep their own reference. + def close + return if @ptr.nil? + + @ptr.free + @ptr = nil + end + + # @api private + def _wv_ptr + raise Error.new(GENERIC_ERROR_CODE, 'CancelToken used after close') if @ptr.nil? + + @ptr + end + end + + # @api private + # Appends values in the value-buffer wire format: little-endian, packed, + # no alignment. + class WvBufferWriter + def initialize + @buf = +''.b + @objects = [] + end + + # Reserves an object token. The strong reference it carries is minted + # by #seal, just before the buffer is handed to the producer, so an + # encoding abandoned on the way never strands a reference. + def write_object(object) + raise TypeError, "expected a wrapper object, got #{object.class}" unless object.is_a?(WvObject) + + @objects << [@buf.bytesize, object] + write_u64(0) + end + + # Mints the reference each reserved object token carries, recording it + # in `minted`, and returns the finished encoding. + def seal(minted) + @objects.each do |offset, object| + ptr = object._wv_clone_ptr + minted << [object.class, ptr] + @buf[offset, 8] = [ptr.address].pack('Q<') + end + @buf + end + + # Every append is a binary String, so the buffer stays binary and byte + # offsets index it directly. + def write_bool(value) + @buf << [value ? 1 : 0].pack('C') + end + + def write_flag(value) + write_bool(value) + end + + def write_i8(value) + @buf << [value].pack('c') + end + + def write_u8(value) + @buf << [value].pack('C') + end + + def write_i16(value) + @buf << [value].pack('s<') + end + + def write_u16(value) + @buf << [value].pack('S<') + end + + def write_i32(value) + @buf << [value].pack('l<') + end + + def write_u32(value) + @buf << [value].pack('L<') + end + + def write_len(value) + write_u32(value) + end + + def write_i64(value) + @buf << [value].pack('q<') + end + + def write_u64(value) + @buf << [value].pack('Q<') + end + + def write_f32(value) + @buf << [value].pack('e') + end + + def write_f64(value) + @buf << [value].pack('E') + end + + def write_string(value) + write_bytes(value.to_s.encode(Encoding::UTF_8)) + end + + def write_bytes(value) + bytes = value.to_s.b + write_u32(bytes.bytesize) + @buf << bytes + end + end + + # @api private + # Reads values in the 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(count, what) + if @pos + count > @data.bytesize + raise Error.new(MARSHAL_ERROR_CODE, "malformed value buffer: truncated #{what}") + end + + bytes = @data.byteslice(@pos, count) + @pos += count + bytes + end + + def read_bool + byte = take(1, 'bool').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: bool byte out of range') if byte > 1 + + byte == 1 + end + + def read_flag + byte = take(1, 'option flag').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: option flag out of range') if byte > 1 + + byte == 1 + end + + def read_i8 + take(1, 'i8').unpack1('c') + end + + def read_u8 + take(1, 'u8').unpack1('C') + end + + def read_i16 + take(2, 'i16').unpack1('s<') + end + + def read_u16 + take(2, 'u16').unpack1('S<') + end + + def read_i32 + take(4, 'i32').unpack1('l<') + end + + def read_u32 + take(4, 'u32').unpack1('L<') + end + + # A u32 element count. Counts are not checked against the remaining + # bytes, since an element can encode to zero bytes; a short buffer still + # fails on the first truncated read. + def read_len + read_u32 + end + + def read_i64 + take(8, 'i64').unpack1('q<') + end + + def read_u64 + take(8, 'u64').unpack1('Q<') + end + + def read_f32 + take(4, 'f32').unpack1('e') + end + + def read_f64 + take(8, 'f64').unpack1('E') + end + + def read_string + text = read_bytes.force_encoding(Encoding::UTF_8) + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: string is not valid UTF-8') unless text.valid_encoding? + + text + end + + def read_bytes + take(read_u32, 'byte run') + end + + # An object token: a non-zero u64 carrying one strong reference the + # caller adopts into a wrapper. + def read_object_token + addr = read_u64 + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: null object token') if addr.zero? + + FFI::Pointer.new(addr) + end + + def expect_end! + return if @pos == @data.bytesize + + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: trailing bytes after value') + end + end + + # @api private + # Seals the writers of one call's object-carrying buffers (see + # WvBufferWriter#seal) and returns their encodings. If any reference can't + # be minted (a closed wrapper), the ones already minted are released before + # the error propagates. The producer adopts every minted reference once the + # call is made. + def self._wv_seal(*writers) + minted = [] + writers.map { |writer| writer.seal(minted) } + rescue Exception # rubocop:disable Lint/RescueException + minted.each { |cls, ptr| cls::WV_PTR.release(ptr) } + raise + end + + # @api private + # Decodes a returned value buffer: copies and releases it, yields a reader + # to the block, and checks that the block consumed every byte. + def self._wv_decode(ptr, len) + reader = WvBufferReader.new(_wv_take_bytes(ptr, len)) + value = yield reader + reader.expect_end! + value + end + + # Ruby objects the producer refers to by an integer key: callback-interface + # implementations (the key is the vtable `ctx`) and pending async + # completions (the key is the completion `context`). The producer never + # sees a Ruby object address. A Monitor rather than a Mutex, so a + # finalizer that runs while this thread holds the lock can't deadlock it. + @wv_handles = {} + @wv_next_handle = 0 + @wv_handles_lock = Monitor.new + + # @api private + def self._wv_handle_put(value) + @wv_handles_lock.synchronize do + @wv_next_handle += 1 + @wv_handles[@wv_next_handle] = value + FFI::Pointer.new(@wv_next_handle) + end + end + + # @api private + def self._wv_handle_get(key) + @wv_handles_lock.synchronize { @wv_handles[key.address] } + end + + # @api private + def self._wv_handle_take(key) + @wv_handles_lock.synchronize { @wv_handles.delete(key.address) } + end + + # @api private + # Registers a callback-interface implementation and returns its `ctx`. + # Never raises, so nothing can strand the registration between this and + # the call that hands it to the producer (an unusable implementation fails + # when the producer calls it, with FOREIGN_ERROR_CODE). + def self._wv_cb_register(impl) + _wv_handle_put([:callback, impl]) + end + + # @api private + # Resolves a trampoline's `ctx` back to the registered implementation. + def self._wv_cb_lookup(ctx) + kind, impl = _wv_handle_get(ctx) + raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') unless kind == :callback + + impl + end + + # @api private + # Drops an implementation when the producer releases its last reference. + def self._wv_cb_free(ctx) + _wv_handle_take(ctx) + nil + end + + # @api private + # Reports a callback implementation's exception through `out_err`. The + # producer copies the message. + def self._wv_cb_fail(out_err, exception) + message = exception.message.to_s + message = exception.class.name if message.empty? + message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).delete("\0") + kitchen_sink_error_set(out_err, FOREIGN_ERROR_CODE, message) + nil + end + + # @api private + # Registers a pending async call; returns its completion context and the + # queue its completion is delivered on. + def self._wv_async_begin + queue = Queue.new + [_wv_handle_put([:async, queue]), queue] + end + + # @api private + # The queue of the pending call a completion belongs to. Each context + # completes exactly once, so its entry is removed here. + def self._wv_async_finish(ctx) + kind, queue = _wv_handle_take(ctx) + kind == :async ? queue : Queue.new + end + + # @api private + # Blocks until a pending call completes, raising its error. If the wait is + # interrupted (Thread#raise, Thread#kill, Timeout), the call's token is + # cancelled before the interruption propagates. + def self._wv_async_wait(queue, token = nil) + value = + begin + queue.pop + rescue Exception # rubocop:disable Lint/RescueException + token&.cancel + raise + end + raise value if value.is_a?(Exception) + + value + end + + # @api private + # The base of every interface wrapper. A wrapper owns one strong reference + # to its native object and releases it exactly once, from #close or (as a + # backstop) from the GC finalizer of its FFI::AutoPointer. Calls borrow the + # pointer through #_wv_call, so a #close that races an in-flight call on + # another thread only releases the reference once the call returns. + class WvObject + # @api private + # Adopts one strong reference the producer handed over. + def self._from_ptr(ptr) + obj = allocate + obj._wv_init(ptr) + obj + end + + # @api private + def _wv_init(ptr) + @wv_handle = self.class::WV_PTR.new(ptr) + @wv_lock = Mutex.new + @wv_calls = 0 + @wv_closing = false + end + + # The native object pointer (borrowed; valid until #close). + def handle + handle = @wv_handle + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if handle.nil? || @wv_closing + + handle + end + + # Whether #close has released this wrapper's reference. + def closed? + @wv_handle.nil? || @wv_closing + end + + # Releases this wrapper's reference now rather than at GC time. + # Idempotent. The object itself lives until its last reference anywhere + # (another wrapper, a record field, the producer) goes. + def close + handle = @wv_lock.synchronize do + next nil if @wv_handle.nil? || @wv_closing + + if @wv_calls.zero? + taken = @wv_handle + @wv_handle = nil + taken + else + @wv_closing = true + nil + end + end + handle&.free + nil + end + + # @api private + # Yields the native pointer, keeping the reference alive until the block + # returns. + def _wv_call + handle = @wv_lock.synchronize do + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if @wv_handle.nil? || @wv_closing + + @wv_calls += 1 + @wv_handle + end + begin + yield handle + ensure + _wv_leave + end + end + + # @api private + # Mints a new strong reference the caller owns (an object written into a + # value buffer carries its own reference). + def _wv_clone_ptr + _wv_call { |ptr| self.class._wv_clone(ptr) } + end + + # dup and clone produce an independent wrapper with its own reference to + # the same object. + def initialize_copy(other) + super + _wv_init(other._wv_clone_ptr) + end + + private + + def _wv_leave + release = @wv_lock.synchronize do + @wv_calls -= 1 + next nil unless @wv_closing && @wv_calls.zero? + + taken = @wv_handle + @wv_handle = nil + @wv_closing = false + taken + end + release&.free + end + end + + # @api private + # Yields the native pointers of `objects` (NULL for nil), each kept alive + # until the block returns. + def self._wv_pin(*objects, &block) + _wv_pin_from(objects, 0, [], &block) + end + + # @api private + def self._wv_pin_from(objects, index, pointers, &block) + return yield(*pointers) if index == objects.length + + object = objects[index] + if object.nil? + _wv_pin_from(objects, index + 1, pointers + [FFI::Pointer::NULL], &block) + else + object._wv_call { |ptr| _wv_pin_from(objects, index + 1, pointers + [ptr], &block) } + end + end + + # @api private + # Checks each top-level module's contract checksum (`module name => + # [checksum function, expected value]`) against the library, so bindings + # generated from a different API fail at require time. + def self._wv_check_contract!(checksums) + checksums.each do |mod, (symbol, expected)| + begin + attach_function symbol, [], :uint64 + rescue FFI::NotFoundError + raise LoadError, "the loaded kitchen_sink library has no module '#{mod}' (#{symbol} is missing)" + end + actual = public_send(symbol) + next if actual == expected + + raise LoadError, "kitchen_sink module '#{mod}' does not match these bindings: expected contract " \ + "checksum 0x#{expected.to_s(16)}, the library reports 0x#{actual.to_s(16)}; " \ + 'regenerate the bindings' + end + end + + # @api private + # Live native resources of one kind (0 objects, 1 callbacks, 2 iterators, + # 3 cancel tokens, 4 byte allocations). Only libraries built with the + # `leak-check` feature export the counter. + def self._wv_debug_live(kind) + unless respond_to?(:kitchen_sink_debug_live) + begin + attach_function :kitchen_sink_debug_live, [:int32], :uint64 + rescue FFI::NotFoundError + raise Error.new(GENERIC_ERROR_CODE, 'kitchen_sink was built without the leak-check feature') + end + end + kitchen_sink_debug_live(kind) + end +end + +# END runtime.rb 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 deleted file mode 100644 index 4d0bd359..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap +++ /dev/null @@ -1,1092 +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 - - # 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 = 2 - 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 - - # The runtime-reserved error codes. Positive codes belong to a module's - # declared error domain; these negative codes are programming errors the - # wrappers raise as a plain Error regardless of the function's `throws`. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - - 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 - - # An object token: a non-zero u64 carrying one strong reference the - # caller adopts into a wrapper. - def read_object_token - addr = read_u64 - raise Error.new(-1, 'malformed value buffer: null object token') if addr.zero? - FFI::Pointer.new(addr) - 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_set, [:pointer, :int32, :string], :void - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # Live callback-interface implementations keyed by the Integer the producer - # holds as `ctx`. Keys start at 1 so a context pointer is never NULL. - @wv_cb_registry = {} - @wv_cb_next_key = 0 - @wv_cb_mutex = Mutex.new - - # @api private - # Registers `impl` and returns the `ctx` pointer to hand the producer. - def self._wv_cb_register(impl) - @wv_cb_mutex.synchronize do - @wv_cb_next_key += 1 - @wv_cb_registry[@wv_cb_next_key] = impl - FFI::Pointer.new(@wv_cb_next_key) - end - end - - # @api private - # Resolves a trampoline's `ctx` back to the registered implementation. - def self._wv_cb_lookup(ctx) - impl = @wv_cb_mutex.synchronize { @wv_cb_registry[ctx.address] } - raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') if impl.nil? - impl - end - - # @api private - # Drops the registry entry when the producer releases its last reference. - def self._wv_cb_free(ctx) - @wv_cb_mutex.synchronize { @wv_cb_registry.delete(ctx.address) } - nil - end - - # @api private - # Reports a callback implementation's exception through `out_err`. The - # producer copies the message, so the Ruby String need not outlive the call. - def self._wv_cb_fail(out_err, exception) - message = exception.message.to_s - message = exception.class.name if message.empty? - weaveffi_error_set(out_err, FOREIGN_ERROR_CODE, message) - nil - end - - # === Module: shared === - - # A reusable token referenced across modules - class Token - attr_reader :id - - attr_reader :label - - def initialize(id:, label:) - @id = id - @label = label - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Token) - return false unless id == other.id - return false unless label == other.label - true - end - end - - # @api private - # Packs a Token into the value-buffer wire format. - def self._wv_write_token(w, v) - w.write_i64(v.id) - w.write_string(v.label) - end - - # @api private - # Unpacks a Token from the value-buffer wire format. - def self._wv_read_token(r) - _wv_id = r.read_i64 - _wv_label = r.read_string - Token.new(id: _wv_id, label: _wv_label) - end - # Trivial cross-module helper - attach_function :weaveffi_shared_ping, [:pointer], :pointer - - # Trivial cross-module helper - def self.ping() - err = ErrorStruct.new - result = weaveffi_shared_ping(err) - check_error!(err) - return '' if result.null? - str = result.read_string.force_encoding(Encoding::UTF_8) - weaveffi_free_string(result) - str - end - - # === Module: kitchen === - - # Base error for the `kitchen` module's error domain. - class KitchenErrorsError < Error - # Item not found - class NotFound < KitchenErrorsError - CODE = 1 - - def initialize(message = 'Item not found') - super(1, message) - end - end - - # Invalid input - class InvalidInput < KitchenErrorsError - CODE = 2 - - def initialize(message = 'Invalid input') - super(2, message) - end - end - end - - # Maps each ABI code of the KitchenErrorsError domain to its error class. - KITCHEN_ERRORS_ERROR_CODES = { - 1 => KitchenErrorsError::NotFound, - 2 => KitchenErrorsError::InvalidInput, - }.freeze - - # Builds the KitchenErrorsError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling, callback failures). - def self.kitchen_errors_error_from(code, message, payload = nil) - cls = KITCHEN_ERRORS_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed KitchenErrorsError for a non-zero error slot. - def self.check_kitchen_errors_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise kitchen_errors_error_from(code, msg, payload) - end - - # Task priority levels - module Priority - # Low priority - LOW = 0 - # Normal priority - NORMAL = 1 - # High priority - HIGH = 2 - end - - # Kitchen sink struct exercising every field feature - class Item - # Stable identifier - attr_reader :id - - # Display name - attr_reader :name - - # Initial count - attr_reader :count - - # Whether the item is enabled - attr_reader :enabled - - # Scaling ratio - attr_reader :ratio - - attr_reader :nick - - attr_reader :payload - - attr_reader :tags - - attr_reader :attrs - - attr_reader :parent - - # Cross-module struct reference - attr_reader :token - - attr_reader :priority - - # An optional object inside a record - attr_reader :gadget - - def initialize(id:, name:, count:, enabled:, ratio:, nick:, payload:, tags:, attrs:, parent:, token:, priority:, gadget:) - @id = id - @name = name - @count = count - @enabled = enabled - @ratio = ratio - @nick = nick - @payload = payload - @tags = tags - @attrs = attrs - @parent = parent - @token = token - @priority = priority - @gadget = gadget - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Item) - return false unless id == other.id - return false unless name == other.name - return false unless count == other.count - return false unless enabled == other.enabled - return false unless ratio == other.ratio - return false unless nick == other.nick - return false unless payload == other.payload - return false unless tags == other.tags - return false unless attrs == other.attrs - return false unless parent == other.parent - return false unless token == other.token - return false unless priority == other.priority - return false unless gadget == other.gadget - true - end - end - - # @api private - # Packs a Item into the value-buffer wire format. - def self._wv_write_item(w, v) - w.write_i64(v.id) - w.write_string(v.name) - w.write_i32(v.count) - w.write_bool(v.enabled) - w.write_f64(v.ratio) - w.write_u32(v.nick) - w.write_bytes(v.payload) - w.write_len(v.tags.length) - v.tags.each do |_wv_e0| - w.write_string(_wv_e0) - end - w.write_len(v.attrs.length) - v.attrs.each do |_wv_k0, _wv_v0| - w.write_string(_wv_k0) - w.write_string(_wv_v0) - end - if v.parent.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_i64(v.parent) - end - _wv_write_token(w, v.token) - w.write_i32(v.priority) - if v.gadget.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_u64(v.gadget._wv_clone_ptr.address) - end - end - - # @api private - # Unpacks a Item from the value-buffer wire format. - def self._wv_read_item(r) - _wv_id = r.read_i64 - _wv_name = r.read_string - _wv_count = r.read_i32 - _wv_enabled = r.read_bool - _wv_ratio = r.read_f64 - _wv_nick = r.read_u32 - _wv_payload = r.read_bytes - _wv_tags = Array.new(r.read_len) do - _wv_e0 = r.read_string - _wv_e0 - end - _wv_attrs = {} - r.read_len.times do - _wv_k0 = r.read_string - _wv_v0 = r.read_string - _wv_attrs[_wv_k0] = _wv_v0 - end - if r.read_flag - _wv_parent = r.read_i64 - else - _wv_parent = nil - end - _wv_token = _wv_read_token(r) - _wv_priority = r.read_i32 - if r.read_flag - _wv_gadget = Gadget._from_ptr(r.read_object_token) - else - _wv_gadget = nil - end - Item.new(id: _wv_id, name: _wv_name, count: _wv_count, enabled: _wv_enabled, ratio: _wv_ratio, nick: _wv_nick, payload: _wv_payload, tags: _wv_tags, attrs: _wv_attrs, parent: _wv_parent, token: _wv_token, priority: _wv_priority, gadget: _wv_gadget) - end - - attach_function :weaveffi_kitchen_Gadget_clone, [:pointer], :pointer - attach_function :weaveffi_kitchen_Gadget_destroy, [:pointer], :void - # Create a gadget with the given id - attach_function :weaveffi_kitchen_Gadget_new, [:int64, :pointer], :pointer - # Render the gadget as a human-readable string - attach_function :weaveffi_kitchen_Gadget_describe, [:pointer, :pointer], :pointer - # Poke the gadget a number of times, failing on a negative count - attach_function :weaveffi_kitchen_Gadget_poke, [:pointer, :int32, :pointer], :int32 - # The gadget subsystem version string - attach_function :weaveffi_kitchen_Gadget_version, [:pointer], :pointer - # Identity for bool - attach_function :weaveffi_kitchen_bool_id, [:int32, :pointer], :int32 - attach_function :weaveffi_kitchen_i32_id, [:int32, :pointer], :int32 - attach_function :weaveffi_kitchen_u32_id, [:uint32, :pointer], :uint32 - attach_function :weaveffi_kitchen_i64_id, [:int64, :pointer], :int64 - attach_function :weaveffi_kitchen_f64_id, [:double, :pointer], :double - attach_function :weaveffi_kitchen_echo_string, [:string, :pointer], :pointer - attach_function :weaveffi_kitchen_echo_bytes, [:pointer, :size_t, :pointer, :pointer], :pointer - # Register a listener, returning how many are attached - attach_function :weaveffi_kitchen_subscribe, [:pointer, :pointer, :pointer], :int32 - # Optional object in both directions - attach_function :weaveffi_kitchen_maybe_gadget, [:pointer, :pointer], :pointer - # A list of objects - attach_function :weaveffi_kitchen_gadgets, [:pointer, :pointer], :pointer - # An iterator over objects - attach_function :weaveffi_kitchen_stream_gadgets, [:pointer], :pointer - attach_function :weaveffi_kitchen_StreamGadgetsIterator_next, [:pointer, :pointer, :pointer], :int32 - attach_function :weaveffi_kitchen_StreamGadgetsIterator_destroy, [:pointer], :void - # Optional struct return - attach_function :weaveffi_kitchen_maybe_item, [:int64, :pointer, :pointer], :pointer - # List of structs - attach_function :weaveffi_kitchen_list_items, [:pointer, :pointer], :pointer - # Map return type - attach_function :weaveffi_kitchen_get_attrs, [:pointer, :pointer], :pointer - # Iterator return type - attach_function :weaveffi_kitchen_stream_items, [:pointer], :pointer - attach_function :weaveffi_kitchen_StreamItemsIterator_next, [:pointer, :pointer, :pointer, :pointer], :int32 - attach_function :weaveffi_kitchen_StreamItemsIterator_destroy, [:pointer], :void - # Returns the shared Token type from another module - attach_function :weaveffi_kitchen_cross_module_token, [:pointer, :pointer], :pointer - # Async operation - callback :weaveffi_kitchen_do_async_callback, [:pointer, :pointer, :pointer], :void - attach_function :weaveffi_kitchen_do_async_async, [:string, :weaveffi_kitchen_do_async_callback, :pointer], :void - # Cancellable async operation - callback :weaveffi_kitchen_do_cancellable_callback, [:pointer, :pointer, :pointer], :void - attach_function :weaveffi_kitchen_do_cancellable_async, [:string, :pointer, :weaveffi_kitchen_do_cancellable_callback, :pointer], :void - # Legacy operation kept for compatibility - attach_function :weaveffi_kitchen_legacy_op, [:pointer], :int32 - # Replacement for legacy_op - attach_function :weaveffi_kitchen_new_op, [:pointer], :int32 - - # Consumer-implemented observer of ready items - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a ReadyListener parameter is expected; - # include this module to inherit NotImplementedError defaults. The - # producer may call the methods from any thread until it releases the - # implementation. - module ReadyListener - # Fires when an item is ready - def on_ready(code, msg) - raise NotImplementedError, "#{self.class}#on_ready is not implemented" - end - - # Receives the item itself and says whether to keep listening - # @return [Object] a bool - def on_item(item, gadget) - raise NotImplementedError, "#{self.class}#on_item is not implemented" - end - end - - # @api private - # The C vtable layout the producer calls a ReadyListener through. - class WvReadyListenerVtable < FFI::Struct - layout :on_ready, :pointer, - :on_item, :pointer, - :free, :pointer - end - - # @api private - # Trampoline for ReadyListener#on_ready. - WV_READY_LISTENER_ON_READY = FFI::Function.new(:void, [:pointer, :int32, :string, :pointer]) do |ctx, code, msg, out_err| - begin - impl = _wv_cb_lookup(ctx) - code_v = code - msg_v = msg.nil? ? '' : msg.force_encoding(Encoding::UTF_8) - impl.on_ready(code_v, msg_v) - nil - rescue Exception => e - _wv_cb_fail(out_err, e) - nil - end - end - - # @api private - # Trampoline for ReadyListener#on_item. - WV_READY_LISTENER_ON_ITEM = FFI::Function.new(:int32, [:pointer, :pointer, :size_t, :pointer, :pointer]) do |ctx, item_ptr, item_len, gadget, out_err| - begin - impl = _wv_cb_lookup(ctx) - item_r = WvBufferReader.new(item_ptr.null? ? ''.b : item_ptr.read_string(item_len)) - item_v = _wv_read_item(item_r) - item_r.expect_end! - gadget_v = Gadget._from_ptr(gadget) - impl.on_item(item_v, gadget_v) ? 1 : 0 - rescue Exception => e - _wv_cb_fail(out_err, e) - 0 - end - end - - # @api private - # Releases a ReadyListener implementation when the producer drops its last - # reference. - WV_READY_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end - - # The one process-wide ReadyListener vtable every registration hands the - # producer; its entries live for the process lifetime. - WV_READY_LISTENER_VTABLE = WvReadyListenerVtable.new - WV_READY_LISTENER_VTABLE[:on_ready] = WV_READY_LISTENER_ON_READY - WV_READY_LISTENER_VTABLE[:on_item] = WV_READY_LISTENER_ON_ITEM - WV_READY_LISTENER_VTABLE[:free] = WV_READY_LISTENER_FREE - - # @api private - # Owns one strong reference to a Gadget; releases it exactly once. - class GadgetPtr < FFI::AutoPointer - def self.release(ptr) - WeaveFFI.weaveffi_kitchen_Gadget_destroy(ptr) - end - end - - # A pocket-sized interface exercising the object surface - class Gadget - # @api private - # Adopts one strong reference the producer handed over, without - # re-running initialize. - def self._from_ptr(ptr) - obj = allocate - obj.instance_variable_set(:@handle, GadgetPtr.new(ptr)) - obj - end - - # The borrowed object pointer passed to producer calls. - def handle - raise Error.new(-1, 'Gadget used after close') if @handle.nil? - @handle - end - - # Whether close has released this wrapper's reference. - def closed? - @handle.nil? - end - - # Releases this wrapper's reference now rather than at GC time. - # Idempotent; the object itself is dropped when the last reference - # anywhere (another wrapper, a record field, the producer) goes. - def close - return if @handle.nil? - @handle.free - @handle = nil - end - - # @api private - # Mints a new strong reference the caller owns (used when this - # object is written into a value buffer). - def _wv_clone_ptr - WeaveFFI.weaveffi_kitchen_Gadget_clone(handle) - end - - # dup and clone produce an independent wrapper with its own reference. - def initialize_copy(other) - super - @handle = GadgetPtr.new(WeaveFFI.weaveffi_kitchen_Gadget_clone(other.handle)) - end - - # Create a gadget with the given id - def initialize(id) - err = ErrorStruct.new - result = WeaveFFI.weaveffi_kitchen_Gadget_new(id, err) - WeaveFFI.check_error!(err) - raise Error.new(-1, 'null pointer') if result.null? - @handle = GadgetPtr.new(result) - end - - # Render the gadget as a human-readable string - def describe() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_kitchen_Gadget_describe(handle, err) - WeaveFFI.check_error!(err) - return '' if result.null? - str = result.read_string.force_encoding(Encoding::UTF_8) - WeaveFFI.weaveffi_free_string(result) - str - end - - # Poke the gadget a number of times, failing on a negative count - def poke(times) - err = ErrorStruct.new - result = WeaveFFI.weaveffi_kitchen_Gadget_poke(handle, times, err) - WeaveFFI.check_kitchen_errors_error!(err) - result - end - - # The gadget subsystem version string - def self.version() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_kitchen_Gadget_version(err) - WeaveFFI.check_error!(err) - return '' if result.null? - str = result.read_string.force_encoding(Encoding::UTF_8) - WeaveFFI.weaveffi_free_string(result) - str - end - end - - # Identity for bool - def self.bool_id(x) - err = ErrorStruct.new - x_c = x ? 1 : 0 - result = weaveffi_kitchen_bool_id(x_c, err) - check_error!(err) - (result != 0) - end - - def self.i32_id(x) - err = ErrorStruct.new - result = weaveffi_kitchen_i32_id(x, err) - check_error!(err) - result - end - - def self.u32_id(x) - err = ErrorStruct.new - result = weaveffi_kitchen_u32_id(x, err) - check_error!(err) - result - end - - def self.i64_id(x) - err = ErrorStruct.new - result = weaveffi_kitchen_i64_id(x, err) - check_error!(err) - result - end - - def self.f64_id(x) - err = ErrorStruct.new - result = weaveffi_kitchen_f64_id(x, err) - check_error!(err) - result - end - - def self.echo_string(s) - err = ErrorStruct.new - result = weaveffi_kitchen_echo_string(s, err) - check_error!(err) - return '' if result.null? - str = result.read_string.force_encoding(Encoding::UTF_8) - weaveffi_free_string(result) - str - end - - def self.echo_bytes(b) - err = ErrorStruct.new - b_buf = FFI::MemoryPointer.new(:uint8, b.bytesize) - b_buf.put_bytes(0, b) - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_kitchen_echo_bytes(b_buf, b.bytesize, out_len, err) - check_error!(err) - return ''.b if result.null? - len = out_len.read(:size_t) - data = result.read_string(len) - weaveffi_free_bytes(result, len) - data - end - - # Register a listener, returning how many are attached - def self.subscribe(listener) - err = ErrorStruct.new - listener_ctx = _wv_cb_register(listener) - result = weaveffi_kitchen_subscribe(listener_ctx, WV_READY_LISTENER_VTABLE.to_ptr, err) - check_error!(err) - result - end - - # Optional object in both directions - def self.maybe_gadget(fallback) - err = ErrorStruct.new - result = weaveffi_kitchen_maybe_gadget(fallback&.handle, err) - check_error!(err) - return nil if result.null? - Gadget._from_ptr(result) - end - - # A list of objects - def self.gadgets() - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_kitchen_gadgets(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 = Array.new(_wv_r.read_len) do - _wv_e0 = Gadget._from_ptr(_wv_r.read_object_token) - _wv_e0 - end - _wv_r.expect_end! - _wv_value - end - - # An iterator over objects - # 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.stream_gadgets() - Enumerator.new do |y| - err = ErrorStruct.new - iter = weaveffi_kitchen_stream_gadgets(err) - begin - check_error!(err) - unless iter.null? - loop do - out_item = FFI::MemoryPointer.new(:pointer) - item_err = ErrorStruct.new - has_item = weaveffi_kitchen_StreamGadgetsIterator_next(iter, out_item, item_err) - check_error!(item_err) - break if has_item.zero? - item_ptr = out_item.read_pointer - raise Error.new(-1, 'null pointer') if item_ptr.null? - y << Gadget._from_ptr(item_ptr) - end - end - ensure - weaveffi_kitchen_StreamGadgetsIterator_destroy(iter) unless iter.null? - end - end - end - - # Optional struct return - def self.maybe_item(id) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_kitchen_maybe_item(id, out_len, err) - check_kitchen_errors_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_read_item(_wv_r) - else - _wv_value = nil - end - _wv_r.expect_end! - _wv_value - end - - # List of structs - def self.list_items() - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_kitchen_list_items(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 = Array.new(_wv_r.read_len) do - _wv_e0 = _wv_read_item(_wv_r) - _wv_e0 - end - _wv_r.expect_end! - _wv_value - end - - # Map return type - def self.get_attrs() - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_kitchen_get_attrs(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_r.read_len.times do - _wv_k0 = _wv_r.read_string - _wv_v0 = _wv_r.read_i32 - _wv_value[_wv_k0] = _wv_v0 - end - _wv_r.expect_end! - _wv_value - end - - # Iterator return type - # 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.stream_items() - Enumerator.new do |y| - err = ErrorStruct.new - iter = weaveffi_kitchen_stream_items(err) - begin - check_error!(err) - unless iter.null? - loop do - out_item = FFI::MemoryPointer.new(:pointer) - out_item_len = FFI::MemoryPointer.new(:size_t) - item_err = ErrorStruct.new - has_item = weaveffi_kitchen_StreamItemsIterator_next(iter, out_item, out_item_len, item_err) - check_error!(item_err) - break if has_item.zero? - item_ptr = out_item.read_pointer - item_len = out_item_len.read(:size_t) - _wv_data = item_ptr.null? ? ''.b : item_ptr.read_string(item_len) - weaveffi_free_bytes(item_ptr, item_len) unless item_ptr.null? - _wv_r = WvBufferReader.new(_wv_data) - _wv_item = _wv_read_item(_wv_r) - _wv_r.expect_end! - y << _wv_item - end - end - ensure - weaveffi_kitchen_StreamItemsIterator_destroy(iter) unless iter.null? - end - end - end - - # Returns the shared Token type from another module - def self.cross_module_token() - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_kitchen_cross_module_token(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_token(_wv_r) - _wv_r.expect_end! - _wv_value - end - - # Async operation - # Blocks the current thread until the async producer completes; the - # result (or error) is delivered through the completion callback. - def self.do_async(input) - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_free(err_ptr) - queue << Error.new(code, msg) - else - _wv_s = result.null? ? '' : result.read_string.force_encoding(Encoding::UTF_8) - weaveffi_free_string(result) unless result.null? - queue << _wv_s - end - end - weaveffi_kitchen_do_async_async(input, callback, FFI::Pointer::NULL) - value = queue.pop - raise value if value.is_a?(Error) - value - end - - # Cancellable async operation - # 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.do_cancellable(input) - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_free(err_ptr) - queue << Error.new(code, msg) - else - _wv_s = result.null? ? '' : result.read_string.force_encoding(Encoding::UTF_8) - weaveffi_free_string(result) unless result.null? - queue << _wv_s - end - end - weaveffi_kitchen_do_cancellable_async(input, FFI::Pointer::NULL, callback, FFI::Pointer::NULL) - value = queue.pop - raise value if value.is_a?(Error) - value - end - - # Legacy operation kept for compatibility - def self.legacy_op() - warn "[DEPRECATED] Use new_op instead" - err = ErrorStruct.new - result = weaveffi_kitchen_legacy_op(err) - check_error!(err) - result - end - - # Replacement for legacy_op - def self.new_op() - err = ErrorStruct.new - result = weaveffi_kitchen_new_op(err) - check_error!(err) - result - end - - # === Module: kitchen_nested === - # Trivial nested-module function - attach_function :weaveffi_kitchen_nested_hello, [:pointer], :pointer - - # Trivial nested-module function - def self.hello() - err = ErrorStruct.new - result = weaveffi_kitchen_nested_hello(err) - check_error!(err) - return '' if result.null? - str = result.read_string.force_encoding(Encoding::UTF_8) - weaveffi_free_string(result) - str - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__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__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__README_md.snap index dfa27948..35016398 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__README_md.snap @@ -1,30 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Ruby) +# nested_modules (Ruby) -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. +Ruby bindings for `nested_modules` 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. +- Ruby 2.7 or newer +- The native library (`libnested_modules.so`, `libnested_modules.dylib`, or `nested_modules.dll`) on the library + search path, or its path in the `NESTED_MODULES_LIBRARY` environment variable. ## Install ```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem +gem build nested_modules.gemspec +gem install nested_modules-0.1.0.gem ``` ## Usage ```ruby -require 'weaveffi' +require 'nested_modules' + +NestedModules::ABI_VERSION ``` diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_rb.snap new file mode 100644 index 00000000..077d5ac6 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_rb.snap @@ -0,0 +1,231 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require_relative 'nested_modules/runtime' + +# Ruby bindings for nested_modules. +module NestedModules + # Contract checksums of the top-level modules these bindings were + # generated from; a library built from a different API fails to load. + _wv_check_contract!( + 'graphics' => [:nested_modules_graphics_checksum, 0x44d31b5b10fdb90d], + ) + + # === Module: graphics === + + # Base error for the `graphics` module's error domain. + class GraphicsError < Error + # coordinate out of bounds + class OutOfBounds < GraphicsError + CODE = 1 + + def initialize(message = 'coordinate out of bounds') + super(1, message) + end + end + end + + # Maps each ABI code of the GraphicsError domain to its error class. + GRAPHICS_ERROR_CODES = { + 1 => GraphicsError::OutOfBounds, + }.freeze + + # Builds the GraphicsError subclass matching `code`, decoding any payload + # fields the code declares, or a generic Error for codes outside + # the domain (panics, marshalling, callback failures). + def self.graphics_error_from(code, message, payload = nil) + cls = GRAPHICS_ERROR_CODES[code] + return _wv_error(code, message) if cls.nil? + message.empty? ? cls.new : cls.new(message) + end + + # Raises the typed GraphicsError for a non-zero error slot. + def self.check_graphics_error!(err) + taken = _wv_take_error(err) + raise graphics_error_from(*taken) if taken + end + + # A measurement unit shared with nested submodules. + module Unit + PIXELS = 0 + POINTS = 1 + end + + # Top-level struct that references a struct defined one level deeper. + class Canvas + attr_reader :width + + # Reference down into a nested module + attr_reader :root + + def initialize(width:, root:) + @width = width + @root = root + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(Canvas) + return false unless width == other.width + return false unless root == other.root + true + end + end + + # @api private + # Packs a Canvas into the value-buffer wire format. + def self._wv_write_canvas(w, v) + w.write_i32(v.width) + _wv_write_shape(w, v.root) + end + + # @api private + # Unpacks a Canvas from the value-buffer wire format. + def self._wv_read_canvas(r) + _wv_width = r.read_i32 + _wv_root = _wv_read_shape(r) + Canvas.new(width: _wv_width, root: _wv_root) + end + + attach_function :nested_modules_graphics_new_canvas, [:int32, :pointer, :pointer], :pointer + + # Construct a Canvas. + def self.new_canvas(width) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = nested_modules_graphics_new_canvas(width, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_canvas(_wv_r) + _wv_value + end + end + + # === Module: graphics.shapes === + + # A struct one level deep that references both an ancestor enum and a deeper struct. + class Shape + attr_reader :sides + + # Reference up to the parent module's enum + attr_reader :unit + + # Reference down into a deeper module + attr_reader :detail + + def initialize(sides:, unit:, detail:) + @sides = sides + @unit = unit + @detail = detail + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(Shape) + return false unless sides == other.sides + return false unless unit == other.unit + return false unless detail == other.detail + true + end + end + + # @api private + # Packs a Shape into the value-buffer wire format. + def self._wv_write_shape(w, v) + w.write_i32(v.sides) + w.write_i32(v.unit) + _wv_write_detail(w, v.detail) + end + + # @api private + # Unpacks a Shape from the value-buffer wire format. + def self._wv_read_shape(r) + _wv_sides = r.read_i32 + _wv_unit = r.read_i32 + _wv_detail = _wv_read_detail(r) + Shape.new(sides: _wv_sides, unit: _wv_unit, detail: _wv_detail) + end + + attach_function :nested_modules_graphics_shapes_make_shape, [:int32, :pointer, :pointer], :pointer + attach_function :nested_modules_graphics_shapes_describe, [:pointer, :pointer], :pointer + + # Construct a Shape. + def self.make_shape(sides) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = nested_modules_graphics_shapes_make_shape(sides, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_shape(_wv_r) + _wv_value + end + end + + # Nested function returning a struct owned by an ancestor module. + def self.describe + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = nested_modules_graphics_shapes_describe(out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_canvas(_wv_r) + _wv_value + end + end + + # === Module: graphics.shapes.geometry === + + # A struct two levels deep. + class Detail + attr_reader :area + + def initialize(area:) + @area = area + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(Detail) + return false unless area == other.area + true + end + end + + # @api private + # Packs a Detail into the value-buffer wire format. + def self._wv_write_detail(w, v) + w.write_f64(v.area) + end + + # @api private + # Unpacks a Detail from the value-buffer wire format. + def self._wv_read_detail(r) + _wv_area = r.read_f64 + Detail.new(area: _wv_area) + end + + attach_function :nested_modules_graphics_shapes_geometry_compute, [:pointer, :size_t, :pointer, :pointer], :pointer + + # Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. + def self.compute(shape) + shape_w = WvBufferWriter.new + _wv_write_shape(shape_w, shape) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + shape_s = _wv_seal(shape_w).first + result = nested_modules_graphics_shapes_geometry_compute(shape_s, shape_s.bytesize, out_len, err) + check_graphics_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_detail(_wv_r) + _wv_value + end + end +end + +# END nested_modules.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_runtime_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_runtime_rb.snap new file mode 100644 index 00000000..4e9941a8 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_nested_modules_runtime_rb.snap @@ -0,0 +1,705 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require 'ffi' +require 'monitor' + +# The fixed runtime every generated NestedModules binding builds on: the native +# library loader, the load-time ABI check, the error classes, the +# value-buffer reader and writer, cancel tokens, the handle table behind +# callback interfaces and async completions, and the object wrapper base. +module NestedModules + extend FFI::Library + + # An explicit path in NESTED_MODULES_LIBRARY wins. Otherwise a library bundled + # under lib/native/ (a platform gem) is preferred over the system search + # path. + _wv_override = ENV['NESTED_MODULES_LIBRARY'] + if _wv_override && !_wv_override.empty? + ffi_lib _wv_override + else + _wv_name = + case FFI::Platform::OS + when /darwin/ then 'libnested_modules.dylib' + when /mswin|mingw|windows/ then 'nested_modules.dll' + else 'libnested_modules.so' + end + _wv_bundled = File.expand_path(File.join('..', 'native', _wv_name), __dir__) + ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) + end + + # The C ABI revision these bindings speak. Checked before anything else is + # attached, so a mismatched library fails at require time instead of + # misreading an error struct or a value buffer later. + ABI_VERSION = 3 + begin + attach_function :nested_modules_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, "the loaded nested_modules library does not export nested_modules_abi_version " \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = nested_modules_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "nested_modules ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + + # The `nested_modules_error` out-parameter every call reports through. + class ErrorStruct < FFI::Struct + layout :code, :int32, + :message, :pointer, + :payload_ptr, :pointer, + :payload_len, :size_t + end + + # Codes reserved by the runtime. Positive codes belong to a module's error + # domain; these negative codes are raised as a plain Error (or Cancelled) + # regardless of whether the function declares errors. + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + # A callback-interface implementation raised; the message carries the + # exception text. + FOREIGN_ERROR_CODE = -4 + # The call was cancelled through its CancelToken. + CANCELLED_ERROR_CODE = -5 + + # The base class of every error the library raises. + class Error < StandardError + # The ABI error code: positive for a domain error, negative for a runtime + # failure. + attr_reader :code + + def initialize(code, message) + @code = code + super(message) + end + end + + # Raised when a cancellable call completes because its CancelToken fired. + class Cancelled < Error + def initialize(message = 'cancelled') + super(CANCELLED_ERROR_CODE, message) + end + end + + attach_function :nested_modules_error_set, [:pointer, :int32, :string], :void + attach_function :nested_modules_error_clear, [:pointer], :void + attach_function :nested_modules_error_free, [:pointer], :void + attach_function :nested_modules_free_bytes, [:pointer, :size_t], :void + attach_function :nested_modules_cancel_token_create, [], :pointer + attach_function :nested_modules_cancel_token_cancel, [:pointer], :void + attach_function :nested_modules_cancel_token_is_cancelled, [:pointer], :bool + attach_function :nested_modules_cancel_token_destroy, [:pointer], :void + + # @api private + # The error for a code outside any declared domain. + def self._wv_error(code, message) + return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE + + Error.new(code, message) + end + + # @api private + # Copies the code, message, and payload out of a reported error and clears + # it. Returns nil when the struct reports success. + def self._wv_take_error(err) + code = err[:code] + return nil if code.zero? + + msg_ptr = err[:message] + message = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) + payload_ptr = err[:payload_ptr] + payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) + nested_modules_error_clear(err.to_ptr) + [code, message, payload] + end + + # @api private + # Like _wv_take_error for the heap-boxed error an async completion + # receives; the box is released with nested_modules_error_free. + def self._wv_take_boxed_error(err_ptr) + return nil if err_ptr.null? + + taken = _wv_take_error(ErrorStruct.new(err_ptr)) + nested_modules_error_free(err_ptr) + taken + end + + # @api private + # Raises the generic error for a call that declares no error domain. + def self.check_error!(err) + taken = _wv_take_error(err) + return if taken.nil? + + raise _wv_error(taken[0], taken[1]) + end + + # @api private + # The UTF-8 bytes of a String argument, as a private binary copy the + # producer borrows for the call. + def self._wv_str(value) + value.to_s.encode(Encoding::UTF_8).b + end + + # @api private + # A private binary copy of a bytes argument. + def self._wv_bytes(value) + value.to_s.b + end + + # @api private + # Copies a borrowed (ptr, len) run without releasing it. + def self._wv_borrow_bytes(ptr, len) + ptr.null? || len.zero? ? ''.b : ptr.read_string(len) + end + + # @api private + # Copies a returned (ptr, len) allocation, then releases it. + def self._wv_take_bytes(ptr, len) + return ''.b if ptr.null? + + data = len.zero? ? ''.b : ptr.read_string(len) + nested_modules_free_bytes(ptr, len) + data + end + + # @api private + # Copies a returned UTF-8 string, then releases it. + def self._wv_take_string(ptr, len) + _wv_take_bytes(ptr, len).force_encoding(Encoding::UTF_8) + end + + # A cancellation signal for a cancellable async call. Pass one as the + # `cancel:` keyword, then call #cancel from any thread; the call raises + # Cancelled unless it already completed. A token may be shared by several + # calls and stays cancelled once cancelled. + class CancelToken + # @api private + # Owns the consumer's reference to the native token. + class Ptr < FFI::AutoPointer + def self.release(ptr) + NestedModules.nested_modules_cancel_token_destroy(ptr) + end + end + + def initialize + @ptr = Ptr.new(NestedModules.nested_modules_cancel_token_create) + end + + # Requests cancellation. Idempotent. + def cancel + NestedModules.nested_modules_cancel_token_cancel(@ptr) unless @ptr.nil? + nil + end + + # Whether #cancel has been called. + def cancelled? + !@ptr.nil? && NestedModules.nested_modules_cancel_token_is_cancelled(@ptr) + end + + # Releases the native token now rather than at GC time. Calls already + # launched with it keep their own reference. + def close + return if @ptr.nil? + + @ptr.free + @ptr = nil + end + + # @api private + def _wv_ptr + raise Error.new(GENERIC_ERROR_CODE, 'CancelToken used after close') if @ptr.nil? + + @ptr + end + end + + # @api private + # Appends values in the value-buffer wire format: little-endian, packed, + # no alignment. + class WvBufferWriter + def initialize + @buf = +''.b + @objects = [] + end + + # Reserves an object token. The strong reference it carries is minted + # by #seal, just before the buffer is handed to the producer, so an + # encoding abandoned on the way never strands a reference. + def write_object(object) + raise TypeError, "expected a wrapper object, got #{object.class}" unless object.is_a?(WvObject) + + @objects << [@buf.bytesize, object] + write_u64(0) + end + + # Mints the reference each reserved object token carries, recording it + # in `minted`, and returns the finished encoding. + def seal(minted) + @objects.each do |offset, object| + ptr = object._wv_clone_ptr + minted << [object.class, ptr] + @buf[offset, 8] = [ptr.address].pack('Q<') + end + @buf + end + + # Every append is a binary String, so the buffer stays binary and byte + # offsets index it directly. + def write_bool(value) + @buf << [value ? 1 : 0].pack('C') + end + + def write_flag(value) + write_bool(value) + end + + def write_i8(value) + @buf << [value].pack('c') + end + + def write_u8(value) + @buf << [value].pack('C') + end + + def write_i16(value) + @buf << [value].pack('s<') + end + + def write_u16(value) + @buf << [value].pack('S<') + end + + def write_i32(value) + @buf << [value].pack('l<') + end + + def write_u32(value) + @buf << [value].pack('L<') + end + + def write_len(value) + write_u32(value) + end + + def write_i64(value) + @buf << [value].pack('q<') + end + + def write_u64(value) + @buf << [value].pack('Q<') + end + + def write_f32(value) + @buf << [value].pack('e') + end + + def write_f64(value) + @buf << [value].pack('E') + end + + def write_string(value) + write_bytes(value.to_s.encode(Encoding::UTF_8)) + end + + def write_bytes(value) + bytes = value.to_s.b + write_u32(bytes.bytesize) + @buf << bytes + end + end + + # @api private + # Reads values in the 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(count, what) + if @pos + count > @data.bytesize + raise Error.new(MARSHAL_ERROR_CODE, "malformed value buffer: truncated #{what}") + end + + bytes = @data.byteslice(@pos, count) + @pos += count + bytes + end + + def read_bool + byte = take(1, 'bool').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: bool byte out of range') if byte > 1 + + byte == 1 + end + + def read_flag + byte = take(1, 'option flag').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: option flag out of range') if byte > 1 + + byte == 1 + end + + def read_i8 + take(1, 'i8').unpack1('c') + end + + def read_u8 + take(1, 'u8').unpack1('C') + end + + def read_i16 + take(2, 'i16').unpack1('s<') + end + + def read_u16 + take(2, 'u16').unpack1('S<') + end + + def read_i32 + take(4, 'i32').unpack1('l<') + end + + def read_u32 + take(4, 'u32').unpack1('L<') + end + + # A u32 element count. Counts are not checked against the remaining + # bytes, since an element can encode to zero bytes; a short buffer still + # fails on the first truncated read. + def read_len + read_u32 + end + + def read_i64 + take(8, 'i64').unpack1('q<') + end + + def read_u64 + take(8, 'u64').unpack1('Q<') + end + + def read_f32 + take(4, 'f32').unpack1('e') + end + + def read_f64 + take(8, 'f64').unpack1('E') + end + + def read_string + text = read_bytes.force_encoding(Encoding::UTF_8) + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: string is not valid UTF-8') unless text.valid_encoding? + + text + end + + def read_bytes + take(read_u32, 'byte run') + end + + # An object token: a non-zero u64 carrying one strong reference the + # caller adopts into a wrapper. + def read_object_token + addr = read_u64 + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: null object token') if addr.zero? + + FFI::Pointer.new(addr) + end + + def expect_end! + return if @pos == @data.bytesize + + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: trailing bytes after value') + end + end + + # @api private + # Seals the writers of one call's object-carrying buffers (see + # WvBufferWriter#seal) and returns their encodings. If any reference can't + # be minted (a closed wrapper), the ones already minted are released before + # the error propagates. The producer adopts every minted reference once the + # call is made. + def self._wv_seal(*writers) + minted = [] + writers.map { |writer| writer.seal(minted) } + rescue Exception # rubocop:disable Lint/RescueException + minted.each { |cls, ptr| cls::WV_PTR.release(ptr) } + raise + end + + # @api private + # Decodes a returned value buffer: copies and releases it, yields a reader + # to the block, and checks that the block consumed every byte. + def self._wv_decode(ptr, len) + reader = WvBufferReader.new(_wv_take_bytes(ptr, len)) + value = yield reader + reader.expect_end! + value + end + + # Ruby objects the producer refers to by an integer key: callback-interface + # implementations (the key is the vtable `ctx`) and pending async + # completions (the key is the completion `context`). The producer never + # sees a Ruby object address. A Monitor rather than a Mutex, so a + # finalizer that runs while this thread holds the lock can't deadlock it. + @wv_handles = {} + @wv_next_handle = 0 + @wv_handles_lock = Monitor.new + + # @api private + def self._wv_handle_put(value) + @wv_handles_lock.synchronize do + @wv_next_handle += 1 + @wv_handles[@wv_next_handle] = value + FFI::Pointer.new(@wv_next_handle) + end + end + + # @api private + def self._wv_handle_get(key) + @wv_handles_lock.synchronize { @wv_handles[key.address] } + end + + # @api private + def self._wv_handle_take(key) + @wv_handles_lock.synchronize { @wv_handles.delete(key.address) } + end + + # @api private + # Registers a callback-interface implementation and returns its `ctx`. + # Never raises, so nothing can strand the registration between this and + # the call that hands it to the producer (an unusable implementation fails + # when the producer calls it, with FOREIGN_ERROR_CODE). + def self._wv_cb_register(impl) + _wv_handle_put([:callback, impl]) + end + + # @api private + # Resolves a trampoline's `ctx` back to the registered implementation. + def self._wv_cb_lookup(ctx) + kind, impl = _wv_handle_get(ctx) + raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') unless kind == :callback + + impl + end + + # @api private + # Drops an implementation when the producer releases its last reference. + def self._wv_cb_free(ctx) + _wv_handle_take(ctx) + nil + end + + # @api private + # Reports a callback implementation's exception through `out_err`. The + # producer copies the message. + def self._wv_cb_fail(out_err, exception) + message = exception.message.to_s + message = exception.class.name if message.empty? + message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).delete("\0") + nested_modules_error_set(out_err, FOREIGN_ERROR_CODE, message) + nil + end + + # @api private + # Registers a pending async call; returns its completion context and the + # queue its completion is delivered on. + def self._wv_async_begin + queue = Queue.new + [_wv_handle_put([:async, queue]), queue] + end + + # @api private + # The queue of the pending call a completion belongs to. Each context + # completes exactly once, so its entry is removed here. + def self._wv_async_finish(ctx) + kind, queue = _wv_handle_take(ctx) + kind == :async ? queue : Queue.new + end + + # @api private + # Blocks until a pending call completes, raising its error. If the wait is + # interrupted (Thread#raise, Thread#kill, Timeout), the call's token is + # cancelled before the interruption propagates. + def self._wv_async_wait(queue, token = nil) + value = + begin + queue.pop + rescue Exception # rubocop:disable Lint/RescueException + token&.cancel + raise + end + raise value if value.is_a?(Exception) + + value + end + + # @api private + # The base of every interface wrapper. A wrapper owns one strong reference + # to its native object and releases it exactly once, from #close or (as a + # backstop) from the GC finalizer of its FFI::AutoPointer. Calls borrow the + # pointer through #_wv_call, so a #close that races an in-flight call on + # another thread only releases the reference once the call returns. + class WvObject + # @api private + # Adopts one strong reference the producer handed over. + def self._from_ptr(ptr) + obj = allocate + obj._wv_init(ptr) + obj + end + + # @api private + def _wv_init(ptr) + @wv_handle = self.class::WV_PTR.new(ptr) + @wv_lock = Mutex.new + @wv_calls = 0 + @wv_closing = false + end + + # The native object pointer (borrowed; valid until #close). + def handle + handle = @wv_handle + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if handle.nil? || @wv_closing + + handle + end + + # Whether #close has released this wrapper's reference. + def closed? + @wv_handle.nil? || @wv_closing + end + + # Releases this wrapper's reference now rather than at GC time. + # Idempotent. The object itself lives until its last reference anywhere + # (another wrapper, a record field, the producer) goes. + def close + handle = @wv_lock.synchronize do + next nil if @wv_handle.nil? || @wv_closing + + if @wv_calls.zero? + taken = @wv_handle + @wv_handle = nil + taken + else + @wv_closing = true + nil + end + end + handle&.free + nil + end + + # @api private + # Yields the native pointer, keeping the reference alive until the block + # returns. + def _wv_call + handle = @wv_lock.synchronize do + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if @wv_handle.nil? || @wv_closing + + @wv_calls += 1 + @wv_handle + end + begin + yield handle + ensure + _wv_leave + end + end + + # @api private + # Mints a new strong reference the caller owns (an object written into a + # value buffer carries its own reference). + def _wv_clone_ptr + _wv_call { |ptr| self.class._wv_clone(ptr) } + end + + # dup and clone produce an independent wrapper with its own reference to + # the same object. + def initialize_copy(other) + super + _wv_init(other._wv_clone_ptr) + end + + private + + def _wv_leave + release = @wv_lock.synchronize do + @wv_calls -= 1 + next nil unless @wv_closing && @wv_calls.zero? + + taken = @wv_handle + @wv_handle = nil + @wv_closing = false + taken + end + release&.free + end + end + + # @api private + # Yields the native pointers of `objects` (NULL for nil), each kept alive + # until the block returns. + def self._wv_pin(*objects, &block) + _wv_pin_from(objects, 0, [], &block) + end + + # @api private + def self._wv_pin_from(objects, index, pointers, &block) + return yield(*pointers) if index == objects.length + + object = objects[index] + if object.nil? + _wv_pin_from(objects, index + 1, pointers + [FFI::Pointer::NULL], &block) + else + object._wv_call { |ptr| _wv_pin_from(objects, index + 1, pointers + [ptr], &block) } + end + end + + # @api private + # Checks each top-level module's contract checksum (`module name => + # [checksum function, expected value]`) against the library, so bindings + # generated from a different API fail at require time. + def self._wv_check_contract!(checksums) + checksums.each do |mod, (symbol, expected)| + begin + attach_function symbol, [], :uint64 + rescue FFI::NotFoundError + raise LoadError, "the loaded nested_modules library has no module '#{mod}' (#{symbol} is missing)" + end + actual = public_send(symbol) + next if actual == expected + + raise LoadError, "nested_modules module '#{mod}' does not match these bindings: expected contract " \ + "checksum 0x#{expected.to_s(16)}, the library reports 0x#{actual.to_s(16)}; " \ + 'regenerate the bindings' + end + end + + # @api private + # Live native resources of one kind (0 objects, 1 callbacks, 2 iterators, + # 3 cancel tokens, 4 byte allocations). Only libraries built with the + # `leak-check` feature export the counter. + def self._wv_debug_live(kind) + unless respond_to?(:nested_modules_debug_live) + begin + attach_function :nested_modules_debug_live, [:int32], :uint64 + rescue FFI::NotFoundError + raise Error.new(GENERIC_ERROR_CODE, 'nested_modules was built without the leak-check feature') + end + end + nested_modules_debug_live(kind) + end +end + +# END runtime.rb 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 deleted file mode 100644 index fea71f45..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap +++ /dev/null @@ -1,499 +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 - - # 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 = 2 - 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 - - # The runtime-reserved error codes. Positive codes belong to a module's - # declared error domain; these negative codes are programming errors the - # wrappers raise as a plain Error regardless of the function's `throws`. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - - 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 - - # An object token: a non-zero u64 carrying one strong reference the - # caller adopts into a wrapper. - def read_object_token - addr = read_u64 - raise Error.new(-1, 'malformed value buffer: null object token') if addr.zero? - FFI::Pointer.new(addr) - 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_set, [:pointer, :int32, :string], :void - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # === Module: graphics === - - # Base error for the `graphics` module's error domain. - class GraphicsError < Error - # coordinate out of bounds - class OutOfBounds < GraphicsError - CODE = 1 - - def initialize(message = 'coordinate out of bounds') - super(1, message) - end - end - end - - # Maps each ABI code of the GraphicsError domain to its error class. - GRAPHICS_ERROR_CODES = { - 1 => GraphicsError::OutOfBounds, - }.freeze - - # Builds the GraphicsError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling, callback failures). - def self.graphics_error_from(code, message, payload = nil) - cls = GRAPHICS_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed GraphicsError for a non-zero error slot. - def self.check_graphics_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise graphics_error_from(code, msg, payload) - end - - # A measurement unit shared with nested submodules. - module Unit - PIXELS = 0 - POINTS = 1 - end - - # Top-level struct that references a struct defined one level deeper. - class Canvas - attr_reader :width - - # Reference down into a nested module - attr_reader :root - - def initialize(width:, root:) - @width = width - @root = root - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Canvas) - return false unless width == other.width - return false unless root == other.root - true - end - end - - # @api private - # Packs a Canvas into the value-buffer wire format. - def self._wv_write_canvas(w, v) - w.write_i32(v.width) - _wv_write_shape(w, v.root) - end - - # @api private - # Unpacks a Canvas from the value-buffer wire format. - def self._wv_read_canvas(r) - _wv_width = r.read_i32 - _wv_root = _wv_read_shape(r) - Canvas.new(width: _wv_width, root: _wv_root) - end - # Construct a Canvas. - attach_function :weaveffi_graphics_new_canvas, [:int32, :pointer, :pointer], :pointer - - # Construct a Canvas. - def self.new_canvas(width) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_graphics_new_canvas(width, 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_canvas(_wv_r) - _wv_r.expect_end! - _wv_value - end - - # === Module: graphics_shapes === - - # A struct one level deep that references both an ancestor enum and a deeper struct. - class Shape - attr_reader :sides - - # Reference up to the parent module's enum - attr_reader :unit - - # Reference down into a deeper module - attr_reader :detail - - def initialize(sides:, unit:, detail:) - @sides = sides - @unit = unit - @detail = detail - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Shape) - return false unless sides == other.sides - return false unless unit == other.unit - return false unless detail == other.detail - true - end - end - - # @api private - # Packs a Shape into the value-buffer wire format. - def self._wv_write_shape(w, v) - w.write_i32(v.sides) - w.write_i32(v.unit) - _wv_write_detail(w, v.detail) - end - - # @api private - # Unpacks a Shape from the value-buffer wire format. - def self._wv_read_shape(r) - _wv_sides = r.read_i32 - _wv_unit = r.read_i32 - _wv_detail = _wv_read_detail(r) - Shape.new(sides: _wv_sides, unit: _wv_unit, detail: _wv_detail) - end - # Construct a Shape. - attach_function :weaveffi_graphics_shapes_make_shape, [:int32, :pointer, :pointer], :pointer - # Nested function returning a struct owned by an ancestor module. - attach_function :weaveffi_graphics_shapes_describe, [:pointer, :pointer], :pointer - - # Construct a Shape. - def self.make_shape(sides) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_graphics_shapes_make_shape(sides, 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_shape(_wv_r) - _wv_r.expect_end! - _wv_value - end - - # Nested function returning a struct owned by an ancestor module. - def self.describe() - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_graphics_shapes_describe(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_canvas(_wv_r) - _wv_r.expect_end! - _wv_value - end - - # === Module: graphics_shapes_geometry === - - # A struct two levels deep. - class Detail - attr_reader :area - - def initialize(area:) - @area = area - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Detail) - return false unless area == other.area - true - end - end - - # @api private - # Packs a Detail into the value-buffer wire format. - def self._wv_write_detail(w, v) - w.write_f64(v.area) - end - - # @api private - # Unpacks a Detail from the value-buffer wire format. - def self._wv_read_detail(r) - _wv_area = r.read_f64 - Detail.new(area: _wv_area) - end - # Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - attach_function :weaveffi_graphics_shapes_geometry_compute, [:pointer, :size_t, :pointer, :pointer], :pointer - - # Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - def self.compute(shape) - err = ErrorStruct.new - shape_w = WvBufferWriter.new - _wv_write_shape(shape_w, shape) - shape_data = shape_w.data - shape_buf = FFI::MemoryPointer.new(:uint8, shape_data.bytesize) - shape_buf.put_bytes(0, shape_data) - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_graphics_shapes_geometry_compute(shape_buf, shape_data.bytesize, out_len, err) - check_graphics_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_detail(_wv_r) - _wv_r.expect_end! - _wv_value - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__nested_modules_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__nested_modules_gemspec.snap new file mode 100644 index 00000000..323d0099 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__nested_modules_gemspec.snap @@ -0,0 +1,20 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +Gem::Specification.new do |s| + s.name = 'nested_modules' + s.version = '0.1.0' + s.summary = 'Native bindings for nested_modules' + s.authors = ['nested_modules'] + s.files = Dir['lib/**/*.rb'] + s.require_paths = ['lib'] + s.required_ruby_version = '>= 2.7' + + s.add_dependency 'ffi', '~> 1.15' +end + +# END nested_modules.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__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_shapes__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__README_md.snap index dfa27948..439163f5 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__README_md.snap @@ -1,30 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# weaveffi (Ruby) +# shapes (Ruby) -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. +Ruby bindings for `shapes` 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. +- Ruby 2.7 or newer +- The native library (`libshapes.so`, `libshapes.dylib`, or `shapes.dll`) on the library + search path, or its path in the `SHAPES_LIBRARY` environment variable. ## Install ```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem +gem build shapes.gemspec +gem install shapes-0.1.0.gem ``` ## Usage ```ruby -require 'weaveffi' +require 'shapes' + +Shapes::ABI_VERSION ``` diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_rb.snap new file mode 100644 index 00000000..38d3a63a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_rb.snap @@ -0,0 +1,329 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require_relative 'shapes/runtime' + +# Ruby bindings for shapes. +module Shapes + # Contract checksums of the top-level modules these bindings were + # generated from; a library built from a different API fails to load. + _wv_check_contract!( + 'shapes' => [:shapes_shapes_checksum, 0x06acf433ffa2a0f0], + ) + + # === Module: shapes === + + # An algebraic shape (sum type with associated data) + class Shape + # The active variant's integer tag. + def tag + self.class::TAG + end + + # The empty shape (a unit variant) + class Empty < Shape + TAG = 0 + + # Structural equality over the variant and its fields. + def ==(other) + return false unless other.is_a?(Empty) + true + end + end + + # A circle with a radius + class Circle < Shape + TAG = 1 + + # Radius in points + attr_reader :radius + + def initialize(radius:) + @radius = radius + end + + # Structural equality over the variant and its fields. + def ==(other) + return false unless other.is_a?(Circle) + return false unless radius == other.radius + true + end + end + + # An axis-aligned rectangle + class Rectangle < Shape + TAG = 2 + + attr_reader :width + + attr_reader :height + + def initialize(width:, height:) + @width = width + @height = height + end + + # Structural equality over the variant and its fields. + def ==(other) + return false unless other.is_a?(Rectangle) + return false unless width == other.width + return false unless height == other.height + true + end + end + + # An integer-bounded shape (exercises narrow + wide ints) + class Bounded < Shape + TAG = 3 + + attr_reader :min + + attr_reader :max + + attr_reader :weight + + def initialize(min:, max:, weight:) + @min = min + @max = max + @weight = weight + end + + # Structural equality over the variant and its fields. + def ==(other) + return false unless other.is_a?(Bounded) + return false unless min == other.min + return false unless max == other.max + return false unless weight == other.weight + true + end + end + + # A labeled shape with a small count + class Labeled < Shape + TAG = 4 + + attr_reader :label + + attr_reader :count + + def initialize(label:, count:) + @label = label + @count = count + end + + # Structural equality over the variant and its fields. + def ==(other) + return false unless other.is_a?(Labeled) + return false unless label == other.label + return false unless count == other.count + true + end + end + end + + # A plain C-style enum (no payloads) + module Channel + RED = 0 + GREEN = 1 + BLUE = 2 + end + + # One field per supported numeric primitive + class Numerics + attr_reader :a + + attr_reader :b + + attr_reader :c + + attr_reader :d + + attr_reader :e + + attr_reader :f + + attr_reader :g + + attr_reader :h + + attr_reader :i + + attr_reader :j + + def initialize(a:, b:, c:, d:, e:, f:, g:, h:, i:, j:) + @a = a + @b = b + @c = c + @d = d + @e = e + @f = f + @g = g + @h = h + @i = i + @j = j + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(Numerics) + return false unless a == other.a + return false unless b == other.b + return false unless c == other.c + return false unless d == other.d + return false unless e == other.e + return false unless f == other.f + return false unless g == other.g + return false unless h == other.h + return false unless i == other.i + return false unless j == other.j + true + end + end + + # @api private + # Packs a Numerics into the value-buffer wire format. + def self._wv_write_numerics(w, v) + w.write_i8(v.a) + w.write_i16(v.b) + w.write_i32(v.c) + w.write_i64(v.d) + w.write_u8(v.e) + w.write_u16(v.f) + w.write_u32(v.g) + w.write_u64(v.h) + w.write_f32(v.i) + w.write_f64(v.j) + end + + # @api private + # Unpacks a Numerics from the value-buffer wire format. + def self._wv_read_numerics(r) + _wv_a = r.read_i8 + _wv_b = r.read_i16 + _wv_c = r.read_i32 + _wv_d = r.read_i64 + _wv_e = r.read_u8 + _wv_f = r.read_u16 + _wv_g = r.read_u32 + _wv_h = r.read_u64 + _wv_i = r.read_f32 + _wv_j = r.read_f64 + Numerics.new(a: _wv_a, b: _wv_b, c: _wv_c, d: _wv_d, e: _wv_e, f: _wv_f, g: _wv_g, h: _wv_h, i: _wv_i, j: _wv_j) + end + + # @api private + # Packs a Shape into the value-buffer wire format. + def self._wv_write_shape(w, v) + case v + when Shape::Empty + w.write_i32(0) + when Shape::Circle + w.write_i32(1) + w.write_f64(v.radius) + when Shape::Rectangle + w.write_i32(2) + w.write_f32(v.width) + w.write_f32(v.height) + when Shape::Bounded + w.write_i32(3) + w.write_i16(v.min) + w.write_i16(v.max) + w.write_u64(v.weight) + when Shape::Labeled + w.write_i32(4) + w.write_string(v.label) + w.write_u8(v.count) + else + raise Error.new(MARSHAL_ERROR_CODE, 'unknown Shape variant') + end + end + + # @api private + # Unpacks a Shape from the value-buffer wire format. + def self._wv_read_shape(r) + tag = r.read_i32 + case tag + when 0 + Shape::Empty.new + when 1 + _wv_radius = r.read_f64 + Shape::Circle.new(radius: _wv_radius) + when 2 + _wv_width = r.read_f32 + _wv_height = r.read_f32 + Shape::Rectangle.new(width: _wv_width, height: _wv_height) + when 3 + _wv_min = r.read_i16 + _wv_max = r.read_i16 + _wv_weight = r.read_u64 + Shape::Bounded.new(min: _wv_min, max: _wv_max, weight: _wv_weight) + when 4 + _wv_label = r.read_string + _wv_count = r.read_u8 + Shape::Labeled.new(label: _wv_label, count: _wv_count) + else + raise Error.new(MARSHAL_ERROR_CODE, "malformed value buffer: unknown Shape tag #{tag}") + end + end + + attach_function :shapes_shapes_describe, [:pointer, :size_t, :pointer, :pointer], :pointer + attach_function :shapes_shapes_scale, [:pointer, :size_t, :double, :pointer, :pointer], :pointer + attach_function :shapes_shapes_sum_bytes, [:pointer, :size_t, :pointer], :uint64 + attach_function :shapes_shapes_make_numerics, [:int8, :int16, :int32, :int64, :uint8, :uint16, :uint32, :uint64, :float, :double, :pointer, :pointer], :pointer + + # Render a shape to a string + def self.describe(shape) + shape_w = WvBufferWriter.new + _wv_write_shape(shape_w, shape) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + shape_s = _wv_seal(shape_w).first + result = shapes_shapes_describe(shape_s, shape_s.bytesize, out_len, err) + check_error!(err) + _wv_take_string(result, out_len.read(:size_t)) + end + + # Scale a shape by a factor, returning a new shape + def self.scale(shape, factor) + shape_w = WvBufferWriter.new + _wv_write_shape(shape_w, shape) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + shape_s = _wv_seal(shape_w).first + result = shapes_shapes_scale(shape_s, shape_s.bytesize, factor, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_shape(_wv_r) + _wv_value + end + end + + # Sum a list of bytes into a wide integer (numerics smoke) + def self.sum_bytes(values) + values_s = _wv_bytes(values) + err = ErrorStruct.new + result = shapes_shapes_sum_bytes(values_s, values_s.bytesize, err) + check_error!(err) + result + end + + # Build a Numerics struct from every numeric primitive + def self.make_numerics(a, b, c, d, e, f, g, h, i, j) + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, out_len, err) + check_error!(err) + _wv_decode(result, out_len.read(:size_t)) do |_wv_r| + _wv_value = _wv_read_numerics(_wv_r) + _wv_value + end + end +end + +# END shapes.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_runtime_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_runtime_rb.snap new file mode 100644 index 00000000..3a154ba9 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_shapes_runtime_rb.snap @@ -0,0 +1,705 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from api.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate api.yml -o + +# frozen_string_literal: true + +require 'ffi' +require 'monitor' + +# The fixed runtime every generated Shapes binding builds on: the native +# library loader, the load-time ABI check, the error classes, the +# value-buffer reader and writer, cancel tokens, the handle table behind +# callback interfaces and async completions, and the object wrapper base. +module Shapes + extend FFI::Library + + # An explicit path in SHAPES_LIBRARY wins. Otherwise a library bundled + # under lib/native/ (a platform gem) is preferred over the system search + # path. + _wv_override = ENV['SHAPES_LIBRARY'] + if _wv_override && !_wv_override.empty? + ffi_lib _wv_override + else + _wv_name = + case FFI::Platform::OS + when /darwin/ then 'libshapes.dylib' + when /mswin|mingw|windows/ then 'shapes.dll' + else 'libshapes.so' + end + _wv_bundled = File.expand_path(File.join('..', 'native', _wv_name), __dir__) + ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) + end + + # The C ABI revision these bindings speak. Checked before anything else is + # attached, so a mismatched library fails at require time instead of + # misreading an error struct or a value buffer later. + ABI_VERSION = 3 + begin + attach_function :shapes_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, "the loaded shapes library does not export shapes_abi_version " \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = shapes_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "shapes ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + + # The `shapes_error` out-parameter every call reports through. + class ErrorStruct < FFI::Struct + layout :code, :int32, + :message, :pointer, + :payload_ptr, :pointer, + :payload_len, :size_t + end + + # Codes reserved by the runtime. Positive codes belong to a module's error + # domain; these negative codes are raised as a plain Error (or Cancelled) + # regardless of whether the function declares errors. + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + # A callback-interface implementation raised; the message carries the + # exception text. + FOREIGN_ERROR_CODE = -4 + # The call was cancelled through its CancelToken. + CANCELLED_ERROR_CODE = -5 + + # The base class of every error the library raises. + class Error < StandardError + # The ABI error code: positive for a domain error, negative for a runtime + # failure. + attr_reader :code + + def initialize(code, message) + @code = code + super(message) + end + end + + # Raised when a cancellable call completes because its CancelToken fired. + class Cancelled < Error + def initialize(message = 'cancelled') + super(CANCELLED_ERROR_CODE, message) + end + end + + attach_function :shapes_error_set, [:pointer, :int32, :string], :void + attach_function :shapes_error_clear, [:pointer], :void + attach_function :shapes_error_free, [:pointer], :void + attach_function :shapes_free_bytes, [:pointer, :size_t], :void + attach_function :shapes_cancel_token_create, [], :pointer + attach_function :shapes_cancel_token_cancel, [:pointer], :void + attach_function :shapes_cancel_token_is_cancelled, [:pointer], :bool + attach_function :shapes_cancel_token_destroy, [:pointer], :void + + # @api private + # The error for a code outside any declared domain. + def self._wv_error(code, message) + return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE + + Error.new(code, message) + end + + # @api private + # Copies the code, message, and payload out of a reported error and clears + # it. Returns nil when the struct reports success. + def self._wv_take_error(err) + code = err[:code] + return nil if code.zero? + + msg_ptr = err[:message] + message = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) + payload_ptr = err[:payload_ptr] + payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) + shapes_error_clear(err.to_ptr) + [code, message, payload] + end + + # @api private + # Like _wv_take_error for the heap-boxed error an async completion + # receives; the box is released with shapes_error_free. + def self._wv_take_boxed_error(err_ptr) + return nil if err_ptr.null? + + taken = _wv_take_error(ErrorStruct.new(err_ptr)) + shapes_error_free(err_ptr) + taken + end + + # @api private + # Raises the generic error for a call that declares no error domain. + def self.check_error!(err) + taken = _wv_take_error(err) + return if taken.nil? + + raise _wv_error(taken[0], taken[1]) + end + + # @api private + # The UTF-8 bytes of a String argument, as a private binary copy the + # producer borrows for the call. + def self._wv_str(value) + value.to_s.encode(Encoding::UTF_8).b + end + + # @api private + # A private binary copy of a bytes argument. + def self._wv_bytes(value) + value.to_s.b + end + + # @api private + # Copies a borrowed (ptr, len) run without releasing it. + def self._wv_borrow_bytes(ptr, len) + ptr.null? || len.zero? ? ''.b : ptr.read_string(len) + end + + # @api private + # Copies a returned (ptr, len) allocation, then releases it. + def self._wv_take_bytes(ptr, len) + return ''.b if ptr.null? + + data = len.zero? ? ''.b : ptr.read_string(len) + shapes_free_bytes(ptr, len) + data + end + + # @api private + # Copies a returned UTF-8 string, then releases it. + def self._wv_take_string(ptr, len) + _wv_take_bytes(ptr, len).force_encoding(Encoding::UTF_8) + end + + # A cancellation signal for a cancellable async call. Pass one as the + # `cancel:` keyword, then call #cancel from any thread; the call raises + # Cancelled unless it already completed. A token may be shared by several + # calls and stays cancelled once cancelled. + class CancelToken + # @api private + # Owns the consumer's reference to the native token. + class Ptr < FFI::AutoPointer + def self.release(ptr) + Shapes.shapes_cancel_token_destroy(ptr) + end + end + + def initialize + @ptr = Ptr.new(Shapes.shapes_cancel_token_create) + end + + # Requests cancellation. Idempotent. + def cancel + Shapes.shapes_cancel_token_cancel(@ptr) unless @ptr.nil? + nil + end + + # Whether #cancel has been called. + def cancelled? + !@ptr.nil? && Shapes.shapes_cancel_token_is_cancelled(@ptr) + end + + # Releases the native token now rather than at GC time. Calls already + # launched with it keep their own reference. + def close + return if @ptr.nil? + + @ptr.free + @ptr = nil + end + + # @api private + def _wv_ptr + raise Error.new(GENERIC_ERROR_CODE, 'CancelToken used after close') if @ptr.nil? + + @ptr + end + end + + # @api private + # Appends values in the value-buffer wire format: little-endian, packed, + # no alignment. + class WvBufferWriter + def initialize + @buf = +''.b + @objects = [] + end + + # Reserves an object token. The strong reference it carries is minted + # by #seal, just before the buffer is handed to the producer, so an + # encoding abandoned on the way never strands a reference. + def write_object(object) + raise TypeError, "expected a wrapper object, got #{object.class}" unless object.is_a?(WvObject) + + @objects << [@buf.bytesize, object] + write_u64(0) + end + + # Mints the reference each reserved object token carries, recording it + # in `minted`, and returns the finished encoding. + def seal(minted) + @objects.each do |offset, object| + ptr = object._wv_clone_ptr + minted << [object.class, ptr] + @buf[offset, 8] = [ptr.address].pack('Q<') + end + @buf + end + + # Every append is a binary String, so the buffer stays binary and byte + # offsets index it directly. + def write_bool(value) + @buf << [value ? 1 : 0].pack('C') + end + + def write_flag(value) + write_bool(value) + end + + def write_i8(value) + @buf << [value].pack('c') + end + + def write_u8(value) + @buf << [value].pack('C') + end + + def write_i16(value) + @buf << [value].pack('s<') + end + + def write_u16(value) + @buf << [value].pack('S<') + end + + def write_i32(value) + @buf << [value].pack('l<') + end + + def write_u32(value) + @buf << [value].pack('L<') + end + + def write_len(value) + write_u32(value) + end + + def write_i64(value) + @buf << [value].pack('q<') + end + + def write_u64(value) + @buf << [value].pack('Q<') + end + + def write_f32(value) + @buf << [value].pack('e') + end + + def write_f64(value) + @buf << [value].pack('E') + end + + def write_string(value) + write_bytes(value.to_s.encode(Encoding::UTF_8)) + end + + def write_bytes(value) + bytes = value.to_s.b + write_u32(bytes.bytesize) + @buf << bytes + end + end + + # @api private + # Reads values in the 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(count, what) + if @pos + count > @data.bytesize + raise Error.new(MARSHAL_ERROR_CODE, "malformed value buffer: truncated #{what}") + end + + bytes = @data.byteslice(@pos, count) + @pos += count + bytes + end + + def read_bool + byte = take(1, 'bool').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: bool byte out of range') if byte > 1 + + byte == 1 + end + + def read_flag + byte = take(1, 'option flag').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: option flag out of range') if byte > 1 + + byte == 1 + end + + def read_i8 + take(1, 'i8').unpack1('c') + end + + def read_u8 + take(1, 'u8').unpack1('C') + end + + def read_i16 + take(2, 'i16').unpack1('s<') + end + + def read_u16 + take(2, 'u16').unpack1('S<') + end + + def read_i32 + take(4, 'i32').unpack1('l<') + end + + def read_u32 + take(4, 'u32').unpack1('L<') + end + + # A u32 element count. Counts are not checked against the remaining + # bytes, since an element can encode to zero bytes; a short buffer still + # fails on the first truncated read. + def read_len + read_u32 + end + + def read_i64 + take(8, 'i64').unpack1('q<') + end + + def read_u64 + take(8, 'u64').unpack1('Q<') + end + + def read_f32 + take(4, 'f32').unpack1('e') + end + + def read_f64 + take(8, 'f64').unpack1('E') + end + + def read_string + text = read_bytes.force_encoding(Encoding::UTF_8) + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: string is not valid UTF-8') unless text.valid_encoding? + + text + end + + def read_bytes + take(read_u32, 'byte run') + end + + # An object token: a non-zero u64 carrying one strong reference the + # caller adopts into a wrapper. + def read_object_token + addr = read_u64 + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: null object token') if addr.zero? + + FFI::Pointer.new(addr) + end + + def expect_end! + return if @pos == @data.bytesize + + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: trailing bytes after value') + end + end + + # @api private + # Seals the writers of one call's object-carrying buffers (see + # WvBufferWriter#seal) and returns their encodings. If any reference can't + # be minted (a closed wrapper), the ones already minted are released before + # the error propagates. The producer adopts every minted reference once the + # call is made. + def self._wv_seal(*writers) + minted = [] + writers.map { |writer| writer.seal(minted) } + rescue Exception # rubocop:disable Lint/RescueException + minted.each { |cls, ptr| cls::WV_PTR.release(ptr) } + raise + end + + # @api private + # Decodes a returned value buffer: copies and releases it, yields a reader + # to the block, and checks that the block consumed every byte. + def self._wv_decode(ptr, len) + reader = WvBufferReader.new(_wv_take_bytes(ptr, len)) + value = yield reader + reader.expect_end! + value + end + + # Ruby objects the producer refers to by an integer key: callback-interface + # implementations (the key is the vtable `ctx`) and pending async + # completions (the key is the completion `context`). The producer never + # sees a Ruby object address. A Monitor rather than a Mutex, so a + # finalizer that runs while this thread holds the lock can't deadlock it. + @wv_handles = {} + @wv_next_handle = 0 + @wv_handles_lock = Monitor.new + + # @api private + def self._wv_handle_put(value) + @wv_handles_lock.synchronize do + @wv_next_handle += 1 + @wv_handles[@wv_next_handle] = value + FFI::Pointer.new(@wv_next_handle) + end + end + + # @api private + def self._wv_handle_get(key) + @wv_handles_lock.synchronize { @wv_handles[key.address] } + end + + # @api private + def self._wv_handle_take(key) + @wv_handles_lock.synchronize { @wv_handles.delete(key.address) } + end + + # @api private + # Registers a callback-interface implementation and returns its `ctx`. + # Never raises, so nothing can strand the registration between this and + # the call that hands it to the producer (an unusable implementation fails + # when the producer calls it, with FOREIGN_ERROR_CODE). + def self._wv_cb_register(impl) + _wv_handle_put([:callback, impl]) + end + + # @api private + # Resolves a trampoline's `ctx` back to the registered implementation. + def self._wv_cb_lookup(ctx) + kind, impl = _wv_handle_get(ctx) + raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') unless kind == :callback + + impl + end + + # @api private + # Drops an implementation when the producer releases its last reference. + def self._wv_cb_free(ctx) + _wv_handle_take(ctx) + nil + end + + # @api private + # Reports a callback implementation's exception through `out_err`. The + # producer copies the message. + def self._wv_cb_fail(out_err, exception) + message = exception.message.to_s + message = exception.class.name if message.empty? + message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).delete("\0") + shapes_error_set(out_err, FOREIGN_ERROR_CODE, message) + nil + end + + # @api private + # Registers a pending async call; returns its completion context and the + # queue its completion is delivered on. + def self._wv_async_begin + queue = Queue.new + [_wv_handle_put([:async, queue]), queue] + end + + # @api private + # The queue of the pending call a completion belongs to. Each context + # completes exactly once, so its entry is removed here. + def self._wv_async_finish(ctx) + kind, queue = _wv_handle_take(ctx) + kind == :async ? queue : Queue.new + end + + # @api private + # Blocks until a pending call completes, raising its error. If the wait is + # interrupted (Thread#raise, Thread#kill, Timeout), the call's token is + # cancelled before the interruption propagates. + def self._wv_async_wait(queue, token = nil) + value = + begin + queue.pop + rescue Exception # rubocop:disable Lint/RescueException + token&.cancel + raise + end + raise value if value.is_a?(Exception) + + value + end + + # @api private + # The base of every interface wrapper. A wrapper owns one strong reference + # to its native object and releases it exactly once, from #close or (as a + # backstop) from the GC finalizer of its FFI::AutoPointer. Calls borrow the + # pointer through #_wv_call, so a #close that races an in-flight call on + # another thread only releases the reference once the call returns. + class WvObject + # @api private + # Adopts one strong reference the producer handed over. + def self._from_ptr(ptr) + obj = allocate + obj._wv_init(ptr) + obj + end + + # @api private + def _wv_init(ptr) + @wv_handle = self.class::WV_PTR.new(ptr) + @wv_lock = Mutex.new + @wv_calls = 0 + @wv_closing = false + end + + # The native object pointer (borrowed; valid until #close). + def handle + handle = @wv_handle + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if handle.nil? || @wv_closing + + handle + end + + # Whether #close has released this wrapper's reference. + def closed? + @wv_handle.nil? || @wv_closing + end + + # Releases this wrapper's reference now rather than at GC time. + # Idempotent. The object itself lives until its last reference anywhere + # (another wrapper, a record field, the producer) goes. + def close + handle = @wv_lock.synchronize do + next nil if @wv_handle.nil? || @wv_closing + + if @wv_calls.zero? + taken = @wv_handle + @wv_handle = nil + taken + else + @wv_closing = true + nil + end + end + handle&.free + nil + end + + # @api private + # Yields the native pointer, keeping the reference alive until the block + # returns. + def _wv_call + handle = @wv_lock.synchronize do + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if @wv_handle.nil? || @wv_closing + + @wv_calls += 1 + @wv_handle + end + begin + yield handle + ensure + _wv_leave + end + end + + # @api private + # Mints a new strong reference the caller owns (an object written into a + # value buffer carries its own reference). + def _wv_clone_ptr + _wv_call { |ptr| self.class._wv_clone(ptr) } + end + + # dup and clone produce an independent wrapper with its own reference to + # the same object. + def initialize_copy(other) + super + _wv_init(other._wv_clone_ptr) + end + + private + + def _wv_leave + release = @wv_lock.synchronize do + @wv_calls -= 1 + next nil unless @wv_closing && @wv_calls.zero? + + taken = @wv_handle + @wv_handle = nil + @wv_closing = false + taken + end + release&.free + end + end + + # @api private + # Yields the native pointers of `objects` (NULL for nil), each kept alive + # until the block returns. + def self._wv_pin(*objects, &block) + _wv_pin_from(objects, 0, [], &block) + end + + # @api private + def self._wv_pin_from(objects, index, pointers, &block) + return yield(*pointers) if index == objects.length + + object = objects[index] + if object.nil? + _wv_pin_from(objects, index + 1, pointers + [FFI::Pointer::NULL], &block) + else + object._wv_call { |ptr| _wv_pin_from(objects, index + 1, pointers + [ptr], &block) } + end + end + + # @api private + # Checks each top-level module's contract checksum (`module name => + # [checksum function, expected value]`) against the library, so bindings + # generated from a different API fail at require time. + def self._wv_check_contract!(checksums) + checksums.each do |mod, (symbol, expected)| + begin + attach_function symbol, [], :uint64 + rescue FFI::NotFoundError + raise LoadError, "the loaded shapes library has no module '#{mod}' (#{symbol} is missing)" + end + actual = public_send(symbol) + next if actual == expected + + raise LoadError, "shapes module '#{mod}' does not match these bindings: expected contract " \ + "checksum 0x#{expected.to_s(16)}, the library reports 0x#{actual.to_s(16)}; " \ + 'regenerate the bindings' + end + end + + # @api private + # Live native resources of one kind (0 objects, 1 callbacks, 2 iterators, + # 3 cancel tokens, 4 byte allocations). Only libraries built with the + # `leak-check` feature export the counter. + def self._wv_debug_live(kind) + unless respond_to?(:shapes_debug_live) + begin + attach_function :shapes_debug_live, [:int32], :uint64 + rescue FFI::NotFoundError + raise Error.new(GENERIC_ERROR_CODE, 'shapes was built without the leak-check feature') + end + end + shapes_debug_live(kind) + end +end + +# END runtime.rb 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 deleted file mode 100644 index 38854662..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap +++ /dev/null @@ -1,592 +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 - - # 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 = 2 - 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 - - # The runtime-reserved error codes. Positive codes belong to a module's - # declared error domain; these negative codes are programming errors the - # wrappers raise as a plain Error regardless of the function's `throws`. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - - 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 - - # An object token: a non-zero u64 carrying one strong reference the - # caller adopts into a wrapper. - def read_object_token - addr = read_u64 - raise Error.new(-1, 'malformed value buffer: null object token') if addr.zero? - FFI::Pointer.new(addr) - 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_set, [:pointer, :int32, :string], :void - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # === Module: shapes === - - # An algebraic shape (sum type with associated data) - class Shape - # The active variant's integer tag. - def tag - self.class::TAG - end - - # The empty shape (a unit variant) - class Empty < Shape - TAG = 0 - - # Structural equality over the variant and its fields. - def ==(other) - return false unless other.is_a?(Empty) - true - end - end - - # A circle with a radius - class Circle < Shape - TAG = 1 - - # Radius in points - attr_reader :radius - - def initialize(radius:) - @radius = radius - end - - # Structural equality over the variant and its fields. - def ==(other) - return false unless other.is_a?(Circle) - return false unless radius == other.radius - true - end - end - - # An axis-aligned rectangle - class Rectangle < Shape - TAG = 2 - - attr_reader :width - - attr_reader :height - - def initialize(width:, height:) - @width = width - @height = height - end - - # Structural equality over the variant and its fields. - def ==(other) - return false unless other.is_a?(Rectangle) - return false unless width == other.width - return false unless height == other.height - true - end - end - - # An integer-bounded shape (exercises narrow + wide ints) - class Bounded < Shape - TAG = 3 - - attr_reader :min - - attr_reader :max - - attr_reader :weight - - def initialize(min:, max:, weight:) - @min = min - @max = max - @weight = weight - end - - # Structural equality over the variant and its fields. - def ==(other) - return false unless other.is_a?(Bounded) - return false unless min == other.min - return false unless max == other.max - return false unless weight == other.weight - true - end - end - - # A labeled shape with a small count - class Labeled < Shape - TAG = 4 - - attr_reader :label - - attr_reader :count - - def initialize(label:, count:) - @label = label - @count = count - end - - # Structural equality over the variant and its fields. - def ==(other) - return false unless other.is_a?(Labeled) - return false unless label == other.label - return false unless count == other.count - true - end - end - end - - # A plain C-style enum (no payloads) - module Channel - RED = 0 - GREEN = 1 - BLUE = 2 - end - - # One field per supported numeric primitive - class Numerics - attr_reader :a - - attr_reader :b - - attr_reader :c - - attr_reader :d - - attr_reader :e - - attr_reader :f - - attr_reader :g - - attr_reader :h - - attr_reader :i - - attr_reader :j - - def initialize(a:, b:, c:, d:, e:, f:, g:, h:, i:, j:) - @a = a - @b = b - @c = c - @d = d - @e = e - @f = f - @g = g - @h = h - @i = i - @j = j - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Numerics) - return false unless a == other.a - return false unless b == other.b - return false unless c == other.c - return false unless d == other.d - return false unless e == other.e - return false unless f == other.f - return false unless g == other.g - return false unless h == other.h - return false unless i == other.i - return false unless j == other.j - true - end - end - - # @api private - # Packs a Numerics into the value-buffer wire format. - def self._wv_write_numerics(w, v) - w.write_i8(v.a) - w.write_i16(v.b) - w.write_i32(v.c) - w.write_i64(v.d) - w.write_u8(v.e) - w.write_u16(v.f) - w.write_u32(v.g) - w.write_u64(v.h) - w.write_f32(v.i) - w.write_f64(v.j) - end - - # @api private - # Unpacks a Numerics from the value-buffer wire format. - def self._wv_read_numerics(r) - _wv_a = r.read_i8 - _wv_b = r.read_i16 - _wv_c = r.read_i32 - _wv_d = r.read_i64 - _wv_e = r.read_u8 - _wv_f = r.read_u16 - _wv_g = r.read_u32 - _wv_h = r.read_u64 - _wv_i = r.read_f32 - _wv_j = r.read_f64 - Numerics.new(a: _wv_a, b: _wv_b, c: _wv_c, d: _wv_d, e: _wv_e, f: _wv_f, g: _wv_g, h: _wv_h, i: _wv_i, j: _wv_j) - end - - # @api private - # Packs a Shape into the value-buffer wire format. - def self._wv_write_shape(w, v) - case v - when Shape::Empty - w.write_i32(0) - when Shape::Circle - w.write_i32(1) - w.write_f64(v.radius) - when Shape::Rectangle - w.write_i32(2) - w.write_f32(v.width) - w.write_f32(v.height) - when Shape::Bounded - w.write_i32(3) - w.write_i16(v.min) - w.write_i16(v.max) - w.write_u64(v.weight) - when Shape::Labeled - w.write_i32(4) - w.write_string(v.label) - w.write_u8(v.count) - else - raise Error.new(-1, 'unknown Shape variant') - end - end - - # @api private - # Unpacks a Shape from the value-buffer wire format. - def self._wv_read_shape(r) - tag = r.read_i32 - case tag - when 0 - Shape::Empty.new - when 1 - _wv_radius = r.read_f64 - Shape::Circle.new(radius: _wv_radius) - when 2 - _wv_width = r.read_f32 - _wv_height = r.read_f32 - Shape::Rectangle.new(width: _wv_width, height: _wv_height) - when 3 - _wv_min = r.read_i16 - _wv_max = r.read_i16 - _wv_weight = r.read_u64 - Shape::Bounded.new(min: _wv_min, max: _wv_max, weight: _wv_weight) - when 4 - _wv_label = r.read_string - _wv_count = r.read_u8 - Shape::Labeled.new(label: _wv_label, count: _wv_count) - else - raise Error.new(-1, "malformed value buffer: unknown Shape tag #{tag}") - end - end - # Render a shape to a string - attach_function :weaveffi_shapes_describe, [:pointer, :size_t, :pointer], :pointer - # Scale a shape by a factor, returning a new shape - attach_function :weaveffi_shapes_scale, [:pointer, :size_t, :double, :pointer, :pointer], :pointer - # Sum a list of bytes into a wide integer (numerics smoke) - attach_function :weaveffi_shapes_sum_bytes, [:pointer, :size_t, :pointer], :uint64 - # Build a Numerics struct from every numeric primitive - attach_function :weaveffi_shapes_make_numerics, [:int8, :int16, :int32, :int64, :uint8, :uint16, :uint32, :uint64, :float, :double, :pointer, :pointer], :pointer - - # Render a shape to a string - def self.describe(shape) - err = ErrorStruct.new - shape_w = WvBufferWriter.new - _wv_write_shape(shape_w, shape) - shape_data = shape_w.data - shape_buf = FFI::MemoryPointer.new(:uint8, shape_data.bytesize) - shape_buf.put_bytes(0, shape_data) - result = weaveffi_shapes_describe(shape_buf, shape_data.bytesize, err) - check_error!(err) - return '' if result.null? - str = result.read_string.force_encoding(Encoding::UTF_8) - weaveffi_free_string(result) - str - end - - # Scale a shape by a factor, returning a new shape - def self.scale(shape, factor) - err = ErrorStruct.new - shape_w = WvBufferWriter.new - _wv_write_shape(shape_w, shape) - shape_data = shape_w.data - shape_buf = FFI::MemoryPointer.new(:uint8, shape_data.bytesize) - shape_buf.put_bytes(0, shape_data) - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_shapes_scale(shape_buf, shape_data.bytesize, factor, 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_shape(_wv_r) - _wv_r.expect_end! - _wv_value - end - - # Sum a list of bytes into a wide integer (numerics smoke) - def self.sum_bytes(values) - err = ErrorStruct.new - values_buf = FFI::MemoryPointer.new(:uint8, values.bytesize) - values_buf.put_bytes(0, values) - result = weaveffi_shapes_sum_bytes(values_buf, values.bytesize, err) - check_error!(err) - result - end - - # Build a Numerics struct from every numeric primitive - def self.make_numerics(a, b, c, d, e, f, g, h, i, j) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, 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_numerics(_wv_r) - _wv_r.expect_end! - _wv_value - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__shapes_gemspec.snap similarity index 50% rename from crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__weaveffi_gemspec.snap rename to crates/weaveffi-cli/tests/snapshots/ruby_shapes__shapes_gemspec.snap index c132abf7..e7740347 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__weaveffi_gemspec.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__shapes_gemspec.snap @@ -1,18 +1,20 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -# Generated by WeaveFFI [VERSION] from weaveffi.yml +# Generated by WeaveFFI [VERSION] from api.yml # DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o +# To regenerate: weaveffi generate api.yml -o Gem::Specification.new do |s| - s.name = 'weaveffi' + s.name = 'shapes' s.version = '0.1.0' - s.summary = 'weaveffi bindings generated by WeaveFFI' + s.summary = 'Native bindings for shapes' + s.authors = ['shapes'] s.files = Dir['lib/**/*.rb'] s.require_paths = ['lib'] + s.required_ruby_version = '>= 2.7' s.add_dependency 'ffi', '~> 1.15' end -# END weaveffi.gemspec +# END shapes.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__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/swift_docs_everywhere__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Package_swift.snap index 7ec6e100..683f7ccc 100644 --- a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Package_swift.snap +++ b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Package_swift.snap @@ -1,22 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// swift-tools-version:5.9 +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o +import Foundation import PackageDescription +// The C module `CDocsEverywhere` comes from one of two places. Next to this +// manifest, a prebuilt `CDocsEverywhere.xcframework` (assembled from the native +// libraries, see the README) wins. Otherwise `Sources/CDocsEverywhere` declares +// the generated header as a system library that links `docs_everywhere` from the +// linker search path, e.g. `swift build -Xlinker -L/path/to/lib`. +let xcframework = "CDocsEverywhere.xcframework" +let cModule: Target = FileManager.default.fileExists(atPath: "\(Context.packageDirectory)/\(xcframework)") + ? .binaryTarget(name: "CDocsEverywhere", path: xcframework) + : .systemLibrary(name: "CDocsEverywhere") + let package = Package( - name: "WeaveFFI", + name: "DocsEverywhere", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), + .library(name: "DocsEverywhere", targets: ["DocsEverywhere"]), ], targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), + cModule, + .target(name: "DocsEverywhere", dependencies: ["CDocsEverywhere"]), ] ) diff --git a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_CDocsEverywhere_docs_everywhere_h.snap b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_CDocsEverywhere_docs_everywhere_h.snap new file mode 100644 index 00000000..31ab85e2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_CDocsEverywhere_docs_everywhere_h.snap @@ -0,0 +1,196 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef DOCS_EVERYWHERE_H +#define DOCS_EVERYWHERE_H + +#include +#include +#include + +#ifndef DOCS_EVERYWHERE_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef DOCS_EVERYWHERE_BUILD +# define DOCS_EVERYWHERE_API __declspec(dllexport) +# else +# define DOCS_EVERYWHERE_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define DOCS_EVERYWHERE_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define DOCS_EVERYWHERE_API __attribute__((visibility("default"))) +# else +# define DOCS_EVERYWHERE_API +# endif +#endif + +#ifndef DOCS_EVERYWHERE_DEPRECATED +# if defined(_MSC_VER) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define DOCS_EVERYWHERE_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define DOCS_EVERYWHERE_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports docs_everywhere_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define DOCS_EVERYWHERE_ABI_VERSION 3u +DOCS_EVERYWHERE_API uint32_t docs_everywhere_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by docs_everywhere_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct docs_everywhere_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} docs_everywhere_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_set(docs_everywhere_error* err, int32_t code, const char* message); +DOCS_EVERYWHERE_API void docs_everywhere_error_clear(docs_everywhere_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + docs_everywhere_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_error_free(docs_everywhere_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +DOCS_EVERYWHERE_API void docs_everywhere_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct docs_everywhere_cancel_token docs_everywhere_cancel_token; +DOCS_EVERYWHERE_API docs_everywhere_cancel_token* docs_everywhere_cancel_token_create(void); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_cancel(docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API bool docs_everywhere_cancel_token_is_cancelled(const docs_everywhere_cancel_token* token); +DOCS_EVERYWHERE_API void docs_everywhere_cancel_token_destroy(docs_everywhere_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +DOCS_EVERYWHERE_API uint8_t* docs_everywhere_alloc(uint32_t size); +DOCS_EVERYWHERE_API void docs_everywhere_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +DOCS_EVERYWHERE_API uint64_t docs_everywhere_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define DOCS_EVERYWHERE_DOCS_CHECKSUM 0x7950eec1e9cfc819ull +DOCS_EVERYWHERE_API uint64_t docs_everywhere_docs_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with docs_everywhere_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * docs_everywhere_buffer.h declares a C struct and codec for each. + */ + +/** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ +typedef enum { + /** Document is being edited */ + docs_everywhere_docs_Status_Draft = 0, + /** Document is publicly visible */ + docs_everywhere_docs_Status_Published = 1, + /** Document is hidden from listings */ + docs_everywhere_docs_Status_Archived = 2 +} docs_everywhere_docs_Status; +/** Error codes reported by throwing functions in the `docs` module tree. */ +typedef enum { + /** Returned when no document matches the requested id */ + docs_everywhere_docs_DocsErrors_not_found = 1, + /** Raised when attempting to move into a forbidden state */ + docs_everywhere_docs_DocsErrors_invalid_status = 2 +} docs_everywhere_docs_DocsErrors; +typedef struct docs_everywhere_docs_Notebook docs_everywhere_docs_Notebook; +/** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + * + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct docs_everywhere_docs_SavedListener_vtable { + /** Fires after a document has been persisted successfully */ + void (*on_saved)(void* ctx, int64_t doc_id, int32_t revision, docs_everywhere_error* out_err); + void (*free)(void* ctx); +} docs_everywhere_docs_SavedListener_vtable; +typedef void (*docs_everywhere_docs_publish_async_callback)(void* context, docs_everywhere_error* err, bool result); + +// Module: docs +/** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ +/** Create an empty notebook */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_new(docs_everywhere_error* out_err); +/** The largest number of documents one notebook will hold */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_max_documents(docs_everywhere_error* out_err); +/** Add a document, returning its position in the notebook */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_insert(const docs_everywhere_docs_Notebook* self, const uint8_t* document_ptr, size_t document_len, docs_everywhere_error* out_err); +/** Look up a document by identifier, failing when none matches */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_Notebook_find(const docs_everywhere_docs_Notebook* self, int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** The number of documents currently held */ +DOCS_EVERYWHERE_API int32_t docs_everywhere_docs_Notebook_size(const docs_everywhere_docs_Notebook* self, docs_everywhere_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +DOCS_EVERYWHERE_API docs_everywhere_docs_Notebook* docs_everywhere_docs_Notebook_clone(const docs_everywhere_docs_Notebook* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_Notebook_destroy(docs_everywhere_docs_Notebook* self); + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_create_document(const uint8_t* title_ptr, size_t title_len, const uint8_t* tags_ptr, size_t tags_len, size_t* out_len, docs_everywhere_error* out_err); +/** Look up a document by identifier */ +DOCS_EVERYWHERE_API const uint8_t* docs_everywhere_docs_get_document(int64_t id, size_t* out_len, docs_everywhere_error* out_err); +/** Publish a document in the background */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_publish_async(int64_t id, docs_everywhere_docs_publish_async_callback callback, void* context); +/** Register a listener for saved notifications */ +DOCS_EVERYWHERE_API void docs_everywhere_docs_watch(void* listener_ctx, const docs_everywhere_docs_SavedListener_vtable* listener_vtable, docs_everywhere_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // DOCS_EVERYWHERE_H + +// END docs_everywhere.h diff --git a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_CDocsEverywhere_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_CDocsEverywhere_module_modulemap.snap new file mode 100644 index 00000000..9cbbef82 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_CDocsEverywhere_module_modulemap.snap @@ -0,0 +1,14 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +module CDocsEverywhere [system] { + header "docs_everywhere.h" + link "docs_everywhere" + export * +} + +// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__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_docs_everywhere__Sources_DocsEverywhere_DocsEverywhere_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_DocsEverywhere_DocsEverywhere_swift.snap new file mode 100644 index 00000000..15326810 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_DocsEverywhere_DocsEverywhere_swift.snap @@ -0,0 +1,695 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import CDocsEverywhere +import Foundation + +// MARK: - Runtime support + +/// A failure the native library reports outside any declared error domain: +/// an unknown error code or a runtime trap. Throwing wrappers raise it; +/// non-throwing wrappers stop the process with `fatalError` instead, since +/// Swift has no unchecked errors. +public struct DocsEverywhereRuntimeError: Error, LocalizedError, Hashable, Sendable { + /// The code of a generic runtime failure. + public static let genericCode: Int32 = -1 + /// The code of a panic caught at the library boundary. + public static let panicCode: Int32 = -2 + /// The code of an argument or result that failed to marshal. + public static let marshalCode: Int32 = -3 + /// The code of a callback implementation that threw. + public static let foreignCode: Int32 = -4 + + /// The numeric code the native library reported. + public let errorCode: Int32 + /// The message the native library reported. + public let message: String + + /// Creates an error carrying a native code and message. + public init(errorCode: Int32, message: String) { + self.errorCode = errorCode + self.message = message + } + + public var errorDescription: String? { message } +} + +/// The error slot every native call reports through. +typealias WvError = docs_everywhere_error + +/// The C ABI revision these bindings were generated against. +let wvAbiVersion: UInt32 = 3 + +/// The contract check, run once before the first native call: the library +/// must implement this ABI revision and every top-level module must carry +/// the checksum these bindings were generated from. +let wvContract: Void = { + let abi = docs_everywhere_abi_version() + guard abi == wvAbiVersion else { + fatalError("DocsEverywhere: the native library 'docs_everywhere' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") + } + wvCheckModule("docs", docs_everywhere_docs_checksum(), 0x7950eec1e9cfc819) +}() + +/// Runs the contract check if it hasn't run yet. +@inline(__always) +func wvLoad() { + _ = wvContract +} + +/// Stops the process when a module's contract checksum doesn't match. +func wvCheckModule(_ module: StaticString, _ actual: UInt64, _ expected: UInt64) { + guard actual == expected else { + fatalError("DocsEverywhere: module '\(module)' of the native library 'docs_everywhere' doesn't match these bindings (checksum 0x\(String(actual, radix: 16)), expected 0x\(String(expected, radix: 16))); regenerate the bindings or rebuild the library") + } +} + +/// The message carried by an error slot. +func wvErrorMessage(_ err: WvError) -> String { + err.message.map { String(cString: $0) } ?? "" +} + +/// The Swift error for a code outside any declared domain: cancellation +/// (`-5`) becomes `CancellationError`, anything else the runtime error. +func wvRuntimeError(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + return DocsEverywhereRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) +} + +/// Stops the process for a failed call that can't throw, naming the code and +/// message the library reported. +func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { + fatalError("DocsEverywhere.\(function) failed with code \(code): \(message)") +} + +/// Throws the runtime error for a filled error slot, clearing it. +@inline(__always) +func wvCheck(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = wvRuntimeError(err) + docs_everywhere_error_clear(&err) + throw error +} + +/// Stops the process for a filled error slot of a call that can't throw. +@inline(__always) +func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { + guard err.code != 0 else { return } + let code = err.code + let message = wvErrorMessage(err) + docs_everywhere_error_clear(&err) + wvFatal(code, message, function) +} + +/// Maps and releases the heap-boxed error an async completion received. +func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { + let error = map(err.pointee) + docs_everywhere_error_free(err) + return error +} + +/// The error mapping of a cancellable call that declares no error domain: +/// cancellation throws `CancellationError`, anything else stops the process. +func wvCancelledOrTrap(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + wvFatal(err.code, wvErrorMessage(err), "async call") +} + +/// Stops the process for the boxed error of an async call that can't throw. +func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { + wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) +} + +/// Unwraps a pointer the library promised is non-null. +@inline(__always) +func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { + guard let ptr = ptr else { + wvFatal(DocsEverywhereRuntimeError.marshalCode, "the native library returned a null pointer", function) + } + return ptr +} + +/// The Swift enum case for a C-style enum discriminant. +func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { + guard let value = E(rawValue: raw) else { + wvDecodeFailure("unknown \(E.self) discriminant \(raw)") + } + return value +} + +// MARK: Strings, bytes, and buffers + +/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. +@inline(__always) +func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + var s = s + return try s.withUTF8 { try body($0.baseAddress, $0.count) } +} + +/// Lends the bytes of `d` for one call. +@inline(__always) +func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } +} + +/// Lends an encoded value buffer for one call. +@inline(__always) +func wvWithBuffer(_ w: WvWriter, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } +} + +/// Copies a returned string, then releases the library's allocation. +func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + defer { docs_everywhere_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies returned bytes, then releases the library's allocation. +func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + defer { docs_everywhere_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return Data(bytes: ptr, count: len) +} + +/// Decodes a returned value buffer in place, then releases it. +func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + defer { + if let ptr = ptr { docs_everywhere_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + } + return wvBorrowBuffer(ptr, len, decode) +} + +/// Copies a borrowed string. +func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies borrowed bytes. +func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + return Data(bytes: ptr, count: len) +} + +/// Decodes a borrowed value buffer in place. +func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + var r = WvReader(UnsafeRawPointer(ptr), len) + let value = decode(&r) + r.finish() + return value +} + +/// Stops the process on a malformed value buffer, which the wire format +/// treats like a marshalling failure. +func wvDecodeFailure(_ context: String) -> Never { + fatalError("DocsEverywhere: malformed value buffer: \(context)") +} + +/// Serializes values in the value-buffer wire format: little-endian, packed, +/// no alignment. +struct WvWriter { + var bytes: [UInt8] = [] + + init() { + bytes.reserveCapacity(64) + } + + @inline(__always) + private mutating func append(_ v: T) { + withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } + mutating func writeI8(_ v: Int8) { append(v) } + mutating func writeU8(_ v: UInt8) { append(v) } + mutating func writeI16(_ v: Int16) { append(v) } + mutating func writeU16(_ v: UInt16) { append(v) } + mutating func writeI32(_ v: Int32) { append(v) } + mutating func writeU32(_ v: UInt32) { append(v) } + mutating func writeI64(_ v: Int64) { append(v) } + mutating func writeU64(_ v: UInt64) { append(v) } + mutating func writeF32(_ v: Float) { append(v.bitPattern) } + mutating func writeF64(_ v: Double) { append(v.bitPattern) } + + /// Writes a `u32` length or element count. + mutating func writeLen(_ n: Int) { + precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") + append(UInt32(n)) + } + + mutating func writeString(_ v: String) { + let utf8 = v.utf8 + writeLen(utf8.count) + bytes.append(contentsOf: utf8) + } + + mutating func writeBytes(_ v: Data) { + writeLen(v.count) + bytes.append(contentsOf: v) + } + + mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } + + /// Writes an object token. `p` must be a strong reference the buffer now + /// owns (a freshly cloned pointer), never one a wrapper still holds. + mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } +} + +/// Deserializes values from the value-buffer wire format directly from the +/// library's memory, rejecting truncated buffers, invalid flag bytes, +/// oversized length prefixes, and trailing bytes. +struct WvReader { + private let base: UnsafeRawPointer? + private let count: Int + private var pos = 0 + + init(_ base: UnsafeRawPointer?, _ count: Int) { + self.base = base + self.count = base == nil ? 0 : count + } + + /// The number of unread bytes. + var remaining: Int { count - pos } + + private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { + guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } + defer { pos += n } + return base + pos + } + + @inline(__always) + private mutating func load(_ type: T.Type, _ what: StaticString) -> T { + T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) + } + + mutating func readBool() -> Bool { + switch load(UInt8.self, "bool") { + case 0: return false + case 1: return true + default: wvDecodeFailure("bool byte out of range") + } + } + mutating func readI8() -> Int8 { load(Int8.self, "i8") } + mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } + mutating func readI16() -> Int16 { load(Int16.self, "i16") } + mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } + mutating func readI32() -> Int32 { load(Int32.self, "i32") } + mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } + mutating func readI64() -> Int64 { load(Int64.self, "i64") } + mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } + mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } + mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } + + /// Reads a byte length, which can't exceed the unread bytes. + mutating func readLen() -> Int { + let n = Int(load(UInt32.self, "length")) + guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } + return n + } + + /// Reads an element count. Elements may encode to zero bytes, so the + /// count isn't bounded by the unread bytes; callers cap preallocation. + mutating func readCount() -> Int { + Int(load(UInt32.self, "count")) + } + + mutating func readString() -> String { + let n = readLen() + if n == 0 { return "" } + let p = take(n, "string") + guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { + wvDecodeFailure("string is not valid UTF-8") + } + return s + } + + mutating func readBytes() -> Data { + let n = readLen() + if n == 0 { return Data() } + return Data(bytes: take(n, "bytes"), count: n) + } + + mutating func readOptionFlag() -> Bool { + switch load(UInt8.self, "option flag") { + case 0: return false + case 1: return true + default: wvDecodeFailure("option flag byte out of range") + } + } + + /// Reads an object token: one strong reference the caller adopts into a + /// wrapper whose deinit releases it. + mutating func readObject() -> OpaquePointer { + guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { + wvDecodeFailure("null object token") + } + return p + } + + func finish() { + if remaining != 0 { wvDecodeFailure("trailing bytes after value") } + } +} + +// MARK: Async and callbacks + +/// A native cancel token owned by one cancellable call. The library takes its +/// own reference at launch, so this one is released whenever the wrapper is +/// done with it. +final class WvCancelToken: @unchecked Sendable { + let raw: OpaquePointer + + init() { + raw = wvNonNull(docs_everywhere_cancel_token_create()) + } + + deinit { + docs_everywhere_cancel_token_destroy(raw) + } + + func cancel() { + docs_everywhere_cancel_token_cancel(raw) + } +} + +/// A continuation boxed for the C `context` slot of an async launch. +final class WvContinuation { + let value: CheckedContinuation + + init(_ value: CheckedContinuation) { + self.value = value + } +} + +/// One process-wide callback vtable at a stable address the library may hold +/// for the life of the process. +final class WvVtable: @unchecked Sendable { + let pointer: UnsafePointer + + init(_ value: T) { + let cell = UnsafeMutablePointer.allocate(capacity: 1) + cell.initialize(to: value) + pointer = UnsafePointer(cell) + } +} + +/// Reports a thrown Swift error to the library as a callback failure +/// (code -4); the library aborts its current call with that code and message. +func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { + let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) + message.withCString { docs_everywhere_error_set(outErr, DocsEverywhereRuntimeError.foreignCode, $0) } +} + +// MARK: - API + +/// Typed errors reported by the `docs` module. +public enum DocsErrorsError: Error, LocalizedError, Sendable { + /// Returned when no document matches the requested id + case notFound(message: String) + /// Raised when attempting to move into a forbidden state + case invalidStatus(message: String) + + /// The message the library reported, or the code's documented default. + public var errorDescription: String? { + switch self { + case let .notFound(message): return message + case let .invalidStatus(message): return message + } + } + + /// The numeric ABI code carried by this error. + public var errorCode: Int32 { + switch self { + case .notFound: return 1 + case .invalidStatus: return 2 + } + } +} + +func mapDocs(_ err: WvError) -> Error { + let message = wvErrorMessage(err) + switch err.code { + case 1: return DocsErrorsError.notFound(message: message.isEmpty ? "Document not found" : message) + case 2: return DocsErrorsError.invalidStatus(message: message.isEmpty ? "Invalid status transition" : message) + default: return wvRuntimeError(err) + } +} + +func checkDocs(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = mapDocs(err) + docs_everywhere_error_clear(&err) + throw error +} + +/// Lifecycle status of a document. +/// +/// Used to drive UI badges and filters. +public enum Status: Int32, Sendable { + /// Document is being edited + case draft = 0 + /// Document is publicly visible + case published = 1 + /// Document is hidden from listings + case archived = 2 +} + +/// Represents a single document tracked by the system. +/// +/// Documents are persisted to disk and exposed via the public API. +public struct Document: Hashable, Sendable { + /// Stable opaque identifier + public var id: Int64 + /// Human-readable title shown in lists + public var title: String + /// Current lifecycle status + public var status: Status + /// Free-form tags for grouping + public var tags: [String] + + /// Creates a value from its fields. + public init(id: Int64, title: String, status: Status, tags: [String]) { + self.id = id + self.title = title + self.status = status + self.tags = tags + } +} + +func wvWriteDocument(_ value: Document, into w: inout WvWriter) { + w.writeI64(value.id) + w.writeString(value.title) + w.writeI32(value.status.rawValue) + w.writeLen(value.tags.count) + for v0 in value.tags { + w.writeString(v0) + } +} + +func wvReadDocument(_ r: inout WvReader) -> Document { + let v1 = r.readI64() + let v2 = r.readString() + let v3 = wvEnumCase(Status.self, r.readI32()) + let n5 = r.readCount() + var v4: [String] = [] + v4.reserveCapacity(min(n5, r.remaining)) + for _ in 0..(docs_everywhere_docs_SavedListener_vtable( + on_saved: { ctx, doc_id, revision, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + try wvBox.impl.onSaved(docId: doc_id, revision: revision) + } catch { + wvForeignError(out_err, error) + } + }, + free: { ctx in + Unmanaged.fromOpaque(ctx!).release() + } + )) +} + +/// An ordered collection of documents. +/// +/// Notebooks own their documents and release them when destroyed. +public final class Notebook: @unchecked Sendable { + let ptr: OpaquePointer + + init(ptr: OpaquePointer) { + self.ptr = ptr + } + + deinit { + docs_everywhere_docs_Notebook_destroy(ptr) + } + + /// Returns a new strong reference to the same object, for a position that + /// takes ownership of it (an object token inside a value buffer). + func clonePtr() -> OpaquePointer { + wvNonNull(docs_everywhere_docs_Notebook_clone(ptr)) + } + + /// Create an empty notebook + public init() { + wvLoad() + var err = WvError() + let rv = docs_everywhere_docs_Notebook_new(&err) + wvTrap(&err) + self.ptr = wvNonNull(rv) + } + + /// Add a document, returning its position in the notebook + /// - Parameter document: The document to add + public func insert(document: Document) -> Int32 { + var err = WvError() + var documentWriter = WvWriter() + wvWriteDocument(document, into: &documentWriter) + let rv: Int32 = wvWithBuffer(documentWriter) { document_ptr, document_len in + docs_everywhere_docs_Notebook_insert(ptr, document_ptr, document_len, &err) + } + wvTrap(&err) + return rv + } + + /// Look up a document by identifier, failing when none matches + /// - Parameter id: Identifier of the document to find + public func find(id: Int64) throws -> Document { + var err = WvError() + var outLen = 0 + let rv = docs_everywhere_docs_Notebook_find(ptr, id, &outLen, &err) + try checkDocs(&err) + return wvTakeBuffer(rv, outLen, wvReadDocument) + } + + /// The number of documents currently held + public func size() -> Int32 { + var err = WvError() + let rv = docs_everywhere_docs_Notebook_size(ptr, &err) + wvTrap(&err) + return rv + } + + /// The largest number of documents one notebook will hold + public static func maxDocuments() -> Int32 { + wvLoad() + var err = WvError() + let rv = docs_everywhere_docs_Notebook_max_documents(&err) + wvTrap(&err) + return rv + } +} + +public enum Docs { + /// Create a brand new document. + /// + /// The document starts in `Draft` status; callers must publish + /// it explicitly to make it visible. + /// - Parameter title: Human-readable title for the new document + /// - Parameter tags: Initial set of tags (may be empty) + public static func createDocument(title: String, tags: [String]) -> Document { + wvLoad() + var err = WvError() + var tagsWriter = WvWriter() + tagsWriter.writeLen(tags.count) + for v0 in tags { + tagsWriter.writeString(v0) + } + var outLen = 0 + let rv: UnsafePointer? = wvWithUTF8(title) { title_ptr, title_len in + wvWithBuffer(tagsWriter) { tags_ptr, tags_len in + docs_everywhere_docs_create_document(title_ptr, title_len, tags_ptr, tags_len, &outLen, &err) + } + } + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadDocument) + } + + /// Look up a document by identifier + /// - Parameter id: Identifier returned from create_document + public static func getDocument(id: Int64) -> Document? { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = docs_everywhere_docs_get_document(id, &outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen) { (r: inout WvReader) -> Document? in + var v0: Document? = nil + if r.readOptionFlag() { + let v1 = wvReadDocument(&r) + v0 = v1 + } + return v0 + } + } + + /// Publish a document in the background + /// - Parameter id: Identifier of the document to publish + public static func publishAsync(id: Int64) async -> Bool { + wvLoad() + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + docs_everywhere_docs_publish_async(id, { context, err, result in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + wvTrapBoxed(err) + } + cont.resume(returning: result) + }, context) + } + } + + /// Register a listener for saved notifications + /// - Parameter listener: Receiver of saved notifications + public static func watch(listener: SavedListener) { + wvLoad() + var err = WvError() + let listener_ctx = Unmanaged.passRetained(WvSavedListenerBox(listener)).toOpaque() + docs_everywhere_docs_watch(listener_ctx, WvSavedListenerVtable.shared.pointer, &err) + wvTrap(&err) + } +} + +// END DocsEverywhere.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index dce3741d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_docs_everywhere__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,489 +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) } - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { writeUInt64(UInt64(UInt(bitPattern: p))) } -} - -/// 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") - } - } - /// Reads an object token: one strong reference the caller must adopt - /// into a wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(readUInt64())) else { - wvDecodeFailure("null object token") - } - return p - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -private final class ContinuationRef { - let value: CheckedContinuation - init(_ value: CheckedContinuation) { self.value = value } -} - -/// Reports a thrown Swift error to the producer as a foreign callback failure -/// (code -4); the producer aborts its current call with that code and message. -func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { weaveffi_error_set(outErr, -4, $0) } -} - -/// Typed errors reported by the `docs` module. -public enum DocsErrorsError: Error, LocalizedError { - /// Returned when no document matches the requested id - case notFound(message: String) - /// Raised when attempting to move into a forbidden state - case invalidStatus(message: String) - public var errorDescription: String? { - switch self { - case let .notFound(message): return message - case let .invalidStatus(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .notFound: return 1 - case .invalidStatus: return 2 - } - } -} - -@inline(__always) -func mapDocs(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1: return DocsErrorsError.notFound(message: message.isEmpty ? "Document not found" : message) - case 2: return DocsErrorsError.invalidStatus(message: message.isEmpty ? "Invalid status transition" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkDocs(_ 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 mapDocs(code: code, message: message, payload: payload) - } -} - -/// Lifecycle status of a document. -/// -/// Used to drive UI badges and filters. -public enum Status: UInt32 { - /// Document is being edited - case draft = 0 - /// Document is publicly visible - case published = 1 - /// Document is hidden from listings - case archived = 2 -} - -/// Represents a single document tracked by the system. -/// -/// Documents are persisted to disk and exposed via the public API. -public struct Document { - /// Stable opaque identifier - public var id: Int64 - /// Human-readable title shown in lists - public var title: String - /// Current lifecycle status - public var status: Status - /// Free-form tags for grouping - public var tags: [String] - - /// Creates a `Document` value. - public init(id: Int64, title: String, status: Status, tags: [String]) { - self.id = id - self.title = title - self.status = status - self.tags = tags - } -} - -/// Serializes a `Document` into the value-buffer wire format. -func wvWriteDocument(_ value: Document, into w: inout WvWriter) { - w.writeInt64(value.id) - w.writeString(value.title) - w.writeInt32(Int32(bitPattern: value.status.rawValue)) - w.writeLen(value.tags.count) - for v0 in value.tags { - w.writeString(v0) - } -} - -/// Deserializes a `Document` from the value-buffer wire format. -func wvReadDocument(_ r: inout WvReader) -> Document { - let v1 = r.readInt64() - let v2 = r.readString() - let v3 = Status(rawValue: UInt32(bitPattern: r.readInt32()))! - let n5 = r.readLen() - var v4: [String] = [] - v4.reserveCapacity(n5) - for _ in 0...fromOpaque(ctx!).takeUnretainedValue() - do { - try wvBox.impl.onSaved(docId: doc_id, revision: revision) - } catch { - wvForeignError(out_err, error) - } - }, - free: { ctx in - Unmanaged.fromOpaque(ctx!).release() - } - ) - static let pointer: UnsafePointer = { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - return UnsafePointer(cell) - }() -} - -/// An ordered collection of documents. -/// -/// Notebooks own their documents and release them when destroyed. -public final class Notebook { - let ptr: OpaquePointer - - init(ptr: OpaquePointer) { - self.ptr = ptr - } - - deinit { - weaveffi_docs_Notebook_destroy(ptr) - } - - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it (an object token inside a value buffer). - func clonePtr() -> OpaquePointer { - weaveffi_docs_Notebook_clone(ptr)! - } - /// Create an empty notebook - public init() { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_docs_Notebook_new(&err) - trap(&err) - guard let rv = rv else { fatalError("-1: null pointer") } - self.ptr = rv - } - /// Add a document, returning its position in the notebook - /// - Parameter document: The document to add - public func insert(document: Document) -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var documentWriter = WvWriter() - wvWriteDocument(document, into: &documentWriter) - let rv: Int32 = documentWriter.bytes.withUnsafeBufferPointer { document_buf in - let document_ptr = document_buf.baseAddress - let document_len = document_buf.count - return weaveffi_docs_Notebook_insert(ptr, document_ptr, document_len, &err) - } - trap(&err) - return rv - } - /// Look up a document by identifier, failing when none matches - /// - Parameter id: Identifier of the document to find - public func find(id: Int64) throws -> Document { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_docs_Notebook_find(ptr, id, &outLen, &err) - try checkDocs(&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 = wvReadDocument(&rvReader) - rvReader.finish() - return v0 - } - /// The number of documents currently held - public func size() -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_docs_Notebook_size(ptr, &err) - trap(&err) - return rv - } - /// The largest number of documents one notebook will hold - public static func maxDocuments() -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_docs_Notebook_max_documents(&err) - trap(&err) - return rv - } -} - -public enum Docs { - /// Create a brand new document. - /// - /// The document starts in `Draft` status; callers must publish - /// it explicitly to make it visible. - /// - Parameter title: Human-readable title for the new document - /// - Parameter tags: Initial set of tags (may be empty) - public static func createDocument(title: String, tags: [String]) -> Document { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var tagsWriter = WvWriter() - tagsWriter.writeLen(tags.count) - for v0 in tags { - tagsWriter.writeString(v0) - } - var outLen: Int = 0 - let rv: UnsafePointer? = title.withCString { title_ptr in - return tagsWriter.bytes.withUnsafeBufferPointer { tags_buf in - let tags_ptr = tags_buf.baseAddress - let tags_len = tags_buf.count - return weaveffi_docs_create_document(title_ptr, tags_ptr, tags_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 v1 = wvReadDocument(&rvReader) - rvReader.finish() - return v1 - } - /// Look up a document by identifier - /// - Parameter id: Identifier returned from create_document - public static func getDocument(id: Int64) -> Document? { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_docs_get_document(id, &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 v0: Document? = nil - if rvReader.readOptionFlag() { - let v1 = wvReadDocument(&rvReader) - v0 = v1 - } - rvReader.finish() - return v0 - } - /// Publish a document in the background - /// - Parameter id: Identifier of the document to publish - public static func publishAsync(id: Int64) async -> Bool { - await withCheckedContinuation { (continuation: CheckedContinuation) in - let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() - weaveffi_docs_publish_async_async(id, { 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) - } - } - /// Register a listener for saved notifications - /// - Parameter listener: Receiver of saved notifications - public static func watch(listener: SavedListener) -> Void { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let listener_ctx = Unmanaged.passRetained(WvSavedListenerBox(listener)).toOpaque() - weaveffi_docs_watch(listener_ctx, WvSavedListenerVtable.pointer, &err) - trap(&err) - } -} - -// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Package_swift.snap index 7ec6e100..abd466d1 100644 --- a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Package_swift.snap +++ b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Package_swift.snap @@ -1,22 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// swift-tools-version:5.9 +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o +import Foundation import PackageDescription +// The C module `CEdgeCases` comes from one of two places. Next to this +// manifest, a prebuilt `CEdgeCases.xcframework` (assembled from the native +// libraries, see the README) wins. Otherwise `Sources/CEdgeCases` declares +// the generated header as a system library that links `edge_cases` from the +// linker search path, e.g. `swift build -Xlinker -L/path/to/lib`. +let xcframework = "CEdgeCases.xcframework" +let cModule: Target = FileManager.default.fileExists(atPath: "\(Context.packageDirectory)/\(xcframework)") + ? .binaryTarget(name: "CEdgeCases", path: xcframework) + : .systemLibrary(name: "CEdgeCases") + let package = Package( - name: "WeaveFFI", + name: "EdgeCases", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), + .library(name: "EdgeCases", targets: ["EdgeCases"]), ], targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), + cModule, + .target(name: "EdgeCases", dependencies: ["CEdgeCases"]), ] ) diff --git a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CEdgeCases_edge_cases_h.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CEdgeCases_edge_cases_h.snap new file mode 100644 index 00000000..94864086 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CEdgeCases_edge_cases_h.snap @@ -0,0 +1,245 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef EDGE_CASES_H +#define EDGE_CASES_H + +#include +#include +#include + +#ifndef EDGE_CASES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef EDGE_CASES_BUILD +# define EDGE_CASES_API __declspec(dllexport) +# else +# define EDGE_CASES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define EDGE_CASES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define EDGE_CASES_API __attribute__((visibility("default"))) +# else +# define EDGE_CASES_API +# endif +#endif + +#ifndef EDGE_CASES_DEPRECATED +# if defined(_MSC_VER) +# define EDGE_CASES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define EDGE_CASES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define EDGE_CASES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports edge_cases_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define EDGE_CASES_ABI_VERSION 3u +EDGE_CASES_API uint32_t edge_cases_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by edge_cases_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct edge_cases_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} edge_cases_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +EDGE_CASES_API void edge_cases_error_set(edge_cases_error* err, int32_t code, const char* message); +EDGE_CASES_API void edge_cases_error_clear(edge_cases_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + edge_cases_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +EDGE_CASES_API void edge_cases_error_free(edge_cases_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +EDGE_CASES_API void edge_cases_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct edge_cases_cancel_token edge_cases_cancel_token; +EDGE_CASES_API edge_cases_cancel_token* edge_cases_cancel_token_create(void); +EDGE_CASES_API void edge_cases_cancel_token_cancel(edge_cases_cancel_token* token); +EDGE_CASES_API bool edge_cases_cancel_token_is_cancelled(const edge_cases_cancel_token* token); +EDGE_CASES_API void edge_cases_cancel_token_destroy(edge_cases_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +EDGE_CASES_API uint8_t* edge_cases_alloc(uint32_t size); +EDGE_CASES_API void edge_cases_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +EDGE_CASES_API uint64_t edge_cases_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define EDGE_CASES_EDGE_CASES_CHECKSUM 0x1567f441073bd4aaull +EDGE_CASES_API uint64_t edge_cases_edge_cases_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with edge_cases_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * edge_cases_buffer.h declares a C struct and codec for each. + */ + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +typedef enum { edge_cases_edge_cases_Legacy_None = 0, edge_cases_edge_cases_Legacy_Default = 5, edge_cases_edge_cases_Legacy_Class = 10 } edge_cases_edge_cases_Legacy; +typedef enum { edge_cases_edge_cases_Mode_Fast = 1, edge_cases_edge_cases_Mode_Safe = 2 } edge_cases_edge_cases_Mode; +/** Error codes reported by throwing functions in the `edge_cases` module tree. */ +typedef enum { edge_cases_edge_cases_EdgeErrors_Busy = 7 } edge_cases_edge_cases_EdgeErrors; +typedef struct edge_cases_edge_cases_Widget edge_cases_edge_cases_Widget; +typedef struct edge_cases_edge_cases_Vault edge_cases_edge_cases_Vault; +typedef struct edge_cases_edge_cases_WidgetStreamIterator edge_cases_edge_cases_WidgetStreamIterator; +typedef struct edge_cases_edge_cases_StringStreamIterator edge_cases_edge_cases_StringStreamIterator; +typedef struct edge_cases_edge_cases_ScalarStreamIterator edge_cases_edge_cases_ScalarStreamIterator; +typedef struct edge_cases_edge_cases_ModeStreamIterator edge_cases_edge_cases_ModeStreamIterator; +/** + * Callback interface carrying composite payloads and objects + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_BatchListener_vtable { + /** Composite payloads cross as value buffers */ + void (*on_batch)(void* ctx, const uint8_t* items_ptr, size_t items_len, const uint8_t* raw_ptr, size_t raw_len, const uint8_t* first_ptr, size_t first_len, edge_cases_edge_cases_Mode mode, edge_cases_error* out_err); + /** Objects cross as strong references the consumer must release */ + void (*on_widget)(void* ctx, edge_cases_edge_cases_Widget* widget, edge_cases_edge_cases_Widget* fallback, const uint8_t* children_ptr, size_t children_len, edge_cases_error* out_err); + void (*on_tick)(void* ctx, edge_cases_error* out_err); + /** Scalar return travels back through the vtable */ + bool (*should_continue)(void* ctx, int32_t class_, edge_cases_error* out_err); + /** Enum return travels back as its integer discriminant */ + edge_cases_edge_cases_Mode (*pick_mode)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_BatchListener_vtable; +/** + * Callback interface whose name and methods are keywords + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct edge_cases_edge_cases_Keyword_vtable { + void (*delete_)(void* ctx, int32_t self, edge_cases_error* out_err); + int32_t (*import)(void* ctx, edge_cases_error* out_err); + void (*free)(void* ctx); +} edge_cases_edge_cases_Keyword_vtable; +typedef void (*edge_cases_edge_cases_async_widget_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Widget* result); +typedef void (*edge_cases_edge_cases_async_void_callback)(void* context, edge_cases_error* err); +typedef void (*edge_cases_edge_cases_async_bytes_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_struct_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_list_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_optional_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_async_enum_callback)(void* context, edge_cases_error* err, edge_cases_edge_cases_Mode result); +typedef void (*edge_cases_edge_cases_async_keywords_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*edge_cases_edge_cases_Widget_fetch_callback)(void* context, edge_cases_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: edge_cases +/** Interface with no constructor; instances come from make_widget */ +/** Void method that can fail */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_refresh(const edge_cases_edge_cases_Widget* self, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_rename(const edge_cases_edge_cases_Widget* self, const uint8_t* class_ptr, size_t class_len, const uint8_t* import_ptr, size_t import_len, size_t* out_len, edge_cases_error* out_err); +/** Async method with a struct result */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_fetch(const edge_cases_edge_cases_Widget* self, int64_t id, edge_cases_edge_cases_Widget_fetch_callback callback, void* context); +EDGE_CASES_DEPRECATED("Use rename instead") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_Widget_old_name(const edge_cases_edge_cases_Widget* self, size_t* out_len, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Widget_clone(const edge_cases_edge_cases_Widget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Widget_destroy(edge_cases_edge_cases_Widget* self); + +/** Deprecated interface with a throwing constructor */ +/** Fails when the vault is busy */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_open(const uint8_t* path_ptr, size_t path_len, edge_cases_error* out_err); +/** Throwing static */ +EDGE_CASES_API bool edge_cases_edge_cases_Vault_probe(edge_cases_error* out_err); +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_Vault_peek(const edge_cases_edge_cases_Vault* self, edge_cases_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +EDGE_CASES_API edge_cases_edge_cases_Vault* edge_cases_edge_cases_Vault_clone(const edge_cases_edge_cases_Vault* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +EDGE_CASES_API void edge_cases_edge_cases_Vault_destroy(edge_cases_edge_cases_Vault* self); + +/** No parameters and no result */ +EDGE_CASES_API void edge_cases_edge_cases_noop(edge_cases_error* out_err); +/** Function named after a keyword whose parameters are all keywords */ +EDGE_CASES_API int32_t edge_cases_edge_cases_select(int32_t class_, const uint8_t* from_ptr, size_t from_len, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, edge_cases_error* out_err); +/** Optional list of optional structs, the deepest legal nesting */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_optional_params(const uint8_t* text_ptr, size_t text_len, const uint8_t* count_ptr, size_t count_len, const uint8_t* outer_ptr, size_t outer_len, const uint8_t* tags_ptr, size_t tags_len, const uint8_t* blob_ptr, size_t blob_len, const uint8_t* mode_ptr, size_t mode_len, size_t* out_len, edge_cases_error* out_err); +/** Two callback interface parameters on one function */ +EDGE_CASES_API void edge_cases_edge_cases_register_batch(void* listener_ctx, const edge_cases_edge_cases_BatchListener_vtable* listener_vtable, void* keyword_ctx, const edge_cases_edge_cases_Keyword_vtable* keyword_vtable, edge_cases_error* out_err); +/** Objects in every buffered position */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_widgets_everywhere(const uint8_t* list_ptr, size_t list_len, const uint8_t* by_name_ptr, size_t by_name_len, const uint8_t* sparse_ptr, size_t sparse_len, const uint8_t* holder_ptr, size_t holder_len, size_t* out_len, edge_cases_error* out_err); +/** Iterator of objects */ +EDGE_CASES_API edge_cases_edge_cases_WidgetStreamIterator* edge_cases_edge_cases_widget_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_WidgetStreamIterator_next(edge_cases_edge_cases_WidgetStreamIterator* iter, edge_cases_edge_cases_Widget** out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_WidgetStreamIterator_destroy(edge_cases_edge_cases_WidgetStreamIterator* iter); +/** Async returning an object */ +EDGE_CASES_API void edge_cases_edge_cases_async_widget(edge_cases_edge_cases_async_widget_callback callback, void* context); +/** Factory for an interface with no constructor */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_make_widget(const uint8_t* label_ptr, size_t label_len, edge_cases_error* out_err); +/** Optional interface return */ +EDGE_CASES_API edge_cases_edge_cases_Widget* edge_cases_edge_cases_find_widget(int64_t id, edge_cases_error* out_err); +/** Interface parameter, including an optional one */ +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_inspect_widget(const edge_cases_edge_cases_Widget* widget, const edge_cases_edge_cases_Widget* fallback, size_t* out_len, edge_cases_error* out_err); +/** Iterator of strings */ +EDGE_CASES_API edge_cases_edge_cases_StringStreamIterator* edge_cases_edge_cases_string_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_StringStreamIterator_next(edge_cases_edge_cases_StringStreamIterator* iter, const uint8_t** out_item, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_StringStreamIterator_destroy(edge_cases_edge_cases_StringStreamIterator* iter); +/** Iterator of scalars */ +EDGE_CASES_API edge_cases_edge_cases_ScalarStreamIterator* edge_cases_edge_cases_scalar_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ScalarStreamIterator_next(edge_cases_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ScalarStreamIterator_destroy(edge_cases_edge_cases_ScalarStreamIterator* iter); +/** Iterator of enums */ +EDGE_CASES_API edge_cases_edge_cases_ModeStreamIterator* edge_cases_edge_cases_mode_stream(edge_cases_error* out_err); +EDGE_CASES_API int32_t edge_cases_edge_cases_ModeStreamIterator_next(edge_cases_edge_cases_ModeStreamIterator* iter, edge_cases_edge_cases_Mode* out_item, edge_cases_error* out_err); +EDGE_CASES_API void edge_cases_edge_cases_ModeStreamIterator_destroy(edge_cases_edge_cases_ModeStreamIterator* iter); +/** Async with no result */ +EDGE_CASES_API void edge_cases_edge_cases_async_void(edge_cases_edge_cases_async_void_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_bytes(uint32_t size, edge_cases_edge_cases_async_bytes_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_struct(edge_cases_edge_cases_async_struct_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_list(edge_cases_cancel_token* cancel_token, edge_cases_edge_cases_async_list_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_optional(edge_cases_edge_cases_async_optional_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_enum(edge_cases_edge_cases_async_enum_callback callback, void* context); +EDGE_CASES_API void edge_cases_edge_cases_async_keywords(const uint8_t* class_ptr, size_t class_len, int32_t import, edge_cases_edge_cases_async_keywords_callback callback, void* context); +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, edge_cases_error* out_err); +EDGE_CASES_DEPRECATED("Use Mode-based APIs") +EDGE_CASES_API const uint8_t* edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy value, size_t* out_len, edge_cases_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // EDGE_CASES_H + +// END edge_cases.h diff --git a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CEdgeCases_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CEdgeCases_module_modulemap.snap new file mode 100644 index 00000000..d8186d40 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CEdgeCases_module_modulemap.snap @@ -0,0 +1,14 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +module CEdgeCases [system] { + header "edge_cases.h" + link "edge_cases" + export * +} + +// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__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_edge_cases__Sources_EdgeCases_EdgeCases_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_EdgeCases_EdgeCases_swift.snap new file mode 100644 index 00000000..eac2dbe9 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_EdgeCases_EdgeCases_swift.snap @@ -0,0 +1,1863 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import CEdgeCases +import Foundation + +// MARK: - Runtime support + +/// A failure the native library reports outside any declared error domain: +/// an unknown error code or a runtime trap. Throwing wrappers raise it; +/// non-throwing wrappers stop the process with `fatalError` instead, since +/// Swift has no unchecked errors. +public struct EdgeCasesRuntimeError: Error, LocalizedError, Hashable, Sendable { + /// The code of a generic runtime failure. + public static let genericCode: Int32 = -1 + /// The code of a panic caught at the library boundary. + public static let panicCode: Int32 = -2 + /// The code of an argument or result that failed to marshal. + public static let marshalCode: Int32 = -3 + /// The code of a callback implementation that threw. + public static let foreignCode: Int32 = -4 + + /// The numeric code the native library reported. + public let errorCode: Int32 + /// The message the native library reported. + public let message: String + + /// Creates an error carrying a native code and message. + public init(errorCode: Int32, message: String) { + self.errorCode = errorCode + self.message = message + } + + public var errorDescription: String? { message } +} + +/// The error slot every native call reports through. +typealias WvError = edge_cases_error + +/// The C ABI revision these bindings were generated against. +let wvAbiVersion: UInt32 = 3 + +/// The contract check, run once before the first native call: the library +/// must implement this ABI revision and every top-level module must carry +/// the checksum these bindings were generated from. +let wvContract: Void = { + let abi = edge_cases_abi_version() + guard abi == wvAbiVersion else { + fatalError("EdgeCases: the native library 'edge_cases' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") + } + wvCheckModule("edge_cases", edge_cases_edge_cases_checksum(), 0x1567f441073bd4aa) +}() + +/// Runs the contract check if it hasn't run yet. +@inline(__always) +func wvLoad() { + _ = wvContract +} + +/// Stops the process when a module's contract checksum doesn't match. +func wvCheckModule(_ module: StaticString, _ actual: UInt64, _ expected: UInt64) { + guard actual == expected else { + fatalError("EdgeCases: module '\(module)' of the native library 'edge_cases' doesn't match these bindings (checksum 0x\(String(actual, radix: 16)), expected 0x\(String(expected, radix: 16))); regenerate the bindings or rebuild the library") + } +} + +/// The message carried by an error slot. +func wvErrorMessage(_ err: WvError) -> String { + err.message.map { String(cString: $0) } ?? "" +} + +/// The Swift error for a code outside any declared domain: cancellation +/// (`-5`) becomes `CancellationError`, anything else the runtime error. +func wvRuntimeError(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + return EdgeCasesRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) +} + +/// Stops the process for a failed call that can't throw, naming the code and +/// message the library reported. +func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { + fatalError("EdgeCases.\(function) failed with code \(code): \(message)") +} + +/// Throws the runtime error for a filled error slot, clearing it. +@inline(__always) +func wvCheck(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = wvRuntimeError(err) + edge_cases_error_clear(&err) + throw error +} + +/// Stops the process for a filled error slot of a call that can't throw. +@inline(__always) +func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { + guard err.code != 0 else { return } + let code = err.code + let message = wvErrorMessage(err) + edge_cases_error_clear(&err) + wvFatal(code, message, function) +} + +/// Maps and releases the heap-boxed error an async completion received. +func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { + let error = map(err.pointee) + edge_cases_error_free(err) + return error +} + +/// The error mapping of a cancellable call that declares no error domain: +/// cancellation throws `CancellationError`, anything else stops the process. +func wvCancelledOrTrap(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + wvFatal(err.code, wvErrorMessage(err), "async call") +} + +/// Stops the process for the boxed error of an async call that can't throw. +func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { + wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) +} + +/// Unwraps a pointer the library promised is non-null. +@inline(__always) +func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { + guard let ptr = ptr else { + wvFatal(EdgeCasesRuntimeError.marshalCode, "the native library returned a null pointer", function) + } + return ptr +} + +/// The Swift enum case for a C-style enum discriminant. +func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { + guard let value = E(rawValue: raw) else { + wvDecodeFailure("unknown \(E.self) discriminant \(raw)") + } + return value +} + +// MARK: Strings, bytes, and buffers + +/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. +@inline(__always) +func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + var s = s + return try s.withUTF8 { try body($0.baseAddress, $0.count) } +} + +/// Lends the bytes of `d` for one call. +@inline(__always) +func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } +} + +/// Lends an encoded value buffer for one call. +@inline(__always) +func wvWithBuffer(_ w: WvWriter, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } +} + +/// Copies a returned string, then releases the library's allocation. +func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + defer { edge_cases_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies returned bytes, then releases the library's allocation. +func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + defer { edge_cases_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return Data(bytes: ptr, count: len) +} + +/// Decodes a returned value buffer in place, then releases it. +func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + defer { + if let ptr = ptr { edge_cases_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + } + return wvBorrowBuffer(ptr, len, decode) +} + +/// Copies a borrowed string. +func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies borrowed bytes. +func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + return Data(bytes: ptr, count: len) +} + +/// Decodes a borrowed value buffer in place. +func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + var r = WvReader(UnsafeRawPointer(ptr), len) + let value = decode(&r) + r.finish() + return value +} + +/// Stops the process on a malformed value buffer, which the wire format +/// treats like a marshalling failure. +func wvDecodeFailure(_ context: String) -> Never { + fatalError("EdgeCases: malformed value buffer: \(context)") +} + +/// Serializes values in the value-buffer wire format: little-endian, packed, +/// no alignment. +struct WvWriter { + var bytes: [UInt8] = [] + + init() { + bytes.reserveCapacity(64) + } + + @inline(__always) + private mutating func append(_ v: T) { + withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } + mutating func writeI8(_ v: Int8) { append(v) } + mutating func writeU8(_ v: UInt8) { append(v) } + mutating func writeI16(_ v: Int16) { append(v) } + mutating func writeU16(_ v: UInt16) { append(v) } + mutating func writeI32(_ v: Int32) { append(v) } + mutating func writeU32(_ v: UInt32) { append(v) } + mutating func writeI64(_ v: Int64) { append(v) } + mutating func writeU64(_ v: UInt64) { append(v) } + mutating func writeF32(_ v: Float) { append(v.bitPattern) } + mutating func writeF64(_ v: Double) { append(v.bitPattern) } + + /// Writes a `u32` length or element count. + mutating func writeLen(_ n: Int) { + precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") + append(UInt32(n)) + } + + mutating func writeString(_ v: String) { + let utf8 = v.utf8 + writeLen(utf8.count) + bytes.append(contentsOf: utf8) + } + + mutating func writeBytes(_ v: Data) { + writeLen(v.count) + bytes.append(contentsOf: v) + } + + mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } + + /// Writes an object token. `p` must be a strong reference the buffer now + /// owns (a freshly cloned pointer), never one a wrapper still holds. + mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } +} + +/// Deserializes values from the value-buffer wire format directly from the +/// library's memory, rejecting truncated buffers, invalid flag bytes, +/// oversized length prefixes, and trailing bytes. +struct WvReader { + private let base: UnsafeRawPointer? + private let count: Int + private var pos = 0 + + init(_ base: UnsafeRawPointer?, _ count: Int) { + self.base = base + self.count = base == nil ? 0 : count + } + + /// The number of unread bytes. + var remaining: Int { count - pos } + + private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { + guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } + defer { pos += n } + return base + pos + } + + @inline(__always) + private mutating func load(_ type: T.Type, _ what: StaticString) -> T { + T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) + } + + mutating func readBool() -> Bool { + switch load(UInt8.self, "bool") { + case 0: return false + case 1: return true + default: wvDecodeFailure("bool byte out of range") + } + } + mutating func readI8() -> Int8 { load(Int8.self, "i8") } + mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } + mutating func readI16() -> Int16 { load(Int16.self, "i16") } + mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } + mutating func readI32() -> Int32 { load(Int32.self, "i32") } + mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } + mutating func readI64() -> Int64 { load(Int64.self, "i64") } + mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } + mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } + mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } + + /// Reads a byte length, which can't exceed the unread bytes. + mutating func readLen() -> Int { + let n = Int(load(UInt32.self, "length")) + guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } + return n + } + + /// Reads an element count. Elements may encode to zero bytes, so the + /// count isn't bounded by the unread bytes; callers cap preallocation. + mutating func readCount() -> Int { + Int(load(UInt32.self, "count")) + } + + mutating func readString() -> String { + let n = readLen() + if n == 0 { return "" } + let p = take(n, "string") + guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { + wvDecodeFailure("string is not valid UTF-8") + } + return s + } + + mutating func readBytes() -> Data { + let n = readLen() + if n == 0 { return Data() } + return Data(bytes: take(n, "bytes"), count: n) + } + + mutating func readOptionFlag() -> Bool { + switch load(UInt8.self, "option flag") { + case 0: return false + case 1: return true + default: wvDecodeFailure("option flag byte out of range") + } + } + + /// Reads an object token: one strong reference the caller adopts into a + /// wrapper whose deinit releases it. + mutating func readObject() -> OpaquePointer { + guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { + wvDecodeFailure("null object token") + } + return p + } + + func finish() { + if remaining != 0 { wvDecodeFailure("trailing bytes after value") } + } +} + +// MARK: Async and callbacks + +/// A native cancel token owned by one cancellable call. The library takes its +/// own reference at launch, so this one is released whenever the wrapper is +/// done with it. +final class WvCancelToken: @unchecked Sendable { + let raw: OpaquePointer + + init() { + raw = wvNonNull(edge_cases_cancel_token_create()) + } + + deinit { + edge_cases_cancel_token_destroy(raw) + } + + func cancel() { + edge_cases_cancel_token_cancel(raw) + } +} + +/// A continuation boxed for the C `context` slot of an async launch. +final class WvContinuation { + let value: CheckedContinuation + + init(_ value: CheckedContinuation) { + self.value = value + } +} + +/// One process-wide callback vtable at a stable address the library may hold +/// for the life of the process. +final class WvVtable: @unchecked Sendable { + let pointer: UnsafePointer + + init(_ value: T) { + let cell = UnsafeMutablePointer.allocate(capacity: 1) + cell.initialize(to: value) + pointer = UnsafePointer(cell) + } +} + +/// Reports a thrown Swift error to the library as a callback failure +/// (code -4); the library aborts its current call with that code and message. +func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { + let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) + message.withCString { edge_cases_error_set(outErr, EdgeCasesRuntimeError.foreignCode, $0) } +} + +// MARK: - API + +/// Typed errors reported by the `edge_cases` module. +public enum EdgeErrorsError: Error, LocalizedError, Sendable { + case busy(message: String) + + /// The message the library reported, or the code's documented default. + public var errorDescription: String? { + switch self { + case let .busy(message): return message + } + } + + /// The numeric ABI code carried by this error. + public var errorCode: Int32 { + switch self { + case .busy: return 7 + } + } +} + +func mapEdgeCases(_ err: WvError) -> Error { + let message = wvErrorMessage(err) + switch err.code { + case 7: return EdgeErrorsError.busy(message: message.isEmpty ? "Resource is busy" : message) + default: return wvRuntimeError(err) + } +} + +func checkEdgeCases(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = mapEdgeCases(err) + edge_cases_error_clear(&err) + throw error +} + +/// Deprecated enum with non-contiguous values and keyword-like variants +@available(*, deprecated, message: "Use Mode instead") +public enum Legacy: Int32, Sendable { + case none = 0 + case default_ = 5 + case class_ = 10 +} + +public enum Mode: Int32, Sendable { + case fast = 1 + case safe = 2 +} + +public struct Leaf: Hashable, Sendable { + public var weight: Float + + /// Creates a value from its fields. + public init(weight: Float) { + self.weight = weight + } +} + +func wvWriteLeaf(_ value: Leaf, into w: inout WvWriter) { + w.writeF32(value.weight) +} + +func wvReadLeaf(_ r: inout WvReader) -> Leaf { + let v0 = r.readF32() + return Leaf(weight: v0) +} + +/// Middle layer of a three-deep struct nesting +public struct Inner: Hashable, Sendable { + public var leaf: Leaf + public var leaves: [Leaf] + + /// Creates a value from its fields. + public init(leaf: Leaf, leaves: [Leaf]) { + self.leaf = leaf + self.leaves = leaves + } +} + +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) + } +} + +func wvReadInner(_ r: inout WvReader) -> Inner { + let v1 = wvReadLeaf(&r) + let n3 = r.readCount() + var v2: [Leaf] = [] + v2.reserveCapacity(min(n3, r.remaining)) + for _ in 0.. Outer { + let v12 = wvReadInner(&r) + var v13: Inner? = nil + if r.readOptionFlag() { + let v14 = wvReadInner(&r) + v13 = v14 + } + let n16 = r.readCount() + var v15: [String: Inner] = [:] + v15.reserveCapacity(min(n16, r.remaining)) + for _ in 0.. Keywords { + let v0 = r.readI32() + let v1 = r.readI32() + let v2 = r.readI32() + let v3 = r.readI32() + let v4 = r.readI32() + let v5 = r.readI32() + let v6 = r.readI32() + let v7 = r.readI32() + let v8 = r.readI32() + let v9 = r.readI32() + let v10 = r.readI32() + let v11 = r.readI32() + let v12 = r.readI32() + let v13 = r.readI32() + let v14 = r.readI32() + let v15 = r.readI32() + let v16 = r.readI32() + let v17 = r.readI32() + let v18 = r.readI32() + let v19 = r.readI32() + let v20 = r.readI32() + let v21 = r.readI32() + let v22 = r.readI32() + let v23 = r.readI32() + let v24 = r.readI32() + let v25 = r.readI32() + let v26 = r.readI32() + let v27 = r.readI32() + let v28 = r.readI32() + let v29 = r.readI32() + let v30 = r.readI32() + let v31 = r.readI32() + let v32 = r.readI32() + let v33 = r.readI32() + let v34 = r.readI32() + let v35 = r.readI32() + let v36 = r.readI32() + let v37 = r.readI32() + let v38 = r.readI32() + let v39 = r.readI32() + let v40 = r.readI32() + let v41 = r.readI32() + let v42 = r.readI32() + let v43 = r.readI32() + let v44 = r.readI32() + let v45 = r.readI32() + let v46 = r.readI32() + let v47 = r.readI32() + let v48 = r.readI32() + let v49 = r.readI32() + let v50 = r.readI32() + let v51 = r.readI32() + let v52 = r.readI32() + let v53 = r.readI32() + let v54 = r.readI32() + let v55 = r.readI32() + let v56 = r.readI32() + let v57 = r.readI32() + let v58 = r.readI32() + 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 +@available(*, deprecated, message: "Use Outer instead") +public struct Retired: Hashable, Sendable { + public var id: Int64 + + /// Creates a value from its fields. + public init(id: Int64) { + self.id = id + } +} + +@available(*, deprecated, message: "Use Outer instead") +func wvWriteRetired(_ value: Retired, into w: inout WvWriter) { + w.writeI64(value.id) +} + +@available(*, deprecated, message: "Use Outer instead") +func wvReadRetired(_ r: inout WvReader) -> Retired { + let v0 = r.readI64() + return Retired(id: v0) +} + +/// Record carrying objects in every buffered position +public struct WidgetHolder: Sendable { + public var primary: Widget + public var fallback: Widget? + public var extras: [Widget] + public var byName: [String: Widget] + + /// Creates a value from its fields. + public init(primary: Widget, fallback: Widget?, extras: [Widget], byName: [String: Widget]) { + self.primary = primary + self.fallback = fallback + self.extras = extras + self.byName = byName + } +} + +func wvWriteWidgetHolder(_ value: WidgetHolder, into w: inout WvWriter) { + w.writeObject(value.primary.clonePtr()) + if let v0 = value.fallback { + w.writeOptionFlag(true) + w.writeObject(v0.clonePtr()) + } else { + w.writeOptionFlag(false) + } + w.writeLen(value.extras.count) + for v1 in value.extras { + w.writeObject(v1.clonePtr()) + } + w.writeLen(value.byName.count) + for (v2, v3) in value.byName { + w.writeString(v2) + w.writeObject(v3.clonePtr()) + } +} + +func wvReadWidgetHolder(_ r: inout WvReader) -> WidgetHolder { + let v4 = Widget(ptr: r.readObject()) + var v5: Widget? = nil + if r.readOptionFlag() { + let v6 = Widget(ptr: r.readObject()) + v5 = v6 + } + let n8 = r.readCount() + var v7: [Widget] = [] + v7.reserveCapacity(min(n8, r.remaining)) + for _ in 0.. Bool + /// Enum return travels back as its integer discriminant + func pickMode() throws -> Mode +} + +/// Retains one `BatchListener` implementation across the C boundary; the library's +/// `free` entry releases it. +final class WvBatchListenerBox: Sendable { + let impl: any BatchListener + init(_ impl: any BatchListener) { self.impl = impl } +} + +/// The process-wide `edge_cases_edge_cases_BatchListener_vtable` the library calls `BatchListener` +/// implementations through. Every entry recovers the box from `ctx`, copies +/// or adopts its arguments, and reports a thrown error through `out_err` +/// instead of unwinding. +enum WvBatchListenerVtable { + static let shared = WvVtable(edge_cases_edge_cases_BatchListener_vtable( + on_batch: { ctx, items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + try wvBox.impl.onBatch(items: wvBorrowBuffer(items_ptr, items_len) { (r: inout WvReader) -> [Outer] in + let n1 = r.readCount() + var v0: [Outer] = [] + v0.reserveCapacity(min(n1, r.remaining)) + for _ in 0.. Outer? in + var v0: Outer? = nil + if r.readOptionFlag() { + let v1 = wvReadOuter(&r) + v0 = v1 + } + return v0 + }, mode: wvEnumCase(Mode.self, numericCast(mode.rawValue))) + } catch { + wvForeignError(out_err, error) + } + }, + on_widget: { ctx, widget, fallback, children_ptr, children_len, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + try wvBox.impl.onWidget(widget: Widget(ptr: wvNonNull(widget)), fallback: fallback.map { Widget(ptr: $0) }, children: wvBorrowBuffer(children_ptr, children_len) { (r: inout WvReader) -> [Widget] in + let n1 = r.readCount() + var v0: [Widget] = [] + v0.reserveCapacity(min(n1, r.remaining)) + for _ in 0...fromOpaque(ctx!).takeUnretainedValue() + do { + try wvBox.impl.onTick() + } catch { + wvForeignError(out_err, error) + } + }, + should_continue: { ctx, class_, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + return try wvBox.impl.shouldContinue(class_: class_) + } catch { + wvForeignError(out_err, error) + return false + } + }, + pick_mode: { ctx, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + let value = try wvBox.impl.pickMode() + return edge_cases_edge_cases_Mode(rawValue: numericCast(value.rawValue)) + } catch { + wvForeignError(out_err, error) + return edge_cases_edge_cases_Mode(rawValue: 0) + } + }, + free: { ctx in + Unmanaged.fromOpaque(ctx!).release() + } + )) +} + +/// Callback interface whose name and methods are keywords +/// +/// Implement this protocol with a class and pass an instance where the API +/// expects it; the library keeps it alive for as long as it holds the +/// callback and may call any method from any thread. A thrown error aborts +/// the library's call and surfaces to the original caller as a foreign error +/// (code -4). +public protocol Keyword: AnyObject, Sendable { + func delete(self_: Int32) throws + func import_() throws -> Int32 +} + +/// Retains one `Keyword` implementation across the C boundary; the library's +/// `free` entry releases it. +final class WvKeywordBox: Sendable { + let impl: any Keyword + init(_ impl: any Keyword) { self.impl = impl } +} + +/// The process-wide `edge_cases_edge_cases_Keyword_vtable` the library calls `Keyword` +/// implementations through. Every entry recovers the box from `ctx`, copies +/// or adopts its arguments, and reports a thrown error through `out_err` +/// instead of unwinding. +enum WvKeywordVtable { + static let shared = WvVtable(edge_cases_edge_cases_Keyword_vtable( + delete_: { ctx, self_, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + try wvBox.impl.delete(self_: self_) + } catch { + wvForeignError(out_err, error) + } + }, + import: { ctx, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + return try wvBox.impl.import_() + } catch { + wvForeignError(out_err, error) + return 0 + } + }, + free: { ctx in + Unmanaged.fromOpaque(ctx!).release() + } + )) +} + +/// Interface with no constructor; instances come from make_widget +public final class Widget: @unchecked Sendable { + let ptr: OpaquePointer + + init(ptr: OpaquePointer) { + self.ptr = ptr + } + + deinit { + edge_cases_edge_cases_Widget_destroy(ptr) + } + + /// Returns a new strong reference to the same object, for a position that + /// takes ownership of it (an object token inside a value buffer). + func clonePtr() -> OpaquePointer { + wvNonNull(edge_cases_edge_cases_Widget_clone(ptr)) + } + + /// Void method that can fail + public func refresh() throws { + var err = WvError() + edge_cases_edge_cases_Widget_refresh(ptr, &err) + try checkEdgeCases(&err) + } + + public func rename(class_: String, import_: String?) -> String { + var err = WvError() + var import_Writer = WvWriter() + if let v0 = import_ { + import_Writer.writeOptionFlag(true) + import_Writer.writeString(v0) + } else { + import_Writer.writeOptionFlag(false) + } + var outLen = 0 + let rv: UnsafePointer? = wvWithUTF8(class_) { class__ptr, class__len in + wvWithBuffer(import_Writer) { import__ptr, import__len in + edge_cases_edge_cases_Widget_rename(ptr, class__ptr, class__len, import__ptr, import__len, &outLen, &err) + } + } + wvTrap(&err) + return wvTakeString(rv, outLen) + } + + /// Async method with a struct result + public func fetch(id: Int64) async throws -> Outer { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + edge_cases_edge_cases_Widget_fetch(self.ptr, id, { context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + cont.resume(throwing: wvTakeError(err, mapEdgeCases)) + return + } + cont.resume(returning: wvTakeBuffer(resultPtr, resultLen, wvReadOuter)) + }, context) + } + } + + @available(*, deprecated, message: "Use rename instead") + public func oldName() -> String { + var err = WvError() + var outLen = 0 + let rv = edge_cases_edge_cases_Widget_old_name(ptr, &outLen, &err) + wvTrap(&err) + return wvTakeString(rv, outLen) + } +} + +/// Deprecated interface with a throwing constructor +@available(*, deprecated, message: "Use Widget instead") +public final class Vault: @unchecked Sendable { + let ptr: OpaquePointer + + init(ptr: OpaquePointer) { + self.ptr = ptr + } + + deinit { + edge_cases_edge_cases_Vault_destroy(ptr) + } + + /// Returns a new strong reference to the same object, for a position that + /// takes ownership of it (an object token inside a value buffer). + func clonePtr() -> OpaquePointer { + wvNonNull(edge_cases_edge_cases_Vault_clone(ptr)) + } + + /// Fails when the vault is busy + public static func open(path: String) throws -> Vault { + wvLoad() + var err = WvError() + let rv: OpaquePointer? = wvWithUTF8(path) { path_ptr, path_len in + edge_cases_edge_cases_Vault_open(path_ptr, path_len, &err) + } + try checkEdgeCases(&err) + return Vault(ptr: wvNonNull(rv)) + } + + public func peek() -> Widget? { + var err = WvError() + let rv = edge_cases_edge_cases_Vault_peek(ptr, &err) + wvTrap(&err) + return rv.map { Widget(ptr: $0) } + } + + /// Throwing static + public static func probe() throws -> Bool { + wvLoad() + var err = WvError() + let rv = edge_cases_edge_cases_Vault_probe(&err) + try checkEdgeCases(&err) + return rv + } +} + +/// A lazy sequence over the `Widget` elements streamed by `edge_cases_edge_cases_widget_stream`. +/// +/// Each `next()` call pulls exactly one element from the library. The +/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` +/// when iteration is abandoned early. +public final class EdgeCasesWidgetStreamIterator: Sequence, IteratorProtocol { + private var handle: OpaquePointer? + + init(handle: OpaquePointer) { + self.handle = handle + } + + deinit { + destroyHandle() + } + + private func destroyHandle() { + guard let handle = handle else { return } + edge_cases_edge_cases_WidgetStreamIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the library, or returns `nil` once the + /// stream is exhausted (destroying the underlying iterator). + public func next() -> Widget? { + guard let handle = handle else { return nil } + var item: OpaquePointer? = nil + var err = WvError() + if edge_cases_edge_cases_WidgetStreamIterator_next(handle, &item, &err) == 0 { + wvTrap(&err) + destroyHandle() + return nil + } + return Widget(ptr: wvNonNull(item)) + } +} + +/// A lazy sequence over the `String` elements streamed by `edge_cases_edge_cases_string_stream`. +/// +/// Each `next()` call pulls exactly one element from the library. The +/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` +/// when iteration is abandoned early. +public final class EdgeCasesStringStreamIterator: Sequence, IteratorProtocol { + private var handle: OpaquePointer? + + init(handle: OpaquePointer) { + self.handle = handle + } + + deinit { + destroyHandle() + } + + private func destroyHandle() { + guard let handle = handle else { return } + edge_cases_edge_cases_StringStreamIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the library, or returns `nil` once the + /// stream is exhausted (destroying the underlying iterator). + public func next() -> String? { + guard let handle = handle else { return nil } + var item: UnsafePointer? = nil + var itemLen = 0 + var err = WvError() + if edge_cases_edge_cases_StringStreamIterator_next(handle, &item, &itemLen, &err) == 0 { + wvTrap(&err) + destroyHandle() + return nil + } + return wvTakeString(item, itemLen) + } +} + +/// A lazy sequence over the `Int32` elements streamed by `edge_cases_edge_cases_scalar_stream`. +/// +/// Each `next()` call pulls exactly one element from the library. The +/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` +/// when iteration is abandoned early. +public final class EdgeCasesScalarStreamIterator: Sequence, IteratorProtocol { + private var handle: OpaquePointer? + + init(handle: OpaquePointer) { + self.handle = handle + } + + deinit { + destroyHandle() + } + + private func destroyHandle() { + guard let handle = handle else { return } + edge_cases_edge_cases_ScalarStreamIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the library, or returns `nil` once the + /// stream is exhausted (destroying the underlying iterator). + public func next() -> Int32? { + guard let handle = handle else { return nil } + var item: Int32 = 0 + var err = WvError() + if edge_cases_edge_cases_ScalarStreamIterator_next(handle, &item, &err) == 0 { + wvTrap(&err) + destroyHandle() + return nil + } + return item + } +} + +/// A lazy sequence over the `Mode` elements streamed by `edge_cases_edge_cases_mode_stream`. +/// +/// Each `next()` call pulls exactly one element from the library. The +/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` +/// when iteration is abandoned early. +public final class EdgeCasesModeStreamIterator: Sequence, IteratorProtocol { + private var handle: OpaquePointer? + + init(handle: OpaquePointer) { + self.handle = handle + } + + deinit { + destroyHandle() + } + + private func destroyHandle() { + guard let handle = handle else { return } + edge_cases_edge_cases_ModeStreamIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the library, or returns `nil` once the + /// stream is exhausted (destroying the underlying iterator). + public func next() -> Mode? { + guard let handle = handle else { return nil } + var item = edge_cases_edge_cases_Mode(rawValue: 0) + var err = WvError() + if edge_cases_edge_cases_ModeStreamIterator_next(handle, &item, &err) == 0 { + wvTrap(&err) + destroyHandle() + return nil + } + return wvEnumCase(Mode.self, numericCast(item.rawValue)) + } +} + +public enum EdgeCases { + /// No parameters and no result + public static func noop() { + wvLoad() + var err = WvError() + edge_cases_edge_cases_noop(&err) + wvTrap(&err) + } + + /// Function named after a keyword whose parameters are all keywords + public static func select(class_: Int32, from: String, func_: [String], range: Int32?, type: Bool) -> Int32 { + wvLoad() + var err = WvError() + 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.writeI32(v1) + } else { + rangeWriter.writeOptionFlag(false) + } + let rv: Int32 = wvWithUTF8(from) { from_ptr, from_len in + wvWithBuffer(func_Writer) { func__ptr, func__len in + wvWithBuffer(rangeWriter) { range_ptr, range_len in + edge_cases_edge_cases_select(class_, from_ptr, from_len, func__ptr, func__len, range_ptr, range_len, type, &err) + } + } + } + wvTrap(&err) + return rv + } + + /// Optional list of optional structs, the deepest legal nesting + public static func nestedOptional(items: [Outer?]?) -> [Outer?]? { + wvLoad() + var err = WvError() + var 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 = 0 + let rv: UnsafePointer? = wvWithBuffer(itemsWriter) { items_ptr, items_len in + edge_cases_edge_cases_nested_optional(items_ptr, items_len, &outLen, &err) + } + wvTrap(&err) + return wvTakeBuffer(rv, outLen) { (r: inout WvReader) -> [Outer?]? in + var v0: [Outer?]? = nil + if r.readOptionFlag() { + let n2 = r.readCount() + var v1: [Outer?] = [] + v1.reserveCapacity(min(n2, r.remaining)) + for _ in 0.. Mode? { + wvLoad() + var err = WvError() + 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.writeI32(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.writeI32(v6.rawValue) + } else { + modeWriter.writeOptionFlag(false) + } + var outLen = 0 + let rv: UnsafePointer? = wvWithBuffer(textWriter) { text_ptr, text_len in + wvWithBuffer(countWriter) { count_ptr, count_len in + wvWithBuffer(outerWriter) { outer_ptr, outer_len in + wvWithBuffer(tagsWriter) { tags_ptr, tags_len in + wvWithBuffer(blobWriter) { blob_ptr, blob_len in + wvWithBuffer(modeWriter) { mode_ptr, mode_len in + edge_cases_edge_cases_optional_params(text_ptr, text_len, count_ptr, count_len, outer_ptr, outer_len, tags_ptr, tags_len, blob_ptr, blob_len, mode_ptr, mode_len, &outLen, &err) + } + } + } + } + } + } + wvTrap(&err) + return wvTakeBuffer(rv, outLen) { (r: inout WvReader) -> Mode? in + var v0: Mode? = nil + if r.readOptionFlag() { + let v1 = wvEnumCase(Mode.self, r.readI32()) + v0 = v1 + } + return v0 + } + } + + /// Two callback interface parameters on one function + public static func registerBatch(listener: BatchListener, keyword: Keyword) { + wvLoad() + var err = WvError() + let listener_ctx = Unmanaged.passRetained(WvBatchListenerBox(listener)).toOpaque() + let keyword_ctx = Unmanaged.passRetained(WvKeywordBox(keyword)).toOpaque() + edge_cases_edge_cases_register_batch(listener_ctx, WvBatchListenerVtable.shared.pointer, keyword_ctx, WvKeywordVtable.shared.pointer, &err) + wvTrap(&err) + } + + /// Objects in every buffered position + public static func widgetsEverywhere(list: [Widget], byName: [String: Widget], sparse: [Widget?], holder: WidgetHolder) -> WidgetHolder { + wvLoad() + var err = WvError() + var listWriter = WvWriter() + listWriter.writeLen(list.count) + for v0 in list { + listWriter.writeObject(v0.clonePtr()) + } + var byNameWriter = WvWriter() + byNameWriter.writeLen(byName.count) + for (v1, v2) in byName { + byNameWriter.writeString(v1) + byNameWriter.writeObject(v2.clonePtr()) + } + var sparseWriter = WvWriter() + sparseWriter.writeLen(sparse.count) + for v3 in sparse { + if let v4 = v3 { + sparseWriter.writeOptionFlag(true) + sparseWriter.writeObject(v4.clonePtr()) + } else { + sparseWriter.writeOptionFlag(false) + } + } + var holderWriter = WvWriter() + wvWriteWidgetHolder(holder, into: &holderWriter) + var outLen = 0 + let rv: UnsafePointer? = wvWithBuffer(listWriter) { list_ptr, list_len in + wvWithBuffer(byNameWriter) { byName_ptr, byName_len in + wvWithBuffer(sparseWriter) { sparse_ptr, sparse_len in + wvWithBuffer(holderWriter) { holder_ptr, holder_len in + edge_cases_edge_cases_widgets_everywhere(list_ptr, list_len, byName_ptr, byName_len, sparse_ptr, sparse_len, holder_ptr, holder_len, &outLen, &err) + } + } + } + } + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadWidgetHolder) + } + + /// Iterator of objects + /// - Returns: A lazy sequence that pulls one element per step from the + /// library; the underlying iterator is destroyed when the sequence is + /// exhausted or deinitialized. + public static func widgetStream() -> EdgeCasesWidgetStreamIterator { + wvLoad() + var err = WvError() + let rv = edge_cases_edge_cases_widget_stream(&err) + wvTrap(&err) + return EdgeCasesWidgetStreamIterator(handle: wvNonNull(rv)) + } + + /// Async returning an object + public static func asyncWidget() async throws -> Widget { + wvLoad() + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + edge_cases_edge_cases_async_widget({ context, err, result in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + cont.resume(throwing: wvTakeError(err, mapEdgeCases)) + return + } + cont.resume(returning: Widget(ptr: wvNonNull(result))) + }, context) + } + } + + /// Factory for an interface with no constructor + public static func makeWidget(label: String) -> Widget { + wvLoad() + var err = WvError() + let rv: OpaquePointer? = wvWithUTF8(label) { label_ptr, label_len in + edge_cases_edge_cases_make_widget(label_ptr, label_len, &err) + } + wvTrap(&err) + return Widget(ptr: wvNonNull(rv)) + } + + /// Optional interface return + public static func findWidget(id: Int64) throws -> Widget? { + wvLoad() + var err = WvError() + let rv = edge_cases_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 { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = edge_cases_edge_cases_inspect_widget(widget.ptr, fallback?.ptr, &outLen, &err) + wvTrap(&err) + return wvTakeString(rv, outLen) + } + + /// Iterator of strings + /// - Returns: A lazy sequence that pulls one element per step from the + /// library; the underlying iterator is destroyed when the sequence is + /// exhausted or deinitialized. + public static func stringStream() -> EdgeCasesStringStreamIterator { + wvLoad() + var err = WvError() + let rv = edge_cases_edge_cases_string_stream(&err) + wvTrap(&err) + return EdgeCasesStringStreamIterator(handle: wvNonNull(rv)) + } + + /// Iterator of scalars + /// - Returns: A lazy sequence that pulls one element per step from the + /// library; the underlying iterator is destroyed when the sequence is + /// exhausted or deinitialized. + public static func scalarStream() -> EdgeCasesScalarStreamIterator { + wvLoad() + var err = WvError() + let rv = edge_cases_edge_cases_scalar_stream(&err) + wvTrap(&err) + return EdgeCasesScalarStreamIterator(handle: wvNonNull(rv)) + } + + /// Iterator of enums + /// - Returns: A lazy sequence that pulls one element per step from the + /// library; the underlying iterator is destroyed when the sequence is + /// exhausted or deinitialized. + public static func modeStream() -> EdgeCasesModeStreamIterator { + wvLoad() + var err = WvError() + let rv = edge_cases_edge_cases_mode_stream(&err) + wvTrap(&err) + return EdgeCasesModeStreamIterator(handle: wvNonNull(rv)) + } + + /// Async with no result + public static func asyncVoid() async { + wvLoad() + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + edge_cases_edge_cases_async_void({ context, err in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + wvTrapBoxed(err) + } + cont.resume(returning: ()) + }, context) + } + } + + public static func asyncBytes(size: UInt32) async -> Data { + wvLoad() + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + edge_cases_edge_cases_async_bytes(size, { context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + wvTrapBoxed(err) + } + cont.resume(returning: wvTakeBytes(resultPtr, resultLen)) + }, context) + } + } + + public static func asyncStruct() async throws -> Outer { + wvLoad() + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + edge_cases_edge_cases_async_struct({ context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + cont.resume(throwing: wvTakeError(err, mapEdgeCases)) + return + } + cont.resume(returning: wvTakeBuffer(resultPtr, resultLen, wvReadOuter)) + }, context) + } + } + + /// - Throws: `CancellationError` when the calling task is cancelled, which + /// also cancels the native call. + public static func asyncList() async throws -> [Outer] { + wvLoad() + let token = WvCancelToken() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[Outer], Error>) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + edge_cases_edge_cases_async_list(token.raw, { context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + cont.resume(throwing: wvTakeError(err, mapEdgeCases)) + return + } + cont.resume(returning: wvTakeBuffer(resultPtr, resultLen) { (r: inout WvReader) -> [Outer] in + let n1 = r.readCount() + var v0: [Outer] = [] + v0.reserveCapacity(min(n1, r.remaining)) + for _ in 0.. Int64? { + wvLoad() + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + edge_cases_edge_cases_async_optional({ context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + wvTrapBoxed(err) + } + cont.resume(returning: wvTakeBuffer(resultPtr, resultLen) { (r: inout WvReader) -> Int64? in + var v0: Int64? = nil + if r.readOptionFlag() { + let v1 = r.readI64() + v0 = v1 + } + return v0 + }) + }, context) + } + } + + public static func asyncEnum() async -> Mode { + wvLoad() + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + edge_cases_edge_cases_async_enum({ context, err, result in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + wvTrapBoxed(err) + } + cont.resume(returning: wvEnumCase(Mode.self, numericCast(result.rawValue))) + }, context) + } + } + + public static func asyncKeywords(class_: String, import_: Int32) async -> String { + wvLoad() + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + wvWithUTF8(class_) { class__ptr, class__len in + edge_cases_edge_cases_async_keywords(class__ptr, class__len, import_, { context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + wvTrapBoxed(err) + } + cont.resume(returning: wvTakeString(resultPtr, resultLen)) + }, context) + } + } + } + + public static func keywordsRoundtrip(value: Keywords) -> Keywords { + wvLoad() + var err = WvError() + var valueWriter = WvWriter() + wvWriteKeywords(value, into: &valueWriter) + var outLen = 0 + let rv: UnsafePointer? = wvWithBuffer(valueWriter) { value_ptr, value_len in + edge_cases_edge_cases_keywords_roundtrip(value_ptr, value_len, &outLen, &err) + } + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadKeywords) + } + + @available(*, deprecated, message: "Use Mode-based APIs") + public static func legacyValue(value: Legacy) -> Retired { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = edge_cases_edge_cases_legacy_value(edge_cases_edge_cases_Legacy(rawValue: numericCast(value.rawValue)), &outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadRetired) + } +} + +// END EdgeCases.swift 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 deleted file mode 100644 index 79fd765a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,1761 +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) } - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { writeUInt64(UInt64(UInt(bitPattern: p))) } -} - -/// 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") - } - } - /// Reads an object token: one strong reference the caller must adopt - /// into a wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(readUInt64())) else { - wvDecodeFailure("null object token") - } - return p - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -private final class ContinuationRef { - let value: CheckedContinuation - init(_ value: CheckedContinuation) { self.value = value } -} - -/// Reports a thrown Swift error to the producer as a foreign callback failure -/// (code -4); the producer aborts its current call with that code and message. -func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { weaveffi_error_set(outErr, -4, $0) } -} - -/// 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) -} - -/// Record carrying objects in every buffered position -public struct WidgetHolder { - public var primary: Widget - public var fallback: Widget? - public var extras: [Widget] - public var byName: [String: Widget] - - /// Creates a `WidgetHolder` value. - public init(primary: Widget, fallback: Widget?, extras: [Widget], byName: [String: Widget]) { - self.primary = primary - self.fallback = fallback - self.extras = extras - self.byName = byName - } -} - -/// Serializes a `WidgetHolder` into the value-buffer wire format. -func wvWriteWidgetHolder(_ value: WidgetHolder, into w: inout WvWriter) { - w.writeObject(value.primary.clonePtr()) - if let v0 = value.fallback { - w.writeOptionFlag(true) - w.writeObject(v0.clonePtr()) - } else { - w.writeOptionFlag(false) - } - w.writeLen(value.extras.count) - for v1 in value.extras { - w.writeObject(v1.clonePtr()) - } - w.writeLen(value.byName.count) - for (v2, v3) in value.byName { - w.writeString(v2) - w.writeObject(v3.clonePtr()) - } -} - -/// Deserializes a `WidgetHolder` from the value-buffer wire format. -func wvReadWidgetHolder(_ r: inout WvReader) -> WidgetHolder { - let v4 = Widget(ptr: r.readObject()) - var v5: Widget? = nil - if r.readOptionFlag() { - let v6 = Widget(ptr: r.readObject()) - v5 = v6 - } - let n8 = r.readLen() - var v7: [Widget] = [] - v7.reserveCapacity(n8) - for _ in 0.. Bool - /// Enum return travels back as its integer discriminant - func pickMode() throws -> Mode -} - -/// Retains one `BatchListener` implementation across the C boundary; the producer's -/// `free` entry releases it. -final class WvBatchListenerBox { - let impl: any BatchListener - init(_ impl: any BatchListener) { self.impl = impl } -} - -/// The process-wide `weaveffi_edge_cases_BatchListener_vtable` the producer calls `BatchListener` -/// implementations through. Every entry recovers the box from `ctx`, copies -/// or adopts its arguments, and reports a thrown error through `out_err` -/// instead of unwinding. -enum WvBatchListenerVtable { - static let value = weaveffi_edge_cases_BatchListener_vtable( - on_batch: { ctx, items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, out_err in - let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() - let b0 = [UInt8](UnsafeBufferPointer(start: items_ptr, count: items_len)) - var r1 = WvReader(bytes: b0) - let n3 = r1.readLen() - var v2: [Outer] = [] - v2.reserveCapacity(n3) - for _ in 0...fromOpaque(ctx!).takeUnretainedValue() - let b0 = [UInt8](UnsafeBufferPointer(start: children_ptr, count: children_len)) - var r1 = WvReader(bytes: b0) - let n3 = r1.readLen() - var v2: [Widget] = [] - v2.reserveCapacity(n3) - for _ in 0...fromOpaque(ctx!).takeUnretainedValue() - do { - try wvBox.impl.onTick() - } catch { - wvForeignError(out_err, error) - } - }, - should_continue: { ctx, class_, out_err in - let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() - do { - return try wvBox.impl.shouldContinue(class_: class_) - } catch { - wvForeignError(out_err, error) - return false - } - }, - pick_mode: { ctx, out_err in - let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() - do { - return weaveffi_edge_cases_Mode(try wvBox.impl.pickMode().rawValue) - } catch { - wvForeignError(out_err, error) - return weaveffi_edge_cases_Mode(0) - } - }, - free: { ctx in - Unmanaged.fromOpaque(ctx!).release() - } - ) - static let pointer: UnsafePointer = { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - return UnsafePointer(cell) - }() -} - -/// Callback interface whose name and methods are keywords -/// -/// Implement this protocol and pass the value where the API expects it; the -/// producer keeps it alive for as long as it holds the callback and may call -/// any method from any thread. A thrown error aborts the producer's call and -/// surfaces to the original caller as a foreign error (code -4). -public protocol Keyword { - func delete(self_: Int32) throws - func import_() throws -> Int32 -} - -/// Retains one `Keyword` implementation across the C boundary; the producer's -/// `free` entry releases it. -final class WvKeywordBox { - let impl: any Keyword - init(_ impl: any Keyword) { self.impl = impl } -} - -/// The process-wide `weaveffi_edge_cases_Keyword_vtable` the producer calls `Keyword` -/// implementations through. Every entry recovers the box from `ctx`, copies -/// or adopts its arguments, and reports a thrown error through `out_err` -/// instead of unwinding. -enum WvKeywordVtable { - static let value = weaveffi_edge_cases_Keyword_vtable( - delete_: { ctx, self_, out_err in - let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() - do { - try wvBox.impl.delete(self_: self_) - } catch { - wvForeignError(out_err, error) - } - }, - import: { ctx, out_err in - let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() - do { - return try wvBox.impl.import_() - } catch { - wvForeignError(out_err, error) - return 0 - } - }, - free: { ctx in - Unmanaged.fromOpaque(ctx!).release() - } - ) - static let pointer: UnsafePointer = { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - return UnsafePointer(cell) - }() -} - -/// 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) - } - - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it (an object token inside a value buffer). - func clonePtr() -> OpaquePointer { - weaveffi_edge_cases_Widget_clone(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) - } - - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it (an object token inside a value buffer). - func clonePtr() -> OpaquePointer { - weaveffi_edge_cases_Vault_clone(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 `Widget` elements streamed by `weaveffi_edge_cases_widget_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 EdgeCasesWidgetStreamIterator: 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_WidgetStreamIterator_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() -> Widget? { - guard let handle = handle else { return nil } - var item: OpaquePointer? = nil - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - if weaveffi_edge_cases_WidgetStreamIterator_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 - } - return Widget(ptr: item!) - } -} - -/// 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 - } - return item - } -} - -/// 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 - } - return Mode(rawValue: item.rawValue)! - } -} - -public enum EdgeCases { - /// 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 - } - /// Two callback interface parameters on one function - public static func registerBatch(listener: BatchListener, keyword: Keyword) -> Void { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let listener_ctx = Unmanaged.passRetained(WvBatchListenerBox(listener)).toOpaque() - let keyword_ctx = Unmanaged.passRetained(WvKeywordBox(keyword)).toOpaque() - weaveffi_edge_cases_register_batch(listener_ctx, WvBatchListenerVtable.pointer, keyword_ctx, WvKeywordVtable.pointer, &err) - trap(&err) - } - /// Objects in every buffered position - public static func widgetsEverywhere(list: [Widget], byName: [String: Widget], sparse: [Widget?], holder: WidgetHolder) -> WidgetHolder { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var listWriter = WvWriter() - listWriter.writeLen(list.count) - for v0 in list { - listWriter.writeObject(v0.clonePtr()) - } - var byNameWriter = WvWriter() - byNameWriter.writeLen(byName.count) - for (v1, v2) in byName { - byNameWriter.writeString(v1) - byNameWriter.writeObject(v2.clonePtr()) - } - var sparseWriter = WvWriter() - sparseWriter.writeLen(sparse.count) - for v3 in sparse { - if let v4 = v3 { - sparseWriter.writeOptionFlag(true) - sparseWriter.writeObject(v4.clonePtr()) - } else { - sparseWriter.writeOptionFlag(false) - } - } - var holderWriter = WvWriter() - wvWriteWidgetHolder(holder, into: &holderWriter) - var outLen: Int = 0 - let rv: UnsafePointer? = listWriter.bytes.withUnsafeBufferPointer { list_buf in - let list_ptr = list_buf.baseAddress - let list_len = list_buf.count - return byNameWriter.bytes.withUnsafeBufferPointer { byName_buf in - let byName_ptr = byName_buf.baseAddress - let byName_len = byName_buf.count - return sparseWriter.bytes.withUnsafeBufferPointer { sparse_buf in - let sparse_ptr = sparse_buf.baseAddress - let sparse_len = sparse_buf.count - return holderWriter.bytes.withUnsafeBufferPointer { holder_buf in - let holder_ptr = holder_buf.baseAddress - let holder_len = holder_buf.count - return weaveffi_edge_cases_widgets_everywhere(list_ptr, list_len, byName_ptr, byName_len, sparse_ptr, sparse_len, holder_ptr, holder_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 v5 = wvReadWidgetHolder(&rvReader) - rvReader.finish() - return v5 - } - /// Iterator of objects - /// - 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 widgetStream() -> EdgeCasesWidgetStreamIterator { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_edge_cases_widget_stream(&err) - trap(&err) - guard let rv = rv else { fatalError("-1: null iterator") } - return EdgeCasesWidgetStreamIterator(handle: rv) - } - /// Async returning an object - public static func asyncWidget() async throws -> Widget { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() - weaveffi_edge_cases_async_widget_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) } ?? "" - 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 result = result else { - contRef.value.resume(throwing: WeaveFFIError.error(code: -1, message: "null pointer")) - return - } - contRef.value.resume(returning: Widget(ptr: result)) - } - }, ctx) - } - } - /// 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) - } - /// 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_kitchen_sink__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Package_swift.snap index 7ec6e100..c40434d4 100644 --- a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Package_swift.snap +++ b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Package_swift.snap @@ -1,22 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// swift-tools-version:5.9 +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o +import Foundation import PackageDescription +// The C module `CKitchenSink` comes from one of two places. Next to this +// manifest, a prebuilt `CKitchenSink.xcframework` (assembled from the native +// libraries, see the README) wins. Otherwise `Sources/CKitchenSink` declares +// the generated header as a system library that links `kitchen_sink` from the +// linker search path, e.g. `swift build -Xlinker -L/path/to/lib`. +let xcframework = "CKitchenSink.xcframework" +let cModule: Target = FileManager.default.fileExists(atPath: "\(Context.packageDirectory)/\(xcframework)") + ? .binaryTarget(name: "CKitchenSink", path: xcframework) + : .systemLibrary(name: "CKitchenSink") + let package = Package( - name: "WeaveFFI", + name: "KitchenSink", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), + .library(name: "KitchenSink", targets: ["KitchenSink"]), ], targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), + cModule, + .target(name: "KitchenSink", dependencies: ["CKitchenSink"]), ] ) diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_kitchen_sink_h.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_kitchen_sink_h.snap new file mode 100644 index 00000000..14faeffb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_kitchen_sink_h.snap @@ -0,0 +1,219 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef KITCHEN_SINK_H +#define KITCHEN_SINK_H + +#include +#include +#include + +#ifndef KITCHEN_SINK_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef KITCHEN_SINK_BUILD +# define KITCHEN_SINK_API __declspec(dllexport) +# else +# define KITCHEN_SINK_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define KITCHEN_SINK_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define KITCHEN_SINK_API __attribute__((visibility("default"))) +# else +# define KITCHEN_SINK_API +# endif +#endif + +#ifndef KITCHEN_SINK_DEPRECATED +# if defined(_MSC_VER) +# define KITCHEN_SINK_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define KITCHEN_SINK_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define KITCHEN_SINK_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports kitchen_sink_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define KITCHEN_SINK_ABI_VERSION 3u +KITCHEN_SINK_API uint32_t kitchen_sink_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by kitchen_sink_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct kitchen_sink_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} kitchen_sink_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +KITCHEN_SINK_API void kitchen_sink_error_set(kitchen_sink_error* err, int32_t code, const char* message); +KITCHEN_SINK_API void kitchen_sink_error_clear(kitchen_sink_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + kitchen_sink_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_error_free(kitchen_sink_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +KITCHEN_SINK_API void kitchen_sink_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct kitchen_sink_cancel_token kitchen_sink_cancel_token; +KITCHEN_SINK_API kitchen_sink_cancel_token* kitchen_sink_cancel_token_create(void); +KITCHEN_SINK_API void kitchen_sink_cancel_token_cancel(kitchen_sink_cancel_token* token); +KITCHEN_SINK_API bool kitchen_sink_cancel_token_is_cancelled(const kitchen_sink_cancel_token* token); +KITCHEN_SINK_API void kitchen_sink_cancel_token_destroy(kitchen_sink_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +KITCHEN_SINK_API uint8_t* kitchen_sink_alloc(uint32_t size); +KITCHEN_SINK_API void kitchen_sink_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +KITCHEN_SINK_API uint64_t kitchen_sink_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define KITCHEN_SINK_SHARED_CHECKSUM 0x42c4ce2c8d0af052ull +KITCHEN_SINK_API uint64_t kitchen_sink_shared_checksum(void); +#define KITCHEN_SINK_KITCHEN_CHECKSUM 0xd83da75b24b545ffull +KITCHEN_SINK_API uint64_t kitchen_sink_kitchen_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with kitchen_sink_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * kitchen_sink_buffer.h declares a C struct and codec for each. + */ + +/** Task priority levels */ +typedef enum { + /** Low priority */ + kitchen_sink_kitchen_Priority_Low = 0, + /** Normal priority */ + kitchen_sink_kitchen_Priority_Normal = 1, + /** High priority */ + kitchen_sink_kitchen_Priority_High = 2 +} kitchen_sink_kitchen_Priority; +/** Error codes reported by throwing functions in the `kitchen` module tree. */ +typedef enum { kitchen_sink_kitchen_KitchenErrors_NotFound = 1, kitchen_sink_kitchen_KitchenErrors_InvalidInput = 2 } kitchen_sink_kitchen_KitchenErrors; +typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget; +typedef struct kitchen_sink_kitchen_StreamGadgetsIterator kitchen_sink_kitchen_StreamGadgetsIterator; +typedef struct kitchen_sink_kitchen_StreamItemsIterator kitchen_sink_kitchen_StreamItemsIterator; +/** + * Consumer-implemented observer of ready items + * + * Consumer-implemented callback interface. The consumer passes a context pointer plus a pointer to a static instance of this vtable; the producer may call any entry from any thread until it calls `free(ctx)` exactly once. + */ +typedef struct kitchen_sink_kitchen_ReadyListener_vtable { + /** Fires when an item is ready */ + void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); + /** Receives the item itself and says whether to keep listening */ + bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); + void (*free)(void* ctx); +} kitchen_sink_kitchen_ReadyListener_vtable; +typedef void (*kitchen_sink_kitchen_do_async_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*kitchen_sink_kitchen_do_cancellable_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); + +// Module: shared +/** Trivial cross-module helper */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_shared_ping(size_t* out_len, kitchen_sink_error* out_err); + +// Module: kitchen +/** A pocket-sized interface exercising the object surface */ +/** Create a gadget with the given id */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(int64_t id, kitchen_sink_error* out_err); +/** The gadget subsystem version string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_version(size_t* out_len, kitchen_sink_error* out_err); +/** Render the gadget as a human-readable string */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err); +/** Poke the gadget a number of times, failing on a negative count */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_Gadget_poke(const kitchen_sink_kitchen_Gadget* self, int32_t times, kitchen_sink_error* out_err); +/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self); +/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ +KITCHEN_SINK_API void kitchen_sink_kitchen_Gadget_destroy(kitchen_sink_kitchen_Gadget* self); + +/** Identity for bool */ +KITCHEN_SINK_API bool kitchen_sink_kitchen_bool_id(bool x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_i32_id(int32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API uint32_t kitchen_sink_kitchen_u32_id(uint32_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API int64_t kitchen_sink_kitchen_i64_id(int64_t x, kitchen_sink_error* out_err); +KITCHEN_SINK_API double kitchen_sink_kitchen_f64_id(double x, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_string(const uint8_t* s_ptr, size_t s_len, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_echo_bytes(const uint8_t* b_ptr, size_t b_len, size_t* out_len, kitchen_sink_error* out_err); +/** Register a listener, returning how many are attached */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err); +/** Optional object in both directions */ +KITCHEN_SINK_API kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_maybe_gadget(const kitchen_sink_kitchen_Gadget* fallback, kitchen_sink_error* out_err); +/** A list of objects */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_gadgets(size_t* out_len, kitchen_sink_error* out_err); +/** An iterator over objects */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamGadgetsIterator* kitchen_sink_kitchen_stream_gadgets(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamGadgetsIterator_next(kitchen_sink_kitchen_StreamGadgetsIterator* iter, kitchen_sink_kitchen_Gadget** out_item, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamGadgetsIterator_destroy(kitchen_sink_kitchen_StreamGadgetsIterator* iter); +/** Optional struct return */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_maybe_item(int64_t id, size_t* out_len, kitchen_sink_error* out_err); +/** List of structs */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_list_items(size_t* out_len, kitchen_sink_error* out_err); +/** Map return type */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_get_attrs(size_t* out_len, kitchen_sink_error* out_err); +/** Iterator return type */ +KITCHEN_SINK_API kitchen_sink_kitchen_StreamItemsIterator* kitchen_sink_kitchen_stream_items(kitchen_sink_error* out_err); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamItemsIterator_next(kitchen_sink_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamItemsIterator_destroy(kitchen_sink_kitchen_StreamItemsIterator* iter); +/** Returns the shared Token type from another module */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_cross_module_token(size_t* out_len, kitchen_sink_error* out_err); +/** Async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_async(const uint8_t* input_ptr, size_t input_len, kitchen_sink_kitchen_do_async_callback callback, void* context); +/** Cancellable async operation */ +KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_ptr, size_t input_len, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_do_cancellable_callback callback, void* context); +/** Legacy operation kept for compatibility */ +KITCHEN_SINK_DEPRECATED("Use new_op instead") +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_legacy_op(kitchen_sink_error* out_err); +/** Replacement for legacy_op */ +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_new_op(kitchen_sink_error* out_err); + +// Module: kitchen_nested +/** Trivial nested-module function */ +KITCHEN_SINK_API const uint8_t* kitchen_sink_kitchen_nested_hello(size_t* out_len, kitchen_sink_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // KITCHEN_SINK_H + +// END kitchen_sink.h diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_module_modulemap.snap new file mode 100644 index 00000000..1aa9b7c9 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CKitchenSink_module_modulemap.snap @@ -0,0 +1,14 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +module CKitchenSink [system] { + header "kitchen_sink.h" + link "kitchen_sink" + export * +} + +// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__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_kitchen_sink__Sources_KitchenSink_KitchenSink_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_KitchenSink_KitchenSink_swift.snap new file mode 100644 index 00000000..99157aaf --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_KitchenSink_KitchenSink_swift.snap @@ -0,0 +1,1056 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import CKitchenSink +import Foundation + +// MARK: - Runtime support + +/// A failure the native library reports outside any declared error domain: +/// an unknown error code or a runtime trap. Throwing wrappers raise it; +/// non-throwing wrappers stop the process with `fatalError` instead, since +/// Swift has no unchecked errors. +public struct KitchenSinkRuntimeError: Error, LocalizedError, Hashable, Sendable { + /// The code of a generic runtime failure. + public static let genericCode: Int32 = -1 + /// The code of a panic caught at the library boundary. + public static let panicCode: Int32 = -2 + /// The code of an argument or result that failed to marshal. + public static let marshalCode: Int32 = -3 + /// The code of a callback implementation that threw. + public static let foreignCode: Int32 = -4 + + /// The numeric code the native library reported. + public let errorCode: Int32 + /// The message the native library reported. + public let message: String + + /// Creates an error carrying a native code and message. + public init(errorCode: Int32, message: String) { + self.errorCode = errorCode + self.message = message + } + + public var errorDescription: String? { message } +} + +/// The error slot every native call reports through. +typealias WvError = kitchen_sink_error + +/// The C ABI revision these bindings were generated against. +let wvAbiVersion: UInt32 = 3 + +/// The contract check, run once before the first native call: the library +/// must implement this ABI revision and every top-level module must carry +/// the checksum these bindings were generated from. +let wvContract: Void = { + let abi = kitchen_sink_abi_version() + guard abi == wvAbiVersion else { + fatalError("KitchenSink: the native library 'kitchen_sink' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") + } + wvCheckModule("shared", kitchen_sink_shared_checksum(), 0x42c4ce2c8d0af052) + wvCheckModule("kitchen", kitchen_sink_kitchen_checksum(), 0xd83da75b24b545ff) +}() + +/// Runs the contract check if it hasn't run yet. +@inline(__always) +func wvLoad() { + _ = wvContract +} + +/// Stops the process when a module's contract checksum doesn't match. +func wvCheckModule(_ module: StaticString, _ actual: UInt64, _ expected: UInt64) { + guard actual == expected else { + fatalError("KitchenSink: module '\(module)' of the native library 'kitchen_sink' doesn't match these bindings (checksum 0x\(String(actual, radix: 16)), expected 0x\(String(expected, radix: 16))); regenerate the bindings or rebuild the library") + } +} + +/// The message carried by an error slot. +func wvErrorMessage(_ err: WvError) -> String { + err.message.map { String(cString: $0) } ?? "" +} + +/// The Swift error for a code outside any declared domain: cancellation +/// (`-5`) becomes `CancellationError`, anything else the runtime error. +func wvRuntimeError(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + return KitchenSinkRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) +} + +/// Stops the process for a failed call that can't throw, naming the code and +/// message the library reported. +func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { + fatalError("KitchenSink.\(function) failed with code \(code): \(message)") +} + +/// Throws the runtime error for a filled error slot, clearing it. +@inline(__always) +func wvCheck(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = wvRuntimeError(err) + kitchen_sink_error_clear(&err) + throw error +} + +/// Stops the process for a filled error slot of a call that can't throw. +@inline(__always) +func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { + guard err.code != 0 else { return } + let code = err.code + let message = wvErrorMessage(err) + kitchen_sink_error_clear(&err) + wvFatal(code, message, function) +} + +/// Maps and releases the heap-boxed error an async completion received. +func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { + let error = map(err.pointee) + kitchen_sink_error_free(err) + return error +} + +/// The error mapping of a cancellable call that declares no error domain: +/// cancellation throws `CancellationError`, anything else stops the process. +func wvCancelledOrTrap(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + wvFatal(err.code, wvErrorMessage(err), "async call") +} + +/// Stops the process for the boxed error of an async call that can't throw. +func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { + wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) +} + +/// Unwraps a pointer the library promised is non-null. +@inline(__always) +func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { + guard let ptr = ptr else { + wvFatal(KitchenSinkRuntimeError.marshalCode, "the native library returned a null pointer", function) + } + return ptr +} + +/// The Swift enum case for a C-style enum discriminant. +func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { + guard let value = E(rawValue: raw) else { + wvDecodeFailure("unknown \(E.self) discriminant \(raw)") + } + return value +} + +// MARK: Strings, bytes, and buffers + +/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. +@inline(__always) +func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + var s = s + return try s.withUTF8 { try body($0.baseAddress, $0.count) } +} + +/// Lends the bytes of `d` for one call. +@inline(__always) +func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } +} + +/// Lends an encoded value buffer for one call. +@inline(__always) +func wvWithBuffer(_ w: WvWriter, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } +} + +/// Copies a returned string, then releases the library's allocation. +func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + defer { kitchen_sink_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies returned bytes, then releases the library's allocation. +func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + defer { kitchen_sink_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return Data(bytes: ptr, count: len) +} + +/// Decodes a returned value buffer in place, then releases it. +func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + defer { + if let ptr = ptr { kitchen_sink_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + } + return wvBorrowBuffer(ptr, len, decode) +} + +/// Copies a borrowed string. +func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies borrowed bytes. +func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + return Data(bytes: ptr, count: len) +} + +/// Decodes a borrowed value buffer in place. +func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + var r = WvReader(UnsafeRawPointer(ptr), len) + let value = decode(&r) + r.finish() + return value +} + +/// Stops the process on a malformed value buffer, which the wire format +/// treats like a marshalling failure. +func wvDecodeFailure(_ context: String) -> Never { + fatalError("KitchenSink: malformed value buffer: \(context)") +} + +/// Serializes values in the value-buffer wire format: little-endian, packed, +/// no alignment. +struct WvWriter { + var bytes: [UInt8] = [] + + init() { + bytes.reserveCapacity(64) + } + + @inline(__always) + private mutating func append(_ v: T) { + withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } + mutating func writeI8(_ v: Int8) { append(v) } + mutating func writeU8(_ v: UInt8) { append(v) } + mutating func writeI16(_ v: Int16) { append(v) } + mutating func writeU16(_ v: UInt16) { append(v) } + mutating func writeI32(_ v: Int32) { append(v) } + mutating func writeU32(_ v: UInt32) { append(v) } + mutating func writeI64(_ v: Int64) { append(v) } + mutating func writeU64(_ v: UInt64) { append(v) } + mutating func writeF32(_ v: Float) { append(v.bitPattern) } + mutating func writeF64(_ v: Double) { append(v.bitPattern) } + + /// Writes a `u32` length or element count. + mutating func writeLen(_ n: Int) { + precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") + append(UInt32(n)) + } + + mutating func writeString(_ v: String) { + let utf8 = v.utf8 + writeLen(utf8.count) + bytes.append(contentsOf: utf8) + } + + mutating func writeBytes(_ v: Data) { + writeLen(v.count) + bytes.append(contentsOf: v) + } + + mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } + + /// Writes an object token. `p` must be a strong reference the buffer now + /// owns (a freshly cloned pointer), never one a wrapper still holds. + mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } +} + +/// Deserializes values from the value-buffer wire format directly from the +/// library's memory, rejecting truncated buffers, invalid flag bytes, +/// oversized length prefixes, and trailing bytes. +struct WvReader { + private let base: UnsafeRawPointer? + private let count: Int + private var pos = 0 + + init(_ base: UnsafeRawPointer?, _ count: Int) { + self.base = base + self.count = base == nil ? 0 : count + } + + /// The number of unread bytes. + var remaining: Int { count - pos } + + private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { + guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } + defer { pos += n } + return base + pos + } + + @inline(__always) + private mutating func load(_ type: T.Type, _ what: StaticString) -> T { + T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) + } + + mutating func readBool() -> Bool { + switch load(UInt8.self, "bool") { + case 0: return false + case 1: return true + default: wvDecodeFailure("bool byte out of range") + } + } + mutating func readI8() -> Int8 { load(Int8.self, "i8") } + mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } + mutating func readI16() -> Int16 { load(Int16.self, "i16") } + mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } + mutating func readI32() -> Int32 { load(Int32.self, "i32") } + mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } + mutating func readI64() -> Int64 { load(Int64.self, "i64") } + mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } + mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } + mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } + + /// Reads a byte length, which can't exceed the unread bytes. + mutating func readLen() -> Int { + let n = Int(load(UInt32.self, "length")) + guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } + return n + } + + /// Reads an element count. Elements may encode to zero bytes, so the + /// count isn't bounded by the unread bytes; callers cap preallocation. + mutating func readCount() -> Int { + Int(load(UInt32.self, "count")) + } + + mutating func readString() -> String { + let n = readLen() + if n == 0 { return "" } + let p = take(n, "string") + guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { + wvDecodeFailure("string is not valid UTF-8") + } + return s + } + + mutating func readBytes() -> Data { + let n = readLen() + if n == 0 { return Data() } + return Data(bytes: take(n, "bytes"), count: n) + } + + mutating func readOptionFlag() -> Bool { + switch load(UInt8.self, "option flag") { + case 0: return false + case 1: return true + default: wvDecodeFailure("option flag byte out of range") + } + } + + /// Reads an object token: one strong reference the caller adopts into a + /// wrapper whose deinit releases it. + mutating func readObject() -> OpaquePointer { + guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { + wvDecodeFailure("null object token") + } + return p + } + + func finish() { + if remaining != 0 { wvDecodeFailure("trailing bytes after value") } + } +} + +// MARK: Async and callbacks + +/// A native cancel token owned by one cancellable call. The library takes its +/// own reference at launch, so this one is released whenever the wrapper is +/// done with it. +final class WvCancelToken: @unchecked Sendable { + let raw: OpaquePointer + + init() { + raw = wvNonNull(kitchen_sink_cancel_token_create()) + } + + deinit { + kitchen_sink_cancel_token_destroy(raw) + } + + func cancel() { + kitchen_sink_cancel_token_cancel(raw) + } +} + +/// A continuation boxed for the C `context` slot of an async launch. +final class WvContinuation { + let value: CheckedContinuation + + init(_ value: CheckedContinuation) { + self.value = value + } +} + +/// One process-wide callback vtable at a stable address the library may hold +/// for the life of the process. +final class WvVtable: @unchecked Sendable { + let pointer: UnsafePointer + + init(_ value: T) { + let cell = UnsafeMutablePointer.allocate(capacity: 1) + cell.initialize(to: value) + pointer = UnsafePointer(cell) + } +} + +/// Reports a thrown Swift error to the library as a callback failure +/// (code -4); the library aborts its current call with that code and message. +func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { + let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) + message.withCString { kitchen_sink_error_set(outErr, KitchenSinkRuntimeError.foreignCode, $0) } +} + +// MARK: - API + +/// A reusable token referenced across modules +public struct Token: Hashable, Sendable { + public var id: Int64 + public var label: String + + /// Creates a value from its fields. + public init(id: Int64, label: String) { + self.id = id + self.label = label + } +} + +func wvWriteToken(_ value: Token, into w: inout WvWriter) { + w.writeI64(value.id) + w.writeString(value.label) +} + +func wvReadToken(_ r: inout WvReader) -> Token { + let v0 = r.readI64() + let v1 = r.readString() + return Token(id: v0, label: v1) +} + +/// Typed errors reported by the `kitchen` module. +public enum KitchenErrorsError: Error, LocalizedError, Sendable { + case notFound(message: String) + case invalidInput(message: String) + + /// The message the library reported, or the code's documented default. + public var errorDescription: String? { + switch self { + case let .notFound(message): return message + case let .invalidInput(message): return message + } + } + + /// The numeric ABI code carried by this error. + public var errorCode: Int32 { + switch self { + case .notFound: return 1 + case .invalidInput: return 2 + } + } +} + +func mapKitchen(_ err: WvError) -> Error { + let message = wvErrorMessage(err) + switch err.code { + case 1: return KitchenErrorsError.notFound(message: message.isEmpty ? "Item not found" : message) + case 2: return KitchenErrorsError.invalidInput(message: message.isEmpty ? "Invalid input" : message) + default: return wvRuntimeError(err) + } +} + +func checkKitchen(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = mapKitchen(err) + kitchen_sink_error_clear(&err) + throw error +} + +/// Task priority levels +public enum Priority: Int32, Sendable { + /// Low priority + case low = 0 + /// Normal priority + case normal = 1 + /// High priority + case high = 2 +} + +/// Kitchen sink struct exercising every field feature +public struct Item: Sendable { + /// Stable identifier + public var id: Int64 + /// Display name + public var name: String + /// Initial count + public var count: Int32 + /// Whether the item is enabled + public var enabled: Bool + /// Scaling ratio + public var ratio: Double + public var nick: UInt32 + public var payload: Data + public var tags: [String] + public var attrs: [String: String] + public var parent: Int64? + /// Cross-module struct reference + public var token: Token + public var priority: Priority + /// An optional object inside a record + public var gadget: Gadget? + + /// Creates a value from its fields. + public init(id: Int64, name: String, count: Int32, enabled: Bool, ratio: Double, nick: UInt32, payload: Data, tags: [String], attrs: [String: String], parent: Int64?, token: Token, priority: Priority, gadget: Gadget?) { + self.id = id + self.name = name + self.count = count + self.enabled = enabled + self.ratio = ratio + self.nick = nick + self.payload = payload + self.tags = tags + self.attrs = attrs + self.parent = parent + self.token = token + self.priority = priority + self.gadget = gadget + } +} + +func wvWriteItem(_ value: Item, into w: inout WvWriter) { + w.writeI64(value.id) + w.writeString(value.name) + w.writeI32(value.count) + w.writeBool(value.enabled) + w.writeF64(value.ratio) + w.writeU32(value.nick) + w.writeBytes(value.payload) + w.writeLen(value.tags.count) + for v0 in value.tags { + w.writeString(v0) + } + w.writeLen(value.attrs.count) + for (v1, v2) in value.attrs { + w.writeString(v1) + w.writeString(v2) + } + if let v3 = value.parent { + w.writeOptionFlag(true) + w.writeI64(v3) + } else { + w.writeOptionFlag(false) + } + wvWriteToken(value.token, into: &w) + w.writeI32(value.priority.rawValue) + if let v4 = value.gadget { + w.writeOptionFlag(true) + w.writeObject(v4.clonePtr()) + } else { + w.writeOptionFlag(false) + } +} + +func wvReadItem(_ r: inout WvReader) -> Item { + let v5 = r.readI64() + let v6 = r.readString() + let v7 = r.readI32() + let v8 = r.readBool() + let v9 = r.readF64() + let v10 = r.readU32() + let v11 = r.readBytes() + let n13 = r.readCount() + var v12: [String] = [] + v12.reserveCapacity(min(n13, r.remaining)) + for _ in 0.. Bool +} + +/// Retains one `ReadyListener` implementation across the C boundary; the library's +/// `free` entry releases it. +final class WvReadyListenerBox: Sendable { + let impl: any ReadyListener + init(_ impl: any ReadyListener) { self.impl = impl } +} + +/// The process-wide `kitchen_sink_kitchen_ReadyListener_vtable` the library calls `ReadyListener` +/// implementations through. Every entry recovers the box from `ctx`, copies +/// or adopts its arguments, and reports a thrown error through `out_err` +/// instead of unwinding. +enum WvReadyListenerVtable { + static let shared = WvVtable(kitchen_sink_kitchen_ReadyListener_vtable( + on_ready: { ctx, code, msg_ptr, msg_len, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + try wvBox.impl.onReady(code: code, msg: wvBorrowString(msg_ptr, msg_len)) + } catch { + wvForeignError(out_err, error) + } + }, + on_item: { ctx, item_ptr, item_len, gadget, out_err in + let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + do { + return try wvBox.impl.onItem(item: wvBorrowBuffer(item_ptr, item_len, wvReadItem), gadget: Gadget(ptr: wvNonNull(gadget))) + } catch { + wvForeignError(out_err, error) + return false + } + }, + free: { ctx in + Unmanaged.fromOpaque(ctx!).release() + } + )) +} + +/// A pocket-sized interface exercising the object surface +public final class Gadget: @unchecked Sendable { + let ptr: OpaquePointer + + init(ptr: OpaquePointer) { + self.ptr = ptr + } + + deinit { + kitchen_sink_kitchen_Gadget_destroy(ptr) + } + + /// Returns a new strong reference to the same object, for a position that + /// takes ownership of it (an object token inside a value buffer). + func clonePtr() -> OpaquePointer { + wvNonNull(kitchen_sink_kitchen_Gadget_clone(ptr)) + } + + /// Create a gadget with the given id + public init(id: Int64) { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_Gadget_new(id, &err) + wvTrap(&err) + self.ptr = wvNonNull(rv) + } + + /// Render the gadget as a human-readable string + public func describe() -> String { + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_Gadget_describe(ptr, &outLen, &err) + wvTrap(&err) + return wvTakeString(rv, outLen) + } + + /// Poke the gadget a number of times, failing on a negative count + public func poke(times: Int32) throws -> Int32 { + var err = WvError() + let rv = kitchen_sink_kitchen_Gadget_poke(ptr, times, &err) + try checkKitchen(&err) + return rv + } + + /// The gadget subsystem version string + public static func version() -> String { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_Gadget_version(&outLen, &err) + wvTrap(&err) + return wvTakeString(rv, outLen) + } +} + +/// A lazy sequence over the `Gadget` elements streamed by `kitchen_sink_kitchen_stream_gadgets`. +/// +/// Each `next()` call pulls exactly one element from the library. The +/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` +/// when iteration is abandoned early. +public final class KitchenStreamGadgetsIterator: Sequence, IteratorProtocol { + private var handle: OpaquePointer? + + init(handle: OpaquePointer) { + self.handle = handle + } + + deinit { + destroyHandle() + } + + private func destroyHandle() { + guard let handle = handle else { return } + kitchen_sink_kitchen_StreamGadgetsIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the library, or returns `nil` once the + /// stream is exhausted (destroying the underlying iterator). + public func next() -> Gadget? { + guard let handle = handle else { return nil } + var item: OpaquePointer? = nil + var err = WvError() + if kitchen_sink_kitchen_StreamGadgetsIterator_next(handle, &item, &err) == 0 { + wvTrap(&err) + destroyHandle() + return nil + } + return Gadget(ptr: wvNonNull(item)) + } +} + +/// A lazy sequence over the `Item` elements streamed by `kitchen_sink_kitchen_stream_items`. +/// +/// Each `next()` call pulls exactly one element from the library. The +/// underlying iterator is destroyed eagerly on exhaustion and from `deinit` +/// when iteration is abandoned early. +public final class KitchenStreamItemsIterator: Sequence, IteratorProtocol { + private var handle: OpaquePointer? + + init(handle: OpaquePointer) { + self.handle = handle + } + + deinit { + destroyHandle() + } + + private func destroyHandle() { + guard let handle = handle else { return } + kitchen_sink_kitchen_StreamItemsIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the library, or returns `nil` once the + /// stream is exhausted (destroying the underlying iterator). + public func next() -> Item? { + guard let handle = handle else { return nil } + var item: UnsafePointer? = nil + var itemLen = 0 + var err = WvError() + if kitchen_sink_kitchen_StreamItemsIterator_next(handle, &item, &itemLen, &err) == 0 { + wvTrap(&err) + destroyHandle() + return nil + } + return wvTakeBuffer(item, itemLen, wvReadItem) + } +} + +public enum Shared { + /// Trivial cross-module helper + public static func ping() -> String { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_shared_ping(&outLen, &err) + wvTrap(&err) + return wvTakeString(rv, outLen) + } +} + +public enum Kitchen { + /// Identity for bool + public static func boolId(x: Bool) -> Bool { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_bool_id(x, &err) + wvTrap(&err) + return rv + } + + public static func i32Id(x: Int32) -> Int32 { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_i32_id(x, &err) + wvTrap(&err) + return rv + } + + public static func u32Id(x: UInt32) -> UInt32 { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_u32_id(x, &err) + wvTrap(&err) + return rv + } + + public static func i64Id(x: Int64) -> Int64 { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_i64_id(x, &err) + wvTrap(&err) + return rv + } + + public static func f64Id(x: Double) -> Double { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_f64_id(x, &err) + wvTrap(&err) + return rv + } + + public static func echoString(s: String) -> String { + wvLoad() + var err = WvError() + var outLen = 0 + let rv: UnsafePointer? = wvWithUTF8(s) { s_ptr, s_len in + kitchen_sink_kitchen_echo_string(s_ptr, s_len, &outLen, &err) + } + wvTrap(&err) + return wvTakeString(rv, outLen) + } + + public static func echoBytes(b: Data) -> Data { + wvLoad() + var err = WvError() + var outLen = 0 + let rv: UnsafePointer? = wvWithBytes(b) { b_ptr, b_len in + kitchen_sink_kitchen_echo_bytes(b_ptr, b_len, &outLen, &err) + } + wvTrap(&err) + return wvTakeBytes(rv, outLen) + } + + /// Register a listener, returning how many are attached + public static func subscribe(listener: ReadyListener) -> Int32 { + wvLoad() + var err = WvError() + let listener_ctx = Unmanaged.passRetained(WvReadyListenerBox(listener)).toOpaque() + let rv = kitchen_sink_kitchen_subscribe(listener_ctx, WvReadyListenerVtable.shared.pointer, &err) + wvTrap(&err) + return rv + } + + /// Optional object in both directions + public static func maybeGadget(fallback: Gadget?) -> Gadget? { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_maybe_gadget(fallback?.ptr, &err) + wvTrap(&err) + return rv.map { Gadget(ptr: $0) } + } + + /// A list of objects + public static func gadgets() -> [Gadget] { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_gadgets(&outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen) { (r: inout WvReader) -> [Gadget] in + let n1 = r.readCount() + var v0: [Gadget] = [] + v0.reserveCapacity(min(n1, r.remaining)) + for _ in 0.. KitchenStreamGadgetsIterator { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_stream_gadgets(&err) + wvTrap(&err) + return KitchenStreamGadgetsIterator(handle: wvNonNull(rv)) + } + + /// Optional struct return + public static func maybeItem(id: Int64) throws -> Item? { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_maybe_item(id, &outLen, &err) + try checkKitchen(&err) + return wvTakeBuffer(rv, outLen) { (r: inout WvReader) -> Item? in + var v0: Item? = nil + if r.readOptionFlag() { + let v1 = wvReadItem(&r) + v0 = v1 + } + return v0 + } + } + + /// List of structs + public static func listItems() -> [Item] { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_list_items(&outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen) { (r: inout WvReader) -> [Item] in + let n1 = r.readCount() + var v0: [Item] = [] + v0.reserveCapacity(min(n1, r.remaining)) + for _ in 0.. [String: Int32] { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_get_attrs(&outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen) { (r: inout WvReader) -> [String: Int32] in + let n1 = r.readCount() + var v0: [String: Int32] = [:] + v0.reserveCapacity(min(n1, r.remaining)) + for _ in 0.. KitchenStreamItemsIterator { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_stream_items(&err) + wvTrap(&err) + return KitchenStreamItemsIterator(handle: wvNonNull(rv)) + } + + /// Returns the shared Token type from another module + public static func crossModuleToken() -> Token { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_cross_module_token(&outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadToken) + } + + /// Async operation + public static func doAsync(input: String) async -> String { + wvLoad() + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + wvWithUTF8(input) { input_ptr, input_len in + kitchen_sink_kitchen_do_async(input_ptr, input_len, { context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + wvTrapBoxed(err) + } + cont.resume(returning: wvTakeString(resultPtr, resultLen)) + }, context) + } + } + } + + /// Cancellable async operation + /// - Throws: `CancellationError` when the calling task is cancelled, which + /// also cancels the native call. + public static func doCancellable(input: String) async throws -> String { + wvLoad() + let token = WvCancelToken() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + wvWithUTF8(input) { input_ptr, input_len in + kitchen_sink_kitchen_do_cancellable(input_ptr, input_len, token.raw, { context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + cont.resume(throwing: wvTakeError(err, wvCancelledOrTrap)) + return + } + cont.resume(returning: wvTakeString(resultPtr, resultLen)) + }, context) + } + } + } onCancel: { + token.cancel() + } + } + + /// Legacy operation kept for compatibility + @available(*, deprecated, message: "Use new_op instead") + public static func legacyOp() -> Int32 { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_legacy_op(&err) + wvTrap(&err) + return rv + } + + /// Replacement for legacy_op + public static func newOp() -> Int32 { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_new_op(&err) + wvTrap(&err) + return rv + } + + public enum Nested { + /// Trivial nested-module function + public static func hello() -> String { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_nested_hello(&outLen, &err) + wvTrap(&err) + return wvTakeString(rv, outLen) + } + } +} + +// END KitchenSink.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index 97b1a72c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_kitchen_sink__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,853 +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) } - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { writeUInt64(UInt64(UInt(bitPattern: p))) } -} - -/// 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") - } - } - /// Reads an object token: one strong reference the caller must adopt - /// into a wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(readUInt64())) else { - wvDecodeFailure("null object token") - } - return p - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -private final class ContinuationRef { - let value: CheckedContinuation - init(_ value: CheckedContinuation) { self.value = value } -} - -/// Reports a thrown Swift error to the producer as a foreign callback failure -/// (code -4); the producer aborts its current call with that code and message. -func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { - let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) - message.withCString { weaveffi_error_set(outErr, -4, $0) } -} - -/// A reusable token referenced across modules -public struct Token { - public var id: Int64 - public var label: String - - /// Creates a `Token` value. - public init(id: Int64, label: String) { - self.id = id - self.label = label - } -} - -/// Serializes a `Token` into the value-buffer wire format. -func wvWriteToken(_ value: Token, into w: inout WvWriter) { - w.writeInt64(value.id) - w.writeString(value.label) -} - -/// Deserializes a `Token` from the value-buffer wire format. -func wvReadToken(_ r: inout WvReader) -> Token { - let v0 = r.readInt64() - let v1 = r.readString() - return Token(id: v0, label: v1) -} - -public enum Shared { - /// Trivial cross-module helper - public static func ping() -> String { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_shared_ping(&err) - trap(&err) - guard let rv = rv else { fatalError("-1: null string") } - defer { weaveffi_free_string(rv) } - return String(cString: rv) - } -} - -/// Typed errors reported by the `kitchen` module. -public enum KitchenErrorsError: Error, LocalizedError { - case notFound(message: String) - case invalidInput(message: String) - public var errorDescription: String? { - switch self { - case let .notFound(message): return message - case let .invalidInput(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .notFound: return 1 - case .invalidInput: return 2 - } - } -} - -@inline(__always) -func mapKitchen(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1: return KitchenErrorsError.notFound(message: message.isEmpty ? "Item not found" : message) - case 2: return KitchenErrorsError.invalidInput(message: message.isEmpty ? "Invalid input" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkKitchen(_ 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 mapKitchen(code: code, message: message, payload: payload) - } -} - -/// Task priority levels -public enum Priority: UInt32 { - /// Low priority - case low = 0 - /// Normal priority - case normal = 1 - /// High priority - case high = 2 -} - -/// Kitchen sink struct exercising every field feature -public struct Item { - /// Stable identifier - public var id: Int64 - /// Display name - public var name: String - /// Initial count - public var count: Int32 - /// Whether the item is enabled - public var enabled: Bool - /// Scaling ratio - public var ratio: Double - public var nick: UInt32 - public var payload: Data - public var tags: [String] - public var attrs: [String: String] - public var parent: Int64? - /// Cross-module struct reference - public var token: Token - public var priority: Priority - /// An optional object inside a record - public var gadget: Gadget? - - /// Creates a `Item` value. - public init(id: Int64, name: String, count: Int32, enabled: Bool, ratio: Double, nick: UInt32, payload: Data, tags: [String], attrs: [String: String], parent: Int64?, token: Token, priority: Priority, gadget: Gadget?) { - self.id = id - self.name = name - self.count = count - self.enabled = enabled - self.ratio = ratio - self.nick = nick - self.payload = payload - self.tags = tags - self.attrs = attrs - self.parent = parent - self.token = token - self.priority = priority - self.gadget = gadget - } -} - -/// Serializes a `Item` into the value-buffer wire format. -func wvWriteItem(_ value: Item, into w: inout WvWriter) { - w.writeInt64(value.id) - w.writeString(value.name) - w.writeInt32(value.count) - w.writeBool(value.enabled) - w.writeDouble(value.ratio) - w.writeUInt32(value.nick) - w.writeBytes(value.payload) - w.writeLen(value.tags.count) - for v0 in value.tags { - w.writeString(v0) - } - w.writeLen(value.attrs.count) - for (v1, v2) in value.attrs { - w.writeString(v1) - w.writeString(v2) - } - if let v3 = value.parent { - w.writeOptionFlag(true) - w.writeInt64(v3) - } else { - w.writeOptionFlag(false) - } - wvWriteToken(value.token, into: &w) - w.writeInt32(Int32(bitPattern: value.priority.rawValue)) - if let v4 = value.gadget { - w.writeOptionFlag(true) - w.writeObject(v4.clonePtr()) - } else { - w.writeOptionFlag(false) - } -} - -/// Deserializes a `Item` from the value-buffer wire format. -func wvReadItem(_ r: inout WvReader) -> Item { - let v5 = r.readInt64() - let v6 = r.readString() - let v7 = r.readInt32() - let v8 = r.readBool() - let v9 = r.readDouble() - let v10 = r.readUInt32() - let v11 = r.readBytes() - let n13 = r.readLen() - var v12: [String] = [] - v12.reserveCapacity(n13) - for _ in 0.. Bool -} - -/// Retains one `ReadyListener` implementation across the C boundary; the producer's -/// `free` entry releases it. -final class WvReadyListenerBox { - let impl: any ReadyListener - init(_ impl: any ReadyListener) { self.impl = impl } -} - -/// The process-wide `weaveffi_kitchen_ReadyListener_vtable` the producer calls `ReadyListener` -/// implementations through. Every entry recovers the box from `ctx`, copies -/// or adopts its arguments, and reports a thrown error through `out_err` -/// instead of unwinding. -enum WvReadyListenerVtable { - static let value = weaveffi_kitchen_ReadyListener_vtable( - on_ready: { ctx, code, msg, out_err in - let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() - do { - try wvBox.impl.onReady(code: code, msg: String(cString: msg!)) - } catch { - wvForeignError(out_err, error) - } - }, - on_item: { ctx, item_ptr, item_len, gadget, out_err in - let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() - let b0 = [UInt8](UnsafeBufferPointer(start: item_ptr, count: item_len)) - var r1 = WvReader(bytes: b0) - let v2 = wvReadItem(&r1) - r1.finish() - do { - return try wvBox.impl.onItem(item: v2, gadget: Gadget(ptr: gadget!)) - } catch { - wvForeignError(out_err, error) - return false - } - }, - free: { ctx in - Unmanaged.fromOpaque(ctx!).release() - } - ) - static let pointer: UnsafePointer = { - let cell = UnsafeMutablePointer.allocate(capacity: 1) - cell.initialize(to: value) - return UnsafePointer(cell) - }() -} - -/// A pocket-sized interface exercising the object surface -public final class Gadget { - let ptr: OpaquePointer - - init(ptr: OpaquePointer) { - self.ptr = ptr - } - - deinit { - weaveffi_kitchen_Gadget_destroy(ptr) - } - - /// Returns a new strong reference to the same object, for a position that - /// takes ownership of it (an object token inside a value buffer). - func clonePtr() -> OpaquePointer { - weaveffi_kitchen_Gadget_clone(ptr)! - } - /// Create a gadget with the given id - public init(id: Int64) { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_Gadget_new(id, &err) - trap(&err) - guard let rv = rv else { fatalError("-1: null pointer") } - self.ptr = rv - } - /// Render the gadget as a human-readable string - public func describe() -> String { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_Gadget_describe(ptr, &err) - trap(&err) - guard let rv = rv else { fatalError("-1: null string") } - defer { weaveffi_free_string(rv) } - return String(cString: rv) - } - /// Poke the gadget a number of times, failing on a negative count - public func poke(times: Int32) throws -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_Gadget_poke(ptr, times, &err) - try checkKitchen(&err) - return rv - } - /// The gadget subsystem version string - public static func version() -> String { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_Gadget_version(&err) - trap(&err) - guard let rv = rv else { fatalError("-1: null string") } - defer { weaveffi_free_string(rv) } - return String(cString: rv) - } -} - -/// A lazy sequence over the `Gadget` elements streamed by `weaveffi_kitchen_stream_gadgets`. -/// -/// 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 KitchenStreamGadgetsIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - weaveffi_kitchen_StreamGadgetsIterator_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() -> Gadget? { - guard let handle = handle else { return nil } - var item: OpaquePointer? = nil - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - if weaveffi_kitchen_StreamGadgetsIterator_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 - } - return Gadget(ptr: item!) - } -} - -/// A lazy sequence over the `Item` elements streamed by `weaveffi_kitchen_stream_items`. -/// -/// 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 KitchenStreamItemsIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - weaveffi_kitchen_StreamItemsIterator_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() -> Item? { - guard let handle = handle else { return nil } - var item: UnsafePointer? = nil - var itemLen: Int = 0 - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - if weaveffi_kitchen_StreamItemsIterator_next(handle, &item, &itemLen, &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 itemBytes = [UInt8](UnsafeBufferPointer(start: item, count: itemLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: item), itemLen) - var itemReader = WvReader(bytes: itemBytes) - let v0 = wvReadItem(&itemReader) - itemReader.finish() - return v0 - } -} - -public enum Kitchen { - /// Identity for bool - public static func boolId(x: Bool) -> Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_bool_id(x, &err) - trap(&err) - return rv - } - public static func i32Id(x: Int32) -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_i32_id(x, &err) - trap(&err) - return rv - } - public static func u32Id(x: UInt32) -> UInt32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_u32_id(x, &err) - trap(&err) - return rv - } - public static func i64Id(x: Int64) -> Int64 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_i64_id(x, &err) - trap(&err) - return rv - } - public static func f64Id(x: Double) -> Double { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_f64_id(x, &err) - trap(&err) - return rv - } - public static func echoString(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_kitchen_echo_string(s_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 echoBytes(b: Data) -> Data { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let bBytes = Array(b) - var outLen: Int = 0 - let rv: UnsafePointer? = bBytes.withUnsafeBufferPointer { b_buf in - let b_ptr = b_buf.baseAddress - let b_len = b_buf.count - return weaveffi_kitchen_echo_bytes(b_ptr, b_len, &outLen, &err) - } - trap(&err) - guard let rv = rv else { return Data() } - defer { weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) } - return Data(bytes: rv, count: outLen) - } - /// Register a listener, returning how many are attached - public static func subscribe(listener: ReadyListener) -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let listener_ctx = Unmanaged.passRetained(WvReadyListenerBox(listener)).toOpaque() - let rv = weaveffi_kitchen_subscribe(listener_ctx, WvReadyListenerVtable.pointer, &err) - trap(&err) - return rv - } - /// Optional object in both directions - public static func maybeGadget(fallback: Gadget?) -> Gadget? { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_maybe_gadget(fallback?.ptr, &err) - trap(&err) - return rv.map { Gadget(ptr: $0) } - } - /// A list of objects - public static func gadgets() -> [Gadget] { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_kitchen_gadgets(&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: [Gadget] = [] - v0.reserveCapacity(n1) - for _ in 0.. KitchenStreamGadgetsIterator { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_stream_gadgets(&err) - trap(&err) - guard let rv = rv else { fatalError("-1: null iterator") } - return KitchenStreamGadgetsIterator(handle: rv) - } - /// Optional struct return - public static func maybeItem(id: Int64) throws -> Item? { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_kitchen_maybe_item(id, &outLen, &err) - try checkKitchen(&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: Item? = nil - if rvReader.readOptionFlag() { - let v1 = wvReadItem(&rvReader) - v0 = v1 - } - rvReader.finish() - return v0 - } - /// List of structs - public static func listItems() -> [Item] { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_kitchen_list_items(&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: [Item] = [] - v0.reserveCapacity(n1) - for _ in 0.. [String: Int32] { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_kitchen_get_attrs(&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: [String: Int32] = [:] - v0.reserveCapacity(n1) - for _ in 0.. KitchenStreamItemsIterator { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_stream_items(&err) - trap(&err) - guard let rv = rv else { fatalError("-1: null iterator") } - return KitchenStreamItemsIterator(handle: rv) - } - /// Returns the shared Token type from another module - public static func crossModuleToken() -> Token { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_kitchen_cross_module_token(&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 = wvReadToken(&rvReader) - rvReader.finish() - return v0 - } - /// Async operation - public static func doAsync(input: String) async -> String { - await withCheckedContinuation { (continuation: CheckedContinuation) in - let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() - input.withCString { input_ptr in - weaveffi_kitchen_do_async_async(input_ptr, { 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) - } - } - } - /// Cancellable async operation - public static func doCancellable(input: String) async -> String { - await withCheckedContinuation { (continuation: CheckedContinuation) in - let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() - input.withCString { input_ptr in - weaveffi_kitchen_do_cancellable_async(input_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) } ?? "" - 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) - } - } - } - /// Legacy operation kept for compatibility - @available(*, deprecated, message: "Use new_op instead") - public static func legacyOp() -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_legacy_op(&err) - trap(&err) - return rv - } - /// Replacement for legacy_op - public static func newOp() -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_new_op(&err) - trap(&err) - return rv - } - public enum Nested { - /// Trivial nested-module function - public static func hello() -> String { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kitchen_nested_hello(&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_nested_modules__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Package_swift.snap index 7ec6e100..49a75d55 100644 --- a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Package_swift.snap +++ b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Package_swift.snap @@ -1,22 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// swift-tools-version:5.9 +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o +import Foundation import PackageDescription +// The C module `CNestedModules` comes from one of two places. Next to this +// manifest, a prebuilt `CNestedModules.xcframework` (assembled from the native +// libraries, see the README) wins. Otherwise `Sources/CNestedModules` declares +// the generated header as a system library that links `nested_modules` from the +// linker search path, e.g. `swift build -Xlinker -L/path/to/lib`. +let xcframework = "CNestedModules.xcframework" +let cModule: Target = FileManager.default.fileExists(atPath: "\(Context.packageDirectory)/\(xcframework)") + ? .binaryTarget(name: "CNestedModules", path: xcframework) + : .systemLibrary(name: "CNestedModules") + let package = Package( - name: "WeaveFFI", + name: "NestedModules", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), + .library(name: "NestedModules", targets: ["NestedModules"]), ], targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), + cModule, + .target(name: "NestedModules", dependencies: ["CNestedModules"]), ] ) diff --git a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_CNestedModules_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_CNestedModules_module_modulemap.snap new file mode 100644 index 00000000..4bd72477 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_CNestedModules_module_modulemap.snap @@ -0,0 +1,14 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +module CNestedModules [system] { + header "nested_modules.h" + link "nested_modules" + export * +} + +// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_CNestedModules_nested_modules_h.snap b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_CNestedModules_nested_modules_h.snap new file mode 100644 index 00000000..27c97f2b --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_CNestedModules_nested_modules_h.snap @@ -0,0 +1,144 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef NESTED_MODULES_H +#define NESTED_MODULES_H + +#include +#include +#include + +#ifndef NESTED_MODULES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef NESTED_MODULES_BUILD +# define NESTED_MODULES_API __declspec(dllexport) +# else +# define NESTED_MODULES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define NESTED_MODULES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define NESTED_MODULES_API __attribute__((visibility("default"))) +# else +# define NESTED_MODULES_API +# endif +#endif + +#ifndef NESTED_MODULES_DEPRECATED +# if defined(_MSC_VER) +# define NESTED_MODULES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define NESTED_MODULES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define NESTED_MODULES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports nested_modules_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define NESTED_MODULES_ABI_VERSION 3u +NESTED_MODULES_API uint32_t nested_modules_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by nested_modules_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct nested_modules_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} nested_modules_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +NESTED_MODULES_API void nested_modules_error_set(nested_modules_error* err, int32_t code, const char* message); +NESTED_MODULES_API void nested_modules_error_clear(nested_modules_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + nested_modules_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +NESTED_MODULES_API void nested_modules_error_free(nested_modules_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +NESTED_MODULES_API void nested_modules_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct nested_modules_cancel_token nested_modules_cancel_token; +NESTED_MODULES_API nested_modules_cancel_token* nested_modules_cancel_token_create(void); +NESTED_MODULES_API void nested_modules_cancel_token_cancel(nested_modules_cancel_token* token); +NESTED_MODULES_API bool nested_modules_cancel_token_is_cancelled(const nested_modules_cancel_token* token); +NESTED_MODULES_API void nested_modules_cancel_token_destroy(nested_modules_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +NESTED_MODULES_API uint8_t* nested_modules_alloc(uint32_t size); +NESTED_MODULES_API void nested_modules_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +NESTED_MODULES_API uint64_t nested_modules_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define NESTED_MODULES_GRAPHICS_CHECKSUM 0x44d31b5b10fdb90dull +NESTED_MODULES_API uint64_t nested_modules_graphics_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with nested_modules_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * nested_modules_buffer.h declares a C struct and codec for each. + */ + +/** A measurement unit shared with nested submodules. */ +typedef enum { nested_modules_graphics_Unit_pixels = 0, nested_modules_graphics_Unit_points = 1 } nested_modules_graphics_Unit; +/** Error codes reported by throwing functions in the `graphics` module tree. */ +typedef enum { nested_modules_graphics_GraphicsError_OutOfBounds = 1 } nested_modules_graphics_GraphicsError; + +// Module: graphics +/** Construct a Canvas. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_new_canvas(int32_t width, size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes +/** Construct a Shape. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_make_shape(int32_t sides, size_t* out_len, nested_modules_error* out_err); +/** Nested function returning a struct owned by an ancestor module. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_describe(size_t* out_len, nested_modules_error* out_err); + +// Module: graphics_shapes_geometry +/** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ +NESTED_MODULES_API const uint8_t* nested_modules_graphics_shapes_geometry_compute(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, nested_modules_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // NESTED_MODULES_H + +// END nested_modules.h diff --git a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__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_nested_modules__Sources_NestedModules_NestedModules_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_NestedModules_NestedModules_swift.snap new file mode 100644 index 00000000..71d61a75 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_NestedModules_NestedModules_swift.snap @@ -0,0 +1,576 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import CNestedModules +import Foundation + +// MARK: - Runtime support + +/// A failure the native library reports outside any declared error domain: +/// an unknown error code or a runtime trap. Throwing wrappers raise it; +/// non-throwing wrappers stop the process with `fatalError` instead, since +/// Swift has no unchecked errors. +public struct NestedModulesRuntimeError: Error, LocalizedError, Hashable, Sendable { + /// The code of a generic runtime failure. + public static let genericCode: Int32 = -1 + /// The code of a panic caught at the library boundary. + public static let panicCode: Int32 = -2 + /// The code of an argument or result that failed to marshal. + public static let marshalCode: Int32 = -3 + /// The code of a callback implementation that threw. + public static let foreignCode: Int32 = -4 + + /// The numeric code the native library reported. + public let errorCode: Int32 + /// The message the native library reported. + public let message: String + + /// Creates an error carrying a native code and message. + public init(errorCode: Int32, message: String) { + self.errorCode = errorCode + self.message = message + } + + public var errorDescription: String? { message } +} + +/// The error slot every native call reports through. +typealias WvError = nested_modules_error + +/// The C ABI revision these bindings were generated against. +let wvAbiVersion: UInt32 = 3 + +/// The contract check, run once before the first native call: the library +/// must implement this ABI revision and every top-level module must carry +/// the checksum these bindings were generated from. +let wvContract: Void = { + let abi = nested_modules_abi_version() + guard abi == wvAbiVersion else { + fatalError("NestedModules: the native library 'nested_modules' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") + } + wvCheckModule("graphics", nested_modules_graphics_checksum(), 0x44d31b5b10fdb90d) +}() + +/// Runs the contract check if it hasn't run yet. +@inline(__always) +func wvLoad() { + _ = wvContract +} + +/// Stops the process when a module's contract checksum doesn't match. +func wvCheckModule(_ module: StaticString, _ actual: UInt64, _ expected: UInt64) { + guard actual == expected else { + fatalError("NestedModules: module '\(module)' of the native library 'nested_modules' doesn't match these bindings (checksum 0x\(String(actual, radix: 16)), expected 0x\(String(expected, radix: 16))); regenerate the bindings or rebuild the library") + } +} + +/// The message carried by an error slot. +func wvErrorMessage(_ err: WvError) -> String { + err.message.map { String(cString: $0) } ?? "" +} + +/// The Swift error for a code outside any declared domain: cancellation +/// (`-5`) becomes `CancellationError`, anything else the runtime error. +func wvRuntimeError(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + return NestedModulesRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) +} + +/// Stops the process for a failed call that can't throw, naming the code and +/// message the library reported. +func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { + fatalError("NestedModules.\(function) failed with code \(code): \(message)") +} + +/// Throws the runtime error for a filled error slot, clearing it. +@inline(__always) +func wvCheck(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = wvRuntimeError(err) + nested_modules_error_clear(&err) + throw error +} + +/// Stops the process for a filled error slot of a call that can't throw. +@inline(__always) +func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { + guard err.code != 0 else { return } + let code = err.code + let message = wvErrorMessage(err) + nested_modules_error_clear(&err) + wvFatal(code, message, function) +} + +/// Maps and releases the heap-boxed error an async completion received. +func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { + let error = map(err.pointee) + nested_modules_error_free(err) + return error +} + +/// The error mapping of a cancellable call that declares no error domain: +/// cancellation throws `CancellationError`, anything else stops the process. +func wvCancelledOrTrap(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + wvFatal(err.code, wvErrorMessage(err), "async call") +} + +/// Stops the process for the boxed error of an async call that can't throw. +func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { + wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) +} + +/// Unwraps a pointer the library promised is non-null. +@inline(__always) +func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { + guard let ptr = ptr else { + wvFatal(NestedModulesRuntimeError.marshalCode, "the native library returned a null pointer", function) + } + return ptr +} + +/// The Swift enum case for a C-style enum discriminant. +func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { + guard let value = E(rawValue: raw) else { + wvDecodeFailure("unknown \(E.self) discriminant \(raw)") + } + return value +} + +// MARK: Strings, bytes, and buffers + +/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. +@inline(__always) +func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + var s = s + return try s.withUTF8 { try body($0.baseAddress, $0.count) } +} + +/// Lends the bytes of `d` for one call. +@inline(__always) +func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } +} + +/// Lends an encoded value buffer for one call. +@inline(__always) +func wvWithBuffer(_ w: WvWriter, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } +} + +/// Copies a returned string, then releases the library's allocation. +func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + defer { nested_modules_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies returned bytes, then releases the library's allocation. +func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + defer { nested_modules_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return Data(bytes: ptr, count: len) +} + +/// Decodes a returned value buffer in place, then releases it. +func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + defer { + if let ptr = ptr { nested_modules_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + } + return wvBorrowBuffer(ptr, len, decode) +} + +/// Copies a borrowed string. +func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies borrowed bytes. +func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + return Data(bytes: ptr, count: len) +} + +/// Decodes a borrowed value buffer in place. +func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + var r = WvReader(UnsafeRawPointer(ptr), len) + let value = decode(&r) + r.finish() + return value +} + +/// Stops the process on a malformed value buffer, which the wire format +/// treats like a marshalling failure. +func wvDecodeFailure(_ context: String) -> Never { + fatalError("NestedModules: malformed value buffer: \(context)") +} + +/// Serializes values in the value-buffer wire format: little-endian, packed, +/// no alignment. +struct WvWriter { + var bytes: [UInt8] = [] + + init() { + bytes.reserveCapacity(64) + } + + @inline(__always) + private mutating func append(_ v: T) { + withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } + mutating func writeI8(_ v: Int8) { append(v) } + mutating func writeU8(_ v: UInt8) { append(v) } + mutating func writeI16(_ v: Int16) { append(v) } + mutating func writeU16(_ v: UInt16) { append(v) } + mutating func writeI32(_ v: Int32) { append(v) } + mutating func writeU32(_ v: UInt32) { append(v) } + mutating func writeI64(_ v: Int64) { append(v) } + mutating func writeU64(_ v: UInt64) { append(v) } + mutating func writeF32(_ v: Float) { append(v.bitPattern) } + mutating func writeF64(_ v: Double) { append(v.bitPattern) } + + /// Writes a `u32` length or element count. + mutating func writeLen(_ n: Int) { + precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") + append(UInt32(n)) + } + + mutating func writeString(_ v: String) { + let utf8 = v.utf8 + writeLen(utf8.count) + bytes.append(contentsOf: utf8) + } + + mutating func writeBytes(_ v: Data) { + writeLen(v.count) + bytes.append(contentsOf: v) + } + + mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } + + /// Writes an object token. `p` must be a strong reference the buffer now + /// owns (a freshly cloned pointer), never one a wrapper still holds. + mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } +} + +/// Deserializes values from the value-buffer wire format directly from the +/// library's memory, rejecting truncated buffers, invalid flag bytes, +/// oversized length prefixes, and trailing bytes. +struct WvReader { + private let base: UnsafeRawPointer? + private let count: Int + private var pos = 0 + + init(_ base: UnsafeRawPointer?, _ count: Int) { + self.base = base + self.count = base == nil ? 0 : count + } + + /// The number of unread bytes. + var remaining: Int { count - pos } + + private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { + guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } + defer { pos += n } + return base + pos + } + + @inline(__always) + private mutating func load(_ type: T.Type, _ what: StaticString) -> T { + T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) + } + + mutating func readBool() -> Bool { + switch load(UInt8.self, "bool") { + case 0: return false + case 1: return true + default: wvDecodeFailure("bool byte out of range") + } + } + mutating func readI8() -> Int8 { load(Int8.self, "i8") } + mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } + mutating func readI16() -> Int16 { load(Int16.self, "i16") } + mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } + mutating func readI32() -> Int32 { load(Int32.self, "i32") } + mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } + mutating func readI64() -> Int64 { load(Int64.self, "i64") } + mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } + mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } + mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } + + /// Reads a byte length, which can't exceed the unread bytes. + mutating func readLen() -> Int { + let n = Int(load(UInt32.self, "length")) + guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } + return n + } + + /// Reads an element count. Elements may encode to zero bytes, so the + /// count isn't bounded by the unread bytes; callers cap preallocation. + mutating func readCount() -> Int { + Int(load(UInt32.self, "count")) + } + + mutating func readString() -> String { + let n = readLen() + if n == 0 { return "" } + let p = take(n, "string") + guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { + wvDecodeFailure("string is not valid UTF-8") + } + return s + } + + mutating func readBytes() -> Data { + let n = readLen() + if n == 0 { return Data() } + return Data(bytes: take(n, "bytes"), count: n) + } + + mutating func readOptionFlag() -> Bool { + switch load(UInt8.self, "option flag") { + case 0: return false + case 1: return true + default: wvDecodeFailure("option flag byte out of range") + } + } + + /// Reads an object token: one strong reference the caller adopts into a + /// wrapper whose deinit releases it. + mutating func readObject() -> OpaquePointer { + guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { + wvDecodeFailure("null object token") + } + return p + } + + func finish() { + if remaining != 0 { wvDecodeFailure("trailing bytes after value") } + } +} + +// MARK: Async and callbacks + +/// A native cancel token owned by one cancellable call. The library takes its +/// own reference at launch, so this one is released whenever the wrapper is +/// done with it. +final class WvCancelToken: @unchecked Sendable { + let raw: OpaquePointer + + init() { + raw = wvNonNull(nested_modules_cancel_token_create()) + } + + deinit { + nested_modules_cancel_token_destroy(raw) + } + + func cancel() { + nested_modules_cancel_token_cancel(raw) + } +} + +/// A continuation boxed for the C `context` slot of an async launch. +final class WvContinuation { + let value: CheckedContinuation + + init(_ value: CheckedContinuation) { + self.value = value + } +} + +/// One process-wide callback vtable at a stable address the library may hold +/// for the life of the process. +final class WvVtable: @unchecked Sendable { + let pointer: UnsafePointer + + init(_ value: T) { + let cell = UnsafeMutablePointer.allocate(capacity: 1) + cell.initialize(to: value) + pointer = UnsafePointer(cell) + } +} + +/// Reports a thrown Swift error to the library as a callback failure +/// (code -4); the library aborts its current call with that code and message. +func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { + let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) + message.withCString { nested_modules_error_set(outErr, NestedModulesRuntimeError.foreignCode, $0) } +} + +// MARK: - API + +/// Typed errors reported by the `graphics` module. +public enum GraphicsError: Error, LocalizedError, Sendable { + case outOfBounds(message: String) + + /// The message the library reported, or the code's documented default. + public var errorDescription: String? { + switch self { + case let .outOfBounds(message): return message + } + } + + /// The numeric ABI code carried by this error. + public var errorCode: Int32 { + switch self { + case .outOfBounds: return 1 + } + } +} + +func mapGraphics(_ err: WvError) -> Error { + let message = wvErrorMessage(err) + switch err.code { + case 1: return GraphicsError.outOfBounds(message: message.isEmpty ? "coordinate out of bounds" : message) + default: return wvRuntimeError(err) + } +} + +func checkGraphics(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = mapGraphics(err) + nested_modules_error_clear(&err) + throw error +} + +/// A measurement unit shared with nested submodules. +public enum Unit: Int32, Sendable { + case pixels = 0 + case points = 1 +} + +/// Top-level struct that references a struct defined one level deeper. +public struct Canvas: Hashable, Sendable { + public var width: Int32 + /// Reference down into a nested module + public var root: Shape + + /// Creates a value from its fields. + public init(width: Int32, root: Shape) { + self.width = width + self.root = root + } +} + +func wvWriteCanvas(_ value: Canvas, into w: inout WvWriter) { + w.writeI32(value.width) + wvWriteShape(value.root, into: &w) +} + +func wvReadCanvas(_ r: inout WvReader) -> Canvas { + let v0 = r.readI32() + let v1 = wvReadShape(&r) + return Canvas(width: v0, root: v1) +} + +/// A struct one level deep that references both an ancestor enum and a deeper struct. +public struct Shape: Hashable, Sendable { + public var sides: Int32 + /// Reference up to the parent module's enum + public var unit: Unit + /// Reference down into a deeper module + public var detail: Detail + + /// Creates a value from its fields. + public init(sides: Int32, unit: Unit, detail: Detail) { + self.sides = sides + self.unit = unit + self.detail = detail + } +} + +func wvWriteShape(_ value: Shape, into w: inout WvWriter) { + w.writeI32(value.sides) + w.writeI32(value.unit.rawValue) + wvWriteDetail(value.detail, into: &w) +} + +func wvReadShape(_ r: inout WvReader) -> Shape { + let v0 = r.readI32() + let v1 = wvEnumCase(Unit.self, r.readI32()) + let v2 = wvReadDetail(&r) + return Shape(sides: v0, unit: v1, detail: v2) +} + +/// A struct two levels deep. +public struct Detail: Hashable, Sendable { + public var area: Double + + /// Creates a value from its fields. + public init(area: Double) { + self.area = area + } +} + +func wvWriteDetail(_ value: Detail, into w: inout WvWriter) { + w.writeF64(value.area) +} + +func wvReadDetail(_ r: inout WvReader) -> Detail { + let v0 = r.readF64() + return Detail(area: v0) +} + +public enum Graphics { + /// Construct a Canvas. + public static func newCanvas(width: Int32) -> Canvas { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = nested_modules_graphics_new_canvas(width, &outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadCanvas) + } + + public enum Shapes { + /// Construct a Shape. + public static func makeShape(sides: Int32) -> Shape { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = nested_modules_graphics_shapes_make_shape(sides, &outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadShape) + } + + /// Nested function returning a struct owned by an ancestor module. + public static func describe() -> Canvas { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = nested_modules_graphics_shapes_describe(&outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadCanvas) + } + + public enum Geometry { + /// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. + public static func compute(shape: Shape) throws -> Detail { + wvLoad() + var err = WvError() + var shapeWriter = WvWriter() + wvWriteShape(shape, into: &shapeWriter) + var outLen = 0 + let rv: UnsafePointer? = wvWithBuffer(shapeWriter) { shape_ptr, shape_len in + nested_modules_graphics_shapes_geometry_compute(shape_ptr, shape_len, &outLen, &err) + } + try checkGraphics(&err) + return wvTakeBuffer(rv, outLen, wvReadDetail) + } + } + } +} + +// END NestedModules.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index 89b960ce..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_nested_modules__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,367 +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) } - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { writeUInt64(UInt64(UInt(bitPattern: p))) } -} - -/// 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") - } - } - /// Reads an object token: one strong reference the caller must adopt - /// into a wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(readUInt64())) else { - wvDecodeFailure("null object token") - } - return p - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -/// Typed errors reported by the `graphics` module. -public enum GraphicsError: Error, LocalizedError { - case outOfBounds(message: String) - public var errorDescription: String? { - switch self { - case let .outOfBounds(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .outOfBounds: return 1 - } - } -} - -@inline(__always) -func mapGraphics(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1: return GraphicsError.outOfBounds(message: message.isEmpty ? "coordinate out of bounds" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkGraphics(_ 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 mapGraphics(code: code, message: message, payload: payload) - } -} - -/// A measurement unit shared with nested submodules. -public enum Unit: UInt32 { - case pixels = 0 - case points = 1 -} - -/// Top-level struct that references a struct defined one level deeper. -public struct Canvas { - public var width: Int32 - /// Reference down into a nested module - public var root: Shape - - /// Creates a `Canvas` value. - public init(width: Int32, root: Shape) { - self.width = width - self.root = root - } -} - -/// Serializes a `Canvas` into the value-buffer wire format. -func wvWriteCanvas(_ value: Canvas, into w: inout WvWriter) { - w.writeInt32(value.width) - wvWriteShape(value.root, into: &w) -} - -/// Deserializes a `Canvas` from the value-buffer wire format. -func wvReadCanvas(_ r: inout WvReader) -> Canvas { - let v0 = r.readInt32() - let v1 = wvReadShape(&r) - return Canvas(width: v0, root: v1) -} - -/// A struct one level deep that references both an ancestor enum and a deeper struct. -public struct Shape { - public var sides: Int32 - /// Reference up to the parent module's enum - public var unit: Unit - /// Reference down into a deeper module - public var detail: Detail - - /// Creates a `Shape` value. - public init(sides: Int32, unit: Unit, detail: Detail) { - self.sides = sides - self.unit = unit - self.detail = detail - } -} - -/// Serializes a `Shape` into the value-buffer wire format. -func wvWriteShape(_ value: Shape, into w: inout WvWriter) { - w.writeInt32(value.sides) - w.writeInt32(Int32(bitPattern: value.unit.rawValue)) - wvWriteDetail(value.detail, into: &w) -} - -/// Deserializes a `Shape` from the value-buffer wire format. -func wvReadShape(_ r: inout WvReader) -> Shape { - let v0 = r.readInt32() - let v1 = Unit(rawValue: UInt32(bitPattern: r.readInt32()))! - let v2 = wvReadDetail(&r) - return Shape(sides: v0, unit: v1, detail: v2) -} - -/// A struct two levels deep. -public struct Detail { - public var area: Double - - /// Creates a `Detail` value. - public init(area: Double) { - self.area = area - } -} - -/// Serializes a `Detail` into the value-buffer wire format. -func wvWriteDetail(_ value: Detail, into w: inout WvWriter) { - w.writeDouble(value.area) -} - -/// Deserializes a `Detail` from the value-buffer wire format. -func wvReadDetail(_ r: inout WvReader) -> Detail { - let v0 = r.readDouble() - return Detail(area: v0) -} - -public enum Graphics { - /// Construct a Canvas. - public static func newCanvas(width: Int32) -> Canvas { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_graphics_new_canvas(width, &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 = wvReadCanvas(&rvReader) - rvReader.finish() - return v0 - } - public enum Shapes { - /// Construct a Shape. - public static func makeShape(sides: Int32) -> Shape { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_graphics_shapes_make_shape(sides, &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 = wvReadShape(&rvReader) - rvReader.finish() - return v0 - } - /// Nested function returning a struct owned by an ancestor module. - public static func describe() -> Canvas { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_graphics_shapes_describe(&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 = wvReadCanvas(&rvReader) - rvReader.finish() - return v0 - } - public enum Geometry { - /// Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - public static func compute(shape: Shape) throws -> Detail { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var shapeWriter = WvWriter() - wvWriteShape(shape, into: &shapeWriter) - var outLen: Int = 0 - let rv: UnsafePointer? = shapeWriter.bytes.withUnsafeBufferPointer { shape_buf in - let shape_ptr = shape_buf.baseAddress - let shape_len = shape_buf.count - return weaveffi_graphics_shapes_geometry_compute(shape_ptr, shape_len, &outLen, &err) - } - try checkGraphics(&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 = wvReadDetail(&rvReader) - rvReader.finish() - return v0 - } - } - } -} - -// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Package_swift.snap index 7ec6e100..db67e1ba 100644 --- a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Package_swift.snap +++ b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Package_swift.snap @@ -1,22 +1,33 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml +// swift-tools-version:5.9 +// Generated by WeaveFFI [VERSION] from api.yml // DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o +// To regenerate: weaveffi generate api.yml -o +import Foundation import PackageDescription +// The C module `CShapes` comes from one of two places. Next to this +// manifest, a prebuilt `CShapes.xcframework` (assembled from the native +// libraries, see the README) wins. Otherwise `Sources/CShapes` declares +// the generated header as a system library that links `shapes` from the +// linker search path, e.g. `swift build -Xlinker -L/path/to/lib`. +let xcframework = "CShapes.xcframework" +let cModule: Target = FileManager.default.fileExists(atPath: "\(Context.packageDirectory)/\(xcframework)") + ? .binaryTarget(name: "CShapes", path: xcframework) + : .systemLibrary(name: "CShapes") + let package = Package( - name: "WeaveFFI", + name: "Shapes", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), + .library(name: "Shapes", targets: ["Shapes"]), ], targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), + cModule, + .target(name: "Shapes", dependencies: ["CShapes"]), ] ) diff --git a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_CShapes_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_CShapes_module_modulemap.snap new file mode 100644 index 00000000..5ce537e7 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_CShapes_module_modulemap.snap @@ -0,0 +1,14 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +module CShapes [system] { + header "shapes.h" + link "shapes" + export * +} + +// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_CShapes_shapes_h.snap b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_CShapes_shapes_h.snap new file mode 100644 index 00000000..df20c3f5 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_CShapes_shapes_h.snap @@ -0,0 +1,151 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +#ifndef SHAPES_H +#define SHAPES_H + +#include +#include +#include + +#ifndef SHAPES_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef SHAPES_BUILD +# define SHAPES_API __declspec(dllexport) +# else +# define SHAPES_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define SHAPES_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define SHAPES_API __attribute__((visibility("default"))) +# else +# define SHAPES_API +# endif +#endif + +#ifndef SHAPES_DEPRECATED +# if defined(_MSC_VER) +# define SHAPES_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define SHAPES_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define SHAPES_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The C ABI revision this header was generated against. The producer + exports shapes_abi_version() so a consumer can refuse to load a + library built for a different revision. */ +#define SHAPES_ABI_VERSION 3u +SHAPES_API uint32_t shapes_abi_version(void); + +/* Error slot written by every fallible call. `message` is NUL-terminated + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields + serialized in the value buffer format (null when the code declares no + fields). Both are released by shapes_error_clear. Positive codes are + the API's declared error codes; negative codes are runtime traps: + -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + interface implementation failed, -5 cancelled. */ +typedef struct shapes_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} shapes_error; + +/* Fill `err` with `code` and a producer-owned copy of `message`. Callback + interface implementations call this to report a failure (code -4) + without allocating with a foreign allocator. */ +SHAPES_API void shapes_error_set(shapes_error* err, int32_t code, const char* message); +SHAPES_API void shapes_error_clear(shapes_error* err); + +/* Async completions receive a heap-boxed error the consumer owns; + shapes_error_free releases the message, the payload, and the box. + NULL is a no-op. */ +SHAPES_API void shapes_error_free(shapes_error* err); + +/* Release a string, bytes, or value buffer the producer returned. */ +SHAPES_API void shapes_free_bytes(uint8_t* ptr, size_t len); + +/* Cancellation for `cancellable` async functions. A token starts with + one reference owned by the consumer; the producer takes its own for + each call it is passed to, so the consumer may cancel and destroy its + reference at any time. A cancelled call completes with code -5. */ +typedef struct shapes_cancel_token shapes_cancel_token; +SHAPES_API shapes_cancel_token* shapes_cancel_token_create(void); +SHAPES_API void shapes_cancel_token_cancel(shapes_cancel_token* token); +SHAPES_API bool shapes_cancel_token_is_cancelled(const shapes_cancel_token* token); +SHAPES_API void shapes_cancel_token_destroy(shapes_cancel_token* token); + +/* Linear-memory allocator the Wasm glue stages arguments through. + Exported by wasm32 producers only. */ +SHAPES_API uint8_t* shapes_alloc(uint32_t size); +SHAPES_API void shapes_dealloc(uint8_t* ptr, uint32_t size); + +/* Live-allocation counters for leak checks in tests: 0 objects, + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations. + Always 0 unless the producer enables the `leak-check` feature. */ +SHAPES_API uint64_t shapes_debug_live(int32_t kind); + +/* Contract checksums, one per top-level module. A consumer compares + each function's result with the constant it was generated against + before making any other call. */ +#define SHAPES_SHAPES_CHECKSUM 0x06acf433ffa2a0f0ull +SHAPES_API uint64_t shapes_shapes_checksum(void); + +/* + * Strings, bytes, and value buffers cross as (ptr, len) pairs. A + * parameter named "v" expands to a view borrowed for the call: + * const uint8_t* v_ptr, size_t v_len + * Strings are UTF-8 without a NUL terminator; NULL with length 0 is + * the empty value. A returned string, bytes, or buffer is + * `const uint8_t*` with a trailing `size_t* out_len`, owned by the + * caller and released with shapes_free_bytes. + * Records, rich enums, lists, maps, and optionals are serialized + * in the little-endian value buffer format; when generated, + * shapes_buffer.h declares a C struct and codec for each. + */ + +/** An algebraic shape (sum type with associated data) */ +typedef enum { + /** The empty shape (a unit variant) */ + shapes_shapes_Shape_Empty = 0, + /** A circle with a radius */ + shapes_shapes_Shape_Circle = 1, + /** An axis-aligned rectangle */ + shapes_shapes_Shape_Rectangle = 2, + /** An integer-bounded shape (exercises narrow + wide ints) */ + shapes_shapes_Shape_Bounded = 3, + /** A labeled shape with a small count */ + shapes_shapes_Shape_Labeled = 4 +} shapes_shapes_Shape_Tag; +/** A plain C-style enum (no payloads) */ +typedef enum { shapes_shapes_Channel_Red = 0, shapes_shapes_Channel_Green = 1, shapes_shapes_Channel_Blue = 2 } shapes_shapes_Channel; + +// Module: shapes +/** Render a shape to a string */ +SHAPES_API const uint8_t* shapes_shapes_describe(const uint8_t* shape_ptr, size_t shape_len, size_t* out_len, shapes_error* out_err); +/** Scale a shape by a factor, returning a new shape */ +SHAPES_API const uint8_t* shapes_shapes_scale(const uint8_t* shape_ptr, size_t shape_len, double factor, size_t* out_len, shapes_error* out_err); +/** Sum a list of bytes into a wide integer (numerics smoke) */ +SHAPES_API uint64_t shapes_shapes_sum_bytes(const uint8_t* values_ptr, size_t values_len, shapes_error* out_err); +/** Build a Numerics struct from every numeric primitive */ +SHAPES_API const uint8_t* shapes_shapes_make_numerics(int8_t a, int16_t b, int32_t c, int64_t d, uint8_t e, uint16_t f, uint32_t g, uint64_t h, float i, double j, size_t* out_len, shapes_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // SHAPES_H + +// END shapes.h diff --git a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_shapes__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_shapes__Sources_Shapes_Shapes_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_Shapes_Shapes_swift.snap new file mode 100644 index 00000000..3d983c30 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_Shapes_Shapes_swift.snap @@ -0,0 +1,590 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import CShapes +import Foundation + +// MARK: - Runtime support + +/// A failure the native library reports outside any declared error domain: +/// an unknown error code or a runtime trap. Throwing wrappers raise it; +/// non-throwing wrappers stop the process with `fatalError` instead, since +/// Swift has no unchecked errors. +public struct ShapesRuntimeError: Error, LocalizedError, Hashable, Sendable { + /// The code of a generic runtime failure. + public static let genericCode: Int32 = -1 + /// The code of a panic caught at the library boundary. + public static let panicCode: Int32 = -2 + /// The code of an argument or result that failed to marshal. + public static let marshalCode: Int32 = -3 + /// The code of a callback implementation that threw. + public static let foreignCode: Int32 = -4 + + /// The numeric code the native library reported. + public let errorCode: Int32 + /// The message the native library reported. + public let message: String + + /// Creates an error carrying a native code and message. + public init(errorCode: Int32, message: String) { + self.errorCode = errorCode + self.message = message + } + + public var errorDescription: String? { message } +} + +/// The error slot every native call reports through. +typealias WvError = shapes_error + +/// The C ABI revision these bindings were generated against. +let wvAbiVersion: UInt32 = 3 + +/// The contract check, run once before the first native call: the library +/// must implement this ABI revision and every top-level module must carry +/// the checksum these bindings were generated from. +let wvContract: Void = { + let abi = shapes_abi_version() + guard abi == wvAbiVersion else { + fatalError("Shapes: the native library 'shapes' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") + } + wvCheckModule("shapes", shapes_shapes_checksum(), 0x06acf433ffa2a0f0) +}() + +/// Runs the contract check if it hasn't run yet. +@inline(__always) +func wvLoad() { + _ = wvContract +} + +/// Stops the process when a module's contract checksum doesn't match. +func wvCheckModule(_ module: StaticString, _ actual: UInt64, _ expected: UInt64) { + guard actual == expected else { + fatalError("Shapes: module '\(module)' of the native library 'shapes' doesn't match these bindings (checksum 0x\(String(actual, radix: 16)), expected 0x\(String(expected, radix: 16))); regenerate the bindings or rebuild the library") + } +} + +/// The message carried by an error slot. +func wvErrorMessage(_ err: WvError) -> String { + err.message.map { String(cString: $0) } ?? "" +} + +/// The Swift error for a code outside any declared domain: cancellation +/// (`-5`) becomes `CancellationError`, anything else the runtime error. +func wvRuntimeError(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + return ShapesRuntimeError(errorCode: err.code, message: wvErrorMessage(err)) +} + +/// Stops the process for a failed call that can't throw, naming the code and +/// message the library reported. +func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { + fatalError("Shapes.\(function) failed with code \(code): \(message)") +} + +/// Throws the runtime error for a filled error slot, clearing it. +@inline(__always) +func wvCheck(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = wvRuntimeError(err) + shapes_error_clear(&err) + throw error +} + +/// Stops the process for a filled error slot of a call that can't throw. +@inline(__always) +func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { + guard err.code != 0 else { return } + let code = err.code + let message = wvErrorMessage(err) + shapes_error_clear(&err) + wvFatal(code, message, function) +} + +/// Maps and releases the heap-boxed error an async completion received. +func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { + let error = map(err.pointee) + shapes_error_free(err) + return error +} + +/// The error mapping of a cancellable call that declares no error domain: +/// cancellation throws `CancellationError`, anything else stops the process. +func wvCancelledOrTrap(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + wvFatal(err.code, wvErrorMessage(err), "async call") +} + +/// Stops the process for the boxed error of an async call that can't throw. +func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { + wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) +} + +/// Unwraps a pointer the library promised is non-null. +@inline(__always) +func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { + guard let ptr = ptr else { + wvFatal(ShapesRuntimeError.marshalCode, "the native library returned a null pointer", function) + } + return ptr +} + +/// The Swift enum case for a C-style enum discriminant. +func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { + guard let value = E(rawValue: raw) else { + wvDecodeFailure("unknown \(E.self) discriminant \(raw)") + } + return value +} + +// MARK: Strings, bytes, and buffers + +/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. +@inline(__always) +func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + var s = s + return try s.withUTF8 { try body($0.baseAddress, $0.count) } +} + +/// Lends the bytes of `d` for one call. +@inline(__always) +func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } +} + +/// Lends an encoded value buffer for one call. +@inline(__always) +func wvWithBuffer(_ w: WvWriter, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } +} + +/// Copies a returned string, then releases the library's allocation. +func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + defer { shapes_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies returned bytes, then releases the library's allocation. +func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + defer { shapes_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return Data(bytes: ptr, count: len) +} + +/// Decodes a returned value buffer in place, then releases it. +func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + defer { + if let ptr = ptr { shapes_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + } + return wvBorrowBuffer(ptr, len, decode) +} + +/// Copies a borrowed string. +func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies borrowed bytes. +func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + return Data(bytes: ptr, count: len) +} + +/// Decodes a borrowed value buffer in place. +func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + var r = WvReader(UnsafeRawPointer(ptr), len) + let value = decode(&r) + r.finish() + return value +} + +/// Stops the process on a malformed value buffer, which the wire format +/// treats like a marshalling failure. +func wvDecodeFailure(_ context: String) -> Never { + fatalError("Shapes: malformed value buffer: \(context)") +} + +/// Serializes values in the value-buffer wire format: little-endian, packed, +/// no alignment. +struct WvWriter { + var bytes: [UInt8] = [] + + init() { + bytes.reserveCapacity(64) + } + + @inline(__always) + private mutating func append(_ v: T) { + withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } + mutating func writeI8(_ v: Int8) { append(v) } + mutating func writeU8(_ v: UInt8) { append(v) } + mutating func writeI16(_ v: Int16) { append(v) } + mutating func writeU16(_ v: UInt16) { append(v) } + mutating func writeI32(_ v: Int32) { append(v) } + mutating func writeU32(_ v: UInt32) { append(v) } + mutating func writeI64(_ v: Int64) { append(v) } + mutating func writeU64(_ v: UInt64) { append(v) } + mutating func writeF32(_ v: Float) { append(v.bitPattern) } + mutating func writeF64(_ v: Double) { append(v.bitPattern) } + + /// Writes a `u32` length or element count. + mutating func writeLen(_ n: Int) { + precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") + append(UInt32(n)) + } + + mutating func writeString(_ v: String) { + let utf8 = v.utf8 + writeLen(utf8.count) + bytes.append(contentsOf: utf8) + } + + mutating func writeBytes(_ v: Data) { + writeLen(v.count) + bytes.append(contentsOf: v) + } + + mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } + + /// Writes an object token. `p` must be a strong reference the buffer now + /// owns (a freshly cloned pointer), never one a wrapper still holds. + mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } +} + +/// Deserializes values from the value-buffer wire format directly from the +/// library's memory, rejecting truncated buffers, invalid flag bytes, +/// oversized length prefixes, and trailing bytes. +struct WvReader { + private let base: UnsafeRawPointer? + private let count: Int + private var pos = 0 + + init(_ base: UnsafeRawPointer?, _ count: Int) { + self.base = base + self.count = base == nil ? 0 : count + } + + /// The number of unread bytes. + var remaining: Int { count - pos } + + private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { + guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } + defer { pos += n } + return base + pos + } + + @inline(__always) + private mutating func load(_ type: T.Type, _ what: StaticString) -> T { + T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) + } + + mutating func readBool() -> Bool { + switch load(UInt8.self, "bool") { + case 0: return false + case 1: return true + default: wvDecodeFailure("bool byte out of range") + } + } + mutating func readI8() -> Int8 { load(Int8.self, "i8") } + mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } + mutating func readI16() -> Int16 { load(Int16.self, "i16") } + mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } + mutating func readI32() -> Int32 { load(Int32.self, "i32") } + mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } + mutating func readI64() -> Int64 { load(Int64.self, "i64") } + mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } + mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } + mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } + + /// Reads a byte length, which can't exceed the unread bytes. + mutating func readLen() -> Int { + let n = Int(load(UInt32.self, "length")) + guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } + return n + } + + /// Reads an element count. Elements may encode to zero bytes, so the + /// count isn't bounded by the unread bytes; callers cap preallocation. + mutating func readCount() -> Int { + Int(load(UInt32.self, "count")) + } + + mutating func readString() -> String { + let n = readLen() + if n == 0 { return "" } + let p = take(n, "string") + guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { + wvDecodeFailure("string is not valid UTF-8") + } + return s + } + + mutating func readBytes() -> Data { + let n = readLen() + if n == 0 { return Data() } + return Data(bytes: take(n, "bytes"), count: n) + } + + mutating func readOptionFlag() -> Bool { + switch load(UInt8.self, "option flag") { + case 0: return false + case 1: return true + default: wvDecodeFailure("option flag byte out of range") + } + } + + /// Reads an object token: one strong reference the caller adopts into a + /// wrapper whose deinit releases it. + mutating func readObject() -> OpaquePointer { + guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { + wvDecodeFailure("null object token") + } + return p + } + + func finish() { + if remaining != 0 { wvDecodeFailure("trailing bytes after value") } + } +} + +// MARK: Async and callbacks + +/// A native cancel token owned by one cancellable call. The library takes its +/// own reference at launch, so this one is released whenever the wrapper is +/// done with it. +final class WvCancelToken: @unchecked Sendable { + let raw: OpaquePointer + + init() { + raw = wvNonNull(shapes_cancel_token_create()) + } + + deinit { + shapes_cancel_token_destroy(raw) + } + + func cancel() { + shapes_cancel_token_cancel(raw) + } +} + +/// A continuation boxed for the C `context` slot of an async launch. +final class WvContinuation { + let value: CheckedContinuation + + init(_ value: CheckedContinuation) { + self.value = value + } +} + +/// One process-wide callback vtable at a stable address the library may hold +/// for the life of the process. +final class WvVtable: @unchecked Sendable { + let pointer: UnsafePointer + + init(_ value: T) { + let cell = UnsafeMutablePointer.allocate(capacity: 1) + cell.initialize(to: value) + pointer = UnsafePointer(cell) + } +} + +/// Reports a thrown Swift error to the library as a callback failure +/// (code -4); the library aborts its current call with that code and message. +func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { + let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) + message.withCString { shapes_error_set(outErr, ShapesRuntimeError.foreignCode, $0) } +} + +// MARK: - API + +/// An algebraic shape (sum type with associated data) +public enum Shape: Hashable, Sendable { + /// The empty shape (a unit variant) + case empty + /// A circle with a radius + case circle(radius: Double) + /// An axis-aligned rectangle + case rectangle(width: Float, height: Float) + /// An integer-bounded shape (exercises narrow + wide ints) + case bounded(min: Int16, max: Int16, weight: UInt64) + /// A labeled shape with a small count + case labeled(label: String, count: UInt8) +} + +func wvWriteShape(_ value: Shape, into w: inout WvWriter) { + switch value { + case .empty: + w.writeI32(0) + case let .circle(v0): + w.writeI32(1) + w.writeF64(v0) + case let .rectangle(v1, v2): + w.writeI32(2) + w.writeF32(v1) + w.writeF32(v2) + case let .bounded(v3, v4, v5): + w.writeI32(3) + w.writeI16(v3) + w.writeI16(v4) + w.writeU64(v5) + case let .labeled(v6, v7): + w.writeI32(4) + w.writeString(v6) + w.writeU8(v7) + } +} + +func wvReadShape(_ r: inout WvReader) -> Shape { + let tag = r.readI32() + switch tag { + case 0: + return .empty + case 1: + let v8 = r.readF64() + return .circle(radius: v8) + case 2: + let v9 = r.readF32() + let v10 = r.readF32() + return .rectangle(width: v9, height: v10) + case 3: + let v11 = r.readI16() + let v12 = r.readI16() + let v13 = r.readU64() + return .bounded(min: v11, max: v12, weight: v13) + case 4: + let v14 = r.readString() + let v15 = r.readU8() + return .labeled(label: v14, count: v15) + default: + wvDecodeFailure("unknown Shape tag \(tag)") + } +} + +/// A plain C-style enum (no payloads) +public enum Channel: Int32, Sendable { + case red = 0 + case green = 1 + case blue = 2 +} + +/// One field per supported numeric primitive +public struct Numerics: Hashable, Sendable { + public var a: Int8 + public var b: Int16 + public var c: Int32 + public var d: Int64 + public var e: UInt8 + public var f: UInt16 + public var g: UInt32 + public var h: UInt64 + public var i: Float + public var j: Double + + /// Creates a value from its fields. + public init(a: Int8, b: Int16, c: Int32, d: Int64, e: UInt8, f: UInt16, g: UInt32, h: UInt64, i: Float, j: Double) { + self.a = a + self.b = b + self.c = c + self.d = d + self.e = e + self.f = f + self.g = g + self.h = h + self.i = i + self.j = j + } +} + +func wvWriteNumerics(_ value: Numerics, into w: inout WvWriter) { + w.writeI8(value.a) + w.writeI16(value.b) + w.writeI32(value.c) + w.writeI64(value.d) + w.writeU8(value.e) + w.writeU16(value.f) + w.writeU32(value.g) + w.writeU64(value.h) + w.writeF32(value.i) + w.writeF64(value.j) +} + +func wvReadNumerics(_ r: inout WvReader) -> Numerics { + let v0 = r.readI8() + let v1 = r.readI16() + let v2 = r.readI32() + let v3 = r.readI64() + let v4 = r.readU8() + let v5 = r.readU16() + let v6 = r.readU32() + let v7 = r.readU64() + let v8 = r.readF32() + let v9 = r.readF64() + return Numerics(a: v0, b: v1, c: v2, d: v3, e: v4, f: v5, g: v6, h: v7, i: v8, j: v9) +} + +public enum Shapes { + /// Render a shape to a string + public static func describe(shape: Shape) -> String { + wvLoad() + var err = WvError() + var shapeWriter = WvWriter() + wvWriteShape(shape, into: &shapeWriter) + var outLen = 0 + let rv: UnsafePointer? = wvWithBuffer(shapeWriter) { shape_ptr, shape_len in + shapes_shapes_describe(shape_ptr, shape_len, &outLen, &err) + } + wvTrap(&err) + return wvTakeString(rv, outLen) + } + + /// Scale a shape by a factor, returning a new shape + public static func scale(shape: Shape, factor: Double) -> Shape { + wvLoad() + var err = WvError() + var shapeWriter = WvWriter() + wvWriteShape(shape, into: &shapeWriter) + var outLen = 0 + let rv: UnsafePointer? = wvWithBuffer(shapeWriter) { shape_ptr, shape_len in + shapes_shapes_scale(shape_ptr, shape_len, factor, &outLen, &err) + } + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadShape) + } + + /// Sum a list of bytes into a wide integer (numerics smoke) + public static func sumBytes(values: Data) -> UInt64 { + wvLoad() + var err = WvError() + let rv: UInt64 = wvWithBytes(values) { values_ptr, values_len in + shapes_shapes_sum_bytes(values_ptr, values_len, &err) + } + wvTrap(&err) + return rv + } + + /// Build a Numerics struct from every numeric primitive + public static func makeNumerics(a: Int8, b: Int16, c: Int32, d: Int64, e: UInt8, f: UInt16, g: UInt32, h: UInt64, i: Float, j: Double) -> Numerics { + wvLoad() + var err = WvError() + var outLen = 0 + let rv = shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &outLen, &err) + wvTrap(&err) + return wvTakeBuffer(rv, outLen, wvReadNumerics) + } +} + +// END Shapes.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index 18381fb2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_shapes__Sources_WeaveFFI_WeaveFFI_swift.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 - -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) } - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { writeUInt64(UInt64(UInt(bitPattern: p))) } -} - -/// 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") - } - } - /// Reads an object token: one strong reference the caller must adopt - /// into a wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(readUInt64())) else { - wvDecodeFailure("null object token") - } - return p - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -/// An algebraic shape (sum type with associated data) -public enum Shape { - /// The empty shape (a unit variant) - case empty - /// A circle with a radius - case circle(radius: Double) - /// An axis-aligned rectangle - case rectangle(width: Float, height: Float) - /// An integer-bounded shape (exercises narrow + wide ints) - case bounded(min: Int16, max: Int16, weight: UInt64) - /// A labeled shape with a small count - case labeled(label: String, count: UInt8) -} - -/// Serializes a `Shape` into the value-buffer wire format. -func wvWriteShape(_ value: Shape, into w: inout WvWriter) { - switch value { - case .empty: - w.writeInt32(0) - case let .circle(v0): - w.writeInt32(1) - w.writeDouble(v0) - case let .rectangle(v1, v2): - w.writeInt32(2) - w.writeFloat(v1) - w.writeFloat(v2) - case let .bounded(v3, v4, v5): - w.writeInt32(3) - w.writeInt16(v3) - w.writeInt16(v4) - w.writeUInt64(v5) - case let .labeled(v6, v7): - w.writeInt32(4) - w.writeString(v6) - w.writeUInt8(v7) - } -} - -/// Deserializes a `Shape` from the value-buffer wire format. -func wvReadShape(_ r: inout WvReader) -> Shape { - let tag = r.readInt32() - switch tag { - case 0: - return .empty - case 1: - let v8 = r.readDouble() - return .circle(radius: v8) - case 2: - let v9 = r.readFloat() - let v10 = r.readFloat() - return .rectangle(width: v9, height: v10) - case 3: - let v11 = r.readInt16() - let v12 = r.readInt16() - let v13 = r.readUInt64() - return .bounded(min: v11, max: v12, weight: v13) - case 4: - let v14 = r.readString() - let v15 = r.readUInt8() - return .labeled(label: v14, count: v15) - default: - wvDecodeFailure("unknown Shape tag \(tag)") - } -} - -/// A plain C-style enum (no payloads) -public enum Channel: UInt32 { - case red = 0 - case green = 1 - case blue = 2 -} - -/// One field per supported numeric primitive -public struct Numerics { - public var a: Int8 - public var b: Int16 - public var c: Int32 - public var d: Int64 - public var e: UInt8 - public var f: UInt16 - public var g: UInt32 - public var h: UInt64 - public var i: Float - public var j: Double - - /// Creates a `Numerics` value. - public init(a: Int8, b: Int16, c: Int32, d: Int64, e: UInt8, f: UInt16, g: UInt32, h: UInt64, i: Float, j: Double) { - self.a = a - self.b = b - self.c = c - self.d = d - self.e = e - self.f = f - self.g = g - self.h = h - self.i = i - self.j = j - } -} - -/// Serializes a `Numerics` into the value-buffer wire format. -func wvWriteNumerics(_ value: Numerics, into w: inout WvWriter) { - w.writeInt8(value.a) - w.writeInt16(value.b) - w.writeInt32(value.c) - w.writeInt64(value.d) - w.writeUInt8(value.e) - w.writeUInt16(value.f) - w.writeUInt32(value.g) - w.writeUInt64(value.h) - w.writeFloat(value.i) - w.writeDouble(value.j) -} - -/// Deserializes a `Numerics` from the value-buffer wire format. -func wvReadNumerics(_ r: inout WvReader) -> Numerics { - let v0 = r.readInt8() - let v1 = r.readInt16() - let v2 = r.readInt32() - let v3 = r.readInt64() - let v4 = r.readUInt8() - let v5 = r.readUInt16() - let v6 = r.readUInt32() - let v7 = r.readUInt64() - let v8 = r.readFloat() - let v9 = r.readDouble() - return Numerics(a: v0, b: v1, c: v2, d: v3, e: v4, f: v5, g: v6, h: v7, i: v8, j: v9) -} - -public enum Shapes { - /// Render a shape to a string - public static func describe(shape: Shape) -> String { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var shapeWriter = WvWriter() - wvWriteShape(shape, into: &shapeWriter) - let rv: UnsafePointer? = shapeWriter.bytes.withUnsafeBufferPointer { shape_buf in - let shape_ptr = shape_buf.baseAddress - let shape_len = shape_buf.count - return weaveffi_shapes_describe(shape_ptr, shape_len, &err) - } - trap(&err) - guard let rv = rv else { fatalError("-1: null string") } - defer { weaveffi_free_string(rv) } - return String(cString: rv) - } - /// Scale a shape by a factor, returning a new shape - public static func scale(shape: Shape, factor: Double) -> Shape { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var shapeWriter = WvWriter() - wvWriteShape(shape, into: &shapeWriter) - var outLen: Int = 0 - let rv: UnsafePointer? = shapeWriter.bytes.withUnsafeBufferPointer { shape_buf in - let shape_ptr = shape_buf.baseAddress - let shape_len = shape_buf.count - return weaveffi_shapes_scale(shape_ptr, shape_len, factor, &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 = wvReadShape(&rvReader) - rvReader.finish() - return v0 - } - /// Sum a list of bytes into a wide integer (numerics smoke) - public static func sumBytes(values: Data) -> UInt64 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let valuesBytes = Array(values) - let rv: UInt64 = valuesBytes.withUnsafeBufferPointer { values_buf in - let values_ptr = values_buf.baseAddress - let values_len = values_buf.count - return weaveffi_shapes_sum_bytes(values_ptr, values_len, &err) - } - trap(&err) - return rv - } - /// Build a Numerics struct from every numeric primitive - public static func makeNumerics(a: Int8, b: Int16, c: Int32, d: Int64, e: UInt8, f: UInt16, g: UInt32, h: UInt64, i: Float, j: Double) -> Numerics { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j, &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 = wvReadNumerics(&rvReader) - rvReader.finish() - return v0 - } -} - -// END WeaveFFI.swift 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 28735168..999e96c8 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 @@ -1,278 +1,20 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Wasm (experimental) +# docs_everywhere -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. +Native bindings for docs_everywhere -Build (example): +WebAssembly bindings for a `wasm32-unknown-unknown` build of the `docs_everywhere` library, linked with `--export-table --growable-table` (for example `RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table"`). `init()` loads `docs_everywhere.wasm` from next to this package, or from the path in `DOCS_EVERYWHERE_LIBRARY` on Node.js; pass a URL, bytes, or a compiled module to load it from elsewhere: -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it: the loader accepts a URL to fetch, the module's bytes (for Node, where `fetch` cannot read files), or an already compiled `WebAssembly.Module`. - -## 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. - -### Objects - -Interfaces are **wrapper classes** holding one reference to a producer object (an `i32` pointer into linear memory). A wrapper you receive is yours: release it with `close()` (or bind it with `using`, which calls `Symbol.dispose`); a wrapper collected unclosed is released by a `FinalizationRegistry` backstop where the runtime provides one, and destroy runs exactly once either way. Passing a wrapper to a function lends its reference for the duration of the call. Inside a value buffer an object crosses as a `u64` token carrying one reference: the writer clones the wrapper's reference and the reader adopts the token into a new wrapper. - -### 64-bit Integers - -`i64` and `u64` are JavaScript **`bigint`** values everywhere: as arguments, returns, record fields, and callback parameters. Map keys of these types become string property names on the decoded object. A `number` is accepted where a `bigint` is expected (it's widened with `BigInt(v)`, so a fractional value throws a `RangeError`). Values crossing by value follow the wasm calling convention: an out-of-range `bigint` wraps modulo 2^64 rather than throwing, and unsigned results are read back as unsigned (`u64` via `BigInt.asUintN`, `u32` via `>>> 0`). - -### 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) and maps to `Wrapper | null`. - -### 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. - -### Async Functions - -`async` functions return a **`Promise`**. `wasm32-unknown-unknown` has no threads, so the producer's default spawner drives each future to completion inline, inside the launch call: the completion callback fires (through a function-table trampoline) before the wrapper returns, and the Promise is already settled when you `await` it. There is nothing to pump or poll from JS. A future that returns `Pending` can't be woken on this target with the default spawner; a producer whose futures wait on external events needs its own spawner (`weaveffi::set_spawner`), and driving that executor is then the producer's responsibility, not the glue's. A typed error rejects the Promise with the same class the synchronous wrapper would throw. - -### Callback Interfaces - -A callback interface is a TypeScript `interface`; pass any object with the matching methods where a parameter of that type is expected. The loader keeps one **static vtable per interface** in linear memory, filled with function-table indices of long-lived trampolines, and passes a small integer context key with it; the producer calls `free(ctx)` when it drops the callback, which releases your object. Delivery is **synchronous and same-thread**: `wasm32-unknown-unknown` is single-threaded, so a method runs only while a call into the module is on the stack. A producer that calls back from a spawned thread cannot run on this target at all. - -Method arguments that are strings, bytes, or buffered values are **borrowed for the duration of the call**: they're copied or decoded into JS values before your method runs. Object arguments transfer one reference to you: `close()` the wrapper when you're done with it. An exception thrown from a method is reported to the producer as error code `-4` (with the exception's message) and the method's default value is returned in its place; the producer then aborts the call it was making, and the original caller sees a `WeaveFFIError` with code `-4` carrying your message (see *Traps* below for how that abort reaches the caller on this target). - -### 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_error_set(err_ptr: i32, code: i32, msg: i32)`: fill an error slot (used to report callback exceptions) -- `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, marshalling failures, and callback exceptions -(code `-4`). 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. - -#### Callback failures without unwinding - -`wasm32-unknown-unknown` builds with `panic = "abort"`, so when a callback interface method throws, the producer can't unwind out of the call the way native targets do. Instead the runtime records the failure, the producer's code runs to completion on the method's default return value (`0`, `0n`, or `false`), and the thunk then reports code `-4` with the exception's message in place of the result, so the wrapper throws (or the Promise rejects with) the same `WeaveFFIError` a native build would produce. While that producer call is still on the stack, the glue refuses every further callback invocation with the same error rather than calling your implementation again, so your code observes the failure exactly as it would under unwinding. Producers must tolerate a callback returning its default once (a well-behaved producer already does, because a consumer could return that value on purpose). - -#### Traps - -A producer panic can't be caught on the Rust side either: it executes `unreachable`, and the engine throws a `WebAssembly.RuntimeError` out of the call. The glue translates that trap for you: the wrapper throws (or the Promise rejects with) a `WeaveFFIError` with code `-2` (`producer panicked`), or code `-4` if a callback method had already failed during the call. The error slot and every staged argument are released either way. The producer frames between the trap and the call are *not* unwound, so their cleanup does not run: a lock held across the panic stays locked, and allocations those frames owned leak. - -## API Reference - -### Module: `docs` - -#### Error Domain: `DocsErrorsError` - -Throwing wrappers in this module raise `DocsErrorsError` (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 | -|-------|------|-----------------|----------------| -| `NotFound` | `1` | Document not found | (none) | -| `InvalidStatus` | `2` | Invalid status transition | (none) | - -#### Functions - -##### `weaveffi_docs_create_document` - -Create a brand new document. - -The document starts in `Draft` status; callers must publish -it explicitly to make it visible. - - -`weaveffi_docs_create_document(title: i32, tags: i32, i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `title` | `string` | `i32` | NUL-terminated C string pointer | -| `tags` | `[string]` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `Document` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_docs_get_document` - -Look up a document by identifier - -`weaveffi_docs_get_document(id: i64) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `Document?` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_docs_publish_async` - -Publish a document in the background - -`weaveffi_docs_publish_async(id: i64) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_docs_watch` - -Register a listener for saved notifications +```js +import { init, docs } from 'docs_everywhere'; -`weaveffi_docs_watch(listener: i32, i32) -> void` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `listener` | `SavedListener` | `i32, i32` | context key + static vtable pointer | - -#### Interfaces - -##### `Notebook` - -An ordered collection of documents. - -Notebooks own their documents and release them when destroyed. - - -Passed as an **object pointer** (`i32`), wrapped by a JS class holding one reference. Constructors return an owned reference; methods lend the pointer as the implicit leading `self` argument; `close()` (or `Symbol.dispose`) releases the reference via the destroy symbol exactly once. - -##### `weaveffi_docs_Notebook_new` - -Create an empty notebook - -`weaveffi_docs_Notebook_new() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `Notebook` | `i32` | borrowed object pointer | - -##### `weaveffi_docs_Notebook_insert` - -Add a document, returning its position in the notebook - -`weaveffi_docs_Notebook_insert(document: i32, i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `document` | `Document` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_docs_Notebook_find` - -Look up a document by identifier, failing when none matches - -`weaveffi_docs_Notebook_find(id: i64) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `Document` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_docs_Notebook_size` - -The number of documents currently held - -`weaveffi_docs_Notebook_size() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_docs_Notebook_max_documents` - -The largest number of documents one notebook will hold - -`weaveffi_docs_Notebook_max_documents() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_docs_Notebook_clone` - -Returns a second reference to the same object. Used when the wrapper is encoded as an object token inside a value buffer. - -##### `weaveffi_docs_Notebook_destroy` - -Releases one object reference. Called by the wrapper's `close()` (and by the finalizer backstop). - -#### Callback Interfaces - -##### `SavedListener` - -Consumer-implemented observer of document-saved notifications. - -Implementations must be safe to call from any thread. - - -Implemented in JS as an object with the methods below. Passed as an `i32` context key plus an `i32` pointer to the static `weaveffi_docs_SavedListener_vtable` vtable; the vtable's trailing `free(ctx)` entry releases the JS object. - -| Method | Parameters | Returns | -|--------|------------|---------| -| `onSaved` | `doc_id: bigint`, `revision: number` | `void` | - -#### Structs - -##### `Document` - -Represents a single document tracked by the system. - -Documents are persisted to disk and exposed via the public API. - - -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` | -| `title` | `string` | -| `status` | `Status` | -| `tags` | `[string]` | - -#### Enums - -##### `Status` - -Lifecycle status of a document. - -Used to drive UI badges and filters. - - -Passed as `i32` discriminant. - -| Variant | Value | -|---------|-------| -| `Draft` | `0` | -| `Published` | `1` | -| `Archived` | `2` | +await init(); +``` diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_d_ts.snap new file mode 100644 index 00000000..b7cb6012 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_d_ts.snap @@ -0,0 +1,160 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class DocsEverywhereError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends DocsEverywhereError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof DocsEverywhereError; +/** + * Load the WebAssembly module and check that it matches these bindings. + * Every other export throws until the returned promise resolves. + */ +export declare function init( + source?: string | URL | BufferSource | WebAssembly.Module | Response, +): Promise; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + */ +export declare namespace docs { + /** The errors of the `docs` error domain. */ + export class DocsErrorsError extends $Error { + constructor(code: number, message?: string); + } + /** Returned when no document matches the requested id */ + export class NotFoundError extends docs.DocsErrorsError { + static readonly CODE: 1; + constructor(message?: string); + } + /** Raised when attempting to move into a forbidden state */ + export class InvalidStatusError extends docs.DocsErrorsError { + static readonly CODE: 2; + constructor(message?: string); + } + /** + * Lifecycle status of a document. + * + * Used to drive UI badges and filters. + */ + export enum Status { + /** Document is being edited */ + Draft = 0, + /** Document is publicly visible */ + Published = 1, + /** Document is hidden from listings */ + Archived = 2, + } + /** + * Represents a single document tracked by the system. + * + * Documents are persisted to disk and exposed via the public API. + */ + export interface Document { + /** Stable opaque identifier */ + id: bigint; + /** Human-readable title shown in lists */ + title: string; + /** Current lifecycle status */ + status: docs.Status; + /** Free-form tags for grouping */ + tags: string[]; + } + /** + * Consumer-implemented observer of document-saved notifications. + * + * Implementations must be safe to call from any thread. + */ + export interface SavedListener { + /** + * Fires after a document has been persisted successfully + * @param docId Identifier of the saved document + * @param revision Monotonically increasing revision + */ + onSaved(docId: bigint, revision: number): void; + } + /** + * An ordered collection of documents. + * + * Notebooks own their documents and release them when destroyed. + */ + export class Notebook { + /** Create an empty notebook */ + constructor(); + /** + * Add a document, returning its position in the notebook + * @param document The document to add + */ + insert(document: docs.Document): number; + /** + * Look up a document by identifier, failing when none matches + * @param id Identifier of the document to find + * @throws {docs.DocsErrorsError} + */ + find(id: bigint): docs.Document; + /** The number of documents currently held */ + size(): number; + /** The largest number of documents one notebook will hold */ + static maxDocuments(): number; + /** + * Release this wrapper's reference to the native object. Safe to call + * more than once; a wrapper that is never closed is released when it is + * garbage collected. Using the wrapper after `close()` throws. + */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; + } + /** + * Create a brand new document. + * + * The document starts in `Draft` status; callers must publish + * it explicitly to make it visible. + * @param title Human-readable title for the new document + * @param tags Initial set of tags (may be empty) + */ + export function createDocument(title: string, tags: string[]): docs.Document; + /** + * Look up a document by identifier + * @param id Identifier returned from create_document + */ + export function getDocument(id: bigint): docs.Document | null; + /** + * Publish a document in the background + * @param id Identifier of the document to publish + */ + export function publishAsync(id: bigint): Promise; + /** + * Register a listener for saved notifications + * @param listener Receiver of saved notifications + */ + export function watch(listener: docs.SavedListener): void; +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_js.snap new file mode 100644 index 00000000..04b73368 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__index_js.snap @@ -0,0 +1,304 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + DocsEverywhereError as $Error, +} from './runtime.js'; +export { DocsEverywhereError, CancelledError } from './runtime.js'; +import { $loadWasm, $unloaded } from './linear.js'; + +let $raw = $unloaded('docs_everywhere'); +// Handles are linear-memory addresses; object tokens are u64s. +const $token = Number; +let $loading = null; + +/** + * Load the WebAssembly module (by default `docs_everywhere.wasm` next to this file, or + * the file named by `DOCS_EVERYWHERE_LIBRARY` on Node.js) and check that it matches + * these bindings. Every other export throws until the returned promise + * resolves. + */ +export function init(source) { + $loading ??= $loadWasm(source, 'docs_everywhere', 'DOCS_EVERYWHERE_LIBRARY', new URL('./docs_everywhere.wasm', import.meta.url)) + .then((m) => { + $raw = $bind(m); + try { + $verify($raw, 'docs_everywhere', 'docs_everywhere', 3, [['docs', 0x7950eec1e9cfc819n]]); + } catch (e) { + $raw = $unloaded('docs_everywhere'); + throw e; + } + }) + .catch((e) => { + $loading = null; + throw e; + }); + return $loading; +} + +function $vt_docs_everywhere_docs_SavedListener(m) { + return [ + [['i32', 'i64', 'i32', 'i32'], [], 'vijii', (ctx, p0, p1, err) => { + try { + m.adapter(ctx).on_saved(p0, p1); + } catch (e) { + m.foreign(err, e); + } + }], + ]; +} + +// The raw entry points, one per C symbol. +function $bind(m) { + const x = m.x; + return { + docs_everywhere_abi_version: () => x.docs_everywhere_abi_version() >>> 0, + docs_everywhere_debug_live: (kind) => BigInt.asUintN(64, x.docs_everywhere_debug_live(m.num(kind))), + docs_everywhere_cancel_token_create: () => x.docs_everywhere_cancel_token_create(), + docs_everywhere_cancel_token_cancel: (t) => x.docs_everywhere_cancel_token_cancel(m.handle(t)), + docs_everywhere_cancel_token_destroy: (t) => x.docs_everywhere_cancel_token_destroy(m.handle(t)), + docs_everywhere_docs_checksum: () => BigInt.asUintN(64, x.docs_everywhere_docs_checksum()), + docs_everywhere_docs_Notebook_clone: (h) => x.docs_everywhere_docs_Notebook_clone(m.handle(h)), + docs_everywhere_docs_Notebook_destroy: (h) => x.docs_everywhere_docs_Notebook_destroy(m.handle(h)), + docs_everywhere_docs_create_document: (a0, a1) => { + let s0 = null, s1 = null; + try { + s0 = m.str(a0); + s1 = m.data(a1); + const r = x.docs_everywhere_docs_create_document(s0[0], s0[1], s1[0], s1[1], m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + } finally { + m.unstage(s0); + m.unstage(s1); + } + }, + docs_everywhere_docs_get_document: (a0) => { + const r = x.docs_everywhere_docs_get_document(m.i64(a0), m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + docs_everywhere_docs_publish_async: (a0) => { + return m.launch(['i32', 'i32', 'i32'], [], 'viii', (cb, ctx) => x.docs_everywhere_docs_publish_async(m.i64(a0), cb, ctx), (result) => result !== 0); + }, + docs_everywhere_docs_watch: (a0) => { + x.docs_everywhere_docs_watch(m.register(a0), m.vtable('docs_everywhere_docs_SavedListener', () => $vt_docs_everywhere_docs_SavedListener(m)), m.err); + m.check(); + }, + docs_everywhere_docs_Notebook_new: () => { + const r = x.docs_everywhere_docs_Notebook_new(m.err); + m.check(); + return r; + }, + docs_everywhere_docs_Notebook_insert: (self, a0) => { + let s0 = null; + try { + s0 = m.data(a0); + const r = x.docs_everywhere_docs_Notebook_insert(m.handle(self), s0[0], s0[1], m.err); + m.check(); + return r; + } finally { + m.unstage(s0); + } + }, + docs_everywhere_docs_Notebook_find: (self, a0) => { + const r = x.docs_everywhere_docs_Notebook_find(m.handle(self), m.i64(a0), m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + docs_everywhere_docs_Notebook_size: (self) => { + const r = x.docs_everywhere_docs_Notebook_size(m.handle(self), m.err); + m.check(); + return r; + }, + docs_everywhere_docs_Notebook_max_documents: () => { + const r = x.docs_everywhere_docs_Notebook_max_documents(m.err); + m.check(); + return r; + }, + }; +} + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.docs_everywhere_debug_live(kind); +} + +const docs$DocsErrorsError = class DocsErrorsError extends $Error {}; +const docs$NotFoundError = class NotFoundError extends docs$DocsErrorsError { + static CODE = 1; + constructor(message = 'Document not found') { + super(1, message); + } +}; +const docs$InvalidStatusError = class InvalidStatusError extends docs$DocsErrorsError { + static CODE = 2; + constructor(message = 'Invalid status transition') { + super(2, message); + } +}; +const $codes$docs$DocsErrors = new Map([[1, docs$NotFoundError], [2, docs$InvalidStatusError]]); +function $from$docs$DocsErrors(e) { + return $domain(e, $codes$docs$DocsErrors); +} + +const docs$Status = Object.freeze({ + Draft: 0, + Published: 1, + Archived: 2, + 0: 'Draft', + 1: 'Published', + 2: 'Archived', +}); + +function $w$docs$Document(w, v) { + w.writeI64(v.id); + w.writeString(v.title); + w.writeI32(v.status); + w.writeList(v.tags, $W.String); +} +function $r$docs$Document(r) { + return { + id: r.readI64(), + title: r.readString(), + status: r.readI32(), + tags: r.readList($R.String), + }; +} + +function $adapt$docs$SavedListener(impl) { + $impl(impl, 'SavedListener'); + return { + on_saved($a0, $a1) { + impl.onSaved($a0, $a1); + }, + }; +} + +const docs$Notebook = class Notebook extends $Object { + constructor() { + super(); + try { + $own(this, $raw.docs_everywhere_docs_Notebook_new()); + } catch ($e) { + throw $fault($e); + } + } + static $destroy(h) { + $raw.docs_everywhere_docs_Notebook_destroy(h); + } + static $clone(h) { + return $raw.docs_everywhere_docs_Notebook_clone(h); + } + insert(document) { + const $self = $lend(this, docs$Notebook); + try { + return $raw.docs_everywhere_docs_Notebook_insert($self, $encode(document, $w$docs$Document)); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } + } + find(id) { + const $self = $lend(this, docs$Notebook); + try { + return $decode($raw.docs_everywhere_docs_Notebook_find($self, id), $r$docs$Document); + } catch ($e) { + throw $from$docs$DocsErrors($e); + } finally { + $unlend(this); + } + } + size() { + const $self = $lend(this, docs$Notebook); + try { + return $raw.docs_everywhere_docs_Notebook_size($self); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } + } + static maxDocuments() { + try { + return $raw.docs_everywhere_docs_Notebook_max_documents(); + } catch ($e) { + throw $fault($e); + } + } +}; + +const docs$createDocument = function createDocument(title, tags) { + try { + return $decode($raw.docs_everywhere_docs_create_document(title, $encode(tags, (w, v) => w.writeList(v, $W.String))), $r$docs$Document); + } catch ($e) { + throw $fault($e); + } +}; + +const docs$getDocument = function getDocument(id) { + try { + return $decode($raw.docs_everywhere_docs_get_document(id), (r) => r.readOpt($r$docs$Document)); + } catch ($e) { + throw $fault($e); + } +}; + +const docs$publishAsync = async function publishAsync(id) { + try { + return await $raw.docs_everywhere_docs_publish_async(id); + } catch ($e) { + throw $fault($e); + } +}; + +const docs$watch = function watch(listener) { + try { + $raw.docs_everywhere_docs_watch($adapt$docs$SavedListener(listener)); + } catch ($e) { + throw $fault($e); + } +}; + +export const docs = Object.freeze({ + DocsErrorsError: docs$DocsErrorsError, + NotFoundError: docs$NotFoundError, + InvalidStatusError: docs$InvalidStatusError, + Status: docs$Status, + Notebook: docs$Notebook, + createDocument: docs$createDocument, + getDocument: docs$getDocument, + publishAsync: docs$publishAsync, + watch: docs$watch, +}); + +// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__linear_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__linear_js.snap new file mode 100644 index 00000000..608de11a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__linear_js.snap @@ -0,0 +1,403 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The WebAssembly transport: loading the module, staging arguments in its +// linear memory, reading results and errors back, and installing JavaScript +// functions in its function table (async completions and callback +// interface vtables). The generated `index.js` builds one native entry point +// per C symbol on top of this. + +import { $Fault } from './runtime.js'; + +const $enc = new TextEncoder(); +const $dec = new TextDecoder(); + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v, bits, what) { + let b; + if (typeof v === 'bigint') { + b = v; + } else if (typeof v === 'number' && Number.isInteger(v)) { + b = BigInt(v); + } else { + throw $expected('a bigint', v); + } + if (bits(64, b) !== b) throw new RangeError(`bigint does not fit in ${what} 64-bit integer`); + return b; +} + +/** + * One loaded module's linear memory and runtime symbols. `x` holds the + * module's exports by C symbol name; `memory()` returns the current + * `ArrayBuffer`; `fnptr(params, results, sig, fn)` installs `fn` in the + * function table and returns its index. + */ +export class $Linear { + constructor(x, prefix, memory, fnptr) { + this.x = x; + this.memory = memory; + this.fnptr = fnptr; + this.allocFn = x[`${prefix}_alloc`]; + this.deallocFn = x[`${prefix}_dealloc`]; + this.freeBytesFn = x[`${prefix}_free_bytes`]; + this.errorClearFn = x[`${prefix}_error_clear`]; + this.errorFreeFn = x[`${prefix}_error_free`]; + this.errorSetFn = x[`${prefix}_error_set`]; + if (typeof this.allocFn !== 'function' || typeof this.deallocFn !== 'function') { + throw new Error(`the WebAssembly module does not export ${prefix}_alloc and ${prefix}_dealloc`); + } + // One scratch block reused by every call: the error slot (16 bytes), + // the out_len slot (4), and the iterator out_item slot (8). The producer + // only writes them as a call returns, so a call made from a callback in + // the middle of another call can reuse them safely. + const scratch = this.allocFn(32); + this.bytes().fill(0, scratch, scratch + 32); + this.err = scratch; + this.len = scratch + 16; + this.item = scratch + 24; + this.callbacks = new Map(); + this.nextCallback = 1; + this.pending = new Map(); + this.nextPending = 1; + this.completions = new Map(); + this.vtables = new Map(); + } + + bytes() { + const buffer = this.memory(); + if (buffer !== this.buffer) { + this.buffer = buffer; + this.u8 = new Uint8Array(buffer); + this.dv = new DataView(buffer); + } + return this.u8; + } + + view() { + this.bytes(); + return this.dv; + } + + // Argument checks and conversions for by-value slots. + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + i64(v) { + return $bigint(v, BigInt.asIntN, 'a signed'); + } + + u64(v) { + return $bigint(v, BigInt.asUintN, 'an unsigned'); + } + + bool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + return v ? 1 : 0; + } + + handle(v) { + if (typeof v !== 'number' || v === 0) throw $expected('an object handle', v); + return v; + } + + handleOpt(v) { + return v === null || v === undefined ? 0 : this.handle(v); + } + + // Staging: copy a string or bytes into linear memory for one call. A + // staged value is [ptr, len, size]; release it with unstage(). + str(s) { + if (typeof s !== 'string') throw $expected('a string', s); + if (s.length === 0) return [0, 0, 0]; + const size = s.length * 3; + const ptr = this.allocFn(size); + const { written } = $enc.encodeInto(s, this.bytes().subarray(ptr, ptr + size)); + return [ptr, written, size]; + } + + data(b) { + if (!(b instanceof Uint8Array)) throw $expected('a Uint8Array', b); + if (b.length === 0) return [0, 0, 0]; + const ptr = this.allocFn(b.length); + this.bytes().set(b, ptr); + return [ptr, b.length, b.length]; + } + + unstage(staged) { + if (staged !== null && staged[2] !== 0) this.deallocFn(staged[0], staged[2]); + } + + // Results. + outLen() { + return this.view().getUint32(this.len, true); + } + + readStr(ptr, len) { + return len === 0 ? '' : $dec.decode(this.bytes().subarray(ptr, ptr + len)); + } + + readData(ptr, len) { + return len === 0 ? new Uint8Array(0) : this.bytes().slice(ptr, ptr + len); + } + + takeStr(ptr, len) { + const s = this.readStr(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return s; + } + + takeData(ptr, len) { + const b = this.readData(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return b; + } + + cstr(ptr) { + if (ptr === 0) return ''; + const u8 = this.bytes(); + let end = ptr; + while (u8[end] !== 0) end++; + return $dec.decode(u8.subarray(ptr, end)); + } + + fault(errPtr) { + const dv = this.view(); + const code = dv.getInt32(errPtr, true); + if (code === 0) return null; + const payloadPtr = dv.getUint32(errPtr + 8, true); + const payloadLen = dv.getUint32(errPtr + 12, true); + return new $Fault( + code, + this.cstr(dv.getUint32(errPtr + 4, true)), + payloadPtr === 0 ? null : this.bytes().slice(payloadPtr, payloadPtr + payloadLen), + ); + } + + /** Throw the error the last call wrote to the scratch error slot. */ + check() { + if (this.view().getInt32(this.err, true) === 0) return; + const fault = this.fault(this.err); + this.errorClearFn(this.err); + throw fault; + } + + // Callback interfaces: implementations live in a table keyed by the + // integer passed as `ctx`, until the producer calls the vtable's `free`. + register(adapter) { + const id = this.nextCallback++; + this.callbacks.set(id, adapter); + return id; + } + + adapter(id) { + const adapter = this.callbacks.get(id); + if (adapter === undefined) throw new Error('callback implementation was already released'); + return adapter; + } + + unregister(id) { + this.callbacks.delete(id); + } + + /** Report a callback implementation's exception through `errPtr` (code -4). */ + foreign(errPtr, e) { + const message = e instanceof Error ? e.message || e.name : String(e); + const bytes = $enc.encode(message); + const ptr = this.allocFn(bytes.length + 1); + const u8 = this.bytes(); + u8.set(bytes, ptr); + u8[ptr + bytes.length] = 0; + this.errorSetFn(errPtr, -4, ptr); + this.deallocFn(ptr, bytes.length + 1); + } + + /** + * The static vtable of one callback interface: one table entry per + * method (`methods()` returns each as `[params, results, sig, fn]`), then + * `free`. Built on first use. + */ + vtable(name, methods) { + let ptr = this.vtables.get(name); + if (ptr !== undefined) return ptr; + const entries = [...methods(), [['i32'], [], 'vi', (ctx) => this.unregister(ctx)]]; + ptr = this.allocFn(entries.length * 4); + const dv = this.view(); + entries.forEach(([params, results, sig, fn], i) => { + dv.setUint32(ptr + i * 4, this.fnptr(params, results, sig, fn), true); + }); + this.vtables.set(name, ptr); + return ptr; + } + + /** + * Launch an async call. `call(callback, context)` invokes the launcher; + * the completion (one table entry per signature, shared by every call) + * settles the returned promise, converting the result slots with + * `convert`. On this single-threaded target the completion normally + * fires before the launcher returns. + */ + launch(params, results, sig, call, convert) { + let callback = this.completions.get(sig); + if (callback === undefined) { + const complete = (ctx, errPtr, ...result) => this.complete(ctx, errPtr, result); + callback = this.fnptr(params, results, sig, complete); + this.completions.set(sig, callback); + } + const id = this.nextPending++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject, convert }); + try { + call(callback, id); + } catch (e) { + this.pending.delete(id); + reject(e); + } + }); + } + + complete(id, errPtr, result) { + const pending = this.pending.get(id); + if (pending === undefined) return; + this.pending.delete(id); + try { + const fault = errPtr === 0 ? null : this.fault(errPtr); + if (errPtr !== 0) this.errorFreeFn(errPtr); + if (fault !== null) { + pending.reject(fault); + } else { + pending.resolve(pending.convert(...result)); + } + } catch (e) { + pending.reject(e); + } + } +} + +/** A stand-in for the native entry points until `init()` has finished. */ +export function $unloaded(library) { + return new Proxy( + {}, + { + get() { + throw new Error(`${library}: call init() before using the bindings`); + }, + }, + ); +} + +const $valtype = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c }; + +function $leb(n) { + const out = []; + do { + let b = n & 0x7f; + n >>>= 7; + if (n !== 0) b |= 0x80; + out.push(b); + } while (n !== 0); + return out; +} + +function $section(id, body) { + return [id, ...$leb(body.length), ...body]; +} + +/** + * Turn a JavaScript function into a WebAssembly function with the given + * signature: `WebAssembly.Function` where the engine has it, otherwise a + * tiny module that imports `fn` and re-exports it. + */ +function $wasmFunction(params, results, fn) { + if (typeof WebAssembly.Function === 'function') { + return new WebAssembly.Function({ parameters: params, results }, fn); + } + const type = [0x60, ...$leb(params.length), ...params.map((t) => $valtype[t])]; + type.push(...$leb(results.length), ...results.map((t) => $valtype[t])); + const bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + ...$section(1, [1, ...type]), + ...$section(2, [1, 1, 0x65, 1, 0x66, 0x00, 0x00]), + ...$section(7, [1, 1, 0x66, 0x00, 0x00]), + ]); + const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), { e: { f: fn } }); + return instance.exports.f; +} + +async function $bytesOf(source, envVar, fallback) { + if (source === undefined || source === null) { + const env = typeof process === 'object' && process.env ? process.env[envVar] : undefined; + if (env) { + const { readFile } = await import('node:fs/promises'); + return readFile(env); + } + source = fallback; + } + if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { + return source; + } + if (typeof Response === 'function' && source instanceof Response) { + return source.arrayBuffer(); + } + const url = source instanceof URL ? source : new URL(String(source), fallback); + if (url.protocol === 'file:') { + const { readFile } = await import('node:fs/promises'); + return readFile(url); + } + const response = await fetch(url); + if (!response.ok) throw new Error(`failed to fetch ${url}: ${response.status}`); + return response.arrayBuffer(); +} + +/** + * Instantiate a `wasm32-unknown-unknown` module from `source` (a URL, a + * path, bytes, a `Response`, or a compiled module). Without a source it + * reads the file named by the `envVar` environment variable (on Node.js), + * else `fallback`. The module must export its memory and a growable + * function table (`__indirect_function_table`). + */ +export async function $loadWasm(source, prefix, envVar, fallback) { + const bytes = await $bytesOf(source, envVar, fallback); + const module = bytes instanceof WebAssembly.Module ? bytes : await WebAssembly.compile(bytes); + const { exports } = await WebAssembly.instantiate(module, {}); + const table = exports.__indirect_function_table; + const fnptr = (params, results, sig, fn) => { + if (!(table instanceof WebAssembly.Table)) { + throw new Error('the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)'); + } + const index = table.grow(1); + table.set(index, $wasmFunction(params, results, fn)); + return index; + }; + return new $Linear(exports, prefix, () => exports.memory.buffer, fnptr); +} + +/** + * Adopt an initialized Emscripten module (or the promise its `MODULARIZE` + * factory returns). Its exports carry a leading underscore, its memory is + * read through `HEAPU8`, and table entries are installed with + * `addFunction`, so it must be linked with `-sALLOW_TABLE_GROWTH`, + * `-sWASM_BIGINT`, and `-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`. + */ +export async function $loadEmscripten(module, prefix, symbols) { + const m = await module; + const x = {}; + for (const symbol of symbols) x[symbol] = m[`_${symbol}`]; + const fnptr = (params, results, sig, fn) => { + if (typeof m.addFunction !== 'function') { + throw new Error('the Emscripten module does not export addFunction (link with -sALLOW_TABLE_GROWTH and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8)'); + } + return m.addFunction(fn, sig); + }; + return new $Linear(x, prefix, () => m.HEAPU8.buffer, fnptr); +} + +// END linear.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__package_json.snap index cee4ead6..90367f12 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__package_json.snap @@ -2,13 +2,25 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "docs_everywhere", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for docs_everywhere", "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" + "main": "index.js", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "linear.js" + ] } diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__runtime_js.snap new file mode 100644 index 00000000..75fa389b --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class DocsEverywhereError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends DocsEverywhereError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new DocsEverywhereError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new DocsEverywhereError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new DocsEverywhereError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new DocsEverywhereError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js 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 deleted file mode 100644 index 812a67bb..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_d_ts.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 - -// 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, producer - * panics, and callback-interface implementations that raised (code -4). - * Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** Base error for the `docs` module's error domain. */ -export declare class DocsErrorsError extends WeaveFFIError { - static readonly NotFound: typeof NotFound; - static readonly InvalidStatus: typeof InvalidStatus; -} - -/** Returned when no document matches the requested id */ -export declare class NotFound extends DocsErrorsError { - constructor(message?: string); - static readonly CODE: 1; -} - -/** Raised when attempting to move into a forbidden state */ -export declare class InvalidStatus extends DocsErrorsError { - constructor(message?: string); - static readonly CODE: 2; -} - -/** - * Represents a single document tracked by the system. - * - * Documents are persisted to disk and exposed via the public API. - */ -export interface Document { - /** Stable opaque identifier */ - id: bigint; - /** Human-readable title shown in lists */ - title: string; - /** Current lifecycle status */ - status: Status; - /** Free-form tags for grouping */ - tags: string[]; -} - -/** - * Lifecycle status of a document. - * - * Used to drive UI badges and filters. - */ -export declare const Status: Readonly<{ - /** Document is being edited */ - Draft: 0; - /** Document is publicly visible */ - Published: 1; - /** Document is hidden from listings */ - Archived: 2; -}>; -export type Status = (typeof Status)[keyof typeof Status]; - -/** - * Consumer-implemented observer of document-saved notifications. - * - * Implementations must be safe to call from any thread. - * - * Implemented by the consumer; the producer calls these methods synchronously. An exception thrown from a method is reported to the producer as an error (code -4). - */ -export interface SavedListener { - /** - * Fires after a document has been persisted successfully - * @param docId Identifier of the saved document - * @param revision Monotonically increasing revision - */ - onSaved(docId: bigint, revision: number): void; -} - -/** - * An ordered collection of documents. - * - * Notebooks own their documents and release them when destroyed. - * - * Wraps one reference to a producer object; release it with `close()`. - */ -export declare class Notebook { - /** - * Create an empty notebook - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @throws {WeaveFFIError} if the native call fails - */ - constructor(); - /** - * Add a document, returning its position in the notebook - * @param document The document to add - * @throws {WeaveFFIError} if the native call fails - */ - insert(document: Document): number; - /** - * Look up a document by identifier, failing when none matches - * @param id Identifier of the document to find - * @throws {DocsErrorsError} on a domain error code - */ - find(id: bigint): Document; - /** - * The number of documents currently held - * @throws {WeaveFFIError} if the native call fails - */ - size(): number; - /** - * The largest number of documents one notebook will hold - * @throws {WeaveFFIError} if the native call fails - */ - static maxDocuments(): number; - /** Releases this wrapper's reference exactly once; later calls are - * no-ops. Also reachable as `[Symbol.dispose]()` for `using` - * declarations. A wrapper collected unclosed is released by a - * finalizer where the runtime provides one. */ - close(): void; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - docs: { - /** - * Create a brand new document. - * - * The document starts in `Draft` status; callers must publish - * it explicitly to make it visible. - * @param title Human-readable title for the new document - * @param tags Initial set of tags (may be empty) - * @throws {WeaveFFIError} if the native call fails - */ - createDocument(title: string, tags: string[]): Document; - /** - * Look up a document by identifier - * @param id Identifier returned from create_document - * @throws {WeaveFFIError} if the native call fails - */ - getDocument(id: bigint): Document | null; - /** - * Publish a document in the background - * @param id Identifier of the document to publish - * @throws {WeaveFFIError} if the native call fails - */ - publishAsync(id: bigint): Promise; - /** - * Register a listener for saved notifications - * @param listener Receiver of saved notifications - * @throws {WeaveFFIError} if the native call fails - */ - watch(listener: SavedListener): void; - Notebook: typeof Notebook; - }; -} - -export function loadWeaveffiWasm(source: string | URL | BufferSource | WebAssembly.Module): Promise; - -// END weaveffi_wasm.d.ts 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 deleted file mode 100644 index 399bbb5f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap +++ /dev/null @@ -1,719 +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); one -// strong reference per wrapper, released by close() -// 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 -// Callbacks -> i32 context key + i32 pointer to a static vtable - -// The ABI revision this glue was generated against. -const _ABI_VERSION = 2; - -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, producer - * panics, and callback-interface implementations that raised (code -4). - * 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 `docs` module's error domain. */ -export class DocsErrorsError extends WeaveFFIError {} - -// Returned when no document matches the requested id -export class NotFound extends DocsErrorsError { - constructor(message = "Document not found") { - super(1, message); - } -} -NotFound.CODE = 1; -DocsErrorsError.NotFound = NotFound; - -// Raised when attempting to move into a forbidden state -export class InvalidStatus extends DocsErrorsError { - constructor(message = "Invalid status transition") { - super(2, message); - } -} -InvalidStatus.CODE = 2; -DocsErrorsError.InvalidStatus = InvalidStatus; - -const _DOCS_ERRORS_ERROR_CODES = Object.freeze({ - 1: NotFound, - 2: InvalidStatus, -}); - -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); } - // An object token: the pointer (a wasm32 i32, read unsigned) widened to u64. - obj(ptr) { this.u64(BigInt(ptr >>> 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; - } - // An object token narrowed back to a wasm32 pointer; zero is never a - // valid token (an absent `Interface?` uses the option flag instead). - obj() { - const t = this.u64(); - if (t === 0n || t > 0xffffffffn) this._bad('object token out of range'); - return Number(t); - } - 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 (-2), a marshalling failure (-3), or a callback -// interface implementation that raised (-4), surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - _pendingForeign = null; - 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) { - _pendingForeign = null; - wasm.weaveffi_dealloc(errPtr, 16); -} - -// The message of a callback-interface failure a trampoline reported through -// out_err, held until the producer call on the stack returns (every error -// checker and _freeErr clear it) so later callbacks in that call are refused. -let _pendingForeign = null; - -// Translate an exception raised while a producer call was on the stack. -// wasm32-unknown-unknown has no unwinding runtime: a producer panic executes -// `unreachable`, so the engine throws a WebAssembly.RuntimeError instead of -// the producer writing out_err. A trap that follows a callback-interface -// failure is labelled with that failure (-4); any other trap is a producer -// panic (-2). Non-trap exceptions pass through unchanged. The producer -// frames between the trap and the call are not unwound, so a lock held -// across a callback stays locked; a producer that snapshots its state -// before calling out stays usable. -function _trapError(e) { - const foreign = _pendingForeign; - _pendingForeign = null; - if (!(e instanceof WebAssembly.RuntimeError)) return e; - if (foreign !== null) return new WeaveFFIError(-4, foreign); - return new WeaveFFIError(-2, 'producer panicked: ' + e.message); -} - -// Release the error slot a failed call never filled, then translate. -function _trap(wasm, errPtr, e) { - _freeErr(wasm, errPtr); - return _trapError(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) { - _pendingForeign = null; - 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; -} - -// `using` declarations (explicit resource management) call this symbol; -// runtimes that predate it get the registered stand-in so the method is -// still reachable by name. -const _dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); - -// Finalization backstop: a wrapper that is garbage collected without -// close() still releases its reference. The held value is the -// [destroy, handle] pair (never the wrapper, which would keep it alive); -// the wrapper is the unregister token so close() disarms the backstop and -// destroy runs exactly once either way. Absent on runtimes without -// FinalizationRegistry, where close() is the only release path. -const _finalizer = typeof FinalizationRegistry === 'function' - ? new FinalizationRegistry(([destroy, handle]) => destroy(handle)) - : null; - -// Bind one strong reference to a wrapper and arm the backstop. -function _adopt(obj, handle, destroy) { - obj._handle = handle; - obj._destroy = destroy; - if (_finalizer !== null) _finalizer.register(obj, [destroy, handle], obj); - return obj; -} - -// Release a wrapper's reference exactly once; later calls are no-ops. -function _release(obj) { - if (!obj._handle) return; - if (_finalizer !== null) _finalizer.unregister(obj); - obj._destroy(obj._handle); - obj._handle = 0; -} - -// The pointer of a live wrapper, lent to the producer for one call (the -// wrapper keeps its own reference). A closed or non-object argument is a -// consumer programming error, reported with the marshalling code. -function _borrow(obj) { - if (obj === null || obj === undefined || !obj._handle) { - throw new WeaveFFIError(-3, 'expected a live object wrapper'); - } - return obj._handle; -} - -// Report a callback-interface failure through the trampoline's out_err -// slot as FOREIGN_ERROR_CODE (-4). weaveffi_error_set copies the message -// with the producer's allocator, so the staged C string is released as -// soon as it returns. The message is parked in _pendingForeign until the -// producer call on the stack returns: the producer can't unwind on wasm32, -// so it keeps running on the trampoline's default return value, and any -// further callback it makes during that call is refused with this error -// instead of reaching the implementation again. -function _reportForeign(wasm, errPtr, msg) { - if (_pendingForeign === null) _pendingForeign = msg; - const [p, s] = _cstr(wasm, msg); - wasm.weaveffi_error_set(errPtr, -4, p); - wasm.weaveffi_dealloc(p, s); -} - -// Report an exception thrown by a callback-interface implementation. -function _setForeignError(wasm, errPtr, e) { - _reportForeign(wasm, errPtr, e instanceof Error ? (e.message || e.name) : String(e)); -} - -function _registerTrampoline(table, paramTypes, resultTypes, handler) { - const idx = table.grow(1); - table.set(idx, new WebAssembly.Function( - { parameters: paramTypes, results: resultTypes }, - handler - )); - return idx; -} - -export const Status = Object.freeze({ - Draft: 0, - Published: 1, - Archived: 2, -}); - -/** - * Load a WeaveFFI Wasm module. - * - * @param {string|URL|BufferSource|WebAssembly.Module} source - A URL to - * fetch the `.wasm` from, its bytes, or an already compiled module - * (use the bytes form where `fetch` cannot read files, e.g. Node). - * @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 }); - * - * // Object: a class wrapping one producer reference; close() releases it - * // (or let `using` do it). Passing it lends the reference for the call. - * const store = new api.kv.Store(); - * api.kv.inspect(store); - * store.close(); - * - * // 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(source) { - let module; - if (source instanceof WebAssembly.Module) { - module = source; - } else if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { - module = await WebAssembly.compile(source); - } else { - const response = await fetch(source); - module = await WebAssembly.compile(await response.arrayBuffer()); - } - const instance = await WebAssembly.instantiate(module, {}); - const wasm = instance.exports; - - _checkAbiVersion(wasm); - - // Build the DocsErrorsError subclass matching `code`, or a generic - // WeaveFFIError for codes outside the domain (panics, marshalling, - // foreign callback failures). - function _docsErrorsErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _DOCS_ERRORS_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; - } - - // Throw the `DocsErrorsError` domain error (and free the slot) if the error slot - // carries a non-zero code. - function _checkDocsErrorsError(wasm, errPtr) { - _pendingForeign = null; - 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 = _docsErrorsErrorFrom(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 `docs.Document` record into the value-buffer wire format. - function _write_docs_Document(w, v) { - w.i64(v.id); - w.str(v.title); - w.i32(v.status); - const _a1 = v.tags || []; - w.len(_a1.length); - for (const _e1 of _a1) { - w.str(_e1); - } - } - - // Decode a `docs.Document` record from the value-buffer wire format. - function _read_docs_Document(r) { - const v = {}; - v.id = r.i64(); - v.title = r.str(); - v.status = 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; - } - - 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 = _registerTrampoline(_table, ['i32', 'i32', 'i32'], [], _asyncHandler); - - // Callback-interface implementations, keyed by the integer context - // the loader passes as `ctx`; an entry lives until the producer - // calls the vtable's `free(ctx)`. - let _nextCbId = 1; - const _callbacks = new Map(); - - // Trampolines for the `SavedListener` callback interface: one WebAssembly.Function - // per vtable entry, mirroring the C signature of each method. Borrowed - // arguments are decoded before the JS method runs; object arguments - // are adopted; a thrown exception becomes a -4 error on out_err. - const _cb_SavedListener_on_saved = _registerTrampoline(_table, ['i32', 'i64', 'i32', 'i32'], [], (_ctx, a0, a1, _err) => { - try { - const _impl = _callbacks.get(_ctx); - const _p0 = a0; - const _p1 = a1; - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return; - } - _impl.onSaved(_p0, _p1); - } catch (e) { - _setForeignError(wasm, _err, e); - } - }); - // `free(ctx)`: the producer dropped its last reference; forget the - // implementation so it can be collected. - const _cb_SavedListener_free = _registerTrampoline(_table, ['i32'], [], (_ctx) => { _callbacks.delete(_ctx); }); - // The one static `weaveffi_docs_SavedListener_vtable` for this instance, filled with the table indices - // above in declaration order, then `free`. - const _vtable_SavedListener = wasm.weaveffi_alloc(8); - { - const _dv = new DataView(wasm.memory.buffer); - _dv.setUint32(_vtable_SavedListener + 0, _cb_SavedListener_on_saved, true); - _dv.setUint32(_vtable_SavedListener + 4, _cb_SavedListener_free, true); - } - - // An ordered collection of documents. - // - // Notebooks own their documents and release them when destroyed. - class Notebook { - constructor() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_docs_Notebook_new(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - _adopt(this, _r, wasm.weaveffi_docs_Notebook_destroy); - } - static _wrap(handle) { - return _adopt(Object.create(Notebook.prototype), handle, wasm.weaveffi_docs_Notebook_destroy); - } - _clone() { - return wasm.weaveffi_docs_Notebook_clone(_borrow(this)); - } - close() { - _release(this); - } - [_dispose]() { - _release(this); - } - insert(document) { - const a0_w = new _BufWriter(); - _write_docs_Document(a0_w, document); - const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_docs_Notebook_insert(_borrow(this), a0_p, a0_l, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_l); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - } - find(id) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_docs_Notebook_find(_borrow(this), BigInt(id), _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _checkDocsErrorsError(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_docs_Document(_rd); - _rd.end(); - return _out; - } - size() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_docs_Notebook_size(_borrow(this), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - } - static maxDocuments() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_docs_Notebook_max_documents(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - } - } - - return { - _raw: wasm, - docs: { - createDocument(title, tags) { - const [a0_p, a0_s] = _cstr(wasm, title); - const a1_w = new _BufWriter(); - const _a1 = tags || []; - a1_w.len(_a1.length); - for (const _e1 of _a1) { - a1_w.str(_e1); - } - const [a1_p, a1_l] = _bytes(wasm, a1_w.finish()); - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_docs_create_document(a0_p, a1_p, a1_l, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_s); - wasm.weaveffi_dealloc(a1_p, a1_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_docs_Document(_rd); - _rd.end(); - return _out; - }, - getDocument(id) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_docs_get_document(BigInt(id), _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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() ? _read_docs_Document(_rd) : null); - _rd.end(); - return _out; - }, - publishAsync(id) { - return new Promise((resolve, reject) => { - const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, r) => r !== 0 }); - try { - wasm.weaveffi_docs_publish_async_async(BigInt(id), _cbPtr_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - }, - watch(listener) { - const a0_ctx = _nextCbId++; - _callbacks.set(a0_ctx, listener); - const _err = _allocErr(wasm); - try { - wasm.weaveffi_docs_watch(a0_ctx, _vtable_SavedListener, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - }, - Notebook: Notebook, - }, - }; -} - -// END weaveffi_wasm.js 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 index 40450c4a..7b081a38 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__README_md.snap @@ -1,625 +1,20 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Wasm (experimental) +# edge_cases -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. +Native bindings for edge_cases -Build (example): +WebAssembly bindings for a `wasm32-unknown-unknown` build of the `edge_cases` library, linked with `--export-table --growable-table` (for example `RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table"`). `init()` loads `edge_cases.wasm` from next to this package, or from the path in `EDGE_CASES_LIBRARY` on Node.js; pass a URL, bytes, or a compiled module to load it from elsewhere: -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it: the loader accepts a URL to fetch, the module's bytes (for Node, where `fetch` cannot read files), or an already compiled `WebAssembly.Module`. - -## 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. - -### Objects - -Interfaces are **wrapper classes** holding one reference to a producer object (an `i32` pointer into linear memory). A wrapper you receive is yours: release it with `close()` (or bind it with `using`, which calls `Symbol.dispose`); a wrapper collected unclosed is released by a `FinalizationRegistry` backstop where the runtime provides one, and destroy runs exactly once either way. Passing a wrapper to a function lends its reference for the duration of the call. Inside a value buffer an object crosses as a `u64` token carrying one reference: the writer clones the wrapper's reference and the reader adopts the token into a new wrapper. - -### 64-bit Integers - -`i64` and `u64` are JavaScript **`bigint`** values everywhere: as arguments, returns, record fields, and callback parameters. Map keys of these types become string property names on the decoded object. A `number` is accepted where a `bigint` is expected (it's widened with `BigInt(v)`, so a fractional value throws a `RangeError`). Values crossing by value follow the wasm calling convention: an out-of-range `bigint` wraps modulo 2^64 rather than throwing, and unsigned results are read back as unsigned (`u64` via `BigInt.asUintN`, `u32` via `>>> 0`). - -### 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) and maps to `Wrapper | null`. - -### 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. - -### Async Functions - -`async` functions return a **`Promise`**. `wasm32-unknown-unknown` has no threads, so the producer's default spawner drives each future to completion inline, inside the launch call: the completion callback fires (through a function-table trampoline) before the wrapper returns, and the Promise is already settled when you `await` it. There is nothing to pump or poll from JS. A future that returns `Pending` can't be woken on this target with the default spawner; a producer whose futures wait on external events needs its own spawner (`weaveffi::set_spawner`), and driving that executor is then the producer's responsibility, not the glue's. A typed error rejects the Promise with the same class the synchronous wrapper would throw. - -### Callback Interfaces - -A callback interface is a TypeScript `interface`; pass any object with the matching methods where a parameter of that type is expected. The loader keeps one **static vtable per interface** in linear memory, filled with function-table indices of long-lived trampolines, and passes a small integer context key with it; the producer calls `free(ctx)` when it drops the callback, which releases your object. Delivery is **synchronous and same-thread**: `wasm32-unknown-unknown` is single-threaded, so a method runs only while a call into the module is on the stack. A producer that calls back from a spawned thread cannot run on this target at all. - -Method arguments that are strings, bytes, or buffered values are **borrowed for the duration of the call**: they're copied or decoded into JS values before your method runs. Object arguments transfer one reference to you: `close()` the wrapper when you're done with it. An exception thrown from a method is reported to the producer as error code `-4` (with the exception's message) and the method's default value is returned in its place; the producer then aborts the call it was making, and the original caller sees a `WeaveFFIError` with code `-4` carrying your message (see *Traps* below for how that abort reaches the caller on this target). - -### 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_error_set(err_ptr: i32, code: i32, msg: i32)`: fill an error slot (used to report callback exceptions) -- `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, marshalling failures, and callback exceptions -(code `-4`). 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. - -#### Callback failures without unwinding - -`wasm32-unknown-unknown` builds with `panic = "abort"`, so when a callback interface method throws, the producer can't unwind out of the call the way native targets do. Instead the runtime records the failure, the producer's code runs to completion on the method's default return value (`0`, `0n`, or `false`), and the thunk then reports code `-4` with the exception's message in place of the result, so the wrapper throws (or the Promise rejects with) the same `WeaveFFIError` a native build would produce. While that producer call is still on the stack, the glue refuses every further callback invocation with the same error rather than calling your implementation again, so your code observes the failure exactly as it would under unwinding. Producers must tolerate a callback returning its default once (a well-behaved producer already does, because a consumer could return that value on purpose). - -#### Traps - -A producer panic can't be caught on the Rust side either: it executes `unreachable`, and the engine throws a `WebAssembly.RuntimeError` out of the call. The glue translates that trap for you: the wrapper throws (or the Promise rejects with) a `WeaveFFIError` with code `-2` (`producer panicked`), or code `-4` if a callback method had already failed during the call. The error slot and every staged argument are released either way. The producer frames between the trap and the call are *not* unwound, so their cleanup does not run: a lock held across the panic stays locked, and allocations those frames owned leak. - -## 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_register_batch` - -Two callback interface parameters on one function - -`weaveffi_edge_cases_register_batch(listener: i32, i32, keyword: i32, i32) -> void` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `listener` | `BatchListener` | `i32, i32` | context key + static vtable pointer | -| `keyword` | `Keyword` | `i32, i32` | context key + static vtable pointer | - -##### `weaveffi_edge_cases_widgets_everywhere` - -Objects in every buffered position - -`weaveffi_edge_cases_widgets_everywhere(list: i32, i32, by_name: i32, i32, sparse: i32, i32, holder: i32, i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `list` | `[Widget]` | `i32, i32` | value buffer: ptr + len in linear memory | -| `by_name` | `{string:Widget}` | `i32, i32` | value buffer: ptr + len in linear memory | -| `sparse` | `[Widget?]` | `i32, i32` | value buffer: ptr + len in linear memory | -| `holder` | `WidgetHolder` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `WidgetHolder` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_edge_cases_widget_stream` - -Iterator of objects - -`weaveffi_edge_cases_widget_stream() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `iter` | `i32` | opaque iterator handle | - -##### `weaveffi_edge_cases_async_widget` - -Async returning an object - -`weaveffi_edge_cases_async_widget() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `Widget` | `i32` | borrowed object pointer | - -##### `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` | borrowed 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` | borrowed object pointer | -| `fallback` | `Widget?` | `i32` | nullable object pointer, 0 = absent | -| _returns_ | `string` | `i32` | NUL-terminated C string 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` +```js +import { init, edge_cases } from 'edge_cases'; -| 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 **object pointer** (`i32`), wrapped by a JS class holding one reference. Constructors return an owned reference; methods lend the pointer as the implicit leading `self` argument; `close()` (or `Symbol.dispose`) releases the reference via the destroy symbol exactly once. - -##### `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_clone` - -Returns a second reference to the same object. Used when the wrapper is encoded as an object token inside a value buffer. - -##### `weaveffi_edge_cases_Widget_destroy` - -Releases one object reference. Called by the wrapper's `close()` (and by the finalizer backstop). - -##### `Vault` - -Deprecated interface with a throwing constructor - -Passed as an **object pointer** (`i32`), wrapped by a JS class holding one reference. Constructors return an owned reference; methods lend the pointer as the implicit leading `self` argument; `close()` (or `Symbol.dispose`) releases the reference via the destroy symbol exactly once. - -##### `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` | borrowed 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_clone` - -Returns a second reference to the same object. Used when the wrapper is encoded as an object token inside a value buffer. - -##### `weaveffi_edge_cases_Vault_destroy` - -Releases one object reference. Called by the wrapper's `close()` (and by the finalizer backstop). - -#### Callback Interfaces - -##### `BatchListener` - -Callback interface carrying composite payloads and objects - -Implemented in JS as an object with the methods below. Passed as an `i32` context key plus an `i32` pointer to the static `weaveffi_edge_cases_BatchListener_vtable` vtable; the vtable's trailing `free(ctx)` entry releases the JS object. - -| Method | Parameters | Returns | -|--------|------------|---------| -| `onBatch` | `items: Outer[]`, `raw: Uint8Array`, `first: Outer | null`, `mode: Mode` | `void` | -| `onWidget` | `widget: Widget`, `fallback: Widget | null`, `children: Widget[]` | `void` | -| `onTick` | (none) | `void` | -| `shouldContinue` | `class: number` | `boolean` | -| `pickMode` | (none) | `Mode` | - -##### `Keyword` - -Callback interface whose name and methods are keywords - -Implemented in JS as an object with the methods below. Passed as an `i32` context key plus an `i32` pointer to the static `weaveffi_edge_cases_Keyword_vtable` vtable; the vtable's trailing `free(ctx)` entry releases the JS object. - -| Method | Parameters | Returns | -|--------|------------|---------| -| `delete` | `self: number` | `void` | -| `import` | (none) | `number` | - -#### 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` | - -##### `WidgetHolder` - -Record carrying objects in every buffered position - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `primary` | `Widget` | -| `fallback` | `Widget?` | -| `extras` | `[Widget]` | -| `by_name` | `{string:Widget}` | - -#### 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` | +await init(); +``` diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_d_ts.snap new file mode 100644 index 00000000..0a65aa11 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_d_ts.snap @@ -0,0 +1,277 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class EdgeCasesError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends EdgeCasesError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof EdgeCasesError; +/** + * Load the WebAssembly module and check that it matches these bindings. + * Every other export throws until the returned promise resolves. + */ +export declare function init( + source?: string | URL | BufferSource | WebAssembly.Module | Response, +): Promise; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** No parameters and no result */ +export declare namespace edge_cases { + /** The errors of the `edge_cases` error domain. */ + export class EdgeErrorsError extends $Error { + constructor(code: number, message?: string); + } + /** Resource is busy */ + export class BusyError extends edge_cases.EdgeErrorsError { + static readonly CODE: 7; + constructor(message?: string); + } + /** + * Deprecated enum with non-contiguous values and keyword-like variants + * @deprecated Use Mode instead + */ + export enum Legacy { + None = 0, + Default = 5, + Class = 10, + } + export enum Mode { + Fast = 1, + Safe = 2, + } + export interface Leaf { + weight: number; + } + /** Middle layer of a three-deep struct nesting */ + export interface Inner { + leaf: edge_cases.Leaf; + leaves: edge_cases.Leaf[]; + } + export interface Outer { + inner: edge_cases.Inner; + maybe_inner: edge_cases.Inner | null; + by_name: Record; + modes: edge_cases.Mode[]; + matrix: number[][]; + sparse: (bigint | null)[]; + blob: Uint8Array | null; + note: string | null; + by_id: Record; + _hidden: boolean; + v2: number; + } + /** Every field is a reserved word somewhere */ + export interface Keywords { + class: number; + def: number; + import: number; + from: number; + func: number; + range: number; + register: number; + volatile: number; + new: number; + default: number; + end: number; + in: number; + is: number; + int: number; + var: number; + val: number; + self: number; + this: number; + object: number; + string: number; + case: number; + select: number; + switch: number; + void: number; + union: number; + namespace: number; + operator: number; + template: number; + package: number; + protocol: number; + guard: number; + defer: number; + inout: number; + internal: number; + when: number; + fun: number; + data: number; + late: number; + part: number; + out: number; + ref: number; + event: number; + base: number; + lambda: number; + yield: number; + nil: number; + null: number; + begin: number; + rescue: number; + module: number; + chan: number; + go: number; + let: number; + export: number; + delete: number; + typedef: number; + sizeof: number; + static: number; + super: number; + } + /** + * Deprecated struct + * @deprecated Use Outer instead + */ + export interface Retired { + id: bigint; + } + /** Record carrying objects in every buffered position */ + export interface WidgetHolder { + primary: edge_cases.Widget; + fallback: edge_cases.Widget | null; + extras: edge_cases.Widget[]; + by_name: Record; + } + /** Callback interface carrying composite payloads and objects */ + export interface BatchListener { + /** Composite payloads cross as value buffers */ + onBatch(items: edge_cases.Outer[], raw: Uint8Array, first: edge_cases.Outer | null, mode: edge_cases.Mode): void; + /** Objects cross as strong references the consumer must release */ + onWidget(widget: edge_cases.Widget, fallback: edge_cases.Widget | null, children: edge_cases.Widget[]): void; + onTick(): void; + /** Scalar return travels back through the vtable */ + shouldContinue(class_: number): boolean; + /** Enum return travels back as its integer discriminant */ + pickMode(): edge_cases.Mode; + } + /** Callback interface whose name and methods are keywords */ + export interface Keyword { + delete(self: number): void; + import(): number; + } + /** Interface with no constructor; instances come from make_widget */ + export class Widget { + private constructor(); + /** + * Void method that can fail + * @throws {edge_cases.EdgeErrorsError} + */ + refresh(): void; + rename(class_: string, import_: string | null): string; + /** + * Async method with a struct result + * @throws {edge_cases.EdgeErrorsError} + */ + fetch(id: bigint): Promise; + /** @deprecated Use rename instead */ + oldName(): string; + /** + * Release this wrapper's reference to the native object. Safe to call + * more than once; a wrapper that is never closed is released when it is + * garbage collected. Using the wrapper after `close()` throws. + */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; + } + /** + * Deprecated interface with a throwing constructor + * @deprecated Use Widget instead + */ + export class Vault { + private constructor(); + /** + * Fails when the vault is busy + * @throws {edge_cases.EdgeErrorsError} + */ + static open(path: string): edge_cases.Vault; + peek(): edge_cases.Widget | null; + /** + * Throwing static + * @throws {edge_cases.EdgeErrorsError} + */ + static probe(): boolean; + /** + * Release this wrapper's reference to the native object. Safe to call + * more than once; a wrapper that is never closed is released when it is + * garbage collected. Using the wrapper after `close()` throws. + */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; + } + /** No parameters and no result */ + export function noop(): void; + /** Function named after a keyword whose parameters are all keywords */ + export function select(class_: number, from: string, func: string[], range: number | null, type: boolean): number; + /** Optional list of optional structs, the deepest legal nesting */ + export function nestedOptional(items: (edge_cases.Outer | null)[] | null): (edge_cases.Outer | null)[] | null; + export function optionalParams(text: string | null, count: number | null, outer: edge_cases.Outer | null, tags: string[] | null, blob: Uint8Array | null, mode: edge_cases.Mode | null): edge_cases.Mode | null; + /** Two callback interface parameters on one function */ + export function registerBatch(listener: edge_cases.BatchListener, keyword: edge_cases.Keyword): void; + /** Objects in every buffered position */ + export function widgetsEverywhere(list: edge_cases.Widget[], byName: Record, sparse: (edge_cases.Widget | null)[], holder: edge_cases.WidgetHolder): edge_cases.WidgetHolder; + /** Iterator of objects */ + export function widgetStream(): IterableIterator; + /** + * Async returning an object + * @throws {edge_cases.EdgeErrorsError} + */ + export function asyncWidget(): Promise; + /** Factory for an interface with no constructor */ + export function makeWidget(label: string): edge_cases.Widget; + /** + * Optional interface return + * @throws {edge_cases.EdgeErrorsError} + */ + export function findWidget(id: bigint): edge_cases.Widget | null; + /** Interface parameter, including an optional one */ + export function inspectWidget(widget: edge_cases.Widget, fallback: edge_cases.Widget | null): string; + /** Iterator of strings */ + export function stringStream(): IterableIterator; + /** Iterator of scalars */ + export function scalarStream(): IterableIterator; + /** Iterator of enums */ + export function modeStream(): IterableIterator; + /** Async with no result */ + export function asyncVoid(): Promise; + export function asyncBytes(size: number): Promise; + /** @throws {edge_cases.EdgeErrorsError} */ + export function asyncStruct(): Promise; + /** @throws {edge_cases.EdgeErrorsError} */ + export function asyncList(options?: { signal?: AbortSignal }): Promise; + export function asyncOptional(): Promise; + export function asyncEnum(): Promise; + export function asyncKeywords(class_: string, import_: number): Promise; + export function keywordsRoundtrip(value: edge_cases.Keywords): edge_cases.Keywords; + /** @deprecated Use Mode-based APIs */ + export function legacyValue(value: edge_cases.Legacy): edge_cases.Retired; +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_js.snap new file mode 100644 index 00000000..641e87b9 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__index_js.snap @@ -0,0 +1,985 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + EdgeCasesError as $Error, +} from './runtime.js'; +export { EdgeCasesError, CancelledError } from './runtime.js'; +import { $loadWasm, $unloaded } from './linear.js'; + +let $raw = $unloaded('edge_cases'); +// Handles are linear-memory addresses; object tokens are u64s. +const $token = Number; +let $loading = null; + +/** + * Load the WebAssembly module (by default `edge_cases.wasm` next to this file, or + * the file named by `EDGE_CASES_LIBRARY` on Node.js) and check that it matches + * these bindings. Every other export throws until the returned promise + * resolves. + */ +export function init(source) { + $loading ??= $loadWasm(source, 'edge_cases', 'EDGE_CASES_LIBRARY', new URL('./edge_cases.wasm', import.meta.url)) + .then((m) => { + $raw = $bind(m); + try { + $verify($raw, 'edge_cases', 'edge_cases', 3, [['edge_cases', 0x1567f441073bd4aan]]); + } catch (e) { + $raw = $unloaded('edge_cases'); + throw e; + } + }) + .catch((e) => { + $loading = null; + throw e; + }); + return $loading; +} + +function $vt_edge_cases_edge_cases_BatchListener(m) { + return [ + [['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], [], 'viiiiiiiii', (ctx, p0, p1, p2, p3, p4, p5, p6, err) => { + try { + m.adapter(ctx).on_batch(m.readData(p0, p1), m.readData(p2, p3), m.readData(p4, p5), p6); + } catch (e) { + m.foreign(err, e); + } + }], + [['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], [], 'viiiiii', (ctx, p0, p1, p2, p3, err) => { + try { + m.adapter(ctx).on_widget(p0, p1 === 0 ? null : p1, m.readData(p2, p3)); + } catch (e) { + m.foreign(err, e); + } + }], + [['i32', 'i32'], [], 'vii', (ctx, err) => { + try { + m.adapter(ctx).on_tick(); + } catch (e) { + m.foreign(err, e); + } + }], + [['i32', 'i32', 'i32'], ['i32'], 'iiii', (ctx, p0, err) => { + try { + return m.adapter(ctx).should_continue(p0) ? 1 : 0; + } catch (e) { + m.foreign(err, e); + return 0; + } + }], + [['i32', 'i32'], ['i32'], 'iii', (ctx, err) => { + try { + return m.adapter(ctx).pick_mode(); + } catch (e) { + m.foreign(err, e); + return 0; + } + }], + ]; +} + +function $vt_edge_cases_edge_cases_Keyword(m) { + return [ + [['i32', 'i32', 'i32'], [], 'viii', (ctx, p0, err) => { + try { + m.adapter(ctx).delete(p0); + } catch (e) { + m.foreign(err, e); + } + }], + [['i32', 'i32'], ['i32'], 'iii', (ctx, err) => { + try { + return m.adapter(ctx).import(); + } catch (e) { + m.foreign(err, e); + return 0; + } + }], + ]; +} + +// The raw entry points, one per C symbol. +function $bind(m) { + const x = m.x; + return { + edge_cases_abi_version: () => x.edge_cases_abi_version() >>> 0, + edge_cases_debug_live: (kind) => BigInt.asUintN(64, x.edge_cases_debug_live(m.num(kind))), + edge_cases_cancel_token_create: () => x.edge_cases_cancel_token_create(), + edge_cases_cancel_token_cancel: (t) => x.edge_cases_cancel_token_cancel(m.handle(t)), + edge_cases_cancel_token_destroy: (t) => x.edge_cases_cancel_token_destroy(m.handle(t)), + edge_cases_edge_cases_checksum: () => BigInt.asUintN(64, x.edge_cases_edge_cases_checksum()), + edge_cases_edge_cases_Widget_clone: (h) => x.edge_cases_edge_cases_Widget_clone(m.handle(h)), + edge_cases_edge_cases_Widget_destroy: (h) => x.edge_cases_edge_cases_Widget_destroy(m.handle(h)), + edge_cases_edge_cases_Vault_clone: (h) => x.edge_cases_edge_cases_Vault_clone(m.handle(h)), + edge_cases_edge_cases_Vault_destroy: (h) => x.edge_cases_edge_cases_Vault_destroy(m.handle(h)), + edge_cases_edge_cases_noop: () => { + x.edge_cases_edge_cases_noop(m.err); + m.check(); + }, + edge_cases_edge_cases_select: (a0, a1, a2, a3, a4) => { + let s1 = null, s2 = null, s3 = null; + try { + s1 = m.str(a1); + s2 = m.data(a2); + s3 = m.data(a3); + const r = x.edge_cases_edge_cases_select(m.num(a0), s1[0], s1[1], s2[0], s2[1], s3[0], s3[1], m.bool(a4), m.err); + m.check(); + return r; + } finally { + m.unstage(s1); + m.unstage(s2); + m.unstage(s3); + } + }, + edge_cases_edge_cases_nested_optional: (a0) => { + let s0 = null; + try { + s0 = m.data(a0); + const r = x.edge_cases_edge_cases_nested_optional(s0[0], s0[1], m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + } finally { + m.unstage(s0); + } + }, + edge_cases_edge_cases_optional_params: (a0, a1, a2, a3, a4, a5) => { + let s0 = null, s1 = null, s2 = null, s3 = null, s4 = null, s5 = null; + try { + s0 = m.data(a0); + s1 = m.data(a1); + s2 = m.data(a2); + s3 = m.data(a3); + s4 = m.data(a4); + s5 = m.data(a5); + const r = x.edge_cases_edge_cases_optional_params(s0[0], s0[1], s1[0], s1[1], s2[0], s2[1], s3[0], s3[1], s4[0], s4[1], s5[0], s5[1], m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + } finally { + m.unstage(s0); + m.unstage(s1); + m.unstage(s2); + m.unstage(s3); + m.unstage(s4); + m.unstage(s5); + } + }, + edge_cases_edge_cases_register_batch: (a0, a1) => { + x.edge_cases_edge_cases_register_batch(m.register(a0), m.vtable('edge_cases_edge_cases_BatchListener', () => $vt_edge_cases_edge_cases_BatchListener(m)), m.register(a1), m.vtable('edge_cases_edge_cases_Keyword', () => $vt_edge_cases_edge_cases_Keyword(m)), m.err); + m.check(); + }, + edge_cases_edge_cases_widgets_everywhere: (a0, a1, a2, a3) => { + let s0 = null, s1 = null, s2 = null, s3 = null; + try { + s0 = m.data(a0); + s1 = m.data(a1); + s2 = m.data(a2); + s3 = m.data(a3); + const r = x.edge_cases_edge_cases_widgets_everywhere(s0[0], s0[1], s1[0], s1[1], s2[0], s2[1], s3[0], s3[1], m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + } finally { + m.unstage(s0); + m.unstage(s1); + m.unstage(s2); + m.unstage(s3); + } + }, + edge_cases_edge_cases_widget_stream: () => { + const r = x.edge_cases_edge_cases_widget_stream(m.err); + m.check(); + return r; + }, + edge_cases_edge_cases_WidgetStreamIterator_next: (h) => { + const has = x.edge_cases_edge_cases_WidgetStreamIterator_next(m.handle(h), m.item, m.err); + m.check(); + if (has === 0) return undefined; + const p = m.view().getUint32(m.item, true); + return p; + }, + edge_cases_edge_cases_WidgetStreamIterator_destroy: (h) => x.edge_cases_edge_cases_WidgetStreamIterator_destroy(m.handle(h)), + edge_cases_edge_cases_async_widget: () => { + return m.launch(['i32', 'i32', 'i32'], [], 'viii', (cb, ctx) => x.edge_cases_edge_cases_async_widget(cb, ctx), (result) => result); + }, + edge_cases_edge_cases_make_widget: (a0) => { + let s0 = null; + try { + s0 = m.str(a0); + const r = x.edge_cases_edge_cases_make_widget(s0[0], s0[1], m.err); + m.check(); + return r; + } finally { + m.unstage(s0); + } + }, + edge_cases_edge_cases_find_widget: (a0) => { + const r = x.edge_cases_edge_cases_find_widget(m.i64(a0), m.err); + m.check(); + return r === 0 ? null : r; + }, + edge_cases_edge_cases_inspect_widget: (a0, a1) => { + const r = x.edge_cases_edge_cases_inspect_widget(m.handle(a0), m.handleOpt(a1), m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + }, + edge_cases_edge_cases_string_stream: () => { + const r = x.edge_cases_edge_cases_string_stream(m.err); + m.check(); + return r; + }, + edge_cases_edge_cases_StringStreamIterator_next: (h) => { + const has = x.edge_cases_edge_cases_StringStreamIterator_next(m.handle(h), m.item, m.len, m.err); + m.check(); + if (has === 0) return undefined; + const p = m.view().getUint32(m.item, true); + return m.takeStr(p, m.outLen()); + }, + edge_cases_edge_cases_StringStreamIterator_destroy: (h) => x.edge_cases_edge_cases_StringStreamIterator_destroy(m.handle(h)), + edge_cases_edge_cases_scalar_stream: () => { + const r = x.edge_cases_edge_cases_scalar_stream(m.err); + m.check(); + return r; + }, + edge_cases_edge_cases_ScalarStreamIterator_next: (h) => { + const has = x.edge_cases_edge_cases_ScalarStreamIterator_next(m.handle(h), m.item, m.err); + m.check(); + if (has === 0) return undefined; + return m.view().getInt32(m.item, true); + }, + edge_cases_edge_cases_ScalarStreamIterator_destroy: (h) => x.edge_cases_edge_cases_ScalarStreamIterator_destroy(m.handle(h)), + edge_cases_edge_cases_mode_stream: () => { + const r = x.edge_cases_edge_cases_mode_stream(m.err); + m.check(); + return r; + }, + edge_cases_edge_cases_ModeStreamIterator_next: (h) => { + const has = x.edge_cases_edge_cases_ModeStreamIterator_next(m.handle(h), m.item, m.err); + m.check(); + if (has === 0) return undefined; + return m.view().getInt32(m.item, true); + }, + edge_cases_edge_cases_ModeStreamIterator_destroy: (h) => x.edge_cases_edge_cases_ModeStreamIterator_destroy(m.handle(h)), + edge_cases_edge_cases_async_void: () => { + return m.launch(['i32', 'i32'], [], 'vii', (cb, ctx) => x.edge_cases_edge_cases_async_void(cb, ctx), () => undefined); + }, + edge_cases_edge_cases_async_bytes: (a0) => { + return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_bytes(m.num(a0), cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); + }, + edge_cases_edge_cases_async_struct: () => { + return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_struct(cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); + }, + edge_cases_edge_cases_async_list: (token) => { + return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_list(m.handleOpt(token), cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); + }, + edge_cases_edge_cases_async_optional: () => { + return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_optional(cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); + }, + edge_cases_edge_cases_async_enum: () => { + return m.launch(['i32', 'i32', 'i32'], [], 'viii', (cb, ctx) => x.edge_cases_edge_cases_async_enum(cb, ctx), (result) => result); + }, + edge_cases_edge_cases_async_keywords: (a0, a1) => { + let s0 = null; + try { + s0 = m.str(a0); + return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_async_keywords(s0[0], s0[1], m.num(a1), cb, ctx), (result_ptr, result_len) => m.takeStr(result_ptr, result_len)); + } finally { + m.unstage(s0); + } + }, + edge_cases_edge_cases_keywords_roundtrip: (a0) => { + let s0 = null; + try { + s0 = m.data(a0); + const r = x.edge_cases_edge_cases_keywords_roundtrip(s0[0], s0[1], m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + } finally { + m.unstage(s0); + } + }, + edge_cases_edge_cases_legacy_value: (a0) => { + const r = x.edge_cases_edge_cases_legacy_value(m.num(a0), m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + edge_cases_edge_cases_Widget_refresh: (self) => { + x.edge_cases_edge_cases_Widget_refresh(m.handle(self), m.err); + m.check(); + }, + edge_cases_edge_cases_Widget_rename: (self, a0, a1) => { + let s0 = null, s1 = null; + try { + s0 = m.str(a0); + s1 = m.data(a1); + const r = x.edge_cases_edge_cases_Widget_rename(m.handle(self), s0[0], s0[1], s1[0], s1[1], m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + } finally { + m.unstage(s0); + m.unstage(s1); + } + }, + edge_cases_edge_cases_Widget_fetch: (self, a0) => { + return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.edge_cases_edge_cases_Widget_fetch(m.handle(self), m.i64(a0), cb, ctx), (result_ptr, result_len) => m.takeData(result_ptr, result_len)); + }, + edge_cases_edge_cases_Widget_old_name: (self) => { + const r = x.edge_cases_edge_cases_Widget_old_name(m.handle(self), m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + }, + edge_cases_edge_cases_Vault_open: (a0) => { + let s0 = null; + try { + s0 = m.str(a0); + const r = x.edge_cases_edge_cases_Vault_open(s0[0], s0[1], m.err); + m.check(); + return r; + } finally { + m.unstage(s0); + } + }, + edge_cases_edge_cases_Vault_peek: (self) => { + const r = x.edge_cases_edge_cases_Vault_peek(m.handle(self), m.err); + m.check(); + return r === 0 ? null : r; + }, + edge_cases_edge_cases_Vault_probe: () => { + const r = x.edge_cases_edge_cases_Vault_probe(m.err); + m.check(); + return r !== 0; + }, + }; +} + +// The native cancel tokens behind `AbortSignal`s. +const $tokens = { + create: () => $raw.edge_cases_cancel_token_create(), + cancel: (t) => $raw.edge_cases_cancel_token_cancel(t), + destroy: (t) => $raw.edge_cases_cancel_token_destroy(t), +}; + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.edge_cases_debug_live(kind); +} + +const edge_cases$EdgeErrorsError = class EdgeErrorsError extends $Error {}; +const edge_cases$BusyError = class BusyError extends edge_cases$EdgeErrorsError { + static CODE = 7; + constructor(message = 'Resource is busy') { + super(7, message); + } +}; +const $codes$edge_cases$EdgeErrors = new Map([[7, edge_cases$BusyError]]); +function $from$edge_cases$EdgeErrors(e) { + return $domain(e, $codes$edge_cases$EdgeErrors); +} + +const edge_cases$Legacy = Object.freeze({ + None: 0, + Default: 5, + Class: 10, + 0: 'None', + 5: 'Default', + 10: 'Class', +}); + +const edge_cases$Mode = Object.freeze({ + Fast: 1, + Safe: 2, + 1: 'Fast', + 2: 'Safe', +}); + +function $w$edge_cases$Leaf(w, v) { + w.writeF32(v.weight); +} +function $r$edge_cases$Leaf(r) { + return { + weight: r.readF32(), + }; +} +function $w$edge_cases$Inner(w, v) { + $w$edge_cases$Leaf(w, v.leaf); + w.writeList(v.leaves, $w$edge_cases$Leaf); +} +function $r$edge_cases$Inner(r) { + return { + leaf: $r$edge_cases$Leaf(r), + leaves: r.readList($r$edge_cases$Leaf), + }; +} +function $w$edge_cases$Outer(w, v) { + $w$edge_cases$Inner(w, v.inner); + w.writeOpt(v.maybe_inner, $w$edge_cases$Inner); + w.writeMap(v.by_name, $W.String, $w$edge_cases$Inner); + w.writeList(v.modes, $W.I32); + w.writeList(v.matrix, (w, v) => w.writeList(v, $W.I32)); + w.writeList(v.sparse, (w, v) => w.writeOpt(v, $W.I64)); + w.writeOpt(v.blob, $W.Bytes); + w.writeOpt(v.note, $W.String); + w.writeMap(v.by_id, (w, k) => w.writeI64(BigInt(k)), $W.String); + w.writeBool(v._hidden); + w.writeU8(v.v2); +} +function $r$edge_cases$Outer(r) { + return { + inner: $r$edge_cases$Inner(r), + maybe_inner: r.readOpt($r$edge_cases$Inner), + by_name: r.readMap($R.String, $r$edge_cases$Inner), + modes: r.readList($R.I32), + matrix: r.readList((r) => r.readList($R.I32)), + sparse: r.readList((r) => r.readOpt($R.I64)), + blob: r.readOpt($R.Bytes), + note: r.readOpt($R.String), + by_id: r.readMap($R.I64, $R.String), + _hidden: r.readBool(), + v2: r.readU8(), + }; +} +function $w$edge_cases$Keywords(w, v) { + w.writeI32(v.class); + w.writeI32(v.def); + w.writeI32(v.import); + w.writeI32(v.from); + w.writeI32(v.func); + w.writeI32(v.range); + w.writeI32(v.register); + w.writeI32(v.volatile); + w.writeI32(v.new); + w.writeI32(v.default); + w.writeI32(v.end); + w.writeI32(v.in); + w.writeI32(v.is); + w.writeI32(v.int); + w.writeI32(v.var); + w.writeI32(v.val); + w.writeI32(v.self); + w.writeI32(v.this); + w.writeI32(v.object); + w.writeI32(v.string); + w.writeI32(v.case); + w.writeI32(v.select); + w.writeI32(v.switch); + w.writeI32(v.void); + w.writeI32(v.union); + w.writeI32(v.namespace); + w.writeI32(v.operator); + w.writeI32(v.template); + w.writeI32(v.package); + w.writeI32(v.protocol); + w.writeI32(v.guard); + w.writeI32(v.defer); + w.writeI32(v.inout); + w.writeI32(v.internal); + w.writeI32(v.when); + w.writeI32(v.fun); + w.writeI32(v.data); + w.writeI32(v.late); + w.writeI32(v.part); + w.writeI32(v.out); + w.writeI32(v.ref); + w.writeI32(v.event); + w.writeI32(v.base); + w.writeI32(v.lambda); + w.writeI32(v.yield); + w.writeI32(v.nil); + w.writeI32(v.null); + w.writeI32(v.begin); + w.writeI32(v.rescue); + w.writeI32(v.module); + w.writeI32(v.chan); + w.writeI32(v.go); + w.writeI32(v.let); + w.writeI32(v.export); + w.writeI32(v.delete); + w.writeI32(v.typedef); + w.writeI32(v.sizeof); + w.writeI32(v.static); + w.writeI32(v.super); +} +function $r$edge_cases$Keywords(r) { + return { + class: r.readI32(), + def: r.readI32(), + import: r.readI32(), + from: r.readI32(), + func: r.readI32(), + range: r.readI32(), + register: r.readI32(), + volatile: r.readI32(), + new: r.readI32(), + default: r.readI32(), + end: r.readI32(), + in: r.readI32(), + is: r.readI32(), + int: r.readI32(), + var: r.readI32(), + val: r.readI32(), + self: r.readI32(), + this: r.readI32(), + object: r.readI32(), + string: r.readI32(), + case: r.readI32(), + select: r.readI32(), + switch: r.readI32(), + void: r.readI32(), + union: r.readI32(), + namespace: r.readI32(), + operator: r.readI32(), + template: r.readI32(), + package: r.readI32(), + protocol: r.readI32(), + guard: r.readI32(), + defer: r.readI32(), + inout: r.readI32(), + internal: r.readI32(), + when: r.readI32(), + fun: r.readI32(), + data: r.readI32(), + late: r.readI32(), + part: r.readI32(), + out: r.readI32(), + ref: r.readI32(), + event: r.readI32(), + base: r.readI32(), + lambda: r.readI32(), + yield: r.readI32(), + nil: r.readI32(), + null: r.readI32(), + begin: r.readI32(), + rescue: r.readI32(), + module: r.readI32(), + chan: r.readI32(), + go: r.readI32(), + let: r.readI32(), + export: r.readI32(), + delete: r.readI32(), + typedef: r.readI32(), + sizeof: r.readI32(), + static: r.readI32(), + super: r.readI32(), + }; +} +function $w$edge_cases$Retired(w, v) { + w.writeI64(v.id); +} +function $r$edge_cases$Retired(r) { + return { + id: r.readI64(), + }; +} +function $w$edge_cases$WidgetHolder(w, v) { + w.writeU64(BigInt($clone(v.primary, edge_cases$Widget))); + w.writeOpt(v.fallback, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget)))); + w.writeList(v.extras, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget)))); + w.writeMap(v.by_name, $W.String, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget)))); +} +function $r$edge_cases$WidgetHolder(r) { + return { + primary: $adopt(edge_cases$Widget, $token(r.readU64())), + fallback: r.readOpt((r) => $adopt(edge_cases$Widget, $token(r.readU64()))), + extras: r.readList((r) => $adopt(edge_cases$Widget, $token(r.readU64()))), + by_name: r.readMap($R.String, (r) => $adopt(edge_cases$Widget, $token(r.readU64()))), + }; +} + +function $adapt$edge_cases$BatchListener(impl) { + $impl(impl, 'BatchListener'); + return { + on_batch($a0, $a1, $a2, $a3) { + impl.onBatch($decode($a0, (r) => r.readList($r$edge_cases$Outer)), $a1, $decode($a2, (r) => r.readOpt($r$edge_cases$Outer)), $a3); + }, + on_widget($a0, $a1, $a2) { + impl.onWidget($adopt(edge_cases$Widget, $a0), $adoptOpt(edge_cases$Widget, $a1), $decode($a2, (r) => r.readList((r) => $adopt(edge_cases$Widget, $token(r.readU64()))))); + }, + on_tick() { + impl.onTick(); + }, + should_continue($a0) { + return $ret.Bool(impl.shouldContinue($a0), 'BatchListener.shouldContinue'); + }, + pick_mode() { + return $ret.I32(impl.pickMode(), 'BatchListener.pickMode'); + }, + }; +} + +function $adapt$edge_cases$Keyword(impl) { + $impl(impl, 'Keyword'); + return { + delete($a0) { + impl.delete($a0); + }, + import() { + return $ret.I32(impl.import(), 'Keyword.import'); + }, + }; +} + +const edge_cases$Widget = class Widget extends $Object { + constructor() { + super(); + throw new TypeError('Widget has no public constructor; use its factory functions'); + } + static $destroy(h) { + $raw.edge_cases_edge_cases_Widget_destroy(h); + } + static $clone(h) { + return $raw.edge_cases_edge_cases_Widget_clone(h); + } + refresh() { + const $self = $lend(this, edge_cases$Widget); + try { + $raw.edge_cases_edge_cases_Widget_refresh($self); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } finally { + $unlend(this); + } + } + rename(class_, import_) { + const $self = $lend(this, edge_cases$Widget); + try { + return $raw.edge_cases_edge_cases_Widget_rename($self, class_, $encode(import_, (w, v) => w.writeOpt(v, $W.String))); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } + } + async fetch(id) { + const $self = $lend(this, edge_cases$Widget); + try { + return $decode(await $raw.edge_cases_edge_cases_Widget_fetch($self, id), $r$edge_cases$Outer); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } finally { + $unlend(this); + } + } + oldName() { + const $self = $lend(this, edge_cases$Widget); + try { + return $raw.edge_cases_edge_cases_Widget_old_name($self); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } + } +}; + +const edge_cases$Vault = class Vault extends $Object { + constructor() { + super(); + throw new TypeError('Vault has no public constructor; use its factory functions'); + } + static $destroy(h) { + $raw.edge_cases_edge_cases_Vault_destroy(h); + } + static $clone(h) { + return $raw.edge_cases_edge_cases_Vault_clone(h); + } + static open(path) { + try { + return $adopt(edge_cases$Vault, $raw.edge_cases_edge_cases_Vault_open(path)); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } + } + peek() { + const $self = $lend(this, edge_cases$Vault); + try { + return $adoptOpt(edge_cases$Widget, $raw.edge_cases_edge_cases_Vault_peek($self)); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } + } + static probe() { + try { + return $raw.edge_cases_edge_cases_Vault_probe(); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } + } +}; + +const edge_cases$noop = function noop() { + try { + $raw.edge_cases_edge_cases_noop(); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$select = function select(class_, from, func, range, type) { + try { + return $raw.edge_cases_edge_cases_select(class_, from, $encode(func, (w, v) => w.writeList(v, $W.String)), $encode(range, (w, v) => w.writeOpt(v, $W.I32)), type); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$nestedOptional = function nestedOptional(items) { + try { + return $decode($raw.edge_cases_edge_cases_nested_optional($encode(items, (w, v) => w.writeOpt(v, (w, v) => w.writeList(v, (w, v) => w.writeOpt(v, $w$edge_cases$Outer))))), (r) => r.readOpt((r) => r.readList((r) => r.readOpt($r$edge_cases$Outer)))); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$optionalParams = function optionalParams(text, count, outer, tags, blob, mode) { + try { + return $decode($raw.edge_cases_edge_cases_optional_params($encode(text, (w, v) => w.writeOpt(v, $W.String)), $encode(count, (w, v) => w.writeOpt(v, $W.I32)), $encode(outer, (w, v) => w.writeOpt(v, $w$edge_cases$Outer)), $encode(tags, (w, v) => w.writeOpt(v, (w, v) => w.writeList(v, $W.String))), $encode(blob, (w, v) => w.writeOpt(v, $W.Bytes)), $encode(mode, (w, v) => w.writeOpt(v, $W.I32))), (r) => r.readOpt($R.I32)); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$registerBatch = function registerBatch(listener, keyword) { + try { + $raw.edge_cases_edge_cases_register_batch($adapt$edge_cases$BatchListener(listener), $adapt$edge_cases$Keyword(keyword)); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$widgetsEverywhere = function widgetsEverywhere(list, byName, sparse, holder) { + try { + return $decode($raw.edge_cases_edge_cases_widgets_everywhere($encode(list, (w, v) => w.writeList(v, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget))))), $encode(byName, (w, v) => w.writeMap(v, $W.String, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget))))), $encode(sparse, (w, v) => w.writeList(v, (w, v) => w.writeOpt(v, (w, v) => w.writeU64(BigInt($clone(v, edge_cases$Widget)))))), $encode(holder, $w$edge_cases$WidgetHolder)), $r$edge_cases$WidgetHolder); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$edge_cases$widget_stream = { + next: (h) => $raw.edge_cases_edge_cases_WidgetStreamIterator_next(h), + destroy: (h) => $raw.edge_cases_edge_cases_WidgetStreamIterator_destroy(h), + map: $fault, + convert: (v) => $adopt(edge_cases$Widget, v), +}; +const edge_cases$widgetStream = function widgetStream() { + try { + return new $Iterator($raw.edge_cases_edge_cases_widget_stream(), $it$edge_cases$widget_stream); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$asyncWidget = async function asyncWidget() { + try { + return $adopt(edge_cases$Widget, await $raw.edge_cases_edge_cases_async_widget()); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } +}; + +const edge_cases$makeWidget = function makeWidget(label) { + try { + return $adopt(edge_cases$Widget, $raw.edge_cases_edge_cases_make_widget(label)); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$findWidget = function findWidget(id) { + try { + return $adoptOpt(edge_cases$Widget, $raw.edge_cases_edge_cases_find_widget(id)); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } +}; + +const edge_cases$inspectWidget = function inspectWidget(widget, fallback) { + const $o0 = $lend(widget, edge_cases$Widget); + try { + const $o1 = $lendOpt(fallback, edge_cases$Widget); + try { + return $raw.edge_cases_edge_cases_inspect_widget($o0, $o1); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(fallback); + } + } finally { + $unlend(widget); + } +}; + +const $it$edge_cases$string_stream = { + next: (h) => $raw.edge_cases_edge_cases_StringStreamIterator_next(h), + destroy: (h) => $raw.edge_cases_edge_cases_StringStreamIterator_destroy(h), + map: $fault, + convert: null, +}; +const edge_cases$stringStream = function stringStream() { + try { + return new $Iterator($raw.edge_cases_edge_cases_string_stream(), $it$edge_cases$string_stream); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$edge_cases$scalar_stream = { + next: (h) => $raw.edge_cases_edge_cases_ScalarStreamIterator_next(h), + destroy: (h) => $raw.edge_cases_edge_cases_ScalarStreamIterator_destroy(h), + map: $fault, + convert: null, +}; +const edge_cases$scalarStream = function scalarStream() { + try { + return new $Iterator($raw.edge_cases_edge_cases_scalar_stream(), $it$edge_cases$scalar_stream); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$edge_cases$mode_stream = { + next: (h) => $raw.edge_cases_edge_cases_ModeStreamIterator_next(h), + destroy: (h) => $raw.edge_cases_edge_cases_ModeStreamIterator_destroy(h), + map: $fault, + convert: null, +}; +const edge_cases$modeStream = function modeStream() { + try { + return new $Iterator($raw.edge_cases_edge_cases_mode_stream(), $it$edge_cases$mode_stream); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$asyncVoid = async function asyncVoid() { + try { + await $raw.edge_cases_edge_cases_async_void(); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$asyncBytes = async function asyncBytes(size) { + try { + return await $raw.edge_cases_edge_cases_async_bytes(size); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$asyncStruct = async function asyncStruct() { + try { + return $decode(await $raw.edge_cases_edge_cases_async_struct(), $r$edge_cases$Outer); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } +}; + +const edge_cases$asyncList = async function asyncList($options) { + try { + return $decode(await $cancellable($options?.signal, $tokens, ($t) => $raw.edge_cases_edge_cases_async_list($t)), (r) => r.readList($r$edge_cases$Outer)); + } catch ($e) { + throw $from$edge_cases$EdgeErrors($e); + } +}; + +const edge_cases$asyncOptional = async function asyncOptional() { + try { + return $decode(await $raw.edge_cases_edge_cases_async_optional(), (r) => r.readOpt($R.I64)); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$asyncEnum = async function asyncEnum() { + try { + return await $raw.edge_cases_edge_cases_async_enum(); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$asyncKeywords = async function asyncKeywords(class_, import_) { + try { + return await $raw.edge_cases_edge_cases_async_keywords(class_, import_); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$keywordsRoundtrip = function keywordsRoundtrip(value) { + try { + return $decode($raw.edge_cases_edge_cases_keywords_roundtrip($encode(value, $w$edge_cases$Keywords)), $r$edge_cases$Keywords); + } catch ($e) { + throw $fault($e); + } +}; + +const edge_cases$legacyValue = function legacyValue(value) { + try { + return $decode($raw.edge_cases_edge_cases_legacy_value(value), $r$edge_cases$Retired); + } catch ($e) { + throw $fault($e); + } +}; + +export const edge_cases = Object.freeze({ + EdgeErrorsError: edge_cases$EdgeErrorsError, + BusyError: edge_cases$BusyError, + Legacy: edge_cases$Legacy, + Mode: edge_cases$Mode, + Widget: edge_cases$Widget, + Vault: edge_cases$Vault, + noop: edge_cases$noop, + select: edge_cases$select, + nestedOptional: edge_cases$nestedOptional, + optionalParams: edge_cases$optionalParams, + registerBatch: edge_cases$registerBatch, + widgetsEverywhere: edge_cases$widgetsEverywhere, + widgetStream: edge_cases$widgetStream, + asyncWidget: edge_cases$asyncWidget, + makeWidget: edge_cases$makeWidget, + findWidget: edge_cases$findWidget, + inspectWidget: edge_cases$inspectWidget, + stringStream: edge_cases$stringStream, + scalarStream: edge_cases$scalarStream, + modeStream: edge_cases$modeStream, + asyncVoid: edge_cases$asyncVoid, + asyncBytes: edge_cases$asyncBytes, + asyncStruct: edge_cases$asyncStruct, + asyncList: edge_cases$asyncList, + asyncOptional: edge_cases$asyncOptional, + asyncEnum: edge_cases$asyncEnum, + asyncKeywords: edge_cases$asyncKeywords, + keywordsRoundtrip: edge_cases$keywordsRoundtrip, + legacyValue: edge_cases$legacyValue, +}); + +// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__linear_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__linear_js.snap new file mode 100644 index 00000000..608de11a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__linear_js.snap @@ -0,0 +1,403 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The WebAssembly transport: loading the module, staging arguments in its +// linear memory, reading results and errors back, and installing JavaScript +// functions in its function table (async completions and callback +// interface vtables). The generated `index.js` builds one native entry point +// per C symbol on top of this. + +import { $Fault } from './runtime.js'; + +const $enc = new TextEncoder(); +const $dec = new TextDecoder(); + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v, bits, what) { + let b; + if (typeof v === 'bigint') { + b = v; + } else if (typeof v === 'number' && Number.isInteger(v)) { + b = BigInt(v); + } else { + throw $expected('a bigint', v); + } + if (bits(64, b) !== b) throw new RangeError(`bigint does not fit in ${what} 64-bit integer`); + return b; +} + +/** + * One loaded module's linear memory and runtime symbols. `x` holds the + * module's exports by C symbol name; `memory()` returns the current + * `ArrayBuffer`; `fnptr(params, results, sig, fn)` installs `fn` in the + * function table and returns its index. + */ +export class $Linear { + constructor(x, prefix, memory, fnptr) { + this.x = x; + this.memory = memory; + this.fnptr = fnptr; + this.allocFn = x[`${prefix}_alloc`]; + this.deallocFn = x[`${prefix}_dealloc`]; + this.freeBytesFn = x[`${prefix}_free_bytes`]; + this.errorClearFn = x[`${prefix}_error_clear`]; + this.errorFreeFn = x[`${prefix}_error_free`]; + this.errorSetFn = x[`${prefix}_error_set`]; + if (typeof this.allocFn !== 'function' || typeof this.deallocFn !== 'function') { + throw new Error(`the WebAssembly module does not export ${prefix}_alloc and ${prefix}_dealloc`); + } + // One scratch block reused by every call: the error slot (16 bytes), + // the out_len slot (4), and the iterator out_item slot (8). The producer + // only writes them as a call returns, so a call made from a callback in + // the middle of another call can reuse them safely. + const scratch = this.allocFn(32); + this.bytes().fill(0, scratch, scratch + 32); + this.err = scratch; + this.len = scratch + 16; + this.item = scratch + 24; + this.callbacks = new Map(); + this.nextCallback = 1; + this.pending = new Map(); + this.nextPending = 1; + this.completions = new Map(); + this.vtables = new Map(); + } + + bytes() { + const buffer = this.memory(); + if (buffer !== this.buffer) { + this.buffer = buffer; + this.u8 = new Uint8Array(buffer); + this.dv = new DataView(buffer); + } + return this.u8; + } + + view() { + this.bytes(); + return this.dv; + } + + // Argument checks and conversions for by-value slots. + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + i64(v) { + return $bigint(v, BigInt.asIntN, 'a signed'); + } + + u64(v) { + return $bigint(v, BigInt.asUintN, 'an unsigned'); + } + + bool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + return v ? 1 : 0; + } + + handle(v) { + if (typeof v !== 'number' || v === 0) throw $expected('an object handle', v); + return v; + } + + handleOpt(v) { + return v === null || v === undefined ? 0 : this.handle(v); + } + + // Staging: copy a string or bytes into linear memory for one call. A + // staged value is [ptr, len, size]; release it with unstage(). + str(s) { + if (typeof s !== 'string') throw $expected('a string', s); + if (s.length === 0) return [0, 0, 0]; + const size = s.length * 3; + const ptr = this.allocFn(size); + const { written } = $enc.encodeInto(s, this.bytes().subarray(ptr, ptr + size)); + return [ptr, written, size]; + } + + data(b) { + if (!(b instanceof Uint8Array)) throw $expected('a Uint8Array', b); + if (b.length === 0) return [0, 0, 0]; + const ptr = this.allocFn(b.length); + this.bytes().set(b, ptr); + return [ptr, b.length, b.length]; + } + + unstage(staged) { + if (staged !== null && staged[2] !== 0) this.deallocFn(staged[0], staged[2]); + } + + // Results. + outLen() { + return this.view().getUint32(this.len, true); + } + + readStr(ptr, len) { + return len === 0 ? '' : $dec.decode(this.bytes().subarray(ptr, ptr + len)); + } + + readData(ptr, len) { + return len === 0 ? new Uint8Array(0) : this.bytes().slice(ptr, ptr + len); + } + + takeStr(ptr, len) { + const s = this.readStr(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return s; + } + + takeData(ptr, len) { + const b = this.readData(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return b; + } + + cstr(ptr) { + if (ptr === 0) return ''; + const u8 = this.bytes(); + let end = ptr; + while (u8[end] !== 0) end++; + return $dec.decode(u8.subarray(ptr, end)); + } + + fault(errPtr) { + const dv = this.view(); + const code = dv.getInt32(errPtr, true); + if (code === 0) return null; + const payloadPtr = dv.getUint32(errPtr + 8, true); + const payloadLen = dv.getUint32(errPtr + 12, true); + return new $Fault( + code, + this.cstr(dv.getUint32(errPtr + 4, true)), + payloadPtr === 0 ? null : this.bytes().slice(payloadPtr, payloadPtr + payloadLen), + ); + } + + /** Throw the error the last call wrote to the scratch error slot. */ + check() { + if (this.view().getInt32(this.err, true) === 0) return; + const fault = this.fault(this.err); + this.errorClearFn(this.err); + throw fault; + } + + // Callback interfaces: implementations live in a table keyed by the + // integer passed as `ctx`, until the producer calls the vtable's `free`. + register(adapter) { + const id = this.nextCallback++; + this.callbacks.set(id, adapter); + return id; + } + + adapter(id) { + const adapter = this.callbacks.get(id); + if (adapter === undefined) throw new Error('callback implementation was already released'); + return adapter; + } + + unregister(id) { + this.callbacks.delete(id); + } + + /** Report a callback implementation's exception through `errPtr` (code -4). */ + foreign(errPtr, e) { + const message = e instanceof Error ? e.message || e.name : String(e); + const bytes = $enc.encode(message); + const ptr = this.allocFn(bytes.length + 1); + const u8 = this.bytes(); + u8.set(bytes, ptr); + u8[ptr + bytes.length] = 0; + this.errorSetFn(errPtr, -4, ptr); + this.deallocFn(ptr, bytes.length + 1); + } + + /** + * The static vtable of one callback interface: one table entry per + * method (`methods()` returns each as `[params, results, sig, fn]`), then + * `free`. Built on first use. + */ + vtable(name, methods) { + let ptr = this.vtables.get(name); + if (ptr !== undefined) return ptr; + const entries = [...methods(), [['i32'], [], 'vi', (ctx) => this.unregister(ctx)]]; + ptr = this.allocFn(entries.length * 4); + const dv = this.view(); + entries.forEach(([params, results, sig, fn], i) => { + dv.setUint32(ptr + i * 4, this.fnptr(params, results, sig, fn), true); + }); + this.vtables.set(name, ptr); + return ptr; + } + + /** + * Launch an async call. `call(callback, context)` invokes the launcher; + * the completion (one table entry per signature, shared by every call) + * settles the returned promise, converting the result slots with + * `convert`. On this single-threaded target the completion normally + * fires before the launcher returns. + */ + launch(params, results, sig, call, convert) { + let callback = this.completions.get(sig); + if (callback === undefined) { + const complete = (ctx, errPtr, ...result) => this.complete(ctx, errPtr, result); + callback = this.fnptr(params, results, sig, complete); + this.completions.set(sig, callback); + } + const id = this.nextPending++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject, convert }); + try { + call(callback, id); + } catch (e) { + this.pending.delete(id); + reject(e); + } + }); + } + + complete(id, errPtr, result) { + const pending = this.pending.get(id); + if (pending === undefined) return; + this.pending.delete(id); + try { + const fault = errPtr === 0 ? null : this.fault(errPtr); + if (errPtr !== 0) this.errorFreeFn(errPtr); + if (fault !== null) { + pending.reject(fault); + } else { + pending.resolve(pending.convert(...result)); + } + } catch (e) { + pending.reject(e); + } + } +} + +/** A stand-in for the native entry points until `init()` has finished. */ +export function $unloaded(library) { + return new Proxy( + {}, + { + get() { + throw new Error(`${library}: call init() before using the bindings`); + }, + }, + ); +} + +const $valtype = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c }; + +function $leb(n) { + const out = []; + do { + let b = n & 0x7f; + n >>>= 7; + if (n !== 0) b |= 0x80; + out.push(b); + } while (n !== 0); + return out; +} + +function $section(id, body) { + return [id, ...$leb(body.length), ...body]; +} + +/** + * Turn a JavaScript function into a WebAssembly function with the given + * signature: `WebAssembly.Function` where the engine has it, otherwise a + * tiny module that imports `fn` and re-exports it. + */ +function $wasmFunction(params, results, fn) { + if (typeof WebAssembly.Function === 'function') { + return new WebAssembly.Function({ parameters: params, results }, fn); + } + const type = [0x60, ...$leb(params.length), ...params.map((t) => $valtype[t])]; + type.push(...$leb(results.length), ...results.map((t) => $valtype[t])); + const bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + ...$section(1, [1, ...type]), + ...$section(2, [1, 1, 0x65, 1, 0x66, 0x00, 0x00]), + ...$section(7, [1, 1, 0x66, 0x00, 0x00]), + ]); + const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), { e: { f: fn } }); + return instance.exports.f; +} + +async function $bytesOf(source, envVar, fallback) { + if (source === undefined || source === null) { + const env = typeof process === 'object' && process.env ? process.env[envVar] : undefined; + if (env) { + const { readFile } = await import('node:fs/promises'); + return readFile(env); + } + source = fallback; + } + if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { + return source; + } + if (typeof Response === 'function' && source instanceof Response) { + return source.arrayBuffer(); + } + const url = source instanceof URL ? source : new URL(String(source), fallback); + if (url.protocol === 'file:') { + const { readFile } = await import('node:fs/promises'); + return readFile(url); + } + const response = await fetch(url); + if (!response.ok) throw new Error(`failed to fetch ${url}: ${response.status}`); + return response.arrayBuffer(); +} + +/** + * Instantiate a `wasm32-unknown-unknown` module from `source` (a URL, a + * path, bytes, a `Response`, or a compiled module). Without a source it + * reads the file named by the `envVar` environment variable (on Node.js), + * else `fallback`. The module must export its memory and a growable + * function table (`__indirect_function_table`). + */ +export async function $loadWasm(source, prefix, envVar, fallback) { + const bytes = await $bytesOf(source, envVar, fallback); + const module = bytes instanceof WebAssembly.Module ? bytes : await WebAssembly.compile(bytes); + const { exports } = await WebAssembly.instantiate(module, {}); + const table = exports.__indirect_function_table; + const fnptr = (params, results, sig, fn) => { + if (!(table instanceof WebAssembly.Table)) { + throw new Error('the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)'); + } + const index = table.grow(1); + table.set(index, $wasmFunction(params, results, fn)); + return index; + }; + return new $Linear(exports, prefix, () => exports.memory.buffer, fnptr); +} + +/** + * Adopt an initialized Emscripten module (or the promise its `MODULARIZE` + * factory returns). Its exports carry a leading underscore, its memory is + * read through `HEAPU8`, and table entries are installed with + * `addFunction`, so it must be linked with `-sALLOW_TABLE_GROWTH`, + * `-sWASM_BIGINT`, and `-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`. + */ +export async function $loadEmscripten(module, prefix, symbols) { + const m = await module; + const x = {}; + for (const symbol of symbols) x[symbol] = m[`_${symbol}`]; + const fnptr = (params, results, sig, fn) => { + if (typeof m.addFunction !== 'function') { + throw new Error('the Emscripten module does not export addFunction (link with -sALLOW_TABLE_GROWTH and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8)'); + } + return m.addFunction(fn, sig); + }; + return new $Linear(x, prefix, () => m.HEAPU8.buffer, fnptr); +} + +// END linear.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__package_json.snap index cee4ead6..903ddfb1 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__package_json.snap @@ -2,13 +2,25 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "edge_cases", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for edge_cases", "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" + "main": "index.js", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "linear.js" + ] } diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__runtime_js.snap new file mode 100644 index 00000000..d7fbec01 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class EdgeCasesError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends EdgeCasesError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new EdgeCasesError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new EdgeCasesError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new EdgeCasesError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new EdgeCasesError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js 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 deleted file mode 100644 index e77514c4..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,364 +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, producer - * panics, and callback-interface implementations that raised (code -4). - * 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; -} - -/** Record carrying objects in every buffered position */ -export interface WidgetHolder { - primary: Widget; - fallback: Widget | null; - extras: Widget[]; - by_name: Record; -} - -/** 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]; - -/** - * Callback interface carrying composite payloads and objects - * - * Implemented by the consumer; the producer calls these methods synchronously. An exception thrown from a method is reported to the producer as an error (code -4). - */ -export interface BatchListener { - /** - * Composite payloads cross as value buffers - */ - onBatch(items: Outer[], raw: Uint8Array, first: Outer | null, mode: Mode): void; - /** - * Objects cross as strong references the consumer must release - */ - onWidget(widget: Widget, fallback: Widget | null, children: Widget[]): void; - onTick(): void; - /** - * Scalar return travels back through the vtable - */ - shouldContinue(class_: number): boolean; - /** - * Enum return travels back as its integer discriminant - */ - pickMode(): Mode; -} - -/** - * Callback interface whose name and methods are keywords - * - * Implemented by the consumer; the producer calls these methods synchronously. An exception thrown from a method is reported to the producer as an error (code -4). - */ -export interface Keyword { - delete(self: number): void; - import(): number; -} - -/** - * Interface with no constructor; instances come from make_widget - * - * Wraps one reference to a producer object; release it with `close()`. - */ -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 this wrapper's reference exactly once; later calls are - * no-ops. Also reachable as `[Symbol.dispose]()` for `using` - * declarations. A wrapper collected unclosed is released by a - * finalizer where the runtime provides one. */ - close(): void; -} - -/** - * Deprecated interface with a throwing constructor - * - * Wraps one reference to a producer object; release it with `close()`. - */ -export declare class Vault { - /** - * Fails when the vault is busy - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @throws {EdgeErrorsError} on a domain error code - */ - static open(path: string): Vault; - /** - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @throws {WeaveFFIError} if the native call fails - */ - peek(): Widget | null; - /** - * Throwing static - * @throws {EdgeErrorsError} on a domain error code - */ - static probe(): boolean; - /** Releases this wrapper's reference exactly once; later calls are - * no-ops. Also reachable as `[Symbol.dispose]()` for `using` - * declarations. A wrapper collected unclosed is released by a - * finalizer where the runtime provides one. */ - close(): 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; - /** - * Two callback interface parameters on one function - * @throws {WeaveFFIError} if the native call fails - */ - registerBatch(listener: BatchListener, keyword: Keyword): void; - /** - * Objects in every buffered position - * @throws {WeaveFFIError} if the native call fails - */ - widgetsEverywhere(list: Widget[], byName: Record, sparse: (Widget | null)[], holder: WidgetHolder): WidgetHolder; - /** - * Iterator of objects - * @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 - */ - widgetStream(): IterableIterator; - /** - * Async returning an object - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @throws {EdgeErrorsError} on a domain error code - */ - asyncWidget(): Promise; - /** - * Factory for an interface with no constructor - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @throws {WeaveFFIError} if the native call fails - */ - makeWidget(label: string): Widget; - /** - * Optional interface return - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @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; - /** - * 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; - Widget: typeof Widget; - Vault: typeof Vault; - }; -} - -export function loadWeaveffiWasm(source: string | URL | BufferSource | WebAssembly.Module): 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 deleted file mode 100644 index 87ce7c63..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__weaveffi_wasm_js.snap +++ /dev/null @@ -1,1626 +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); one -// strong reference per wrapper, released by close() -// 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 -// Callbacks -> i32 context key + i32 pointer to a static vtable - -// The ABI revision this glue was generated against. -const _ABI_VERSION = 2; - -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, producer - * panics, and callback-interface implementations that raised (code -4). - * 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, -}); - -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); } - // An object token: the pointer (a wasm32 i32, read unsigned) widened to u64. - obj(ptr) { this.u64(BigInt(ptr >>> 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; - } - // An object token narrowed back to a wasm32 pointer; zero is never a - // valid token (an absent `Interface?` uses the option flag instead). - obj() { - const t = this.u64(); - if (t === 0n || t > 0xffffffffn) this._bad('object token out of range'); - return Number(t); - } - 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 (-2), a marshalling failure (-3), or a callback -// interface implementation that raised (-4), surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - _pendingForeign = null; - 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) { - _pendingForeign = null; - wasm.weaveffi_dealloc(errPtr, 16); -} - -// The message of a callback-interface failure a trampoline reported through -// out_err, held until the producer call on the stack returns (every error -// checker and _freeErr clear it) so later callbacks in that call are refused. -let _pendingForeign = null; - -// Translate an exception raised while a producer call was on the stack. -// wasm32-unknown-unknown has no unwinding runtime: a producer panic executes -// `unreachable`, so the engine throws a WebAssembly.RuntimeError instead of -// the producer writing out_err. A trap that follows a callback-interface -// failure is labelled with that failure (-4); any other trap is a producer -// panic (-2). Non-trap exceptions pass through unchanged. The producer -// frames between the trap and the call are not unwound, so a lock held -// across a callback stays locked; a producer that snapshots its state -// before calling out stays usable. -function _trapError(e) { - const foreign = _pendingForeign; - _pendingForeign = null; - if (!(e instanceof WebAssembly.RuntimeError)) return e; - if (foreign !== null) return new WeaveFFIError(-4, foreign); - return new WeaveFFIError(-2, 'producer panicked: ' + e.message); -} - -// Release the error slot a failed call never filled, then translate. -function _trap(wasm, errPtr, e) { - _freeErr(wasm, errPtr); - return _trapError(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) { - _pendingForeign = null; - 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; -} - -// `using` declarations (explicit resource management) call this symbol; -// runtimes that predate it get the registered stand-in so the method is -// still reachable by name. -const _dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); - -// Finalization backstop: a wrapper that is garbage collected without -// close() still releases its reference. The held value is the -// [destroy, handle] pair (never the wrapper, which would keep it alive); -// the wrapper is the unregister token so close() disarms the backstop and -// destroy runs exactly once either way. Absent on runtimes without -// FinalizationRegistry, where close() is the only release path. -const _finalizer = typeof FinalizationRegistry === 'function' - ? new FinalizationRegistry(([destroy, handle]) => destroy(handle)) - : null; - -// Bind one strong reference to a wrapper and arm the backstop. -function _adopt(obj, handle, destroy) { - obj._handle = handle; - obj._destroy = destroy; - if (_finalizer !== null) _finalizer.register(obj, [destroy, handle], obj); - return obj; -} - -// Release a wrapper's reference exactly once; later calls are no-ops. -function _release(obj) { - if (!obj._handle) return; - if (_finalizer !== null) _finalizer.unregister(obj); - obj._destroy(obj._handle); - obj._handle = 0; -} - -// The pointer of a live wrapper, lent to the producer for one call (the -// wrapper keeps its own reference). A closed or non-object argument is a -// consumer programming error, reported with the marshalling code. -function _borrow(obj) { - if (obj === null || obj === undefined || !obj._handle) { - throw new WeaveFFIError(-3, 'expected a live object wrapper'); - } - return obj._handle; -} - -// Report a callback-interface failure through the trampoline's out_err -// slot as FOREIGN_ERROR_CODE (-4). weaveffi_error_set copies the message -// with the producer's allocator, so the staged C string is released as -// soon as it returns. The message is parked in _pendingForeign until the -// producer call on the stack returns: the producer can't unwind on wasm32, -// so it keeps running on the trampoline's default return value, and any -// further callback it makes during that call is refused with this error -// instead of reaching the implementation again. -function _reportForeign(wasm, errPtr, msg) { - if (_pendingForeign === null) _pendingForeign = msg; - const [p, s] = _cstr(wasm, msg); - wasm.weaveffi_error_set(errPtr, -4, p); - wasm.weaveffi_dealloc(p, s); -} - -// Report an exception thrown by a callback-interface implementation. -function _setForeignError(wasm, errPtr, e) { - _reportForeign(wasm, errPtr, e instanceof Error ? (e.message || e.name) : String(e)); -} - -// 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); - } catch (e) { - // A trap: the slot was never filled, so release it here. - this._close(); - throw _trap(wasm, _err, e); - } - try { - // 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, resultTypes, handler) { - const idx = table.grow(1); - table.set(idx, new WebAssembly.Function( - { parameters: paramTypes, results: resultTypes }, - 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. - * - * @param {string|URL|BufferSource|WebAssembly.Module} source - A URL to - * fetch the `.wasm` from, its bytes, or an already compiled module - * (use the bytes form where `fetch` cannot read files, e.g. Node). - * @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 }); - * - * // Object: a class wrapping one producer reference; close() releases it - * // (or let `using` do it). Passing it lends the reference for the call. - * const store = new api.kv.Store(); - * api.kv.inspect(store); - * store.close(); - * - * // 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(source) { - let module; - if (source instanceof WebAssembly.Module) { - module = source; - } else if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { - module = await WebAssembly.compile(source); - } else { - const response = await fetch(source); - module = await WebAssembly.compile(await response.arrayBuffer()); - } - const instance = await WebAssembly.instantiate(module, {}); - const wasm = instance.exports; - - _checkAbiVersion(wasm); - - // Build the EdgeErrorsError subclass matching `code`, or a generic - // WeaveFFIError for codes outside the domain (panics, marshalling, - // foreign callback failures). - 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; - } - - // Throw the `EdgeErrorsError` domain error (and free the slot) if the error slot - // carries a non-zero code. - function _checkEdgeErrorsError(wasm, errPtr) { - _pendingForeign = null; - 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; - } - } - - // 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; - } - - // Serialize a `edge_cases.WidgetHolder` record into the value-buffer wire format. - function _write_edge_cases_WidgetHolder(w, v) { - w.obj(v.primary._clone()); - if (v.fallback === null || v.fallback === undefined) { - w.flag(false); - } else { - w.flag(true); - w.obj(v.fallback._clone()); - } - const _a1 = v.extras || []; - w.len(_a1.length); - for (const _e1 of _a1) { - w.obj(_e1._clone()); - } - const _s2 = v.by_name || {}; - const _m2 = _s2 instanceof Map ? [..._s2.entries()] : Object.entries(_s2); - w.len(_m2.length); - for (const [_k2, _v2] of _m2) { - w.str(_k2); - w.obj(_v2._clone()); - } - } - - // Decode a `edge_cases.WidgetHolder` record from the value-buffer wire format. - function _read_edge_cases_WidgetHolder(r) { - const v = {}; - v.primary = Widget._wrap(r.obj()); - v.fallback = (r.flag() ? Widget._wrap(r.obj()) : null); - v.extras = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(Widget._wrap(r.obj())); return _arr; })(); - v.by_name = (() => { const _n = r.len(); const _obj = {}; for (let _i = 0; _i < _n; _i++) { const _k = r.str(); _obj[_k] = Widget._wrap(r.obj()); } return _obj; })(); - return v; - } - - 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 = _registerTrampoline(_table, ['i32', 'i32', 'i32'], [], _asyncHandler); - const _cbPtr_i32_i32 = _registerTrampoline(_table, ['i32', 'i32'], [], _asyncHandler); - const _cbPtr_i32_i32_i32_i32 = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32'], [], _asyncHandler); - - // Callback-interface implementations, keyed by the integer context - // the loader passes as `ctx`; an entry lives until the producer - // calls the vtable's `free(ctx)`. - let _nextCbId = 1; - const _callbacks = new Map(); - - // Trampolines for the `BatchListener` callback interface: one WebAssembly.Function - // per vtable entry, mirroring the C signature of each method. Borrowed - // arguments are decoded before the JS method runs; object arguments - // are adopted; a thrown exception becomes a -4 error on out_err. - const _cb_BatchListener_on_batch = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], [], (_ctx, a0, a1, a2, a3, a4, a5, a6, _err) => { - try { - const _impl = _callbacks.get(_ctx); - 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; - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return; - } - _impl.onBatch(_p0, _p1, _p2, _p3); - } catch (e) { - _setForeignError(wasm, _err, e); - } - }); - const _cb_BatchListener_on_widget = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], [], (_ctx, a0, a1, a2, a3, _err) => { - try { - const _impl = _callbacks.get(_ctx); - const _p0 = Widget._wrap(a0); - const _p1 = a1 === 0 ? null : Widget._wrap(a1); - const _p2_b = (a2 === 0 || a3 === 0) ? new Uint8Array(0) : new Uint8Array(wasm.memory.buffer, a2, a3).slice(); - const _p2_r = new _BufReader(_p2_b); - const _p2 = (() => { const _n = _p2_r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(Widget._wrap(_p2_r.obj())); return _arr; })(); - _p2_r.end(); - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return; - } - _impl.onWidget(_p0, _p1, _p2); - } catch (e) { - _setForeignError(wasm, _err, e); - } - }); - const _cb_BatchListener_on_tick = _registerTrampoline(_table, ['i32', 'i32'], [], (_ctx, _err) => { - try { - const _impl = _callbacks.get(_ctx); - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return; - } - _impl.onTick(); - } catch (e) { - _setForeignError(wasm, _err, e); - } - }); - const _cb_BatchListener_should_continue = _registerTrampoline(_table, ['i32', 'i32', 'i32'], ['i32'], (_ctx, a0, _err) => { - try { - const _impl = _callbacks.get(_ctx); - const _p0 = a0; - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return 0; - } - return _impl.shouldContinue(_p0) ? 1 : 0; - } catch (e) { - _setForeignError(wasm, _err, e); - return 0; - } - }); - const _cb_BatchListener_pick_mode = _registerTrampoline(_table, ['i32', 'i32'], ['i32'], (_ctx, _err) => { - try { - const _impl = _callbacks.get(_ctx); - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return 0; - } - return _impl.pickMode(); - } catch (e) { - _setForeignError(wasm, _err, e); - return 0; - } - }); - // `free(ctx)`: the producer dropped its last reference; forget the - // implementation so it can be collected. - const _cb_BatchListener_free = _registerTrampoline(_table, ['i32'], [], (_ctx) => { _callbacks.delete(_ctx); }); - // The one static `weaveffi_edge_cases_BatchListener_vtable` for this instance, filled with the table indices - // above in declaration order, then `free`. - const _vtable_BatchListener = wasm.weaveffi_alloc(24); - { - const _dv = new DataView(wasm.memory.buffer); - _dv.setUint32(_vtable_BatchListener + 0, _cb_BatchListener_on_batch, true); - _dv.setUint32(_vtable_BatchListener + 4, _cb_BatchListener_on_widget, true); - _dv.setUint32(_vtable_BatchListener + 8, _cb_BatchListener_on_tick, true); - _dv.setUint32(_vtable_BatchListener + 12, _cb_BatchListener_should_continue, true); - _dv.setUint32(_vtable_BatchListener + 16, _cb_BatchListener_pick_mode, true); - _dv.setUint32(_vtable_BatchListener + 20, _cb_BatchListener_free, true); - } - - // Trampolines for the `Keyword` callback interface: one WebAssembly.Function - // per vtable entry, mirroring the C signature of each method. Borrowed - // arguments are decoded before the JS method runs; object arguments - // are adopted; a thrown exception becomes a -4 error on out_err. - const _cb_Keyword_delete = _registerTrampoline(_table, ['i32', 'i32', 'i32'], [], (_ctx, a0, _err) => { - try { - const _impl = _callbacks.get(_ctx); - const _p0 = a0; - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return; - } - _impl.delete(_p0); - } catch (e) { - _setForeignError(wasm, _err, e); - } - }); - const _cb_Keyword_import = _registerTrampoline(_table, ['i32', 'i32'], ['i32'], (_ctx, _err) => { - try { - const _impl = _callbacks.get(_ctx); - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return 0; - } - return _impl.import(); - } catch (e) { - _setForeignError(wasm, _err, e); - return 0; - } - }); - // `free(ctx)`: the producer dropped its last reference; forget the - // implementation so it can be collected. - const _cb_Keyword_free = _registerTrampoline(_table, ['i32'], [], (_ctx) => { _callbacks.delete(_ctx); }); - // The one static `weaveffi_edge_cases_Keyword_vtable` for this instance, filled with the table indices - // above in declaration order, then `free`. - const _vtable_Keyword = wasm.weaveffi_alloc(12); - { - const _dv = new DataView(wasm.memory.buffer); - _dv.setUint32(_vtable_Keyword + 0, _cb_Keyword_delete, true); - _dv.setUint32(_vtable_Keyword + 4, _cb_Keyword_import, true); - _dv.setUint32(_vtable_Keyword + 8, _cb_Keyword_free, true); - } - - // Interface with no constructor; instances come from make_widget - class Widget { - static _wrap(handle) { - return _adopt(Object.create(Widget.prototype), handle, wasm.weaveffi_edge_cases_Widget_destroy); - } - _clone() { - return wasm.weaveffi_edge_cases_Widget_clone(_borrow(this)); - } - close() { - _release(this); - } - [_dispose]() { - _release(this); - } - refresh() { - const _err = _allocErr(wasm); - try { - wasm.weaveffi_edge_cases_Widget_refresh(_borrow(this), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _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); - let _r; - try { - _r = wasm.weaveffi_edge_cases_Widget_rename(_borrow(this), a0_p, a1_p, a1_l, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - 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; - } }); - try { - wasm.weaveffi_edge_cases_Widget_fetch_async(_borrow(this), BigInt(id), _cbPtr_i32_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - } - /** @deprecated Use rename instead */ - oldName() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_edge_cases_Widget_old_name(_borrow(this), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - } - } - - // Deprecated interface with a throwing constructor - class Vault { - static _wrap(handle) { - return _adopt(Object.create(Vault.prototype), handle, wasm.weaveffi_edge_cases_Vault_destroy); - } - _clone() { - return wasm.weaveffi_edge_cases_Vault_clone(_borrow(this)); - } - close() { - _release(this); - } - [_dispose]() { - _release(this); - } - static open(path) { - const [a0_p, a0_s] = _cstr(wasm, path); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_edge_cases_Vault_open(a0_p, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_s); - } - _checkEdgeErrorsError(wasm, _err); - _freeErr(wasm, _err); - return Vault._wrap(_r); - } - peek() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_edge_cases_Vault_peek(_borrow(this), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r === 0 ? null : Widget._wrap(_r); - } - static probe() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_edge_cases_Vault_probe(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkEdgeErrorsError(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - } - } - - return { - _raw: wasm, - edge_cases: { - noop() { - const _err = _allocErr(wasm); - try { - wasm.weaveffi_edge_cases_noop(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _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); - let _r; - try { - _r = wasm.weaveffi_edge_cases_select(class_, a1_p, a2_p, a2_l, a3_p, a3_l, type ? 1 : 0, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - 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); - let _r; - try { - _r = wasm.weaveffi_edge_cases_nested_optional(a0_p, a0_l, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } finally { - 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); - let _r; - try { - _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); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } finally { - 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; - }, - registerBatch(listener, keyword) { - const a0_ctx = _nextCbId++; - _callbacks.set(a0_ctx, listener); - const a1_ctx = _nextCbId++; - _callbacks.set(a1_ctx, keyword); - const _err = _allocErr(wasm); - try { - wasm.weaveffi_edge_cases_register_batch(a0_ctx, _vtable_BatchListener, a1_ctx, _vtable_Keyword, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - }, - widgetsEverywhere(list, byName, sparse, holder) { - const a0_w = new _BufWriter(); - const _a1 = list || []; - a0_w.len(_a1.length); - for (const _e1 of _a1) { - a0_w.obj(_e1._clone()); - } - const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); - const a1_w = new _BufWriter(); - const _s1 = byName || {}; - const _m1 = _s1 instanceof Map ? [..._s1.entries()] : Object.entries(_s1); - a1_w.len(_m1.length); - for (const [_k1, _v1] of _m1) { - a1_w.str(_k1); - a1_w.obj(_v1._clone()); - } - const [a1_p, a1_l] = _bytes(wasm, a1_w.finish()); - const a2_w = new _BufWriter(); - const _a1 = sparse || []; - a2_w.len(_a1.length); - for (const _e1 of _a1) { - if (_e1 === null || _e1 === undefined) { - a2_w.flag(false); - } else { - a2_w.flag(true); - a2_w.obj(_e1._clone()); - } - } - const [a2_p, a2_l] = _bytes(wasm, a2_w.finish()); - const a3_w = new _BufWriter(); - _write_edge_cases_WidgetHolder(a3_w, holder); - const [a3_p, a3_l] = _bytes(wasm, a3_w.finish()); - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_edge_cases_widgets_everywhere(a0_p, a0_l, a1_p, a1_l, a2_p, a2_l, a3_p, a3_l, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } finally { - 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); - } - _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_WidgetHolder(_rd); - _rd.end(); - return _out; - }, - widgetStream() { - const _err = _allocErr(wasm); - let _it; - try { - _it = wasm.weaveffi_edge_cases_widget_stream(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return new _WeaveFFIIterator(wasm, _it, 4, - (it, slot, ep) => wasm.weaveffi_edge_cases_WidgetStreamIterator_next(it, slot, ep), - (it) => wasm.weaveffi_edge_cases_WidgetStreamIterator_destroy(it), - _checkErr, (w, p) => Widget._wrap(new DataView(w.memory.buffer).getUint32(p, true))); - }, - asyncWidget() { - return new Promise((resolve, reject) => { - const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject, mkErr: _edgeErrorsErrorFrom, unwrap: (w, h) => Widget._wrap(h) }); - try { - wasm.weaveffi_edge_cases_async_widget_async(_cbPtr_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - }, - makeWidget(label) { - const [a0_p, a0_s] = _cstr(wasm, label); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_edge_cases_make_widget(a0_p, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_s); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return Widget._wrap(_r); - }, - findWidget(id) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_edge_cases_find_widget(BigInt(id), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkEdgeErrorsError(wasm, _err); - _freeErr(wasm, _err); - return _r === 0 ? null : Widget._wrap(_r); - }, - inspectWidget(widget, fallback) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_edge_cases_inspect_widget(_borrow(widget), (fallback === null || fallback === undefined ? 0 : _borrow(fallback)), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - }, - stringStream() { - const _err = _allocErr(wasm); - let _it; - try { - _it = wasm.weaveffi_edge_cases_string_stream(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _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); - let _it; - try { - _it = wasm.weaveffi_edge_cases_scalar_stream(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _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); - let _it; - try { - _it = wasm.weaveffi_edge_cases_mode_stream(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _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 }); - try { - wasm.weaveffi_edge_cases_async_void_async(_cbPtr_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - }, - 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; - } }); - try { - wasm.weaveffi_edge_cases_async_bytes_async(size, _cbPtr_i32_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - }, - 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; - } }); - try { - wasm.weaveffi_edge_cases_async_struct_async(_cbPtr_i32_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - }, - 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; - } }); - try { - wasm.weaveffi_edge_cases_async_list_async(0, _cbPtr_i32_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - }, - 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; - } }); - try { - wasm.weaveffi_edge_cases_async_optional_async(_cbPtr_i32_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - }, - asyncEnum() { - return new Promise((resolve, reject) => { - const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject }); - try { - wasm.weaveffi_edge_cases_async_enum_async(_cbPtr_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); - }, - 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_); - try { - wasm.weaveffi_edge_cases_async_keywords_async(a0_p, import_, _cbPtr_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } finally { - 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); - let _r; - try { - _r = wasm.weaveffi_edge_cases_keywords_roundtrip(a0_p, a0_l, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } finally { - 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); - let _r; - try { - _r = wasm.weaveffi_edge_cases_legacy_value(value, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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; - }, - Widget: Widget, - Vault: Vault, - }, - }; -} - -// 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 44341e7c..72a2d3b8 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 @@ -1,447 +1,20 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Wasm (experimental) +# kitchen_sink -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. +Native bindings for kitchen_sink -Build (example): +WebAssembly bindings for a `wasm32-unknown-unknown` build of the `kitchen_sink` library, linked with `--export-table --growable-table` (for example `RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table"`). `init()` loads `kitchen_sink.wasm` from next to this package, or from the path in `KITCHEN_SINK_LIBRARY` on Node.js; pass a URL, bytes, or a compiled module to load it from elsewhere: -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it: the loader accepts a URL to fetch, the module's bytes (for Node, where `fetch` cannot read files), or an already compiled `WebAssembly.Module`. - -## 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. - -### Objects - -Interfaces are **wrapper classes** holding one reference to a producer object (an `i32` pointer into linear memory). A wrapper you receive is yours: release it with `close()` (or bind it with `using`, which calls `Symbol.dispose`); a wrapper collected unclosed is released by a `FinalizationRegistry` backstop where the runtime provides one, and destroy runs exactly once either way. Passing a wrapper to a function lends its reference for the duration of the call. Inside a value buffer an object crosses as a `u64` token carrying one reference: the writer clones the wrapper's reference and the reader adopts the token into a new wrapper. - -### 64-bit Integers - -`i64` and `u64` are JavaScript **`bigint`** values everywhere: as arguments, returns, record fields, and callback parameters. Map keys of these types become string property names on the decoded object. A `number` is accepted where a `bigint` is expected (it's widened with `BigInt(v)`, so a fractional value throws a `RangeError`). Values crossing by value follow the wasm calling convention: an out-of-range `bigint` wraps modulo 2^64 rather than throwing, and unsigned results are read back as unsigned (`u64` via `BigInt.asUintN`, `u32` via `>>> 0`). - -### 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) and maps to `Wrapper | null`. - -### 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. - -### Async Functions - -`async` functions return a **`Promise`**. `wasm32-unknown-unknown` has no threads, so the producer's default spawner drives each future to completion inline, inside the launch call: the completion callback fires (through a function-table trampoline) before the wrapper returns, and the Promise is already settled when you `await` it. There is nothing to pump or poll from JS. A future that returns `Pending` can't be woken on this target with the default spawner; a producer whose futures wait on external events needs its own spawner (`weaveffi::set_spawner`), and driving that executor is then the producer's responsibility, not the glue's. A typed error rejects the Promise with the same class the synchronous wrapper would throw. - -### Callback Interfaces - -A callback interface is a TypeScript `interface`; pass any object with the matching methods where a parameter of that type is expected. The loader keeps one **static vtable per interface** in linear memory, filled with function-table indices of long-lived trampolines, and passes a small integer context key with it; the producer calls `free(ctx)` when it drops the callback, which releases your object. Delivery is **synchronous and same-thread**: `wasm32-unknown-unknown` is single-threaded, so a method runs only while a call into the module is on the stack. A producer that calls back from a spawned thread cannot run on this target at all. - -Method arguments that are strings, bytes, or buffered values are **borrowed for the duration of the call**: they're copied or decoded into JS values before your method runs. Object arguments transfer one reference to you: `close()` the wrapper when you're done with it. An exception thrown from a method is reported to the producer as error code `-4` (with the exception's message) and the method's default value is returned in its place; the producer then aborts the call it was making, and the original caller sees a `WeaveFFIError` with code `-4` carrying your message (see *Traps* below for how that abort reaches the caller on this target). - -### 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_error_set(err_ptr: i32, code: i32, msg: i32)`: fill an error slot (used to report callback exceptions) -- `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, marshalling failures, and callback exceptions -(code `-4`). 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. - -#### Callback failures without unwinding - -`wasm32-unknown-unknown` builds with `panic = "abort"`, so when a callback interface method throws, the producer can't unwind out of the call the way native targets do. Instead the runtime records the failure, the producer's code runs to completion on the method's default return value (`0`, `0n`, or `false`), and the thunk then reports code `-4` with the exception's message in place of the result, so the wrapper throws (or the Promise rejects with) the same `WeaveFFIError` a native build would produce. While that producer call is still on the stack, the glue refuses every further callback invocation with the same error rather than calling your implementation again, so your code observes the failure exactly as it would under unwinding. Producers must tolerate a callback returning its default once (a well-behaved producer already does, because a consumer could return that value on purpose). - -#### Traps - -A producer panic can't be caught on the Rust side either: it executes `unreachable`, and the engine throws a `WebAssembly.RuntimeError` out of the call. The glue translates that trap for you: the wrapper throws (or the Promise rejects with) a `WeaveFFIError` with code `-2` (`producer panicked`), or code `-4` if a callback method had already failed during the call. The error slot and every staged argument are released either way. The producer frames between the trap and the call are *not* unwound, so their cleanup does not run: a lock held across the panic stays locked, and allocations those frames owned leak. - -## API Reference - -### Module: `shared` - -#### Functions - -##### `weaveffi_shared_ping` - -Trivial cross-module helper - -`weaveffi_shared_ping() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `string` | `i32` | NUL-terminated C string pointer | - -#### Structs - -##### `Token` - -A reusable token referenced across modules - -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` | -| `label` | `string` | - -### Module: `kitchen` - -#### Error Domain: `KitchenErrorsError` - -Throwing wrappers in this module raise `KitchenErrorsError` (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 | -|-------|------|-----------------|----------------| -| `NotFound` | `1` | Item not found | (none) | -| `InvalidInput` | `2` | Invalid input | (none) | - -#### Functions - -##### `weaveffi_kitchen_bool_id` - -Identity for bool - -`weaveffi_kitchen_bool_id(x: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `x` | `bool` | `i32` | 0 = false, 1 = true | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_kitchen_i32_id` - -`weaveffi_kitchen_i32_id(x: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `x` | `i32` | `i32` | native Wasm i32 | -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_kitchen_u32_id` - -`weaveffi_kitchen_u32_id(x: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `x` | `u32` | `i32` | unsigned mapped to i32 | -| _returns_ | `u32` | `i32` | unsigned mapped to i32 | - -##### `weaveffi_kitchen_i64_id` - -`weaveffi_kitchen_i64_id(x: i64) -> i64` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `x` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `i64` | `i64` | native Wasm i64 | - -##### `weaveffi_kitchen_f64_id` - -`weaveffi_kitchen_f64_id(x: f64) -> f64` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `x` | `f64` | `f64` | native Wasm f64 | -| _returns_ | `f64` | `f64` | native Wasm f64 | - -##### `weaveffi_kitchen_echo_string` - -`weaveffi_kitchen_echo_string(s: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `s` | `string` | `i32` | NUL-terminated C string pointer | -| _returns_ | `string` | `i32` | NUL-terminated C string pointer | - -##### `weaveffi_kitchen_echo_bytes` - -`weaveffi_kitchen_echo_bytes(b: i32, i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `b` | `bytes` | `i32, i32` | ptr + len in linear memory | -| _returns_ | `bytes` | `i32, i32` | ptr + len in linear memory | - -##### `weaveffi_kitchen_subscribe` - -Register a listener, returning how many are attached - -`weaveffi_kitchen_subscribe(listener: i32, i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `listener` | `ReadyListener` | `i32, i32` | context key + static vtable pointer | -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_kitchen_maybe_gadget` - -Optional object in both directions - -`weaveffi_kitchen_maybe_gadget(fallback: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `fallback` | `Gadget?` | `i32` | nullable object pointer, 0 = absent | -| _returns_ | `Gadget?` | `i32` | nullable object pointer, 0 = absent | - -##### `weaveffi_kitchen_gadgets` - -A list of objects - -`weaveffi_kitchen_gadgets() -> i32, i32` +```js +import { init, shared, kitchen } from 'kitchen_sink'; -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `[Gadget]` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_kitchen_stream_gadgets` - -An iterator over objects - -`weaveffi_kitchen_stream_gadgets() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `iter` | `i32` | opaque iterator handle | - -##### `weaveffi_kitchen_maybe_item` - -Optional struct return - -`weaveffi_kitchen_maybe_item(id: i64) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `Item?` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_kitchen_list_items` - -List of structs - -`weaveffi_kitchen_list_items() -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `[Item]` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_kitchen_get_attrs` - -Map return type - -`weaveffi_kitchen_get_attrs() -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `{string:i32}` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_kitchen_stream_items` - -Iterator return type - -`weaveffi_kitchen_stream_items() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `iter` | `i32` | opaque iterator handle | - -##### `weaveffi_kitchen_cross_module_token` - -Returns the shared Token type from another module - -`weaveffi_kitchen_cross_module_token() -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `Token` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_kitchen_do_async` - -Async operation - -`weaveffi_kitchen_do_async(input: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `input` | `string` | `i32` | NUL-terminated C string pointer | -| _returns_ | `string` | `i32` | NUL-terminated C string pointer | - -##### `weaveffi_kitchen_do_cancellable` - -Cancellable async operation - -`weaveffi_kitchen_do_cancellable(input: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `input` | `string` | `i32` | NUL-terminated C string pointer | -| _returns_ | `string` | `i32` | NUL-terminated C string pointer | - -##### `weaveffi_kitchen_legacy_op` - -Legacy operation kept for compatibility - -**Deprecated:** Use new_op instead - -`weaveffi_kitchen_legacy_op() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_kitchen_new_op` - -Replacement for legacy_op - -`weaveffi_kitchen_new_op() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i32` | `i32` | native Wasm i32 | - -#### Interfaces - -##### `Gadget` - -A pocket-sized interface exercising the object surface - -Passed as an **object pointer** (`i32`), wrapped by a JS class holding one reference. Constructors return an owned reference; methods lend the pointer as the implicit leading `self` argument; `close()` (or `Symbol.dispose`) releases the reference via the destroy symbol exactly once. - -##### `weaveffi_kitchen_Gadget_new` - -Create a gadget with the given id - -`weaveffi_kitchen_Gadget_new(id: i64) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `Gadget` | `i32` | borrowed object pointer | - -##### `weaveffi_kitchen_Gadget_describe` - -Render the gadget as a human-readable string - -`weaveffi_kitchen_Gadget_describe() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `string` | `i32` | NUL-terminated C string pointer | - -##### `weaveffi_kitchen_Gadget_poke` - -Poke the gadget a number of times, failing on a negative count - -`weaveffi_kitchen_Gadget_poke(times: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `times` | `i32` | `i32` | native Wasm i32 | -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_kitchen_Gadget_version` - -The gadget subsystem version string - -`weaveffi_kitchen_Gadget_version() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `string` | `i32` | NUL-terminated C string pointer | - -##### `weaveffi_kitchen_Gadget_clone` - -Returns a second reference to the same object. Used when the wrapper is encoded as an object token inside a value buffer. - -##### `weaveffi_kitchen_Gadget_destroy` - -Releases one object reference. Called by the wrapper's `close()` (and by the finalizer backstop). - -#### Callback Interfaces - -##### `ReadyListener` - -Consumer-implemented observer of ready items - -Implemented in JS as an object with the methods below. Passed as an `i32` context key plus an `i32` pointer to the static `weaveffi_kitchen_ReadyListener_vtable` vtable; the vtable's trailing `free(ctx)` entry releases the JS object. - -| Method | Parameters | Returns | -|--------|------------|---------| -| `onReady` | `code: number`, `msg: string` | `void` | -| `onItem` | `item: Item`, `gadget: Gadget` | `boolean` | - -#### Structs - -##### `Item` - -Kitchen sink struct exercising every field feature - -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` | -| `count` | `i32` | -| `enabled` | `bool` | -| `ratio` | `f64` | -| `nick` | `u32` | -| `payload` | `bytes` | -| `tags` | `[string]` | -| `attrs` | `{string:string}` | -| `parent` | `i64?` | -| `token` | `Token` | -| `priority` | `Priority` | -| `gadget` | `Gadget?` | - -#### Enums - -##### `Priority` - -Task priority levels - -Passed as `i32` discriminant. - -| Variant | Value | -|---------|-------| -| `Low` | `0` | -| `Normal` | `1` | -| `High` | `2` | +await init(); +``` diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_d_ts.snap new file mode 100644 index 00000000..be649b18 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_d_ts.snap @@ -0,0 +1,175 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class KitchenSinkError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends KitchenSinkError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof KitchenSinkError; +/** + * Load the WebAssembly module and check that it matches these bindings. + * Every other export throws until the returned promise resolves. + */ +export declare function init( + source?: string | URL | BufferSource | WebAssembly.Module | Response, +): Promise; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** Trivial cross-module helper */ +export declare namespace shared { + /** A reusable token referenced across modules */ + export interface Token { + id: bigint; + label: string; + } + /** Trivial cross-module helper */ + export function ping(): string; +} + +/** Identity for bool */ +export declare namespace kitchen { + /** The errors of the `kitchen` error domain. */ + export class KitchenErrorsError extends $Error { + constructor(code: number, message?: string); + } + /** Item not found */ + export class NotFoundError extends kitchen.KitchenErrorsError { + static readonly CODE: 1; + constructor(message?: string); + } + /** Invalid input */ + export class InvalidInputError extends kitchen.KitchenErrorsError { + static readonly CODE: 2; + constructor(message?: string); + } + /** Task priority levels */ + export enum Priority { + /** Low priority */ + Low = 0, + /** Normal priority */ + Normal = 1, + /** High priority */ + High = 2, + } + /** Kitchen sink struct exercising every field feature */ + export interface Item { + /** Stable identifier */ + id: bigint; + /** Display name */ + name: string; + /** Initial count */ + count: number; + /** Whether the item is enabled */ + enabled: boolean; + /** Scaling ratio */ + ratio: number; + nick: number; + payload: Uint8Array; + tags: string[]; + attrs: Record; + parent: bigint | null; + /** Cross-module struct reference */ + token: shared.Token; + priority: kitchen.Priority; + /** An optional object inside a record */ + gadget: kitchen.Gadget | null; + } + /** Consumer-implemented observer of ready items */ + export interface ReadyListener { + /** Fires when an item is ready */ + onReady(code: number, msg: string): void; + /** Receives the item itself and says whether to keep listening */ + onItem(item: kitchen.Item, gadget: kitchen.Gadget): boolean; + } + /** A pocket-sized interface exercising the object surface */ + export class Gadget { + /** Create a gadget with the given id */ + constructor(id: bigint); + /** Render the gadget as a human-readable string */ + describe(): string; + /** + * Poke the gadget a number of times, failing on a negative count + * @throws {kitchen.KitchenErrorsError} + */ + poke(times: number): number; + /** The gadget subsystem version string */ + static version(): string; + /** + * Release this wrapper's reference to the native object. Safe to call + * more than once; a wrapper that is never closed is released when it is + * garbage collected. Using the wrapper after `close()` throws. + */ + close(): void; + /** Alias of `close()` for `using` declarations. */ + [Symbol.dispose](): void; + } + /** Identity for bool */ + export function boolId(x: boolean): boolean; + export function i32Id(x: number): number; + export function u32Id(x: number): number; + export function i64Id(x: bigint): bigint; + export function f64Id(x: number): number; + export function echoString(s: string): string; + export function echoBytes(b: Uint8Array): Uint8Array; + /** Register a listener, returning how many are attached */ + export function subscribe(listener: kitchen.ReadyListener): number; + /** Optional object in both directions */ + export function maybeGadget(fallback: kitchen.Gadget | null): kitchen.Gadget | null; + /** A list of objects */ + export function gadgets(): kitchen.Gadget[]; + /** An iterator over objects */ + export function streamGadgets(): IterableIterator; + /** + * Optional struct return + * @throws {kitchen.KitchenErrorsError} + */ + export function maybeItem(id: bigint): kitchen.Item | null; + /** List of structs */ + export function listItems(): kitchen.Item[]; + /** Map return type */ + export function getAttrs(): Record; + /** Iterator return type */ + export function streamItems(): IterableIterator; + /** Returns the shared Token type from another module */ + export function crossModuleToken(): shared.Token; + /** Async operation */ + export function doAsync(input: string): Promise; + /** Cancellable async operation */ + export function doCancellable(input: string, options?: { signal?: AbortSignal }): Promise; + /** + * Legacy operation kept for compatibility + * @deprecated Use new_op instead + */ + export function legacyOp(): number; + /** Replacement for legacy_op */ + export function newOp(): number; + /** Trivial nested-module function */ + export namespace nested { + /** Trivial nested-module function */ + export function hello(): string; + } +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_js.snap new file mode 100644 index 00000000..0a1347ca --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__index_js.snap @@ -0,0 +1,633 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + KitchenSinkError as $Error, +} from './runtime.js'; +export { KitchenSinkError, CancelledError } from './runtime.js'; +import { $loadWasm, $unloaded } from './linear.js'; + +let $raw = $unloaded('kitchen_sink'); +// Handles are linear-memory addresses; object tokens are u64s. +const $token = Number; +let $loading = null; + +/** + * Load the WebAssembly module (by default `kitchen_sink.wasm` next to this file, or + * the file named by `KITCHEN_SINK_LIBRARY` on Node.js) and check that it matches + * these bindings. Every other export throws until the returned promise + * resolves. + */ +export function init(source) { + $loading ??= $loadWasm(source, 'kitchen_sink', 'KITCHEN_SINK_LIBRARY', new URL('./kitchen_sink.wasm', import.meta.url)) + .then((m) => { + $raw = $bind(m); + try { + $verify($raw, 'kitchen_sink', 'kitchen_sink', 3, [['shared', 0x42c4ce2c8d0af052n], ['kitchen', 0xd83da75b24b545ffn]]); + } catch (e) { + $raw = $unloaded('kitchen_sink'); + throw e; + } + }) + .catch((e) => { + $loading = null; + throw e; + }); + return $loading; +} + +function $vt_kitchen_sink_kitchen_ReadyListener(m) { + return [ + [['i32', 'i32', 'i32', 'i32', 'i32'], [], 'viiiii', (ctx, p0, p1, p2, err) => { + try { + m.adapter(ctx).on_ready(p0, m.readStr(p1, p2)); + } catch (e) { + m.foreign(err, e); + } + }], + [['i32', 'i32', 'i32', 'i32', 'i32'], ['i32'], 'iiiiii', (ctx, p0, p1, p2, err) => { + try { + return m.adapter(ctx).on_item(m.readData(p0, p1), p2) ? 1 : 0; + } catch (e) { + m.foreign(err, e); + return 0; + } + }], + ]; +} + +// The raw entry points, one per C symbol. +function $bind(m) { + const x = m.x; + return { + kitchen_sink_abi_version: () => x.kitchen_sink_abi_version() >>> 0, + kitchen_sink_debug_live: (kind) => BigInt.asUintN(64, x.kitchen_sink_debug_live(m.num(kind))), + kitchen_sink_cancel_token_create: () => x.kitchen_sink_cancel_token_create(), + kitchen_sink_cancel_token_cancel: (t) => x.kitchen_sink_cancel_token_cancel(m.handle(t)), + kitchen_sink_cancel_token_destroy: (t) => x.kitchen_sink_cancel_token_destroy(m.handle(t)), + kitchen_sink_shared_checksum: () => BigInt.asUintN(64, x.kitchen_sink_shared_checksum()), + kitchen_sink_kitchen_checksum: () => BigInt.asUintN(64, x.kitchen_sink_kitchen_checksum()), + kitchen_sink_shared_ping: () => { + const r = x.kitchen_sink_shared_ping(m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + }, + kitchen_sink_kitchen_Gadget_clone: (h) => x.kitchen_sink_kitchen_Gadget_clone(m.handle(h)), + kitchen_sink_kitchen_Gadget_destroy: (h) => x.kitchen_sink_kitchen_Gadget_destroy(m.handle(h)), + kitchen_sink_kitchen_bool_id: (a0) => { + const r = x.kitchen_sink_kitchen_bool_id(m.bool(a0), m.err); + m.check(); + return r !== 0; + }, + kitchen_sink_kitchen_i32_id: (a0) => { + const r = x.kitchen_sink_kitchen_i32_id(m.num(a0), m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_u32_id: (a0) => { + const r = x.kitchen_sink_kitchen_u32_id(m.num(a0), m.err); + m.check(); + return r >>> 0; + }, + kitchen_sink_kitchen_i64_id: (a0) => { + const r = x.kitchen_sink_kitchen_i64_id(m.i64(a0), m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_f64_id: (a0) => { + const r = x.kitchen_sink_kitchen_f64_id(m.num(a0), m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_echo_string: (a0) => { + let s0 = null; + try { + s0 = m.str(a0); + const r = x.kitchen_sink_kitchen_echo_string(s0[0], s0[1], m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + } finally { + m.unstage(s0); + } + }, + kitchen_sink_kitchen_echo_bytes: (a0) => { + let s0 = null; + try { + s0 = m.data(a0); + const r = x.kitchen_sink_kitchen_echo_bytes(s0[0], s0[1], m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + } finally { + m.unstage(s0); + } + }, + kitchen_sink_kitchen_subscribe: (a0) => { + const r = x.kitchen_sink_kitchen_subscribe(m.register(a0), m.vtable('kitchen_sink_kitchen_ReadyListener', () => $vt_kitchen_sink_kitchen_ReadyListener(m)), m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_maybe_gadget: (a0) => { + const r = x.kitchen_sink_kitchen_maybe_gadget(m.handleOpt(a0), m.err); + m.check(); + return r === 0 ? null : r; + }, + kitchen_sink_kitchen_gadgets: () => { + const r = x.kitchen_sink_kitchen_gadgets(m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + kitchen_sink_kitchen_stream_gadgets: () => { + const r = x.kitchen_sink_kitchen_stream_gadgets(m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_StreamGadgetsIterator_next: (h) => { + const has = x.kitchen_sink_kitchen_StreamGadgetsIterator_next(m.handle(h), m.item, m.err); + m.check(); + if (has === 0) return undefined; + const p = m.view().getUint32(m.item, true); + return p; + }, + kitchen_sink_kitchen_StreamGadgetsIterator_destroy: (h) => x.kitchen_sink_kitchen_StreamGadgetsIterator_destroy(m.handle(h)), + kitchen_sink_kitchen_maybe_item: (a0) => { + const r = x.kitchen_sink_kitchen_maybe_item(m.i64(a0), m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + kitchen_sink_kitchen_list_items: () => { + const r = x.kitchen_sink_kitchen_list_items(m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + kitchen_sink_kitchen_get_attrs: () => { + const r = x.kitchen_sink_kitchen_get_attrs(m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + kitchen_sink_kitchen_stream_items: () => { + const r = x.kitchen_sink_kitchen_stream_items(m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_StreamItemsIterator_next: (h) => { + const has = x.kitchen_sink_kitchen_StreamItemsIterator_next(m.handle(h), m.item, m.len, m.err); + m.check(); + if (has === 0) return undefined; + const p = m.view().getUint32(m.item, true); + return m.takeData(p, m.outLen()); + }, + kitchen_sink_kitchen_StreamItemsIterator_destroy: (h) => x.kitchen_sink_kitchen_StreamItemsIterator_destroy(m.handle(h)), + kitchen_sink_kitchen_cross_module_token: () => { + const r = x.kitchen_sink_kitchen_cross_module_token(m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + kitchen_sink_kitchen_do_async: (a0) => { + let s0 = null; + try { + s0 = m.str(a0); + return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.kitchen_sink_kitchen_do_async(s0[0], s0[1], cb, ctx), (result_ptr, result_len) => m.takeStr(result_ptr, result_len)); + } finally { + m.unstage(s0); + } + }, + kitchen_sink_kitchen_do_cancellable: (a0, token) => { + let s0 = null; + try { + s0 = m.str(a0); + return m.launch(['i32', 'i32', 'i32', 'i32'], [], 'viiii', (cb, ctx) => x.kitchen_sink_kitchen_do_cancellable(s0[0], s0[1], m.handleOpt(token), cb, ctx), (result_ptr, result_len) => m.takeStr(result_ptr, result_len)); + } finally { + m.unstage(s0); + } + }, + kitchen_sink_kitchen_legacy_op: () => { + const r = x.kitchen_sink_kitchen_legacy_op(m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_new_op: () => { + const r = x.kitchen_sink_kitchen_new_op(m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_Gadget_new: (a0) => { + const r = x.kitchen_sink_kitchen_Gadget_new(m.i64(a0), m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_Gadget_describe: (self) => { + const r = x.kitchen_sink_kitchen_Gadget_describe(m.handle(self), m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + }, + kitchen_sink_kitchen_Gadget_poke: (self, a0) => { + const r = x.kitchen_sink_kitchen_Gadget_poke(m.handle(self), m.num(a0), m.err); + m.check(); + return r; + }, + kitchen_sink_kitchen_Gadget_version: () => { + const r = x.kitchen_sink_kitchen_Gadget_version(m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + }, + kitchen_sink_kitchen_nested_hello: () => { + const r = x.kitchen_sink_kitchen_nested_hello(m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + }, + }; +} + +// The native cancel tokens behind `AbortSignal`s. +const $tokens = { + create: () => $raw.kitchen_sink_cancel_token_create(), + cancel: (t) => $raw.kitchen_sink_cancel_token_cancel(t), + destroy: (t) => $raw.kitchen_sink_cancel_token_destroy(t), +}; + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.kitchen_sink_debug_live(kind); +} + +const kitchen$KitchenErrorsError = class KitchenErrorsError extends $Error {}; +const kitchen$NotFoundError = class NotFoundError extends kitchen$KitchenErrorsError { + static CODE = 1; + constructor(message = 'Item not found') { + super(1, message); + } +}; +const kitchen$InvalidInputError = class InvalidInputError extends kitchen$KitchenErrorsError { + static CODE = 2; + constructor(message = 'Invalid input') { + super(2, message); + } +}; +const $codes$kitchen$KitchenErrors = new Map([[1, kitchen$NotFoundError], [2, kitchen$InvalidInputError]]); +function $from$kitchen$KitchenErrors(e) { + return $domain(e, $codes$kitchen$KitchenErrors); +} + +const kitchen$Priority = Object.freeze({ + Low: 0, + Normal: 1, + High: 2, + 0: 'Low', + 1: 'Normal', + 2: 'High', +}); + +function $w$shared$Token(w, v) { + w.writeI64(v.id); + w.writeString(v.label); +} +function $r$shared$Token(r) { + return { + id: r.readI64(), + label: r.readString(), + }; +} +function $w$kitchen$Item(w, v) { + w.writeI64(v.id); + w.writeString(v.name); + w.writeI32(v.count); + w.writeBool(v.enabled); + w.writeF64(v.ratio); + w.writeU32(v.nick); + w.writeBytes(v.payload); + w.writeList(v.tags, $W.String); + w.writeMap(v.attrs, $W.String, $W.String); + w.writeOpt(v.parent, $W.I64); + $w$shared$Token(w, v.token); + w.writeI32(v.priority); + w.writeOpt(v.gadget, (w, v) => w.writeU64(BigInt($clone(v, kitchen$Gadget)))); +} +function $r$kitchen$Item(r) { + return { + id: r.readI64(), + name: r.readString(), + count: r.readI32(), + enabled: r.readBool(), + ratio: r.readF64(), + nick: r.readU32(), + payload: r.readBytes(), + tags: r.readList($R.String), + attrs: r.readMap($R.String, $R.String), + parent: r.readOpt($R.I64), + token: $r$shared$Token(r), + priority: r.readI32(), + gadget: r.readOpt((r) => $adopt(kitchen$Gadget, $token(r.readU64()))), + }; +} + +const shared$ping = function ping() { + try { + return $raw.kitchen_sink_shared_ping(); + } catch ($e) { + throw $fault($e); + } +}; + +function $adapt$kitchen$ReadyListener(impl) { + $impl(impl, 'ReadyListener'); + return { + on_ready($a0, $a1) { + impl.onReady($a0, $a1); + }, + on_item($a0, $a1) { + return $ret.Bool(impl.onItem($decode($a0, $r$kitchen$Item), $adopt(kitchen$Gadget, $a1)), 'ReadyListener.onItem'); + }, + }; +} + +const kitchen$Gadget = class Gadget extends $Object { + constructor(id) { + super(); + try { + $own(this, $raw.kitchen_sink_kitchen_Gadget_new(id)); + } catch ($e) { + throw $fault($e); + } + } + static $destroy(h) { + $raw.kitchen_sink_kitchen_Gadget_destroy(h); + } + static $clone(h) { + return $raw.kitchen_sink_kitchen_Gadget_clone(h); + } + describe() { + const $self = $lend(this, kitchen$Gadget); + try { + return $raw.kitchen_sink_kitchen_Gadget_describe($self); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(this); + } + } + poke(times) { + const $self = $lend(this, kitchen$Gadget); + try { + return $raw.kitchen_sink_kitchen_Gadget_poke($self, times); + } catch ($e) { + throw $from$kitchen$KitchenErrors($e); + } finally { + $unlend(this); + } + } + static version() { + try { + return $raw.kitchen_sink_kitchen_Gadget_version(); + } catch ($e) { + throw $fault($e); + } + } +}; + +const kitchen$boolId = function boolId(x) { + try { + return $raw.kitchen_sink_kitchen_bool_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$i32Id = function i32Id(x) { + try { + return $raw.kitchen_sink_kitchen_i32_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$u32Id = function u32Id(x) { + try { + return $raw.kitchen_sink_kitchen_u32_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$i64Id = function i64Id(x) { + try { + return $raw.kitchen_sink_kitchen_i64_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$f64Id = function f64Id(x) { + try { + return $raw.kitchen_sink_kitchen_f64_id(x); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$echoString = function echoString(s) { + try { + return $raw.kitchen_sink_kitchen_echo_string(s); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$echoBytes = function echoBytes(b) { + try { + return $raw.kitchen_sink_kitchen_echo_bytes(b); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$subscribe = function subscribe(listener) { + try { + return $raw.kitchen_sink_kitchen_subscribe($adapt$kitchen$ReadyListener(listener)); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$maybeGadget = function maybeGadget(fallback) { + const $o0 = $lendOpt(fallback, kitchen$Gadget); + try { + return $adoptOpt(kitchen$Gadget, $raw.kitchen_sink_kitchen_maybe_gadget($o0)); + } catch ($e) { + throw $fault($e); + } finally { + $unlend(fallback); + } +}; + +const kitchen$gadgets = function gadgets() { + try { + return $decode($raw.kitchen_sink_kitchen_gadgets(), (r) => r.readList((r) => $adopt(kitchen$Gadget, $token(r.readU64())))); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$kitchen$stream_gadgets = { + next: (h) => $raw.kitchen_sink_kitchen_StreamGadgetsIterator_next(h), + destroy: (h) => $raw.kitchen_sink_kitchen_StreamGadgetsIterator_destroy(h), + map: $fault, + convert: (v) => $adopt(kitchen$Gadget, v), +}; +const kitchen$streamGadgets = function streamGadgets() { + try { + return new $Iterator($raw.kitchen_sink_kitchen_stream_gadgets(), $it$kitchen$stream_gadgets); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$maybeItem = function maybeItem(id) { + try { + return $decode($raw.kitchen_sink_kitchen_maybe_item(id), (r) => r.readOpt($r$kitchen$Item)); + } catch ($e) { + throw $from$kitchen$KitchenErrors($e); + } +}; + +const kitchen$listItems = function listItems() { + try { + return $decode($raw.kitchen_sink_kitchen_list_items(), (r) => r.readList($r$kitchen$Item)); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$getAttrs = function getAttrs() { + try { + return $decode($raw.kitchen_sink_kitchen_get_attrs(), (r) => r.readMap($R.String, $R.I32)); + } catch ($e) { + throw $fault($e); + } +}; + +const $it$kitchen$stream_items = { + next: (h) => $raw.kitchen_sink_kitchen_StreamItemsIterator_next(h), + destroy: (h) => $raw.kitchen_sink_kitchen_StreamItemsIterator_destroy(h), + map: $fault, + convert: (v) => $decode(v, $r$kitchen$Item), +}; +const kitchen$streamItems = function streamItems() { + try { + return new $Iterator($raw.kitchen_sink_kitchen_stream_items(), $it$kitchen$stream_items); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$crossModuleToken = function crossModuleToken() { + try { + return $decode($raw.kitchen_sink_kitchen_cross_module_token(), $r$shared$Token); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$doAsync = async function doAsync(input) { + try { + return await $raw.kitchen_sink_kitchen_do_async(input); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$doCancellable = async function doCancellable(input, $options) { + try { + return await $cancellable($options?.signal, $tokens, ($t) => $raw.kitchen_sink_kitchen_do_cancellable(input, $t)); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$legacyOp = function legacyOp() { + try { + return $raw.kitchen_sink_kitchen_legacy_op(); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$newOp = function newOp() { + try { + return $raw.kitchen_sink_kitchen_new_op(); + } catch ($e) { + throw $fault($e); + } +}; + +const kitchen$nested$hello = function hello() { + try { + return $raw.kitchen_sink_kitchen_nested_hello(); + } catch ($e) { + throw $fault($e); + } +}; + +export const shared = Object.freeze({ + ping: shared$ping, +}); + +export const kitchen = Object.freeze({ + KitchenErrorsError: kitchen$KitchenErrorsError, + NotFoundError: kitchen$NotFoundError, + InvalidInputError: kitchen$InvalidInputError, + Priority: kitchen$Priority, + Gadget: kitchen$Gadget, + boolId: kitchen$boolId, + i32Id: kitchen$i32Id, + u32Id: kitchen$u32Id, + i64Id: kitchen$i64Id, + f64Id: kitchen$f64Id, + echoString: kitchen$echoString, + echoBytes: kitchen$echoBytes, + subscribe: kitchen$subscribe, + maybeGadget: kitchen$maybeGadget, + gadgets: kitchen$gadgets, + streamGadgets: kitchen$streamGadgets, + maybeItem: kitchen$maybeItem, + listItems: kitchen$listItems, + getAttrs: kitchen$getAttrs, + streamItems: kitchen$streamItems, + crossModuleToken: kitchen$crossModuleToken, + doAsync: kitchen$doAsync, + doCancellable: kitchen$doCancellable, + legacyOp: kitchen$legacyOp, + newOp: kitchen$newOp, + nested: Object.freeze({ + hello: kitchen$nested$hello, + }), +}); + +// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__linear_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__linear_js.snap new file mode 100644 index 00000000..608de11a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__linear_js.snap @@ -0,0 +1,403 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The WebAssembly transport: loading the module, staging arguments in its +// linear memory, reading results and errors back, and installing JavaScript +// functions in its function table (async completions and callback +// interface vtables). The generated `index.js` builds one native entry point +// per C symbol on top of this. + +import { $Fault } from './runtime.js'; + +const $enc = new TextEncoder(); +const $dec = new TextDecoder(); + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v, bits, what) { + let b; + if (typeof v === 'bigint') { + b = v; + } else if (typeof v === 'number' && Number.isInteger(v)) { + b = BigInt(v); + } else { + throw $expected('a bigint', v); + } + if (bits(64, b) !== b) throw new RangeError(`bigint does not fit in ${what} 64-bit integer`); + return b; +} + +/** + * One loaded module's linear memory and runtime symbols. `x` holds the + * module's exports by C symbol name; `memory()` returns the current + * `ArrayBuffer`; `fnptr(params, results, sig, fn)` installs `fn` in the + * function table and returns its index. + */ +export class $Linear { + constructor(x, prefix, memory, fnptr) { + this.x = x; + this.memory = memory; + this.fnptr = fnptr; + this.allocFn = x[`${prefix}_alloc`]; + this.deallocFn = x[`${prefix}_dealloc`]; + this.freeBytesFn = x[`${prefix}_free_bytes`]; + this.errorClearFn = x[`${prefix}_error_clear`]; + this.errorFreeFn = x[`${prefix}_error_free`]; + this.errorSetFn = x[`${prefix}_error_set`]; + if (typeof this.allocFn !== 'function' || typeof this.deallocFn !== 'function') { + throw new Error(`the WebAssembly module does not export ${prefix}_alloc and ${prefix}_dealloc`); + } + // One scratch block reused by every call: the error slot (16 bytes), + // the out_len slot (4), and the iterator out_item slot (8). The producer + // only writes them as a call returns, so a call made from a callback in + // the middle of another call can reuse them safely. + const scratch = this.allocFn(32); + this.bytes().fill(0, scratch, scratch + 32); + this.err = scratch; + this.len = scratch + 16; + this.item = scratch + 24; + this.callbacks = new Map(); + this.nextCallback = 1; + this.pending = new Map(); + this.nextPending = 1; + this.completions = new Map(); + this.vtables = new Map(); + } + + bytes() { + const buffer = this.memory(); + if (buffer !== this.buffer) { + this.buffer = buffer; + this.u8 = new Uint8Array(buffer); + this.dv = new DataView(buffer); + } + return this.u8; + } + + view() { + this.bytes(); + return this.dv; + } + + // Argument checks and conversions for by-value slots. + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + i64(v) { + return $bigint(v, BigInt.asIntN, 'a signed'); + } + + u64(v) { + return $bigint(v, BigInt.asUintN, 'an unsigned'); + } + + bool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + return v ? 1 : 0; + } + + handle(v) { + if (typeof v !== 'number' || v === 0) throw $expected('an object handle', v); + return v; + } + + handleOpt(v) { + return v === null || v === undefined ? 0 : this.handle(v); + } + + // Staging: copy a string or bytes into linear memory for one call. A + // staged value is [ptr, len, size]; release it with unstage(). + str(s) { + if (typeof s !== 'string') throw $expected('a string', s); + if (s.length === 0) return [0, 0, 0]; + const size = s.length * 3; + const ptr = this.allocFn(size); + const { written } = $enc.encodeInto(s, this.bytes().subarray(ptr, ptr + size)); + return [ptr, written, size]; + } + + data(b) { + if (!(b instanceof Uint8Array)) throw $expected('a Uint8Array', b); + if (b.length === 0) return [0, 0, 0]; + const ptr = this.allocFn(b.length); + this.bytes().set(b, ptr); + return [ptr, b.length, b.length]; + } + + unstage(staged) { + if (staged !== null && staged[2] !== 0) this.deallocFn(staged[0], staged[2]); + } + + // Results. + outLen() { + return this.view().getUint32(this.len, true); + } + + readStr(ptr, len) { + return len === 0 ? '' : $dec.decode(this.bytes().subarray(ptr, ptr + len)); + } + + readData(ptr, len) { + return len === 0 ? new Uint8Array(0) : this.bytes().slice(ptr, ptr + len); + } + + takeStr(ptr, len) { + const s = this.readStr(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return s; + } + + takeData(ptr, len) { + const b = this.readData(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return b; + } + + cstr(ptr) { + if (ptr === 0) return ''; + const u8 = this.bytes(); + let end = ptr; + while (u8[end] !== 0) end++; + return $dec.decode(u8.subarray(ptr, end)); + } + + fault(errPtr) { + const dv = this.view(); + const code = dv.getInt32(errPtr, true); + if (code === 0) return null; + const payloadPtr = dv.getUint32(errPtr + 8, true); + const payloadLen = dv.getUint32(errPtr + 12, true); + return new $Fault( + code, + this.cstr(dv.getUint32(errPtr + 4, true)), + payloadPtr === 0 ? null : this.bytes().slice(payloadPtr, payloadPtr + payloadLen), + ); + } + + /** Throw the error the last call wrote to the scratch error slot. */ + check() { + if (this.view().getInt32(this.err, true) === 0) return; + const fault = this.fault(this.err); + this.errorClearFn(this.err); + throw fault; + } + + // Callback interfaces: implementations live in a table keyed by the + // integer passed as `ctx`, until the producer calls the vtable's `free`. + register(adapter) { + const id = this.nextCallback++; + this.callbacks.set(id, adapter); + return id; + } + + adapter(id) { + const adapter = this.callbacks.get(id); + if (adapter === undefined) throw new Error('callback implementation was already released'); + return adapter; + } + + unregister(id) { + this.callbacks.delete(id); + } + + /** Report a callback implementation's exception through `errPtr` (code -4). */ + foreign(errPtr, e) { + const message = e instanceof Error ? e.message || e.name : String(e); + const bytes = $enc.encode(message); + const ptr = this.allocFn(bytes.length + 1); + const u8 = this.bytes(); + u8.set(bytes, ptr); + u8[ptr + bytes.length] = 0; + this.errorSetFn(errPtr, -4, ptr); + this.deallocFn(ptr, bytes.length + 1); + } + + /** + * The static vtable of one callback interface: one table entry per + * method (`methods()` returns each as `[params, results, sig, fn]`), then + * `free`. Built on first use. + */ + vtable(name, methods) { + let ptr = this.vtables.get(name); + if (ptr !== undefined) return ptr; + const entries = [...methods(), [['i32'], [], 'vi', (ctx) => this.unregister(ctx)]]; + ptr = this.allocFn(entries.length * 4); + const dv = this.view(); + entries.forEach(([params, results, sig, fn], i) => { + dv.setUint32(ptr + i * 4, this.fnptr(params, results, sig, fn), true); + }); + this.vtables.set(name, ptr); + return ptr; + } + + /** + * Launch an async call. `call(callback, context)` invokes the launcher; + * the completion (one table entry per signature, shared by every call) + * settles the returned promise, converting the result slots with + * `convert`. On this single-threaded target the completion normally + * fires before the launcher returns. + */ + launch(params, results, sig, call, convert) { + let callback = this.completions.get(sig); + if (callback === undefined) { + const complete = (ctx, errPtr, ...result) => this.complete(ctx, errPtr, result); + callback = this.fnptr(params, results, sig, complete); + this.completions.set(sig, callback); + } + const id = this.nextPending++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject, convert }); + try { + call(callback, id); + } catch (e) { + this.pending.delete(id); + reject(e); + } + }); + } + + complete(id, errPtr, result) { + const pending = this.pending.get(id); + if (pending === undefined) return; + this.pending.delete(id); + try { + const fault = errPtr === 0 ? null : this.fault(errPtr); + if (errPtr !== 0) this.errorFreeFn(errPtr); + if (fault !== null) { + pending.reject(fault); + } else { + pending.resolve(pending.convert(...result)); + } + } catch (e) { + pending.reject(e); + } + } +} + +/** A stand-in for the native entry points until `init()` has finished. */ +export function $unloaded(library) { + return new Proxy( + {}, + { + get() { + throw new Error(`${library}: call init() before using the bindings`); + }, + }, + ); +} + +const $valtype = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c }; + +function $leb(n) { + const out = []; + do { + let b = n & 0x7f; + n >>>= 7; + if (n !== 0) b |= 0x80; + out.push(b); + } while (n !== 0); + return out; +} + +function $section(id, body) { + return [id, ...$leb(body.length), ...body]; +} + +/** + * Turn a JavaScript function into a WebAssembly function with the given + * signature: `WebAssembly.Function` where the engine has it, otherwise a + * tiny module that imports `fn` and re-exports it. + */ +function $wasmFunction(params, results, fn) { + if (typeof WebAssembly.Function === 'function') { + return new WebAssembly.Function({ parameters: params, results }, fn); + } + const type = [0x60, ...$leb(params.length), ...params.map((t) => $valtype[t])]; + type.push(...$leb(results.length), ...results.map((t) => $valtype[t])); + const bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + ...$section(1, [1, ...type]), + ...$section(2, [1, 1, 0x65, 1, 0x66, 0x00, 0x00]), + ...$section(7, [1, 1, 0x66, 0x00, 0x00]), + ]); + const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), { e: { f: fn } }); + return instance.exports.f; +} + +async function $bytesOf(source, envVar, fallback) { + if (source === undefined || source === null) { + const env = typeof process === 'object' && process.env ? process.env[envVar] : undefined; + if (env) { + const { readFile } = await import('node:fs/promises'); + return readFile(env); + } + source = fallback; + } + if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { + return source; + } + if (typeof Response === 'function' && source instanceof Response) { + return source.arrayBuffer(); + } + const url = source instanceof URL ? source : new URL(String(source), fallback); + if (url.protocol === 'file:') { + const { readFile } = await import('node:fs/promises'); + return readFile(url); + } + const response = await fetch(url); + if (!response.ok) throw new Error(`failed to fetch ${url}: ${response.status}`); + return response.arrayBuffer(); +} + +/** + * Instantiate a `wasm32-unknown-unknown` module from `source` (a URL, a + * path, bytes, a `Response`, or a compiled module). Without a source it + * reads the file named by the `envVar` environment variable (on Node.js), + * else `fallback`. The module must export its memory and a growable + * function table (`__indirect_function_table`). + */ +export async function $loadWasm(source, prefix, envVar, fallback) { + const bytes = await $bytesOf(source, envVar, fallback); + const module = bytes instanceof WebAssembly.Module ? bytes : await WebAssembly.compile(bytes); + const { exports } = await WebAssembly.instantiate(module, {}); + const table = exports.__indirect_function_table; + const fnptr = (params, results, sig, fn) => { + if (!(table instanceof WebAssembly.Table)) { + throw new Error('the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)'); + } + const index = table.grow(1); + table.set(index, $wasmFunction(params, results, fn)); + return index; + }; + return new $Linear(exports, prefix, () => exports.memory.buffer, fnptr); +} + +/** + * Adopt an initialized Emscripten module (or the promise its `MODULARIZE` + * factory returns). Its exports carry a leading underscore, its memory is + * read through `HEAPU8`, and table entries are installed with + * `addFunction`, so it must be linked with `-sALLOW_TABLE_GROWTH`, + * `-sWASM_BIGINT`, and `-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`. + */ +export async function $loadEmscripten(module, prefix, symbols) { + const m = await module; + const x = {}; + for (const symbol of symbols) x[symbol] = m[`_${symbol}`]; + const fnptr = (params, results, sig, fn) => { + if (typeof m.addFunction !== 'function') { + throw new Error('the Emscripten module does not export addFunction (link with -sALLOW_TABLE_GROWTH and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8)'); + } + return m.addFunction(fn, sig); + }; + return new $Linear(x, prefix, () => m.HEAPU8.buffer, fnptr); +} + +// END linear.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__package_json.snap index cee4ead6..f2e1b29d 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__package_json.snap @@ -2,13 +2,25 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "kitchen_sink", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for kitchen_sink", "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" + "main": "index.js", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "linear.js" + ] } diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__runtime_js.snap new file mode 100644 index 00000000..e11da31e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class KitchenSinkError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends KitchenSinkError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new KitchenSinkError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new KitchenSinkError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new KitchenSinkError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new KitchenSinkError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js 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 deleted file mode 100644 index c0b3853e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,249 +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, producer - * panics, and callback-interface implementations that raised (code -4). - * Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** Base error for the `kitchen` module's error domain. */ -export declare class KitchenErrorsError extends WeaveFFIError { - static readonly NotFound: typeof NotFound; - static readonly InvalidInput: typeof InvalidInput; -} - -/** Item not found */ -export declare class NotFound extends KitchenErrorsError { - constructor(message?: string); - static readonly CODE: 1; -} - -/** Invalid input */ -export declare class InvalidInput extends KitchenErrorsError { - constructor(message?: string); - static readonly CODE: 2; -} - -/** A reusable token referenced across modules */ -export interface Token { - id: bigint; - label: string; -} - -/** Kitchen sink struct exercising every field feature */ -export interface Item { - /** Stable identifier */ - id: bigint; - /** Display name */ - name: string; - /** Initial count */ - count: number; - /** Whether the item is enabled */ - enabled: boolean; - /** Scaling ratio */ - ratio: number; - nick: number; - payload: Uint8Array; - tags: string[]; - attrs: Record; - parent: bigint | null; - /** Cross-module struct reference */ - token: Token; - priority: Priority; - /** An optional object inside a record */ - gadget: Gadget | null; -} - -/** Task priority levels */ -export declare const Priority: Readonly<{ - /** Low priority */ - Low: 0; - /** Normal priority */ - Normal: 1; - /** High priority */ - High: 2; -}>; -export type Priority = (typeof Priority)[keyof typeof Priority]; - -/** - * Consumer-implemented observer of ready items - * - * Implemented by the consumer; the producer calls these methods synchronously. An exception thrown from a method is reported to the producer as an error (code -4). - */ -export interface ReadyListener { - /** - * Fires when an item is ready - */ - onReady(code: number, msg: string): void; - /** - * Receives the item itself and says whether to keep listening - */ - onItem(item: Item, gadget: Gadget): boolean; -} - -/** - * A pocket-sized interface exercising the object surface - * - * Wraps one reference to a producer object; release it with `close()`. - */ -export declare class Gadget { - /** - * Create a gadget with the given id - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @throws {WeaveFFIError} if the native call fails - */ - constructor(id: bigint); - /** - * Render the gadget as a human-readable string - * @throws {WeaveFFIError} if the native call fails - */ - describe(): string; - /** - * Poke the gadget a number of times, failing on a negative count - * @throws {KitchenErrorsError} on a domain error code - */ - poke(times: number): number; - /** - * The gadget subsystem version string - * @throws {WeaveFFIError} if the native call fails - */ - static version(): string; - /** Releases this wrapper's reference exactly once; later calls are - * no-ops. Also reachable as `[Symbol.dispose]()` for `using` - * declarations. A wrapper collected unclosed is released by a - * finalizer where the runtime provides one. */ - close(): void; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - shared: { - /** - * Trivial cross-module helper - * @throws {WeaveFFIError} if the native call fails - */ - ping(): string; - }; - kitchen: { - /** - * Identity for bool - * @throws {WeaveFFIError} if the native call fails - */ - boolId(x: boolean): boolean; - /** - * @throws {WeaveFFIError} if the native call fails - */ - i32Id(x: number): number; - /** - * @throws {WeaveFFIError} if the native call fails - */ - u32Id(x: number): number; - /** - * @throws {WeaveFFIError} if the native call fails - */ - i64Id(x: bigint): bigint; - /** - * @throws {WeaveFFIError} if the native call fails - */ - f64Id(x: number): number; - /** - * @throws {WeaveFFIError} if the native call fails - */ - echoString(s: string): string; - /** - * @throws {WeaveFFIError} if the native call fails - */ - echoBytes(b: Uint8Array): Uint8Array; - /** - * Register a listener, returning how many are attached - * @throws {WeaveFFIError} if the native call fails - */ - subscribe(listener: ReadyListener): number; - /** - * Optional object in both directions - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @throws {WeaveFFIError} if the native call fails - */ - maybeGadget(fallback: Gadget | null): Gadget | null; - /** - * A list of objects - * @throws {WeaveFFIError} if the native call fails - */ - gadgets(): Gadget[]; - /** - * An iterator over objects - * @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 - */ - streamGadgets(): IterableIterator; - /** - * Optional struct return - * @throws {KitchenErrorsError} on a domain error code - */ - maybeItem(id: bigint): Item | null; - /** - * List of structs - * @throws {WeaveFFIError} if the native call fails - */ - listItems(): Item[]; - /** - * Map return type - * @throws {WeaveFFIError} if the native call fails - */ - getAttrs(): Record; - /** - * Iterator return type - * @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 - */ - streamItems(): IterableIterator; - /** - * Returns the shared Token type from another module - * @throws {WeaveFFIError} if the native call fails - */ - crossModuleToken(): Token; - /** - * Async operation - * @throws {WeaveFFIError} if the native call fails - */ - doAsync(input: string): Promise; - /** - * Cancellable async operation - * @throws {WeaveFFIError} if the native call fails - */ - doCancellable(input: string): Promise; - /** - * Legacy operation kept for compatibility - * @deprecated Use new_op instead - * @throws {WeaveFFIError} if the native call fails - */ - legacyOp(): number; - /** - * Replacement for legacy_op - * @throws {WeaveFFIError} if the native call fails - */ - newOp(): number; - Gadget: typeof Gadget; - nested: { - /** - * Trivial nested-module function - * @throws {WeaveFFIError} if the native call fails - */ - hello(): string; - }; - }; -} - -export function loadWeaveffiWasm(source: string | URL | BufferSource | WebAssembly.Module): Promise; - -// END weaveffi_wasm.d.ts 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 deleted file mode 100644 index b89df651..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap +++ /dev/null @@ -1,1087 +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); one -// strong reference per wrapper, released by close() -// 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 -// Callbacks -> i32 context key + i32 pointer to a static vtable - -// The ABI revision this glue was generated against. -const _ABI_VERSION = 2; - -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, producer - * panics, and callback-interface implementations that raised (code -4). - * 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 `kitchen` module's error domain. */ -export class KitchenErrorsError extends WeaveFFIError {} - -// Item not found -export class NotFound extends KitchenErrorsError { - constructor(message = "Item not found") { - super(1, message); - } -} -NotFound.CODE = 1; -KitchenErrorsError.NotFound = NotFound; - -// Invalid input -export class InvalidInput extends KitchenErrorsError { - constructor(message = "Invalid input") { - super(2, message); - } -} -InvalidInput.CODE = 2; -KitchenErrorsError.InvalidInput = InvalidInput; - -const _KITCHEN_ERRORS_ERROR_CODES = Object.freeze({ - 1: NotFound, - 2: InvalidInput, -}); - -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); } - // An object token: the pointer (a wasm32 i32, read unsigned) widened to u64. - obj(ptr) { this.u64(BigInt(ptr >>> 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; - } - // An object token narrowed back to a wasm32 pointer; zero is never a - // valid token (an absent `Interface?` uses the option flag instead). - obj() { - const t = this.u64(); - if (t === 0n || t > 0xffffffffn) this._bad('object token out of range'); - return Number(t); - } - 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 (-2), a marshalling failure (-3), or a callback -// interface implementation that raised (-4), surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - _pendingForeign = null; - 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) { - _pendingForeign = null; - wasm.weaveffi_dealloc(errPtr, 16); -} - -// The message of a callback-interface failure a trampoline reported through -// out_err, held until the producer call on the stack returns (every error -// checker and _freeErr clear it) so later callbacks in that call are refused. -let _pendingForeign = null; - -// Translate an exception raised while a producer call was on the stack. -// wasm32-unknown-unknown has no unwinding runtime: a producer panic executes -// `unreachable`, so the engine throws a WebAssembly.RuntimeError instead of -// the producer writing out_err. A trap that follows a callback-interface -// failure is labelled with that failure (-4); any other trap is a producer -// panic (-2). Non-trap exceptions pass through unchanged. The producer -// frames between the trap and the call are not unwound, so a lock held -// across a callback stays locked; a producer that snapshots its state -// before calling out stays usable. -function _trapError(e) { - const foreign = _pendingForeign; - _pendingForeign = null; - if (!(e instanceof WebAssembly.RuntimeError)) return e; - if (foreign !== null) return new WeaveFFIError(-4, foreign); - return new WeaveFFIError(-2, 'producer panicked: ' + e.message); -} - -// Release the error slot a failed call never filled, then translate. -function _trap(wasm, errPtr, e) { - _freeErr(wasm, errPtr); - return _trapError(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) { - _pendingForeign = null; - 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; -} - -// `using` declarations (explicit resource management) call this symbol; -// runtimes that predate it get the registered stand-in so the method is -// still reachable by name. -const _dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); - -// Finalization backstop: a wrapper that is garbage collected without -// close() still releases its reference. The held value is the -// [destroy, handle] pair (never the wrapper, which would keep it alive); -// the wrapper is the unregister token so close() disarms the backstop and -// destroy runs exactly once either way. Absent on runtimes without -// FinalizationRegistry, where close() is the only release path. -const _finalizer = typeof FinalizationRegistry === 'function' - ? new FinalizationRegistry(([destroy, handle]) => destroy(handle)) - : null; - -// Bind one strong reference to a wrapper and arm the backstop. -function _adopt(obj, handle, destroy) { - obj._handle = handle; - obj._destroy = destroy; - if (_finalizer !== null) _finalizer.register(obj, [destroy, handle], obj); - return obj; -} - -// Release a wrapper's reference exactly once; later calls are no-ops. -function _release(obj) { - if (!obj._handle) return; - if (_finalizer !== null) _finalizer.unregister(obj); - obj._destroy(obj._handle); - obj._handle = 0; -} - -// The pointer of a live wrapper, lent to the producer for one call (the -// wrapper keeps its own reference). A closed or non-object argument is a -// consumer programming error, reported with the marshalling code. -function _borrow(obj) { - if (obj === null || obj === undefined || !obj._handle) { - throw new WeaveFFIError(-3, 'expected a live object wrapper'); - } - return obj._handle; -} - -// Report a callback-interface failure through the trampoline's out_err -// slot as FOREIGN_ERROR_CODE (-4). weaveffi_error_set copies the message -// with the producer's allocator, so the staged C string is released as -// soon as it returns. The message is parked in _pendingForeign until the -// producer call on the stack returns: the producer can't unwind on wasm32, -// so it keeps running on the trampoline's default return value, and any -// further callback it makes during that call is refused with this error -// instead of reaching the implementation again. -function _reportForeign(wasm, errPtr, msg) { - if (_pendingForeign === null) _pendingForeign = msg; - const [p, s] = _cstr(wasm, msg); - wasm.weaveffi_error_set(errPtr, -4, p); - wasm.weaveffi_dealloc(p, s); -} - -// Report an exception thrown by a callback-interface implementation. -function _setForeignError(wasm, errPtr, e) { - _reportForeign(wasm, errPtr, e instanceof Error ? (e.message || e.name) : String(e)); -} - -// 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); - } catch (e) { - // A trap: the slot was never filled, so release it here. - this._close(); - throw _trap(wasm, _err, e); - } - try { - // 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, resultTypes, handler) { - const idx = table.grow(1); - table.set(idx, new WebAssembly.Function( - { parameters: paramTypes, results: resultTypes }, - handler - )); - return idx; -} - -export const Priority = Object.freeze({ - Low: 0, - Normal: 1, - High: 2, -}); - -/** - * Load a WeaveFFI Wasm module. - * - * @param {string|URL|BufferSource|WebAssembly.Module} source - A URL to - * fetch the `.wasm` from, its bytes, or an already compiled module - * (use the bytes form where `fetch` cannot read files, e.g. Node). - * @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 }); - * - * // Object: a class wrapping one producer reference; close() releases it - * // (or let `using` do it). Passing it lends the reference for the call. - * const store = new api.kv.Store(); - * api.kv.inspect(store); - * store.close(); - * - * // 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(source) { - let module; - if (source instanceof WebAssembly.Module) { - module = source; - } else if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { - module = await WebAssembly.compile(source); - } else { - const response = await fetch(source); - module = await WebAssembly.compile(await response.arrayBuffer()); - } - const instance = await WebAssembly.instantiate(module, {}); - const wasm = instance.exports; - - _checkAbiVersion(wasm); - - // Build the KitchenErrorsError subclass matching `code`, or a generic - // WeaveFFIError for codes outside the domain (panics, marshalling, - // foreign callback failures). - function _kitchenErrorsErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _KITCHEN_ERRORS_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; - } - - // Throw the `KitchenErrorsError` domain error (and free the slot) if the error slot - // carries a non-zero code. - function _checkKitchenErrorsError(wasm, errPtr) { - _pendingForeign = null; - 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 = _kitchenErrorsErrorFrom(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 `shared.Token` record into the value-buffer wire format. - function _write_shared_Token(w, v) { - w.i64(v.id); - w.str(v.label); - } - - // Decode a `shared.Token` record from the value-buffer wire format. - function _read_shared_Token(r) { - const v = {}; - v.id = r.i64(); - v.label = r.str(); - return v; - } - - // Serialize a `kitchen.Item` record into the value-buffer wire format. - function _write_kitchen_Item(w, v) { - w.i64(v.id); - w.str(v.name); - w.i32(v.count); - w.bool(v.enabled); - w.f64(v.ratio); - w.u32(v.nick); - w.bytes(v.payload); - const _a1 = v.tags || []; - w.len(_a1.length); - for (const _e1 of _a1) { - w.str(_e1); - } - const _s2 = v.attrs || {}; - 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); - } - if (v.parent === null || v.parent === undefined) { - w.flag(false); - } else { - w.flag(true); - w.i64(v.parent); - } - _write_shared_Token(w, v.token); - w.i32(v.priority); - if (v.gadget === null || v.gadget === undefined) { - w.flag(false); - } else { - w.flag(true); - w.obj(v.gadget._clone()); - } - } - - // Decode a `kitchen.Item` record from the value-buffer wire format. - function _read_kitchen_Item(r) { - const v = {}; - v.id = r.i64(); - v.name = r.str(); - v.count = r.i32(); - v.enabled = r.bool(); - v.ratio = r.f64(); - v.nick = r.u32(); - v.payload = r.bytes(); - v.tags = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(r.str()); return _arr; })(); - v.attrs = (() => { const _n = r.len(); const _obj = {}; for (let _i = 0; _i < _n; _i++) { const _k = r.str(); _obj[_k] = r.str(); } return _obj; })(); - v.parent = (r.flag() ? r.i64() : null); - v.token = _read_shared_Token(r); - v.priority = r.i32(); - v.gadget = (r.flag() ? Gadget._wrap(r.obj()) : null); - return v; - } - - 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 = _registerTrampoline(_table, ['i32', 'i32', 'i32'], [], _asyncHandler); - - // Callback-interface implementations, keyed by the integer context - // the loader passes as `ctx`; an entry lives until the producer - // calls the vtable's `free(ctx)`. - let _nextCbId = 1; - const _callbacks = new Map(); - - // Trampolines for the `ReadyListener` callback interface: one WebAssembly.Function - // per vtable entry, mirroring the C signature of each method. Borrowed - // arguments are decoded before the JS method runs; object arguments - // are adopted; a thrown exception becomes a -4 error on out_err. - const _cb_ReadyListener_on_ready = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32'], [], (_ctx, a0, a1, _err) => { - try { - const _impl = _callbacks.get(_ctx); - const _p0 = a0; - const _p1 = _readCStr(wasm, a1); - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return; - } - _impl.onReady(_p0, _p1); - } catch (e) { - _setForeignError(wasm, _err, e); - } - }); - const _cb_ReadyListener_on_item = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32', 'i32'], ['i32'], (_ctx, a0, a1, a2, _err) => { - try { - const _impl = _callbacks.get(_ctx); - 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 = _read_kitchen_Item(_p0_r); - _p0_r.end(); - const _p1 = Gadget._wrap(a2); - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return 0; - } - return _impl.onItem(_p0, _p1) ? 1 : 0; - } catch (e) { - _setForeignError(wasm, _err, e); - return 0; - } - }); - // `free(ctx)`: the producer dropped its last reference; forget the - // implementation so it can be collected. - const _cb_ReadyListener_free = _registerTrampoline(_table, ['i32'], [], (_ctx) => { _callbacks.delete(_ctx); }); - // The one static `weaveffi_kitchen_ReadyListener_vtable` for this instance, filled with the table indices - // above in declaration order, then `free`. - const _vtable_ReadyListener = wasm.weaveffi_alloc(12); - { - const _dv = new DataView(wasm.memory.buffer); - _dv.setUint32(_vtable_ReadyListener + 0, _cb_ReadyListener_on_ready, true); - _dv.setUint32(_vtable_ReadyListener + 4, _cb_ReadyListener_on_item, true); - _dv.setUint32(_vtable_ReadyListener + 8, _cb_ReadyListener_free, true); - } - - // A pocket-sized interface exercising the object surface - class Gadget { - constructor(id) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_Gadget_new(BigInt(id), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - _adopt(this, _r, wasm.weaveffi_kitchen_Gadget_destroy); - } - static _wrap(handle) { - return _adopt(Object.create(Gadget.prototype), handle, wasm.weaveffi_kitchen_Gadget_destroy); - } - _clone() { - return wasm.weaveffi_kitchen_Gadget_clone(_borrow(this)); - } - close() { - _release(this); - } - [_dispose]() { - _release(this); - } - describe() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_Gadget_describe(_borrow(this), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - } - poke(times) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_Gadget_poke(_borrow(this), times, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkKitchenErrorsError(wasm, _err); - _freeErr(wasm, _err); - return _r; - } - static version() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_Gadget_version(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - } - } - - return { - _raw: wasm, - shared: { - ping() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_shared_ping(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - }, - }, - kitchen: { - boolId(x) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_bool_id(x ? 1 : 0, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - }, - i32Id(x) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_i32_id(x, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - u32Id(x) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_u32_id(x, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r >>> 0; - }, - i64Id(x) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_i64_id(BigInt(x), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - f64Id(x) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_f64_id(x, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - echoString(s) { - const [a0_p, a0_s] = _cstr(wasm, s); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_echo_string(a0_p, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_s); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - }, - echoBytes(b) { - const [a0_p, a0_l] = _bytes(wasm, b); - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_echo_bytes(a0_p, a0_l, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } finally { - 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); - return _takeBytes(wasm, _r, _len); - }, - subscribe(listener) { - const a0_ctx = _nextCbId++; - _callbacks.set(a0_ctx, listener); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_subscribe(a0_ctx, _vtable_ReadyListener, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - maybeGadget(fallback) { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_maybe_gadget((fallback === null || fallback === undefined ? 0 : _borrow(fallback)), _err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r === 0 ? null : Gadget._wrap(_r); - }, - gadgets() { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_gadgets(_lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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(Gadget._wrap(_rd.obj())); return _arr; })(); - _rd.end(); - return _out; - }, - streamGadgets() { - const _err = _allocErr(wasm); - let _it; - try { - _it = wasm.weaveffi_kitchen_stream_gadgets(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return new _WeaveFFIIterator(wasm, _it, 4, - (it, slot, ep) => wasm.weaveffi_kitchen_StreamGadgetsIterator_next(it, slot, ep), - (it) => wasm.weaveffi_kitchen_StreamGadgetsIterator_destroy(it), - _checkErr, (w, p) => Gadget._wrap(new DataView(w.memory.buffer).getUint32(p, true))); - }, - maybeItem(id) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_maybe_item(BigInt(id), _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _checkKitchenErrorsError(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_kitchen_Item(_rd) : null); - _rd.end(); - return _out; - }, - listItems() { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_list_items(_lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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_kitchen_Item(_rd)); return _arr; })(); - _rd.end(); - return _out; - }, - getAttrs() { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_get_attrs(_lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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 _obj = {}; for (let _i = 0; _i < _n; _i++) { const _k = _rd.str(); _obj[_k] = _rd.i32(); } return _obj; })(); - _rd.end(); - return _out; - }, - streamItems() { - const _err = _allocErr(wasm); - let _it; - try { - _it = wasm.weaveffi_kitchen_stream_items(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return new _WeaveFFIIterator(wasm, _it, 8, - (it, slot, ep) => wasm.weaveffi_kitchen_StreamItemsIterator_next(it, slot, slot + 4, ep), - (it) => wasm.weaveffi_kitchen_StreamItemsIterator_destroy(it), - _checkErr, (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_kitchen_Item(_rd); _rd.end(); return _v; }); - }, - crossModuleToken() { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_cross_module_token(_lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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_shared_Token(_rd); - _rd.end(); - return _out; - }, - doAsync(input) { - 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, input); - try { - wasm.weaveffi_kitchen_do_async_async(a0_p, _cbPtr_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_s); - } - }); - }, - doCancellable(input) { - 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, input); - try { - wasm.weaveffi_kitchen_do_cancellable_async(a0_p, 0, _cbPtr_i32_i32_i32, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_s); - } - }); - }, - /** @deprecated Use new_op instead */ - legacyOp() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_legacy_op(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - newOp() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_new_op(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - Gadget: Gadget, - nested: { - hello() { - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kitchen_nested_hello(_err); - } catch (e) { - throw _trap(wasm, _err, e); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - }, - }, - }, - }; -} - -// 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 cabfd935..4b5e06a1 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 @@ -1,135 +1,20 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Wasm (experimental) +# nested_modules -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. +Native bindings for nested_modules -Build (example): +WebAssembly bindings for a `wasm32-unknown-unknown` build of the `nested_modules` library, linked with `--export-table --growable-table` (for example `RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table"`). `init()` loads `nested_modules.wasm` from next to this package, or from the path in `NESTED_MODULES_LIBRARY` on Node.js; pass a URL, bytes, or a compiled module to load it from elsewhere: -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it: the loader accepts a URL to fetch, the module's bytes (for Node, where `fetch` cannot read files), or an already compiled `WebAssembly.Module`. - -## 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. - -### Objects - -Interfaces are **wrapper classes** holding one reference to a producer object (an `i32` pointer into linear memory). A wrapper you receive is yours: release it with `close()` (or bind it with `using`, which calls `Symbol.dispose`); a wrapper collected unclosed is released by a `FinalizationRegistry` backstop where the runtime provides one, and destroy runs exactly once either way. Passing a wrapper to a function lends its reference for the duration of the call. Inside a value buffer an object crosses as a `u64` token carrying one reference: the writer clones the wrapper's reference and the reader adopts the token into a new wrapper. - -### 64-bit Integers - -`i64` and `u64` are JavaScript **`bigint`** values everywhere: as arguments, returns, record fields, and callback parameters. Map keys of these types become string property names on the decoded object. A `number` is accepted where a `bigint` is expected (it's widened with `BigInt(v)`, so a fractional value throws a `RangeError`). Values crossing by value follow the wasm calling convention: an out-of-range `bigint` wraps modulo 2^64 rather than throwing, and unsigned results are read back as unsigned (`u64` via `BigInt.asUintN`, `u32` via `>>> 0`). - -### 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) and maps to `Wrapper | null`. - -### 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_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_error_set(err_ptr: i32, code: i32, msg: i32)`: fill an error slot (used to report callback exceptions) -- `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, marshalling failures, and callback exceptions -(code `-4`). 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. - -#### Callback failures without unwinding +```js +import { init, graphics } from 'nested_modules'; -`wasm32-unknown-unknown` builds with `panic = "abort"`, so when a callback interface method throws, the producer can't unwind out of the call the way native targets do. Instead the runtime records the failure, the producer's code runs to completion on the method's default return value (`0`, `0n`, or `false`), and the thunk then reports code `-4` with the exception's message in place of the result, so the wrapper throws (or the Promise rejects with) the same `WeaveFFIError` a native build would produce. While that producer call is still on the stack, the glue refuses every further callback invocation with the same error rather than calling your implementation again, so your code observes the failure exactly as it would under unwinding. Producers must tolerate a callback returning its default once (a well-behaved producer already does, because a consumer could return that value on purpose). - -#### Traps - -A producer panic can't be caught on the Rust side either: it executes `unreachable`, and the engine throws a `WebAssembly.RuntimeError` out of the call. The glue translates that trap for you: the wrapper throws (or the Promise rejects with) a `WeaveFFIError` with code `-2` (`producer panicked`), or code `-4` if a callback method had already failed during the call. The error slot and every staged argument are released either way. The producer frames between the trap and the call are *not* unwound, so their cleanup does not run: a lock held across the panic stays locked, and allocations those frames owned leak. - -## API Reference - -### Module: `graphics` - -#### Error Domain: `GraphicsError` - -Throwing wrappers in this module raise `GraphicsError` (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 | -|-------|------|-----------------|----------------| -| `OutOfBounds` | `1` | coordinate out of bounds | (none) | - -#### Functions - -##### `weaveffi_graphics_new_canvas` - -Construct a Canvas. - -`weaveffi_graphics_new_canvas(width: i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `width` | `i32` | `i32` | native Wasm i32 | -| _returns_ | `Canvas` | `i32, i32` | value buffer: ptr + len in linear memory | - -#### Structs - -##### `Canvas` - -Top-level struct that references a struct defined one level deeper. - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `width` | `i32` | -| `root` | `Shape` | - -#### Enums - -##### `Unit` - -A measurement unit shared with nested submodules. - -Passed as `i32` discriminant. - -| Variant | Value | -|---------|-------| -| `pixels` | `0` | -| `points` | `1` | +await init(); +``` diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_d_ts.snap new file mode 100644 index 00000000..093fc136 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_d_ts.snap @@ -0,0 +1,93 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class NestedModulesError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends NestedModulesError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof NestedModulesError; +/** + * Load the WebAssembly module and check that it matches these bindings. + * Every other export throws until the returned promise resolves. + */ +export declare function init( + source?: string | URL | BufferSource | WebAssembly.Module | Response, +): Promise; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** Construct a Canvas. */ +export declare namespace graphics { + /** The errors of the `graphics` error domain. */ + export class GraphicsError extends $Error { + constructor(code: number, message?: string); + } + /** coordinate out of bounds */ + export class OutOfBoundsError extends graphics.GraphicsError { + static readonly CODE: 1; + constructor(message?: string); + } + /** A measurement unit shared with nested submodules. */ + export enum Unit { + pixels = 0, + points = 1, + } + /** Top-level struct that references a struct defined one level deeper. */ + export interface Canvas { + width: number; + /** Reference down into a nested module */ + root: graphics.shapes.Shape; + } + /** Construct a Canvas. */ + export function newCanvas(width: number): graphics.Canvas; + /** Construct a Shape. */ + export namespace shapes { + /** A struct one level deep that references both an ancestor enum and a deeper struct. */ + export interface Shape { + sides: number; + /** Reference up to the parent module's enum */ + unit: graphics.Unit; + /** Reference down into a deeper module */ + detail: graphics.shapes.geometry.Detail; + } + /** Construct a Shape. */ + export function makeShape(sides: number): graphics.shapes.Shape; + /** Nested function returning a struct owned by an ancestor module. */ + export function describe(): graphics.Canvas; + /** Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. */ + export namespace geometry { + /** A struct two levels deep. */ + export interface Detail { + area: number; + } + /** + * Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. + * @throws {graphics.GraphicsError} + */ + export function compute(shape: graphics.shapes.Shape): graphics.shapes.geometry.Detail; + } + } +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_js.snap new file mode 100644 index 00000000..466a0d96 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__index_js.snap @@ -0,0 +1,207 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + NestedModulesError as $Error, +} from './runtime.js'; +export { NestedModulesError, CancelledError } from './runtime.js'; +import { $loadWasm, $unloaded } from './linear.js'; + +let $raw = $unloaded('nested_modules'); +// Handles are linear-memory addresses; object tokens are u64s. +const $token = Number; +let $loading = null; + +/** + * Load the WebAssembly module (by default `nested_modules.wasm` next to this file, or + * the file named by `NESTED_MODULES_LIBRARY` on Node.js) and check that it matches + * these bindings. Every other export throws until the returned promise + * resolves. + */ +export function init(source) { + $loading ??= $loadWasm(source, 'nested_modules', 'NESTED_MODULES_LIBRARY', new URL('./nested_modules.wasm', import.meta.url)) + .then((m) => { + $raw = $bind(m); + try { + $verify($raw, 'nested_modules', 'nested_modules', 3, [['graphics', 0x44d31b5b10fdb90dn]]); + } catch (e) { + $raw = $unloaded('nested_modules'); + throw e; + } + }) + .catch((e) => { + $loading = null; + throw e; + }); + return $loading; +} + +// The raw entry points, one per C symbol. +function $bind(m) { + const x = m.x; + return { + nested_modules_abi_version: () => x.nested_modules_abi_version() >>> 0, + nested_modules_debug_live: (kind) => BigInt.asUintN(64, x.nested_modules_debug_live(m.num(kind))), + nested_modules_cancel_token_create: () => x.nested_modules_cancel_token_create(), + nested_modules_cancel_token_cancel: (t) => x.nested_modules_cancel_token_cancel(m.handle(t)), + nested_modules_cancel_token_destroy: (t) => x.nested_modules_cancel_token_destroy(m.handle(t)), + nested_modules_graphics_checksum: () => BigInt.asUintN(64, x.nested_modules_graphics_checksum()), + nested_modules_graphics_new_canvas: (a0) => { + const r = x.nested_modules_graphics_new_canvas(m.num(a0), m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + nested_modules_graphics_shapes_make_shape: (a0) => { + const r = x.nested_modules_graphics_shapes_make_shape(m.num(a0), m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + nested_modules_graphics_shapes_describe: () => { + const r = x.nested_modules_graphics_shapes_describe(m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + nested_modules_graphics_shapes_geometry_compute: (a0) => { + let s0 = null; + try { + s0 = m.data(a0); + const r = x.nested_modules_graphics_shapes_geometry_compute(s0[0], s0[1], m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + } finally { + m.unstage(s0); + } + }, + }; +} + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.nested_modules_debug_live(kind); +} + +const graphics$GraphicsError = class GraphicsError extends $Error {}; +const graphics$OutOfBoundsError = class OutOfBoundsError extends graphics$GraphicsError { + static CODE = 1; + constructor(message = 'coordinate out of bounds') { + super(1, message); + } +}; +const $codes$graphics$GraphicsError = new Map([[1, graphics$OutOfBoundsError]]); +function $from$graphics$GraphicsError(e) { + return $domain(e, $codes$graphics$GraphicsError); +} + +const graphics$Unit = Object.freeze({ + pixels: 0, + points: 1, + 0: 'pixels', + 1: 'points', +}); + +function $w$graphics$Canvas(w, v) { + w.writeI32(v.width); + $w$graphics$shapes$Shape(w, v.root); +} +function $r$graphics$Canvas(r) { + return { + width: r.readI32(), + root: $r$graphics$shapes$Shape(r), + }; +} +function $w$graphics$shapes$Shape(w, v) { + w.writeI32(v.sides); + w.writeI32(v.unit); + $w$graphics$shapes$geometry$Detail(w, v.detail); +} +function $r$graphics$shapes$Shape(r) { + return { + sides: r.readI32(), + unit: r.readI32(), + detail: $r$graphics$shapes$geometry$Detail(r), + }; +} +function $w$graphics$shapes$geometry$Detail(w, v) { + w.writeF64(v.area); +} +function $r$graphics$shapes$geometry$Detail(r) { + return { + area: r.readF64(), + }; +} + +const graphics$newCanvas = function newCanvas(width) { + try { + return $decode($raw.nested_modules_graphics_new_canvas(width), $r$graphics$Canvas); + } catch ($e) { + throw $fault($e); + } +}; + +const graphics$shapes$makeShape = function makeShape(sides) { + try { + return $decode($raw.nested_modules_graphics_shapes_make_shape(sides), $r$graphics$shapes$Shape); + } catch ($e) { + throw $fault($e); + } +}; + +const graphics$shapes$describe = function describe() { + try { + return $decode($raw.nested_modules_graphics_shapes_describe(), $r$graphics$Canvas); + } catch ($e) { + throw $fault($e); + } +}; + +const graphics$shapes$geometry$compute = function compute(shape) { + try { + return $decode($raw.nested_modules_graphics_shapes_geometry_compute($encode(shape, $w$graphics$shapes$Shape)), $r$graphics$shapes$geometry$Detail); + } catch ($e) { + throw $from$graphics$GraphicsError($e); + } +}; + +export const graphics = Object.freeze({ + GraphicsError: graphics$GraphicsError, + OutOfBoundsError: graphics$OutOfBoundsError, + Unit: graphics$Unit, + newCanvas: graphics$newCanvas, + shapes: Object.freeze({ + makeShape: graphics$shapes$makeShape, + describe: graphics$shapes$describe, + geometry: Object.freeze({ + compute: graphics$shapes$geometry$compute, + }), + }), +}); + +// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__linear_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__linear_js.snap new file mode 100644 index 00000000..608de11a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__linear_js.snap @@ -0,0 +1,403 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The WebAssembly transport: loading the module, staging arguments in its +// linear memory, reading results and errors back, and installing JavaScript +// functions in its function table (async completions and callback +// interface vtables). The generated `index.js` builds one native entry point +// per C symbol on top of this. + +import { $Fault } from './runtime.js'; + +const $enc = new TextEncoder(); +const $dec = new TextDecoder(); + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v, bits, what) { + let b; + if (typeof v === 'bigint') { + b = v; + } else if (typeof v === 'number' && Number.isInteger(v)) { + b = BigInt(v); + } else { + throw $expected('a bigint', v); + } + if (bits(64, b) !== b) throw new RangeError(`bigint does not fit in ${what} 64-bit integer`); + return b; +} + +/** + * One loaded module's linear memory and runtime symbols. `x` holds the + * module's exports by C symbol name; `memory()` returns the current + * `ArrayBuffer`; `fnptr(params, results, sig, fn)` installs `fn` in the + * function table and returns its index. + */ +export class $Linear { + constructor(x, prefix, memory, fnptr) { + this.x = x; + this.memory = memory; + this.fnptr = fnptr; + this.allocFn = x[`${prefix}_alloc`]; + this.deallocFn = x[`${prefix}_dealloc`]; + this.freeBytesFn = x[`${prefix}_free_bytes`]; + this.errorClearFn = x[`${prefix}_error_clear`]; + this.errorFreeFn = x[`${prefix}_error_free`]; + this.errorSetFn = x[`${prefix}_error_set`]; + if (typeof this.allocFn !== 'function' || typeof this.deallocFn !== 'function') { + throw new Error(`the WebAssembly module does not export ${prefix}_alloc and ${prefix}_dealloc`); + } + // One scratch block reused by every call: the error slot (16 bytes), + // the out_len slot (4), and the iterator out_item slot (8). The producer + // only writes them as a call returns, so a call made from a callback in + // the middle of another call can reuse them safely. + const scratch = this.allocFn(32); + this.bytes().fill(0, scratch, scratch + 32); + this.err = scratch; + this.len = scratch + 16; + this.item = scratch + 24; + this.callbacks = new Map(); + this.nextCallback = 1; + this.pending = new Map(); + this.nextPending = 1; + this.completions = new Map(); + this.vtables = new Map(); + } + + bytes() { + const buffer = this.memory(); + if (buffer !== this.buffer) { + this.buffer = buffer; + this.u8 = new Uint8Array(buffer); + this.dv = new DataView(buffer); + } + return this.u8; + } + + view() { + this.bytes(); + return this.dv; + } + + // Argument checks and conversions for by-value slots. + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + i64(v) { + return $bigint(v, BigInt.asIntN, 'a signed'); + } + + u64(v) { + return $bigint(v, BigInt.asUintN, 'an unsigned'); + } + + bool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + return v ? 1 : 0; + } + + handle(v) { + if (typeof v !== 'number' || v === 0) throw $expected('an object handle', v); + return v; + } + + handleOpt(v) { + return v === null || v === undefined ? 0 : this.handle(v); + } + + // Staging: copy a string or bytes into linear memory for one call. A + // staged value is [ptr, len, size]; release it with unstage(). + str(s) { + if (typeof s !== 'string') throw $expected('a string', s); + if (s.length === 0) return [0, 0, 0]; + const size = s.length * 3; + const ptr = this.allocFn(size); + const { written } = $enc.encodeInto(s, this.bytes().subarray(ptr, ptr + size)); + return [ptr, written, size]; + } + + data(b) { + if (!(b instanceof Uint8Array)) throw $expected('a Uint8Array', b); + if (b.length === 0) return [0, 0, 0]; + const ptr = this.allocFn(b.length); + this.bytes().set(b, ptr); + return [ptr, b.length, b.length]; + } + + unstage(staged) { + if (staged !== null && staged[2] !== 0) this.deallocFn(staged[0], staged[2]); + } + + // Results. + outLen() { + return this.view().getUint32(this.len, true); + } + + readStr(ptr, len) { + return len === 0 ? '' : $dec.decode(this.bytes().subarray(ptr, ptr + len)); + } + + readData(ptr, len) { + return len === 0 ? new Uint8Array(0) : this.bytes().slice(ptr, ptr + len); + } + + takeStr(ptr, len) { + const s = this.readStr(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return s; + } + + takeData(ptr, len) { + const b = this.readData(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return b; + } + + cstr(ptr) { + if (ptr === 0) return ''; + const u8 = this.bytes(); + let end = ptr; + while (u8[end] !== 0) end++; + return $dec.decode(u8.subarray(ptr, end)); + } + + fault(errPtr) { + const dv = this.view(); + const code = dv.getInt32(errPtr, true); + if (code === 0) return null; + const payloadPtr = dv.getUint32(errPtr + 8, true); + const payloadLen = dv.getUint32(errPtr + 12, true); + return new $Fault( + code, + this.cstr(dv.getUint32(errPtr + 4, true)), + payloadPtr === 0 ? null : this.bytes().slice(payloadPtr, payloadPtr + payloadLen), + ); + } + + /** Throw the error the last call wrote to the scratch error slot. */ + check() { + if (this.view().getInt32(this.err, true) === 0) return; + const fault = this.fault(this.err); + this.errorClearFn(this.err); + throw fault; + } + + // Callback interfaces: implementations live in a table keyed by the + // integer passed as `ctx`, until the producer calls the vtable's `free`. + register(adapter) { + const id = this.nextCallback++; + this.callbacks.set(id, adapter); + return id; + } + + adapter(id) { + const adapter = this.callbacks.get(id); + if (adapter === undefined) throw new Error('callback implementation was already released'); + return adapter; + } + + unregister(id) { + this.callbacks.delete(id); + } + + /** Report a callback implementation's exception through `errPtr` (code -4). */ + foreign(errPtr, e) { + const message = e instanceof Error ? e.message || e.name : String(e); + const bytes = $enc.encode(message); + const ptr = this.allocFn(bytes.length + 1); + const u8 = this.bytes(); + u8.set(bytes, ptr); + u8[ptr + bytes.length] = 0; + this.errorSetFn(errPtr, -4, ptr); + this.deallocFn(ptr, bytes.length + 1); + } + + /** + * The static vtable of one callback interface: one table entry per + * method (`methods()` returns each as `[params, results, sig, fn]`), then + * `free`. Built on first use. + */ + vtable(name, methods) { + let ptr = this.vtables.get(name); + if (ptr !== undefined) return ptr; + const entries = [...methods(), [['i32'], [], 'vi', (ctx) => this.unregister(ctx)]]; + ptr = this.allocFn(entries.length * 4); + const dv = this.view(); + entries.forEach(([params, results, sig, fn], i) => { + dv.setUint32(ptr + i * 4, this.fnptr(params, results, sig, fn), true); + }); + this.vtables.set(name, ptr); + return ptr; + } + + /** + * Launch an async call. `call(callback, context)` invokes the launcher; + * the completion (one table entry per signature, shared by every call) + * settles the returned promise, converting the result slots with + * `convert`. On this single-threaded target the completion normally + * fires before the launcher returns. + */ + launch(params, results, sig, call, convert) { + let callback = this.completions.get(sig); + if (callback === undefined) { + const complete = (ctx, errPtr, ...result) => this.complete(ctx, errPtr, result); + callback = this.fnptr(params, results, sig, complete); + this.completions.set(sig, callback); + } + const id = this.nextPending++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject, convert }); + try { + call(callback, id); + } catch (e) { + this.pending.delete(id); + reject(e); + } + }); + } + + complete(id, errPtr, result) { + const pending = this.pending.get(id); + if (pending === undefined) return; + this.pending.delete(id); + try { + const fault = errPtr === 0 ? null : this.fault(errPtr); + if (errPtr !== 0) this.errorFreeFn(errPtr); + if (fault !== null) { + pending.reject(fault); + } else { + pending.resolve(pending.convert(...result)); + } + } catch (e) { + pending.reject(e); + } + } +} + +/** A stand-in for the native entry points until `init()` has finished. */ +export function $unloaded(library) { + return new Proxy( + {}, + { + get() { + throw new Error(`${library}: call init() before using the bindings`); + }, + }, + ); +} + +const $valtype = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c }; + +function $leb(n) { + const out = []; + do { + let b = n & 0x7f; + n >>>= 7; + if (n !== 0) b |= 0x80; + out.push(b); + } while (n !== 0); + return out; +} + +function $section(id, body) { + return [id, ...$leb(body.length), ...body]; +} + +/** + * Turn a JavaScript function into a WebAssembly function with the given + * signature: `WebAssembly.Function` where the engine has it, otherwise a + * tiny module that imports `fn` and re-exports it. + */ +function $wasmFunction(params, results, fn) { + if (typeof WebAssembly.Function === 'function') { + return new WebAssembly.Function({ parameters: params, results }, fn); + } + const type = [0x60, ...$leb(params.length), ...params.map((t) => $valtype[t])]; + type.push(...$leb(results.length), ...results.map((t) => $valtype[t])); + const bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + ...$section(1, [1, ...type]), + ...$section(2, [1, 1, 0x65, 1, 0x66, 0x00, 0x00]), + ...$section(7, [1, 1, 0x66, 0x00, 0x00]), + ]); + const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), { e: { f: fn } }); + return instance.exports.f; +} + +async function $bytesOf(source, envVar, fallback) { + if (source === undefined || source === null) { + const env = typeof process === 'object' && process.env ? process.env[envVar] : undefined; + if (env) { + const { readFile } = await import('node:fs/promises'); + return readFile(env); + } + source = fallback; + } + if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { + return source; + } + if (typeof Response === 'function' && source instanceof Response) { + return source.arrayBuffer(); + } + const url = source instanceof URL ? source : new URL(String(source), fallback); + if (url.protocol === 'file:') { + const { readFile } = await import('node:fs/promises'); + return readFile(url); + } + const response = await fetch(url); + if (!response.ok) throw new Error(`failed to fetch ${url}: ${response.status}`); + return response.arrayBuffer(); +} + +/** + * Instantiate a `wasm32-unknown-unknown` module from `source` (a URL, a + * path, bytes, a `Response`, or a compiled module). Without a source it + * reads the file named by the `envVar` environment variable (on Node.js), + * else `fallback`. The module must export its memory and a growable + * function table (`__indirect_function_table`). + */ +export async function $loadWasm(source, prefix, envVar, fallback) { + const bytes = await $bytesOf(source, envVar, fallback); + const module = bytes instanceof WebAssembly.Module ? bytes : await WebAssembly.compile(bytes); + const { exports } = await WebAssembly.instantiate(module, {}); + const table = exports.__indirect_function_table; + const fnptr = (params, results, sig, fn) => { + if (!(table instanceof WebAssembly.Table)) { + throw new Error('the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)'); + } + const index = table.grow(1); + table.set(index, $wasmFunction(params, results, fn)); + return index; + }; + return new $Linear(exports, prefix, () => exports.memory.buffer, fnptr); +} + +/** + * Adopt an initialized Emscripten module (or the promise its `MODULARIZE` + * factory returns). Its exports carry a leading underscore, its memory is + * read through `HEAPU8`, and table entries are installed with + * `addFunction`, so it must be linked with `-sALLOW_TABLE_GROWTH`, + * `-sWASM_BIGINT`, and `-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`. + */ +export async function $loadEmscripten(module, prefix, symbols) { + const m = await module; + const x = {}; + for (const symbol of symbols) x[symbol] = m[`_${symbol}`]; + const fnptr = (params, results, sig, fn) => { + if (typeof m.addFunction !== 'function') { + throw new Error('the Emscripten module does not export addFunction (link with -sALLOW_TABLE_GROWTH and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8)'); + } + return m.addFunction(fn, sig); + }; + return new $Linear(x, prefix, () => m.HEAPU8.buffer, fnptr); +} + +// END linear.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__package_json.snap index cee4ead6..f5f96d6c 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__package_json.snap @@ -2,13 +2,25 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "nested_modules", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for nested_modules", "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" + "main": "index.js", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "linear.js" + ] } diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__runtime_js.snap new file mode 100644 index 00000000..97a25292 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class NestedModulesError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends NestedModulesError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new NestedModulesError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new NestedModulesError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new NestedModulesError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new NestedModulesError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js 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 deleted file mode 100644 index cc41fc66..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,90 +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, producer - * panics, and callback-interface implementations that raised (code -4). - * Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** Base error for the `graphics` module's error domain. */ -export declare class GraphicsError extends WeaveFFIError { - static readonly OutOfBounds: typeof OutOfBounds; -} - -/** coordinate out of bounds */ -export declare class OutOfBounds extends GraphicsError { - constructor(message?: string); - static readonly CODE: 1; -} - -/** Top-level struct that references a struct defined one level deeper. */ -export interface Canvas { - width: number; - /** Reference down into a nested module */ - root: Shape; -} - -/** A measurement unit shared with nested submodules. */ -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 { - sides: number; - /** Reference up to the parent module's enum */ - unit: Unit; - /** Reference down into a deeper module */ - detail: Detail; -} - -/** A struct two levels deep. */ -export interface Detail { - area: number; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - graphics: { - /** - * Construct a Canvas. - * @throws {WeaveFFIError} if the native call fails - */ - newCanvas(width: number): Canvas; - shapes: { - /** - * Construct a Shape. - * @throws {WeaveFFIError} if the native call fails - */ - makeShape(sides: number): Shape; - /** - * Nested function returning a struct owned by an ancestor module. - * @throws {WeaveFFIError} if the native call fails - */ - describe(): Canvas; - geometry: { - /** - * Deeper function taking an ancestor-module struct. Throws through the root module's inherited GraphicsError domain. - * @throws {GraphicsError} on a domain error code - */ - compute(shape: Shape): Detail; - }; - }; - }; -} - -export function loadWeaveffiWasm(source: string | URL | BufferSource | WebAssembly.Module): Promise; - -// END weaveffi_wasm.d.ts 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 deleted file mode 100644 index 3f3bda2e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_js.snap +++ /dev/null @@ -1,508 +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); one -// strong reference per wrapper, released by close() -// 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 -// Callbacks -> i32 context key + i32 pointer to a static vtable - -// The ABI revision this glue was generated against. -const _ABI_VERSION = 2; - -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, producer - * panics, and callback-interface implementations that raised (code -4). - * 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 `graphics` module's error domain. */ -export class GraphicsError extends WeaveFFIError {} - -// coordinate out of bounds -export class OutOfBounds extends GraphicsError { - constructor(message = "coordinate out of bounds") { - super(1, message); - } -} -OutOfBounds.CODE = 1; -GraphicsError.OutOfBounds = OutOfBounds; - -const _GRAPHICS_ERROR_CODES = Object.freeze({ - 1: OutOfBounds, -}); - -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); } - // An object token: the pointer (a wasm32 i32, read unsigned) widened to u64. - obj(ptr) { this.u64(BigInt(ptr >>> 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; - } - // An object token narrowed back to a wasm32 pointer; zero is never a - // valid token (an absent `Interface?` uses the option flag instead). - obj() { - const t = this.u64(); - if (t === 0n || t > 0xffffffffn) this._bad('object token out of range'); - return Number(t); - } - 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 (-2), a marshalling failure (-3), or a callback -// interface implementation that raised (-4), surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - _pendingForeign = null; - 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) { - _pendingForeign = null; - wasm.weaveffi_dealloc(errPtr, 16); -} - -// The message of a callback-interface failure a trampoline reported through -// out_err, held until the producer call on the stack returns (every error -// checker and _freeErr clear it) so later callbacks in that call are refused. -let _pendingForeign = null; - -// Translate an exception raised while a producer call was on the stack. -// wasm32-unknown-unknown has no unwinding runtime: a producer panic executes -// `unreachable`, so the engine throws a WebAssembly.RuntimeError instead of -// the producer writing out_err. A trap that follows a callback-interface -// failure is labelled with that failure (-4); any other trap is a producer -// panic (-2). Non-trap exceptions pass through unchanged. The producer -// frames between the trap and the call are not unwound, so a lock held -// across a callback stays locked; a producer that snapshots its state -// before calling out stays usable. -function _trapError(e) { - const foreign = _pendingForeign; - _pendingForeign = null; - if (!(e instanceof WebAssembly.RuntimeError)) return e; - if (foreign !== null) return new WeaveFFIError(-4, foreign); - return new WeaveFFIError(-2, 'producer panicked: ' + e.message); -} - -// Release the error slot a failed call never filled, then translate. -function _trap(wasm, errPtr, e) { - _freeErr(wasm, errPtr); - return _trapError(e); -} - -export const Unit = Object.freeze({ - pixels: 0, - points: 1, -}); - -/** - * Load a WeaveFFI Wasm module. - * - * @param {string|URL|BufferSource|WebAssembly.Module} source - A URL to - * fetch the `.wasm` from, its bytes, or an already compiled module - * (use the bytes form where `fetch` cannot read files, e.g. Node). - * @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 }); - * - * // Object: a class wrapping one producer reference; close() releases it - * // (or let `using` do it). Passing it lends the reference for the call. - * const store = new api.kv.Store(); - * api.kv.inspect(store); - * store.close(); - * - * // 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(source) { - let module; - if (source instanceof WebAssembly.Module) { - module = source; - } else if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { - module = await WebAssembly.compile(source); - } else { - const response = await fetch(source); - module = await WebAssembly.compile(await response.arrayBuffer()); - } - const instance = await WebAssembly.instantiate(module, {}); - const wasm = instance.exports; - - _checkAbiVersion(wasm); - - // Build the GraphicsError subclass matching `code`, or a generic - // WeaveFFIError for codes outside the domain (panics, marshalling, - // foreign callback failures). - function _graphicsErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _GRAPHICS_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; - } - - // Throw the `GraphicsError` domain error (and free the slot) if the error slot - // carries a non-zero code. - function _checkGraphicsError(wasm, errPtr) { - _pendingForeign = null; - 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 = _graphicsErrorFrom(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 `graphics.Canvas` record into the value-buffer wire format. - function _write_graphics_Canvas(w, v) { - w.i32(v.width); - _write_graphics_shapes_Shape(w, v.root); - } - - // Decode a `graphics.Canvas` record from the value-buffer wire format. - function _read_graphics_Canvas(r) { - const v = {}; - v.width = r.i32(); - v.root = _read_graphics_shapes_Shape(r); - return v; - } - - // Serialize a `graphics_shapes.Shape` record into the value-buffer wire format. - function _write_graphics_shapes_Shape(w, v) { - w.i32(v.sides); - w.i32(v.unit); - _write_graphics_shapes_geometry_Detail(w, v.detail); - } - - // Decode a `graphics_shapes.Shape` record from the value-buffer wire format. - function _read_graphics_shapes_Shape(r) { - const v = {}; - v.sides = r.i32(); - v.unit = r.i32(); - v.detail = _read_graphics_shapes_geometry_Detail(r); - return v; - } - - // Serialize a `graphics_shapes_geometry.Detail` record into the value-buffer wire format. - function _write_graphics_shapes_geometry_Detail(w, v) { - w.f64(v.area); - } - - // Decode a `graphics_shapes_geometry.Detail` record from the value-buffer wire format. - function _read_graphics_shapes_geometry_Detail(r) { - const v = {}; - v.area = r.f64(); - return v; - } - - return { - _raw: wasm, - graphics: { - newCanvas(width) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_graphics_new_canvas(width, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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_graphics_Canvas(_rd); - _rd.end(); - return _out; - }, - shapes: { - makeShape(sides) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_graphics_shapes_make_shape(sides, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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_graphics_shapes_Shape(_rd); - _rd.end(); - return _out; - }, - describe() { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_graphics_shapes_describe(_lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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_graphics_Canvas(_rd); - _rd.end(); - return _out; - }, - geometry: { - compute(shape) { - const a0_w = new _BufWriter(); - _write_graphics_shapes_Shape(a0_w, shape); - const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_graphics_shapes_geometry_compute(a0_p, a0_l, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_l); - } - _checkGraphicsError(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_graphics_shapes_geometry_Detail(_rd); - _rd.end(); - return _out; - }, - }, - }, - }, - }; -} - -// END weaveffi_wasm.js 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 c80505fc..6550cedf 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__README_md.snap @@ -1,193 +1,20 @@ --- source: crates/weaveffi-cli/tests/snapshots.rs --- - + - + -# WeaveFFI Wasm (experimental) +# shapes -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. +Native bindings for shapes -Build (example): +WebAssembly bindings for a `wasm32-unknown-unknown` build of the `shapes` library, linked with `--export-table --growable-table` (for example `RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table"`). `init()` loads `shapes.wasm` from next to this package, or from the path in `SHAPES_LIBRARY` on Node.js; pass a URL, bytes, or a compiled module to load it from elsewhere: -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it: the loader accepts a URL to fetch, the module's bytes (for Node, where `fetch` cannot read files), or an already compiled `WebAssembly.Module`. - -## 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. - -### Objects - -Interfaces are **wrapper classes** holding one reference to a producer object (an `i32` pointer into linear memory). A wrapper you receive is yours: release it with `close()` (or bind it with `using`, which calls `Symbol.dispose`); a wrapper collected unclosed is released by a `FinalizationRegistry` backstop where the runtime provides one, and destroy runs exactly once either way. Passing a wrapper to a function lends its reference for the duration of the call. Inside a value buffer an object crosses as a `u64` token carrying one reference: the writer clones the wrapper's reference and the reader adopts the token into a new wrapper. - -### 64-bit Integers - -`i64` and `u64` are JavaScript **`bigint`** values everywhere: as arguments, returns, record fields, and callback parameters. Map keys of these types become string property names on the decoded object. A `number` is accepted where a `bigint` is expected (it's widened with `BigInt(v)`, so a fractional value throws a `RangeError`). Values crossing by value follow the wasm calling convention: an out-of-range `bigint` wraps modulo 2^64 rather than throwing, and unsigned results are read back as unsigned (`u64` via `BigInt.asUintN`, `u32` via `>>> 0`). - -### 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) and maps to `Wrapper | null`. - -### 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_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_error_set(err_ptr: i32, code: i32, msg: i32)`: fill an error slot (used to report callback exceptions) -- `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, marshalling failures, and callback exceptions -(code `-4`). 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. - -#### Callback failures without unwinding - -`wasm32-unknown-unknown` builds with `panic = "abort"`, so when a callback interface method throws, the producer can't unwind out of the call the way native targets do. Instead the runtime records the failure, the producer's code runs to completion on the method's default return value (`0`, `0n`, or `false`), and the thunk then reports code `-4` with the exception's message in place of the result, so the wrapper throws (or the Promise rejects with) the same `WeaveFFIError` a native build would produce. While that producer call is still on the stack, the glue refuses every further callback invocation with the same error rather than calling your implementation again, so your code observes the failure exactly as it would under unwinding. Producers must tolerate a callback returning its default once (a well-behaved producer already does, because a consumer could return that value on purpose). - -#### Traps - -A producer panic can't be caught on the Rust side either: it executes `unreachable`, and the engine throws a `WebAssembly.RuntimeError` out of the call. The glue translates that trap for you: the wrapper throws (or the Promise rejects with) a `WeaveFFIError` with code `-2` (`producer panicked`), or code `-4` if a callback method had already failed during the call. The error slot and every staged argument are released either way. The producer frames between the trap and the call are *not* unwound, so their cleanup does not run: a lock held across the panic stays locked, and allocations those frames owned leak. - -## API Reference - -### Module: `shapes` - -#### Functions - -##### `weaveffi_shapes_describe` +```js +import { init, shapes } from 'shapes'; -Render a shape to a string - -`weaveffi_shapes_describe(shape: i32, i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `shape` | `Shape` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `string` | `i32` | NUL-terminated C string pointer | - -##### `weaveffi_shapes_scale` - -Scale a shape by a factor, returning a new shape - -`weaveffi_shapes_scale(shape: i32, i32, factor: f64) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `shape` | `Shape` | `i32, i32` | value buffer: ptr + len in linear memory | -| `factor` | `f64` | `f64` | native Wasm f64 | -| _returns_ | `Shape` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_shapes_sum_bytes` - -Sum a list of bytes into a wide integer (numerics smoke) - -`weaveffi_shapes_sum_bytes(values: i32, i32) -> i64` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `values` | `bytes` | `i32, i32` | ptr + len in linear memory | -| _returns_ | `u64` | `i64` | unsigned mapped to i64 | - -##### `weaveffi_shapes_make_numerics` - -Build a Numerics struct from every numeric primitive - -`weaveffi_shapes_make_numerics(a: i32, b: i32, c: i32, d: i64, e: i32, f: i32, g: i32, h: i64, i: f32, j: f64) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `a` | `i8` | `i32` | 8-bit signed mapped to i32 | -| `b` | `i16` | `i32` | 16-bit signed mapped to i32 | -| `c` | `i32` | `i32` | native Wasm i32 | -| `d` | `i64` | `i64` | native Wasm i64 | -| `e` | `u8` | `i32` | 8-bit unsigned mapped to i32 | -| `f` | `u16` | `i32` | 16-bit unsigned mapped to i32 | -| `g` | `u32` | `i32` | unsigned mapped to i32 | -| `h` | `u64` | `i64` | unsigned mapped to i64 | -| `i` | `f32` | `f32` | native Wasm f32 | -| `j` | `f64` | `f64` | native Wasm f64 | -| _returns_ | `Numerics` | `i32, i32` | value buffer: ptr + len in linear memory | - -#### Structs - -##### `Numerics` - -One field per supported numeric primitive - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `a` | `i8` | -| `b` | `i16` | -| `c` | `i32` | -| `d` | `i64` | -| `e` | `u8` | -| `f` | `u16` | -| `g` | `u32` | -| `h` | `u64` | -| `i` | `f32` | -| `j` | `f64` | - -#### Enums - -##### `Shape` - -An algebraic shape (sum type with associated data) - -Rich (algebraic) enum: a plain tagged object (`{ tag: "Variant", ...fields }`) serialized in a value buffer as an `i32` tag followed by the active variant's fields in declaration order. - -| Variant | Tag | Fields | -|---------|-----|--------| -| `Empty` | `0` | (none) | -| `Circle` | `1` | `radius: f64` | -| `Rectangle` | `2` | `width: f32`, `height: f32` | -| `Bounded` | `3` | `min: i16`, `max: i16`, `weight: u64` | -| `Labeled` | `4` | `label: string`, `count: u8` | - -##### `Channel` - -A plain C-style enum (no payloads) - -Passed as `i32` discriminant. - -| Variant | Value | -|---------|-------| -| `Red` | `0` | -| `Green` | `1` | -| `Blue` | `2` | +await init(); +``` diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_d_ts.snap new file mode 100644 index 00000000..cb80c226 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_d_ts.snap @@ -0,0 +1,83 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +/// + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export declare class ShapesError extends Error { + readonly code: number; + constructor(code: number, message?: string); +} +/** The error a cancelled async call rejects with (code -5). */ +export declare class CancelledError extends ShapesError { + constructor(message?: string); +} +/** The root error class, under a name no namespace member can shadow. */ +declare const $Error: typeof ShapesError; +/** + * Load the WebAssembly module and check that it matches these bindings. + * Every other export throws until the returned promise resolves. + */ +export declare function init( + source?: string | URL | BufferSource | WebAssembly.Module | Response, +): Promise; +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. + * @internal + */ +export declare function __debugLive(kind: number): bigint; + +/** Rich (algebraic) enums and the expanded numeric primitives. */ +export declare namespace shapes { + /** An algebraic shape (sum type with associated data) */ + export type Shape = + /** The empty shape (a unit variant) */ + | { tag: 'Empty' } + /** A circle with a radius */ + | { tag: 'Circle'; radius: number } + /** An axis-aligned rectangle */ + | { tag: 'Rectangle'; width: number; height: number } + /** An integer-bounded shape (exercises narrow + wide ints) */ + | { tag: 'Bounded'; min: number; max: number; weight: bigint } + /** A labeled shape with a small count */ + | { tag: 'Labeled'; label: string; count: number }; + /** A plain C-style enum (no payloads) */ + export enum Channel { + Red = 0, + Green = 1, + Blue = 2, + } + /** One field per supported numeric primitive */ + export interface Numerics { + a: number; + b: number; + c: number; + d: bigint; + e: number; + f: number; + g: number; + h: bigint; + i: number; + j: number; + } + /** Render a shape to a string */ + export function describe(shape: shapes.Shape): string; + /** Scale a shape by a factor, returning a new shape */ + export function scale(shape: shapes.Shape, factor: number): shapes.Shape; + /** Sum a list of bytes into a wide integer (numerics smoke) */ + export function sumBytes(values: Uint8Array): bigint; + /** Build a Numerics struct from every numeric primitive */ + export function makeNumerics(a: number, b: number, c: number, d: bigint, e: number, f: number, g: number, h: bigint, i: number, j: number): shapes.Numerics; +} + +// END index.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_js.snap new file mode 100644 index 00000000..269496e7 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__index_js.snap @@ -0,0 +1,240 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +import { + $Fault, + $fault, + $domain, + $W, + $R, + $encode, + $decode, + $Object, + $own, + $adopt, + $adoptOpt, + $lend, + $lendOpt, + $unlend, + $clone, + $Iterator, + $cancellable, + $impl, + $ret, + $verify, + ShapesError as $Error, +} from './runtime.js'; +export { ShapesError, CancelledError } from './runtime.js'; +import { $loadWasm, $unloaded } from './linear.js'; + +let $raw = $unloaded('shapes'); +// Handles are linear-memory addresses; object tokens are u64s. +const $token = Number; +let $loading = null; + +/** + * Load the WebAssembly module (by default `shapes.wasm` next to this file, or + * the file named by `SHAPES_LIBRARY` on Node.js) and check that it matches + * these bindings. Every other export throws until the returned promise + * resolves. + */ +export function init(source) { + $loading ??= $loadWasm(source, 'shapes', 'SHAPES_LIBRARY', new URL('./shapes.wasm', import.meta.url)) + .then((m) => { + $raw = $bind(m); + try { + $verify($raw, 'shapes', 'shapes', 3, [['shapes', 0x06acf433ffa2a0f0n]]); + } catch (e) { + $raw = $unloaded('shapes'); + throw e; + } + }) + .catch((e) => { + $loading = null; + throw e; + }); + return $loading; +} + +// The raw entry points, one per C symbol. +function $bind(m) { + const x = m.x; + return { + shapes_abi_version: () => x.shapes_abi_version() >>> 0, + shapes_debug_live: (kind) => BigInt.asUintN(64, x.shapes_debug_live(m.num(kind))), + shapes_cancel_token_create: () => x.shapes_cancel_token_create(), + shapes_cancel_token_cancel: (t) => x.shapes_cancel_token_cancel(m.handle(t)), + shapes_cancel_token_destroy: (t) => x.shapes_cancel_token_destroy(m.handle(t)), + shapes_shapes_checksum: () => BigInt.asUintN(64, x.shapes_shapes_checksum()), + shapes_shapes_describe: (a0) => { + let s0 = null; + try { + s0 = m.data(a0); + const r = x.shapes_shapes_describe(s0[0], s0[1], m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + } finally { + m.unstage(s0); + } + }, + shapes_shapes_scale: (a0, a1) => { + let s0 = null; + try { + s0 = m.data(a0); + const r = x.shapes_shapes_scale(s0[0], s0[1], m.num(a1), m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + } finally { + m.unstage(s0); + } + }, + shapes_shapes_sum_bytes: (a0) => { + let s0 = null; + try { + s0 = m.data(a0); + const r = x.shapes_shapes_sum_bytes(s0[0], s0[1], m.err); + m.check(); + return BigInt.asUintN(64, r); + } finally { + m.unstage(s0); + } + }, + shapes_shapes_make_numerics: (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) => { + const r = x.shapes_shapes_make_numerics(m.num(a0), m.num(a1), m.num(a2), m.i64(a3), m.num(a4), m.num(a5), m.num(a6), m.u64(a7), m.num(a8), m.num(a9), m.len, m.err); + m.check(); + return m.takeData(r, m.outLen()); + }, + }; +} + +/** + * The native library's live-allocation counters (0 objects, 1 callbacks, + * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always + * zero unless the library enables its `leak-check` feature. + */ +export function __debugLive(kind) { + return $raw.shapes_debug_live(kind); +} + +const shapes$Channel = Object.freeze({ + Red: 0, + Green: 1, + Blue: 2, + 0: 'Red', + 1: 'Green', + 2: 'Blue', +}); + +function $w$shapes$Numerics(w, v) { + w.writeI8(v.a); + w.writeI16(v.b); + w.writeI32(v.c); + w.writeI64(v.d); + w.writeU8(v.e); + w.writeU16(v.f); + w.writeU32(v.g); + w.writeU64(v.h); + w.writeF32(v.i); + w.writeF64(v.j); +} +function $r$shapes$Numerics(r) { + return { + a: r.readI8(), + b: r.readI16(), + c: r.readI32(), + d: r.readI64(), + e: r.readU8(), + f: r.readU16(), + g: r.readU32(), + h: r.readU64(), + i: r.readF32(), + j: r.readF64(), + }; +} +function $w$shapes$Shape(w, v) { + switch (v.tag) { + case 'Empty': + w.writeI32(0); + return; + case 'Circle': + w.writeI32(1); + w.writeF64(v.radius); + return; + case 'Rectangle': + w.writeI32(2); + w.writeF32(v.width); + w.writeF32(v.height); + return; + case 'Bounded': + w.writeI32(3); + w.writeI16(v.min); + w.writeI16(v.max); + w.writeU64(v.weight); + return; + case 'Labeled': + w.writeI32(4); + w.writeString(v.label); + w.writeU8(v.count); + return; + default: + throw new TypeError('unknown Shape tag: ' + (v && v.tag)); + } +} +function $r$shapes$Shape(r) { + const tag = r.readI32(); + switch (tag) { + case 0: return { tag: 'Empty' }; + case 1: return { tag: 'Circle', radius: r.readF64() }; + case 2: return { tag: 'Rectangle', width: r.readF32(), height: r.readF32() }; + case 3: return { tag: 'Bounded', min: r.readI16(), max: r.readI16(), weight: r.readU64() }; + case 4: return { tag: 'Labeled', label: r.readString(), count: r.readU8() }; + default: + throw new $Error(-3, 'unknown Shape tag: ' + tag); + } +} + +const shapes$describe = function describe(shape) { + try { + return $raw.shapes_shapes_describe($encode(shape, $w$shapes$Shape)); + } catch ($e) { + throw $fault($e); + } +}; + +const shapes$scale = function scale(shape, factor) { + try { + return $decode($raw.shapes_shapes_scale($encode(shape, $w$shapes$Shape), factor), $r$shapes$Shape); + } catch ($e) { + throw $fault($e); + } +}; + +const shapes$sumBytes = function sumBytes(values) { + try { + return $raw.shapes_shapes_sum_bytes(values); + } catch ($e) { + throw $fault($e); + } +}; + +const shapes$makeNumerics = function makeNumerics(a, b, c, d, e, f, g, h, i, j) { + try { + return $decode($raw.shapes_shapes_make_numerics(a, b, c, d, e, f, g, h, i, j), $r$shapes$Numerics); + } catch ($e) { + throw $fault($e); + } +}; + +export const shapes = Object.freeze({ + Channel: shapes$Channel, + describe: shapes$describe, + scale: shapes$scale, + sumBytes: shapes$sumBytes, + makeNumerics: shapes$makeNumerics, +}); + +// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__linear_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__linear_js.snap new file mode 100644 index 00000000..608de11a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__linear_js.snap @@ -0,0 +1,403 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The WebAssembly transport: loading the module, staging arguments in its +// linear memory, reading results and errors back, and installing JavaScript +// functions in its function table (async completions and callback +// interface vtables). The generated `index.js` builds one native entry point +// per C symbol on top of this. + +import { $Fault } from './runtime.js'; + +const $enc = new TextEncoder(); +const $dec = new TextDecoder(); + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v, bits, what) { + let b; + if (typeof v === 'bigint') { + b = v; + } else if (typeof v === 'number' && Number.isInteger(v)) { + b = BigInt(v); + } else { + throw $expected('a bigint', v); + } + if (bits(64, b) !== b) throw new RangeError(`bigint does not fit in ${what} 64-bit integer`); + return b; +} + +/** + * One loaded module's linear memory and runtime symbols. `x` holds the + * module's exports by C symbol name; `memory()` returns the current + * `ArrayBuffer`; `fnptr(params, results, sig, fn)` installs `fn` in the + * function table and returns its index. + */ +export class $Linear { + constructor(x, prefix, memory, fnptr) { + this.x = x; + this.memory = memory; + this.fnptr = fnptr; + this.allocFn = x[`${prefix}_alloc`]; + this.deallocFn = x[`${prefix}_dealloc`]; + this.freeBytesFn = x[`${prefix}_free_bytes`]; + this.errorClearFn = x[`${prefix}_error_clear`]; + this.errorFreeFn = x[`${prefix}_error_free`]; + this.errorSetFn = x[`${prefix}_error_set`]; + if (typeof this.allocFn !== 'function' || typeof this.deallocFn !== 'function') { + throw new Error(`the WebAssembly module does not export ${prefix}_alloc and ${prefix}_dealloc`); + } + // One scratch block reused by every call: the error slot (16 bytes), + // the out_len slot (4), and the iterator out_item slot (8). The producer + // only writes them as a call returns, so a call made from a callback in + // the middle of another call can reuse them safely. + const scratch = this.allocFn(32); + this.bytes().fill(0, scratch, scratch + 32); + this.err = scratch; + this.len = scratch + 16; + this.item = scratch + 24; + this.callbacks = new Map(); + this.nextCallback = 1; + this.pending = new Map(); + this.nextPending = 1; + this.completions = new Map(); + this.vtables = new Map(); + } + + bytes() { + const buffer = this.memory(); + if (buffer !== this.buffer) { + this.buffer = buffer; + this.u8 = new Uint8Array(buffer); + this.dv = new DataView(buffer); + } + return this.u8; + } + + view() { + this.bytes(); + return this.dv; + } + + // Argument checks and conversions for by-value slots. + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + i64(v) { + return $bigint(v, BigInt.asIntN, 'a signed'); + } + + u64(v) { + return $bigint(v, BigInt.asUintN, 'an unsigned'); + } + + bool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + return v ? 1 : 0; + } + + handle(v) { + if (typeof v !== 'number' || v === 0) throw $expected('an object handle', v); + return v; + } + + handleOpt(v) { + return v === null || v === undefined ? 0 : this.handle(v); + } + + // Staging: copy a string or bytes into linear memory for one call. A + // staged value is [ptr, len, size]; release it with unstage(). + str(s) { + if (typeof s !== 'string') throw $expected('a string', s); + if (s.length === 0) return [0, 0, 0]; + const size = s.length * 3; + const ptr = this.allocFn(size); + const { written } = $enc.encodeInto(s, this.bytes().subarray(ptr, ptr + size)); + return [ptr, written, size]; + } + + data(b) { + if (!(b instanceof Uint8Array)) throw $expected('a Uint8Array', b); + if (b.length === 0) return [0, 0, 0]; + const ptr = this.allocFn(b.length); + this.bytes().set(b, ptr); + return [ptr, b.length, b.length]; + } + + unstage(staged) { + if (staged !== null && staged[2] !== 0) this.deallocFn(staged[0], staged[2]); + } + + // Results. + outLen() { + return this.view().getUint32(this.len, true); + } + + readStr(ptr, len) { + return len === 0 ? '' : $dec.decode(this.bytes().subarray(ptr, ptr + len)); + } + + readData(ptr, len) { + return len === 0 ? new Uint8Array(0) : this.bytes().slice(ptr, ptr + len); + } + + takeStr(ptr, len) { + const s = this.readStr(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return s; + } + + takeData(ptr, len) { + const b = this.readData(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return b; + } + + cstr(ptr) { + if (ptr === 0) return ''; + const u8 = this.bytes(); + let end = ptr; + while (u8[end] !== 0) end++; + return $dec.decode(u8.subarray(ptr, end)); + } + + fault(errPtr) { + const dv = this.view(); + const code = dv.getInt32(errPtr, true); + if (code === 0) return null; + const payloadPtr = dv.getUint32(errPtr + 8, true); + const payloadLen = dv.getUint32(errPtr + 12, true); + return new $Fault( + code, + this.cstr(dv.getUint32(errPtr + 4, true)), + payloadPtr === 0 ? null : this.bytes().slice(payloadPtr, payloadPtr + payloadLen), + ); + } + + /** Throw the error the last call wrote to the scratch error slot. */ + check() { + if (this.view().getInt32(this.err, true) === 0) return; + const fault = this.fault(this.err); + this.errorClearFn(this.err); + throw fault; + } + + // Callback interfaces: implementations live in a table keyed by the + // integer passed as `ctx`, until the producer calls the vtable's `free`. + register(adapter) { + const id = this.nextCallback++; + this.callbacks.set(id, adapter); + return id; + } + + adapter(id) { + const adapter = this.callbacks.get(id); + if (adapter === undefined) throw new Error('callback implementation was already released'); + return adapter; + } + + unregister(id) { + this.callbacks.delete(id); + } + + /** Report a callback implementation's exception through `errPtr` (code -4). */ + foreign(errPtr, e) { + const message = e instanceof Error ? e.message || e.name : String(e); + const bytes = $enc.encode(message); + const ptr = this.allocFn(bytes.length + 1); + const u8 = this.bytes(); + u8.set(bytes, ptr); + u8[ptr + bytes.length] = 0; + this.errorSetFn(errPtr, -4, ptr); + this.deallocFn(ptr, bytes.length + 1); + } + + /** + * The static vtable of one callback interface: one table entry per + * method (`methods()` returns each as `[params, results, sig, fn]`), then + * `free`. Built on first use. + */ + vtable(name, methods) { + let ptr = this.vtables.get(name); + if (ptr !== undefined) return ptr; + const entries = [...methods(), [['i32'], [], 'vi', (ctx) => this.unregister(ctx)]]; + ptr = this.allocFn(entries.length * 4); + const dv = this.view(); + entries.forEach(([params, results, sig, fn], i) => { + dv.setUint32(ptr + i * 4, this.fnptr(params, results, sig, fn), true); + }); + this.vtables.set(name, ptr); + return ptr; + } + + /** + * Launch an async call. `call(callback, context)` invokes the launcher; + * the completion (one table entry per signature, shared by every call) + * settles the returned promise, converting the result slots with + * `convert`. On this single-threaded target the completion normally + * fires before the launcher returns. + */ + launch(params, results, sig, call, convert) { + let callback = this.completions.get(sig); + if (callback === undefined) { + const complete = (ctx, errPtr, ...result) => this.complete(ctx, errPtr, result); + callback = this.fnptr(params, results, sig, complete); + this.completions.set(sig, callback); + } + const id = this.nextPending++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject, convert }); + try { + call(callback, id); + } catch (e) { + this.pending.delete(id); + reject(e); + } + }); + } + + complete(id, errPtr, result) { + const pending = this.pending.get(id); + if (pending === undefined) return; + this.pending.delete(id); + try { + const fault = errPtr === 0 ? null : this.fault(errPtr); + if (errPtr !== 0) this.errorFreeFn(errPtr); + if (fault !== null) { + pending.reject(fault); + } else { + pending.resolve(pending.convert(...result)); + } + } catch (e) { + pending.reject(e); + } + } +} + +/** A stand-in for the native entry points until `init()` has finished. */ +export function $unloaded(library) { + return new Proxy( + {}, + { + get() { + throw new Error(`${library}: call init() before using the bindings`); + }, + }, + ); +} + +const $valtype = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c }; + +function $leb(n) { + const out = []; + do { + let b = n & 0x7f; + n >>>= 7; + if (n !== 0) b |= 0x80; + out.push(b); + } while (n !== 0); + return out; +} + +function $section(id, body) { + return [id, ...$leb(body.length), ...body]; +} + +/** + * Turn a JavaScript function into a WebAssembly function with the given + * signature: `WebAssembly.Function` where the engine has it, otherwise a + * tiny module that imports `fn` and re-exports it. + */ +function $wasmFunction(params, results, fn) { + if (typeof WebAssembly.Function === 'function') { + return new WebAssembly.Function({ parameters: params, results }, fn); + } + const type = [0x60, ...$leb(params.length), ...params.map((t) => $valtype[t])]; + type.push(...$leb(results.length), ...results.map((t) => $valtype[t])); + const bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + ...$section(1, [1, ...type]), + ...$section(2, [1, 1, 0x65, 1, 0x66, 0x00, 0x00]), + ...$section(7, [1, 1, 0x66, 0x00, 0x00]), + ]); + const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), { e: { f: fn } }); + return instance.exports.f; +} + +async function $bytesOf(source, envVar, fallback) { + if (source === undefined || source === null) { + const env = typeof process === 'object' && process.env ? process.env[envVar] : undefined; + if (env) { + const { readFile } = await import('node:fs/promises'); + return readFile(env); + } + source = fallback; + } + if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { + return source; + } + if (typeof Response === 'function' && source instanceof Response) { + return source.arrayBuffer(); + } + const url = source instanceof URL ? source : new URL(String(source), fallback); + if (url.protocol === 'file:') { + const { readFile } = await import('node:fs/promises'); + return readFile(url); + } + const response = await fetch(url); + if (!response.ok) throw new Error(`failed to fetch ${url}: ${response.status}`); + return response.arrayBuffer(); +} + +/** + * Instantiate a `wasm32-unknown-unknown` module from `source` (a URL, a + * path, bytes, a `Response`, or a compiled module). Without a source it + * reads the file named by the `envVar` environment variable (on Node.js), + * else `fallback`. The module must export its memory and a growable + * function table (`__indirect_function_table`). + */ +export async function $loadWasm(source, prefix, envVar, fallback) { + const bytes = await $bytesOf(source, envVar, fallback); + const module = bytes instanceof WebAssembly.Module ? bytes : await WebAssembly.compile(bytes); + const { exports } = await WebAssembly.instantiate(module, {}); + const table = exports.__indirect_function_table; + const fnptr = (params, results, sig, fn) => { + if (!(table instanceof WebAssembly.Table)) { + throw new Error('the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)'); + } + const index = table.grow(1); + table.set(index, $wasmFunction(params, results, fn)); + return index; + }; + return new $Linear(exports, prefix, () => exports.memory.buffer, fnptr); +} + +/** + * Adopt an initialized Emscripten module (or the promise its `MODULARIZE` + * factory returns). Its exports carry a leading underscore, its memory is + * read through `HEAPU8`, and table entries are installed with + * `addFunction`, so it must be linked with `-sALLOW_TABLE_GROWTH`, + * `-sWASM_BIGINT`, and `-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`. + */ +export async function $loadEmscripten(module, prefix, symbols) { + const m = await module; + const x = {}; + for (const symbol of symbols) x[symbol] = m[`_${symbol}`]; + const fnptr = (params, results, sig, fn) => { + if (typeof m.addFunction !== 'function') { + throw new Error('the Emscripten module does not export addFunction (link with -sALLOW_TABLE_GROWTH and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8)'); + } + return m.addFunction(fn, sig); + }; + return new $Linear(x, prefix, () => m.HEAPU8.buffer, fnptr); +} + +// END linear.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__package_json.snap index cee4ead6..e1c5847a 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__package_json.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__package_json.snap @@ -2,13 +2,25 @@ source: crates/weaveffi-cli/tests/snapshots.rs --- { - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", + "//": "Generated by WeaveFFI [VERSION] from api.yml", "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", + "//regenerate": "To regenerate: weaveffi generate api.yml -o ", + "name": "shapes", "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", + "description": "Native bindings for shapes", "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" + "main": "index.js", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "runtime.js", + "linear.js" + ] } diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__runtime_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__runtime_js.snap new file mode 100644 index 00000000..323cda6a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__runtime_js.snap @@ -0,0 +1,578 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from api.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate api.yml -o + +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class ShapesError extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends ShapesError { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new ShapesError(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new ShapesError(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new ShapesError(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new ShapesError(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} + +// END runtime.js 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 deleted file mode 100644 index 705039d7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,77 +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, producer - * panics, and callback-interface implementations that raised (code -4). - * Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** One field per supported numeric primitive */ -export interface Numerics { - a: number; - b: number; - c: number; - d: bigint; - e: number; - f: number; - g: number; - h: bigint; - i: number; - j: number; -} - -/** An algebraic shape (sum type with associated data) */ -export type Shape = - | { tag: "Empty" } - | { tag: "Circle"; radius: number } - | { tag: "Rectangle"; width: number; height: number } - | { tag: "Bounded"; min: number; max: number; weight: bigint } - | { tag: "Labeled"; label: string; count: number }; - -/** A plain C-style enum (no payloads) */ -export declare const Channel: Readonly<{ - Red: 0; - Green: 1; - Blue: 2; -}>; -export type Channel = (typeof Channel)[keyof typeof Channel]; - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - shapes: { - /** - * Render a shape to a string - * @throws {WeaveFFIError} if the native call fails - */ - describe(shape: Shape): string; - /** - * Scale a shape by a factor, returning a new shape - * @throws {WeaveFFIError} if the native call fails - */ - scale(shape: Shape, factor: number): Shape; - /** - * Sum a list of bytes into a wide integer (numerics smoke) - * @throws {WeaveFFIError} if the native call fails - */ - sumBytes(values: Uint8Array): bigint; - /** - * Build a Numerics struct from every numeric primitive - * @throws {WeaveFFIError} if the native call fails - */ - makeNumerics(a: number, b: number, c: number, d: bigint, e: number, f: number, g: number, h: bigint, i: number, j: number): Numerics; - }; -} - -export function loadWeaveffiWasm(source: string | URL | BufferSource | WebAssembly.Module): Promise; - -// END weaveffi_wasm.d.ts 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 deleted file mode 100644 index 7f0942f7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_js.snap +++ /dev/null @@ -1,519 +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); one -// strong reference per wrapper, released by close() -// 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 -// Callbacks -> i32 context key + i32 pointer to a static vtable - -// The ABI revision this glue was generated against. -const _ABI_VERSION = 2; - -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, producer - * panics, and callback-interface implementations that raised (code -4). - * 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; -} - -// 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); } - // An object token: the pointer (a wasm32 i32, read unsigned) widened to u64. - obj(ptr) { this.u64(BigInt(ptr >>> 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; - } - // An object token narrowed back to a wasm32 pointer; zero is never a - // valid token (an absent `Interface?` uses the option flag instead). - obj() { - const t = this.u64(); - if (t === 0n || t > 0xffffffffn) this._bad('object token out of range'); - return Number(t); - } - 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 (-2), a marshalling failure (-3), or a callback -// interface implementation that raised (-4), surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - _pendingForeign = null; - 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) { - _pendingForeign = null; - wasm.weaveffi_dealloc(errPtr, 16); -} - -// The message of a callback-interface failure a trampoline reported through -// out_err, held until the producer call on the stack returns (every error -// checker and _freeErr clear it) so later callbacks in that call are refused. -let _pendingForeign = null; - -// Translate an exception raised while a producer call was on the stack. -// wasm32-unknown-unknown has no unwinding runtime: a producer panic executes -// `unreachable`, so the engine throws a WebAssembly.RuntimeError instead of -// the producer writing out_err. A trap that follows a callback-interface -// failure is labelled with that failure (-4); any other trap is a producer -// panic (-2). Non-trap exceptions pass through unchanged. The producer -// frames between the trap and the call are not unwound, so a lock held -// across a callback stays locked; a producer that snapshots its state -// before calling out stays usable. -function _trapError(e) { - const foreign = _pendingForeign; - _pendingForeign = null; - if (!(e instanceof WebAssembly.RuntimeError)) return e; - if (foreign !== null) return new WeaveFFIError(-4, foreign); - return new WeaveFFIError(-2, 'producer panicked: ' + e.message); -} - -// Release the error slot a failed call never filled, then translate. -function _trap(wasm, errPtr, e) { - _freeErr(wasm, errPtr); - return _trapError(e); -} - -export const Channel = Object.freeze({ - Red: 0, - Green: 1, - Blue: 2, -}); - -/** - * Load a WeaveFFI Wasm module. - * - * @param {string|URL|BufferSource|WebAssembly.Module} source - A URL to - * fetch the `.wasm` from, its bytes, or an already compiled module - * (use the bytes form where `fetch` cannot read files, e.g. Node). - * @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 }); - * - * // Object: a class wrapping one producer reference; close() releases it - * // (or let `using` do it). Passing it lends the reference for the call. - * const store = new api.kv.Store(); - * api.kv.inspect(store); - * store.close(); - * - * // 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(source) { - let module; - if (source instanceof WebAssembly.Module) { - module = source; - } else if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { - module = await WebAssembly.compile(source); - } else { - const response = await fetch(source); - module = await WebAssembly.compile(await response.arrayBuffer()); - } - const instance = await WebAssembly.instantiate(module, {}); - const wasm = instance.exports; - - _checkAbiVersion(wasm); - - // Serialize a `shapes.Numerics` record into the value-buffer wire format. - function _write_shapes_Numerics(w, v) { - w.i8(v.a); - w.i16(v.b); - w.i32(v.c); - w.i64(v.d); - w.u8(v.e); - w.u16(v.f); - w.u32(v.g); - w.u64(v.h); - w.f32(v.i); - w.f64(v.j); - } - - // Decode a `shapes.Numerics` record from the value-buffer wire format. - function _read_shapes_Numerics(r) { - const v = {}; - v.a = r.i8(); - v.b = r.i16(); - v.c = r.i32(); - v.d = r.i64(); - v.e = r.u8(); - v.f = r.u16(); - v.g = r.u32(); - v.h = r.u64(); - v.i = r.f32(); - v.j = r.f64(); - return v; - } - - // Serialize a `shapes.Shape` rich enum into the value-buffer wire format: - // an i32 tag, then the active variant's fields in order. - function _write_shapes_Shape(w, v) { - switch (v.tag) { - case "Empty": { - w.i32(0); - break; - } - case "Circle": { - w.i32(1); - w.f64(v.radius); - break; - } - case "Rectangle": { - w.i32(2); - w.f32(v.width); - w.f32(v.height); - break; - } - case "Bounded": { - w.i32(3); - w.i16(v.min); - w.i16(v.max); - w.u64(v.weight); - break; - } - case "Labeled": { - w.i32(4); - w.str(v.label); - w.u8(v.count); - break; - } - default: - throw new WeaveFFIError(-3, "unknown Shape variant tag: " + v.tag); - } - } - - // Decode a `shapes.Shape` rich enum from the value-buffer wire format. - function _read_shapes_Shape(r) { - const _tag = r.i32(); - switch (_tag) { - case 0: - return { tag: "Empty" }; - case 1: { - const v = { tag: "Circle" }; - v.radius = r.f64(); - return v; - } - case 2: { - const v = { tag: "Rectangle" }; - v.width = r.f32(); - v.height = r.f32(); - return v; - } - case 3: { - const v = { tag: "Bounded" }; - v.min = r.i16(); - v.max = r.i16(); - v.weight = r.u64(); - return v; - } - case 4: { - const v = { tag: "Labeled" }; - v.label = r.str(); - v.count = r.u8(); - return v; - } - default: - throw new WeaveFFIError(-3, "malformed value buffer: unknown Shape tag " + _tag); - } - } - - return { - _raw: wasm, - shapes: { - describe(shape) { - const a0_w = new _BufWriter(); - _write_shapes_Shape(a0_w, shape); - const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_shapes_describe(a0_p, a0_l, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_l); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - }, - scale(shape, factor) { - const a0_w = new _BufWriter(); - _write_shapes_Shape(a0_w, shape); - const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_shapes_scale(a0_p, a0_l, factor, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } finally { - 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_shapes_Shape(_rd); - _rd.end(); - return _out; - }, - sumBytes(values) { - const [a0_p, a0_l] = _bytes(wasm, values); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_shapes_sum_bytes(a0_p, a0_l, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_l); - } - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return BigInt.asUintN(64, _r); - }, - makeNumerics(a, b, c, d, e, f, g, h, i, j) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_shapes_make_numerics(a, b, c, BigInt(d), e, f, g, BigInt(h), i, j, _lp, _err); - } catch (e) { - wasm.weaveffi_dealloc(_lp, 4); - throw _trap(wasm, _err, e); - } - _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_shapes_Numerics(_rd); - _rd.end(); - return _out; - }, - }, - }; -} - -// END weaveffi_wasm.js diff --git a/crates/weaveffi-core/src/cache.rs b/crates/weaveffi-core/src/cache.rs deleted file mode 100644 index cfc7d1d2..00000000 --- a/crates/weaveffi-core/src/cache.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! 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 -/// version automatically invalidates every cache file so users never see -/// stale generator output after an upgrade. -pub const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// 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"); - serde_json::to_string(&value).expect("Value serialization should not fail") -} - -/// The SHA-256 hex digest of the API's canonical JSON. -/// -/// # 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 { - format!("{:x}", Sha256::digest(canonical_json(api).as_bytes())) -} - -/// 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. -/// -/// # Panics -/// -/// 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"v2\0"); - hasher.update(CLI_VERSION.as_bytes()); - hasher.update(b"\0"); - hasher.update(target.as_bytes()); - hasher.update(b"\0"); - hasher.update(canonical_json(api.api()).as_bytes()); - hasher.update(b"\0"); - hasher.update(package.as_bytes()); - hasher.update(b"\0"); - hasher.update(config_bytes); - format!("{:x}", hasher.finalize()) -} - -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 `target`. -/// -/// # Errors -/// -/// 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 exists but cannot be removed. -pub fn invalidate_all(out_dir: &Utf8Path) -> Result<()> { - let cache_dir = out_dir.join(CACHE_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}"))?; - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use weaveffi_ir::ir::{Module, CURRENT_SCHEMA_VERSION}; - - fn api(name: &str) -> Api { - Api { - version: CURRENT_SCHEMA_VERSION.into(), - modules: vec![Module { - name: name.into(), - doc: None, - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callback_interfaces: vec![], - errors: None, - modules: vec![], - }], - } - } - - #[test] - 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!( - 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 entries_round_trip_and_invalidate() { - let dir = tempfile::tempdir().unwrap(); - 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/codegen/mod.rs b/crates/weaveffi-core/src/codegen/mod.rs deleted file mode 100644 index 503ce95b..00000000 --- a/crates/weaveffi-core/src/codegen/mod.rs +++ /dev/null @@ -1,494 +0,0 @@ -//! Target erasure and orchestration. -//! -//! 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 crate::backend::{self, LanguageBackend}; -use crate::cache; -use crate::capabilities::{self, TargetCapabilities}; -use crate::package::{PackageContext, PackagedFile}; -use crate::resolved::ResolvedApi; - -pub mod common; -pub mod writer; - -pub use writer::CodeWriter; - -fn run_hook(label: &str, cmd: &str) -> Result<()> { - let status = if cfg!(target_os = "windows") { - std::process::Command::new("cmd") - .args(["/C", cmd]) - .status()? - } else { - std::process::Command::new("sh") - .arg("-c") - .arg(cmd) - .status()? - }; - if !status.success() { - bail!("{label} hook failed with {status}"); - } - Ok(()) -} - -/// Object-safe view of a [`LanguageBackend`] paired with a concrete config. -/// -/// The orchestrator stores targets as `&dyn Target` so it can hold a -/// heterogeneous set whose `Config` types differ. [`ConfiguredBackend`] is -/// the canonical adapter. -pub trait Target: Send + Sync { - /// The target's stable short name. Mirrors [`LanguageBackend::name`]. - fn name(&self) -> &'static str; - /// The gated features the target implements. Mirrors - /// [`LanguageBackend::capabilities`]. - fn capabilities(&self) -> TargetCapabilities; - /// See [`LanguageBackend::allows_unsupported`], evaluated against the - /// bound config. - fn allows_unsupported(&self) -> bool; - /// Render the bindings under `out_dir`, using the bound config. - /// - /// # Errors - /// - /// 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. - fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec; - /// Assemble the distributable package for this target, using the bound - /// config. Returns `None` when the target does not support packaging. - fn package( - &self, - api: &ResolvedApi, - ctx: &PackageContext, - out_dir: &Utf8Path, - ) -> Option>; - /// Canonical-JSON encoding of the bound config, fed into the cache - /// hash so a config-only change invalidates the entry. - fn config_hash_input(&self) -> Vec; -} - -/// Binds a [`LanguageBackend`] to a concrete config value so it can be erased -/// to `&dyn Target`. -/// -/// ```ignore -/// let swift = ConfiguredBackend::new(SwiftBackend, SwiftConfig::default()); -/// orchestrator.with_target(&swift); -/// ``` -pub struct ConfiguredBackend { - inner: B, - config: B::Config, -} - -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) -> &B::Config { - &self.config - } - - /// Borrow the wrapped backend. - pub fn inner(&self) -> &B { - &self.inner - } -} - -impl Target for ConfiguredBackend { - fn name(&self) -> &'static str { - self.inner.name() - } - - fn capabilities(&self) -> TargetCapabilities { - self.inner.capabilities(&self.config) - } - - fn allows_unsupported(&self) -> bool { - self.inner.allows_unsupported(&self.config) - } - - fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Result<()> { - backend::run(&self.inner, api, out_dir, &self.config) - } - - fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec { - backend::output_files(&self.inner, api, out_dir, &self.config) - } - - fn package( - &self, - api: &ResolvedApi, - ctx: &PackageContext, - out_dir: &Utf8Path, - ) -> Option> { - 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("backend config should serialize to JSON"); - serde_json::to_vec(&value).expect("JSON Value should serialize") - } -} - -/// Global hooks the orchestrator runs around the parallel codegen pass. -#[derive(Default, Debug, Clone)] -pub struct OrchestratorHooks { - /// Shell command run once before the parallel pass, only when at least one - /// target is out of date. `None` skips it. - pub pre_generate: Option, - /// Shell command run once after every target finishes. `None` skips it. - pub post_generate: Option, -} - -/// 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> { - targets: Vec<&'a dyn Target>, -} - -impl<'a> Orchestrator<'a> { - /// Create an orchestrator with no targets registered. - pub fn new() -> Self { - Self::default() - } - - /// 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 - } - - /// Generate every registered target under `out_dir`. - /// - /// Gates each target against the gated features the API uses, skips targets - /// whose cached hash still matches (unless `force` clears the cache first), - /// runs the `pre_generate` and `post_generate` hooks around the parallel - /// pass, and records a fresh cache entry for each regenerated target. - /// - /// # Errors - /// - /// 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 target fails - /// while rendering, or a cache entry cannot be written. - pub fn run( - &self, - api: &ResolvedApi, - out_dir: &Utf8Path, - hooks: &OrchestratorHooks, - force: bool, - ) -> 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 target whose config - // explicitly opted in via `allow_unsupported` downgrades its failure - // 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 t in &self.targets { - let Err(err) = capabilities::check(api.api(), t.name(), &t.capabilities()) else { - continue; - }; - if t.allows_unsupported() { - eprintln!( - "warning: target '{}' does not support every feature this IDL uses; \ - generating anyway because allow_unsupported is set:", - t.name() - ); - for (feature, locations) in &err.violations { - eprintln!(" - {feature} (used by: {})", locations.join(", ")); - } - } else { - violations.push(err.to_string()); - } - } - if !violations.is_empty() { - bail!("{}", violations.join("\n")); - } - - if force { - cache::invalidate_all(out_dir)?; - } - - // 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 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)); - } - } - - if pending.is_empty() { - println!("No changes detected, skipping code generation."); - return Ok(()); - } - - if let Some(cmd) = &hooks.pre_generate { - run_hook("pre_generate", cmd)?; - } - - pending - .par_iter() - .map(|(t, _)| t.generate(api, out_dir)) - .collect::>>()?; - - if let Some(cmd) = &hooks.post_generate { - run_hook("post_generate", cmd)?; - } - - for (t, hash) in &pending { - cache::write_generator_cache(out_dir, t.name(), hash)?; - } - Ok(()) - } -} - -#[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, CallbackInterfaceDef, Function, Module}; - - #[derive(Default, Clone, serde::Serialize)] - struct TestConfig { - knob: Option, - allow_unsupported: bool, - } - - struct Counting { - name: &'static str, - calls: Arc, - caps: TargetCapabilities, - } - - impl LanguageBackend for Counting { - type Config = TestConfig; - - fn name(&self) -> &'static str { - self.name - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - self.caps - } - - fn allows_unsupported(&self, config: &Self::Config) -> bool { - config.allow_unsupported - } - - fn files( - &self, - _api: &ResolvedApi, - _model: &BindingModel, - out_dir: &Utf8Path, - _config: &Self::Config, - ) -> Vec { - self.calls.fetch_add(1, Ordering::SeqCst); - vec![OutputFile::new( - out_dir.join(self.name).join("output.txt"), - "generated", - )] - } - } - - fn module(name: &str) -> Module { - Module { - name: name.into(), - doc: None, - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callback_interfaces: vec![], - errors: None, - modules: vec![], - } - } - - fn api() -> ResolvedApi { - ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![module("math")], - }) - } - - /// An API that uses a callback interface, so a target without callback - /// interface support trips the capability gate. - fn listener_api() -> ResolvedApi { - let mut m = module("math"); - m.callback_interfaces = vec![CallbackInterfaceDef { - name: "OnChange".into(), - doc: None, - deprecated: None, - methods: vec![Function { - name: "changed".into(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - }], - }]; - 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( - name: &'static str, - calls: &Arc, - allow_unsupported: bool, - ) -> ConfiguredBackend { - ConfiguredBackend::new( - Counting { - name, - calls: Arc::clone(calls), - caps: TargetCapabilities { - callback_interfaces: false, - ..TargetCapabilities::full() - }, - }, - TestConfig { - knob: None, - allow_unsupported, - }, - ) - } - - fn out_dir(dir: &tempfile::TempDir) -> &Utf8Path { - Utf8Path::from_path(dir.path()).unwrap() - } - - #[test] - fn capability_gate_blocks_unless_opted_in_and_never_relaxes_others() { - let dir = tempfile::tempdir().unwrap(); - let hooks = OrchestratorHooks::default(); - let strict_calls = Arc::new(AtomicUsize::new(0)); - 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_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.OnChange"), "{err}"); - assert!(!err.contains("target 'opted'"), "{err}"); - assert_eq!(strict_calls.load(Ordering::SeqCst), 0); - assert_eq!(opted_calls.load(Ordering::SeqCst), 0); - - 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 cache_skips_unchanged_and_invalidates_per_target() { - let dir = tempfile::tempdir().unwrap(); - let out = out_dir(&dir); - let hooks = OrchestratorHooks::default(); - 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 all_targets_run_in_parallel() { - let dir = tempfile::tempdir().unwrap(); - let names = ["g0", "g1", "g2", "g3", "g4", "g5"]; - let counters: Vec> = names - .iter() - .map(|_| Arc::new(AtomicUsize::new(0))) - .collect(); - let targets: Vec> = names - .iter() - .zip(&counters) - .map(|(name, calls)| full(name, calls)) - .collect(); - let mut orch = Orchestrator::new(); - for t in &targets { - orch = orch.with_target(t); - } - orch.run(&api(), out_dir(&dir), &OrchestratorHooks::default(), false) - .unwrap(); - 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/lib.rs b/crates/weaveffi-core/src/lib.rs deleted file mode 100644 index 5e5211df..00000000 --- a/crates/weaveffi-core/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! 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)] -#![warn(clippy::doc_markdown)] - -pub mod abi; -pub mod backend; -pub mod cabi; -pub mod cache; -pub mod capabilities; -pub mod codegen; -pub mod errors; -pub mod lang; -pub mod manifest; -pub mod model; -pub mod package; -pub mod pkg; -pub mod plan; -pub mod platform; -pub mod resolved; -pub mod utils; -pub mod validate; - -pub use resolved::ResolvedApi; diff --git a/crates/weaveffi-core/src/pkg.rs b/crates/weaveffi-core/src/pkg.rs deleted file mode 100644 index 5b68e4a9..00000000 --- a/crates/weaveffi-core/src/pkg.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Shared package-identity resolution. -//! -//! 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 call [`resolve`] in their -//! manifest code and map the [`ResolvedPackage`] fields onto whatever their -//! ecosystem's manifest format requires. - -use serde::{Deserialize, Serialize}; - -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 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 { - /// Distribution name (npm/PyPI/gem/NuGet/pub/etc.). - pub name: String, - /// Semantic version stamped into the manifest. - pub version: String, - /// 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]` table (empty when - /// none are declared). - pub authors: Vec, - /// Project homepage URL, or `None` when unset. - pub homepage: Option, - /// Source repository URL, or `None` when unset. - pub repository: Option, -} - -impl ResolvedPackage { - /// Human-readable description, or a generated default when unset. - pub fn description_or_default(&self) -> String { - self.description - .clone() - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| format!("{} bindings generated by WeaveFFI", self.name)) - } - - /// 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"` 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"` becomes `"MyKvStore"`. - pub fn module_name(&self) -> String { - pascal_ident(&self.name) - } -} - -/// 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"` 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; - for ch in name.chars() { - if ch.is_ascii_alphanumeric() { - if start_word { - out.push(ch.to_ascii_uppercase()); - } else { - out.push(ch); - } - start_word = false; - } else { - start_word = true; - } - } - if out.is_empty() { - pascal_ident(DEFAULT_NAME) - } else { - out - } -} - -/// Lower-case identifier-safe form of an arbitrary package name. -pub fn sanitize_ident(name: &str) -> String { - let mut out = String::with_capacity(name.len()); - let mut prev_us = false; - for ch in name.chars() { - if ch.is_ascii_alphanumeric() { - out.push(ch.to_ascii_lowercase()); - prev_us = false; - } else if !prev_us && !out.is_empty() { - out.push('_'); - prev_us = true; - } - } - let trimmed = out.trim_end_matches('_'); - if trimmed.is_empty() { - DEFAULT_NAME.to_string() - } else { - trimmed.to_string() - } -} - -/// Strip directory and extension from an IDL basename to use as a fallback -/// 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()) - .map(|b| b.split('.').next().unwrap_or(b)) - .filter(|s| !s.is_empty()) - .unwrap_or(DEFAULT_NAME) - .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. `[package] name` from the project's `weaveffi.toml`, -/// 3. the IDL file stem (`input_basename`), -/// 4. [`DEFAULT_NAME`]. -/// -/// 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: &ResolvedApi, - name_override: Option<&str>, - input_basename: Option<&str>, -) -> ResolvedPackage { - let pkg = api.package(); - let name = name_override - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(str::to_string) - .or_else(|| non_empty(pkg.and_then(|p| p.name.as_ref()))) - .unwrap_or_else(|| name_from_basename(input_basename)); - ResolvedPackage { - name, - 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: 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::{Api, CURRENT_SCHEMA_VERSION}; - - fn api_with(pkg: Option) -> ResolvedApi { - let api = ResolvedApi::assume_valid(Api { - version: CURRENT_SCHEMA_VERSION.into(), - modules: vec![], - }); - match pkg { - Some(p) => api.with_package(p), - None => api, - } - } - - fn full_pkg() -> Package { - Package { - name: Some("kvstore".into()), - version: Some("1.2.0".into()), - description: Some("KV store".into()), - license: Some("MIT".into()), - authors: vec!["Ada".into()], - homepage: Some("https://example.com".into()), - repository: Some("https://github.com/x/kvstore".into()), - } - } - - #[test] - 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()]); - let r = resolve(&api, Some("kvstore_py"), Some("kvstore.yml")); - assert_eq!(r.name, "kvstore_py"); - assert_eq!(r.version, "1.2.0"); - } - - #[test] - fn falls_back_to_file_stem_then_default() { - let api = api_with(None); - let r = resolve(&api, None, Some("path/to/contacts.yml")); - assert_eq!(r.name, "contacts"); - assert_eq!(r.version, DEFAULT_VERSION); - assert_eq!(resolve(&api, None, None).name, DEFAULT_NAME); - assert_eq!( - resolve(&api, Some("widgets"), None).description_or_default(), - "widgets bindings generated by WeaveFFI" - ); - } - - #[test] - 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 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("--"), "Weaveffi"); - } -} diff --git a/crates/weaveffi-core/tests/bin/hook_helper.rs b/crates/weaveffi-core/tests/bin/hook_helper.rs deleted file mode 100644 index 2ee4bf43..00000000 --- a/crates/weaveffi-core/tests/bin/hook_helper.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Cross-platform helper binary used by codegen hook integration tests. -//! -//! Exits 0 when `argv[1] == "ok"` and exits 1 otherwise so the hook tests -//! can exercise both success and failure paths without depending on -//! `sh` / `cmd.exe` shell builtins. - -fn main() { - let arg = std::env::args().nth(1).unwrap_or_default(); - if arg == "ok" { - std::process::exit(0); - } else { - std::process::exit(1); - } -} diff --git a/crates/weaveffi-core/tests/codegen_hooks.rs b/crates/weaveffi-core/tests/codegen_hooks.rs deleted file mode 100644 index 4ddad9c4..00000000 --- a/crates/weaveffi-core/tests/codegen_hooks.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Cross-platform integration tests for orchestrator pre/post hooks. -//! -//! Lives as an integration test rather than a unit test so we can use -//! `env!("CARGO_BIN_EXE_hook_helper")` to invoke a Rust helper binary -//! that exits 0 or 1 on demand, avoiding any reliance on `sh` / `cmd.exe` -//! shell builtins. - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -use camino::Utf8Path; -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, Module, CURRENT_SCHEMA_VERSION}; - -const HOOK_HELPER: &str = env!("CARGO_BIN_EXE_hook_helper"); - -fn helper_cmd(arg: &str) -> String { - 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)] -struct TestConfig; - -struct Counting(Arc); - -impl LanguageBackend for Counting { - type Config = TestConfig; - - fn name(&self) -> &'static str { - "counting" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn files( - &self, - _api: &ResolvedApi, - _model: &BindingModel, - out_dir: &Utf8Path, - _config: &Self::Config, - ) -> Vec { - self.0.fetch_add(1, Ordering::SeqCst); - vec![OutputFile::new( - out_dir.join("counting/output.txt"), - "generated", - )] - } -} - -fn api() -> ResolvedApi { - ResolvedApi::assume_valid(Api { - version: CURRENT_SCHEMA_VERSION.into(), - modules: vec![Module { - name: "math".into(), - doc: None, - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callback_interfaces: vec![], - errors: None, - modules: vec![], - }], - }) -} - -/// 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 calls = Arc::new(AtomicUsize::new(0)); - 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 hooks_run_around_generation_and_failures_propagate() { - let (ok, calls) = run(OrchestratorHooks { - pre_generate: Some(helper_cmd("ok")), - post_generate: Some(helper_cmd("ok")), - }); - ok.unwrap(); - assert_eq!(calls, 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"); - - let (err, calls) = run(OrchestratorHooks { - pre_generate: None, - post_generate: Some(helper_cmd("fail")), - }); - assert!(err.is_err()); - assert_eq!(calls, 1, "the post hook runs after generation"); -} diff --git a/crates/weaveffi-fuzz/Cargo.toml b/crates/weaveffi-fuzz/Cargo.toml index fe287c77..431cf77d 100644 --- a/crates/weaveffi-fuzz/Cargo.toml +++ b/crates/weaveffi-fuzz/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "weaveffi-fuzz" -version = "0.23.0" -edition = "2021" +version.workspace = true +edition.workspace = true publish = false [package.metadata] @@ -12,8 +12,7 @@ fuzzing = ["dep:libfuzzer-sys"] [dependencies] libfuzzer-sys = { version = "0.4", optional = true } -weaveffi-ir = { path = "../weaveffi-ir" } -weaveffi-core = { path = "../weaveffi-core" } +weaveffi-model = { workspace = true, features = ["idl"] } [lib] name = "weaveffi_fuzz" diff --git a/crates/weaveffi-fuzz/README.md b/crates/weaveffi-fuzz/README.md index d5f95f1b..ae2a6200 100644 --- a/crates/weaveffi-fuzz/README.md +++ b/crates/weaveffi-fuzz/README.md @@ -10,11 +10,11 @@ locally and in CI. | Target | What it exercises | | --- | --- | -| `fuzz_parse_yaml` | `weaveffi_ir::parse::parse_api_str(..., "yaml")` | -| `fuzz_parse_json` | `weaveffi_ir::parse::parse_api_str(..., "json")` | -| `fuzz_parse_toml` | `weaveffi_ir::parse::parse_api_str(..., "toml")` | -| `fuzz_parse_type_ref` | `weaveffi_ir::ir::parse_type_ref` | -| `fuzz_validate` | parses YAML, then runs `weaveffi_core::validate::validate_api` on success | +| `fuzz_parse_yaml` | `weaveffi_model::parse::parse_api_str(..., "yaml")` | +| `fuzz_parse_json` | `weaveffi_model::parse::parse_api_str(..., "json")` | +| `fuzz_parse_toml` | `weaveffi_model::parse::parse_api_str(..., "toml")` | +| `fuzz_parse_type_ref` | `weaveffi_model::ir::parse_type_ref` | +| `fuzz_validate` | parses YAML, then runs `weaveffi_model::validate::validate_api` on success | Seed inputs for each target live in `fuzz/seeds//` and are committed to git. `fuzz/corpus/`, `fuzz/artifacts/`, and `fuzz/coverage/` are generated @@ -68,8 +68,9 @@ cargo +nightly fuzz tmin \ crates/weaveffi-fuzz/fuzz/artifacts/fuzz_parse_yaml/crash- ``` -Then turn the minimized reproducer into a regression test in the relevant -crate (`weaveffi-ir` or `weaveffi-core`) before fixing the bug. +Then turn the minimized reproducer into a regression test in +`weaveffi-model` (the parsers and the validator both live there) before +fixing the bug. ## Why is this gated behind a feature? 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 44a1da20..8091b8c1 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.9.0", + "version": "0.10.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 c3d995c0..3a82d15a 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.9.0" +version = "0.10.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 0e4dd926..767ed103 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.9.0" +version: "0.10.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 5e909b40..df536087 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.9.0" +version: "0.10.0" modules: - name: m functions: diff --git a/crates/weaveffi-fuzz/fuzz_targets/fuzz_validate.rs b/crates/weaveffi-fuzz/fuzz_targets/fuzz_validate.rs index d77db238..3e1b50d4 100644 --- a/crates/weaveffi-fuzz/fuzz_targets/fuzz_validate.rs +++ b/crates/weaveffi-fuzz/fuzz_targets/fuzz_validate.rs @@ -4,9 +4,9 @@ #[cfg(fuzzing)] use libfuzzer_sys::fuzz_target; #[cfg(fuzzing)] -use weaveffi_core::validate::validate_api; +use weaveffi_model::parse::parse_api_str; #[cfg(fuzzing)] -use weaveffi_ir::parse::parse_api_str; +use weaveffi_model::validate::validate_api; #[cfg(fuzzing)] fuzz_target!(|data: &[u8]| { diff --git a/crates/weaveffi-fuzz/fuzz_targets/parse_json.rs b/crates/weaveffi-fuzz/fuzz_targets/parse_json.rs index 3933d48c..cd776b79 100644 --- a/crates/weaveffi-fuzz/fuzz_targets/parse_json.rs +++ b/crates/weaveffi-fuzz/fuzz_targets/parse_json.rs @@ -4,7 +4,7 @@ #[cfg(fuzzing)] use libfuzzer_sys::fuzz_target; #[cfg(fuzzing)] -use weaveffi_ir::parse::parse_api_str; +use weaveffi_model::parse::parse_api_str; #[cfg(fuzzing)] fuzz_target!(|data: &[u8]| { diff --git a/crates/weaveffi-fuzz/fuzz_targets/parse_toml.rs b/crates/weaveffi-fuzz/fuzz_targets/parse_toml.rs index 625d6ec8..f40aa89c 100644 --- a/crates/weaveffi-fuzz/fuzz_targets/parse_toml.rs +++ b/crates/weaveffi-fuzz/fuzz_targets/parse_toml.rs @@ -4,7 +4,7 @@ #[cfg(fuzzing)] use libfuzzer_sys::fuzz_target; #[cfg(fuzzing)] -use weaveffi_ir::parse::parse_api_str; +use weaveffi_model::parse::parse_api_str; #[cfg(fuzzing)] fuzz_target!(|data: &[u8]| { diff --git a/crates/weaveffi-fuzz/fuzz_targets/parse_type_ref.rs b/crates/weaveffi-fuzz/fuzz_targets/parse_type_ref.rs index 51c50d36..2f0d842e 100644 --- a/crates/weaveffi-fuzz/fuzz_targets/parse_type_ref.rs +++ b/crates/weaveffi-fuzz/fuzz_targets/parse_type_ref.rs @@ -7,7 +7,7 @@ use libfuzzer_sys::fuzz_target; #[cfg(fuzzing)] fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { - let _ = weaveffi_ir::ir::parse_type_ref(s); + let _ = weaveffi_model::ir::parse_type_ref(s); } }); diff --git a/crates/weaveffi-fuzz/fuzz_targets/parse_yaml.rs b/crates/weaveffi-fuzz/fuzz_targets/parse_yaml.rs index 83a34dca..35fcf582 100644 --- a/crates/weaveffi-fuzz/fuzz_targets/parse_yaml.rs +++ b/crates/weaveffi-fuzz/fuzz_targets/parse_yaml.rs @@ -4,7 +4,7 @@ #[cfg(fuzzing)] use libfuzzer_sys::fuzz_target; #[cfg(fuzzing)] -use weaveffi_ir::parse::parse_api_str; +use weaveffi_model::parse::parse_api_str; #[cfg(fuzzing)] fuzz_target!(|data: &[u8]| { diff --git a/crates/weaveffi-fuzz/tests/seeds.rs b/crates/weaveffi-fuzz/tests/seeds.rs index 2735346c..6c4ad984 100644 --- a/crates/weaveffi-fuzz/tests/seeds.rs +++ b/crates/weaveffi-fuzz/tests/seeds.rs @@ -4,16 +4,16 @@ //! 1. every committed seed is well-formed for its target's parser, and //! 2. each fuzz target's underlying call pattern (the body of the //! `fuzz_target!` macro invocation) keeps compiling against the upstream -//! APIs in `weaveffi-ir` and `weaveffi-core`. +//! APIs in `weaveffi-model`. //! //! If a parser or validator signature changes, this file is what should //! break first, long before a nightly fuzz run. use std::path::PathBuf; -use weaveffi_core::validate::validate_api; -use weaveffi_ir::ir::parse_type_ref; -use weaveffi_ir::parse::parse_api_str; +use weaveffi_model::ir::parse_type_ref; +use weaveffi_model::parse::parse_api_str; +use weaveffi_model::validate::validate_api; fn seed(target: &str, name: &str) -> String { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/crates/weaveffi-gen-c/Cargo.toml b/crates/weaveffi-gen-c/Cargo.toml deleted file mode 100644 index 3a03d70b..00000000 --- a/crates/weaveffi-gen-c/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "weaveffi-gen-c" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "C header and source file generator for WeaveFFI" -keywords = ["ffi", "codegen", "bindings", "c", "header"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } -camino = { workspace = true } -serde = { workspace = true } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-c/src/header.rs b/crates/weaveffi-gen-c/src/header.rs deleted file mode 100644 index 1222e897..00000000 --- a/crates/weaveffi-gen-c/src/header.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Rendering of the `{prefix}.h` header and its `{prefix}.c` companion. -//! -//! The per-declaration rendering is shared with the C++ backend via -//! [`weaveffi_core::cabi`]; this module adds only the header framing (include -//! guard, includes, prefix aliases, the value-buffer convention comment) and -//! the malloc/free-backed allocator defaults in the `.c` scaffold. - -use std::fmt::Write; - -use weaveffi_core::cabi; -use weaveffi_core::model::BindingModel; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{ - render_abi_prefix_aliases, render_prelude, render_trailer, CommentStyle, -}; - -/// Render the complete `{prefix}.h` for `api` using `prefix` for every symbol. -/// -/// Thin wrapper over [`render_c_header_from_model`] for tests and callers -/// that only hold a [`ResolvedApi`]; the production path renders directly -/// from the driver-built [`BindingModel`] without re-deriving it. -pub fn render_c_header( - api: &ResolvedApi, - prefix: &str, - input_basename: &str, - filename: &str, -) -> String { - render_c_header_from_model(&BindingModel::build(api, prefix), input_basename, filename) -} - -/// Render the complete header from the shared [`BindingModel`]. -/// -/// The per-declaration rendering is shared with the C++ backend via -/// [`weaveffi_core::cabi`]; this function only adds the header framing -/// (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 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 guard = format!("{}_H", prefix.to_uppercase()); - 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"); - out.push_str("#include \n"); - out.push_str("#include \n\n"); - cabi::render_visibility_macros(&mut out, prefix); - out.push_str(&render_abi_prefix_aliases(prefix)); - out.push_str("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n"); - cabi::render_runtime_decls(&mut out, prefix); - out.push_str("/*\n"); - out.push_str(" * Value buffer convention: records, rich enums, lists, maps, and\n"); - out.push_str(" * optionals cross the ABI serialized in the WeaveFFI value buffer\n"); - out.push_str(" * format (little-endian; see the generated bindings or the WeaveFFI\n"); - out.push_str(" * buffer protocol spec for the per-type encoding).\n"); - out.push_str(" * A buffered parameter named \"v\" expands to a borrowed view:\n"); - out.push_str(" * const uint8_t* v_ptr, size_t v_len\n"); - out.push_str(" * A buffered return is a producer-allocated buffer returned as\n"); - out.push_str(" * `const uint8_t*` with a trailing `size_t* out_len`; the caller\n"); - out.push_str(&format!( - " * decodes it and then releases it with {prefix}_free_bytes.\n" - )); - out.push_str(" */\n\n"); - - 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"); - out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); - out -} - -/// Render the `{prefix}.c` companion: malloc/free-backed defaults for the -/// linear-memory allocator the header declares, plus room for future -/// convenience wrappers. -pub(crate) fn render_c_convenience_c(prefix: &str, input_basename: &str, filename: &str) -> String { - let api = format!("{}_API", prefix.to_uppercase()); - let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); - let _ = write!( - out, - "#include \"{prefix}.h\"\n\n\ - #include \n\n\ - // Default implementations of the linear-memory allocator declared in\n\ - // {prefix}.h. The Wasm JS glue stages call arguments through these; a\n\ - // producer that ships its own allocator can omit this file from the\n\ - // build. The size is unused on dealloc because free() recovers it.\n\ - {api} uint8_t* {prefix}_alloc(uint32_t size) {{\n\ - \x20 return (uint8_t*)malloc(size ? size : 1u);\n\ - }}\n\n\ - {api} void {prefix}_dealloc(uint8_t* ptr, uint32_t size) {{\n\ - \x20 (void)size;\n\ - \x20 free(ptr);\n\ - }}\n\n\ - // Optional convenience wrappers can be added here in future versions.\n\n" - ); - out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); - out -} diff --git a/crates/weaveffi-gen-c/src/lib.rs b/crates/weaveffi-gen-c/src/lib.rs deleted file mode 100644 index 50801d6b..00000000 --- a/crates/weaveffi-gen-c/src/lib.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! C header generator for WeaveFFI. -//! -//! Emits a single `{prefix}.h` describing the stable C ABI surface of a -//! [`ResolvedApi`], plus a companion -//! `{prefix}.c` placeholder for future convenience -//! wrappers. This is the canonical backend: the header it emits *is* the C ABI -//! every other language binds to. -//! -//! Like every WeaveFFI backend it renders from the shared -//! [`weaveffi_core::model::BindingModel`], so symbol names and parameter -//! lowering are computed once and shared, never re-derived here. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod header; -mod package; -use camino::Utf8Path; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::resolved::ResolvedApi; - -pub use header::{render_c_header, render_c_header_from_model}; - -use header::render_c_convenience_c; -use package::{render_packaged_cmake, render_packaged_readme}; - -/// Per-target configuration for [`CGenerator`]. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct CConfig { - /// Prefix applied to every emitted C symbol (default `"weaveffi"`). - /// Renames produce both `prefix_*` user symbols and - /// `#define prefix_runtime weaveffi_runtime` aliases for the ABI helpers. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with (e.g. `weaveffi.yml`). - /// Embedded in the prelude header of every generated file. Populated - /// by the CLI; not user-configurable via the `[c]` config section. - #[serde(skip)] - pub input_basename: Option, -} - -impl CConfig { - /// Returns the configured symbol prefix, falling back to `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// C backend: emits the canonical `{prefix}.h` header describing the stable -/// C ABI surface, plus a `{prefix}.c` companion for future wrappers. -pub struct CGenerator; - -impl LanguageBackend for CGenerator { - type Config = CConfig; - - fn name(&self) -> &'static str { - "c" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn files( - &self, - _api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let prefix = config.prefix(); - let input_basename = config.input_basename(); - let dir = out_dir.join("c"); - let header_name = format!("{prefix}.h"); - let source_name = format!("{prefix}.c"); - vec![ - OutputFile::new( - dir.join(&header_name), - render_c_header_from_model(model, input_basename, &header_name), - ), - OutputFile::new( - dir.join(&source_name), - render_c_convenience_c(prefix, input_basename, &source_name), - ), - ] - } - - fn package( - &self, - _api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let prefix = config.prefix(); - let input_basename = config.input_basename(); - let dir = out_dir.join("c"); - let header_name = format!("{prefix}.h"); - let lib = &ctx.binaries.lib_name; - - let mut files = vec![ - PackagedFile::text( - dir.join("include").join(&header_name), - render_c_header_from_model(model, input_basename, &header_name), - ), - PackagedFile::text( - dir.join("CMakeLists.txt"), - render_packaged_cmake(lib, input_basename), - ), - PackagedFile::text( - dir.join("README.md"), - render_packaged_readme(lib, &header_name, ctx, input_basename), - ), - ]; - for nb in &ctx.binaries.binaries { - let dest = dir - .join("lib") - .join(nb.platform.id()) - .join(ctx.binaries.bundled_filename(nb.platform)); - files.push(PackagedFile::copy(dest, nb.source.clone())); - } - Some(files) - } -} diff --git a/crates/weaveffi-gen-c/tests/render_samples.rs b/crates/weaveffi-gen-c/tests/render_samples.rs deleted file mode 100644 index 186d11a5..00000000 --- a/crates/weaveffi-gen-c/tests/render_samples.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Dev helper: render the C header for each sample IDL to `target/dev-c/`. -//! -//! Ignored by default; run with `cargo test -p weaveffi-gen-c --test -//! render_samples -- --ignored` to refresh the rendered headers while the -//! full CLI is unavailable. The conformance suite remains the real check. - -use weaveffi_core::backend::LanguageBackend; -use weaveffi_core::model::BindingModel; - -#[test] -#[ignore = "dev helper, writes rendered headers under target/"] -fn render_sample_headers() { - let root = concat!(env!("CARGO_MANIFEST_DIR"), "/../.."); - let out_dir = format!("{root}/target/dev-c"); - std::fs::create_dir_all(&out_dir).unwrap(); - for sample in ["kvstore", "contacts", "events", "shapes", "calculator"] { - let src = std::fs::read_to_string(format!("{root}/samples/{sample}/{sample}.yml")).unwrap(); - let api = weaveffi_ir::parse::parse_api_str(&src, "yaml").unwrap(); - let api = weaveffi_core::validate::validate_api(api, None).unwrap(); - let model = BindingModel::build(&api, "weaveffi"); - let gen = weaveffi_gen_c::CGenerator; - let cfg = weaveffi_gen_c::CConfig::default(); - let out = camino::Utf8PathBuf::from(&out_dir).join(sample); - for f in gen.files(&api, &model, &out, &cfg) { - std::fs::create_dir_all(f.path.parent().unwrap()).unwrap(); - std::fs::write(&f.path, &f.contents).unwrap(); - eprintln!("wrote {}", f.path); - } - } -} diff --git a/crates/weaveffi-gen-cpp/Cargo.toml b/crates/weaveffi-gen-cpp/Cargo.toml deleted file mode 100644 index a0f7fd42..00000000 --- a/crates/weaveffi-gen-cpp/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "weaveffi-gen-cpp" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "WeaveFFI C++ RAII wrapper generator" -keywords = ["ffi", "codegen", "bindings", "cpp", "raii"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-cpp/src/package.rs b/crates/weaveffi-gen-cpp/src/package.rs deleted file mode 100644 index 6c1575ec..00000000 --- a/crates/weaveffi-gen-cpp/src/package.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! Build-integration files: the `CMakeLists.txt` skeleton, the packaged -//! CMake importing prebuilt libraries, and the READMEs. -//! -//! CMake and Markdown are the only formats emitted here; neither is JSON or -//! XML, so the shared manifest escaping helpers don't apply. The only -//! interpolated values are the resolved package version and library name, -//! both validated identifiers rather than free-form user prose. - -use weaveffi_core::package::PackageContext; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -/// Render a `CMakeLists.txt` that imports the bundled per-platform library as -/// the `weaveffi` target and links it into the `weaveffi_cpp` INTERFACE -/// library, selecting the right library for the host platform. -pub(crate) fn render_packaged_cmake( - lib: &str, - version: &str, - cpp_std: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Hash, input_basename); - let trailer = render_trailer(CommentStyle::Hash, "CMakeLists.txt"); - let body = r#"cmake_minimum_required(VERSION 3.14) -project(weaveffi_cpp VERSION @VERSION@) - -# Select the prebuilt native library bundled for the host platform/arch. -if(APPLE) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64") - set(_wv_plat "darwin-arm64") - else() - set(_wv_plat "darwin-x64") - endif() - set(_wv_libfile "lib@LIB@.dylib") -elseif(WIN32) - set(_wv_plat "windows-x64") - set(_wv_libfile "@LIB@.dll") -else() - if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") - set(_wv_plat "linux-arm64") - else() - set(_wv_plat "linux-x64") - endif() - set(_wv_libfile "lib@LIB@.so") -endif() - -add_library(weaveffi SHARED IMPORTED GLOBAL) -set_target_properties(weaveffi PROPERTIES - IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/lib/${_wv_plat}/${_wv_libfile}") - -add_library(weaveffi_cpp INTERFACE) -target_include_directories(weaveffi_cpp INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) -target_link_libraries(weaveffi_cpp INTERFACE weaveffi) -target_compile_features(weaveffi_cpp INTERFACE cxx_std_@STD@) -"# - .replace("@VERSION@", version) - .replace("@LIB@", lib) - .replace("@STD@", cpp_std); - format!("{prelude}{body}\n{trailer}") -} - -/// README for a packaged C++ artifact bundling the header and the desktop -/// per-platform libraries the packaged CMake can select among. -pub(crate) fn render_packaged_readme( - lib: &str, - header_name: &str, - ctx: &PackageContext, - input_basename: &str, -) -> String { - let mut out = render_prelude(CommentStyle::Xml, input_basename); - let platforms: Vec = ctx - .binaries - .platforms() - .filter(|p| p.is_desktop()) - .map(|p| format!("- `lib/{}/`", p.id())) - .collect(); - let platform_list = platforms.join("\n"); - out.push_str(&format!( - "# {lib} (C++) - -An idiomatic wrapper header (`include/{header_name}`) plus a prebuilt shared -library for each supported platform under `lib//`. - -## Use with CMake - -```cmake -add_subdirectory(path/to/cpp) -target_link_libraries(your_app PRIVATE weaveffi_cpp) -``` - -`CMakeLists.txt` selects the right library for the host platform and links it -into the `weaveffi_cpp` interface target automatically. - -## Bundled platforms - -{platform_list} - -" - )); - out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); - out -} - -/// Render the source-layout `CMakeLists.txt`: an INTERFACE library adding the -/// generated header directory to the include path and linking `weaveffi`. -pub(crate) fn render_cmake(cpp_std: &str, version: &str, input_basename: &str) -> String { - let mut out = render_prelude(CommentStyle::Hash, input_basename); - out.push_str(&format!( - "cmake_minimum_required(VERSION 3.14)\n\ -project(weaveffi_cpp VERSION {version})\n\ -add_library(weaveffi_cpp INTERFACE)\n\ -target_include_directories(weaveffi_cpp INTERFACE ${{CMAKE_CURRENT_SOURCE_DIR}})\n\ -target_link_libraries(weaveffi_cpp INTERFACE weaveffi)\n\ -target_compile_features(weaveffi_cpp INTERFACE cxx_std_{cpp_std})\n\n" - )); - out.push_str(&render_trailer(CommentStyle::Hash, "CMakeLists.txt")); - out -} - -/// README for the source layout: prerequisites and CMake usage. -pub(crate) fn render_readme(input_basename: &str) -> String { - let mut out = render_prelude(CommentStyle::Xml, input_basename); - out.push_str( - "# 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\" -``` - -", - ); - out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); - out -} diff --git a/crates/weaveffi-gen-cpp/src/runtime.rs b/crates/weaveffi-gen-cpp/src/runtime.rs deleted file mode 100644 index c2b98af5..00000000 --- a/crates/weaveffi-gen-cpp/src/runtime.rs +++ /dev/null @@ -1,263 +0,0 @@ -//! The private runtime prelude of the generated header: the generic error -//! surface, the ABI revision check, and the value-buffer reader/writer. - -use weaveffi_core::codegen::CodeWriter; - -/// Emit the generic `WeaveFFIError`, the `check_abi_version()` helper, and -/// the `detail::check`/`detail::make_error` helpers every non-throwing wrapper -/// uses. A nonzero code on a non-throwing callable can only be a runtime trap -/// (a producer panic, a marshalling failure, or a callback-interface -/// implementation that raised), so it surfaces as this generic exception -/// rather than a typed domain error. -pub(crate) fn render_generic_error(out: &mut String, prefix: &str) { - let upper = prefix.to_uppercase(); - let mut w = CodeWriter::four_space(); - w.line("/**"); - w.line(" * Base exception for every error reported through the C ABI."); - w.line(" *"); - w.line(" * Positive codes are the module's declared error codes (see the typed"); - w.line(" * domain exceptions); negative codes are runtime traps: -1 a generic"); - w.line(" * producer error, -2 a producer panic, -3 a marshalling failure, -4 a"); - w.line(" * callback-interface implementation that threw."); - w.line(" */"); - w.line("class WeaveFFIError : public std::runtime_error {"); - w.scope(|w| { - w.line("int32_t code_;"); - w.blank(); - }); - w.line("public:"); - w.scope(|w| { - w.line("WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {}"); - w.line("int32_t code() const { return code_; }"); - }); - w.line("};"); - w.blank(); - - w.line("/**"); - w.line(" * Throws WeaveFFIError when the loaded producer library was built for a"); - w.line(" * different WeaveFFI ABI revision than this header. Call once at startup."); - w.line(" */"); - w.line("inline void check_abi_version() {"); - w.scope(|w| { - w.line(format!("uint32_t actual = {prefix}_abi_version();")); - w.line(format!("if (actual != {upper}_ABI_VERSION) {{")); - w.scope(|w| { - w.line(format!( - "throw WeaveFFIError(-1, \"WeaveFFI ABI revision mismatch: header \" + std::to_string({upper}_ABI_VERSION) + \", library \" + std::to_string(actual));" - )); - }); - w.line("}"); - }); - w.line("}"); - w.blank(); - - w.line("namespace detail {"); - w.blank(); - w.line("/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */"); - w.line(format!("inline void check({prefix}_error& err) {{")); - w.scope(|w| { - w.line("if (err.code == 0) return;"); - w.line("std::string msg(err.message ? err.message : \"unknown error\");"); - w.line("int32_t code = err.code;"); - w.line(format!("{prefix}_error_clear(&err);")); - w.line("throw WeaveFFIError(code, msg);"); - }); - w.line("}"); - w.blank(); - w.line("/** Wrap an async-callback error as the generic WeaveFFIError. */"); - w.line("inline std::exception_ptr make_error(int32_t code, const std::string& msg) {"); - w.scope(|w| { - w.line("return std::make_exception_ptr(WeaveFFIError(code, msg));"); - }); - w.line("}"); - w.blank(); - w.line("} // namespace detail"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the private value-buffer runtime: a writer and reader implementing -/// the WeaveFFI wire format (little-endian, packed, `u32` lengths), plus a -/// scope guard that releases producer-allocated buffers. A malformed buffer -/// is a producer/consumer contract violation, so decode failures throw the -/// generic `WeaveFFIError` (the producer-panic channel), never a typed -/// domain error. -pub(crate) fn render_buffer_runtime(out: &mut String, prefix: &str) { - let body = r#"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. Object tokens adopted before a - * failure live in RAII wrappers that release them on unwind. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - [[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 @PREFIX@_free_bytes on scope exit. */ -struct BufferGuard { - /** The producer-allocated buffer, or null when the call reported an error. */ - const uint8_t* ptr; - /** The buffer length in bytes. */ - size_t len; - - ~BufferGuard() { - if (ptr != nullptr) @PREFIX@_free_bytes(const_cast(ptr), len); - } -}; - -} // namespace detail - -"#; - out.push_str(&body.replace("@PREFIX@", prefix)); -} diff --git a/crates/weaveffi-gen-dart/Cargo.toml b/crates/weaveffi-gen-dart/Cargo.toml deleted file mode 100644 index f3aa69b4..00000000 --- a/crates/weaveffi-gen-dart/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "weaveffi-gen-dart" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "WeaveFFI Dart FFI binding generator" -keywords = ["ffi", "codegen", "bindings", "dart", "flutter"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-dart/src/callbacks.rs b/crates/weaveffi-gen-dart/src/callbacks.rs deleted file mode 100644 index dcb37ef4..00000000 --- a/crates/weaveffi-gen-dart/src/callbacks.rs +++ /dev/null @@ -1,372 +0,0 @@ -//! Callback-interface rendering: the abstract class a consumer implements, -//! the `dart:ffi` `Struct` mirroring the C vtable, one trampoline per method -//! (plus `free`), and the single process-wide vtable instance whose entries -//! are `NativeCallable` function pointers pinned for the process lifetime. -//! -//! Trampolines follow the [`CallbackProtocol`] clauses: borrowed strings, -//! bytes, and buffers are copied or decoded inside the call and never freed; -//! object arguments transfer one strong reference and are adopted into a -//! wrapper; direct returns are written straight into the C return; a thrown -//! Dart exception is reported through `{prefix}_error_set` with the foreign -//! error code and a default value is returned, so nothing ever unwinds through -//! the C frame. -//! -//! [`CallbackProtocol`]: weaveffi_core::plan::CallbackProtocol - -use heck::ToUpperCamelCase; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::{CallbackInterfaceBinding, CallbackMethodBinding, Ty}; -use weaveffi_core::plan::ArgPass; - -use crate::calls::adopt_expr; -use crate::codec::read_expr; -use crate::docs::emit_doc; -use crate::types::{ - dart_class, dart_ident, dart_str_literal, dart_type, default_literal, input_slots, scalar_ffi, - vtable_var, -}; - -/// The Dart `Struct` subclass mirroring one callback interface's C vtable. -fn vtable_struct(class: &str) -> String { - format!("_{class}VtableStruct") -} - -/// The native function typedef of one vtable entry. -fn entry_typedef(class: &str, method: &str) -> String { - format!("_Native{class}Vt{}", method.to_upper_camel_case()) -} - -/// The Dart trampoline function bound into one vtable entry. -fn trampoline_fn(class: &str, method: &str) -> String { - format!("_{}Vt{}", lower_first(class), method.to_upper_camel_case()) -} - -/// Lower-case the first character of a PascalCase class name. -fn lower_first(s: &str) -> String { - let mut chars = s.chars(); - match chars.next() { - Some(c) => c.to_lowercase().collect::() + chars.as_str(), - None => String::new(), - } -} - -/// The (native, dart) return types of one vtable entry; a void method is -/// `(Void, void)`, everything else is a direct scalar. -fn entry_ret(m: &CallbackMethodBinding) -> (String, String) { - match &m.ret { - None => ("Void".into(), "void".into()), - Some(ty) => { - let (n, d) = scalar_ffi(ty); - (n.into(), d.into()) - } - } -} - -/// The `(native type, dart type, dart name)` slot triples of one vtable -/// entry, in ABI order: `ctx`, every parameter's slots, then `out_err`. -fn entry_slots(m: &CallbackMethodBinding) -> Vec<(String, String, String)> { - let mut slots = vec![( - "Pointer".to_string(), - "Pointer".to_string(), - "ctx".to_string(), - )]; - for p in &m.params { - for ((n, d), slot) in input_slots(p).into_iter().zip(p.abi.iter()) { - slots.push((n, d, dart_ident(&slot.name))); - } - } - slots.push(( - "Pointer<_WeaveFFIError>".into(), - "Pointer<_WeaveFFIError>".into(), - "outErr".into(), - )); - slots -} - -/// Render one callback interface: the consumer-facing abstract class, the -/// vtable `Struct`, the per-method trampolines and `free`, and the one static -/// vtable instance. -pub(crate) fn render_callback_interface(out: &mut String, cb: &CallbackInterfaceBinding) { - let class = dart_class(&cb.name); - render_abstract_class(out, cb, &class); - render_vtable_struct(out, cb, &class); - for m in &cb.methods { - render_trampoline(out, cb, m, &class); - } - render_free(out, &class); - render_vtable_instance(out, cb, &class); -} - -/// The abstract class a consumer extends or implements, one abstract method -/// per vtable entry with idiomatic Dart parameter and return types. -fn render_abstract_class(out: &mut String, cb: &CallbackInterfaceBinding, class: &str) { - let mut w = CodeWriter::two_space(); - w.blank(); - { - let mut d = String::new(); - emit_doc(&mut d, &cb.doc, ""); - w.raw(d); - } - if cb.doc.is_some() { - w.line("///"); - } - w.line("/// A callback interface: implement this class and pass an instance to any"); - w.line(format!( - "/// function taking a [{class}]. The producer keeps the instance alive" - )); - w.line("/// (through a private handle table) until it releases the callback, so no"); - w.line("/// other reference needs to be held."); - w.line("///"); - w.line("/// Object arguments are owned by the implementation: call `dispose()` on"); - w.line("/// them (or let the finalizer run) when done. A thrown exception is reported"); - w.line("/// to the producer, which aborts the call it was making; the original Dart"); - w.line("/// caller then observes a [WeaveFFIException] with [WeaveFFIException.foreignCode]"); - w.line("/// carrying the exception's text."); - w.line("///"); - w.line("/// Dart limitation: methods are bound with `NativeCallable.isolateLocal`, so"); - w.line("/// the producer may only invoke them on the thread of the isolate that"); - w.line("/// passed the callback. That holds when the producer calls them"); - w.line("/// synchronously during a call from Dart. A producer that invokes a method"); - w.line("/// from another thread (an async task, a background worker) is unsupported:"); - w.line("/// a `NativeCallable.listener` cannot return a value or read borrowed"); - w.line("/// arguments after the native frame returns."); - if let Some(msg) = &cb.deprecated { - w.line(format!("@Deprecated('{}')", dart_str_literal(msg))); - } - w.block(format!("abstract class {class} {{"), "}", |w| { - for m in &cb.methods { - { - let mut d = String::new(); - emit_doc(&mut d, &m.doc, " "); - w.raw(d); - } - if let Some(msg) = &m.deprecated { - w.line(format!("@Deprecated('{}')", dart_str_literal(msg))); - } - let params: Vec = m - .params - .iter() - .map(|p| format!("{} {}", dart_type(&p.ty), dart_ident(&p.name))) - .collect(); - let ret = m.ret.as_ref().map_or("void".to_string(), dart_type); - w.line(format!( - "{ret} {}({});", - dart_ident(&m.name), - params.join(", ") - )); - } - }); - out.push_str(&w.finish()); -} - -/// The `Struct` whose layout is the C vtable: one function pointer per -/// method in declaration order, then the trailing `free`. -fn render_vtable_struct(out: &mut String, cb: &CallbackInterfaceBinding, class: &str) { - let mut w = CodeWriter::two_space(); - w.blank(); - for m in &cb.methods { - let (native_ret, _) = entry_ret(m); - let natives: Vec = entry_slots(m).into_iter().map(|(n, _, _)| n).collect(); - w.line(format!( - "typedef {} = {native_ret} Function({});", - entry_typedef(class, &m.name), - natives.join(", ") - )); - } - w.line(format!( - "typedef {} = Void Function(Pointer);", - entry_typedef(class, "free") - )); - w.blank(); - w.line(format!("// The C `{}` layout.", cb.vtable_tag)); - w.block( - format!("final class {} extends Struct {{", vtable_struct(class)), - "}", - |w| { - for m in &cb.methods { - w.line(format!( - "external Pointer> {};", - entry_typedef(class, &m.name), - dart_ident(&m.name) - )); - } - w.line(format!( - "external Pointer> free;", - entry_typedef(class, "free") - )); - }, - ); - out.push_str(&w.finish()); -} - -/// Emit the statements converting one method's borrowed slots into the values -/// handed to the implementation, returning the argument expressions. Every -/// conversion happens inside the call (the borrow window); nothing is freed. -fn emit_trampoline_args(w: &mut CodeWriter, m: &CallbackMethodBinding) -> Vec { - let mut args = Vec::new(); - for p in &m.params { - let base = dart_ident(&p.name); - let n0 = dart_ident(&p.abi[0].name); - args.push(match p.arg_pass() { - // Decode the borrowed encoding now; object tokens inside it are - // adopted by the wrappers the codec constructs. - ArgPass::Buffer { len, .. } => { - let n1 = dart_ident(&len.name); - w.line(format!("final {base}Data = _copyNativeBytes({n0}, {n1});")); - w.line(format!("final {base}Reader = _BufferReader({base}Data);")); - w.line(format!( - "final {base}Value = {};", - read_expr(&format!("{base}Reader"), &p.ty) - )); - w.line(format!("{base}Reader.expectEnd();")); - format!("{base}Value") - } - ArgPass::String { .. } => { - format!("{n0} == nullptr ? '' : {n0}.toDartString()") - } - ArgPass::Bytes { len, .. } => { - let n1 = dart_ident(&len.name); - format!("{n0} == nullptr ? [] : {n0}.asTypedList({n1}).toList()") - } - // One strong reference transfers to the implementation: adopt it - // into a local first, so a later failure (a bad `ctx`, a malformed - // buffer) still leaves the reference owned by a finalizable - // wrapper instead of leaking it. - ArgPass::Object { nullable, .. } => { - w.line(format!( - "final {base}Value = {};", - adopt_expr(&n0, &p.ty, nullable) - )); - format!("{base}Value") - } - ArgPass::Callback { .. } => { - unreachable!("validation rejects callback interfaces as callback-method parameters") - } - ArgPass::Direct { .. } => match &p.ty { - Ty::Enum(name) => format!("{}.fromValue({n0})", dart_class(name)), - _ => n0, - }, - }); - } - args -} - -/// One method trampoline: look the implementation up by `ctx`, convert the -/// arguments, call the method, and write a direct return. Any exception is -/// reported through `_foreignError` (`{prefix}_error_set` with the foreign -/// code) and a default value is returned instead of unwinding. -fn render_trampoline( - out: &mut String, - cb: &CallbackInterfaceBinding, - m: &CallbackMethodBinding, - class: &str, -) { - let (_, dart_ret) = entry_ret(m); - let decls: Vec = entry_slots(m) - .into_iter() - .map(|(_, d, name)| format!("{d} {name}")) - .collect(); - let method = dart_ident(&m.name); - let default = m.ret.as_ref().map(default_literal); - - let mut w = CodeWriter::two_space(); - w.blank(); - w.line(format!("// Vtable entry `{}.{}`.", cb.vtable_tag, m.name)); - w.block( - format!( - "{dart_ret} {}({}) {{", - trampoline_fn(class, &m.name), - decls.join(", ") - ), - "}", - |w| { - w.line("try {"); - w.scope(|w| { - // Arguments first: object arguments must be adopted before - // anything that can throw. - let args = emit_trampoline_args(w, m); - w.line(format!("final impl = _callbackFor(ctx) as {class};")); - let call = format!("impl.{method}({})", args.join(", ")); - match &m.ret { - None => w.line(format!("{call};")), - Some(Ty::Enum(_)) => w.line(format!("return {call}.value;")), - Some(_) => w.line(format!("return {call};")), - }; - }); - w.line("} catch (e) {"); - w.scope(|w| { - w.line("_foreignError(outErr, e);"); - if let Some(d) = default { - w.line(format!("return {d};")); - } - }); - w.line("}"); - }, - ); - out.push_str(&w.finish()); -} - -/// The `free(ctx)` trampoline: drop the handle-table entry. The producer never -/// touches `ctx` again after this fires. It's a `NativeCallable.listener` -/// because the producer may release its last reference from any thread (an -/// async completion, a background drop) and the removal can safely run -/// later on the isolate. -fn render_free(out: &mut String, class: &str) { - let mut w = CodeWriter::two_space(); - w.blank(); - w.block( - format!( - "void {}(Pointer ctx) {{", - trampoline_fn(class, "free") - ), - "}", - |w| { - w.line("_callbackTable.remove(ctx.address);"); - }, - ); - out.push_str(&w.finish()); -} - -/// The one static vtable for the interface: allocated once with `calloc`, its -/// entries `NativeCallable.isolateLocal` trampolines (and a `listener` for -/// `free`) pinned in `_callbackCallables` so their native thunks live for the -/// process lifetime. -fn render_vtable_instance(out: &mut String, cb: &CallbackInterfaceBinding, class: &str) { - let strukt = vtable_struct(class); - let mut w = CodeWriter::two_space(); - w.blank(); - w.line(format!( - "// The process-wide static `{}` every {class} passed to the producer", - cb.vtable_tag - )); - w.line("// shares; the per-instance state travels in `ctx`."); - w.block( - format!("final Pointer<{strukt}> {} = () {{", vtable_var(&cb.name)), - "}();", - |w| { - w.line(format!("final vt = calloc<{strukt}>();")); - for m in &cb.methods { - let td = entry_typedef(class, &m.name); - let exceptional = match &m.ret { - None => String::new(), - Some(ty) => format!(", exceptionalReturn: {}", default_literal(ty)), - }; - w.line(format!( - "vt.ref.{} = _pinCallable(NativeCallable<{td}>.isolateLocal(", - dart_ident(&m.name) - )); - w.line(format!( - " {}{exceptional}));", - trampoline_fn(class, &m.name) - )); - } - w.line(format!( - "vt.ref.free = _pinCallable(NativeCallable<{}>.listener(", - entry_typedef(class, "free") - )); - w.line(format!(" {}));", trampoline_fn(class, "free"))); - w.line("return vt;"); - }, - ); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-dart/src/calls.rs b/crates/weaveffi-gen-dart/src/calls.rs deleted file mode 100644 index 9a9b6b46..00000000 --- a/crates/weaveffi-gen-dart/src/calls.rs +++ /dev/null @@ -1,888 +0,0 @@ -//! Callable rendering: the FFI typedef/lookup pairs and idiomatic Dart -//! wrappers for sync, async, and iterator callables. -//! -//! 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 [`Ty`]s. - -use heck::ToLowerCamelCase; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{CallShape, FnBinding, IteratorBinding, ModuleBinding, ParamBinding}; -use weaveffi_core::plan::{self, ArgPass, ErrorStrategy, RetPass}; - -use crate::codec::{read_expr, write_stmts}; -use crate::docs::emit_wrapper_doc; -use crate::entities::dart_exception_name; -use crate::runtime::emit_typedef_and_lookup; -use crate::types::{ - dart_class, dart_ident, dart_type, dart_wrapper_fn_name, input_slots, object_class, return_ffi, - return_out_slots, returns_buffer, scalar_ffi, vtable_var, -}; - -/// Error-reporting context for one wrapper: which check helper guards its -/// out-err slot and which exception its async completion path constructs. -/// -/// The split follows [`ErrorStrategy`]: a throwing callable maps `out_err` -/// onto the module's typed domain exception, while a non-throwing callable -/// traps through the generic brand exception (a reported error there is only -/// ever a producer bug or a runtime trap, never a domain error). -#[derive(Clone, Copy)] -pub(crate) struct ErrCtx<'a> { - /// `true` when the wrapper surfaces typed domain errors (`throws: true`). - throws: bool, - /// The domain exception class in effect (`KvException` names `_checkKvException` - /// and `_mapKvException`); `None` when no error domain is in scope. - exception: Option<&'a str>, -} - -impl<'a> ErrCtx<'a> { - /// The domain exception this wrapper throws, or `None` for a non-throwing - /// wrapper (which reports every failure as the generic brand exception). - pub(crate) fn thrown_exception(&self) -> Option<&'a str> { - self.exception.filter(|_| self.throws) - } - - /// The statement checking the wrapper's `err` slot after a call. - fn check_stmt(&self) -> String { - match self.thrown_exception() { - Some(exc) => format!("_check{exc}(err);"), - None => "_checkError(err);".to_string(), - } - } - - /// The expression building the exception for an async completion's - /// already-captured `code`/`msg` (and, for a domain error, `payload`) - /// locals. - fn map_expr(&self) -> String { - match self.thrown_exception() { - Some(exc) => format!("_map{exc}(code, msg, payload)"), - None => "WeaveFFIException(code, msg)".to_string(), - } - } -} - -/// The [`ErrCtx`] for one callable of a module: its [`ErrorStrategy`] paired -/// with the exception class of the domain in effect (own or inherited). -pub(crate) fn err_ctx<'a>(f: &FnBinding, exception: Option<&'a str>) -> ErrCtx<'a> { - ErrCtx { - throws: matches!(f.error_strategy(), ErrorStrategy::Throws), - exception, - } -} - -/// How one rendered wrapper is declared in Dart source: a top-level function, -/// or a member (method, static, or factory constructor) of an interface class. -pub(crate) enum DartDecl<'a> { - /// A top-level free function. - TopLevel, - /// An instance method of an interface class: the FFI call passes the - /// wrapper's `_handle` as the implicit leading argument. - Method, - /// A `static` method of an interface class. - Static, - /// A `factory` constructor of the interface class. `named` is `false` for - /// the canonical `new` constructor (`factory Store(...)`) and `true` for - /// every other constructor (`factory Store.open(...)`). - Factory { - /// The interface class the factory constructs. - class_name: &'a str, - /// `false` for the canonical `new` constructor. - named: bool, - }, -} - -impl DartDecl<'_> { - /// The declaration's opening line (through the `{`). `ret` is the public - /// return type, already wrapped in `Future<...>` for an async member. - fn open_line(&self, ret: &str, name: &str, params: &str) -> String { - match self { - DartDecl::TopLevel | DartDecl::Method => format!("{ret} {name}({params}) {{"), - DartDecl::Static => format!("static {ret} {name}({params}) {{"), - DartDecl::Factory { - class_name, - named: false, - } => format!("factory {class_name}({params}) {{"), - DartDecl::Factory { - class_name, - named: true, - } => format!("factory {class_name}.{name}({params}) {{"), - } - } - - /// The opening line of a `sync*` generator wrapper (an `iter` return). - /// Constructors never return iterators, so no factory spelling exists. - fn open_line_sync_star(&self, ret: &str, name: &str, params: &str) -> String { - match self { - DartDecl::TopLevel | DartDecl::Method => format!("{ret} {name}({params}) sync* {{"), - DartDecl::Static => format!("static {ret} {name}({params}) sync* {{"), - DartDecl::Factory { .. } => { - unreachable!("constructors cannot return iterators") - } - } - } -} - -/// Render one free function of `module` at top level. -pub(crate) fn render_function( - out: &mut String, - module: &ModuleBinding, - f: &FnBinding, - strip: bool, -) { - let name = dart_wrapper_fn_name(&module.path, &f.name, strip); - let exc = module - .error - .as_ref() - .map(|e| dart_exception_name(&e.type_name)); - let mut decl = String::new(); - render_callable( - out, - &mut decl, - f, - &DartDecl::TopLevel, - &name, - err_ctx(f, exc.as_deref()), - ); - out.push_str(&decl); -} - -/// Render one callable: its FFI typedefs and lookups into `lookups` (always -/// top-level) and its Dart wrapper declaration into `decl` (top-level for a -/// free function, spliced into the class body for an interface member). -pub(crate) fn render_callable( - lookups: &mut String, - decl: &mut String, - f: &FnBinding, - kind: &DartDecl, - name: &str, - err: ErrCtx, -) { - // `c_base` is the prefixed `{prefix}_{module}_{name}` symbol the shared - // BindingModel already computed; the async/iterator suffixing matches the C - // ABI by construction. - let c_sym = f.c_base.as_str(); - let pub_ret = f.ret.as_ref().map_or("void".into(), dart_type); - let wrapper_params: Vec = f - .params - .iter() - .map(|p| format!("{} {}", dart_type(&p.ty), dart_ident(&p.name))) - .collect(); - - if f.is_async { - render_async_function( - lookups, - decl, - c_sym, - f, - kind, - name, - &pub_ret, - &wrapper_params, - err, - ); - return; - } - - // Each input parameter expands to its ABI slots (bytes and buffered - // values fan out to `(ptr, len)`, callback interfaces to `(ctx, vtable)`); - // a bytes or buffered return adds its `out_len` slot; the trailing error - // slot closes the signature. An instance method's `AbiFn` carries an - // implicit leading `self` pointer. - let mut native_params: Vec = Vec::new(); - let mut dart_params: Vec = Vec::new(); - if f.has_self { - native_params.push("Pointer".into()); - dart_params.push("Pointer".into()); - } - for p in &f.params { - for (n, d) in input_slots(p) { - native_params.push(n); - dart_params.push(d); - } - } - if let Some(ret) = &f.ret { - if !matches!(f.shape, CallShape::Iterator(_)) { - for (n, d) in return_out_slots(ret) { - native_params.push(n); - dart_params.push(d); - } - } - } - native_params.push("Pointer<_WeaveFFIError>".into()); - dart_params.push("Pointer<_WeaveFFIError>".into()); - - let (native_ret, dart_ret) = match &f.shape { - // The iterator launcher returns the opaque iterator handle. - CallShape::Iterator(_) => ("Pointer".to_string(), "Pointer".to_string()), - _ => match &f.ret { - Some(ret) => return_ffi(ret), - None => ("Void".into(), "void".into()), - }, - }; - - emit_typedef_and_lookup( - lookups, - c_sym, - &native_params.join(", "), - &dart_params.join(", "), - &native_ret, - &dart_ret, - ); - - // Iterator-returning functions also bind the element `next`/`destroy` - // symbols plus the GC-finalizer backstop for abandoned iterations. - if let CallShape::Iterator(ib) = &f.shape { - emit_iter_lookups(lookups, ib); - } - - let mut w = CodeWriter::two_space(); - w.blank(); - emit_wrapper_doc(&mut w, f, err); - let params = wrapper_params.join(", "); - if let CallShape::Iterator(ib) = &f.shape { - // The wrapper is a lazy `sync*` generator; everything (staging, - // launch, per-element pulls, cleanup) lives in the generator body. - w.line(kind.open_line_sync_star(&pub_ret, name, ¶ms)); - let mut body = String::new(); - emit_iterator_body(&mut body, f, c_sym, ib, err); - w.raw(body); - } else { - w.line(kind.open_line(&pub_ret, name, ¶ms)); - let mut body = String::new(); - emit_function_body(&mut body, f, c_sym, err); - w.raw(body); - } - w.line("}"); - decl.push_str(&w.finish()); -} - -/// The (native, dart, name) slot triples an async completion callback carries -/// 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<&Ty>) -> Vec<(String, String, String)> { - let Some(ty) = ret else { - return vec![]; - }; - let pair = |n: &str, d: &str, name: &str| (n.to_string(), d.to_string(), name.to_string()); - match plan::ret_pass(Some(ty), "", "") { - RetPass::Buffer | RetPass::Bytes => vec![ - pair("Pointer", "Pointer", "result"), - pair("Size", "int", "resultLen"), - ], - RetPass::String => vec![pair("Pointer", "Pointer", "result")], - // A (possibly nullable) adopted object pointer. - RetPass::Object { .. } => vec![pair("Pointer", "Pointer", "result")], - RetPass::Void | RetPass::Direct => { - let (n, d) = scalar_ffi(ty); - vec![pair(n, d, "result")] - } - } -} - -/// Render one async callable: its callback typedef and launcher lookup into -/// `lookups`, and its `Future`-returning wrapper into `decl`. A method's -/// launcher carries the implicit leading `self` pointer. -#[allow(clippy::too_many_arguments)] -fn render_async_function( - lookups: &mut String, - decl: &mut String, - c_sym: &str, - f: &FnBinding, - kind: &DartDecl, - name: &str, - pub_ret: &str, - wrapper_params: &[String], - err: ErrCtx, -) { - let cb_extras = async_cb_result_slots(f.ret.as_ref()); - let cb_native_params: Vec = std::iter::once("Pointer".to_string()) - .chain(std::iter::once("Pointer<_WeaveFFIError>".to_string())) - .chain(cb_extras.iter().map(|(n, _, _)| n.clone())) - .collect(); - - let cb_typedef = format!("_NativeAsyncCb_{c_sym}"); - lookups.push_str(&format!( - "\ntypedef {cb_typedef} = Void Function({});\n", - cb_native_params.join(", ") - )); - - let async_sym = format!("{c_sym}_async"); - let self_slot = if f.has_self { - vec![("Pointer".to_string(), "Pointer".to_string())] - } else { - vec![] - }; - let mut input_ffi: Vec<(String, String)> = self_slot; - for p in &f.params { - input_ffi.extend(input_slots(p)); - } - if f.cancellable { - input_ffi.push(("Pointer".into(), "Pointer".into())); - } - input_ffi.push(( - format!("Pointer>"), - format!("Pointer>"), - )); - input_ffi.push(("Pointer".into(), "Pointer".into())); - let native_params: Vec = input_ffi.iter().map(|(n, _)| n.clone()).collect(); - let dart_params: Vec = input_ffi.iter().map(|(_, d)| d.clone()).collect(); - - emit_typedef_and_lookup( - lookups, - &async_sym, - &native_params.join(", "), - &dart_params.join(", "), - "Void", - "void", - ); - - let completer_type = if f.ret.is_some() { - pub_ret.to_string() - } else { - "void".to_string() - }; - - // Stage every input up front, exactly like the sync path; staged native - // memory is pinned until the future completes and released in - // whenComplete (or in the catch when the launch itself throws). - let mut frees: Vec = Vec::new(); - let mut call_args: Vec = Vec::new(); - if f.has_self { - call_args.push("_handle".into()); - } - let mut stage = CodeWriter::two_space().with_depth(1); - let mut tmp = 0usize; - for p in &f.params { - let args = emit_input(&mut stage, p, &mut frees, &mut tmp); - call_args.extend(args); - } - let staging = stage.finish(); - if f.cancellable { - call_args.push("nullptr".into()); - } - call_args.push("callable.nativeFunction".into()); - call_args.push("nullptr".into()); - - let cb_param_decls: Vec = std::iter::once("Pointer context".to_string()) - .chain(std::iter::once("Pointer<_WeaveFFIError> err".to_string())) - .chain(cb_extras.iter().map(|(_, d, n)| format!("{d} {n}"))) - .collect(); - - let var = async_sym.to_lower_camel_case(); - - let mut ac = String::new(); - emit_async_complete(&mut ac, f.ret.as_ref(), " "); - - let mut w = CodeWriter::two_space(); - w.blank(); - emit_wrapper_doc(&mut w, f, err); - w.block( - kind.open_line( - &format!("Future<{pub_ret}>"), - name, - &wrapper_params.join(", "), - ), - "}", - |w| { - w.line(format!("final completer = Completer<{completer_type}>();")); - w.raw(&staging); - w.line(format!("late NativeCallable<{cb_typedef}> callable;")); - w.line(format!( - "callable = NativeCallable<{cb_typedef}>.listener(({}) {{", - cb_param_decls.join(", ") - )); - w.scope(|w| { - w.line("try {"); - w.scope(|w| { - w.line("if (err.address != 0 && err.ref.code != 0) {"); - w.scope(|w| { - w.line("final code = err.ref.code;"); - w.line("final msg = err.ref.message.toDartString();"); - if err.thrown_exception().is_some() { - w.line( - "final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen);", - ); - } - w.line("_weaveffiErrorFree(err);"); - w.line(format!("completer.completeError({});", err.map_expr())); - w.line("return;"); - }); - w.line("}"); - w.raw(&ac); - }); - w.line("} catch (e) {"); - w.scope(|w| { - w.line("completer.completeError(e);"); - }); - w.line("} finally {"); - w.scope(|w| { - w.line("callable.close();"); - }); - w.line("}"); - }); - w.line("});"); - w.line("try {"); - w.scope(|w| { - w.line(format!("_{var}({});", call_args.join(", "))); - }); - w.line("} catch (e) {"); - w.scope(|w| { - w.line("callable.close();"); - for fr in &frees { - w.line(fr); - } - w.line("rethrow;"); - }); - w.line("}"); - if frees.is_empty() { - w.line("return completer.future;"); - } else { - w.line("return completer.future.whenComplete(() {"); - w.scope(|w| { - for fr in &frees { - w.line(fr); - } - }); - w.line("});"); - } - }, - ); - decl.push_str(&w.finish()); -} - -/// Emit the callback statements that resolve the completer from the result -/// slots. Async results transfer ownership to the consumer (which is what -/// lets `NativeCallable.listener` defer this code past the native callback's -/// 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<&Ty>, indent: &str) { - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - let Some(ty) = ty else { - w.line("completer.complete();"); - out.push_str(&w.finish()); - return; - }; - match plan::ret_pass(Some(ty), "", "") { - // Copy the owned encoding, release it, then decode. - RetPass::Buffer => { - w.line("final resultData = _copyNativeBytes(result, resultLen);"); - w.line("_weaveffiFreeBytes(result, resultLen);"); - w.line("final resultReader = _BufferReader(resultData);"); - w.line(format!( - "final decoded = {};", - read_expr("resultReader", ty) - )); - w.line("resultReader.expectEnd();"); - w.line("completer.complete(decoded);"); - } - RetPass::Bytes => { - w.line("final resultData = _copyNativeBytes(result, resultLen);"); - w.line("_weaveffiFreeBytes(result, resultLen);"); - w.line("completer.complete(resultData);"); - } - // Copy the owned C string, then release the producer allocation. - RetPass::String => { - w.line("final decoded = result.toDartString();"); - w.line("_weaveffiFreeString(result);"); - 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. - RetPass::Object { nullable, .. } => { - w.line(format!( - "completer.complete({});", - adopt_expr("result", ty, nullable) - )); - } - RetPass::Void | RetPass::Direct => match ty { - Ty::Enum(name) => { - w.line(format!( - "completer.complete({}.fromValue(result));", - dart_class(name) - )); - } - _ => { - w.line("completer.complete(result);"); - } - }, - } - out.push_str(&w.finish()); -} - -/// The expression adopting the owned object pointer `expr` into its wrapper -/// class; when `nullable`, a null pointer becomes Dart `null`. -pub(crate) fn adopt_expr(expr: &str, ty: &Ty, nullable: bool) -> String { - let class = object_class(ty); - if nullable { - format!("{expr} == nullptr ? null : {class}._({expr})") - } else { - format!("{class}._({expr})") - } -} - -/// Emit pre-call staging for one input parameter, returning the call-argument -/// expressions it contributes (in ABI order) and appending any cleanup -/// statements to `frees`. Dispatches on the parameter's [`ArgPass`] contract: -/// a buffered value is encoded into a `_BufferWriter`, staged into native -/// memory, and passed as a borrowed `(ptr, len)` pair the callee never frees; -/// an object passes the wrapper's borrowed handle; a callback interface is -/// registered in the handle table and passed as its key plus the interface's -/// static vtable. -fn emit_input( - w: &mut CodeWriter, - p: &ParamBinding, - frees: &mut Vec, - tmp: &mut usize, -) -> Vec { - let name = dart_ident(&p.name); - match p.arg_pass() { - ArgPass::Buffer { .. } => { - let writer = format!("{name}Writer"); - let buf = format!("{name}Buf"); - let ptr = format!("{name}Ptr"); - w.line(format!("final {writer} = _BufferWriter();")); - write_stmts(w, &writer, &name, &p.ty, tmp); - w.line(format!("final {buf} = {writer}.takeBytes();")); - w.line(format!("final {ptr} = _stageBytes({buf});")); - frees.push(format!("calloc.free({ptr});")); - vec![ptr, format!("{buf}.length")] - } - ArgPass::String { .. } => { - let ptr = format!("{name}Ptr"); - w.line(format!("final {ptr} = {name}.toNativeUtf8();")); - frees.push(format!("calloc.free({ptr});")); - vec![ptr] - } - ArgPass::Bytes { .. } => { - let ptr = format!("{name}Ptr"); - w.line(format!( - "final {ptr} = {name}.isEmpty ? nullptr : calloc({name}.length);" - )); - w.line(format!( - "for (var i = 0; i < {name}.length; i++) {{ {ptr}[i] = {name}[i]; }}" - )); - frees.push(format!("if ({ptr} != nullptr) calloc.free({ptr});")); - vec![ptr, format!("{name}.length")] - } - // A borrowed object pointer: the wrapper keeps its own reference and - // the producer clones if it retains the object. When nullable, null - // means none. - ArgPass::Object { - nullable: false, .. - } => vec![format!("{name}._handle")], - ArgPass::Object { nullable: true, .. } => { - vec![format!("{name}?._handle ?? nullptr")] - } - // The implementation is parked in the handle table until the producer - // calls the vtable's `free(ctx)`; nothing to release here. - ArgPass::Callback { .. } => { - let Ty::CallbackInterface(cb) = &p.ty else { - unreachable!("callback family names a callback interface") - }; - vec![ - format!("_registerCallback({name})"), - format!("{}.cast()", vtable_var(cb)), - ] - } - ArgPass::Direct { .. } => match &p.ty { - Ty::Enum(_) => vec![format!("{name}.value")], - _ => vec![name], - }, - } -} - -/// 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: &Ty, frees: &mut Vec) -> Vec { - if returns_buffer(ty) { - w.line("final outLen = calloc();"); - frees.push("calloc.free(outLen);".into()); - vec!["outLen".into()] - } else { - vec![] - } -} - -/// Emit the post-call decode of a return into the wrapper's Dart return -/// 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; an object -/// return is adopted by its wrapper class. -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 => { - w.line("final n = outLen.value;"); - w.line("final data = _copyNativeBytes(result, n);"); - w.line("if (result != nullptr) _weaveffiFreeBytes(result, n);"); - w.line("final reader = _BufferReader(data);"); - // 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 decoded;"); - } - RetPass::Bytes => { - w.line("final n = outLen.value;"); - w.line("if (result == nullptr) return [];"); - w.line("final bytes = List.generate(n, (i) => result[i]);"); - // Copy first, then release the producer's buffer. - w.line("_weaveffiFreeBytes(result, n);"); - w.line("return bytes;"); - } - RetPass::String => { - w.line("final decoded = result.toDartString();"); - w.line("_weaveffiFreeString(result);"); - w.line("return decoded;"); - } - // One owned strong reference the wrapper class adopts; when nullable, - // null means none. - RetPass::Object { nullable, .. } => { - w.line(format!("return {};", adopt_expr("result", ty, nullable))); - } - RetPass::Void | RetPass::Direct => match ty { - Ty::Enum(name) => { - w.line(format!("return {}.fromValue(result);", dart_class(name))); - } - _ => { - w.line("return result;"); - } - }, - } - out.push_str(&w.finish()); -} - -/// Emit the staging, call, error check, and return decode of one synchronous -/// wrapper body. -fn emit_function_body(out: &mut String, f: &FnBinding, c_sym: &str, err: ErrCtx) { - let mut frees: Vec = Vec::new(); - let mut call_args: Vec = Vec::new(); - if f.has_self { - call_args.push("_handle".into()); - } - let mut stage = CodeWriter::two_space().with_depth(1); - let mut tmp = 0usize; - for p in &f.params { - let args = emit_input(&mut stage, p, &mut frees, &mut tmp); - call_args.extend(args); - } - if let Some(ret) = &f.ret { - call_args.extend(emit_return_alloc(&mut stage, ret, &mut frees)); - } - let staging = stage.finish(); - frees.push("calloc.free(err);".into()); - call_args.push("err".into()); - - let var = c_sym.to_lower_camel_case(); - let args = call_args.join(", "); - let void_call = f.ret.is_none(); - let mut dec = String::new(); - if let Some(ret) = &f.ret { - emit_return_decode(&mut dec, ret, " "); - } - - let mut w = CodeWriter::two_space().with_depth(1); - w.raw(staging); - w.line("final err = calloc<_WeaveFFIError>();"); - w.line("try {"); - w.scope(|w| { - if void_call { - w.line(format!("_{var}({args});")); - } else { - w.line(format!("final result = _{var}({args});")); - } - w.line(err.check_stmt()); - w.raw(&dec); - }); - w.line("} finally {"); - w.scope(|w| { - for fr in &frees { - w.line(fr); - } - }); - w.line("}"); - out.push_str(&w.finish()); -} - -/// 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 [`RetPass`] plan. -fn iter_item_slot(elem: &Ty) -> (String, bool) { - match plan::ret_pass(Some(elem), "", "") { - RetPass::Bytes | RetPass::Buffer => ("Pointer".into(), true), - RetPass::String => ("Pointer".into(), false), - RetPass::Object { .. } => ("Pointer".into(), false), - RetPass::Void | RetPass::Direct => (scalar_ffi(elem).0.to_string(), false), - } -} - -/// Bind the element `next`/`destroy` symbols of an iterator-returning -/// function, plus a `NativeFinalizer` over the destroy symbol. The finalizer -/// is the disposal backstop for abandoned iterations: Dart runs a `sync*` -/// body only inside `moveNext`, so a consumer that stops pulling (a broken -/// `for` loop, `first`, `take`) never resumes the generator and its `finally` -/// block never runs; the finalizer reclaims the native handle when the -/// suspended frame is collected instead. -fn emit_iter_lookups(out: &mut String, ib: &IteratorBinding) { - let (pointee, has_len) = iter_item_slot(&ib.elem); - let mut params = vec!["Pointer".to_string(), format!("Pointer<{pointee}>")]; - if has_len { - params.push("Pointer".into()); - } - params.push("Pointer<_WeaveFFIError>".into()); - let joined = params.join(", "); - emit_typedef_and_lookup(out, &ib.next.symbol, &joined, &joined, "Int32", "int"); - emit_typedef_and_lookup( - out, - &ib.destroy_symbol, - "Pointer", - "Pointer", - "Void", - "void", - ); - out.push_str(&format!( - "final _{}Finalizer = NativeFinalizer(\n \ - _lib.lookup)>>('{}'));\n", - ib.destroy_symbol.to_lower_camel_case(), - ib.destroy_symbol - )); -} - -/// Emit the `sync*` generator body of an `iter` wrapper. -/// -/// The body runs lazily, on the first pull: it stages the inputs, launches -/// the C iterator, and then issues exactly one producer `next` call per -/// yielded element, receiving each per the plan's [`RetPass`] (strings are -/// copied and freed through `weaveffi_free_string`; bytes and buffered -/// elements through `weaveffi_free_bytes`; interface elements are adopted by -/// their wrapper class, whose `dispose()` owns the destroy). -/// -/// The handle is destroyed exactly once. The `try`/`finally` destroys it when -/// iteration exhausts, a launch or `next` error throws, or the generator is -/// otherwise torn down, then nulls the local handle so the finalizer detach -/// path cannot double-destroy. For iterations abandoned mid-stream (where the -/// `finally` never runs, see [`emit_iter_lookups`]) the `NativeFinalizer` -/// attached to the generator-local anchor destroys the handle when the frame -/// is collected; the eager path detaches before destroying. -fn emit_iterator_body( - out: &mut String, - f: &FnBinding, - c_sym: &str, - ib: &IteratorBinding, - err: ErrCtx, -) { - let elem_pass = plan::ret_pass(Some(&ib.elem), "", ""); - let mut frees: Vec = Vec::new(); - let mut call_args: Vec = Vec::new(); - if f.has_self { - call_args.push("_handle".into()); - } - let mut stage = CodeWriter::two_space().with_depth(1); - let mut tmp = 0usize; - for p in &f.params { - let args = emit_input(&mut stage, p, &mut frees, &mut tmp); - call_args.extend(args); - } - let staging = stage.finish(); - frees.push("calloc.free(err);".into()); - call_args.push("err".into()); - - let var = c_sym.to_lower_camel_case(); - let elem = &ib.elem; - let (pointee, has_len) = iter_item_slot(elem); - let next_var = ib.next.symbol.to_lower_camel_case(); - let destroy_var = ib.destroy_symbol.to_lower_camel_case(); - let next_args = if has_len { - "iter, outItem, outLen, err" - } else { - "iter, outItem, err" - }; - - let mut w = CodeWriter::two_space().with_depth(1); - w.raw(staging); - w.line("final err = calloc<_WeaveFFIError>();"); - w.line(format!("final outItem = calloc<{pointee}>();")); - if has_len { - w.line("final outLen = calloc();"); - } - w.line("Pointer iter = nullptr;"); - w.line("final anchor = _IteratorLifetime();"); - w.line("try {"); - w.scope(|w| { - w.line(format!("iter = _{var}({});", call_args.join(", "))); - w.line(err.check_stmt()); - w.line(format!( - "_{destroy_var}Finalizer.attach(anchor, iter, detach: anchor);" - )); - w.line(format!("while (_{next_var}({next_args}) != 0) {{")); - w.scope(|w| { - w.line(err.check_stmt()); - match &elem_pass { - RetPass::String => { - w.line("final itemPtr = outItem.value;"); - w.line("final item = itemPtr.toDartString();"); - w.line("_weaveffiFreeString(itemPtr);"); - w.line("yield item;"); - } - // A raw bytes element copies, then releases the producer's - // buffer with weaveffi_free_bytes. - RetPass::Bytes => { - w.line("final itemPtr = outItem.value;"); - w.line("final itemLen = outLen.value;"); - w.line("final item = _copyNativeBytes(itemPtr, itemLen);"); - w.line("if (itemPtr != nullptr) _weaveffiFreeBytes(itemPtr, itemLen);"); - w.line("yield item;"); - } - // A buffered element decodes (adopting any object tokens it - // carries), then releases the producer's buffer. - RetPass::Buffer => { - w.line("final itemPtr = outItem.value;"); - w.line("final itemLen = outLen.value;"); - w.line("final itemData = _copyNativeBytes(itemPtr, itemLen);"); - w.line("if (itemPtr != nullptr) _weaveffiFreeBytes(itemPtr, itemLen);"); - w.line("final itemReader = _BufferReader(itemData);"); - w.line(format!("final item = {};", read_expr("itemReader", elem))); - w.line("itemReader.expectEnd();"); - w.line("yield item;"); - } - // One owned strong reference per element, adopted by the - // wrapper class. - RetPass::Object { nullable, .. } => { - w.line("final itemPtr = outItem.value;"); - w.line(format!("yield {};", adopt_expr("itemPtr", elem, *nullable))); - } - RetPass::Void | RetPass::Direct => { - let read = match elem { - Ty::Enum(n) => format!("{}.fromValue(outItem.value)", dart_class(n)), - _ => "outItem.value".to_string(), - }; - w.line(format!("yield {read};")); - } - } - }); - w.line("}"); - w.line(err.check_stmt()); - }); - w.line("} finally {"); - w.scope(|w| { - w.line("if (iter != nullptr) {"); - w.scope(|w| { - w.line(format!("_{destroy_var}Finalizer.detach(anchor);")); - w.line(format!("_{destroy_var}(iter);")); - w.line("iter = nullptr;"); - }); - w.line("}"); - if has_len { - w.line("calloc.free(outLen);"); - } - w.line("calloc.free(outItem);"); - for fr in &frees { - w.line(fr); - } - }); - w.line("}"); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-dart/src/codec.rs b/crates/weaveffi-gen-dart/src/codec.rs deleted file mode 100644 index 6a830871..00000000 --- a/crates/weaveffi-gen-dart/src/codec.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! Value-buffer codec emitters: the inline read expressions and write -//! statements for any wire shape, naming the per-record and per-rich-enum -//! `_pack{Name}`/`_unpack{Name}` helpers. -//! -//! Every dispatch here goes through [`Ty::wire`], so this module never -//! re-derives the wire folds (interfaces as `u64` object tokens, records and -//! rich enums as one user-codec shape). -//! -//! An object token carries one strong reference. Writing one calls the -//! wrapper's private `_cloneRef()` (the interface's `_clone` symbol) so the -//! encoding owns a fresh reference and the wrapper keeps its own; reading one -//! adopts the pointer into a new wrapper whose `dispose()` (or finalizer) -//! owes the matching `_destroy`. - -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{Prim, WireType}; - -use crate::types::{dart_class, dart_type}; - -/// The `_pack{Name}` helper name for a (possibly dot-qualified) record or -/// rich-enum reference. -pub(crate) fn pack_fn(name: &str) -> String { - format!("_pack{}", dart_class(name)) -} - -/// The `_unpack{Name}` helper name for a (possibly dot-qualified) record or -/// rich-enum reference. -pub(crate) fn unpack_fn(name: &str) -> String { - format!("_unpack{}", dart_class(name)) -} - -/// Mint a fresh `t{n}` temporary name. -pub(crate) fn fresh(tmp: &mut usize) -> String { - let n = *tmp; - *tmp += 1; - format!("t{n}") -} - -/// The Dart expression decoding one value of `ty` from the reader named `r`. -/// -/// 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: &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()"), - // A token is one adopted strong reference: wrap it, never destroy it - // here. - WireType::Object(n) => format!( - "{}._(Pointer.fromAddress({r}.readUint64()))", - dart_class(n) - ), - WireType::Enum(n) => format!("{}.fromValue({r}.readInt32())", dart_class(n)), - 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)) - } - WireType::List(inner) => format!( - "List<{}>.generate({r}.readLength(), (_) => {})", - dart_type(inner), - read_expr(r, inner) - ), - WireType::Map(k, v) => format!( - "<{}, {}>{{ for (var i = {r}.readLength(); i > 0; i--) {}: {} }}", - dart_type(k), - dart_type(v), - read_expr(r, k), - read_expr(r, v) - ), - } -} - -/// Emit the statements encoding `expr` (a value of `ty`) into the writer -/// 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: &Ty, tmp: &mut usize) { - match ty.wire() { - WireType::Prim(Prim::Bool) => { - w.line(format!("{wr}.writeBool({expr});")); - } - WireType::Prim(Prim::I8) => { - w.line(format!("{wr}.writeInt8({expr});")); - } - WireType::Prim(Prim::I16) => { - w.line(format!("{wr}.writeInt16({expr});")); - } - WireType::Prim(Prim::I32) => { - w.line(format!("{wr}.writeInt32({expr});")); - } - WireType::Prim(Prim::I64) => { - w.line(format!("{wr}.writeInt64({expr});")); - } - WireType::Prim(Prim::U8) => { - w.line(format!("{wr}.writeUint8({expr});")); - } - WireType::Prim(Prim::U16) => { - w.line(format!("{wr}.writeUint16({expr});")); - } - WireType::Prim(Prim::U32) => { - w.line(format!("{wr}.writeUint32({expr});")); - } - WireType::Prim(Prim::U64) => { - w.line(format!("{wr}.writeUint64({expr});")); - } - WireType::Prim(Prim::F32) => { - w.line(format!("{wr}.writeFloat32({expr});")); - } - WireType::Prim(Prim::F64) => { - w.line(format!("{wr}.writeFloat64({expr});")); - } - // The token must own its reference, so write a freshly cloned pointer - // rather than the one the wrapper still holds. - WireType::Object(_) => { - w.line(format!("{wr}.writeUint64({expr}._cloneRef().address);")); - } - WireType::Enum(_) => { - w.line(format!("{wr}.writeInt32({expr}.value);")); - } - WireType::Prim(Prim::String) => { - w.line(format!("{wr}.writeString({expr});")); - } - WireType::Prim(Prim::Bytes) => { - w.line(format!("{wr}.writeBytes({expr});")); - } - WireType::User(n) => { - w.line(format!("{}({wr}, {expr});", pack_fn(n))); - } - WireType::Optional(inner) => { - let t = fresh(tmp); - w.line(format!("final {t} = {expr};")); - w.line(format!("if ({t} == null) {{")); - w.scope(|w| { - w.line(format!("{wr}.writeOptionFlag(false);")); - }); - w.line("} else {"); - w.scope(|w| { - w.line(format!("{wr}.writeOptionFlag(true);")); - write_stmts(w, wr, &t, inner, &mut *tmp); - }); - w.line("}"); - } - WireType::List(inner) => { - let t = fresh(tmp); - let e = fresh(tmp); - w.line(format!("final {t} = {expr};")); - w.line(format!("{wr}.writeLength({t}.length);")); - w.line(format!("for (final {e} in {t}) {{")); - w.scope(|w| { - write_stmts(w, wr, &e, inner, &mut *tmp); - }); - w.line("}"); - } - WireType::Map(k, v) => { - let t = fresh(tmp); - let e = fresh(tmp); - w.line(format!("final {t} = {expr};")); - w.line(format!("{wr}.writeLength({t}.length);")); - w.line(format!("for (final {e} in {t}.entries) {{")); - w.scope(|w| { - write_stmts(w, wr, &format!("{e}.key"), k, &mut *tmp); - write_stmts(w, wr, &format!("{e}.value"), v, &mut *tmp); - }); - w.line("}"); - } - } -} diff --git a/crates/weaveffi-gen-dart/src/docs.rs b/crates/weaveffi-gen-dart/src/docs.rs deleted file mode 100644 index ea10e4db..00000000 --- a/crates/weaveffi-gen-dart/src/docs.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Doc-comment emission: `///` comments carried from the IDL, plus the -//! generated notes (streaming contract, thrown exception, deprecation) a -//! wrapper's declaration carries. - -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 crate::calls::ErrCtx; - -/// Append `doc` as a `///` comment block at `indent`, or nothing when absent. -pub(crate) fn emit_doc(out: &mut String, doc: &Option, indent: &str) { - common_emit_doc(out, doc, indent, DocCommentStyle::TripleSlash); -} - -/// Emit a wrapper's doc comment, the streaming/disposal note for an iterator -/// callable, the typed-exception note for a throwing callable, and its -/// `@Deprecated` annotation when present. -pub(crate) fn emit_wrapper_doc(w: &mut CodeWriter, f: &FnBinding, err: ErrCtx) { - { - let mut d = String::new(); - emit_doc(&mut d, &f.doc, ""); - w.raw(d); - } - let mut has_content = f.doc.is_some(); - let separator = |w: &mut CodeWriter, has_content: &mut bool| { - if *has_content { - w.line("///"); - } - *has_content = true; - }; - if let CallShape::Iterator(ib) = &f.shape { - separator(w, &mut has_content); - w.line("/// Returns a lazy [Iterable]: elements are pulled from the native"); - w.line("/// iterator one at a time (one native `next` call per element), and"); - w.line("/// iterating the result again launches a fresh native iterator."); - w.line("///"); - 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 ib.elem.interface_name().is_some() { - w.line("///"); - w.line("/// Each yielded element is owned by the caller: call its `dispose()`"); - w.line("/// when you are done with it."); - } - } else if let Some(ret) = f.ret.as_ref().filter(|r| r.interface_name().is_some()) { - separator(w, &mut has_content); - if matches!(ret, Ty::Optional(_)) { - w.line("/// Returns `null` when the producer reports no object. A non-null result"); - w.line("/// is owned by the caller: call its `dispose()` when you are done with it."); - } else { - w.line("/// The returned object is owned by the caller: call its `dispose()` when"); - w.line("/// you are done with it."); - } - } - if let Some(exc) = err.thrown_exception() { - separator(w, &mut has_content); - w.line(format!("/// Throws [{exc}] on domain errors.")); - } - if let Some(msg) = &f.deprecated { - let escaped = msg.replace('\'', "\\'"); - w.line(format!("@Deprecated('{escaped}')")); - } -} diff --git a/crates/weaveffi-gen-dart/src/entities.rs b/crates/weaveffi-gen-dart/src/entities.rs deleted file mode 100644 index d6a09dd8..00000000 --- a/crates/weaveffi-gen-dart/src/entities.rs +++ /dev/null @@ -1,538 +0,0 @@ -//! Entity rendering: error domains, C-style enums, rich enums, records, and -//! interface wrapper classes, plus their value-buffer codec helpers. - -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 crate::calls::{err_ctx, render_callable, DartDecl}; -use crate::codec::{fresh, read_expr, write_stmts}; -use crate::docs::emit_doc; -use crate::runtime::emit_typedef_and_lookup; -use crate::types::{dart_ident, dart_str_literal, dart_type}; - -/// The Dart exception class named by an error domain or one of its codes: the -/// PascalCase name with a trailing `Error` swapped for `Exception`, so -/// `KvError` becomes `KvException` and a code `IoError` becomes `IoException`. -pub(crate) fn dart_exception_name(raw: &str) -> String { - errors::exception_type_name(raw) -} - -/// Render one module's declared error domain: the domain exception extending -/// the generic [`errors::EXCEPTION_BRAND`], one exception subclass per code -/// carrying its stable code, default message, and any decoded payload fields, -/// and the `_map`/`_check` helpers that throwing wrappers route their out-err -/// slots through. When a code declares payload fields, the mapper decodes the -/// error's payload buffer into the exception's typed properties. Only -/// declared (positive) codes gain a `case`; every other code, including the -/// reserved negative runtime range (generic error, panic, marshalling -/// failure, and a callback-interface implementation that threw), falls -/// through to the generic exception. -pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorBinding) { - let exc = dart_exception_name(&eb.type_name); - let brand = errors::EXCEPTION_BRAND; - - let mut w = CodeWriter::two_space(); - w.blank(); - w.line(format!( - "/// Typed error domain `{}` declared by module `{}`.", - eb.name, module.path - )); - w.block(format!("class {exc} extends {brand} {{"), "}", |w| { - w.line(format!("{exc}(super.code, super.message);")); - }); - - for c in &eb.codes { - let class = dart_exception_name(&c.name); - let message = dart_str_literal(&c.message); - w.blank(); - let doc = c.doc.clone().or_else(|| Some(c.message.clone())); - { - let mut d = String::new(); - emit_doc(&mut d, &doc, ""); - w.raw(d); - } - w.block(format!("class {class} extends {exc} {{"), "}", |w| { - if c.fields.is_empty() { - w.line(format!( - "{class}([String message = '{message}']) : super({}, message);", - c.value - )); - } else { - for f in &c.fields { - let mut fd = String::new(); - emit_doc(&mut fd, &f.doc, " "); - w.raw(fd); - w.line(format!( - "final {} {};", - dart_type(&f.ty), - dart_ident(&f.name) - )); - } - w.blank(); - let params: Vec = c - .fields - .iter() - .map(|f| format!("this.{}", dart_ident(&f.name))) - .collect(); - w.line(format!( - "{class}({}, [String message = '{message}']) : super({}, message);", - params.join(", "), - c.value - )); - } - }); - } - - w.blank(); - w.line(format!( - "{brand} _map{exc}(int code, String message, Uint8List payload) {{" - )); - w.scope(|w| { - w.block("switch (code) {", "}", |w| { - for c in &eb.codes { - let class = dart_exception_name(&c.name); - if c.fields.is_empty() { - w.line(format!("case {}:", c.value)); - w.scope(|w| { - w.line(format!("return {class}(message);")); - }); - } else { - // Braces give each payload-decoding case its own scope, - // so the reader and field locals never collide between - // cases (a Dart switch otherwise shares one scope). - w.line(format!("case {}: {{", c.value)); - w.scope(|w| { - w.line("final r = _BufferReader(payload);"); - let mut args: Vec = Vec::new(); - for (i, f) in c.fields.iter().enumerate() { - w.line(format!("final v{i} = {};", read_expr("r", &f.ty))); - args.push(format!("v{i}")); - } - w.line("r.expectEnd();"); - w.line(format!("return {class}({}, message);", args.join(", "))); - }); - w.line("}"); - } - } - w.line("default:"); - w.scope(|w| { - w.line(format!("return {brand}(code, message);")); - }); - }); - }); - w.line("}"); - - w.blank(); - w.block( - format!("void _check{exc}(Pointer<_WeaveFFIError> err) {{"), - "}", - |w| { - w.block("if (err.ref.code != 0) {", "}", |w| { - w.line("final code = err.ref.code;"); - w.line("final msg = err.ref.message.toDartString();"); - w.line("final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen);"); - w.line("_weaveffiErrorClear(err);"); - w.line(format!("throw _map{exc}(code, msg, payload);")); - }); - }, - ); - out.push_str(&w.finish()); -} - -/// Render one interface as a reference-counted object wrapper class. -/// -/// The wrapper adopts one strong reference (the pointer behind the private -/// `_handle`) and owes exactly one `_destroy` for it: `dispose()` releases it -/// eagerly and a `NativeFinalizer` attached in the constructor releases it -/// when an undisposed wrapper is collected. The two paths never both fire -/// because `dispose()` detaches the finalizer before destroying, and a second -/// `dispose()` is a no-op. Using a disposed wrapper throws a `StateError`. -/// The private `_cloneRef()` calls the interface's `_clone` symbol to mint a -/// second strong reference, which the value-buffer codec writes as an object -/// token. -/// -/// The canonical `new` constructor renders as an unnamed factory -/// (`Store(...)`), every other constructor as a named factory -/// (`Store.open(...)`), instance methods pass `_handle` as the implicit -/// leading FFI argument, and statics are `static` methods. Member FFI -/// typedefs and lookups stay at file scope. -pub(crate) fn render_interface(out: &mut String, module: &ModuleBinding, i: &InterfaceBinding) { - let class_name = i.name.to_upper_camel_case(); - emit_typedef_and_lookup( - out, - &i.clone_symbol, - "Pointer", - "Pointer", - "Pointer", - "Pointer", - ); - emit_typedef_and_lookup( - out, - &i.destroy_symbol, - "Pointer", - "Pointer", - "Void", - "void", - ); - let destroy_var = i.destroy_symbol.to_lower_camel_case(); - let clone_var = i.clone_symbol.to_lower_camel_case(); - let finalizer = format!("_{destroy_var}Finalizer"); - out.push_str(&format!( - "final {finalizer} = NativeFinalizer(\n \ - _lib.lookup)>>('{}'));\n", - i.destroy_symbol - )); - - let exc = module - .error - .as_ref() - .map(|e| dart_exception_name(&e.type_name)); - - // Members render exactly like free functions (depth 0), with the lookups - // going to file scope and the declarations collected for the class body. - let mut members = String::new(); - for c in &i.constructors { - let kind = DartDecl::Factory { - class_name: &class_name, - named: c.name != "new", - }; - render_callable( - out, - &mut members, - c, - &kind, - &dart_ident(&c.name), - err_ctx(c, exc.as_deref()), - ); - } - for m in &i.methods { - render_callable( - out, - &mut members, - m, - &DartDecl::Method, - &dart_ident(&m.name), - err_ctx(m, exc.as_deref()), - ); - } - for s in &i.statics { - render_callable( - out, - &mut members, - s, - &DartDecl::Static, - &dart_ident(&s.name), - err_ctx(s, exc.as_deref()), - ); - } - - let mut w = CodeWriter::two_space(); - w.blank(); - { - let mut d = String::new(); - emit_doc(&mut d, &i.doc, ""); - w.raw(d); - } - if i.doc.is_some() { - w.line("///"); - } - w.line("/// A reference-counted native object. Each instance holds one strong"); - w.line("/// reference, released by [dispose] or, if the instance is collected"); - w.line("/// without being disposed, by a GC finalizer. Instances stay valid for as"); - w.line("/// long as the Dart object is reachable; the producer keeps its own"); - w.line("/// references to any object it retains."); - if let Some(msg) = &i.deprecated { - w.line(format!("@Deprecated('{}')", dart_str_literal(msg))); - } - w.block( - format!("class {class_name} implements Finalizable {{"), - "}", - |w| { - w.line("final Pointer _ptr;"); - w.line("bool _disposed = false;"); - w.blank(); - w.line("/// Adopts one strong reference to the native object."); - w.block(format!("{class_name}._(this._ptr) {{"), "}", |w| { - w.line(format!("{finalizer}.attach(this, _ptr, detach: this);")); - }); - w.blank(); - w.line("/// The borrowed native pointer for the duration of a call."); - w.block("Pointer get _handle {", "}", |w| { - w.line(format!( - "if (_disposed) throw StateError('{class_name} used after dispose()');" - )); - w.line("return _ptr;"); - }); - w.blank(); - w.line("/// Mints a second strong reference (the interface's `_clone` symbol) for"); - w.line("/// an object token written into a value buffer."); - w.line(format!( - "Pointer _cloneRef() => _{clone_var}(_handle);" - )); - w.blank(); - w.line("/// Releases this instance's native reference. Safe to call more than"); - w.line("/// once; the native object is dropped when its last reference (this"); - w.line("/// one, any other wrapper's, or the producer's) is released."); - w.block("void dispose() {", "}", |w| { - w.line("if (_disposed) return;"); - w.line("_disposed = true;"); - w.line(format!("{finalizer}.detach(this);")); - w.line(format!("_{destroy_var}(_ptr);")); - }); - // Reindent the depth-0 member declarations into the class body. - w.block_raw(&members); - }, - ); - out.push_str(&w.finish()); -} - -/// Render one enum. A C-style enum becomes an enhanced Dart `enum`; a rich -/// (algebraic) enum is a value type and becomes a sealed class hierarchy with -/// pack/unpack helpers. -pub(crate) fn render_enum(out: &mut String, e: &EnumBinding) { - if e.is_rich() { - render_rich_enum(out, e); - return; - } - let name = e.name.to_upper_camel_case(); - let mut w = CodeWriter::two_space(); - w.blank(); - { - let mut d = String::new(); - emit_doc(&mut d, &e.doc, ""); - w.raw(d); - } - w.block(format!("enum {name} {{"), "}", |w| { - for v in &e.variants { - let vname = dart_ident(&v.name); - let mut vd = String::new(); - emit_doc(&mut vd, &v.doc, " "); - w.raw(vd); - w.line(format!("{vname}({}),", v.value)); - } - w.line(";"); - w.line(format!("const {name}(this.value);")); - w.line("final int value;"); - w.blank(); - w.line(format!( - "static {name} fromValue(int value) =>\n {name}.values.firstWhere((e) => e.value == value);" - )); - }); - out.push_str(&w.finish()); -} - -/// Render one record as a plain Dart value class (final typed fields, a named -/// constructor argument per field), plus its `_pack{Name}`/`_unpack{Name}` -/// buffer helpers. Records declare no C symbols: no destroy, no getters, no -/// builders; instances cross the ABI serialized in value buffers. -pub(crate) fn render_struct(out: &mut String, s: &StructBinding) { - let class_name = s.name.to_upper_camel_case(); - let mut w = CodeWriter::two_space(); - w.blank(); - { - let mut d = String::new(); - emit_doc(&mut d, &s.doc, ""); - w.raw(d); - } - w.block(format!("class {class_name} {{"), "}", |w| { - for f in &s.fields { - let mut fd = String::new(); - emit_doc(&mut fd, &f.doc, " "); - w.raw(fd); - w.line(format!( - "final {} {};", - dart_type(&f.ty), - dart_ident(&f.name) - )); - } - if !s.fields.is_empty() { - w.blank(); - let params: Vec = s - .fields - .iter() - .map(|f| { - let n = dart_ident(&f.name); - if matches!(f.ty, Ty::Optional(_)) { - format!("this.{n}") - } else { - format!("required this.{n}") - } - }) - .collect(); - w.line(format!("{class_name}({{{}}});", params.join(", "))); - } - }); - - // Pack: each field in declaration (wire) order. - w.blank(); - w.line(format!( - "void _pack{class_name}(_BufferWriter w, {class_name} v) {{" - )); - w.scope(|w| { - let mut tmp = 0usize; - for f in &s.fields { - write_stmts( - w, - "w", - &format!("v.{}", dart_ident(&f.name)), - &f.ty, - &mut tmp, - ); - } - }); - w.line("}"); - - // Unpack: named constructor arguments evaluate in source order, which is - // the field declaration (wire) order. - w.blank(); - w.line(format!( - "{class_name} _unpack{class_name}(_BufferReader r) {{" - )); - w.scope(|w| { - if s.fields.is_empty() { - w.line(format!("return {class_name}();")); - } else { - w.line(format!("return {class_name}(")); - w.scope(|w| { - for f in &s.fields { - w.line(format!( - "{}: {},", - dart_ident(&f.name), - read_expr("r", &f.ty) - )); - } - }); - w.line(");"); - } - }); - w.line("}"); - out.push_str(&w.finish()); -} - -/// The Dart subclass name of one rich-enum variant: `{Enum}{Variant}`. -fn variant_class(base: &str, variant: &str) -> String { - format!("{base}{}", variant.to_upper_camel_case()) -} - -/// Render one rich (algebraic) enum as an idiomatic sealed class hierarchy: -/// a sealed base class plus one subclass per variant carrying that variant's -/// fields, and `_pack{Name}`/`_unpack{Name}` helpers encoding the `i32` tag -/// followed by the active variant's fields. Rich enums declare no C symbols; -/// values cross the ABI serialized in value buffers. -fn render_rich_enum(out: &mut String, e: &EnumBinding) { - let base = e.name.to_upper_camel_case(); - let mut w = CodeWriter::two_space(); - w.blank(); - { - let mut d = String::new(); - emit_doc(&mut d, &e.doc, ""); - w.raw(d); - } - w.block(format!("sealed class {base} {{"), "}", |w| { - w.line(format!("const {base}();")); - }); - - for v in &e.variants { - let cls = variant_class(&base, &v.name); - w.blank(); - { - let mut vd = String::new(); - emit_doc(&mut vd, &v.doc, ""); - w.raw(vd); - } - if v.fields.is_empty() { - w.line(format!("class {cls} extends {base} {{}}")); - } else { - w.block(format!("class {cls} extends {base} {{"), "}", |w| { - for f in &v.fields { - let mut fd = String::new(); - emit_doc(&mut fd, &f.doc, " "); - w.raw(fd); - w.line(format!( - "final {} {};", - dart_type(&f.ty), - dart_ident(&f.name) - )); - } - w.blank(); - let params: Vec = v - .fields - .iter() - .map(|f| format!("this.{}", dart_ident(&f.name))) - .collect(); - w.line(format!("{cls}({});", params.join(", "))); - }); - } - } - - // Pack: the i32 tag, then the active variant's fields in order. The - // sealed base makes the switch exhaustive without a default arm. One - // temp counter spans all cases: a Dart switch shares a single scope for - // plain declarations, so names must stay unique across cases. - w.blank(); - w.line(format!("void _pack{base}(_BufferWriter w, {base} v) {{")); - w.scope(|w| { - w.line("switch (v) {"); - w.scope(|w| { - let mut tmp = 0usize; - for v in &e.variants { - let cls = variant_class(&base, &v.name); - if v.fields.is_empty() { - w.line(format!("case {cls}():")); - w.scope(|w| { - w.line(format!("w.writeInt32({});", v.value)); - }); - } else { - let b = fresh(&mut tmp); - w.line(format!("case final {cls} {b}:")); - w.scope(|w| { - w.line(format!("w.writeInt32({});", v.value)); - for f in &v.fields { - write_stmts( - w, - "w", - &format!("{b}.{}", dart_ident(&f.name)), - &f.ty, - &mut tmp, - ); - } - }); - } - } - }); - w.line("}"); - }); - w.line("}"); - - // Unpack: constructor arguments evaluate left to right, preserving the - // wire order of the variant's fields. - w.blank(); - w.line(format!("{base} _unpack{base}(_BufferReader r) {{")); - w.scope(|w| { - w.line("final tag = r.readInt32();"); - w.line("switch (tag) {"); - w.scope(|w| { - for v in &e.variants { - let cls = variant_class(&base, &v.name); - w.line(format!("case {}:", v.value)); - w.scope(|w| { - let args: Vec = - v.fields.iter().map(|f| read_expr("r", &f.ty)).collect(); - w.line(format!("return {cls}({});", args.join(", "))); - }); - } - w.line("default:"); - w.scope(|w| { - w.line(format!("_bufferError('unknown {base} tag $tag');")); - }); - }); - w.line("}"); - }); - w.line("}"); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-dart/src/lib.rs b/crates/weaveffi-gen-dart/src/lib.rs deleted file mode 100644 index 426f1074..00000000 --- a/crates/weaveffi-gen-dart/src/lib.rs +++ /dev/null @@ -1,343 +0,0 @@ -//! Dart (`dart:ffi`) binding generator for WeaveFFI. -//! -//! Emits a Dart package (`pubspec.yaml` + library) with `dart:ffi` -//! bindings over the C ABI for use in Flutter and Dart projects. -//! Implements [`LanguageBackend`]; the shared driver bridges it into the -//! generator pipeline. -//! -//! Records and rich enums are value types: they render as plain Dart classes -//! (a sealed hierarchy for a rich enum) and cross the ABI serialized in the -//! WeaveFFI value-buffer format as one `(ptr, len)` pair. The generated -//! library ships a small private buffer writer/reader implementing that -//! format, plus one pack and one unpack routine per record and rich enum. -//! -//! Interfaces render as reference-counted wrapper classes: each instance -//! adopts one strong reference, releases it through `dispose()` or a -//! `NativeFinalizer` backstop, and can mint a second reference (the -//! interface's `_clone` symbol) when the codec writes it into a value buffer -//! as an object token. `Interface?` is a nullable wrapper (`Store?`). -//! -//! Callback interfaces render as abstract classes the consumer implements. -//! Passing an implementation parks it in a handle table keyed by an integer -//! that crosses as `ctx`, alongside one process-wide static vtable per -//! interface whose entries are `NativeCallable.isolateLocal` trampolines. -//! Because `isolateLocal` callables can only run on the owning isolate's -//! thread, a producer must invoke callback methods synchronously during a -//! call from Dart; invocation from another producer thread is unsupported. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod callbacks; -mod calls; -mod codec; -mod docs; -mod entities; -mod package; -mod runtime; -mod types; - -use camino::Utf8Path; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::{ - BindingModel, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, - ModuleBinding, StructBinding, -}; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::pkg; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::callbacks::render_callback_interface; -use crate::calls::render_function; -use crate::entities::{render_enum, render_error, render_interface, render_struct}; -use crate::package::{render_packaged_readme, render_pubspec, render_readme}; -use crate::runtime::{ - bundles_platform, dart_loader_original, dart_loader_packaged, render_abi_version_check, - render_buffer_runtime, render_callback_runtime, render_error_plumbing, -}; - -/// Per-target configuration for [`DartGenerator`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct DartConfig { - /// Dart package name (recorded in `pubspec.yaml`). Defaults to - /// `"weaveffi"`. - pub package_name: Option, - /// When `true` (the default), strip the IR module path from emitted - /// free-function names, so a `contacts` module exports `createContact` - /// rather than `contactsCreateContact`. Set to `false` to restore - /// module-prefixed names. - pub strip_module_prefix: bool, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the `dart:ffi` bindings call the - /// same exported symbols the producer emits. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with. - #[serde(skip)] - pub input_basename: Option, -} - -impl Default for DartConfig { - fn default() -> Self { - Self { - package_name: None, - strip_module_prefix: true, - prefix: None, - input_basename: None, - } - } -} - -impl DartConfig { - /// Returns the configured Dart package name, falling back to `"weaveffi"`. - pub fn package_name(&self) -> &str { - self.package_name.as_deref().unwrap_or("weaveffi") - } - - /// Returns the configured C ABI symbol prefix, falling back to - /// `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// Dart backend: emits a Dart package (`pubspec.yaml` plus library) with -/// `dart:ffi` bindings over the C ABI. -pub struct DartGenerator; - -impl DartGenerator { - /// Render the primary `weaveffi.dart` source by composing the shared - /// [`LanguageBackend::emit_members`] walk over every module. Shared by the - /// [`LanguageBackend::files`] and [`LanguageBackend::package`] hooks so - /// there is one assembly path. - fn render_dart_source( - &self, - api: &ResolvedApi, - model: &BindingModel, - config: &DartConfig, - ) -> String { - let input_basename = config.input_basename(); - let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); - let has_async = model.has_async(); - // The default shared-library basename follows the package identity - // (`lib`), matching the producer cdylib. WEAVEFFI_LIBRARY still wins. - let resolved = pkg::resolve(api, None, Some(input_basename)); - let lib_base = resolved.ident_name(); - - out.push_str( - "// ignore_for_file: non_constant_identifier_names, camel_case_types, unused_element\n\n", - ); - if has_async { - out.push_str("import 'dart:async';\n"); - } - out.push_str("import 'dart:convert';\n"); - out.push_str("import 'dart:ffi';\n"); - out.push_str("import 'dart:io' show Platform;\n"); - out.push_str("import 'dart:typed_data';\n\n"); - out.push_str("import 'package:ffi/ffi.dart';\n\n"); - - out.push_str(&dart_loader_original(&lib_base)); - out.push('\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); - - if model.has_callback_interfaces() { - render_callback_runtime(&mut out); - } - - if model.has_iterators() { - out.push_str("\n// Anchors one live native iteration for its GC-finalizer backstop.\n"); - out.push_str( - "// A suspended `sync*` frame keeps the anchor reachable; abandoning the\n", - ); - out.push_str("// iteration drops the frame, and the finalizer destroys the native\n"); - out.push_str("// iterator handle. Exhausted iterations detach before destroying\n"); - out.push_str("// eagerly, so the handle is destroyed exactly once either way.\n"); - out.push_str("final class _IteratorLifetime implements Finalizable {}\n"); - } - - for module in &model.modules { - self.emit_members(&mut out, module, config); - } - - out.push('\n'); - out.push_str(&render_trailer(CommentStyle::DoubleSlash, "weaveffi.dart")); - out - } -} - -impl LanguageBackend for DartGenerator { - type Config = DartConfig; - - fn name(&self) -> &'static str { - "dart" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn render_error( - &self, - out: &mut String, - module: &ModuleBinding, - e: &ErrorBinding, - _config: &Self::Config, - ) { - render_error(out, module, e); - } - - fn render_enum(&self, out: &mut String, e: &EnumBinding, _config: &Self::Config) { - render_enum(out, e); - } - - fn render_struct( - &self, - out: &mut String, - _module: &ModuleBinding, - s: &StructBinding, - _config: &Self::Config, - ) { - render_struct(out, s); - } - - fn render_interface( - &self, - out: &mut String, - module: &ModuleBinding, - i: &InterfaceBinding, - _config: &Self::Config, - ) { - render_interface(out, module, i); - } - - fn render_callback_interface( - &self, - out: &mut String, - _module: &ModuleBinding, - cb: &CallbackInterfaceBinding, - _config: &Self::Config, - ) { - render_callback_interface(out, cb); - } - - fn render_function( - &self, - out: &mut String, - module: &ModuleBinding, - f: &FnBinding, - config: &Self::Config, - ) { - render_function(out, module, f, config.strip_module_prefix); - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let dart_dir = out_dir.join("dart"); - let lib_dir = dart_dir.join("lib"); - let input_basename = config.input_basename(); - vec![ - OutputFile::new( - lib_dir.join("weaveffi.dart"), - self.render_dart_source(api, model, config), - ), - OutputFile::new( - dart_dir.join("pubspec.yaml"), - render_pubspec( - &pkg::resolve( - api, - config.package_name.as_deref(), - config.input_basename.as_deref(), - ), - input_basename, - ), - ), - OutputFile::new( - dart_dir.join("README.md"), - render_readme( - &pkg::resolve( - api, - config.package_name.as_deref(), - config.input_basename.as_deref(), - ), - input_basename, - ), - ), - ] - } - - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let input_basename = config.input_basename(); - let package = pkg::resolve( - api, - config.package_name.as_deref(), - config.input_basename.as_deref(), - ); - // The lib base in the generated source follows the same rule the - // module uses (`pkg::resolve(api, None, basename)`), so reconstruct it - // identically to swap the loader. - let lib_base = pkg::resolve(api, None, Some(input_basename)).ident_name(); - let lib = &ctx.binaries.lib_name; - - let module_src = self - .render_dart_source(api, model, config) - .replace(&dart_loader_original(&lib_base), &dart_loader_packaged(lib)); - - let dart_dir = out_dir.join("dart"); - let mut files = vec![ - PackagedFile::text(dart_dir.join("lib").join("weaveffi.dart"), module_src), - PackagedFile::text( - dart_dir.join("pubspec.yaml"), - render_pubspec(&package, input_basename), - ), - PackagedFile::text( - dart_dir.join("README.md"), - render_packaged_readme(&package, ctx, input_basename), - ), - ]; - // Bundle every prebuilt desktop library under native//; - // the loader has no slot for Android or wasm32 builds. - for nb in ctx - .binaries - .binaries - .iter() - .filter(|nb| bundles_platform(nb.platform)) - { - let dest = dart_dir - .join("native") - .join(nb.platform.id()) - .join(ctx.binaries.bundled_filename(nb.platform)); - files.push(PackagedFile::copy(dest, nb.source.clone())); - } - Some(files) - } -} diff --git a/crates/weaveffi-gen-dart/src/package.rs b/crates/weaveffi-gen-dart/src/package.rs deleted file mode 100644 index a8349b2e..00000000 --- a/crates/weaveffi-gen-dart/src/package.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Package-file emission: `pubspec.yaml` and the README variants for the -//! generated and packaged layouts. - -use weaveffi_core::package::PackageContext; -use weaveffi_core::pkg::ResolvedPackage; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::runtime::bundles_platform; - -/// Render the `pubspec.yaml` manifest for the generated Dart package. -pub(crate) fn render_pubspec(package: &ResolvedPackage, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::Hash, input_basename); - let trailer = render_trailer(CommentStyle::Hash, "pubspec.yaml"); - let name = package.ident_name(); - let version = &package.version; - let description = package.description_or_default(); - let mut meta = format!("description: {description}\n"); - if let Some(homepage) = package.homepage.as_ref().or(package.repository.as_ref()) { - meta.push_str(&format!("homepage: {homepage}\n")); - } - format!( - "{prelude}name: {name}\n\ - version: {version}\n\ - {meta}\ - environment:\n\ - \x20 sdk: '>=3.1.0 <4.0.0'\n\ - dependencies:\n\ - \x20 ffi: ^2.0.0\n\n\ - {trailer}" - ) -} - -/// Render the README for a generated (unpackaged) Dart artifact. -pub(crate) fn render_readme(package: &ResolvedPackage, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = &package.name; - let import_name = package.ident_name(); - format!( - r#"{prelude}# {name} (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:{import_name}/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`. - -## Objects and callbacks - -Interface objects are reference counted by the native library. Each Dart -wrapper holds one reference: call `dispose()` when you are done with it, or let -the garbage collector's finalizer release it. Objects nested inside records and -collections follow the same rule. - -Callback interfaces are abstract classes: implement one and pass an instance to -any function that takes it. Methods are bound with `NativeCallable.isolateLocal`, -so the native library may only invoke them on the isolate's own thread (which -holds when it calls them synchronously during a call from Dart). An exception -thrown by an implementation aborts the native call and surfaces to the original -Dart caller as a `WeaveFFIException` with `WeaveFFIException.foreignCode`. - -## Requirements - -- Dart SDK >= 3.1.0 (for `NativeCallable`) -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. - -{trailer}"# - ) -} - -/// Render the README for a packaged Dart artifact that bundles native -/// libraries. -pub(crate) fn render_packaged_readme( - package: &ResolvedPackage, - ctx: &PackageContext, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = package.name.clone(); - let platforms: Vec = ctx - .binaries - .platforms() - .filter(|p| bundles_platform(*p)) - .map(|p| format!("- `native/{}/`", p.id())) - .collect(); - let platform_list = platforms.join("\n"); - format!( - r#"{prelude}# {name} (Dart) - -Auto-generated `dart:ffi` bindings with prebuilt native libraries bundled under -`native//`. The loader prefers a bundled library (resolved relative to -the working directory) and falls back to the system search path; -`WEAVEFFI_LIBRARY` overrides both. - -## Bundled platforms - -{platform_list} - -{trailer}"#, - ) -} diff --git a/crates/weaveffi-gen-dart/src/runtime.rs b/crates/weaveffi-gen-dart/src/runtime.rs deleted file mode 100644 index ebe340e4..00000000 --- a/crates/weaveffi-gen-dart/src/runtime.rs +++ /dev/null @@ -1,577 +0,0 @@ -//! The private Dart runtime every generated library ships: the dynamic -//! library loaders, the error struct and generic exception plumbing, the -//! value-buffer writer/reader pair, the callback-interface handle table, and -//! the `lookupFunction` binding helper. - -use heck::{ToLowerCamelCase, ToUpperCamelCase}; -use weaveffi_core::cabi::ABI_VERSION; -use weaveffi_core::platform::{Os, Platform}; - -/// The reserved `weaveffi_error.code` a callback-interface trampoline reports -/// when the Dart implementation raised (`FOREIGN_ERROR_CODE` in the ABI). -pub(crate) const FOREIGN_ERROR_CODE: i32 = -4; - -/// 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. -pub(crate) fn dart_loader_original(lib_base: &str) -> String { - let mut out = String::new(); - out.push_str("DynamicLibrary _openLibrary() {\n"); - out.push_str(" // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a\n"); - out.push_str(" // specific build artifact regardless of its file name or location.\n"); - out.push_str(" final override = Platform.environment['WEAVEFFI_LIBRARY'];\n"); - out.push_str( - " if (override != null && override.isNotEmpty) return DynamicLibrary.open(override);\n", - ); - out.push_str(&format!( - " if (Platform.isMacOS) return DynamicLibrary.open('lib{lib_base}.dylib');\n" - )); - out.push_str(&format!( - " if (Platform.isLinux) return DynamicLibrary.open('lib{lib_base}.so');\n" - )); - out.push_str(&format!( - " if (Platform.isWindows) return DynamicLibrary.open('{lib_base}.dll');\n" - )); - out.push_str( - " throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');\n", - ); - out.push_str("}\n"); - out -} - -/// Whether the Dart package bundles a prebuilt library for `platform`: the -/// `dart:ffi` loader resolves `native//` for the desktop matrix -/// only (Android libraries ship through Flutter's `jniLibs`, and a `.wasm` -/// module can't be opened with `DynamicLibrary`). -pub(crate) fn bundles_platform(platform: Platform) -> bool { - platform.is_desktop() -} - -/// The bundled candidate paths the packaged loader tries for one OS family, -/// in [`Platform::DESKTOP`] order, followed by the bare system library name. -fn loader_candidates(os: Os, lib: &str) -> String { - let mut candidates: Vec = Platform::DESKTOP - .iter() - .filter(|p| p.os() == os) - .map(|p| format!("'native/{}/{}'", p.id(), p.lib_filename(lib))) - .collect(); - let bare = Platform::DESKTOP - .iter() - .find(|p| p.os() == os) - .map_or_else(|| format!("lib{lib}.so"), |p| p.lib_filename(lib)); - candidates.push(format!("'{bare}'")); - candidates.join(", ") -} - -/// The packaged `_openLibrary` for `lib`: try the bundled `native//` -/// libraries (relative to the working directory) before the bare system name. -/// `WEAVEFFI_LIBRARY` still overrides. -pub(crate) fn dart_loader_packaged(lib: &str) -> String { - let mut out = String::new(); - out.push_str("DynamicLibrary _openLibrary() {\n"); - out.push_str(" final override = Platform.environment['WEAVEFFI_LIBRARY'];\n"); - out.push_str( - " if (override != null && override.isNotEmpty) return DynamicLibrary.open(override);\n", - ); - out.push_str(" final candidates = [];\n"); - out.push_str(" if (Platform.isMacOS) {\n"); - out.push_str(&format!( - " candidates.addAll([{}]);\n", - loader_candidates(Os::MacOs, lib) - )); - out.push_str(" } else if (Platform.isWindows) {\n"); - out.push_str(&format!( - " candidates.addAll([{}]);\n", - loader_candidates(Os::Windows, lib) - )); - out.push_str(" } else {\n"); - out.push_str(&format!( - " candidates.addAll([{}]);\n", - loader_candidates(Os::Linux, lib) - )); - out.push_str(" }\n"); - out.push_str(" for (final candidate in candidates) {\n"); - out.push_str(" try {\n"); - out.push_str(" return DynamicLibrary.open(candidate);\n"); - out.push_str(" } catch (_) {}\n"); - out.push_str(" }\n"); - out.push_str( - " throw UnsupportedError('Could not load the native library for ${Platform.operatingSystem}');\n", - ); - out.push_str("}\n"); - out -} - -/// Emit the private Dart value-buffer runtime: the little-endian, packed -/// writer and reader plus the staging/copy helpers wrappers use to move -/// encodings across the boundary. -pub(crate) fn render_buffer_runtime(out: &mut String) { - out.push_str( - r#" -// ── 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'); - } -} -"#, - ); -} - -/// Emit the shared callback-interface runtime: the handle table that keeps -/// Dart implementations alive while the producer holds them, the key -/// allocator whose value crosses as `ctx`, the `NativeCallable` anchor list -/// that pins every vtable trampoline for the process lifetime, and the -/// `_foreignError` reporter trampolines route a thrown exception through. -pub(crate) fn render_callback_runtime(out: &mut String) { - out.push_str(&format!( - r#" -// ── WeaveFFI callback-interface runtime ── -// A Dart implementation passed to the producer is stored here under an -// integer key; the key (widened to a pointer) is what crosses as `ctx`, so the -// producer never holds a Dart object and the GC never sees a raw pointer. The -// entry lives until the producer calls the vtable's `free(ctx)`. -final Map _callbackTable = {{}}; -int _nextCallbackKey = 1; - -Pointer _registerCallback(Object impl) {{ - final key = _nextCallbackKey++; - _callbackTable[key] = impl; - return Pointer.fromAddress(key); -}} - -Object _callbackFor(Pointer ctx) {{ - final impl = _callbackTable[ctx.address]; - if (impl == null) {{ - throw StateError('callback interface context ${{ctx.address}} is not registered'); - }} - return impl; -}} - -// Vtable trampolines are process-wide: their NativeCallables are never closed, -// so they are anchored here to keep the native thunks alive. `keepIsolateAlive` -// is cleared on each so an idle isolate can still exit. -final List> _callbackCallables = []; - -Pointer> _pinCallable( - NativeCallable callable) {{ - callable.keepIsolateAlive = false; - _callbackCallables.add(callable); - return callable.nativeFunction; -}} - -// Reports a Dart exception thrown by a callback-interface implementation -// through `weaveffi_error_set` with the reserved foreign error code ({code}). -// The producer copies the message, so the temporary is freed right away. This -// must never throw: it runs in the catch path of a trampoline whose C frame -// an exception must not unwind through. -void _foreignError(Pointer<_WeaveFFIError> outErr, Object error) {{ - if (outErr == nullptr) return; - Pointer message = nullptr; - try {{ - message = error.toString().toNativeUtf8(); - _weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, message); - }} catch (_) {{ - _weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, nullptr); - }} finally {{ - if (message != nullptr) calloc.free(message); - }} -}} -"#, - code = FOREIGN_ERROR_CODE - )); -} - -/// Emit the dart:ffi typedef pair and `lookupFunction` binding for one C -/// symbol. -pub(crate) fn emit_typedef_and_lookup( - out: &mut String, - c_sym: &str, - native_params: &str, - dart_params: &str, - native_ret: &str, - dart_ret: &str, -) { - let td = c_sym.to_upper_camel_case(); - let var = c_sym.to_lower_camel_case(); - out.push_str(&format!( - "\ntypedef _Native{td} = {native_ret} Function({native_params});\n" - )); - out.push_str(&format!( - "typedef _Dart{td} = {dart_ret} Function({dart_params});\n" - )); - out.push_str(&format!( - "final _{var} = _lib.lookupFunction<\n _Native{td}, _Dart{td}>('{c_sym}');\n" - )); -} - -/// Emit the shared error plumbing: the `_WeaveFFIError` struct mirroring the -/// C `weaveffi_error`, the runtime release lookups, the generic branded -/// exception, and the `_checkError` trap helper non-throwing wrappers route -/// their out-err slots through. -pub(crate) fn render_error_plumbing(out: &mut String) { - // The error slot every fallible call writes. `payloadPtr`/`payloadLen` - // hold the matched error code's fields serialized in the value-buffer - // format (null when the code declares no fields); `weaveffi_error_clear` - // releases both the message and the payload. - out.push_str("final class _WeaveFFIError extends Struct {\n"); - out.push_str(" @Int32()\n"); - out.push_str(" external int code;\n"); - out.push_str(" external Pointer message;\n"); - out.push_str(" external Pointer payloadPtr;\n"); - out.push_str(" @Size()\n"); - out.push_str(" external int payloadLen;\n"); - out.push_str("}\n"); - - // Callback-interface trampolines report a failed Dart implementation - // through `weaveffi_error_set`, which copies the borrowed message with the - // producer's allocator (the producer frees `message` itself, so the - // consumer must never write the struct directly). - emit_typedef_and_lookup( - out, - "weaveffi_error_set", - "Pointer<_WeaveFFIError>, Int32, Pointer", - "Pointer<_WeaveFFIError>, int, Pointer", - "Void", - "void", - ); - emit_typedef_and_lookup( - out, - "weaveffi_error_clear", - "Pointer<_WeaveFFIError>", - "Pointer<_WeaveFFIError>", - "Void", - "void", - ); - - // Async completion callbacks receive a heap-boxed error the consumer - // owns; `weaveffi_error_free` releases the message, the payload, and the - // box itself once the deferred listener has copied what it needs. - emit_typedef_and_lookup( - out, - "weaveffi_error_free", - "Pointer<_WeaveFFIError>", - "Pointer<_WeaveFFIError>", - "Void", - "void", - ); - - // Runtime release helpers: every returned `const char*` is freed with - // `weaveffi_free_string` after copying, and every producer-allocated - // buffer (bytes and serialized value buffers) with `weaveffi_free_bytes`. - // The runtime always exports these under the canonical `weaveffi_` names, - // like `weaveffi_error_clear`. - emit_typedef_and_lookup( - out, - "weaveffi_free_string", - "Pointer", - "Pointer", - "Void", - "void", - ); - emit_typedef_and_lookup( - out, - "weaveffi_free_bytes", - "Pointer, Size", - "Pointer, int", - "Void", - "void", - ); - - out.push_str("\n/// Generic WeaveFFI failure: a producer panic ([panicCode]), a marshalling\n"); - out.push_str("/// error ([marshalCode]), a callback-interface implementation that threw\n"); - out.push_str("/// ([foreignCode], carrying the Dart exception's text), or an unknown code.\n"); - out.push_str("class WeaveFFIException implements Exception {\n"); - out.push_str(" /// The producer reported an untyped error.\n"); - out.push_str(" static const int genericCode = -1;\n"); - out.push_str(" /// The producer panicked; [message] carries the panic text.\n"); - out.push_str(" static const int panicCode = -2;\n"); - out.push_str(" /// An argument could not be lifted by the producer.\n"); - out.push_str(" static const int marshalCode = -3;\n"); - out.push_str(" /// A Dart callback-interface implementation threw; [message] carries the\n"); - out.push_str(" /// exception's text.\n"); - out.push_str(&format!( - " static const int foreignCode = {FOREIGN_ERROR_CODE};\n" - )); - out.push_str(" final int code;\n"); - out.push_str(" final String message;\n"); - out.push_str(" WeaveFFIException(this.code, this.message);\n"); - out.push_str(" @override\n"); - out.push_str(" String toString() => '$runtimeType($code): $message';\n"); - out.push_str("}\n\n"); - - out.push_str("void _checkError(Pointer<_WeaveFFIError> err) {\n"); - out.push_str(" if (err.ref.code != 0) {\n"); - // Capture code and message *before* clearing, which zeroes the struct. - out.push_str(" final code = err.ref.code;\n"); - out.push_str(" final msg = err.ref.message.toDartString();\n"); - out.push_str(" _weaveffiErrorClear(err);\n"); - out.push_str(" throw WeaveFFIException(code, msg);\n"); - out.push_str(" }\n"); - out.push_str("}\n"); -} diff --git a/crates/weaveffi-gen-dart/src/types.rs b/crates/weaveffi-gen-dart/src/types.rs deleted file mode 100644 index aad41583..00000000 --- a/crates/weaveffi-gen-dart/src/types.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! Dart type mapping and naming: the `dart:ffi` vocabulary for ABI slots, -//! surface types for signatures, and the identifier policy applied to -//! user-chosen IDL names before they land in generated Dart. - -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}; - -/// The Dart spelling of an IDL value identifier (parameter, field, or the -/// base of a derived local): lowerCamelCase via heck, then keyword-escaped -/// (a name like `class` becomes `class_`). -pub(crate) fn dart_ident(name: &str) -> String { - lang::escape_ident(&name.to_lower_camel_case(), lang::DART_KEYWORDS) -} - -/// The Dart spelling of a module-level free function: the module-path prefix -/// applied per config, then lowerCamelCase, then keyword escaping. -pub(crate) fn dart_wrapper_fn_name( - module_path: &str, - name: &str, - strip_module_prefix: bool, -) -> String { - lang::escape_ident( - &wrapper_name(module_path, name, strip_module_prefix).to_lower_camel_case(), - lang::DART_KEYWORDS, - ) -} - -/// The idiomatic Dart type a [`Ty`] surfaces as. -pub(crate) fn dart_type(ty: &Ty) -> String { - match ty { - Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::U64 => "int".into(), - Ty::F32 | Ty::F64 => "double".into(), - Ty::Bool => "bool".into(), - Ty::StringUtf8 => "String".into(), - Ty::Bytes => "List".into(), - // Records, rich enums, C-style enums, interfaces, and callback - // 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. - Ty::Enum(n) - | Ty::Record(n) - | Ty::RichEnum(n) - | Ty::Interface(n) - | Ty::CallbackInterface(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)), - } -} - -/// The bare local Dart class name of a (possibly dot-qualified) user type. -pub(crate) fn dart_class(name: &str) -> String { - local_type_name(name).to_upper_camel_case() -} - -/// The Dart wrapper class of a direct or nullable interface reference. -/// -/// # Panics -/// -/// Panics when `ty` is not an interface or an optional interface; callers -/// dispatch on [`RetPass::Object`] or [`ArgPass::Object`] first. -pub(crate) fn object_class(ty: &Ty) -> String { - match ty { - Ty::Interface(name) => dart_class(name), - Ty::Optional(inner) => object_class(inner), - _ => unreachable!("object positions are (optional) interfaces"), - } -} - -/// The private Dart global holding a callback interface's one static vtable -/// pointer: `_{Class}Vtable`. -pub(crate) fn vtable_var(name: &str) -> String { - format!("_{}Vtable", dart_class(name)) -} - -/// 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: &Ty) -> (&'static str, &'static str) { - match ty { - 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 => ("Int64", "int"), - Ty::F32 => ("Float", "double"), - Ty::F64 => ("Double", "double"), - _ => ("Int64", "int"), - } -} - -/// The Dart literal a trampoline returns when the implementation raised: the -/// zero value of the method's direct return type (`0`, `0.0`, or `false`). -pub(crate) fn default_literal(ty: &Ty) -> &'static str { - match ty { - Ty::Bool => "false", - Ty::F32 | Ty::F64 => "0.0", - _ => "0", - } -} - -// ── ABI slot typing ── - -/// The (native, dart) FFI typedef slot pairs a single input parameter expands -/// into, mirroring the C ABI and driven by the parameter's [`ArgPass`] -/// contract: a buffered value is one borrowed `(const uint8_t*, size_t)` -/// pair; bytes fan out to `(ptr, len)`; strings and interfaces stay one -/// pointer slot; a callback interface is a `(ctx, vtable)` pointer pair; -/// everything else is a by-value scalar. -pub(crate) fn input_slots(p: &ParamBinding) -> Vec<(String, String)> { - let ptr = |s: &str| (s.to_string(), s.to_string()); - match p.arg_pass() { - ArgPass::Buffer { .. } | ArgPass::Bytes { .. } => { - vec![ptr("Pointer"), ("Size".into(), "int".into())] - } - ArgPass::String { .. } => vec![ptr("Pointer")], - // Interfaces and nullable interfaces are one (possibly null) object - // pointer. - ArgPass::Object { .. } => vec![ptr("Pointer")], - // The consumer's handle-table key and its static vtable. - ArgPass::Callback { .. } => vec![ptr("Pointer"), ptr("Pointer")], - ArgPass::Direct { .. } => { - let (n, d) = scalar_ffi(&p.ty); - vec![(n.into(), d.into())] - } - } -} - -/// The FFI return type (native, dart) of a call symbol, driven by the -/// return's [`RetPass`] contract. Buffered and bytes returns come back as a -/// producer-allocated `Pointer`; strings as `Pointer`; -/// interfaces as opaque pointers. -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 clone and destroy - // symbols, which the FFI typedef never names; empty context is fine here. - match plan::ret_pass(Some(ty), "", "") { - RetPass::Buffer | RetPass::Bytes => ptr("Pointer"), - RetPass::String => ptr("Pointer"), - RetPass::Object { .. } => ptr("Pointer"), - RetPass::Void | RetPass::Direct => { - let (n, d) = scalar_ffi(ty); - (n.into(), d.into()) - } - } -} - -/// 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: &Ty) -> Vec<(String, String)> { - if returns_buffer(ty) { - vec![("Pointer".into(), "Pointer".into())] - } else { - vec![] - } -} - -/// 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: &Ty) -> bool { - matches!( - plan::ret_pass(Some(ty), "", ""), - RetPass::Buffer | RetPass::Bytes - ) -} - -/// Escape a string for embedding in a single-quoted Dart literal. -pub(crate) fn dart_str_literal(s: &str) -> String { - s.replace('\\', "\\\\") - .replace('\'', "\\'") - .replace('$', "\\$") -} diff --git a/crates/weaveffi-gen-dart/tests/abi2.rs b/crates/weaveffi-gen-dart/tests/abi2.rs deleted file mode 100644 index 90b3fe69..00000000 --- a/crates/weaveffi-gen-dart/tests/abi2.rs +++ /dev/null @@ -1,429 +0,0 @@ -//! ABI revision 2 rendering checks: reference-counted objects, nullable -//! objects, object tokens inside value buffers, iterators of objects, and -//! callback-interface vtables with their trampolines. - -use camino::Utf8Path; -use weaveffi_core::backend::LanguageBackend; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::PackageContext; -use weaveffi_core::platform::{BinarySet, Platform}; -use weaveffi_core::validate::validate_api; -use weaveffi_gen_dart::{DartConfig, DartGenerator}; -use weaveffi_ir::ir::{ - Api, CallbackInterfaceDef, EnumDef, EnumVariant, Function, InterfaceDef, Module, Param, - StructDef, StructField, TypeRef, CURRENT_SCHEMA_VERSION, -}; - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } -} - -fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn named(name: &str) -> TypeRef { - TypeRef::Named(name.into()) -} - -/// One module exercising every revision-2 shape: an interface with a -/// constructor and a method, `Interface?` in and out, a record with an -/// `Interface` field and a `[Interface]` field, an iterator over interface -/// elements, a callback interface with string, `i32`, record, and object -/// parameters (one `bool` return, one `void`), and a function taking it. -fn fixture() -> Api { - Api { - version: CURRENT_SCHEMA_VERSION.into(), - modules: vec![Module { - name: "bus".into(), - doc: None, - functions: vec![ - func( - "lookup", - vec![param( - "fallback", - TypeRef::Optional(Box::new(named("Ticker"))), - )], - Some(TypeRef::Optional(Box::new(named("Ticker")))), - ), - func( - "tickers", - vec![], - Some(TypeRef::Iterator(Box::new(named("Ticker")))), - ), - func( - "subscribe", - vec![param("listener", named("Subscriber"))], - None, - ), - func( - "describe", - vec![param("env", named("Envelope"))], - Some(named("Envelope")), - ), - Function { - r#async: true, - ..func( - "fetch", - vec![param("id", TypeRef::I64)], - Some(TypeRef::Optional(Box::new(named("Ticker")))), - ) - }, - ], - interfaces: vec![InterfaceDef { - name: "Ticker".into(), - doc: Some("A counter.".into()), - deprecated: None, - constructors: vec![func("new", vec![param("start", TypeRef::I64)], None)], - methods: vec![func("value", vec![], Some(TypeRef::I64))], - statics: vec![], - }], - callback_interfaces: vec![CallbackInterfaceDef { - name: "Subscriber".into(), - doc: Some("Receives bus events.".into()), - deprecated: None, - methods: vec![ - func( - "on_message", - vec![ - param("text", TypeRef::StringUtf8), - param("weight", TypeRef::I32), - param("envelope", named("Envelope")), - ], - Some(TypeRef::Bool), - ), - func( - "on_ticker", - vec![ - param("ticker", named("Ticker")), - param("alt", TypeRef::Optional(Box::new(named("Ticker")))), - ], - None, - ), - func( - "classify", - vec![param("weight", TypeRef::I32)], - Some(named("Priority")), - ), - ], - }], - structs: vec![StructDef { - name: "Envelope".into(), - doc: None, - deprecated: None, - fields: vec![ - field("topic", TypeRef::StringUtf8), - field("primary", named("Ticker")), - field("others", TypeRef::List(Box::new(named("Ticker")))), - ], - }], - enums: vec![EnumDef { - name: "Priority".into(), - doc: None, - deprecated: None, - variants: vec![ - EnumVariant { - name: "Low".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "High".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - errors: None, - modules: vec![], - }], - } -} - -fn render() -> String { - let api = validate_api(fixture(), None).expect("fixture validates"); - let config = DartConfig::default(); - let model = BindingModel::build(&api, DartGenerator.prefix(&config)); - let files = DartGenerator.files(&api, &model, Utf8Path::new("out"), &config); - files - .into_iter() - .find(|f| f.path.as_str().ends_with("weaveffi.dart")) - .expect("primary source") - .contents -} - -fn assert_has(src: &str, needle: &str) { - assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); -} - -#[test] -fn objects_are_reference_counted_wrappers_with_finalizers() { - let src = render(); - assert_has(&src, "class Ticker implements Finalizable {"); - assert_has(&src, "_lib.lookupFunction<\n _NativeWeaveffiBusTickerClone, _DartWeaveffiBusTickerClone>('weaveffi_bus_Ticker_clone')"); - assert_has(&src, "_lib.lookupFunction<\n _NativeWeaveffiBusTickerDestroy, _DartWeaveffiBusTickerDestroy>('weaveffi_bus_Ticker_destroy')"); - assert_has( - &src, - "final _weaveffiBusTickerDestroyFinalizer = NativeFinalizer(\n _lib.lookup)>>('weaveffi_bus_Ticker_destroy'));", - ); - // Adopting attaches the finalizer; dispose detaches and destroys once. - assert_has(&src, "Ticker._(this._ptr) {\n _weaveffiBusTickerDestroyFinalizer.attach(this, _ptr, detach: this);"); - assert_has(&src, "void dispose() {\n if (_disposed) return;\n _disposed = true;\n _weaveffiBusTickerDestroyFinalizer.detach(this);\n _weaveffiBusTickerDestroy(_ptr);"); - assert_has( - &src, - "if (_disposed) throw StateError('Ticker used after dispose()');", - ); - assert_has( - &src, - "Pointer _cloneRef() => _weaveffiBusTickerClone(_handle);", - ); - // Constructor and method render as factory and instance method. - assert_has(&src, "factory Ticker(int start) {"); - assert_has(&src, "return Ticker._(result);"); - assert_has(&src, "int value() {"); - assert_has(&src, "_weaveffiBusTickerValue(_handle, err)"); -} - -#[test] -fn nullable_objects_are_nullable_pointers_both_ways() { - let src = render(); - assert_has(&src, "Ticker? lookup(Ticker? fallback) {"); - assert_has( - &src, - "_weaveffiBusLookup(fallback?._handle ?? nullptr, err)", - ); - assert_has(&src, "return result == nullptr ? null : Ticker._(result);"); - assert_has(&src, "Returns `null` when the producer reports no object."); -} - -#[test] -fn objects_inside_records_are_cloned_tokens() { - let src = render(); - assert_has(&src, "final Ticker primary;"); - assert_has(&src, "final List others;"); - // Encoding writes a fresh clone, never the wrapper's own pointer. - assert_has(&src, "w.writeUint64(v.primary._cloneRef().address);"); - assert_has(&src, "w.writeUint64(t1._cloneRef().address);"); - // Decoding adopts each token into a new wrapper. - assert_has( - &src, - "primary: Ticker._(Pointer.fromAddress(r.readUint64())),", - ); - assert_has( - &src, - "others: List.generate(r.readLength(), (_) => Ticker._(Pointer.fromAddress(r.readUint64()))),", - ); -} - -#[test] -fn iterators_adopt_object_elements() { - let src = render(); - assert_has(&src, "Iterable tickers() sync* {"); - assert_has(&src, "final outItem = calloc>();"); - assert_has( - &src, - "final itemPtr = outItem.value;\n yield Ticker._(itemPtr);", - ); - assert_has(&src, "_weaveffiBusTickersIteratorDestroy(iter);"); - assert_has(&src, "Each yielded element is owned by the caller"); -} - -#[test] -fn callback_interfaces_render_abstract_class_vtable_and_trampolines() { - let src = render(); - // Consumer-facing abstract class. - assert_has(&src, "abstract class Subscriber {"); - assert_has( - &src, - "bool onMessage(String text, int weight, Envelope envelope);", - ); - assert_has(&src, "void onTicker(Ticker ticker, Ticker? alt);"); - assert_has(&src, "Priority classify(int weight);"); - assert_has( - &src, - "Dart limitation: methods are bound with `NativeCallable.isolateLocal`", - ); - - // The vtable layout: one entry per method in order, then free. - assert_has( - &src, - "final class _SubscriberVtableStruct extends Struct {\n external Pointer> onMessage;\n external Pointer> onTicker;\n external Pointer> classify;\n external Pointer> free;\n}", - ); - // A C-style enum return crosses as its discriminant. - assert_has(&src, "typedef _NativeSubscriberVtClassify = Int32 Function(Pointer, Int32, Pointer<_WeaveFFIError>);"); - assert_has(&src, "return impl.classify(weight).value;"); - assert_has( - &src, - "typedef _NativeSubscriberVtOnMessage = Bool Function(Pointer, Pointer, Int32, Pointer, Size, Pointer<_WeaveFFIError>);", - ); - assert_has( - &src, - "typedef _NativeSubscriberVtOnTicker = Void Function(Pointer, Pointer, Pointer, Pointer<_WeaveFFIError>);", - ); - - // Trampolines decode borrowed args, adopt objects, and trap exceptions. - assert_has( - &src, - "bool _subscriberVtOnMessage(Pointer ctx, Pointer text, int weight, Pointer envelopePtr, int envelopeLen, Pointer<_WeaveFFIError> outErr) {", - ); - assert_has(&src, "final impl = _callbackFor(ctx) as Subscriber;"); - assert_has( - &src, - "final envelopeData = _copyNativeBytes(envelopePtr, envelopeLen);", - ); - assert_has( - &src, - "final envelopeValue = _unpackEnvelope(envelopeReader);", - ); - assert_has( - &src, - "return impl.onMessage(text == nullptr ? '' : text.toDartString(), weight, envelopeValue);", - ); - assert_has( - &src, - "} catch (e) {\n _foreignError(outErr, e);\n return false;\n }", - ); - // Object arguments are adopted before the implementation lookup so a - // failure never leaks the transferred reference. - assert_has( - &src, - "final tickerValue = Ticker._(ticker);\n final altValue = alt == nullptr ? null : Ticker._(alt);\n final impl = _callbackFor(ctx) as Subscriber;\n impl.onTicker(tickerValue, altValue);", - ); - assert_has(&src, "} catch (e) {\n _foreignError(outErr, e);\n }"); - // free drops the handle-table entry. - assert_has( - &src, - "void _subscriberVtFree(Pointer ctx) {\n _callbackTable.remove(ctx.address);", - ); - - // Exactly one static vtable, calloc-allocated, pinned callables. - assert_eq!( - src.matches("final Pointer<_SubscriberVtableStruct> _SubscriberVtable = () {") - .count(), - 1 - ); - assert_has(&src, "final vt = calloc<_SubscriberVtableStruct>();"); - assert_has( - &src, - "vt.ref.onMessage = _pinCallable(NativeCallable<_NativeSubscriberVtOnMessage>.isolateLocal(\n _subscriberVtOnMessage, exceptionalReturn: false));", - ); - assert_has( - &src, - "vt.ref.onTicker = _pinCallable(NativeCallable<_NativeSubscriberVtOnTicker>.isolateLocal(\n _subscriberVtOnTicker));", - ); - assert_has(&src, "vt.ref.free = _pinCallable(NativeCallable<_NativeSubscriberVtFree>.listener(\n _subscriberVtFree));"); - assert_has(&src, "callable.keepIsolateAlive = false;"); - - // Passing an implementation: handle-table key as ctx plus the vtable. - assert_has(&src, "void subscribe(Subscriber listener) {"); - assert_has( - &src, - "_weaveffiBusSubscribe(_registerCallback(listener), _SubscriberVtable.cast(), err)", - ); -} - -#[test] -fn runtime_binds_error_set_and_foreign_code() { - let src = render(); - assert_has(&src, "typedef _NativeWeaveffiErrorSet = Void Function(Pointer<_WeaveFFIError>, Int32, Pointer);"); - assert_has(&src, "'weaveffi_error_set'"); - assert_has(&src, "static const int foreignCode = -4;"); - assert_has( - &src, - "_weaveffiErrorSet(outErr, WeaveFFIException.foreignCode, message);", - ); - assert_has(&src, "const int _abiVersion = 2;"); - for gone in [ - "arena", - "weaveffi_handle_t", - "TypedHandle", - "register_", - "_listenerCallables", - ] { - assert!(!src.contains(gone), "stale `{gone}` in output"); - } -} - -#[test] -fn package_bundles_only_desktop_binaries() { - let api = validate_api(fixture(), None).expect("fixture validates"); - let config = DartConfig::default(); - let model = BindingModel::build(&api, DartGenerator.prefix(&config)); - let mut binaries = BinarySet::new("bus"); - for p in Platform::ALL { - binaries.insert(p, format!("/prebuilt/{}/lib", p.id())); - } - let ctx = PackageContext { - binaries: &binaries, - input_basename: Some("bus.yml"), - }; - let files = DartGenerator - .package(&api, &model, &ctx, Utf8Path::new("out"), &config) - .expect("dart packages"); - let bundled: Vec<&str> = files - .iter() - .filter(|f| f.is_binary()) - .map(|f| f.path.as_str()) - .collect(); - assert_eq!(bundled.len(), Platform::DESKTOP.len()); - for p in Platform::DESKTOP { - assert!( - bundled - .iter() - .any(|b| b.contains(&format!("native/{}/", p.id()))), - "{} missing from {bundled:?}", - p.id() - ); - } - for p in [ - Platform::AndroidArm64, - Platform::AndroidX64, - Platform::Wasm32, - ] { - assert!( - !bundled.iter().any(|b| b.contains(p.id())), - "{} bundled", - p.id() - ); - } - let src = files - .iter() - .find(|f| f.path.as_str().ends_with("weaveffi.dart")) - .and_then(|f| match &f.content { - weaveffi_core::package::FileContent::Text(s) => Some(s.as_str()), - weaveffi_core::package::FileContent::Copy(_) => None, - }) - .expect("packaged source"); - assert_has( - src, - "'native/darwin-arm64/libbus.dylib', 'native/darwin-x64/libbus.dylib', 'libbus.dylib'", - ); - assert_has( - src, - "'native/linux-x64/libbus.so', 'native/linux-arm64/libbus.so', 'libbus.so'", - ); - assert_has(src, "'native/windows-x64/bus.dll', 'bus.dll'"); -} diff --git a/crates/weaveffi-gen-dotnet/Cargo.toml b/crates/weaveffi-gen-dotnet/Cargo.toml deleted file mode 100644 index 3c06b201..00000000 --- a/crates/weaveffi-gen-dotnet/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "weaveffi-gen-dotnet" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "WeaveFFI .NET P/Invoke binding generator" -keywords = ["ffi", "codegen", "bindings", "dotnet", "csharp"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-dotnet/src/callbacks.rs b/crates/weaveffi-gen-dotnet/src/callbacks.rs deleted file mode 100644 index 8007ae3e..00000000 --- a/crates/weaveffi-gen-dotnet/src/callbacks.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! Callback-interface renderers: the consumer-facing C# `interface` plus the -//! process-wide vtable and its `[UnmanagedCallersOnly]` trampolines, honoring -//! the shared `CallbackProtocol` contract. -//! -//! * One static vtable per callback interface: a sequential struct of -//! function pointers written once into unmanaged memory that's never -//! freed, so its address is stable for the process lifetime. -//! * `ctx` is a `GCHandle` to the implementation, allocated when the wrapper -//! passes it and released by the vtable's `free` trampoline, so the GC -//! keeps the object alive exactly as long as the producer holds it. -//! * Trampoline arguments are received like returns: strings, bytes, and -//! buffers are borrowed (copied or decoded, never freed) and objects are -//! adopted into wrappers the implementation owns. -//! * A thrown exception never unwinds through the C frame: the trampoline -//! reports it with `{prefix}_error_set(out_err, -4, message)` and returns a -//! default value. - -use heck::{ToLowerCamelCase, ToUpperCamelCase}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::{ - CallbackInterfaceBinding, CallbackMethodBinding, Family, ModuleBinding, ParamBinding, -}; - -use crate::calls::{adopt_object, direct_from_slot, direct_to_slot, object_class, write_obsolete}; -use crate::codec::emit_buffer_decode; -use crate::docs::{writer_doc, writer_fn_doc}; -use crate::pinvoke::pinvoke_slot; -use crate::types::{ - callback_interface_cs, cs_pinvoke_ctype, cs_type, safe_cs_name, vtable_class_cs, -}; - -/// Render one callback interface: the `public interface I{Name}` the consumer -/// implements, followed by the internal static class hosting the vtable and -/// trampolines that adapt an implementation to the C ABI. -pub(crate) fn render_callback_interface( - out: &mut String, - module: &ModuleBinding, - cb: &CallbackInterfaceBinding, -) { - render_consumer_interface(out, cb); - render_vtable_class(out, module, cb); -} - -/// The C# method name of one callback method. -fn method_cs(m: &CallbackMethodBinding) -> String { - m.name.to_upper_camel_case() -} - -/// The consumer-facing `interface`: one method per callback method with -/// idiomatic C# types (objects as wrapper classes the implementation owns, -/// `Interface?` as a nullable wrapper). -fn render_consumer_interface(out: &mut String, cb: &CallbackInterfaceBinding) { - let iface = callback_interface_cs(&cb.name); - let mut w = CodeWriter::four_space().with_depth(1); - writer_doc(&mut w, &cb.doc); - w.line("/// Implement this interface and pass an instance wherever the"); - w.line("/// native library expects it. The library may call any method from any"); - w.line("/// thread until it releases its last reference to the instance. Object"); - w.line("/// arguments are owned by the implementation, which should dispose them"); - w.line("/// when done. An exception thrown by a method is reported to the native"); - w.line("/// caller as ."); - write_obsolete(&mut w, &cb.deprecated); - w.line(format!("public interface {iface}")); - w.block("{", "}", |w| { - for m in &cb.methods { - let params: Vec = m - .params - .iter() - .map(|p| { - let mut p = p.clone(); - p.name = p.name.to_lower_camel_case(); - p - }) - .collect(); - writer_fn_doc(w, &m.doc, ¶ms); - write_obsolete(w, &m.deprecated); - let ret = m.ret.as_ref().map(cs_type).unwrap_or_else(|| "void".into()); - let sig: Vec = params - .iter() - .map(|p| format!("{} {}", cs_type(&p.ty), safe_cs_name(&p.name))) - .collect(); - w.line(format!("{ret} {}({});", method_cs(m), sig.join(", "))); - } - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// The `delegate* unmanaged[Cdecl]<...>` function-pointer type matching one -/// trampoline's C signature: every slot's P/Invoke type, then the return. -fn function_pointer_type(m: &CallbackMethodBinding) -> String { - let mut parts: Vec = m - .abi_params - .iter() - .map(|slot| cs_pinvoke_ctype(&slot.ty)) - .collect(); - parts.push(cs_pinvoke_ctype(&m.abi_ret)); - format!("delegate* unmanaged[Cdecl]<{}>", parts.join(", ")) -} - -/// The internal static class owning the vtable: a sequential struct of -/// function pointers (one per method in declaration order, then `free`), -/// allocated once in unmanaged memory, plus the trampolines it points at. -fn render_vtable_class(out: &mut String, module: &ModuleBinding, cb: &CallbackInterfaceBinding) { - let iface = callback_interface_cs(&cb.name); - let class = vtable_class_cs(&module.path, &cb.name); - let mut w = CodeWriter::four_space().with_depth(1); - w.line(format!( - "/// The process-wide {} vtable and the trampolines behind", - cb.vtable_tag - )); - w.line(format!( - "/// it, adapting an to the C ABI." - )); - w.line(format!("internal static unsafe class {class}")); - w.line("{"); - w.indent(); - - // The C struct: one function pointer per method in declaration order, - // then the trailing `free`. - w.line("[StructLayout(LayoutKind.Sequential)]"); - w.line("private struct Layout"); - w.block("{", "}", |w| { - for m in &cb.methods { - w.line(format!("public IntPtr {};", safe_cs_name(&m.name))); - } - w.line("public IntPtr free;"); - }); - w.blank(); - - w.line("/// Address of the one static vtable for this interface. It is"); - w.line("/// allocated once and never freed, so the producer may keep the pointer"); - w.line("/// for the process lifetime."); - w.line("internal static readonly IntPtr Pointer = Allocate();"); - w.blank(); - - w.line("private static IntPtr Allocate()"); - w.block("{", "}", |w| { - w.line("var layout = new Layout"); - w.line("{"); - w.scope(|w| { - for m in &cb.methods { - w.line(format!( - "{} = (IntPtr)({})&{}Trampoline,", - safe_cs_name(&m.name), - function_pointer_type(m), - method_cs(m) - )); - } - w.line("free = (IntPtr)(delegate* unmanaged[Cdecl])&FreeTrampoline,"); - }); - w.line("};"); - w.line("var mem = Marshal.AllocHGlobal(Marshal.SizeOf());"); - w.line("Marshal.StructureToPtr(layout, mem, false);"); - w.line("return mem;"); - }); - w.blank(); - - w.line(format!("private static {iface} Target(IntPtr ctx)")); - w.block("{", "}", |w| { - w.line(format!("return ({iface})GCHandle.FromIntPtr(ctx).Target!;")); - }); - w.blank(); - - for m in &cb.methods { - render_trampoline(&mut w, m); - } - - // Fires exactly once, when the producer drops its last reference; the - // GCHandle it frees is the one the wrapper allocated when passing the - // implementation. - w.line("[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]"); - w.line("private static void FreeTrampoline(IntPtr ctx)"); - w.block("{", "}", |w| { - w.line("GCHandle.FromIntPtr(ctx).Free();"); - }); - - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// One `[UnmanagedCallersOnly]` trampoline: decode the borrowed slots, adopt -/// object slots, call the implementation, and write the direct return; any -/// exception is reported through `out_err` with the foreign error code and a -/// default value is returned instead of unwinding. -fn render_trampoline(w: &mut CodeWriter, m: &CallbackMethodBinding) { - let ret_cs = cs_pinvoke_ctype(&m.abi_ret); - let sig: Vec = m.abi_params.iter().map(pinvoke_slot).collect(); - let out_err = safe_cs_name( - &m.abi_params - .last() - .expect("callback methods always end in out_err") - .name, - ); - let ctx = safe_cs_name(&m.abi_params[0].name); - - w.line("[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]"); - w.line(format!( - "private static {ret_cs} {}Trampoline({})", - method_cs(m), - sig.join(", ") - )); - w.block("{", "}", |w| { - w.line("try"); - w.block("{", "}", |w| { - w.line(format!("var impl = Target({ctx});")); - let args: Vec = m - .params - .iter() - .enumerate() - .map(|(idx, p)| render_trampoline_arg(w, p, idx)) - .collect(); - let call = format!("impl.{}({})", method_cs(m), args.join(", ")); - match &m.ret { - None => w.line(format!("{call};")), - Some(ty) => w.line(format!("return {};", direct_to_slot(ty, &call))), - }; - }); - w.line("catch (Exception ex)"); - w.block("{", "}", |w| { - w.line(format!( - "NativeMethods.weaveffi_error_set({out_err}, WeaveFFIException.ForeignErrorCode, ex.Message);" - )); - if m.ret.is_some() { - w.line("return default;"); - } - }); - }); - w.blank(); -} - -/// Statements (written to `w`) plus the expression converting one callback -/// parameter's slots into the value handed to the implementation, receiving -/// it per its [`Family`] like a return of the same type: strings, bytes, and -/// buffers are borrowed for the dispatch and copied (never freed); objects -/// transfer one strong reference and are adopted into a wrapper. -fn render_trampoline_arg(w: &mut CodeWriter, p: &ParamBinding, idx: usize) -> String { - let n0 = safe_cs_name(&p.abi[0].name); - let arg = format!("arg{idx}"); - match p.ty.family() { - Family::Buffer => { - let len = safe_cs_name(&p.abi[1].name); - w.line(format!("var {arg}Buf = new byte[(int){len}];")); - w.line(format!( - "if ({n0} != IntPtr.Zero && (int){len} > 0) Marshal.Copy({n0}, {arg}Buf, 0, (int){len});" - )); - emit_buffer_decode(w, &p.ty, &arg, &format!("{arg}Buf")); - arg - } - Family::String => format!("Marshal.PtrToStringUTF8({n0}) ?? \"\""), - Family::Bytes => { - let len = safe_cs_name(&p.abi[1].name); - w.line(format!("var {arg} = new byte[(int){len}];")); - w.line(format!( - "if ({n0} != IntPtr.Zero && (int){len} > 0) Marshal.Copy({n0}, {arg}, 0, (int){len});" - )); - arg - } - Family::Object { nullable } => { - let adopt = adopt_object(object_class(&p.ty), &n0); - if nullable { - format!("{n0} == IntPtr.Zero ? null : {adopt}") - } else { - adopt - } - } - Family::Direct => direct_from_slot(&p.ty, &n0), - Family::Callback | Family::Iterator => { - unreachable!("{} is not a valid callback method parameter", p.ty) - } - } -} diff --git a/crates/weaveffi-gen-dotnet/src/calls.rs b/crates/weaveffi-gen-dotnet/src/calls.rs deleted file mode 100644 index 52603ebb..00000000 --- a/crates/weaveffi-gen-dotnet/src/calls.rs +++ /dev/null @@ -1,814 +0,0 @@ -//! Call-path renderers: the wrapper methods for every call shape (sync, -//! async, iterator), parameter marshalling driven by the shared -//! [`ArgPass`] plan, return conversions driven by [`Family`], and the -//! per-module static wrapper classes. - -use heck::ToUpperCamelCase; -use weaveffi_core::abi::CType; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::{ - CallShape, ErrorBinding, Family, FnBinding, IteratorBinding, ModuleBinding, ParamBinding, Ty, -}; -use weaveffi_core::plan::{ArgPass, ErrorStrategy}; -use weaveffi_core::utils::{local_type_name, wrapper_name}; - -use crate::codec::{emit_buffer_decode, emit_buffer_write}; -use crate::docs::writer_fn_doc; -use crate::pinvoke::is_error_slot; -use crate::runtime::{check_method_name, dotnet_exception_name}; -use crate::types::{camel_fn, cs_type, safe_cs_name, vtable_class_cs}; - -/// How a wrapper surfaces a non-zero error slot, rendering -/// [`ErrorStrategy`]: [`ErrorStrategy::Throws`] with a domain in scope raises -/// the typed domain exception; everything else (a producer trap, or a -/// throwing function without a declared domain) raises the plain -/// `WeaveFFIException`, which no domain exception check can catch by type. -#[derive(Clone, Copy)] -pub(crate) enum ErrCtx<'a> { - /// Throw the generic `WeaveFFIException`. - Generic, - /// Throw the domain's typed exception via its `FromCode` factory. - Domain(&'a ErrorBinding), -} - -impl<'a> ErrCtx<'a> { - /// The error context for one function: typed when the function's - /// [`ErrorStrategy`] is `Throws` and its module has an error domain in - /// scope, generic otherwise (including every `Trap` function, whose only - /// failures are producer bugs and must not wear the domain type). - pub(crate) fn for_fn(f: &FnBinding, error: Option<&'a ErrorBinding>) -> Self { - match (f.error_strategy(), error) { - (ErrorStrategy::Throws, Some(eb)) => ErrCtx::Domain(eb), - _ => ErrCtx::Generic, - } - } - - /// The check statement placed after a native call writing into `err`. - pub(crate) fn check_stmt(&self) -> String { - self.check_stmt_for("err") - } - - /// The check statement for a named `WeaveFFIError` local. - pub(crate) fn check_stmt_for(&self, var: &str) -> String { - match self { - ErrCtx::Generic => format!("WeaveFFIError.Check({var});"), - ErrCtx::Domain(eb) => format!("WeaveFFIError.{}({var});", check_method_name(eb)), - } - } - - /// The exception expression an async completion callback faults its - /// `TaskCompletionSource` with. A domain exception decodes the error's - /// structured payload, copied out of the heap-boxed error before the - /// callback releases it with `weaveffi_error_free`. - pub(crate) fn async_exception_expr(&self) -> String { - match self { - ErrCtx::Generic => "new WeaveFFIException(wErr.Code, msg)".into(), - ErrCtx::Domain(eb) => { - format!( - "{}.FromCode(wErr.Code, msg, payload)", - dotnet_exception_name(eb) - ) - } - } - } - - /// Emit the `` XML doc line for a throwing wrapper; generic - /// wrappers document nothing extra. - pub(crate) fn write_exception_doc(&self, w: &mut CodeWriter) { - if let ErrCtx::Domain(eb) = self { - w.line(format!( - "/// Thrown when the call reports a {} code.", - dotnet_exception_name(eb), - eb.type_name - )); - } - } -} - -/// The `public` wrapper signature parameter list for `params`. -pub(crate) fn params_sig(params: &[ParamBinding]) -> String { - params - .iter() - .map(|p| format!("{} {}", cs_type(&p.ty), safe_cs_name(&p.name))) - .collect::>() - .join(", ") -} - -/// The `[Obsolete]` attribute line for a deprecated callable. -pub(crate) fn write_obsolete(w: &mut CodeWriter, deprecated: &Option) { - if let Some(msg) = deprecated { - w.line(format!("[Obsolete(\"{}\")]", msg.replace('"', "\\\""))); - } -} - -/// True when a parameter allocates something the wrapper must release after -/// the native call, per its [`ArgPass`] plan: strings (`CoTaskMem` UTF-8 -/// copies), bytes (pinned arrays), and every buffered value (encoded into a -/// pinned value buffer). Direct and object arguments pass inline, and a -/// callback's `GCHandle` is released by the producer's `free(ctx)` instead. -pub(crate) fn param_needs_cleanup(p: &ParamBinding) -> bool { - matches!( - p.arg_pass(), - ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } - ) -} - -/// Emit the setup statements for one parameter before the native call, -/// dispatching on its [`ArgPass`] plan. Strings copy to `CoTaskMem` UTF-8; -/// bytes pin the managed array; buffered parameters encode into a `byte[]` -/// value buffer (`{name}Buf`) and pin it (`{name}Pin`), which the caller -/// owns for the duration of the call; a callback interface implementation is -/// registered in the `GCHandle` table and its handle (`{name}Ctx`) becomes -/// the `ctx` the producer hands back to every trampoline. -pub(crate) fn render_marshal_setup(w: &mut CodeWriter, p: &ParamBinding) { - let name = safe_cs_name(&p.name); - match p.arg_pass() { - ArgPass::Buffer { .. } => { - w.line(format!("var {name}Writer = new WeaveFFIBufferWriter();")); - emit_buffer_write(w, &p.ty, &name, &format!("{name}Writer"), 0); - w.line(format!("var {name}Buf = {name}Writer.ToArray();")); - w.line(format!( - "var {name}Pin = GCHandle.Alloc({name}Buf, GCHandleType.Pinned);" - )); - } - ArgPass::String { .. } => { - w.line(format!( - "var {name}Ptr = Marshal.StringToCoTaskMemUTF8({name});" - )); - } - ArgPass::Bytes { .. } => { - w.line(format!( - "var {name}Pin = GCHandle.Alloc({name}, GCHandleType.Pinned);" - )); - } - // The handle is owned by the producer from here on: its vtable - // `free(ctx)` trampoline releases it when the last clone drops. - ArgPass::Callback { .. } => { - w.line(format!( - "var {name}Ctx = GCHandle.ToIntPtr(GCHandle.Alloc({name}));" - )); - } - ArgPass::Direct { .. } | ArgPass::Object { .. } => {} - } -} - -/// Emit the cleanup statements releasing what [`render_marshal_setup`] -/// allocated: the `CoTaskMem` string copy or the pinned array handle. -pub(crate) fn render_marshal_cleanup(w: &mut CodeWriter, p: &ParamBinding) { - let name = safe_cs_name(&p.name); - match p.arg_pass() { - ArgPass::Buffer { .. } | ArgPass::Bytes { .. } => { - w.line(format!("{name}Pin.Free();")); - } - ArgPass::String { .. } => { - w.line(format!("Marshal.FreeCoTaskMem({name}Ptr);")); - } - ArgPass::Direct { .. } | ArgPass::Object { .. } | ArgPass::Callback { .. } => {} - } -} - -/// Emit every parameter's setup, then `body` inside a `try`/`finally` that -/// runs the cleanups when any parameter needs one, or `body` bare otherwise. -pub(crate) fn render_marshalled_call( - w: &mut CodeWriter, - params: &[ParamBinding], - body: impl FnOnce(&mut CodeWriter), -) { - for p in params { - render_marshal_setup(w, p); - } - if params.iter().any(param_needs_cleanup) { - w.line("try"); - w.line("{"); - w.scope(body); - w.line("}"); - w.line("finally"); - w.line("{"); - w.scope(|w| { - for p in params { - render_marshal_cleanup(w, p); - } - }); - w.line("}"); - } else { - body(w); - } -} - -/// The C# class hosting the static vtable a callback slot points at, named -/// from the slot's `{prefix}_{module}_{Name}_vtable` C type. -fn vtable_class_for_slot(vtable: &CType) -> String { - let CType::Ptr { pointee, .. } = vtable else { - unreachable!("a callback vtable slot is a pointer") - }; - let CType::VtableTag { module, name } = pointee.as_ref() else { - unreachable!("a callback vtable slot points at a vtable tag") - }; - vtable_class_cs(module, name) -} - -/// The joined native-call argument expressions for `params`, one expression -/// per ABI slot, dispatching on each parameter's [`ArgPass`] plan. -pub(crate) fn build_call_args(params: &[ParamBinding]) -> String { - params - .iter() - .flat_map(|p| { - let name = safe_cs_name(&p.name); - match p.arg_pass() { - // A buffered parameter passes its pinned value buffer as the - // borrowed (ptr, len) pair; the caller owns and frees the pin. - ArgPass::Buffer { .. } => vec![ - format!("{name}Pin.AddrOfPinnedObject()"), - format!("(UIntPtr){name}Buf.Length"), - ], - ArgPass::String { .. } => vec![format!("{name}Ptr")], - ArgPass::Bytes { .. } => vec![ - format!("{name}Pin.AddrOfPinnedObject()"), - format!("(UIntPtr){name}.Length"), - ], - // Interface parameters borrow: pass the handle, the wrapper - // keeps its own reference. A nullable object passes null as - // IntPtr.Zero. - ArgPass::Object { nullable: true, .. } => { - vec![format!("{name}?.Handle ?? IntPtr.Zero")] - } - ArgPass::Object { - nullable: false, .. - } => { - vec![format!("{name}.Handle")] - } - // The GCHandle key plus the one process-wide vtable. - ArgPass::Callback { vtable, .. } => vec![ - format!("{name}Ctx"), - format!("{}.Pointer", vtable_class_for_slot(&vtable.ty)), - ], - // Direct slots pass by value; only the C# spelling of the - // value expression depends on the surface type. - ArgPass::Direct { .. } => vec![direct_to_slot(&p.ty, &name)], - } - }) - .collect::>() - .join(", ") -} - -/// The C# expression converting a surface value `expr` of direct-family type -/// `ty` into its by-value ABI slot: `bool` as a byte, a typed enum as its -/// `int`, every other scalar as is. -pub(crate) fn direct_to_slot(ty: &Ty, expr: &str) -> String { - match ty { - Ty::Bool => format!("(byte)({expr} ? 1 : 0)"), - Ty::Enum(_) => format!("(int){expr}"), - _ => expr.to_string(), - } -} - -/// The joined native-call argument list: the implicit self handle (when -/// `self_expr` is given) followed by every lowered parameter slot. -pub(crate) fn full_call_args(f: &FnBinding, self_expr: Option<&str>) -> String { - let args = build_call_args(&f.params); - match self_expr { - Some(s) if args.is_empty() => s.to_string(), - Some(s) => format!("{s}, {args}"), - None => args, - } -} - -/// `args` followed by a separating comma, or empty when there are none, for -/// splicing ahead of the trailing out-params and error slot. -fn args_part(args: &str) -> String { - if args.is_empty() { - String::new() - } else { - format!("{args}, ") - } -} - -/// True when a value crosses back as an owned `ptr` + `len` pair delivered -/// through a trailing `out_len` slot: bytes and every buffered type (records, -/// rich enums, lists, maps, and non-interface optionals). -pub(crate) fn is_ptr_len_result(ty: &Ty) -> bool { - matches!(ty.family(), Family::Bytes | Family::Buffer) -} - -/// Emit the native call, the error check, and the return conversion for one -/// synchronous callable. -pub(crate) fn render_pinvoke_call_and_return( - w: &mut CodeWriter, - f: &FnBinding, - self_expr: Option<&str>, - err: ErrCtx, -) { - let c_sym = &f.c_base; - let args = args_part(&full_call_args(f, self_expr)); - - if let Some(ret) = &f.ret { - let out_len_part = if is_ptr_len_result(ret) { - "out var outLen, " - } else { - "" - }; - w.line(format!( - "var result = NativeMethods.{c_sym}({args}{out_len_part}ref err);" - )); - } else { - w.line(format!("NativeMethods.{c_sym}({args}ref err);")); - } - w.line(err.check_stmt()); - - if let Some(ret_ty) = &f.ret { - render_return_conversion(w, ret_ty); - } -} - -/// 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. Dispatches on the return type's -/// [`Family`], which is exactly the classification behind the shared -/// `RetPass` receiving plan: copy and free a string or bytes, decode and free -/// a buffer, adopt an object. -pub(crate) fn render_return_conversion(w: &mut CodeWriter, ty: &Ty) { - match ty.family() { - // A buffered return is a producer-allocated value buffer: copy the - // bytes, release them with `weaveffi_free_bytes`, then decode the - // copy (adopting any object tokens it carries). - Family::Buffer => { - 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);"); - // Named so it cannot shadow a user parameter (`value` is a common one). - emit_buffer_decode(w, ty, "decoded", "resultBuf"); - w.line("return decoded;"); - } - Family::String => { - w.line("var str = Marshal.PtrToStringUTF8(result);"); - w.line("NativeMethods.weaveffi_free_string(result);"); - w.line("return str ?? \"\";"); - } - Family::Bytes => { - 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);"); - w.line("NativeMethods.weaveffi_free_bytes(result, outLen);"); - w.line("return arr;"); - } - // One strong reference transfers: adopt it into a wrapper whose - // Dispose (or finalizer) releases it exactly once. - Family::Object { nullable } => { - let adopt = adopt_object(object_class(ty), "result"); - if nullable { - w.line(format!("return result == IntPtr.Zero ? null : {adopt};")); - } else { - w.line(format!("return {adopt};")); - } - } - Family::Direct => { - w.line(format!("return {};", direct_from_slot(ty, "result"))); - } - Family::Callback => unreachable!("callback interfaces are never returned"), - Family::Iterator => unreachable!("iterator functions render via CallShape::Iterator"), - } -} - -/// The C# expression adopting the strong reference in `ptr_expr` into a new -/// wrapper of class `cn`, through the `WeaveFFIHandle` marker the adopting -/// constructor takes. -pub(crate) fn adopt_object(cn: &str, ptr_expr: &str) -> String { - format!("new {cn}(new WeaveFFIHandle({ptr_expr}))") -} - -/// The wrapper class name for an object type (`Store` or `Store?`). -pub(crate) fn object_class(ty: &Ty) -> &str { - local_type_name( - ty.interface_name() - .expect("object family types name an interface"), - ) -} - -/// The C# expression converting a by-value slot named `slot` (holding a -/// direct-family value) into its surface type: `bool` from its byte, a typed -/// enum from its `int`, every other scalar as is. -pub(crate) fn direct_from_slot(ty: &Ty, slot: &str) -> String { - match ty { - Ty::Bool => format!("{slot} != 0"), - Ty::Enum(name) => format!("({}){slot}", local_type_name(name)), - _ => slot.to_string(), - } -} - -/// Render one wrapper method (any shape) named `method_name`. `self_expr` is -/// the receiver's handle expression for interface instance methods (`None` -/// for free functions, statics, and factories, which render as `static`); -/// `err` selects the typed or generic error surface. -pub(crate) fn render_wrapper_method( - out: &mut String, - f: &FnBinding, - method_name: &str, - self_expr: Option<&str>, - err: ErrCtx, -) { - if f.is_async { - render_async_wrapper_method(out, f, method_name, self_expr, err); - return; - } - if let CallShape::Iterator(it) = &f.shape { - render_iterator_wrapper_method(out, f, it, method_name, self_expr, err); - return; - } - let f = camel_fn(f); - let ret_cs = f.ret.as_ref().map(cs_type).unwrap_or_else(|| "void".into()); - let staticness = if self_expr.is_none() { "static " } else { "" }; - - let mut w = CodeWriter::four_space().with_depth(2); - writer_fn_doc(&mut w, &f.doc, &f.params); - err.write_exception_doc(&mut w); - write_obsolete(&mut w, &f.deprecated); - - w.line(format!( - "public {staticness}{ret_cs} {method_name}({})", - params_sig(&f.params) - )); - w.line("{"); - w.scope(|w| { - w.line("var err = new WeaveFFIError();"); - render_marshalled_call(w, &f.params, |w| { - render_pinvoke_call_and_return(w, &f, self_expr, err); - }); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// The statements converting one `_next` out-item into the yielded C# value, -/// receiving it per the element's [`Family`] exactly as a return of the same -/// type (the `IteratorProtocol.elem` plan): copy and free a string -/// (`weaveffi_free_string`) or bytes (`weaveffi_free_bytes`), decode and free -/// a buffer, adopt an object into a wrapper the consumer owns. Returns the -/// expression to `yield return`. -pub(crate) fn iterator_item_conversion(w: &mut CodeWriter, elem: &Ty) -> String { - match elem.family() { - Family::Buffer => { - 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);"); - emit_buffer_decode(w, elem, "item", "itemBuf"); - "item".into() - } - Family::String => { - w.line("var item = Marshal.PtrToStringUTF8(out_item) ?? \"\";"); - w.line("NativeMethods.weaveffi_free_string(out_item);"); - "item".into() - } - Family::Bytes => { - w.line("var item = new byte[(int)out_len];"); - w.line("if (out_item != IntPtr.Zero && (int)out_len > 0) Marshal.Copy(out_item, item, 0, (int)out_len);"); - w.line("NativeMethods.weaveffi_free_bytes(out_item, out_len);"); - "item".into() - } - Family::Object { nullable } => { - let adopt = adopt_object(object_class(elem), "out_item"); - if nullable { - format!("out_item == IntPtr.Zero ? null : {adopt}") - } else { - adopt - } - } - Family::Direct => direct_from_slot(elem, "out_item"), - Family::Callback | Family::Iterator => { - unreachable!("{elem} is not a valid iterator element") - } - } -} - -/// An `iter` function surfaces as `IEnumerable`, rendering the -/// `IteratorProtocol` pull contract: an eager launcher call (so launch errors -/// throw immediately, per the function's `ErrorStrategy`), then a lazy -/// `yield return` enumerator issuing exactly one C `next` call per -/// `MoveNext`. Each yielded element is received per its element plan after -/// conversion, and the compiler-generated `finally` destroys the native -/// iterator exactly once, whether enumeration runs to exhaustion or is -/// abandoned early (C# `foreach` disposes the enumerator). Wrapping the -/// single enumerator in `WeaveFFIOnceEnumerable` makes a second enumeration -/// throw instead of double-destroying the consumed handle. -pub(crate) fn render_iterator_wrapper_method( - out: &mut String, - f: &FnBinding, - it: &IteratorBinding, - method_name: &str, - self_expr: Option<&str>, - err: ErrCtx, -) { - let f = camel_fn(f); - let elem_cs = cs_type(&it.elem); - let staticness = if self_expr.is_none() { "static " } else { "" }; - - let launch_call = format!( - "var iter = NativeMethods.{}({}ref err);", - it.launch.symbol, - args_part(&full_call_args(&f, self_expr)) - ); - - let mut w = CodeWriter::four_space().with_depth(2); - writer_fn_doc(&mut w, &f.doc, &f.params); - w.line("/// Streams lazily: each element is pulled from the native"); - w.line("/// iterator on demand, and the iterator is destroyed when enumeration"); - w.line("/// completes or the enumerator is disposed (a foreach disposes it"); - w.line("/// automatically, including on early exit). The returned sequence can be"); - w.line("/// enumerated only once."); - err.write_exception_doc(&mut w); - write_obsolete(&mut w, &f.deprecated); - - let wrap_return = - format!("return new WeaveFFIOnceEnumerable<{elem_cs}>(Enumerate{method_name}(iter));"); - w.line(format!( - "public {staticness}IEnumerable<{elem_cs}> {method_name}({})", - params_sig(&f.params) - )); - w.line("{"); - w.scope(|w| { - w.line("var err = new WeaveFFIError();"); - render_marshalled_call(w, &f.params, |w| { - w.line(launch_call); - w.line(err.check_stmt()); - w.line(wrap_return); - }); - }); - w.line("}"); - w.blank(); - - // The `_next` out-slots after the iterator handle, excluding the error. - let next_out_args: Vec = it - .next - .params - .iter() - .skip(1) - .filter(|slot| !is_error_slot(slot)) - .map(|slot| format!("out var {}", slot.name)) - .collect(); - - // A `yield return` iterator method: the compiler emits the `finally` - // into Dispose(), so the destroy below runs exactly once, on exhaustion - // or when the consumer abandons enumeration early. - w.line(format!( - "private static IEnumerator<{elem_cs}> Enumerate{method_name}(IntPtr iter)" - )); - w.line("{"); - w.scope(|w| { - w.line("try"); - w.line("{"); - w.scope(|w| { - w.line("while (true)"); - w.line("{"); - w.scope(|w| { - w.line("var iterErr = new WeaveFFIError();"); - w.line(format!( - "if (NativeMethods.{}(iter, {}, ref iterErr) == 0)", - it.next.symbol, - next_out_args.join(", ") - )); - w.line("{"); - w.scope(|w| { - w.line(err.check_stmt_for("iterErr")); - w.line("yield break;"); - }); - w.line("}"); - w.line(err.check_stmt_for("iterErr")); - let item_expr = iterator_item_conversion(w, &it.elem); - w.line(format!("yield return {item_expr};")); - }); - w.line("}"); - }); - w.line("}"); - w.line("finally"); - w.line("{"); - w.scope(|w| { - w.line(format!("NativeMethods.{}(iter);", it.destroy_symbol)); - }); - w.line("}"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// 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: &Ty) -> bool { - is_ptr_len_result(ty) -} - -/// 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 { - match ret { - None => "(context, err)", - Some(ty) if async_result_is_ptr_len(ty) => "(context, err, result, resultLen)", - Some(_) => "(context, err, result)", - } -} - -/// Render an async wrapper returning `Task`/`Task` via a -/// `TaskCompletionSource` resolved from the native completion callback. A -/// non-zero error slot faults the task with the typed or generic exception -/// according to `err`. -pub(crate) fn render_async_wrapper_method( - out: &mut String, - f: &FnBinding, - method_name: &str, - self_expr: Option<&str>, - err: ErrCtx, -) { - let f = camel_fn(f); - let c_sym = &f.c_base; - let delegate_name = format!("NativeMethods.AsyncCb_{c_sym}"); - let staticness = if self_expr.is_none() { "static " } else { "" }; - - let task_ret = f - .ret - .as_ref() - .map(|ty| format!("Task<{}>", cs_type(ty))) - .unwrap_or_else(|| "Task".into()); - - let tcs_type = f.ret.as_ref().map(cs_type).unwrap_or_else(|| "bool".into()); - - let mut w = CodeWriter::four_space().with_depth(2); - writer_fn_doc(&mut w, &f.doc, &f.params); - err.write_exception_doc(&mut w); - write_obsolete(&mut w, &f.deprecated); - - w.line(format!( - "public {staticness}async {task_ret} {method_name}({})", - params_sig(&f.params) - )); - w.line("{"); - w.scope(|w| { - w.line(format!( - "var tcs = new TaskCompletionSource<{tcs_type}>(TaskCreationOptions.RunContinuationsAsynchronously);" - )); - - let cb_lambda_params = async_cb_lambda_params(&f.ret); - w.line(format!("{delegate_name} callback = {cb_lambda_params} =>")); - w.line("{"); - w.scope(|w| { - w.line("try"); - w.line("{"); - w.scope(|w| { - w.line("if (err != IntPtr.Zero)"); - w.line("{"); - w.scope(|w| { - w.line("var wErr = Marshal.PtrToStructure(err);"); - w.line("if (wErr.Code != 0)"); - w.line("{"); - w.scope(|w| { - // The boxed error is owned by the consumer: copy the - // message and payload, then release it with - // `weaveffi_error_free`. - w.line("var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? \"\";"); - if matches!(err, ErrCtx::Domain(_)) { - w.line("var payload = WeaveFFIError.CopyPayload(wErr);"); - } - w.line("NativeMethods.weaveffi_error_free(err);"); - w.line(format!("tcs.SetException({});", err.async_exception_expr())); - w.line("return;"); - }); - w.line("}"); - }); - w.line("}"); - render_async_set_result(w, &f.ret); - }); - w.line("}"); - w.line("finally"); - w.line("{"); - w.scope(|w| { - w.line("if (context != IntPtr.Zero)"); - w.line("{"); - w.scope(|w| { - w.line("GCHandle.FromIntPtr(context).Free();"); - }); - w.line("}"); - }); - w.line("}"); - }); - w.line("};"); - w.line("var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal);"); - w.line("var ctx = GCHandle.ToIntPtr(gcHandle);"); - - let args = args_part(&full_call_args(&f, self_expr)); - let cancel_arg = if f.cancellable { "IntPtr.Zero, " } else { "" }; - let launch = format!("NativeMethods.{c_sym}_async({args}{cancel_arg}callback, ctx);"); - - render_marshalled_call(w, &f.params, |w| { - w.line("try"); - w.line("{"); - w.scope(|w| { - w.line(launch); - }); - w.line("}"); - w.line("catch"); - w.line("{"); - w.scope(|w| { - w.line("if (gcHandle.IsAllocated) gcHandle.Free();"); - w.line("throw;"); - }); - w.line("}"); - }); - - if f.ret.is_some() { - w.line("return await tcs.Task;"); - } else { - w.line("await tcs.Task;"); - } - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the statements resolving the `TaskCompletionSource` from the -/// completion callback's result slots, honoring the `AsyncProtocol` owned -/// results clause: string, bytes, and buffered result buffers are owned by -/// the consumer, so they are deep-copied (and buffered results decoded) -/// here and then released through the runtime free symbols. An object -/// result transfers one strong reference instead: the wrapper adopts the -/// pointer. -pub(crate) fn render_async_set_result(w: &mut CodeWriter, ret: &Option) { - let Some(ty) = ret else { - w.line("tcs.SetResult(true);"); - return; - }; - match ty.family() { - Family::Buffer => { - w.line("var resultBuf = new byte[(int)resultLen];"); - w.line( - "if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen);", - ); - w.line( - "if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen);", - ); - emit_buffer_decode(w, ty, "decoded", "resultBuf"); - w.line("tcs.SetResult(decoded);"); - } - Family::String => { - w.line("var str = Marshal.PtrToStringUTF8(result) ?? \"\";"); - w.line("if (result != IntPtr.Zero) NativeMethods.weaveffi_free_string(result);"); - w.line("tcs.SetResult(str);"); - } - Family::Bytes => { - w.line("var arr = new byte[(int)resultLen];"); - w.line( - "if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, arr, 0, (int)resultLen);", - ); - w.line( - "if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen);", - ); - w.line("tcs.SetResult(arr);"); - } - Family::Object { nullable } => { - let adopt = adopt_object(object_class(ty), "result"); - if nullable { - w.line(format!( - "tcs.SetResult(result == IntPtr.Zero ? null : {adopt});" - )); - } else { - w.line(format!("tcs.SetResult({adopt});")); - } - } - Family::Direct => { - w.line(format!( - "tcs.SetResult({});", - direct_from_slot(ty, "result") - )); - } - Family::Callback => unreachable!("callback interfaces are never returned"), - Family::Iterator => unreachable!("iterator functions render via CallShape::Iterator"), - } -} - -/// Renders one module's static wrapper class. Submodules become sibling -/// classes named by their full path (`KvStats`, not a nested `Kv.Stats`): -/// flat classes keep generated type names (`Stats`) unambiguous, since a -/// nested module class with the same name as a struct wrapper would shadow it. -pub(crate) fn render_wrapper_class( - out: &mut String, - mb: &ModuleBinding, - strip_module_prefix: bool, -) { - let class_name: String = mb - .segments - .iter() - .map(|s| s.to_upper_camel_case()) - .collect(); - out.push_str(&format!(" public static class {class_name}\n {{\n")); - - for f in &mb.functions { - let method_name = - wrapper_name(&mb.path, &f.name, strip_module_prefix).to_upper_camel_case(); - let err = ErrCtx::for_fn(f, mb.error.as_ref()); - render_wrapper_method(out, f, &method_name, None, err); - } - - out.push_str(" }\n\n"); -} diff --git a/crates/weaveffi-gen-dotnet/src/codec.rs b/crates/weaveffi-gen-dotnet/src/codec.rs deleted file mode 100644 index a46e1e6b..00000000 --- a/crates/weaveffi-gen-dotnet/src/codec.rs +++ /dev/null @@ -1,242 +0,0 @@ -//! Value-buffer codec emitters: the statements encoding a C# value into a -//! `WeaveFFIBufferWriter` and decoding one back from a -//! `WeaveFFIBufferReader`. -//! -//! Every dispatch here goes through [`Ty::wire`], so this module never -//! re-derives the wire shapes (records and rich enums as one user-codec -//! shape, interfaces as `u64` object tokens). An object token carries one -//! strong reference: writing one clones the wrapper's handle through the -//! interface's `_clone` symbol, and reading one adopts the pointer into a -//! new wrapper whose `Dispose` (or finalizer) releases it. - -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{Prim, WireType}; -use weaveffi_core::utils::local_type_name; - -use crate::calls::adopt_object; -use crate::types::{cs_type, is_cs_value_type}; - -/// Emit statements serializing `expr` (a C# expression of the C# type mapped -/// from `ty`) into the buffer writer named `writer_var`, following the wire -/// format in `docs/src/reference/value-buffers.md`. Nesting recurses; -/// `depth` uniquifies loop locals so nested lists and maps never collide. -pub(crate) fn emit_buffer_write( - w: &mut CodeWriter, - ty: &Ty, - expr: &str, - writer_var: &str, - depth: usize, -) { - match ty.wire() { - WireType::Prim(Prim::I8) => { - w.line(format!("{writer_var}.WriteI8({expr});")); - } - WireType::Prim(Prim::I16) => { - w.line(format!("{writer_var}.WriteI16({expr});")); - } - WireType::Prim(Prim::I32) => { - w.line(format!("{writer_var}.WriteI32({expr});")); - } - WireType::Prim(Prim::U8) => { - w.line(format!("{writer_var}.WriteU8({expr});")); - } - WireType::Prim(Prim::U16) => { - w.line(format!("{writer_var}.WriteU16({expr});")); - } - WireType::Prim(Prim::U32) => { - w.line(format!("{writer_var}.WriteU32({expr});")); - } - WireType::Prim(Prim::I64) => { - w.line(format!("{writer_var}.WriteI64({expr});")); - } - WireType::Prim(Prim::U64) => { - w.line(format!("{writer_var}.WriteU64({expr});")); - } - WireType::Prim(Prim::F32) => { - w.line(format!("{writer_var}.WriteF32({expr});")); - } - WireType::Prim(Prim::F64) => { - w.line(format!("{writer_var}.WriteF64({expr});")); - } - WireType::Prim(Prim::Bool) => { - w.line(format!("{writer_var}.WriteBool({expr});")); - } - // The token must carry its own strong reference, so clone the - // wrapper's handle rather than writing the pointer it still owns. - WireType::Object(_) => { - w.line(format!("{writer_var}.WriteObject({expr}.CloneHandle());")); - } - WireType::Enum(_) => { - w.line(format!("{writer_var}.WriteI32((int){expr});")); - } - WireType::Prim(Prim::String) => { - w.line(format!("{writer_var}.WriteString({expr});")); - } - WireType::Prim(Prim::Bytes) => { - w.line(format!("{writer_var}.WriteBytes({expr});")); - } - WireType::User(_) => { - w.line(format!("{expr}.WriteTo({writer_var});")); - } - WireType::Optional(inner) => { - let value_expr = if is_cs_value_type(inner) { - format!("{expr}.Value") - } else { - format!("{expr}!") - }; - w.line(format!("if ({expr} != null)")); - w.block("{", "}", |w| { - w.line(format!("{writer_var}.WriteOptionFlag(true);")); - emit_buffer_write(w, inner, &value_expr, writer_var, depth); - }); - w.line("else"); - w.block("{", "}", |w| { - w.line(format!("{writer_var}.WriteOptionFlag(false);")); - }); - } - WireType::List(inner) => { - let item = format!("item{depth}"); - w.line(format!("{writer_var}.WriteLen({expr}.Length);")); - w.line(format!("foreach (var {item} in {expr})")); - w.block("{", "}", |w| { - emit_buffer_write(w, inner, &item, writer_var, depth + 1); - }); - } - WireType::Map(k, v) => { - let entry = format!("entry{depth}"); - w.line(format!("{writer_var}.WriteLen({expr}.Count);")); - w.line(format!("foreach (var {entry} in {expr})")); - w.block("{", "}", |w| { - emit_buffer_write(w, k, &format!("{entry}.Key"), writer_var, depth + 1); - emit_buffer_write(w, v, &format!("{entry}.Value"), writer_var, depth + 1); - }); - } - } -} - -/// Emit statements declaring a local named `var` and decoding a value of `ty` -/// into it from the buffer reader named `reader_var`, the inverse of -/// [`emit_buffer_write`]. `depth` uniquifies loop counters across nesting. -pub(crate) fn emit_buffer_read( - w: &mut CodeWriter, - ty: &Ty, - var: &str, - reader_var: &str, - depth: usize, -) { - match ty.wire() { - WireType::Prim(Prim::I8) => { - w.line(format!("var {var} = {reader_var}.ReadI8();")); - } - WireType::Prim(Prim::I16) => { - w.line(format!("var {var} = {reader_var}.ReadI16();")); - } - WireType::Prim(Prim::I32) => { - w.line(format!("var {var} = {reader_var}.ReadI32();")); - } - WireType::Prim(Prim::U8) => { - w.line(format!("var {var} = {reader_var}.ReadU8();")); - } - WireType::Prim(Prim::U16) => { - w.line(format!("var {var} = {reader_var}.ReadU16();")); - } - WireType::Prim(Prim::U32) => { - w.line(format!("var {var} = {reader_var}.ReadU32();")); - } - WireType::Prim(Prim::I64) => { - w.line(format!("var {var} = {reader_var}.ReadI64();")); - } - WireType::Prim(Prim::U64) => { - w.line(format!("var {var} = {reader_var}.ReadU64();")); - } - WireType::Prim(Prim::F32) => { - w.line(format!("var {var} = {reader_var}.ReadF32();")); - } - WireType::Prim(Prim::F64) => { - w.line(format!("var {var} = {reader_var}.ReadF64();")); - } - WireType::Prim(Prim::Bool) => { - w.line(format!("var {var} = {reader_var}.ReadBool();")); - } - // Adopt the token's strong reference into a fresh wrapper; its - // Dispose (or finalizer) owes the interface's `_destroy`. - WireType::Object(name) => { - let adopt = adopt_object(local_type_name(name), &format!("{reader_var}.ReadObject()")); - w.line(format!("var {var} = {adopt};")); - } - WireType::Enum(name) => { - let cn = local_type_name(name); - w.line(format!("var {var} = ({cn}){reader_var}.ReadI32();")); - } - WireType::Prim(Prim::String) => { - w.line(format!("var {var} = {reader_var}.ReadString();")); - } - WireType::Prim(Prim::Bytes) => { - w.line(format!("var {var} = {reader_var}.ReadBytes();")); - } - WireType::User(name) => { - let cn = local_type_name(name); - w.line(format!("var {var} = {cn}.ReadFrom({reader_var});")); - } - WireType::Optional(inner) => { - let cs = cs_type(ty); - w.line(format!("{cs} {var} = null;")); - w.line(format!("if ({reader_var}.ReadOptionFlag())")); - w.block("{", "}", |w| { - emit_buffer_read(w, inner, &format!("{var}Value"), reader_var, depth); - w.line(format!("{var} = {var}Value;")); - }); - } - WireType::List(inner) => { - let i = format!("i{depth}"); - w.line(format!("var {var}Count = {reader_var}.ReadLen();")); - 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); - w.line(format!("{var}[{i}] = {var}Item;")); - }); - } - WireType::Map(k, v) => { - let i = format!("i{depth}"); - let k_cs = cs_type(k); - let v_cs = cs_type(v); - w.line(format!("var {var}Count = {reader_var}.ReadLen();")); - w.line(format!( - "var {var} = new Dictionary<{k_cs}, {v_cs}>({var}Count);" - )); - w.line(format!("for (int {i} = 0; {i} < {var}Count; {i}++)")); - w.block("{", "}", |w| { - emit_buffer_read(w, k, &format!("{var}Key"), reader_var, depth + 1); - emit_buffer_read(w, v, &format!("{var}Val"), reader_var, depth + 1); - w.line(format!("{var}[{var}Key] = {var}Val;")); - }); - } - } -} - -/// `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}") -} - -/// 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: &Ty, var: &str, buf: &str) { - w.line(format!( - "var {var}Reader = new WeaveFFIBufferReader({buf});" - )); - emit_buffer_read(w, ty, var, &format!("{var}Reader"), 0); - w.line(format!("{var}Reader.ExpectEnd();")); -} diff --git a/crates/weaveffi-gen-dotnet/src/docs.rs b/crates/weaveffi-gen-dotnet/src/docs.rs deleted file mode 100644 index 04c21bd6..00000000 --- a/crates/weaveffi-gen-dotnet/src/docs.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! XML doc-comment emission: `` blocks for items and the -//! `` plus `` set for callables. - -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::ParamBinding; - -use crate::types::safe_cs_name; - -/// Emits a C# XML doc comment at `indent`. Single-line docs collapse to -/// `/// text`; multi-line docs expand to a `` -/// block with each input line wrapped in its own line. -pub(crate) fn emit_doc(out: &mut String, doc: &Option, indent: &str) { - let Some(doc) = doc else { - return; - }; - let doc = doc.trim(); - if doc.is_empty() { - return; - } - if doc.contains('\n') { - out.push_str(indent); - out.push_str("/// \n"); - for line in doc.lines() { - out.push_str(indent); - out.push_str("/// "); - out.push_str(line); - out.push('\n'); - } - out.push_str(indent); - out.push_str("/// \n"); - } else { - out.push_str(indent); - out.push_str("/// "); - out.push_str(doc); - out.push_str("\n"); - } -} - -/// Emits a full XML doc block: function `` plus a `` element -/// per documented parameter. Skips entirely when there is nothing to emit. -pub(crate) fn emit_fn_doc( - out: &mut String, - doc: &Option, - params: &[ParamBinding], - indent: &str, -) { - let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty()); - let documented_params: Vec<&ParamBinding> = params - .iter() - .filter(|p| { - p.doc - .as_ref() - .map(|d| !d.trim().is_empty()) - .unwrap_or(false) - }) - .collect(); - if trimmed_doc.is_none() && documented_params.is_empty() { - return; - } - if let Some(d) = trimmed_doc { - if d.contains('\n') { - out.push_str(indent); - out.push_str("/// \n"); - for line in d.lines() { - out.push_str(indent); - out.push_str("/// "); - out.push_str(line); - out.push('\n'); - } - out.push_str(indent); - out.push_str("/// \n"); - } else { - out.push_str(indent); - out.push_str("/// "); - out.push_str(d); - out.push_str("\n"); - } - } - for p in documented_params { - let pdoc = p.doc.as_ref().unwrap().trim(); - let name = safe_cs_name(&p.name); - if pdoc.contains('\n') { - out.push_str(indent); - out.push_str(&format!("/// \n", name)); - for line in pdoc.lines() { - out.push_str(indent); - out.push_str("/// "); - out.push_str(line); - out.push('\n'); - } - out.push_str(indent); - out.push_str("/// \n"); - } else { - out.push_str(indent); - out.push_str(&format!("/// {}\n", name, pdoc)); - } - } -} - -/// Emit [`emit_doc`] at the writer's current depth by rendering into a scratch -/// buffer and splicing it verbatim, so a [`CodeWriter`]-based renderer can -/// interleave XML doc comments without re-implementing their formatting. -pub(crate) fn writer_doc(w: &mut CodeWriter, doc: &Option) { - let mut tmp = String::new(); - emit_doc(&mut tmp, doc, &w.indent_str()); - w.raw(tmp); -} - -/// Emit [`emit_fn_doc`] at the writer's current depth, splicing the rendered -/// ``/`` block in verbatim. The [`CodeWriter`] companion to -/// [`emit_fn_doc`] used by the method renderers. -pub(crate) fn writer_fn_doc(w: &mut CodeWriter, doc: &Option, params: &[ParamBinding]) { - let mut tmp = String::new(); - emit_fn_doc(&mut tmp, doc, params, &w.indent_str()); - w.raw(tmp); -} diff --git a/crates/weaveffi-gen-dotnet/src/entities.rs b/crates/weaveffi-gen-dotnet/src/entities.rs deleted file mode 100644 index 5957ec55..00000000 --- a/crates/weaveffi-gen-dotnet/src/entities.rs +++ /dev/null @@ -1,420 +0,0 @@ -//! Entity renderers: C-style enums, rich (algebraic) enums, records, and -//! the reference-counted interface wrapper classes. - -use heck::{ToLowerCamelCase, ToUpperCamelCase}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::{ - CallShape, EnumBinding, EnumVariantBinding, ErrorBinding, FieldBinding, FnBinding, - InterfaceBinding, StructBinding, -}; - -use crate::calls::{ - build_call_args, params_sig, render_marshalled_call, render_wrapper_method, write_obsolete, - ErrCtx, -}; -use crate::codec::{emit_buffer_read, emit_buffer_write}; -use crate::docs::{writer_doc, writer_fn_doc}; -use crate::types::{camel_fn, cs_type, safe_cs_name}; - -/// Render a plain C-style enum as a C# `enum` with its ABI discriminants. -pub(crate) fn render_enum(out: &mut String, e: &EnumBinding) { - // A rich (algebraic) enum is not a plain C# `enum`; it surfaces as an - // opaque-object class via `render_rich_enum_class`. Guard here so this - // path only ever emits C-style enums. - if e.is_rich() { - return; - } - let mut w = CodeWriter::four_space().with_depth(1); - writer_doc(&mut w, &e.doc); - w.line(format!("public enum {}", e.name)); - w.block("{", "}", |w| { - for v in &e.variants { - writer_doc(w, &v.doc); - w.line(format!("{} = {},", v.name, v.value)); - } - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the get-only properties and the positional constructor shared by -/// record classes and rich-enum variant classes: one PascalCase property per -/// field plus a public constructor taking every field in declaration order. -pub(crate) fn render_value_members(w: &mut CodeWriter, class_name: &str, fields: &[FieldBinding]) { - for field in fields { - writer_doc(w, &field.doc); - w.line(format!( - "public {} {} {{ get; }}", - cs_type(&field.ty), - field.name.to_upper_camel_case() - )); - w.blank(); - } - let params_sig: Vec = fields - .iter() - .map(|f| { - format!( - "{} {}", - cs_type(&f.ty), - safe_cs_name(&f.name.to_lower_camel_case()) - ) - }) - .collect(); - w.line(format!("public {class_name}({})", params_sig.join(", "))); - w.block("{", "}", |w| { - for f in fields { - w.line(format!( - "{} = {};", - f.name.to_upper_camel_case(), - safe_cs_name(&f.name.to_lower_camel_case()) - )); - } - }); -} - -/// The `new {Class}(fField1, fField2, ...)` argument list matching the locals -/// [`emit_buffer_read`] declares for each field in `ReadFrom`. -pub(crate) fn read_ctor_args(fields: &[FieldBinding]) -> String { - fields - .iter() - .map(|f| format!("f{}", f.name.to_upper_camel_case())) - .collect::>() - .join(", ") -} - -/// Render a record as a plain sealed data class: typed get-only properties, a -/// positional constructor, and the internal `WriteTo`/`ReadFrom` pair -/// implementing the record's value-buffer encoding (fields in declaration -/// order). Records own no native resources, so there is no handle, `Dispose`, -/// builder, or getter symbol. -pub(crate) fn render_struct_class(out: &mut String, s: &StructBinding) { - let mut w = CodeWriter::four_space().with_depth(1); - writer_doc(&mut w, &s.doc); - w.line(format!("public sealed class {}", s.name)); - w.line("{"); - w.indent(); - render_value_members(&mut w, &s.name, &s.fields); - w.blank(); - w.line("internal void WriteTo(WeaveFFIBufferWriter writer)"); - w.block("{", "}", |w| { - for f in &s.fields { - emit_buffer_write(w, &f.ty, &f.name.to_upper_camel_case(), "writer", 0); - } - }); - w.blank(); - w.line(format!( - "internal static {} ReadFrom(WeaveFFIBufferReader reader)", - s.name - )); - w.block("{", "}", |w| { - for f in &s.fields { - emit_buffer_read( - w, - &f.ty, - &format!("f{}", f.name.to_upper_camel_case()), - "reader", - 0, - ); - } - w.line(format!( - "return new {}({});", - s.name, - read_ctor_args(&s.fields) - )); - }); - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Render a rich (algebraic) enum as an idiomatic sum type: an abstract base -/// class with a private constructor and one nested sealed class per variant -/// (`Shape.Circle`), each carrying its fields as typed properties. The base -/// class hosts the internal `WriteTo`/`ReadFrom` pair implementing the -/// enum's value-buffer encoding: an `i32` tag followed by the active -/// variant's fields in declaration order. Rich enums own no native -/// resources and declare no C symbols. -pub(crate) fn render_rich_enum_class(out: &mut String, e: &EnumBinding) { - let name = &e.name; - let mut w = CodeWriter::four_space().with_depth(1); - writer_doc(&mut w, &e.doc); - w.line(format!("public abstract class {name}")); - w.line("{"); - w.indent(); - // The private constructor closes the hierarchy: only the nested variant - // classes can derive from the base. - w.line(format!("private {name}()")); - w.line("{"); - w.line("}"); - w.blank(); - - for v in &e.variants { - let mut vw = CodeWriter::four_space().with_depth(2); - render_rich_variant_class(&mut vw, name, v); - w.raw(vw.finish()); - } - - w.line("internal void WriteTo(WeaveFFIBufferWriter writer)"); - w.block("{", "}", |w| { - w.line("switch (this)"); - w.block("{", "}", |w| { - for v in &e.variants { - if v.fields.is_empty() { - w.line(format!("case {} _:", v.name)); - w.indent(); - w.line(format!("writer.WriteI32({});", v.value)); - w.line("break;"); - w.dedent(); - } else { - w.line(format!("case {} v:", v.name)); - w.indent(); - w.line(format!("writer.WriteI32({});", v.value)); - for f in &v.fields { - emit_buffer_write( - w, - &f.ty, - &format!("v.{}", f.name.to_upper_camel_case()), - "writer", - 0, - ); - } - w.line("break;"); - w.dedent(); - } - } - w.line("default:"); - w.indent(); - w.line(format!( - "throw new InvalidOperationException(\"unknown {name} variant\");" - )); - w.dedent(); - }); - }); - w.blank(); - w.line(format!( - "internal static {name} ReadFrom(WeaveFFIBufferReader reader)" - )); - w.block("{", "}", |w| { - w.line("var tag = reader.ReadI32();"); - w.line("switch (tag)"); - w.block("{", "}", |w| { - for v in &e.variants { - w.line(format!("case {}:", v.value)); - if v.fields.is_empty() { - w.indent(); - w.line(format!("return new {}();", v.name)); - w.dedent(); - } else { - w.block("{", "}", |w| { - for f in &v.fields { - emit_buffer_read( - w, - &f.ty, - &format!("f{}", f.name.to_upper_camel_case()), - "reader", - 0, - ); - } - w.line(format!( - "return new {}({});", - v.name, - read_ctor_args(&v.fields) - )); - }); - } - } - w.line("default:"); - w.indent(); - w.line(format!( - "throw new InvalidOperationException(\"malformed WeaveFFI value buffer: unknown {name} tag \" + tag);" - )); - w.dedent(); - }); - }); - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// One nested sealed variant class of a rich enum: typed get-only properties -/// and a positional constructor, exactly like a record. A unit variant has an -/// empty body with the compiler-provided constructor. -pub(crate) fn render_rich_variant_class( - w: &mut CodeWriter, - enum_name: &str, - v: &EnumVariantBinding, -) { - writer_doc(w, &v.doc); - w.line(format!("public sealed class {} : {enum_name}", v.name)); - if v.fields.is_empty() { - w.line("{"); - w.line("}"); - } else { - w.line("{"); - w.indent(); - render_value_members(w, &v.name, &v.fields); - w.dedent(); - w.line("}"); - } - w.blank(); -} - -/// Render one interface as a reference-counted object wrapper: a private -/// `IntPtr` holding one strong reference, `IDisposable` plus a finalizer -/// backstop that release it through the interface's `destroy` symbol exactly -/// once, and an internal `CloneHandle` that mints a second strong reference -/// through the `clone` symbol (what the value-buffer codec writes as an -/// object token). The `new` constructor maps to a real C# constructor, other -/// constructors become static factories, instance methods pass the handle as -/// the leading native argument, and statics are plain static methods. All -/// member shapes reuse the free-function marshalling paths. -pub(crate) fn render_interface_class( - out: &mut String, - i: &InterfaceBinding, - error: Option<&ErrorBinding>, -) { - let name = &i.name; - let mut w = CodeWriter::four_space().with_depth(1); - writer_doc(&mut w, &i.doc); - w.line(format!("public class {name} : IDisposable")); - w.line("{"); - w.indent(); - w.line("private IntPtr _handle;"); - w.line("private int _released;"); - w.blank(); - w.line("/// Adopts one strong reference to a native object."); - w.line(format!("internal {name}(WeaveFFIHandle handle)")); - w.block("{", "}", |w| { - w.line("_handle = handle.Value;"); - }); - w.blank(); - w.line("/// The borrowed native pointer for the duration of a call."); - w.line("/// The wrapper was disposed."); - w.line("internal IntPtr Handle"); - w.block("{", "}", |w| { - w.line("get"); - w.block("{", "}", |w| { - w.line("var h = _handle;"); - w.line("if (h == IntPtr.Zero)"); - w.block("{", "}", |w| { - w.line(format!( - "throw new ObjectDisposedException(nameof({name}));" - )); - }); - w.line("return h;"); - }); - }); - w.blank(); - w.line("/// Mints a second strong reference the caller owns (for"); - w.line("/// example to write this object into a value buffer)."); - w.line("internal IntPtr CloneHandle()"); - w.block("{", "}", |w| { - w.line(format!("return NativeMethods.{}(Handle);", i.clone_symbol)); - }); - w.blank(); - - for c in &i.constructors { - let err = ErrCtx::for_fn(c, error); - let mut tmp = String::new(); - if c.name == "new" && matches!(c.shape, CallShape::Sync(_)) { - render_interface_ctor(&mut tmp, i, c, err); - } else { - render_wrapper_method(&mut tmp, c, &c.name.to_upper_camel_case(), None, err); - } - w.raw(tmp); - } - // Methods borrow the receiver through the checked `Handle` property, so a - // call on a disposed wrapper throws ObjectDisposedException instead of - // handing the producer a null self pointer. - for m in &i.methods { - let err = ErrCtx::for_fn(m, error); - let mut tmp = String::new(); - render_wrapper_method( - &mut tmp, - m, - &m.name.to_upper_camel_case(), - Some("Handle"), - err, - ); - w.raw(tmp); - } - for s in &i.statics { - let err = ErrCtx::for_fn(s, error); - let mut tmp = String::new(); - render_wrapper_method(&mut tmp, s, &s.name.to_upper_camel_case(), None, err); - w.raw(tmp); - } - - w.line("/// Releases this wrapper's reference. The native object is"); - w.line("/// dropped when the producer releases its last reference; other"); - w.line("/// wrappers to the same object stay valid."); - w.line("public void Dispose()"); - w.block("{", "}", |w| { - w.line("Release();"); - w.line("GC.SuppressFinalize(this);"); - }); - w.blank(); - w.line(format!("~{name}()")); - w.block("{", "}", |w| { - w.line("Release();"); - }); - w.blank(); - // Interlocked makes the release idempotent even when Dispose races the - // finalizer or a concurrent Dispose; `destroy` runs at most once. - w.line("private void Release()"); - w.block("{", "}", |w| { - w.line("if (System.Threading.Interlocked.Exchange(ref _released, 1) != 0)"); - w.block("{", "}", |w| { - w.line("return;"); - }); - w.line("var h = _handle;"); - w.line("_handle = IntPtr.Zero;"); - w.line("if (h != IntPtr.Zero)"); - w.block("{", "}", |w| { - w.line(format!("NativeMethods.{}(h);", i.destroy_symbol)); - }); - }); - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Render the `new` constructor as a real C# constructor: the sync call path -/// with the checked result assigned to `_handle` instead of returned. -pub(crate) fn render_interface_ctor( - out: &mut String, - i: &InterfaceBinding, - f: &FnBinding, - err: ErrCtx, -) { - let f = camel_fn(f); - let c_sym = &f.c_base; - - let mut w = CodeWriter::four_space().with_depth(2); - writer_fn_doc(&mut w, &f.doc, &f.params); - err.write_exception_doc(&mut w); - write_obsolete(&mut w, &f.deprecated); - w.line(format!("public {}({})", i.name, params_sig(&f.params))); - w.line("{"); - w.scope(|w| { - w.line("var err = new WeaveFFIError();"); - let call_args = build_call_args(&f.params); - let args_part = if call_args.is_empty() { - String::new() - } else { - format!("{call_args}, ") - }; - let call = format!("var result = NativeMethods.{c_sym}({args_part}ref err);"); - render_marshalled_call(w, &f.params, |w| { - w.line(call); - w.line(err.check_stmt()); - w.line("_handle = result;"); - }); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-dotnet/src/lib.rs b/crates/weaveffi-gen-dotnet/src/lib.rs deleted file mode 100644 index ee769149..00000000 --- a/crates/weaveffi-gen-dotnet/src/lib.rs +++ /dev/null @@ -1,333 +0,0 @@ -//! .NET (P/Invoke) binding generator for WeaveFFI. -//! -//! Emits a C# project (`.csproj` + `.nuspec`) with P/Invoke declarations -//! and idiomatic wrappers over the C ABI, revision 2. Implements -//! [`LanguageBackend`]; the shared driver bridges it into the generator -//! pipeline. -//! -//! * Records, rich enums, optionals, lists, and maps are value types that -//! cross the C ABI serialized in the WeaveFFI value-buffer format (one -//! `const uint8_t*` + `size_t` pair). The generated file carries a small -//! internal writer/reader implementing the wire format, plus one -//! `WriteTo`/`ReadFrom` pair per record and rich enum. -//! * Interfaces are reference-counted objects wrapped in `IDisposable` -//! classes with a finalizer backstop; a returned, async, iterated, or -//! buffered object is adopted into a fresh wrapper, and a top-level object -//! parameter is borrowed for the call. Objects inside value buffers are -//! `u64` tokens minted through the interface's `_clone` symbol. -//! * Callback interfaces surface as C# `interface`s the consumer implements; -//! passing one registers it in a `GCHandle` and hands the producer a -//! pointer to one process-wide static vtable of `[UnmanagedCallersOnly]` -//! trampolines. -//! * Async functions surface as `Task`-returning methods and `iter` -//! returns as lazily streamed `IEnumerable`. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod callbacks; -mod calls; -mod codec; -mod docs; -mod entities; -mod package; -mod pinvoke; -mod runtime; -mod types; - -use camino::Utf8Path; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::{BindingModel, ErrorBinding}; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::callbacks::render_callback_interface; -use crate::calls::render_wrapper_class; -use crate::entities::{ - render_enum, render_interface_class, render_rich_enum_class, render_struct_class, -}; -use crate::package::{ - render_csproj, render_csproj_with_assets, render_nuspec, render_packaged_readme, render_readme, - resolve_dotnet_package, -}; -use crate::pinvoke::render_native_methods; -use crate::runtime::{ - render_buffer_classes, render_domain_exception, render_error_struct, render_exception_class, - render_helpers_class, render_once_enumerable_class, -}; - -/// Per-target configuration for [`DotnetGenerator`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct DotnetConfig { - /// C# namespace (and on-disk basename used for `.cs`/`.csproj`/`.nuspec`). - /// Defaults to `"WeaveFFI"`. - pub namespace: Option, - /// When `true` (the default), strip the IR module name prefix from emitted - /// C# method names; the per-module static class already namespaces them. - /// Set to `false` to restore module-prefixed names. - pub strip_module_prefix: bool, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the P/Invoke bindings call the same - /// exported symbols the producer emits. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with. - #[serde(skip)] - pub input_basename: Option, -} - -impl Default for DotnetConfig { - fn default() -> Self { - Self { - namespace: None, - strip_module_prefix: true, - prefix: None, - input_basename: None, - } - } -} - -impl DotnetConfig { - /// Returns the configured C# namespace, falling back to `"WeaveFFI"`. - pub fn namespace(&self) -> &str { - self.namespace.as_deref().unwrap_or("WeaveFFI") - } - - /// Returns the configured C ABI symbol prefix, falling back to `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// .NET backend: emits a C# project (`.csproj` and `.nuspec`) of P/Invoke -/// declarations and idiomatic wrappers over the C ABI exposed by the -/// underlying cdylib. -pub struct DotnetGenerator; - -impl LanguageBackend for DotnetGenerator { - type Config = DotnetConfig; - - fn name(&self) -> &'static str { - "dotnet" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let namespace = config.namespace(); - let input_basename = config.input_basename(); - let package = resolve_dotnet_package(api, config); - let dir = out_dir.join("dotnet"); - let cs_filename = format!("{namespace}.cs"); - let csproj_filename = format!("{namespace}.csproj"); - let nuspec_filename = format!("{namespace}.nuspec"); - vec![ - OutputFile::new( - dir.join(&cs_filename), - render_csharp( - model, - namespace, - config.strip_module_prefix, - input_basename, - &cs_filename, - ), - ), - OutputFile::new( - dir.join(&csproj_filename), - render_csproj(&package, input_basename, &csproj_filename), - ), - OutputFile::new( - dir.join(&nuspec_filename), - render_nuspec(&package, input_basename, &nuspec_filename), - ), - OutputFile::new( - dir.join("README.md"), - render_readme(&package, input_basename), - ), - ] - } - - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let namespace = config.namespace(); - let input_basename = config.input_basename(); - let package = resolve_dotnet_package(api, config); - let dir = out_dir.join("dotnet"); - let lib_name = &ctx.binaries.lib_name; - - let cs_filename = format!("{namespace}.cs"); - let csproj_filename = format!("{namespace}.csproj"); - let nuspec_filename = format!("{namespace}.nuspec"); - - // Rebind the P/Invoke library name from the WeaveFFI brand to the - // bundled library's base name so `[DllImport]` resolves the file we - // ship under `runtimes//native/`. - let cs = render_csharp( - model, - namespace, - config.strip_module_prefix, - input_basename, - &cs_filename, - ) - .replace( - "private const string LibName = \"weaveffi\";", - &format!("private const string LibName = \"{lib_name}\";"), - ); - - let native_assets = " \n \ - \n \ - PreserveNewest\n \ - \n \n"; - - let mut files = vec![ - PackagedFile::text(dir.join(&cs_filename), cs), - PackagedFile::text( - dir.join(&csproj_filename), - render_csproj_with_assets( - &package, - input_basename, - &csproj_filename, - native_assets, - ), - ), - PackagedFile::text( - dir.join(&nuspec_filename), - render_nuspec(&package, input_basename, &nuspec_filename), - ), - PackagedFile::text( - dir.join("README.md"), - render_packaged_readme(&package, ctx, input_basename), - ), - ]; - - // Bundle each prebuilt library under the NuGet `runtimes//native/` - // layout NuGet auto-resolves at restore time. Platforms NuGet has no - // RID for (Android, wasm32) have no slot in the package and are skipped. - for nb in &ctx.binaries.binaries { - let Some(rid) = nb.platform.nuget_rid() else { - continue; - }; - let dest = dir - .join("runtimes") - .join(rid) - .join("native") - .join(ctx.binaries.bundled_filename(nb.platform)); - files.push(PackagedFile::copy(dest, nb.source.clone())); - } - - Some(files) - } -} - -/// Render the complete generated C# source file: the prelude, usings, the -/// shared runtime types, every module's entities (enums, records, callback -/// interfaces with their vtables, interface wrappers), the `NativeMethods` -/// extern class, and the per-module static wrapper classes. -pub(crate) fn render_csharp( - model: &BindingModel, - namespace: &str, - strip_module_prefix: bool, - input_basename: &str, - filename: &str, -) -> String { - let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); - // Opt the file into the nullable annotation context so the `string?` - // signatures (optional strings) are valid regardless of the consuming - // project's setting; without this, default projects warn CS8632. - out.push_str("#nullable enable\n\n"); - out.push_str( - "using System;\nusing System.Collections.Generic;\nusing System.Runtime.InteropServices;\n", - ); - if model.has_callback_interfaces() { - // `CallConvCdecl` for the `[UnmanagedCallersOnly]` trampolines. - out.push_str("using System.Runtime.CompilerServices;\n"); - } - if model.has_async() { - out.push_str("using System.Threading.Tasks;\n"); - } - out.push('\n'); - out.push_str(&format!("namespace {namespace}\n{{\n")); - - // One typed exception per declaring module; inheriting submodules - // reference the ancestor's type through `ModuleBinding::error`. - let domains: Vec<&ErrorBinding> = model - .modules - .iter() - .filter_map(|m| m.error.as_ref()) - .filter(|eb| eb.declared_here) - .collect(); - - render_exception_class(&mut out); - for eb in &domains { - render_domain_exception(&mut out, eb); - } - render_error_struct(&mut out, &domains); - render_helpers_class(&mut out); - render_buffer_classes(&mut out); - if model.has_iterators() { - render_once_enumerable_class(&mut out); - } - - for m in &model.modules { - for e in &m.enums { - // Rich (algebraic) enums are sum types, emitted as an abstract - // class with one nested variant class each; only plain C-style - // enums map to `enum`. - if e.is_rich() { - render_rich_enum_class(&mut out, e); - } else { - render_enum(&mut out, e); - } - } - for s in &m.structs { - render_struct_class(&mut out, s); - } - for cb in &m.callback_interfaces { - render_callback_interface(&mut out, m, cb); - } - for i in &m.interfaces { - render_interface_class(&mut out, i, m.error.as_ref()); - } - } - - render_native_methods(&mut out, model); - - for m in &model.modules { - render_wrapper_class(&mut out, m, strip_module_prefix); - } - - out.push_str("}\n\n"); - out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); - out -} - -#[cfg(test)] -mod tests; diff --git a/crates/weaveffi-gen-dotnet/src/package.rs b/crates/weaveffi-gen-dotnet/src/package.rs deleted file mode 100644 index a4f1afbd..00000000 --- a/crates/weaveffi-gen-dotnet/src/package.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! Package-surface renderers: the `.csproj` and `.nuspec` manifests, the -//! README variants, and the NuGet package-identity resolution. -//! -//! Every interpolated user string (package id, version, description, authors, -//! license, and URLs) routes through the shared -//! [`xml_escape`](weaveffi_core::manifest::xml_escape), so markup-sensitive -//! characters can't corrupt the XML. - -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 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: &ResolvedApi, config: &DotnetConfig) -> ResolvedPackage { - let namespace = config.namespace(); - let mut p = pkg::resolve( - api, - config.namespace.as_deref(), - config.input_basename.as_deref(), - ); - // 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().and_then(|p| p.name.as_deref()).is_none() - && config.namespace.is_none() - && config.input_basename.is_none() - { - p.name = namespace.to_string(); - } - p -} - -/// Render the `.csproj` emitted by `generate`, with no extra item groups. -pub(crate) fn render_csproj( - package: &ResolvedPackage, - input_basename: &str, - filename: &str, -) -> String { - render_csproj_with_assets(package, input_basename, filename, "") -} - -/// Render the `.csproj`, optionally injecting extra `` blocks -/// (`native_assets`) after the main ``. The `weaveffi package` -/// path passes the `runtimes/**` native-asset item group here; `generate` -/// passes an empty string. -pub(crate) fn render_csproj_with_assets( - package: &ResolvedPackage, - input_basename: &str, - filename: &str, - native_assets: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, filename); - let id = xml_escape(&package.name); - let version = xml_escape(&package.version); - let description = xml_escape(&package.description_or_default()); - let mut extra = format!(" {description}\n"); - if !package.authors.is_empty() { - extra.push_str(&format!( - " {}\n", - xml_escape(&package.authors.join(", ")) - )); - } - if let Some(license) = &package.license { - extra.push_str(&format!( - " {}\n", - xml_escape(license) - )); - } - if let Some(url) = package.homepage.as_ref().or(package.repository.as_ref()) { - extra.push_str(&format!( - " {}\n", - xml_escape(url) - )); - } - format!( - r#"{prelude} - - - net8.0 - {id} - {version} -{extra} true - -{native_assets} - - -{trailer}"#, - ) -} - -/// Render the `.nuspec` package manifest. -pub(crate) fn render_nuspec( - package: &ResolvedPackage, - input_basename: &str, - filename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, filename); - let id = xml_escape(&package.name); - let version = xml_escape(&package.version); - let authors = if package.authors.is_empty() { - "WeaveFFI Contributors".to_string() - } else { - xml_escape(&package.authors.join(", ")) - }; - let description = xml_escape(&package.description_or_default()); - let license = xml_escape(&package.license.clone().unwrap_or_else(|| "MIT".into())); - let project_url = xml_escape( - &package - .homepage - .clone() - .or_else(|| package.repository.clone()) - .unwrap_or_else(|| "https://github.com/weavefoundry/weaveffi".into()), - ); - format!( - r#" -{prelude} - - {id} - {version} - {authors} - {description} - {license} - {project_url} - ffi interop native pinvoke - - - -{trailer}"#, - ) -} - -/// Render the README emitted by `generate`. -pub(crate) fn render_readme(package: &ResolvedPackage, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = &package.name; - format!( - r#"{prelude}# {name} (.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`). - -{trailer}"#, - ) -} - -/// Render the README for a packaged .NET artifact, listing the bundled -/// runtime identifiers so consumers know which platforms ship prebuilt. -/// Platforms without a NuGet RID aren't bundled and aren't listed. -pub(crate) fn render_packaged_readme( - package: &ResolvedPackage, - ctx: &PackageContext, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = &package.name; - let rids: Vec = ctx - .binaries - .platforms() - .filter_map(|p| p.nuget_rid()) - .map(|rid| format!("- `{rid}`")) - .collect(); - let rid_list = rids.join("\n"); - format!( - r#"{prelude}# {name} (.NET) - -Auto-generated P/Invoke bindings for the WeaveFFI native library, with a -prebuilt native library bundled for each supported runtime under `runtimes/`. - -## Install - -```bash -dotnet add package {name} -``` - -The native library loads automatically; no extra setup is required on a -bundled platform. - -## Bundled runtimes - -{rid_list} - -## Pack - -```bash -dotnet pack -c Release -``` - -{trailer}"#, - ) -} diff --git a/crates/weaveffi-gen-dotnet/src/pinvoke.rs b/crates/weaveffi-gen-dotnet/src/pinvoke.rs deleted file mode 100644 index 711daccb..00000000 --- a/crates/weaveffi-gen-dotnet/src/pinvoke.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! The `NativeMethods` class: one `[DllImport]` extern (or unmanaged -//! delegate type) per lowered C symbol, matching each callable's shape -//! exactly. - -use weaveffi_core::abi::{AbiParam, CType}; -use weaveffi_core::cabi::ABI_VERSION; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{ - BindingModel, CallShape, FnBinding, InterfaceBinding, IteratorBinding, ParamBinding, -}; - -use crate::calls::async_result_is_ptr_len; -use crate::types::{cs_out_param, cs_pinvoke_ctype, pinvoke_type, safe_cs_name}; - -/// The P/Invoke spelling of one ABI slot: its C type mapped onto the -/// P/Invoke vocabulary plus the IDL slot name with keyword escaping applied. -pub(crate) fn pinvoke_slot(slot: &AbiParam) -> String { - format!( - "{} {}", - cs_pinvoke_ctype(&slot.ty), - safe_cs_name(&slot.name) - ) -} - -/// The P/Invoke parameter list (one entry per precomputed ABI slot) for one -/// IR-typed parameter. -pub(crate) fn pinvoke_param_list(p: &ParamBinding) -> Vec { - p.abi.iter().map(pinvoke_slot).collect() -} - -/// Whether an ABI slot is the trailing `{prefix}_error* out_err`. -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 ABI-revision check in its -/// static constructor, the shared runtime imports (`abi_version`, -/// `error_set`, `free_string`, `free_bytes`, `error_clear`, `error_free`), -/// and then every interface and function extern in declaration order. -/// Callback interfaces declare no exported symbols; their vtables live next -/// to the consumer-facing interface type. -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"); - w.line("{"); - 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("// Fills a producer-owned error slot with a copy of `message`; callback"); - w.line("// trampolines report a thrown exception through it so no managed"); - w.line("// allocation is ever handed to the producer's allocator."); - w.line("[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]"); - w.line("internal static extern void weaveffi_error_set(IntPtr err, int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string message);"); - w.blank(); - w.line("[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]"); - w.line("internal static extern void weaveffi_free_string(IntPtr ptr);"); - w.blank(); - w.line("[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]"); - w.line("internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len);"); - w.blank(); - w.line("[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]"); - w.line("internal static extern void weaveffi_error_clear(ref WeaveFFIError err);"); - w.blank(); - w.line("[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]"); - w.line("internal static extern void weaveffi_error_free(IntPtr err);"); - w.blank(); - w.dedent(); - - // Records, rich enums, and callback interfaces declare no C symbols, so - // only interfaces and functions declare P/Invokes. - for m in &model.modules { - for i in &m.interfaces { - let mut tmp = String::new(); - render_interface_pinvoke(&mut tmp, i); - w.raw(tmp); - } - for f in &m.functions { - let mut tmp = String::new(); - render_shaped_pinvoke(&mut tmp, f); - w.raw(tmp); - } - } - - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the extern declaration set matching one callable's shape exactly: -/// sync, async (delegate + launcher), or iterator (constructor, `next`, -/// `destroy`). Shared by free functions and interface members. -pub(crate) fn render_shaped_pinvoke(out: &mut String, f: &FnBinding) { - match &f.shape { - CallShape::Sync(_) => render_function_pinvoke(out, f), - CallShape::Async(_) => render_async_function_pinvoke(out, f), - CallShape::Iterator(it) => render_iterator_pinvoke(out, it), - } -} - -/// The `[DllImport]` set backing one interface: the `clone` and `destroy` -/// reference-count symbols plus one shape-matched extern set per member. -/// Instance members carry the implicit leading `self` slot. -pub(crate) fn render_interface_pinvoke(out: &mut String, i: &InterfaceBinding) { - let clone_sym = &i.clone_symbol; - let destroy_sym = &i.destroy_symbol; - let mut w = CodeWriter::four_space().with_depth(2); - w.line(format!( - "[DllImport(LibName, EntryPoint = \"{clone_sym}\", CallingConvention = CallingConvention.Cdecl)]" - )); - w.line(format!( - "internal static extern IntPtr {clone_sym}(IntPtr self);" - )); - w.blank(); - w.line(format!( - "[DllImport(LibName, EntryPoint = \"{destroy_sym}\", CallingConvention = CallingConvention.Cdecl)]" - )); - w.line(format!( - "internal static extern void {destroy_sym}(IntPtr self);" - )); - w.blank(); - out.push_str(&w.finish()); - - for f in i - .constructors - .iter() - .chain(i.methods.iter()) - .chain(i.statics.iter()) - { - render_shaped_pinvoke(out, f); - } -} - -/// The single extern behind one synchronous callable, rendered from its -/// precomputed [`CallShape::Sync`] slots: the optional `self`, every input -/// slot, any return out-params, and the trailing error slot. -pub(crate) fn render_function_pinvoke(out: &mut String, f: &FnBinding) { - let abi = match &f.shape { - CallShape::Sync(abi) => abi, - CallShape::Iterator(it) => { - render_iterator_pinvoke(out, it); - return; - } - CallShape::Async(_) => { - render_async_function_pinvoke(out, f); - return; - } - }; - let c_sym = &abi.symbol; - let params: Vec = abi.params.iter().map(slot_param).collect(); - let ret_type = cs_pinvoke_ctype(&abi.ret); - - let mut w = CodeWriter::four_space().with_depth(2); - w.line(format!( - "[DllImport(LibName, EntryPoint = \"{c_sym}\", CallingConvention = CallingConvention.Cdecl)]" - )); - w.line(format!( - "internal static extern {ret_type} {c_sym}({});", - params.join(", ") - )); - w.blank(); - out.push_str(&w.finish()); -} - -/// One P/Invoke parameter for a sync or iterator ABI slot: the trailing -/// error slot becomes `ref WeaveFFIError`, `out_*` pointer slots become -/// `out` pointee values, everything else is passed by value. -pub(crate) fn slot_param(slot: &AbiParam) -> String { - if is_error_slot(slot) { - return format!("ref WeaveFFIError {}", slot.name); - } - match &slot.ty { - CType::Ptr { .. } if slot.name.starts_with("out_") => cs_out_param(slot), - _ => pinvoke_slot(slot), - } -} - -/// The three entry points behind one `iter` function: the constructor -/// returning the opaque iterator handle, `_next`, and `_destroy`. -pub(crate) fn render_iterator_pinvoke(out: &mut String, it: &IteratorBinding) { - let launch_params: Vec = it.launch.params.iter().map(slot_param).collect(); - let mut w = CodeWriter::four_space().with_depth(2); - w.line(format!( - "[DllImport(LibName, EntryPoint = \"{0}\", CallingConvention = CallingConvention.Cdecl)]", - it.launch.symbol - )); - w.line(format!( - "internal static extern IntPtr {}({});", - it.launch.symbol, - launch_params.join(", ") - )); - w.blank(); - - let next_params: Vec = it.next.params.iter().map(slot_param).collect(); - w.line(format!( - "[DllImport(LibName, EntryPoint = \"{0}\", CallingConvention = CallingConvention.Cdecl)]", - it.next.symbol - )); - w.line(format!( - "internal static extern int {}({});", - it.next.symbol, - next_params.join(", ") - )); - w.blank(); - - w.line(format!( - "[DllImport(LibName, EntryPoint = \"{0}\", CallingConvention = CallingConvention.Cdecl)]", - it.destroy_symbol - )); - w.line(format!( - "internal static extern void {}(IntPtr iter);", - it.destroy_symbol - )); - w.blank(); - out.push_str(&w.finish()); -} - -/// The completion delegate's result parameters for one async return type: -/// nothing for void, an owned `(ptr, len)` pair for bytes and buffered -/// values, and a single by-value slot otherwise (an object result is the -/// adopted `IntPtr`). -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(), - Some(ty) => format!(", {} result", pinvoke_type(ty)), - } -} - -/// The unmanaged completion delegate plus the `_async` launcher extern for -/// one async callable. -pub(crate) fn render_async_function_pinvoke(out: &mut String, f: &FnBinding) { - let c_sym = &f.c_base; - let delegate_name = format!("AsyncCb_{c_sym}"); - let cb_params = async_cb_delegate_result_params(&f.ret); - - let mut w = CodeWriter::four_space().with_depth(2); - w.line("[UnmanagedFunctionPointer(CallingConvention.Cdecl)]"); - w.line(format!( - "internal delegate void {delegate_name}(IntPtr context, IntPtr err{cb_params});" - )); - w.blank(); - - let mut params: Vec = Vec::new(); - if f.has_self { - params.push("IntPtr self".into()); - } - params.extend(f.params.iter().flat_map(pinvoke_param_list)); - if f.cancellable { - params.push("IntPtr cancel_token".into()); - } - params.push(format!("{delegate_name} callback")); - params.push("IntPtr context".into()); - - w.line(format!( - "[DllImport(LibName, EntryPoint = \"{c_sym}_async\", CallingConvention = CallingConvention.Cdecl)]" - )); - w.line(format!( - "internal static extern void {c_sym}_async({});", - params.join(", ") - )); - w.blank(); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-dotnet/src/runtime.rs b/crates/weaveffi-gen-dotnet/src/runtime.rs deleted file mode 100644 index 447dca58..00000000 --- a/crates/weaveffi-gen-dotnet/src/runtime.rs +++ /dev/null @@ -1,596 +0,0 @@ -//! Shared runtime types emitted once per generated file: the branded and -//! typed exceptions, the raw error struct with its check helpers, the memory -//! helpers, the value-buffer writer and reader, and the single-use -//! enumerable wrapping iterator returns. - -use heck::ToUpperCamelCase; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors; -use weaveffi_core::manifest::xml_escape; -use weaveffi_core::model::ErrorBinding; - -use crate::codec::emit_buffer_read; -use crate::docs::writer_doc; -use crate::types::cs_str; - -/// The C# exception class name for one error domain: the domain stem with -/// exactly one `Exception` suffix, so `KvError` becomes `KvException` rather -/// than `KvErrorException`. -pub(crate) fn dotnet_exception_name(eb: &ErrorBinding) -> String { - errors::exception_type_name(&eb.type_name) -} - -/// The per-domain error-check helper name on `WeaveFFIError`; `KvException` -/// is checked by `CheckKv`. -pub(crate) fn check_method_name(eb: &ErrorBinding) -> String { - let exc = dotnet_exception_name(eb); - let stem = exc.strip_suffix("Exception").unwrap_or(&exc).to_string(); - format!("Check{stem}") -} - -/// Render the generic branded exception every failure surfaces through when -/// no typed domain applies. The runtime trap codes are exposed as constants -/// so a caller can tell a producer panic (`-2`) from a callback-interface -/// implementation that threw (`-4`); both arrive through this class rather -/// than a typed domain exception. -pub(crate) fn render_exception_class(out: &mut String) { - let mut w = CodeWriter::four_space().with_depth(1); - w.line("/// Raised for any WeaveFFI failure that isn't a typed domain"); - w.line("/// error: producer bugs, panics, marshalling failures, and exceptions"); - w.line("/// thrown by a callback-interface implementation."); - w.line("public class WeaveFFIException : Exception"); - w.block("{", "}", |w| { - w.line("/// An untyped producer error."); - w.line("public const int GenericErrorCode = -1;"); - w.line("/// The producer panicked; the message carries the panic text."); - w.line("public const int PanicErrorCode = -2;"); - w.line("/// An argument couldn't be lifted by the producer."); - w.line("public const int MarshalErrorCode = -3;"); - w.line("/// A callback-interface implementation threw; the message"); - w.line("/// carries the original exception's message."); - w.line("public const int ForeignErrorCode = -4;"); - w.blank(); - w.line("public int Code { get; }"); - w.blank(); - w.line("public WeaveFFIException(int code, string message) : base(message)"); - w.block("{", "}", |w| { - w.line("Code = code;"); - }); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// One typed exception class per declared error domain, extending the generic -/// brand exception. Each code surfaces as a `public const int` (PascalCase), -/// and `FromCode` maps a raw error slot to the typed exception, falling back -/// to the generic `WeaveFFIException` for unknown codes. Declared codes are -/// validated positive, so every negative runtime code (generic failures, -/// producer panics, marshalling failures, foreign callback failures) takes -/// the fallback. When the -/// matched code declares payload fields, `FromCode` decodes them from the -/// serialized payload buffer and exposes each field in the exception's -/// `Data` dictionary, keyed by the IDL field name. -pub(crate) fn render_domain_exception(out: &mut String, eb: &ErrorBinding) { - let exc = dotnet_exception_name(eb); - let mut w = CodeWriter::four_space().with_depth(1); - w.line(format!( - "/// Typed exception for the {} error domain (module {}).", - eb.type_name, - eb.owner_path.replace('_', ".") - )); - w.line(format!("public class {exc} : WeaveFFIException")); - w.block("{", "}", |w| { - for c in &eb.codes { - if c.doc.is_some() { - writer_doc(w, &c.doc); - } else { - w.line(format!("/// {}", xml_escape(&c.message))); - } - w.line(format!( - "public const int {} = {};", - errors::pascal(&c.name), - c.value - )); - } - w.blank(); - w.line(format!( - "public {exc}(int code, string message) : base(code, message)" - )); - w.line("{"); - w.line("}"); - w.blank(); - w.line("/// Wraps a raw error slot in the typed exception, falling"); - w.line("/// back to for unknown codes. Codes"); - w.line("/// declaring payload fields decode them into Data."); - w.line( - "internal static WeaveFFIException FromCode(int code, string message, byte[]? payload)", - ); - w.block("{", "}", |w| { - w.line("switch (code)"); - w.block("{", "}", |w| { - for c in &eb.codes { - w.line(format!("case {}:", errors::pascal(&c.name))); - if c.fields.is_empty() { - w.indent(); - w.line(format!( - "return new {exc}(code, string.IsNullOrEmpty(message) ? \"{}\" : message);", - cs_str(&c.message) - )); - w.dedent(); - } else { - w.block("{", "}", |w| { - w.line(format!( - "var ex = new {exc}(code, string.IsNullOrEmpty(message) ? \"{}\" : message);", - cs_str(&c.message) - )); - w.line("if (payload != null)"); - w.block("{", "}", |w| { - w.line("var reader = new WeaveFFIBufferReader(payload);"); - for f in &c.fields { - let var = format!("f{}", f.name.to_upper_camel_case()); - emit_buffer_read(w, &f.ty, &var, "reader", 0); - w.line(format!("ex.Data[\"{}\"] = {var};", cs_str(&f.name))); - } - w.line("reader.ExpectEnd();"); - }); - w.line("return ex;"); - }); - } - } - w.line("default:"); - w.indent(); - w.line("return new WeaveFFIException(code, message);"); - w.dedent(); - }); - }); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// The raw error slot plus its check helpers: the generic `Check` (throws -/// `WeaveFFIException` on any non-zero code) and one `Check{Domain}` variant -/// per declared domain (throws the typed exception via `FromCode`). Every -/// check copies the message (and, for domains, the serialized payload) and -/// then calls `weaveffi_error_clear`, which frees both producer allocations, -/// before throwing. -pub(crate) fn render_error_struct(out: &mut String, domains: &[&ErrorBinding]) { - let mut w = CodeWriter::four_space().with_depth(1); - w.line("[StructLayout(LayoutKind.Sequential)]"); - w.line("internal struct WeaveFFIError"); - w.block("{", "}", |w| { - w.line("public int Code;"); - w.line("public IntPtr Message;"); - w.line("public IntPtr PayloadPtr;"); - w.line("public UIntPtr PayloadLen;"); - w.blank(); - w.line("internal static byte[]? CopyPayload(WeaveFFIError err)"); - w.block("{", "}", |w| { - w.line("if (err.PayloadPtr == IntPtr.Zero || (int)err.PayloadLen == 0)"); - w.block("{", "}", |w| { - w.line("return null;"); - }); - w.line("var payload = new byte[(int)err.PayloadLen];"); - w.line("Marshal.Copy(err.PayloadPtr, payload, 0, (int)err.PayloadLen);"); - w.line("return payload;"); - }); - w.blank(); - w.line("internal static void Check(WeaveFFIError err)"); - w.block("{", "}", |w| { - w.line("if (err.Code != 0)"); - w.block("{", "}", |w| { - // The clear zeroes the slot, so capture code and message - // before releasing the producer allocations. - w.line("var code = err.Code;"); - w.line("var msg = Marshal.PtrToStringUTF8(err.Message) ?? \"\";"); - w.line("NativeMethods.weaveffi_error_clear(ref err);"); - w.line("throw new WeaveFFIException(code, msg);"); - }); - }); - for eb in domains { - let exc = dotnet_exception_name(eb); - let check = check_method_name(eb); - w.blank(); - w.line(format!("internal static void {check}(WeaveFFIError err)")); - w.block("{", "}", |w| { - w.line("if (err.Code != 0)"); - w.block("{", "}", |w| { - w.line("var code = err.Code;"); - w.line("var msg = Marshal.PtrToStringUTF8(err.Message) ?? \"\";"); - w.line("var payload = CopyPayload(err);"); - w.line("NativeMethods.weaveffi_error_clear(ref err);"); - w.line(format!("throw {exc}.FromCode(code, msg, payload);")); - }); - }); - } - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// Render the internal string and pointer helpers shared by every wrapper, -/// plus the `WeaveFFIHandle` marker every object wrapper's adopting -/// constructor takes. The marker exists because the generated file is often -/// compiled into the consumer's own assembly, where an `internal` constructor -/// taking a bare `IntPtr` would join overload resolution with the public IDL -/// constructors: an `int` literal converts implicitly to `IntPtr` (`nint`) and -/// C# prefers that over `long`, so `new Token(1)` would silently adopt the -/// pointer `0x1`. No IDL type lowers to `WeaveFFIHandle`, so the adopting -/// constructor can never be chosen by accident. -pub(crate) fn render_helpers_class(out: &mut String) { - let mut w = CodeWriter::four_space().with_depth(1); - w.line("/// One strong reference to a native object, on its way into"); - w.line("/// a wrapper that adopts it. A distinct type (rather than a bare"); - w.line("/// IntPtr) so the adopting constructor never competes with a"); - w.line("/// public constructor taking an integer."); - w.line("internal readonly struct WeaveFFIHandle"); - w.block("{", "}", |w| { - w.line("internal readonly IntPtr Value;"); - w.blank(); - w.line("internal WeaveFFIHandle(IntPtr value)"); - w.block("{", "}", |w| { - w.line("Value = value;"); - }); - }); - w.blank(); - w.line("internal static class WeaveFFIHelpers"); - w.block("{", "}", |w| { - w.line("internal static IntPtr StringToPtr(string? s)"); - w.block("{", "}", |w| { - w.line("return s == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(s);"); - }); - w.blank(); - w.line("internal static string? PtrToString(IntPtr ptr)"); - w.block("{", "}", |w| { - w.line("return ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr);"); - }); - w.blank(); - w.line("internal static void FreePtr(IntPtr ptr)"); - w.block("{", "}", |w| { - w.line("Marshal.FreeCoTaskMem(ptr);"); - }); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// The private buffer writer and reader implementing the WeaveFFI value-buffer -/// wire format (little-endian, packed, no alignment) over managed byte arrays. -/// The reader rejects malformed input (truncation, invalid bool or option -/// flags, oversized length prefixes, a zero object token, trailing bytes) by -/// throwing `InvalidOperationException`; a malformed buffer is always a -/// producer or consumer bug, never a recoverable domain error. Object tokens -/// are the pointer zero-extended to `u64`, so the conversions go through the -/// native-sized unsigned integer rather than `long`. -pub(crate) fn render_buffer_classes(out: &mut String) { - let mut w = CodeWriter::four_space().with_depth(1); - w.block_raw( - r#"/// 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 void WriteObject(IntPtr token) - { - WriteU64((ulong)(nuint)(nint)token); - } - - 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 IntPtr ReadObject() - { - var token = ReadU64(); - if (token == 0) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: null object token"); - } - return (nint)(nuint)token; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } -} -"#, - ); - w.blank(); - out.push_str(&w.finish()); -} - -/// The single-use `IEnumerable` wrapping every iterator return. The -/// native iterator is consumed (and destroyed) by its one enumerator, so a -/// second `GetEnumerator()` cannot yield anything; surfacing it as an -/// `InvalidOperationException` beats silently returning an empty or -/// double-destroyed sequence. -pub(crate) fn render_once_enumerable_class(out: &mut String) { - let mut w = CodeWriter::four_space().with_depth(1); - w.line("/// A lazily streamed sequence backed by a native iterator."); - w.line("/// It can be enumerated exactly once; enumerate it promptly (or call"); - w.line("/// a materializing operator such as ToList) and let the enumerator be"); - w.line("/// disposed to release the native iterator."); - w.line("internal sealed class WeaveFFIOnceEnumerable : IEnumerable"); - w.block("{", "}", |w| { - w.line("private IEnumerator? _enumerator;"); - w.blank(); - w.line("internal WeaveFFIOnceEnumerable(IEnumerator enumerator)"); - w.block("{", "}", |w| { - w.line("_enumerator = enumerator;"); - }); - w.blank(); - w.line("public IEnumerator GetEnumerator()"); - w.block("{", "}", |w| { - w.line("var e = System.Threading.Interlocked.Exchange(ref _enumerator, null);"); - w.line("if (e == null)"); - w.block("{", "}", |w| { - w.line( - "throw new InvalidOperationException(\"this sequence can be enumerated only once\");", - ); - }); - w.line("return e;"); - }); - w.blank(); - w.line("System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()"); - w.block("{", "}", |w| { - w.line("return GetEnumerator();"); - }); - }); - w.blank(); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-dotnet/src/tests.rs b/crates/weaveffi-gen-dotnet/src/tests.rs deleted file mode 100644 index 584a9792..00000000 --- a/crates/weaveffi-gen-dotnet/src/tests.rs +++ /dev/null @@ -1,452 +0,0 @@ -//! Rendering tests over a small hand-built API exercising every ABI -//! revision 2 shape: reference-counted objects, nullable objects, objects in -//! records and lists, iterators of objects, and a callback interface. - -use camino::Utf8Path; -use weaveffi_core::backend::LanguageBackend; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::PackageContext; -use weaveffi_core::platform::{BinarySet, Platform}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::validate::validate_api; -use weaveffi_ir::ir::{ - Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, - TypeRef, -}; - -use crate::{render_csharp, DotnetConfig, DotnetGenerator}; - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } -} - -fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn named(name: &str) -> TypeRef { - TypeRef::Named(name.into()) -} - -/// The fixture: a `bus` module with a `Ticker` interface, a `Bundle` record -/// holding a `Ticker` and a `[Ticker]`, a `Subscriber` callback interface, -/// and free functions covering nullable objects, iterators of objects, an -/// async object result, and a callback-interface parameter. -fn fixture() -> ResolvedApi { - let ticker = InterfaceDef { - name: "Ticker".into(), - doc: Some("A counter the producer owns.".into()), - deprecated: None, - constructors: vec![func("new", vec![param("start", TypeRef::I32)], None)], - methods: vec![func("value", vec![], Some(TypeRef::I32))], - statics: vec![], - }; - let bundle = StructDef { - name: "Bundle".into(), - doc: None, - deprecated: None, - fields: vec![ - field("primary", named("Ticker")), - field("others", TypeRef::List(Box::new(named("Ticker")))), - field("label", TypeRef::StringUtf8), - ], - }; - let subscriber = CallbackInterfaceDef { - name: "Subscriber".into(), - doc: Some("Receives bus events.".into()), - deprecated: None, - methods: vec![ - func( - "on_message", - vec![ - param("text", TypeRef::StringUtf8), - param("weight", TypeRef::I32), - param("bundle", named("Bundle")), - param("ticker", named("Ticker")), - ], - Some(TypeRef::Bool), - ), - func("on_close", vec![param("code", TypeRef::I64)], None), - ], - }; - let module = Module { - name: "bus".into(), - doc: None, - functions: vec![ - func( - "maybe_ticker", - vec![param("input", TypeRef::Optional(Box::new(named("Ticker"))))], - Some(TypeRef::Optional(Box::new(named("Ticker")))), - ), - func( - "tickers", - vec![], - Some(TypeRef::Iterator(Box::new(named("Ticker")))), - ), - func( - "subscribe", - vec![param("listener", named("Subscriber"))], - None, - ), - Function { - r#async: true, - ..func("spawn", vec![], Some(named("Ticker"))) - }, - func( - "roundtrip_bundle", - vec![param("b", named("Bundle"))], - Some(named("Bundle")), - ), - ], - interfaces: vec![ticker], - callback_interfaces: vec![subscriber], - structs: vec![bundle], - enums: vec![], - errors: None, - modules: vec![], - }; - validate_api( - Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![module], - }, - None, - ) - .expect("fixture validates") -} - -fn render() -> String { - let api = fixture(); - let model = BindingModel::build(&api, "weaveffi"); - render_csharp(&model, "Bus", true, "bus.yml", "Bus.cs") -} - -fn assert_has(out: &str, needle: &str) { - assert!(out.contains(needle), "missing `{needle}` in:\n{out}"); -} - -#[test] -fn interface_wrapper_is_reference_counted_and_disposes_once() { - let out = render(); - assert_has(&out, "public class Ticker : IDisposable"); - assert_has( - &out, - "internal static extern IntPtr weaveffi_bus_Ticker_clone(IntPtr self);", - ); - assert_has( - &out, - "internal static extern void weaveffi_bus_Ticker_destroy(IntPtr self);", - ); - // A second strong reference for the codec. - assert_has( - &out, - "return NativeMethods.weaveffi_bus_Ticker_clone(Handle);", - ); - // Exactly one destroy across Dispose and the finalizer. - assert_has( - &out, - "if (System.Threading.Interlocked.Exchange(ref _released, 1) != 0)", - ); - assert_has(&out, "NativeMethods.weaveffi_bus_Ticker_destroy(h);"); - assert_has(&out, "~Ticker()"); - assert_has(&out, "throw new ObjectDisposedException(nameof(Ticker));"); - // The constructor adopts the returned reference; the method borrows - // through the checked property so a disposed wrapper throws. - assert_has(&out, "public Ticker(int start)"); - assert_has( - &out, - "var result = NativeMethods.weaveffi_bus_Ticker_new(start, ref err);", - ); - assert_has(&out, "_handle = result;"); - assert_has(&out, "public int Value()"); - assert_has( - &out, - "var result = NativeMethods.weaveffi_bus_Ticker_value(Handle, ref err);", - ); -} - -#[test] -fn adopting_constructor_takes_the_handle_marker_not_a_bare_pointer() { - let out = render(); - // Compiled into the consumer's assembly, an `internal Ticker(IntPtr)` - // would win overload resolution over the public `Ticker(int start)` for - // an integer literal (int -> nint is implicit and preferred over long), - // silently adopting a bogus pointer. The marker struct rules that out. - assert_has(&out, "internal readonly struct WeaveFFIHandle"); - assert_has(&out, "internal WeaveFFIHandle(IntPtr value)"); - assert_has(&out, "internal Ticker(WeaveFFIHandle handle)"); - assert_has(&out, "_handle = handle.Value;"); - assert!( - !out.contains("internal Ticker(IntPtr handle)"), - "adopting constructor must not take a bare IntPtr" - ); - assert!( - !out.contains("new Ticker(result)"), - "every adoption must go through the WeaveFFIHandle marker" - ); -} - -#[test] -fn nullable_objects_are_single_nullable_slots() { - let out = render(); - assert_has(&out, "public static Ticker? MaybeTicker(Ticker? input)"); - assert_has( - &out, - "NativeMethods.weaveffi_bus_maybe_ticker(input?.Handle ?? IntPtr.Zero, ref err);", - ); - assert_has( - &out, - "return result == IntPtr.Zero ? null : new Ticker(new WeaveFFIHandle(result));", - ); - assert_has( - &out, - "internal static extern IntPtr weaveffi_bus_maybe_ticker(IntPtr input, ref WeaveFFIError out_err);", - ); -} - -#[test] -fn objects_in_records_use_cloned_tokens_and_adopt_on_read() { - let out = render(); - assert_has(&out, "public sealed class Bundle"); - assert_has(&out, "public Ticker Primary { get; }"); - assert_has(&out, "public Ticker[] Others { get; }"); - // Writing clones through the interface's clone symbol. - assert_has(&out, "writer.WriteObject(Primary.CloneHandle());"); - assert_has(&out, "writer.WriteObject(item0.CloneHandle());"); - // Reading adopts the token into a fresh wrapper. - assert_has( - &out, - "var fPrimary = new Ticker(new WeaveFFIHandle(reader.ReadObject()));", - ); - assert_has( - &out, - "var fOthersItem = new Ticker(new WeaveFFIHandle(reader.ReadObject()));", - ); - // The runtime codec helpers exist and reject a null token. - assert_has(&out, "internal void WriteObject(IntPtr token)"); - assert_has(&out, "internal IntPtr ReadObject()"); - assert_has(&out, "null object token"); -} - -#[test] -fn iterator_of_objects_adopts_each_element() { - let out = render(); - assert_has(&out, "public static IEnumerable Tickers()"); - assert_has( - &out, - "if (NativeMethods.weaveffi_bus_TickersIterator_next(iter, out var out_item, ref iterErr) == 0)", - ); - assert_has( - &out, - "yield return new Ticker(new WeaveFFIHandle(out_item));", - ); - assert_has( - &out, - "NativeMethods.weaveffi_bus_TickersIterator_destroy(iter);", - ); -} - -#[test] -fn async_object_result_is_adopted() { - let out = render(); - assert_has(&out, "public static async Task Spawn()"); - assert_has( - &out, - "tcs.SetResult(new Ticker(new WeaveFFIHandle(result)));", - ); - assert_has( - &out, - "NativeMethods.weaveffi_bus_spawn_async(callback, ctx);", - ); -} - -#[test] -fn callback_interface_renders_interface_vtable_and_trampolines() { - let out = render(); - assert_has(&out, "using System.Runtime.CompilerServices;"); - // The consumer-facing interface. - assert_has(&out, "public interface ISubscriber"); - assert_has( - &out, - "bool OnMessage(string text, int weight, Bundle bundle, Ticker ticker);", - ); - assert_has(&out, "void OnClose(long code);"); - - // One static vtable, method order then `free`, allocated once. - assert_has( - &out, - "internal static unsafe class WeaveFFIVtable_bus_Subscriber", - ); - assert_has(&out, "[StructLayout(LayoutKind.Sequential)]"); - let layout_start = out.find("private struct Layout").expect("layout struct"); - let layout = &out[layout_start..]; - let on_message = layout.find("public IntPtr on_message;").unwrap(); - let on_close = layout.find("public IntPtr on_close;").unwrap(); - let free = layout.find("public IntPtr free;").unwrap(); - assert!(on_message < on_close && on_close < free); - assert_has( - &out, - "internal static readonly IntPtr Pointer = Allocate();", - ); - assert_has( - &out, - "on_message = (IntPtr)(delegate* unmanaged[Cdecl])&OnMessageTrampoline,", - ); - assert_has( - &out, - "on_close = (IntPtr)(delegate* unmanaged[Cdecl])&OnCloseTrampoline,", - ); - assert_has( - &out, - "free = (IntPtr)(delegate* unmanaged[Cdecl])&FreeTrampoline,", - ); - assert_has( - &out, - "var mem = Marshal.AllocHGlobal(Marshal.SizeOf());", - ); - - // Trampolines: borrowed string and buffer decoded, object adopted, - // direct return written through, failures reported via error_set. - assert_has( - &out, - "[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]", - ); - assert_has( - &out, - "private static byte OnMessageTrampoline(IntPtr ctx, IntPtr text, int weight, IntPtr bundle_ptr, UIntPtr bundle_len, IntPtr ticker, IntPtr out_err)", - ); - assert_has(&out, "var impl = Target(ctx);"); - assert_has(&out, "var arg2Buf = new byte[(int)bundle_len];"); - assert_has(&out, "var arg2Reader = new WeaveFFIBufferReader(arg2Buf);"); - assert_has( - &out, - "return (byte)(impl.OnMessage(Marshal.PtrToStringUTF8(text) ?? \"\", weight, arg2, new Ticker(new WeaveFFIHandle(ticker))) ? 1 : 0);", - ); - assert_has( - &out, - "private static void OnCloseTrampoline(IntPtr ctx, long code, IntPtr out_err)", - ); - assert_has(&out, "impl.OnClose(code);"); - assert_has( - &out, - "NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message);", - ); - assert_has(&out, "return default;"); - assert_has(&out, "public const int ForeignErrorCode = -4;"); - // No string or buffer is ever freed inside a trampoline. - let tramp_start = out.find("private static byte OnMessageTrampoline").unwrap(); - let tramp_end = out[tramp_start..].find("FreeTrampoline").unwrap() + tramp_start; - let tramp = &out[tramp_start..tramp_end]; - assert!(!tramp.contains("weaveffi_free_"), "{tramp}"); - - // `free` releases the GCHandle. - assert_has(&out, "private static void FreeTrampoline(IntPtr ctx)"); - assert_has(&out, "GCHandle.FromIntPtr(ctx).Free();"); - - // The error_set runtime symbol is bound. - assert_has( - &out, - "internal static extern void weaveffi_error_set(IntPtr err, int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string message);", - ); -} - -#[test] -fn passing_a_callback_interface_registers_a_gchandle_and_the_static_vtable() { - let out = render(); - assert_has(&out, "public static void Subscribe(ISubscriber listener)"); - assert_has( - &out, - "var listenerCtx = GCHandle.ToIntPtr(GCHandle.Alloc(listener));", - ); - assert_has( - &out, - "NativeMethods.weaveffi_bus_subscribe(listenerCtx, WeaveFFIVtable_bus_Subscriber.Pointer, ref err);", - ); - assert_has( - &out, - "internal static extern void weaveffi_bus_subscribe(IntPtr listener_ctx, IntPtr listener_vtable, ref WeaveFFIError out_err);", - ); -} - -#[test] -fn legacy_runtime_surface_is_gone() { - let out = render(); - for legacy in [ - "arena", - "weaveffi_handle_t", - "Handle(IntPtr raw)", - "_listenerRefs", - ] { - assert!(!out.contains(legacy), "found legacy `{legacy}` in:\n{out}"); - } - assert_has(&out, "internal const uint AbiVersion = 2;"); -} - -#[test] -fn output_is_deterministic() { - assert_eq!(render(), render()); -} - -#[test] -fn package_skips_platforms_without_a_nuget_rid() { - let api = fixture(); - let model = BindingModel::build(&api, "weaveffi"); - let mut binaries = BinarySet::new("bus"); - for p in Platform::ALL { - binaries.insert(p, format!("/tmp/{}/lib", p.id())); - } - let ctx = PackageContext { - binaries: &binaries, - input_basename: Some("bus"), - }; - let files = DotnetGenerator - .package( - &api, - &model, - &ctx, - Utf8Path::new("out"), - &DotnetConfig::default(), - ) - .expect("dotnet packages"); - let paths: Vec = files.iter().map(|f| f.path.to_string()).collect(); - let native: Vec<&String> = paths.iter().filter(|p| p.contains("/runtimes/")).collect(); - assert_eq!(native.len(), Platform::DESKTOP.len(), "{paths:?}"); - for rid in [ - "osx-arm64", - "osx-x64", - "linux-x64", - "linux-arm64", - "win-x64", - ] { - assert!( - paths - .iter() - .any(|p| p.contains(&format!("/runtimes/{rid}/native/"))), - "{paths:?}" - ); - } - assert!(!paths - .iter() - .any(|p| p.contains("android") || p.contains("wasm"))); -} diff --git a/crates/weaveffi-gen-dotnet/src/types.rs b/crates/weaveffi-gen-dotnet/src/types.rs deleted file mode 100644 index aee539b5..00000000 --- a/crates/weaveffi-gen-dotnet/src/types.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! C# type mapping: the idiomatic surface type and the P/Invoke spelling of -//! every IR type, plus identifier escaping and string-literal escaping. - -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; - -/// The C# interface a consumer implements for one callback interface: the -/// bare local name with the conventional `I` prefix (`ISubscriber` for -/// `Subscriber`, or for a cross-module `bus.Subscriber`). -pub(crate) fn callback_interface_cs(name: &str) -> String { - format!("I{}", local_type_name(name)) -} - -/// The internal static class hosting one callback interface's vtable and -/// trampolines, named from the C module path and the bare name so two -/// modules declaring the same callback interface name never collide. -pub(crate) fn vtable_class_cs(module_path: &str, name: &str) -> String { - format!("WeaveFFIVtable_{module_path}_{name}") -} - -/// The idiomatic C# surface type for one IR type, as it appears in wrapper -/// signatures, properties, and locals. -pub(crate) fn cs_type(ty: &Ty) -> String { - match ty { - 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 => "string".into(), - Ty::Bytes => "byte[]".into(), - // Records are plain data classes; rich enums are abstract sum types. - // Both are value types decoded from value buffers. - 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::Enum(name) => format!("{}?", local_type_name(name)), - Ty::StringUtf8 => "string?".into(), - Ty::Record(name) | Ty::RichEnum(name) => { - format!("{}?", local_type_name(name)) - } - // `Interface?` and nested composites are nullable reference types. - _ => format!("{}?", cs_type(inner)), - }, - Ty::List(inner) => format!("{}[]", cs_type(inner)), - Ty::Iterator(inner) => format!("IEnumerable<{}>", cs_type(inner)), - Ty::Map(k, v) => format!("Dictionary<{}, {}>", cs_type(k), cs_type(v)), - // Interfaces surface as their reference-counted wrapper class; a - // cross-module reference (`kv.Store`) uses the bare local name. - Ty::Interface(name) => local_type_name(name).into(), - Ty::CallbackInterface(name) => callback_interface_cs(name), - } -} - -/// 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: &Ty) -> String { - match ty { - 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. - Ty::Bool => "byte".into(), - Ty::StringUtf8 - | Ty::Bytes - | Ty::Record(_) - | Ty::RichEnum(_) - | Ty::Interface(_) - | Ty::CallbackInterface(_) - | Ty::Optional(_) - | Ty::List(_) - | Ty::Iterator(_) - | Ty::Map(_, _) => "IntPtr".into(), - Ty::Enum(_) => "int".into(), - } -} - -/// Maps a shared ABI [`CType`] to its P/Invoke spelling. All pointers collapse -/// to `IntPtr`; `size_t` becomes `UIntPtr`. The structural lowering (which slots -/// exist, in what order) comes from [`weaveffi_core::abi`]. -pub(crate) fn cs_pinvoke_ctype(ty: &CType) -> String { - match ty { - CType::Int32 | CType::Enum { .. } => "int".into(), - // C `bool` is one byte on every supported ABI. - CType::Bool => "byte".into(), - CType::Uint32 => "uint".into(), - CType::Int64 => "long".into(), - CType::Uint64 => "ulong".into(), - CType::Double => "double".into(), - CType::Float => "float".into(), - CType::Size => "UIntPtr".into(), - CType::Void => "void".into(), - CType::Int8 => "sbyte".into(), - CType::Int16 => "short".into(), - CType::Uint8 => "byte".into(), - CType::Uint16 => "ushort".into(), - CType::Char => "sbyte".into(), - CType::Ptr { .. } - | CType::StructTag { .. } - | CType::VtableTag { .. } - | CType::CancelToken - | CType::Error - | CType::Named(_) => "IntPtr".into(), - } -} - -/// Renders a return out-param. C# expresses the trailing pointer level of a -/// `T*` out-slot with the `out` keyword on the pointee value type. -pub(crate) fn cs_out_param(p: &AbiParam) -> String { - let pointee = match &p.ty { - CType::Ptr { pointee, .. } => cs_pinvoke_ctype(pointee), - other => cs_pinvoke_ctype(other), - }; - format!("out {} {}", pointee, safe_cs_name(&p.name)) -} - -/// True when `ty` surfaces as a C# value type, so its optional wrapper is -/// `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: &Ty) -> bool { - matches!( - ty, - Ty::I8 - | Ty::I16 - | Ty::I32 - | Ty::U8 - | Ty::U16 - | Ty::U32 - | Ty::I64 - | Ty::U64 - | Ty::F32 - | Ty::F64 - | Ty::Bool - | Ty::Enum(_) - ) -} - -/// Escapes a string for embedding in a C# string literal. -pub(crate) fn cs_str(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") -} - -/// Escape `name` when C# reserves it, using the `@` verbatim-identifier -/// prefix the crate has always emitted. The keyword table is the shared -/// [`lang::CSHARP_KEYWORDS`], so coverage can't drift from the other -/// backends. -pub(crate) fn safe_cs_name(name: &str) -> String { - if lang::is_reserved(name, lang::CSHARP_KEYWORDS) { - format!("@{name}") - } else { - name.to_string() - } -} - -/// A copy of `f` whose parameter names are lowerCamelCase, the C# parameter -/// convention for public wrapper signatures. Only the wrapper signature and -/// its marshalling locals derive from these names; ABI slot names and the -/// P/Invoke declarations keep the IDL spelling. -pub(crate) fn camel_fn(f: &FnBinding) -> FnBinding { - let mut f = f.clone(); - for p in &mut f.params { - p.name = p.name.to_lower_camel_case(); - } - f -} diff --git a/crates/weaveffi-gen-go/Cargo.toml b/crates/weaveffi-gen-go/Cargo.toml deleted file mode 100644 index dfe45d24..00000000 --- a/crates/weaveffi-gen-go/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "weaveffi-gen-go" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "WeaveFFI Go CGo binding generator" -keywords = ["ffi", "codegen", "bindings", "go", "cgo"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-go/src/calls.rs b/crates/weaveffi-gen-go/src/calls.rs deleted file mode 100644 index d46eaf2d..00000000 --- a/crates/weaveffi-gen-go/src/calls.rs +++ /dev/null @@ -1,1142 +0,0 @@ -//! Call rendering: sync, async, and iterator wrappers, callback-interface -//! types and trampolines, and the argument/return marshalling they share. -//! -//! 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 `Ty`. - -use heck::ToUpperCamelCase; -use weaveffi_core::abi::{AbiParam, CType}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::lang; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{ - AsyncBinding, BindingModel, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, - FnBinding, IteratorBinding, ModuleBinding, ParamBinding, -}; -use weaveffi_core::plan::{self, ArgPass, ErrorStrategy, RetPass}; -use weaveffi_core::utils::c_abi_struct_name; - -use crate::codec::{emit_buffer_read, emit_buffer_write}; -use crate::docs::{emit_doc, emit_fn_doc}; -use crate::types::{ - c_scalar_conv, cgo_slot_type, go_adopt_expr, go_param_ident, go_scalar_conv, go_type, go_zero, - strip_const, vtable_accessor, vtable_var, -}; - -// ── Errors ── - -/// How a wrapper body reports a non-zero `weaveffi_error` slot. -/// -/// A callable with `throws == true` returns `(T, error)` and maps codes -/// through the declaring module's typed helper (`wvMapKv`), falling back to -/// the generic [`ERROR_BRAND`](weaveffi_core::errors::ERROR_BRAND) struct -/// when no domain is in scope. A callable with `throws == false` has a plain -/// signature and panics via `wvTrap` instead, since a reported error can only -/// be a producer panic, an argument-marshalling failure, or a callback -/// implementation that panicked. -#[derive(Clone, Copy)] -pub(crate) struct ErrCtx<'a> { - /// `true` when the wrapper returns `(T, error)` and surfaces typed errors. - pub(crate) throws: bool, - /// PascalCase stem of the domain in effect (`Kv` names `wvMapKv`); `None` - /// falls back to the generic `wvBrandError` constructor. - stem: Option<&'a str>, -} - -impl<'a> ErrCtx<'a> { - /// Build the wrapper error context for `f` from the shared plan's - /// [`ErrorStrategy`]: a `Throws` callable returns `(T, error)` through - /// `stem`'s typed domain, a `Trap` callable panics via `wvTrap`. - pub(crate) fn of(f: &FnBinding, stem: Option<&'a str>) -> Self { - Self { - throws: matches!(f.error_strategy(), ErrorStrategy::Throws), - stem, - } - } - - /// The Go expression converting a taken `(code, message, payload)` triple - /// into an `error` value. - pub(crate) fn map_call(&self, args: &str) -> String { - match self.stem { - Some(stem) => format!("wvMap{stem}({args})"), - None => format!("wvBrandError({args})"), - } - } - - /// Emit the statement(s) checking the error slot named `slot` at `w`'s - /// current depth. A throwing wrapper returns `zero` (when the function - /// has a result) plus the mapped error; a plain wrapper traps. - fn emit_check(&self, w: &mut CodeWriter, slot: &str, zero: Option<&str>) { - if self.throws { - let map = self.map_call(&format!("wvTakeError(&{slot})")); - w.block(format!("if {slot}.code != 0 {{"), "}", |w| { - match zero { - Some(z) => w.line(format!("return {z}, {map}")), - None => w.line(format!("return {map}")), - }; - }); - } else { - w.line(format!("wvTrap(&{slot})")); - } - } - - /// 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 { - match (ret, self.throws) { - (Some(r), true) => format!(" ({}, error)", go_type(r)), - (Some(r), false) => format!(" {}", go_type(r)), - (None, true) => " error".into(), - (None, false) => String::new(), - } - } - - /// The suffix appended to every successful `return` statement: `, nil` - /// when the wrapper also returns an error, empty otherwise. - fn ok_tail(&self) -> &'static str { - if self.throws { - ", nil" - } else { - "" - } - } -} - -// ── Trampolines and the cgo preamble ── - -/// The Go spelling of one C ABI slot name inside an exported trampoline's -/// formal list: a slot named after a Go keyword (an IDL parameter called -/// `type`, `func`, `range`) gains the trailing-underscore escape. The same -/// spelling is used in the preamble `extern` so the two prototypes agree. -fn go_slot_ident(name: &str) -> String { - lang::escape_ident(name, lang::GO_KEYWORDS) -} - -/// The C name of the exported Go trampoline for an async completion typedef. -pub(crate) fn trampoline_name(c_type_name: &str) -> String { - format!("goWv_{c_type_name}") -} - -/// The C name of the exported Go trampoline behind one callback-interface -/// vtable entry (`goWv_{c_tag}_{method}`); `free` names the trailing entry. -fn cb_trampoline_name(c_tag: &str, method: &str) -> String { - format!("goWv_{c_tag}_{method}") -} - -/// The preamble `extern` declaration for one exported trampoline. Pointer -/// types are rendered const-free to match the prototypes cgo writes into -/// `_cgo_export.h` from the Go signature. -fn extern_decl(name: &str, ret: &CType, params: &[AbiParam], prefix: &str) -> String { - let args: Vec = params - .iter() - .map(|p| { - format!( - "{} {}", - strip_const(&p.ty).render_c(prefix), - go_slot_ident(&p.name) - ) - }) - .collect(); - format!( - "extern {} {name}({});", - ret.render_c(prefix), - args.join(", ") - ) -} - -/// The preamble definition of the one process-wide static vtable for `cb`: -/// one trampoline per method in declaration order, then `free`. An entry -/// whose C signature carries `const` pointers is cast back to the vtable's -/// exact field type, since the exported Go function is declared const-free. -fn vtable_def(cb: &CallbackInterfaceBinding, prefix: &str) -> String { - let mut s = format!( - "static const {} {} = {{\n", - cb.vtable_tag, - vtable_var(&cb.c_tag) - ); - for m in &cb.methods { - let tramp = cb_trampoline_name(&cb.c_tag, &m.name); - let needs_cast = m.abi_params.iter().any(|p| strip_const(&p.ty) != p.ty); - if needs_cast { - let types: Vec = m.abi_params.iter().map(|p| p.ty.render_c(prefix)).collect(); - s.push_str(&format!( - " ({} (*)({})){tramp},\n", - m.abi_ret.render_c(prefix), - types.join(", ") - )); - } else { - s.push_str(&format!(" {tramp},\n")); - } - } - s.push_str(&format!( - " {},\n}};\n", - cb_trampoline_name(&cb.c_tag, "free") - )); - // cgo reaches a C variable through `//go:cgo_import_static`, which needs - // external linkage, so a `static const` table can't be named as - // `&C.wvVtable_...` from Go. A static accessor function is callable - // through the ordinary cgo stub in the same translation unit. - s.push_str(&format!( - "static const {}* {}(void) {{ return &{}; }}", - cb.vtable_tag, - vtable_accessor(&cb.c_tag), - vtable_var(&cb.c_tag) - )); - s -} - -/// Every declaration the cgo preamble needs beyond the header include: for -/// each callback interface, the `extern` prototypes of its method and `free` -/// trampolines followed by its static vtable and accessor; then one `extern` -/// per async completion callback, including async interface members. -/// -/// A file that uses `//export` may only put declarations in its preamble -/// (the preamble is compiled into two C translation units); the vtable is a -/// `static const` so each unit gets a private copy and no symbol is -/// duplicated. Go takes the address of the copy in its own unit through the -/// static accessor, so the producer always sees one vtable whose entries -/// live for the process. -pub(crate) fn collect_preamble_decls(model: &BindingModel, prefix: &str) -> Vec { - let mut decls = Vec::new(); - for m in &model.modules { - for cb in &m.callback_interfaces { - for meth in &cb.methods { - decls.push(extern_decl( - &cb_trampoline_name(&cb.c_tag, &meth.name), - &meth.abi_ret, - &meth.abi_params, - prefix, - )); - } - decls.push(extern_decl( - &cb_trampoline_name(&cb.c_tag, "free"), - &CType::Void, - &[AbiParam::new("ctx", CType::ptr(CType::Void))], - prefix, - )); - decls.push(vtable_def(cb, prefix)); - } - for f in m.callables() { - if let CallShape::Async(ab) = &f.shape { - decls.push(extern_decl( - &trampoline_name(&ab.callback_type), - &CType::Void, - &ab.callback_params, - prefix, - )); - } - } - } - decls -} - -// ── Callback interfaces ── - -/// The Go method signature of one callback-interface method, as it appears -/// in the consumer-implemented interface type: `OnMessage(text string, -/// weight int32) int64`. -fn cb_method_sig(m: &CallbackMethodBinding) -> String { - let params: Vec = m - .params - .iter() - .map(|p| format!("{} {}", go_param_ident(&p.name), go_type(&p.ty))) - .collect(); - let ret = match &m.ret { - Some(ty) => format!(" {}", go_type(ty)), - None => String::new(), - }; - format!( - "{}({}){ret}", - m.name.to_upper_camel_case(), - params.join(", ") - ) -} - -/// Emit statements converting one callback-method parameter's C slots into -/// a Go value bound to `arg{idx}`, returning that local's name. -/// -/// Strings, bytes, and buffers arriving in a trampoline are borrowed for the -/// dispatch: they are copied or decoded and nothing is freed. An object -/// argument transfers one strong reference, which is adopted into a wrapper -/// (a null `Interface?` adopts to nil). -fn emit_cb_param_arg(out: &mut String, idx: usize, p: &ParamBinding) -> String { - let arg = format!("arg{idx}"); - let mut w = CodeWriter::tabs().with_depth(1); - match p.arg_pass() { - ArgPass::Buffer { ptr, len } => { - w.line(format!( - "rArg{idx} := &wvReader{{buf: wvBorrowBuffer({}, {})}}", - go_slot_ident(&ptr.name), - go_slot_ident(&len.name) - )); - w.line(format!("var {arg} {}", go_type(&p.ty))); - emit_buffer_read( - &mut w, - &format!("rArg{idx}"), - &arg, - &p.ty, - &format!("Arg{idx}"), - 0, - ); - w.line(format!("rArg{idx}.expectEnd()")); - } - ArgPass::String { slot } => { - let n = go_slot_ident(&slot.name); - w.line(format!("{arg} := \"\"")); - w.block(format!("if {n} != nil {{"), "}", |w| { - w.line(format!("{arg} = C.GoString({n})")); - }); - } - ArgPass::Bytes { ptr, len } => { - let pn = go_slot_ident(&ptr.name); - let ln = go_slot_ident(&len.name); - w.line(format!("var {arg} []byte")); - w.block(format!("if {pn} != nil {{"), "}", |w| { - w.line(format!( - "{arg} = C.GoBytes(unsafe.Pointer({pn}), C.int({ln}))" - )); - }); - } - ArgPass::Object { slot, .. } => { - w.line(format!( - "{arg} := {}", - go_adopt_expr(&p.ty, &go_slot_ident(&slot.name)) - )); - } - ArgPass::Direct { slot } => { - let n = go_slot_ident(&slot.name); - w.line(format!("{arg} := {}", go_scalar_conv(&n, &p.ty))); - } - ArgPass::Callback { .. } => { - unreachable!("validation rejects callback interfaces as callback-method parameters") - } - } - out.push_str(&w.finish()); - arg -} - -/// Emit the exported trampoline behind one vtable entry. It recovers the -/// implementation from the `cgo.Handle` passed as `ctx`, converts the -/// borrowed arguments, calls the Go method, and writes a direct-family -/// result into the C return. A panic in the implementation is recovered, -/// reported through `weaveffi_error_set(out_err, -4, message)`, and the -/// zero value is returned; nothing ever unwinds through the C frame. -fn render_cb_trampoline( - w: &mut CodeWriter, - prefix: &str, - module: &str, - cb: &CallbackInterfaceBinding, - m: &CallbackMethodBinding, - iface_name: &str, -) { - let tramp = cb_trampoline_name(&cb.c_tag, &m.name); - let formals: Vec = m - .abi_params - .iter() - .map(|s| { - format!( - "{} {}", - go_slot_ident(&s.name), - cgo_slot_type(&s.ty, prefix) - ) - }) - .collect(); - let ret_sig = match &m.ret { - Some(_) => format!(" (ret {})", cgo_slot_type(&m.abi_ret, prefix)), - None => String::new(), - }; - w.line(format!("//export {tramp}")); - w.block( - format!("func {tramp}({}){ret_sig} {{", formals.join(", ")), - "}", - |w| { - w.block("defer func() {", "}()", |w| { - w.block("if r := recover(); r != nil {", "}", |w| { - w.line("wvForeignError(out_err, r)"); - }); - }); - w.line(format!( - "impl := cgo.Handle(uintptr(ctx)).Value().({iface_name})" - )); - let mut args = Vec::new(); - for (idx, p) in m.params.iter().enumerate() { - let mut body = String::new(); - args.push(emit_cb_param_arg(&mut body, idx, p)); - w.raw(body); - } - let call = format!("impl.{}({})", m.name.to_upper_camel_case(), args.join(", ")); - match &m.ret { - Some(ty) => { - w.line(format!( - "ret = {}", - c_scalar_conv(&call, ty, prefix, module) - )); - w.line("return"); - } - None => { - w.line(call); - } - } - }, - ); - w.blank(); -} - -/// Render one callback interface: the Go `interface` type the consumer -/// implements (one method per IDL method, PascalCase, direct-family or void -/// returns), the exported trampoline behind each vtable entry, and the -/// `free` trampoline that deletes the `cgo.Handle` once the producer drops -/// its last reference to the callback. -/// -/// Passing an implementation to a producer function stores it in a -/// `cgo.Handle` (the `void* ctx` slot) and passes the address of the -/// interface's static vtable from the cgo preamble (see -/// [`collect_preamble_decls`]). The producer may invoke any trampoline from -/// any thread; cgo attaches the calling thread to the Go runtime. -pub(crate) fn render_callback_interface( - out: &mut String, - prefix: &str, - module: &ModuleBinding, - cb: &CallbackInterfaceBinding, -) { - let name = cb.name.to_upper_camel_case(); - let mut w = CodeWriter::tabs(); - let mut d = String::new(); - emit_doc(&mut d, &cb.doc, "", Some(&name)); - if d.is_empty() { - w.line(format!( - "// {name} is a callback interface: implement it in Go and pass the value to" - )); - w.line("// native functions that accept it."); - } else { - w.raw(d); - w.line("//"); - w.line("// Implement this interface in Go and pass the value to native functions"); - w.line("// that accept it."); - } - w.line("//"); - w.line("// The native library may call any method from any thread until it releases"); - w.line("// the implementation. A panic in a method is reported to the native caller"); - w.line("// as a foreign error (code -4) instead of crashing the process."); - if let Some(msg) = &cb.deprecated { - w.line("//"); - w.line(format!("// Deprecated: {msg}")); - } - w.block(format!("type {name} interface {{"), "}", |w| { - for m in &cb.methods { - let mut md = String::new(); - emit_fn_doc( - &mut md, - &m.doc, - &m.params, - "\t", - &m.name.to_upper_camel_case(), - ); - w.raw(md); - if let Some(msg) = &m.deprecated { - w.line(format!("// Deprecated: {msg}")); - } - w.line(cb_method_sig(m)); - } - }); - w.blank(); - - for m in &cb.methods { - render_cb_trampoline(&mut w, prefix, &module.path, cb, m, &name); - } - - let free = cb_trampoline_name(&cb.c_tag, "free"); - w.line(format!("//export {free}")); - w.block(format!("func {free}(ctx unsafe.Pointer) {{"), "}", |w| { - w.line("cgo.Handle(uintptr(ctx)).Delete()"); - }); - w.blank(); - out.push_str(&w.finish()); -} - -// ── Async ── - -/// The per-async-function outcome payload type name, derived from the -/// (unique) base C symbol with the ABI prefix dropped: free function -/// `weaveffi_io_read` names `wvOutcomeIoRead`, interface member -/// `weaveffi_kv_Store_compact` names `wvOutcomeKvStoreCompact`. -fn async_outcome_type(prefix: &str, f: &FnBinding) -> String { - let base = f - .c_base - .strip_prefix(&format!("{prefix}_")) - .unwrap_or(&f.c_base); - format!("wvOutcome{}", base.to_upper_camel_case()) -} - -/// Send the converted async result over the outcome channel. Runs inside the -/// completion trampoline after the error path has been handled. -/// -/// Result buffers (strings, bytes, value buffers) are owned by the consumer -/// per the shared async protocol: they are copied or decoded here and then -/// released through the runtime free symbols. An owned object result -/// transfers one strong reference, adopted into a wrapper. -fn emit_async_result_send( - out: &mut String, - ret: &Option, - outcome: &str, - prefix: &str, - module: &str, -) { - let mut w = CodeWriter::tabs().with_depth(1); - let Some(ty) = ret else { - w.line(format!("ch <- {outcome}{{}}")); - out.push_str(&w.finish()); - return; - }; - match plan::ret_pass(Some(ty), module, prefix) { - RetPass::Void => unreachable!("a present return type is never void"), - RetPass::Buffer => { - // Owned by the consumer: wvCopyBuffer copies, then frees. - w.line("rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)}"); - w.line(format!("var val {}", go_type(ty))); - emit_buffer_read(&mut w, "rRes", "val", ty, "Res", 0); - w.line("rRes.expectEnd()"); - w.line(format!("ch <- {outcome}{{val: val}}")); - } - RetPass::String => { - // Owned by the consumer: copy, then free. - w.line("val := \"\""); - w.block("if result != nil {", "}", |w| { - w.line("val = C.GoString(result)"); - w.line("C.weaveffi_free_string(result)"); - }); - w.line(format!("ch <- {outcome}{{val: val}}")); - } - RetPass::Bytes => { - // Owned by the consumer: copy, then free. - w.line("var val []byte"); - w.block("if result != nil {", "}", |w| { - w.line("val = C.GoBytes(unsafe.Pointer(result), C.int(result_len))"); - w.line("C.weaveffi_free_bytes(result, result_len)"); - }); - w.line(format!("ch <- {outcome}{{val: val}}")); - } - RetPass::Object { .. } => { - w.line(format!( - "ch <- {outcome}{{val: {}}}", - go_adopt_expr(ty, "result") - )); - } - RetPass::Direct => { - w.line(format!( - "ch <- {outcome}{{val: {}}}", - go_scalar_conv("result", ty) - )); - } - } - out.push_str(&w.finish()); -} - -/// An async callable: a blocking Go wrapper that launches the C call with a -/// completion trampoline and waits on a buffered channel, plus the outcome -/// type and the exported trampoline itself. The channel travels to the -/// producer as a `cgo.Handle` in the `context` slot; the trampoline resolves -/// and deletes it, so the completion is delivered exactly once. -/// -/// The error split follows the shared plan's [`ErrorStrategy`]. A throwing -/// wrapper returns `(T, error)` and the trampoline maps a reported error -/// through the domain (`wvMap{Stem}`). A plain wrapper returns bare `T`; a -/// reported error can only be a producer bug, so the trampoline wraps it as -/// the generic brand error (never the typed domain) and the wrapper panics -/// with it on the calling goroutine (the trampoline itself must never panic: -/// it runs on a producer thread entered from C). With `receiver` set, the -/// wrapper is a method on that wrapper type passing `s.ptr` as the leading -/// launch argument and keeping the receiver alive until completion. -#[allow(clippy::too_many_arguments)] -pub(crate) fn render_async_function( - out: &mut String, - prefix: &str, - module: &str, - f: &FnBinding, - ab: &AsyncBinding, - go_name: &str, - receiver: Option<&str>, - err: ErrCtx, -) { - let outcome = async_outcome_type(prefix, f); - let tramp = trampoline_name(&ab.callback_type); - - let mut w = CodeWriter::tabs(); - - // Outcome payload: the converted result (if any) or the producer error. - w.block(format!("type {outcome} struct {{"), "}", |w| { - if let Some(ret) = &f.ret { - w.line(format!("val {}", go_type(ret))); - } - w.line("err error"); - }); - w.blank(); - - // The exported completion trampoline. It always converts a reported error - // into a Go error and sends it over the channel; the wrapper decides - // whether to return or panic with it. - let formals: Vec = ab - .callback_params - .iter() - .map(|s| { - format!( - "{} {}", - go_slot_ident(&s.name), - cgo_slot_type(&s.ty, prefix) - ) - }) - .collect(); - let mut tramp_body = String::new(); - emit_async_result_send(&mut tramp_body, &f.ret, &outcome, prefix, module); - // A non-throwing function's error slot can only carry a producer bug: - // brand it generically rather than dressing it as a typed domain error. - let map_err = if err.throws { - err.map_call("wvTakeBoxedError(err)") - } else { - "wvBrandError(wvTakeBoxedError(err))".to_string() - }; - w.line(format!("//export {tramp}")); - w.block( - format!("func {tramp}({}) {{", formals.join(", ")), - "}", - |w| { - w.line("h := cgo.Handle(uintptr(context))"); - w.line(format!("ch := h.Value().(chan {outcome})")); - w.line("h.Delete()"); - w.block("if err != nil && err.code != 0 {", "}", |w| { - w.line(format!("ch <- {outcome}{{err: {map_err}}}")); - w.line("return"); - }); - w.raw(tramp_body.as_str()); - }, - ); - w.blank(); - - // The blocking wrapper. Cancellation tokens are not surfaced (NULL). - let go_params: Vec = f - .params - .iter() - .map(|p| format!("{} {}", go_param_ident(&p.name), go_type(&p.ty))) - .collect(); - let ret_sig = err.ret_sig(&f.ret); - let mut doc = String::new(); - emit_fn_doc(&mut doc, &f.doc, &f.params, "", go_name); - w.raw(doc); - w.line("// Blocks the calling goroutine until the async producer completes."); - if let Some(msg) = &f.deprecated { - w.line(format!("// Deprecated: {msg}")); - } - - let mut pre = String::new(); - let mut c_args: Vec = Vec::new(); - if receiver.is_some() { - c_args.push("s.ptr".into()); - } - for p in &f.params { - emit_param(&mut pre, &mut c_args, p, prefix, module); - } - if f.cancellable { - c_args.push("nil".into()); - } - c_args.push(format!("C.{}(unsafe.Pointer(C.{tramp}))", ab.callback_type)); - c_args.push("C.wvHandlePtr(C.uintptr_t(h))".into()); - let launch_args = c_args.join(", "); - - let header = match receiver { - Some(ty) => format!( - "func (s *{ty}) {go_name}({}){ret_sig} {{", - go_params.join(", ") - ), - None => format!("func {go_name}({}){ret_sig} {{", go_params.join(", ")), - }; - w.block(header, "}", |w| { - if let Some(ty) = receiver { - w.raw(receiver_guard(ty, "\t")); - } - w.line(format!("ch := make(chan {outcome}, 1)")); - w.line("h := cgo.NewHandle(ch)"); - w.raw(pre.as_str()); - w.line(format!("C.{}({})", ab.launch.symbol, launch_args)); - w.line("outcome := <-ch"); - if err.throws { - if let Some(ret) = &f.ret { - w.block("if outcome.err != nil {", "}", |w| { - w.line(format!("return {}, outcome.err", go_zero(ret))); - }); - w.line("return outcome.val, nil"); - } else { - w.line("return outcome.err"); - } - } else { - w.block("if outcome.err != nil {", "}", |w| { - w.line("panic(outcome.err)"); - }); - if f.ret.is_some() { - w.line("return outcome.val"); - } - } - }); - w.blank(); - out.push_str(&w.finish()); -} - -// ── Functions ── - -/// A sync or iterator callable: the Go wrapper marshalling parameters in, -/// invoking the C symbol, checking the error slot per `err` (typed -/// `(T, error)` when throwing, `wvTrap` panic when plain), and converting the -/// result out. An iterator-returning callable renders through -/// [`render_iterator_fn`] as a lazy sequence instead. With `receiver` set, -/// the wrapper is a method on that wrapper type passing `s.ptr` as the -/// leading C argument and keeping the receiver alive across the call. -#[allow(clippy::too_many_arguments)] -pub(crate) fn render_function( - out: &mut String, - prefix: &str, - module: &str, - f: &FnBinding, - go_name: &str, - receiver: Option<&str>, - err: ErrCtx, -) { - if let CallShape::Iterator(ib) = &f.shape { - render_iterator_fn(out, prefix, module, f, ib, go_name, receiver, err); - return; - } - - let c_sym = &f.c_base; - - let go_params: Vec = f - .params - .iter() - .map(|p| format!("{} {}", go_param_ident(&p.name), go_type(&p.ty))) - .collect(); - - let ret_sig = err.ret_sig(&f.ret); - let header = match receiver { - Some(ty) => format!( - "func (s *{ty}) {go_name}({}){ret_sig} {{", - go_params.join(", ") - ), - None => format!("func {go_name}({}){ret_sig} {{", go_params.join(", ")), - }; - - let mut w = CodeWriter::tabs(); - let mut doc = String::new(); - emit_fn_doc(&mut doc, &f.doc, &f.params, "", go_name); - w.raw(doc); - if let Some(msg) = &f.deprecated { - w.line(format!("// Deprecated: {msg}")); - } - - let mut pre = String::new(); - let mut c_args: Vec = Vec::new(); - if let Some(ty) = receiver { - pre.push_str(&receiver_guard(ty, "\t")); - c_args.push("s.ptr".into()); - } - - for p in &f.params { - emit_param(&mut pre, &mut c_args, p, prefix, module); - } - - if let Some(ref ret) = f.ret { - emit_return_out_params(&mut pre, &mut c_args, ret, prefix, module); - } - - pre.push_str("\tvar cErr C.weaveffi_error\n"); - c_args.push("&cErr".into()); - - let args = c_args.join(", "); - - w.block(header, "}", |w| { - w.raw(pre.as_str()); - - if f.ret.is_some() { - w.line(format!("result := C.{c_sym}({args})")); - } else { - w.line(format!("C.{c_sym}({args})")); - } - - err.emit_check(w, "cErr", f.ret.as_ref().map(go_zero).as_deref()); - - if let Some(ref ret) = f.ret { - let mut tail = String::new(); - emit_return(&mut tail, ret, prefix, module, err.ok_tail()); - w.raw(tail); - } else if err.throws { - w.line("return nil"); - } - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// Go type of the `out_item` local whose address is passed to an iterator's -/// `next`: the pointee of the `T* out_item` slot, so one indirection less -/// than the C slot. Strings arrive as `*C.char`, bytes and buffered elements -/// as a `*C.uint8_t` buffer pointer, objects as `*C.{tag}`, and direct -/// values as their scalar C type. -fn iter_out_item_type(ib: &IteratorBinding, prefix: &str) -> String { - let CType::Ptr { pointee, .. } = &ib.next.params[1].ty else { - unreachable!("an iterator's out_item slot is always a pointer") - }; - cgo_slot_type(pointee, prefix) -} - -/// Re-indent `block` by one tab per non-empty line, used to move depth-1 -/// staging code inside the sequence closure body. -fn indent_block(block: &str) -> String { - let mut out = String::with_capacity(block.len()); - for line in block.lines() { - if line.is_empty() { - out.push('\n'); - } else { - out.push('\t'); - out.push_str(line); - out.push('\n'); - } - } - out -} - -/// Emit the statements converting one freshly-pulled `next` slot (`outItem`, -/// plus `outLen` for bytes/buffered elements) into a Go value bound to -/// `item`, receiving it per the protocol's [`RetPass`] plan: strings are -/// copied then freed, bytes and buffered elements are copied/decoded and -/// released with `weaveffi_free_bytes` (via `wvCopyBuffer`), objects are -/// adopted into a wrapper, and by-value elements owe nothing. -fn emit_iter_elem_bind(w: &mut CodeWriter, inner: &Ty, elem: &RetPass) { - match elem { - RetPass::Void => unreachable!("an iterator element is never void"), - RetPass::String => { - w.line("item := C.GoString(outItem)"); - w.line("C.weaveffi_free_string(outItem)"); - } - RetPass::Bytes => { - w.line("item := wvCopyBuffer(outItem, outLen)"); - } - RetPass::Buffer => { - w.line("rItem := &wvReader{buf: wvCopyBuffer(outItem, outLen)}"); - w.line(format!("var item {}", go_type(inner))); - emit_buffer_read(w, "rItem", "item", inner, "Item", 0); - w.line("rItem.expectEnd()"); - } - RetPass::Object { .. } => { - w.line(format!("item := {}", go_adopt_expr(inner, "outItem"))); - } - RetPass::Direct => { - w.line(format!("item := {}", go_scalar_conv("outItem", inner))); - } - } -} - -/// An `iter`-returning callable, rendered per the shared -/// [`weaveffi_core::plan::IteratorProtocol`] pull contract as Go's standard -/// lazy iteration idiom (the `iter` package, Go 1.23+): -/// -/// - A non-throwing function returns `iter.Seq[T]`. A launch or per-`next` -/// error can only be a producer bug, so it panics with the weaveffi -/// message via `wvTrap` ([`ErrorStrategy::Trap`]). -/// - A throwing function returns `iter.Seq2[T, error]`. A launch or -/// per-`next` domain error is yielded as the final `(zero, err)` pair and -/// iteration stops ([`ErrorStrategy::Throws`]). -/// -/// The producer iterator is launched lazily inside the returned closure, so -/// an unused sequence allocates nothing on the producer side. One C `next` -/// call runs per consumer step, each yielded element is received per the -/// protocol's element [`RetPass`] after conversion, and the destroy runs -/// exactly once through a `defer` inside the closure, whether the sequence -/// is exhausted, stops on an error, or is abandoned by an early `break`. -#[allow(clippy::too_many_arguments)] -fn render_iterator_fn( - out: &mut String, - prefix: &str, - module: &str, - f: &FnBinding, - ib: &IteratorBinding, - go_name: &str, - receiver: Option<&str>, - err: ErrCtx, -) { - let proto = ib.protocol(f, module, prefix); - let throws = matches!(proto.error, ErrorStrategy::Throws); - let elem = &ib.elem; - let elem_go = go_type(elem); - let item_ty = iter_out_item_type(ib, prefix); - let has_len = matches!(proto.elem, RetPass::Bytes | RetPass::Buffer); - let zero = go_zero(elem); - - let go_params: Vec = f - .params - .iter() - .map(|p| format!("{} {}", go_param_ident(&p.name), go_type(&p.ty))) - .collect(); - let (seq_ty, yield_ty) = if throws { - ( - format!("iter.Seq2[{elem_go}, error]"), - format!("func({elem_go}, error) bool"), - ) - } else { - ( - format!("iter.Seq[{elem_go}]"), - format!("func({elem_go}) bool"), - ) - }; - let header = match receiver { - Some(ty) => format!( - "func (s *{ty}) {go_name}({}) {seq_ty} {{", - go_params.join(", ") - ), - None => format!("func {go_name}({}) {seq_ty} {{", go_params.join(", ")), - }; - - let mut w = CodeWriter::tabs(); - let mut doc = String::new(); - emit_fn_doc(&mut doc, &f.doc, &f.params, "", go_name); - w.raw(doc); - w.line("// Returns a lazy sequence: the producer iterator is launched on first"); - w.line("// iteration and one producer next call runs per element. The iterator is"); - w.line("// destroyed exactly once, whether the sequence is exhausted or abandoned"); - w.line("// early; each range over the sequence launches a fresh producer iterator."); - if throws { - w.line("// A launch or per-element error is yielded as the final (zero value,"); - w.line("// error) pair, and iteration stops."); - } else { - w.line("// A reported error can only be a producer bug and panics with the"); - w.line("// weaveffi message."); - } - if let Some(msg) = &f.deprecated { - w.line(format!("// Deprecated: {msg}")); - } - - // Parameter staging runs inside the closure so C strings and buffers are - // live at launch time and each range restages them. - let mut pre = String::new(); - let mut c_args: Vec = Vec::new(); - if let Some(ty) = receiver { - pre.push_str(&receiver_guard(ty, "\t")); - c_args.push("s.ptr".into()); - } - for p in &f.params { - emit_param(&mut pre, &mut c_args, p, prefix, module); - } - c_args.push("&cErr".into()); - - // Statements surfacing a non-zero error slot: yield the mapped domain - // error and stop when throwing, trap when plain. - let emit_err_check = |w: &mut CodeWriter, slot: &str| { - if throws { - let map = err.map_call(&format!("wvTakeError(&{slot})")); - w.block(format!("if {slot}.code != 0 {{"), "}", |w| { - w.line(format!("yield({zero}, {map})")); - w.line("return"); - }); - } else { - w.line(format!("wvTrap(&{slot})")); - } - }; - - let next_args = if has_len { - "it, &outItem, &outLen, &iterErr" - } else { - "it, &outItem, &iterErr" - }; - - w.block(header, "}", |w| { - w.block(format!("return func(yield {yield_ty}) {{"), "}", |w| { - w.raw(indent_block(&pre)); - w.line("var cErr C.weaveffi_error"); - w.line(format!( - "it := C.{}({})", - ib.launch.symbol, - c_args.join(", ") - )); - emit_err_check(w, "cErr"); - w.line(format!("defer C.{}(it)", ib.destroy_symbol)); - w.block("for {", "}", |w| { - w.line(format!("var outItem {item_ty}")); - if has_len { - w.line("var outLen C.size_t"); - } - w.line("var iterErr C.weaveffi_error"); - w.line(format!("ok := C.{}({next_args}) != 0", ib.next.symbol)); - emit_err_check(w, "iterErr"); - w.block("if !ok {", "}", |w| { - w.line("return"); - }); - emit_iter_elem_bind(w, elem, &proto.elem); - let yield_call = if throws { - "if !yield(item, nil) {" - } else { - "if !yield(item) {" - }; - w.block(yield_call, "}", |w| { - w.line("return"); - }); - }); - }); - }); - w.blank(); - out.push_str(&w.finish()); -} - -// ── Parameter and return marshalling ── - -/// The statements every method opens with: reject a wrapper whose reference -/// was already released by `Close` (the ABI never accepts a null object -/// there, so a Go panic beats a producer-side abort), then pin the wrapper -/// so its finalizer cannot release the object while the call is in flight. -fn receiver_guard(ty: &str, indent: &str) -> String { - format!( - "{indent}if s.ptr == nil {{\n{indent}\tpanic(\"weaveffi: {ty} used after Close\")\n{indent}}}\n{indent}defer runtime.KeepAlive(s)\n" - ) -} - -/// Emit the staging statements and C argument expressions for one Go -/// parameter, dispatching on the shared [`ArgPass`] contract. -/// -/// A buffered parameter is packed into a `wvWriter` and passed as a borrowed -/// `(ptr, len)` pair; the encoding lives in Go memory kept alive for the -/// duration of the call by cgo's argument-pinning rules. An object parameter -/// passes the wrapper's own pointer (borrowed; the wrapper keeps its -/// reference) and pins the wrapper with `runtime.KeepAlive` so its finalizer -/// cannot release the object mid-call; a nil `Interface?` stages a NULL -/// slot. A callback interface stores the implementation in a `cgo.Handle` -/// passed as `ctx` alongside the address of the interface's static vtable. -fn emit_param( - pre: &mut String, - args: &mut Vec, - p: &ParamBinding, - prefix: &str, - module: &str, -) { - let name = go_param_ident(&p.name); - let mut w = CodeWriter::tabs().with_depth(1); - match p.arg_pass() { - ArgPass::Buffer { .. } => { - let n = name.to_upper_camel_case(); - w.line(format!("w{n} := &wvWriter{{}}")); - emit_buffer_write(&mut w, &format!("w{n}"), &name, &p.ty, &n, 0); - w.line(format!("var c{n}Ptr *C.uint8_t")); - w.block(format!("if len(w{n}.buf) > 0 {{"), "}", |w| { - w.line(format!( - "c{n}Ptr = (*C.uint8_t)(unsafe.Pointer(&w{n}.buf[0]))" - )); - }); - args.push(format!("c{n}Ptr")); - args.push(format!("C.size_t(len(w{n}.buf))")); - } - ArgPass::String { .. } => { - let cv = format!("c{}", name.to_upper_camel_case()); - w.line(format!("{cv} := C.CString({name})")); - w.line(format!("defer C.free(unsafe.Pointer({cv}))")); - args.push(cv); - } - ArgPass::Bytes { .. } => { - let pv = format!("c{}Ptr", name.to_upper_camel_case()); - let lv = format!("c{}Len", name.to_upper_camel_case()); - w.line(format!("var {pv} *C.uint8_t")); - w.line(format!("{lv} := C.size_t(len({name}))")); - w.block(format!("if len({name}) > 0 {{"), "}", |w| { - w.line(format!("{pv} = (*C.uint8_t)(unsafe.Pointer(&{name}[0]))")); - }); - args.push(pv); - args.push(lv); - } - ArgPass::Object { slot, nullable } => { - w.line(format!("defer runtime.KeepAlive({name})")); - if nullable { - let cv = format!("c{}", name.to_upper_camel_case()); - w.line(format!("var {cv} {}", cgo_slot_type(&slot.ty, prefix))); - w.block(format!("if {name} != nil {{"), "}", |w| { - w.line(format!("{cv} = {name}.ptr")); - }); - args.push(cv); - } else { - args.push(format!("{name}.ptr")); - } - } - ArgPass::Callback { .. } => { - let Ty::CallbackInterface(cb) = &p.ty else { - unreachable!("callback family names a callback interface") - }; - let hv = format!("h{}", name.to_upper_camel_case()); - w.line(format!("{hv} := cgo.NewHandle({name})")); - args.push(format!("C.wvHandlePtr(C.uintptr_t({hv}))")); - args.push(format!( - "C.{}()", - vtable_accessor(&c_abi_struct_name(cb, module, prefix)) - )); - } - ArgPass::Direct { .. } => args.push(c_scalar_conv(&name, &p.ty, prefix, module)), - } - pre.push_str(&w.finish()); -} - -/// Emit the out-parameter locals a return type needs. Bytes and buffered -/// returns carry one trailing `size_t* out_len` slot; everything else has -/// none. -fn emit_return_out_params( - pre: &mut String, - args: &mut Vec, - ty: &Ty, - prefix: &str, - module: &str, -) { - if matches!( - plan::ret_pass(Some(ty), module, prefix), - RetPass::Bytes | RetPass::Buffer - ) { - let mut w = CodeWriter::tabs().with_depth(1); - w.line("var cOutLen C.size_t"); - args.push("&cOutLen".into()); - pre.push_str(&w.finish()); - } -} - -/// Emit the success-path return conversion, dispatching on the shared -/// [`RetPass`] contract. `tail` is [`ErrCtx::ok_tail`]: `", nil"` when the -/// wrapper also returns an error, empty when plain. -/// -/// A buffered return is copied out of the producer-allocated buffer (which -/// `wvCopyBuffer` releases with `weaveffi_free_bytes`), decoded, and checked -/// for trailing bytes. An object return transfers one strong reference that -/// the wrapper adopts; a null `Interface?` adopts to nil. -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"), - RetPass::Buffer => { - w.line("rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)}"); - w.line(format!("var goResult {}", go_type(ty))); - emit_buffer_read(&mut w, "rRes", "goResult", ty, "Res", 0); - w.line("rRes.expectEnd()"); - w.line(format!("return goResult{tail}")); - } - RetPass::String => { - w.line("goResult := C.GoString(result)"); - w.line("C.weaveffi_free_string(result)"); - w.line(format!("return goResult{tail}")); - } - RetPass::Bytes => { - w.block("if result == nil {", "}", |w| { - w.line(format!("return nil{tail}")); - }); - w.line("goResult := C.GoBytes(unsafe.Pointer(result), C.int(cOutLen))"); - w.line("C.weaveffi_free_bytes(result, cOutLen)"); - w.line(format!("return goResult{tail}")); - } - RetPass::Object { .. } => { - w.line(format!("return {}{tail}", go_adopt_expr(ty, "result"))); - } - RetPass::Direct => { - let conv = go_scalar_conv("result", ty); - w.line(format!("return {conv}{tail}")); - } - } - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-go/src/lib.rs b/crates/weaveffi-gen-go/src/lib.rs deleted file mode 100644 index fe0dd548..00000000 --- a/crates/weaveffi-gen-go/src/lib.rs +++ /dev/null @@ -1,876 +0,0 @@ -//! Go (cgo) binding generator for WeaveFFI. -//! -//! Emits a Go module (`go.mod` + package) with cgo bindings over the C ABI -//! (revision 2) exposed by the underlying cdylib. Implements -//! [`LanguageBackend`]; the shared driver bridges it into the generator -//! pipeline. -//! -//! Records, rich enums, optionals, lists, and maps are value types that -//! cross the C ABI serialized in the WeaveFFI value-buffer format (one -//! `const uint8_t*` + `size_t` pair). The generated package carries a small -//! private writer/reader implementing the wire format, plus one pack and one -//! unpack function per record and rich enum. -//! -//! Interfaces are reference-counted objects: each Go wrapper holds one strong -//! reference released by `Close` or, as a backstop, by a finalizer, and an -//! object inside a value buffer crosses as a cloned-reference token. -//! Callback interfaces are Go `interface` types the consumer implements; an -//! implementation crosses as a `cgo.Handle` plus the address of one static -//! vtable per interface, filled with exported Go trampolines that recover -//! panics into the producer's error slot. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod calls; -mod codec; -mod docs; -mod entities; -mod package; -mod runtime; -mod types; - -use camino::Utf8Path; -use heck::ToUpperCamelCase; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::{ - BindingModel, CallShape, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, - InterfaceBinding, ModuleBinding, StructBinding, Ty, -}; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::pkg; -use weaveffi_core::plan::{elem_free, Free}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, wrapper_name, CommentStyle}; - -use crate::calls::{ - collect_preamble_decls, render_async_function, render_callback_interface, render_function, - ErrCtx, -}; -use crate::entities::{ - domain_stem, render_enum, render_error, render_interface, render_rich_enum, render_struct, -}; -use crate::package::{package_files, render_go_mod, render_readme}; -use crate::runtime::{ - render_abi_version_check, render_bool_helpers, render_buffer_runtime, render_error_infra, - render_foreign_error, -}; - -/// Per-target configuration for [`GoGenerator`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct GoConfig { - /// Go module path written to `go.mod` (default `"weaveffi"`). - pub module_path: Option, - /// When `true` (the default), strip the IR module path from emitted - /// package-level function names, so module `kv`'s `delete` surfaces as - /// `Delete` rather than `KvDelete`. Set to `false` to restore the - /// module-prefixed spelling. Interface members are namespaced by their - /// wrapper type and never carry the module prefix. - pub strip_module_prefix: bool, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the cgo bindings call the same - /// exported symbols the producer emits. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with. - #[serde(skip)] - pub input_basename: Option, -} - -impl Default for GoConfig { - fn default() -> Self { - Self { - module_path: None, - strip_module_prefix: true, - prefix: None, - input_basename: None, - } - } -} - -impl GoConfig { - /// Returns the configured Go module path, falling back to `"weaveffi"`. - pub fn module_path(&self) -> &str { - self.module_path.as_deref().unwrap_or("weaveffi") - } - - /// Returns the configured C ABI symbol prefix, falling back to `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// Go backend: emits a cgo package (`weaveffi.go`, `go.mod`, and a README) -/// binding the C ABI exposed by the underlying cdylib. -pub struct GoGenerator; - -impl LanguageBackend for GoGenerator { - type Config = GoConfig; - - fn name(&self) -> &'static str { - "go" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn render_enum(&self, out: &mut String, e: &EnumBinding, _config: &Self::Config) { - // A plain C-style enum becomes an `int32` + constants; a rich - // (algebraic) enum becomes a sealed sum type. Each renderer skips - // the other kind. - render_enum(out, e); - render_rich_enum(out, e); - } - - fn render_struct( - &self, - out: &mut String, - _module: &ModuleBinding, - s: &StructBinding, - _config: &Self::Config, - ) { - render_struct(out, s); - } - - fn render_error( - &self, - out: &mut String, - module: &ModuleBinding, - e: &ErrorBinding, - _config: &Self::Config, - ) { - // Emitted once, in the declaring module; inheriting submodules - // reference the ancestor's type through `wvMap{Stem}`. - render_error(out, module, e); - } - - fn render_callback_interface( - &self, - out: &mut String, - module: &ModuleBinding, - cb: &CallbackInterfaceBinding, - config: &Self::Config, - ) { - render_callback_interface(out, config.prefix(), module, cb); - } - - fn render_interface( - &self, - out: &mut String, - module: &ModuleBinding, - i: &InterfaceBinding, - config: &Self::Config, - ) { - let stem = domain_stem(module); - render_interface(out, config.prefix(), module, i, stem.as_deref()); - } - - fn render_function( - &self, - out: &mut String, - module: &ModuleBinding, - f: &FnBinding, - config: &Self::Config, - ) { - let go_name = - wrapper_name(&module.path, &f.name, config.strip_module_prefix).to_upper_camel_case(); - let stem = domain_stem(module); - let err = ErrCtx::of(f, stem.as_deref()); - let prefix = config.prefix(); - if let CallShape::Async(ab) = &f.shape { - render_async_function(out, prefix, &module.path, f, ab, &go_name, None, err); - } else { - render_function(out, prefix, &module.path, f, &go_name, None, err); - } - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let dir = out_dir.join("go"); - let input_basename = config.input_basename(); - vec![ - OutputFile::new(dir.join("weaveffi.go"), render_go(api, model, config)), - OutputFile::new( - dir.join("go.mod"), - render_go_mod( - &pkg::resolve( - api, - config.module_path.as_deref(), - config.input_basename.as_deref(), - ) - .name, - input_basename, - ), - ), - OutputFile::new(dir.join("README.md"), render_readme(input_basename)), - ] - } - - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - Some(package_files(api, model, ctx, out_dir, config)) - } -} - -// ── 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: &Ty) -> bool { - match ty { - Ty::Bool => true, - Ty::Iterator(inner) => matches!(inner.as_ref(), Ty::Bool), - _ => false, - } -} - -/// What the generated file's preamble must pull in, computed by one pass over -/// the lowered model. -#[derive(Default, Clone, Copy)] -struct Imports { - /// `iter` (lazy sequences returned by `iter` functions). - iter: bool, - /// `unsafe` (pointer staging for strings/bytes/buffers, object tokens, - /// callback contexts). - unsafe_ptr: bool, - /// The `boolToC`/`cToBool` helpers. - bool_helpers: bool, - /// `runtime` (`SetFinalizer` and `KeepAlive` on object wrappers). - runtime: bool, - /// `runtime/cgo` (`cgo.Handle` for callback-interface implementations - /// and async completion channels). - cgo: bool, - /// The shared error plumbing: the - /// [`ERROR_BRAND`](weaveffi_core::errors::ERROR_BRAND) type plus the - /// `wvTakeError`/`wvBrandError`/`wvTrap` helpers. - err_infra: bool, - /// The value-buffer runtime (`wvWriter`/`wvReader` and buffer copy - /// helpers), pulling in `encoding/binary`, `math`, and `unicode/utf8`. - buffer_runtime: bool, - /// The `wvForeignError` reporter used by callback-interface trampolines. - foreign_error: bool, - /// The `wvHandlePtr` preamble helper that widens a `cgo.Handle` or an - /// object token into the `void*` the ABI carries. Doing the integer to - /// pointer conversion in C keeps `go vet` from flagging the generated - /// file for a "possible misuse of unsafe.Pointer". - handle_ptr: bool, -} - -/// Scan the lowered model for everything [`Imports`] tracks. Interface -/// members participate exactly like free functions (via -/// [`weaveffi_core::model::ModuleBinding::callables`]). -fn scan_imports(model: &BindingModel) -> Imports { - let mut any_callable = false; - let mut has_async = false; - let mut has_iter = false; - let mut has_domain = false; - let mut bool_helpers = false; - let mut buffer_runtime = false; - let has_interfaces = model.has_interfaces(); - let has_callback_interfaces = model.has_callback_interfaces(); - - for m in &model.modules { - has_domain |= m.declares_error(); - // Records and rich enums always carry pack/unpack functions; a - // declared domain with payload fields decodes them through a reader. - buffer_runtime |= !m.structs.is_empty(); - buffer_runtime |= m.enums.iter().any(|e| e.is_rich()); - if let Some(eb) = &m.error { - buffer_runtime |= eb.declared_here && eb.codes.iter().any(|c| !c.fields.is_empty()); - } - for f in m.callables() { - any_callable = true; - has_async |= f.is_async; - has_iter |= matches!(f.shape, CallShape::Iterator(_)); - 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 |= ret.is_buffered(); - bool_helpers |= ret_direct_bool(ret); - } - if let CallShape::Iterator(ib) = &f.shape { - // Bytes and buffered elements copy through wvCopyBuffer. - buffer_runtime |= matches!(elem_free(&ib.elem), Free::Bytes); - } - } - for cb in &m.callback_interfaces { - for meth in &cb.methods { - buffer_runtime |= meth.params.iter().any(|p| p.ty.is_buffered()); - bool_helpers |= meth.params.iter().any(|p| matches!(p.ty, Ty::Bool)); - bool_helpers |= matches!(meth.ret, Some(Ty::Bool)); - } - } - } - - // Every callable checks its error slot (returning or trapping), so any - // callable at all pulls in the error plumbing; a declared domain also - // needs it for the brand-error fallback of its mapping helper. - let err_infra = any_callable || has_domain; - // wvTakeError copies the payload through unsafe.Pointer; the buffer - // runtime copies C buffers; object token helpers widen pointers; callback - // contexts and async trampolines carry `void*`. - let unsafe_ptr = - err_infra || buffer_runtime || has_interfaces || has_callback_interfaces || has_async; - - Imports { - iter: has_iter, - unsafe_ptr, - bool_helpers, - runtime: has_interfaces, - cgo: has_callback_interfaces || has_async, - err_infra, - buffer_runtime, - foreign_error: has_callback_interfaces, - handle_ptr: has_interfaces || has_callback_interfaces || has_async, - } -} - -// ── Top-level rendering ── - -/// Render the complete generated Go source file: the cgo preamble, imports, -/// runtime prelude, and every module's entities and wrappers in the canonical -/// member order (error domain, enums, structs, callback interfaces, -/// interfaces, functions). -pub(crate) fn render_go(api: &ResolvedApi, model: &BindingModel, config: &GoConfig) -> String { - let prefix = config.prefix(); - let input_basename = config.input_basename(); - let imports = scan_imports(model); - let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); - - // The Go package clause and the linked library name follow the resolved - // package identity (e.g. `package kvstore` / `-lkvstore`) rather than the - // `weaveffi` brand, so the bindings link the shared library the producer - // emits for this package. The C header keeps the ABI-prefix name. - let resolved = pkg::resolve(api, None, Some(input_basename)); - let go_pkg = resolved.ident_name(); - let link_name = resolved.ident_name(); - - out.push_str(&format!("package {go_pkg}\n\n")); - - out.push_str("/*\n"); - out.push_str(&format!("#cgo LDFLAGS: -l{link_name}\n")); - out.push_str(&format!("#include \"{prefix}.h\"\n")); - out.push_str("#include \n"); - if imports.handle_ptr { - out.push_str("static void* wvHandlePtr(uintptr_t h) { return (void*)h; }\n"); - } - // Forward declarations for the //export trampolines below (mirroring the - // const-free prototypes cgo emits into _cgo_export.h) and the static - // vtable of each callback interface. - for decl in collect_preamble_decls(model, prefix) { - out.push_str(&decl); - out.push('\n'); - } - out.push_str("*/\n"); - out.push_str("import \"C\"\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_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.runtime { - out.push_str("\t\"runtime\"\n"); - } - if imports.cgo { - out.push_str("\t\"runtime/cgo\"\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); - } - - if imports.err_infra { - render_error_infra(&mut out); - } - - if imports.buffer_runtime { - render_buffer_runtime(&mut out); - } - - if imports.foreign_error { - render_foreign_error(&mut out); - } - - for m in &model.modules { - GoGenerator.emit_members(&mut out, m, config); - } - - // Exactly one blank line before the trailer keeps the file gofmt-clean. - out.truncate(out.trim_end().len()); - out.push_str("\n\n"); - out.push_str(&render_trailer(CommentStyle::DoubleSlash, "weaveffi.go")); - out -} - -#[cfg(test)] -mod tests { - use super::*; - use weaveffi_ir::ir::{ - Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, - TypeRef, - }; - - fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } - } - - fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } - } - - fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } - } - - fn named(n: &str) -> TypeRef { - TypeRef::Named(n.into()) - } - - fn optional(t: TypeRef) -> TypeRef { - TypeRef::Optional(Box::new(t)) - } - - /// One module exercising every revision-2 shape: an interface with a - /// constructor and a method, `Interface?` in and out, a record with - /// `Interface` and `[Interface]` fields, an iterator of interfaces, a - /// callback interface taking a string, an i32, a record, and an object - /// (one method returning `bool`, one returning void), and a function - /// taking that callback interface. - fn fixture() -> ResolvedApi { - let module = Module { - name: "bus".into(), - doc: None, - functions: vec![ - func( - "pick", - vec![param("preferred", optional(named("Ticker")))], - Some(optional(named("Ticker"))), - ), - func( - "all_tickers", - vec![], - Some(TypeRef::Iterator(Box::new(named("Ticker")))), - ), - func( - "subscribe", - vec![param("subscriber", named("Subscriber"))], - None, - ), - Function { - r#async: true, - ..func( - "fetch", - vec![param("id", TypeRef::I32)], - Some(named("Ticker")), - ) - }, - ], - interfaces: vec![InterfaceDef { - name: "Ticker".into(), - doc: Some("A ticking counter.".into()), - deprecated: None, - constructors: vec![func("new", vec![param("start", TypeRef::I64)], None)], - methods: vec![func("value", vec![], Some(TypeRef::I64))], - statics: vec![], - }], - callback_interfaces: vec![CallbackInterfaceDef { - name: "Subscriber".into(), - doc: Some("Receives bus events.".into()), - deprecated: None, - methods: vec![ - func( - "on_message", - vec![ - param("text", TypeRef::StringUtf8), - param("weight", TypeRef::I32), - param("envelope", named("Envelope")), - ], - None, - ), - func( - "on_ticker", - vec![param("ticker", named("Ticker"))], - Some(TypeRef::Bool), - ), - ], - }], - structs: vec![StructDef { - name: "Envelope".into(), - doc: None, - deprecated: None, - fields: vec![ - field("topic", TypeRef::StringUtf8), - field("primary", named("Ticker")), - field("others", TypeRef::List(Box::new(named("Ticker")))), - ], - }], - enums: vec![], - errors: None, - modules: vec![], - }; - ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![module], - }) - } - - fn render(config: &GoConfig) -> String { - let api = fixture(); - let model = BindingModel::build(&api, config.prefix()); - render_go(&api, &model, config) - } - - fn assert_has(src: &str, needle: &str) { - assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); - } - - #[test] - fn preamble_declares_trampolines_and_one_static_vtable() { - let src = render(&GoConfig::default()); - assert_has(&src, "#include \"weaveffi.h\""); - assert_has( - &src, - "extern void goWv_weaveffi_bus_Subscriber_on_message(void* ctx, char* text, int32_t weight, uint8_t* envelope_ptr, size_t envelope_len, weaveffi_error* out_err);", - ); - assert_has( - &src, - "extern bool goWv_weaveffi_bus_Subscriber_on_ticker(void* ctx, weaveffi_bus_Ticker* ticker, weaveffi_error* out_err);", - ); - assert_has( - &src, - "extern void goWv_weaveffi_bus_Subscriber_free(void* ctx);", - ); - assert_has( - &src, - "static const weaveffi_bus_Subscriber_vtable wvVtable_weaveffi_bus_Subscriber = {", - ); - // The const-carrying entry is cast back to the vtable's field type; - // the const-free one and `free` are used directly. - assert_has( - &src, - "(void (*)(void*, const char*, int32_t, const uint8_t*, size_t, weaveffi_error*))goWv_weaveffi_bus_Subscriber_on_message,", - ); - assert_has(&src, " goWv_weaveffi_bus_Subscriber_on_ticker,"); - assert_has(&src, " goWv_weaveffi_bus_Subscriber_free,\n};"); - assert_eq!(src.matches("wvVtable_weaveffi_bus_Subscriber =").count(), 1); - // A `static` table has no external linkage for cgo to import, so a - // static accessor hands its address to Go. - assert_has( - &src, - "static const weaveffi_bus_Subscriber_vtable* wvVtablePtr_weaveffi_bus_Subscriber(void) { return &wvVtable_weaveffi_bus_Subscriber; }", - ); - assert!( - !src.contains("&C.wvVtable_"), - "Go must not take a static C variable's address directly" - ); - // The async completion trampoline is declared too. - assert_has( - &src, - "extern void goWv_weaveffi_bus_fetch_callback(void* context, weaveffi_error* err, weaveffi_bus_Ticker* result);", - ); - // Imports needed by objects and callbacks. - assert_has(&src, "\t\"runtime\"\n"); - assert_has(&src, "\t\"runtime/cgo\"\n"); - assert_has(&src, "\t\"unsafe\"\n"); - assert!(!src.contains("\"sync\""), "the registry mutex is gone"); - } - - #[test] - fn callback_interface_renders_go_interface_and_trampolines() { - let src = render(&GoConfig::default()); - assert_has(&src, "type Subscriber interface {"); - assert_has( - &src, - "\tOnMessage(text string, weight int32, envelope Envelope)\n", - ); - assert_has(&src, "\tOnTicker(ticker *Ticker) bool\n"); - - // The void-returning trampoline: borrowed string and buffer, direct - // i32, recover into error_set with -4. - assert_has( - &src, - "//export goWv_weaveffi_bus_Subscriber_on_message\nfunc goWv_weaveffi_bus_Subscriber_on_message(ctx unsafe.Pointer, text *C.char, weight C.int32_t, envelope_ptr *C.uint8_t, envelope_len C.size_t, out_err *C.weaveffi_error) {", - ); - assert_has( - &src, - "impl := cgo.Handle(uintptr(ctx)).Value().(Subscriber)", - ); - assert_has(&src, "wvForeignError(out_err, r)"); - assert_has(&src, "C.weaveffi_error_set(outErr, -4, msg)"); - assert_has(&src, "arg0 = C.GoString(text)"); - assert_has(&src, "arg1 := int32(weight)"); - assert_has( - &src, - "rArg2 := &wvReader{buf: wvBorrowBuffer(envelope_ptr, envelope_len)}", - ); - assert_has(&src, "arg2 = wvUnpackEnvelope(rArg2)"); - assert_has(&src, "impl.OnMessage(arg0, arg1, arg2)\n"); - assert!( - !src.contains("C.weaveffi_free_bytes(envelope_ptr"), - "borrowed callback buffers are never freed" - ); - - // The bool-returning trampoline adopts its object argument and - // writes a direct return. - assert_has( - &src, - "func goWv_weaveffi_bus_Subscriber_on_ticker(ctx unsafe.Pointer, ticker *C.weaveffi_bus_Ticker, out_err *C.weaveffi_error) (ret C._Bool) {", - ); - assert_has(&src, "arg0 := wvAdoptTicker(ticker)"); - assert_has(&src, "ret = boolToC(impl.OnTicker(arg0))"); - - // `free` deletes the handle. - assert_has( - &src, - "//export goWv_weaveffi_bus_Subscriber_free\nfunc goWv_weaveffi_bus_Subscriber_free(ctx unsafe.Pointer) {\n\tcgo.Handle(uintptr(ctx)).Delete()\n}", - ); - - // Passing an implementation: handle as ctx, static vtable address. - assert_has(&src, "func Subscribe(subscriber Subscriber) {"); - assert_has(&src, "hSubscriber := cgo.NewHandle(subscriber)"); - assert_has( - &src, - "C.weaveffi_bus_subscribe(C.wvHandlePtr(C.uintptr_t(hSubscriber)), C.wvVtablePtr_weaveffi_bus_Subscriber(), &cErr)", - ); - } - - #[test] - fn interface_wrapper_adopts_clones_and_destroys_once() { - let src = render(&GoConfig::default()); - assert_has( - &src, - "type Ticker struct {\n\tptr *C.weaveffi_bus_Ticker\n}", - ); - assert_has( - &src, - "func wvAdoptTicker(ptr *C.weaveffi_bus_Ticker) *Ticker {\n\tif ptr == nil {\n\t\treturn nil\n\t}\n\ts := &Ticker{ptr: ptr}\n\truntime.SetFinalizer(s, (*Ticker).Close)\n\treturn s\n}", - ); - assert_has( - &src, - "func (s *Ticker) Close() {\n\tif s.ptr != nil {\n\t\tC.weaveffi_bus_Ticker_destroy(s.ptr)\n\t\ts.ptr = nil\n\t\truntime.SetFinalizer(s, nil)\n\t}\n}", - ); - // Token helpers use clone for writing and adopt for reading. - assert_has( - &src, - "return uint64(uintptr(unsafe.Pointer(C.weaveffi_bus_Ticker_clone(o.ptr))))", - ); - assert_has( - &src, - "func wvUntokenTicker(token uint64) *Ticker {\n\tif token == 0 {", - ); - assert_has( - &src, - "return wvAdoptTicker((*C.weaveffi_bus_Ticker)(C.wvHandlePtr(C.uintptr_t(token))))", - ); - - // Constructor and method: a returned object is adopted, the receiver - // is kept alive across the call. - assert_has(&src, "func NewTicker(start int64) *Ticker {"); - assert_has(&src, "return wvAdoptTicker(result)"); - assert_has( - &src, - "func (s *Ticker) Value() int64 {\n\tif s.ptr == nil {\n\t\tpanic(\"weaveffi: Ticker used after Close\")\n\t}\n\tdefer runtime.KeepAlive(s)\n\tvar cErr C.weaveffi_error\n\tresult := C.weaveffi_bus_Ticker_value(s.ptr, &cErr)", - ); - } - - #[test] - fn nullable_objects_are_nil_wrapper_pointers() { - let src = render(&GoConfig::default()); - assert_has(&src, "func Pick(preferred *Ticker) *Ticker {"); - assert_has(&src, "defer runtime.KeepAlive(preferred)"); - assert_has( - &src, - "var cPreferred *C.weaveffi_bus_Ticker\n\tif preferred != nil {\n\t\tcPreferred = preferred.ptr\n\t}", - ); - assert_has(&src, "result := C.weaveffi_bus_pick(cPreferred, &cErr)"); - // A null return adopts to nil through the same helper. - assert_has(&src, "\treturn wvAdoptTicker(result)\n}"); - } - - #[test] - fn records_carry_object_tokens() { - let src = render(&GoConfig::default()); - assert_has( - &src, - "type Envelope struct {\n\tTopic string\n\tPrimary *Ticker\n\tOthers []*Ticker\n}", - ); - assert_has(&src, "w.writeU64(wvTokenTicker(v.Primary))"); - assert_has( - &src, - "for _, eOthers0 := range v.Others {\n\t\tw.writeU64(wvTokenTicker(eOthers0))", - ); - assert_has(&src, "v.Primary = wvUntokenTicker(r.readU64())"); - assert_has(&src, "v.Others[iOthers0] = wvUntokenTicker(r.readU64())"); - } - - #[test] - fn iterator_and_async_adopt_object_elements() { - let src = render(&GoConfig::default()); - assert_has(&src, "func AllTickers() iter.Seq[*Ticker] {"); - assert_has(&src, "var outItem *C.weaveffi_bus_Ticker"); - assert_has( - &src, - "ok := C.weaveffi_bus_AllTickersIterator_next(it, &outItem, &iterErr) != 0", - ); - assert_has(&src, "item := wvAdoptTicker(outItem)"); - assert_has(&src, "defer C.weaveffi_bus_AllTickersIterator_destroy(it)"); - - assert_has(&src, "func Fetch(id int32) *Ticker {"); - assert_has(&src, "h := cgo.NewHandle(ch)"); - assert_has( - &src, - "C.weaveffi_bus_fetch_async(C.int32_t(id), C.weaveffi_bus_fetch_callback(unsafe.Pointer(C.goWv_weaveffi_bus_fetch_callback)), C.wvHandlePtr(C.uintptr_t(h)))", - ); - assert_has( - &src, - "h := cgo.Handle(uintptr(context))\n\tch := h.Value().(chan wvOutcomeBusFetch)\n\th.Delete()", - ); - assert_has(&src, "ch <- wvOutcomeBusFetch{val: wvAdoptTicker(result)}"); - } - - #[test] - fn custom_prefix_keeps_runtime_symbols_canonical() { - let config = GoConfig { - prefix: Some("acme".into()), - ..GoConfig::default() - }; - let src = render(&config); - assert_has(&src, "#include \"acme.h\""); - assert_has(&src, "C.acme_bus_Ticker_clone(o.ptr)"); - assert_has(&src, "C.wvVtablePtr_acme_bus_Subscriber()"); - assert_has(&src, "C.weaveffi_error_set(outErr, -4, msg)"); - assert_has(&src, "C.weaveffi_abi_version()"); - assert!(!src.contains("arena"), "no arena bindings remain"); - assert!( - !src.contains("handle_t"), - "no untyped handle typedef remains" - ); - } - - #[test] - fn undocumented_enum_members_are_gofmt_aligned() { - use weaveffi_ir::ir::{EnumDef, EnumVariant}; - let variant = |name: &str, value: i32, fields: Vec| EnumVariant { - name: name.into(), - value, - doc: None, - fields, - }; - let module = Module { - name: "paint".into(), - doc: None, - functions: vec![], - interfaces: vec![], - callback_interfaces: vec![], - structs: vec![], - enums: vec![ - EnumDef { - name: "Channel".into(), - doc: None, - deprecated: None, - variants: vec![ - variant("red", 0, vec![]), - variant("green", 1, vec![]), - variant("blue", 2, vec![]), - ], - }, - EnumDef { - name: "Shape".into(), - doc: None, - deprecated: None, - variants: vec![variant( - "rectangle", - 0, - vec![field("width", TypeRef::F32), field("height", TypeRef::F32)], - )], - }, - ], - errors: None, - modules: vec![], - }; - let api = ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![module], - }); - let config = GoConfig::default(); - let model = BindingModel::build(&api, config.prefix()); - let src = render_go(&api, &model, &config); - // gofmt pads a run of undocumented const and field names to one - // column; emitting it pre-aligned keeps the file gofmt-clean. - assert_has( - &src, - "const (\n\tChannelRed Channel = 0\n\tChannelGreen Channel = 1\n\tChannelBlue Channel = 2\n)", - ); - assert_has( - &src, - "type ShapeRectangle struct {\n\tWidth float32\n\tHeight float32\n}", - ); - } - - #[test] - fn output_is_deterministic() { - let a = render(&GoConfig::default()); - let b = render(&GoConfig::default()); - assert_eq!(a, b); - } -} diff --git a/crates/weaveffi-gen-go/src/package.rs b/crates/weaveffi-gen-go/src/package.rs deleted file mode 100644 index 5eb4cf4b..00000000 --- a/crates/weaveffi-gen-go/src/package.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! Packaging scaffold: `go.mod`, the README flavors, and the `package` hook -//! bundling per-platform shared libraries with a rewritten cgo preamble. - -use camino::Utf8Path; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::pkg; -use weaveffi_core::platform::{NativeBinary, Platform}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::{render_go, GoConfig}; - -/// The `(GOOS, GOARCH)` build-constraint tokens for a [`Platform`], used on -/// `#cgo` directive lines, or `None` for a platform the cgo package has no -/// slot for (Android and wasm32 binaries are skipped when packaging). -fn go_build_tags(p: Platform) -> Option<(&'static str, &'static str)> { - match p { - Platform::MacosArm64 => Some(("darwin", "arm64")), - Platform::MacosX64 => Some(("darwin", "amd64")), - Platform::LinuxX64 => Some(("linux", "amd64")), - Platform::LinuxArm64 => Some(("linux", "arm64")), - Platform::WindowsX64 => Some(("windows", "amd64")), - Platform::AndroidArm64 | Platform::AndroidX64 | Platform::Wasm32 => None, - } -} - -/// The bundled binaries this package has a `#cgo` slot for, in the binary -/// set's order. -fn bundled<'a>( - ctx: &'a PackageContext<'a>, -) -> impl Iterator { - ctx.binaries - .binaries - .iter() - .filter_map(|nb| go_build_tags(nb.platform).map(|(goos, goarch)| (nb, goos, goarch))) -} - -/// The generated `go.mod` for the emitted package. -pub(crate) fn render_go_mod(module_path: &str, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::DoubleSlash, input_basename); - let trailer = render_trailer(CommentStyle::DoubleSlash, "go.mod"); - // Go 1.23 is required for the standard `iter` package the lazy - // `iter` wrappers return. - format!("{prelude}module {module_path}\n\ngo 1.23\n\n{trailer}") -} - -/// README for the generate-mode output, describing the manual build setup. -pub(crate) fn render_readme(input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - format!( - r#"{prelude}# 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. - -Interfaces are reference-counted objects: each wrapper holds one strong -reference that `Close()` releases (a finalizer releases it if you forget), and -a nullable interface is a nil wrapper pointer. Callback interfaces are Go -`interface` types: implement one and pass the value to any function that -accepts it; the native library may call it from any thread, and a panic in -your implementation is reported to the native caller instead of crashing. - -{trailer}"# - ) -} - -/// README for a packaged Go module that bundles per-platform libraries. -fn render_packaged_readme(ctx: &PackageContext, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let platforms: Vec = bundled(ctx) - .map(|(nb, _, _)| format!("- `lib/{}/`", nb.platform.id())) - .collect(); - let platform_list = platforms.join("\n"); - format!( - r#"{prelude}# WeaveFFI (Go) - -Auto-generated cgo bindings with a prebuilt shared library bundled for each -platform under `lib//`. The cgo preamble adds the matching -`${{SRCDIR}}`-relative library search path and rpath per GOOS/GOARCH, so -`go build` links the right library with no manual `CGO_LDFLAGS`. - -The C ABI header is expected at `../c/include/` (package the `c` target -alongside Go, for example `weaveffi package --target c,go`). - -## Bundled platforms - -{platform_list} - -{trailer}"#, - ) -} - -/// The full packaged file set: the Go source with a self-contained cgo -/// preamble, `go.mod`, the packaged README, and one bundled shared library -/// per desktop platform. Binaries for platforms without a `GOOS,GOARCH` cgo -/// slot (Android, wasm32) are skipped. -pub(crate) fn package_files( - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &GoConfig, -) -> Vec { - let dir = out_dir.join("go"); - let input_basename = config.input_basename(); - let link_name = pkg::resolve(api, None, Some(input_basename)).ident_name(); - let module_path = pkg::resolve( - api, - config.module_path.as_deref(), - config.input_basename.as_deref(), - ) - .name; - - // Expand the single generate-mode `#cgo LDFLAGS` line into a - // self-contained, relocatable set: a header include path plus per - // GOOS/GOARCH library search + rpath directives (all `${SRCDIR}` - // relative). cgo selects the matching line at build time. - let original = format!("#cgo LDFLAGS: -l{link_name}\n"); - let mut cgo = String::from("#cgo CFLAGS: -I${SRCDIR}/../c/include\n"); - for (nb, goos, goarch) in bundled(ctx) { - let id = nb.platform.id(); - if nb.platform == Platform::WindowsX64 { - cgo.push_str(&format!( - "#cgo {goos},{goarch} LDFLAGS: -L${{SRCDIR}}/lib/{id}\n" - )); - } else { - cgo.push_str(&format!( - "#cgo {goos},{goarch} LDFLAGS: -L${{SRCDIR}}/lib/{id} -Wl,-rpath,${{SRCDIR}}/lib/{id}\n" - )); - } - } - cgo.push_str(&format!("#cgo LDFLAGS: -l{link_name}\n")); - let go_src = render_go(api, model, config).replace(&original, &cgo); - - let mut files = vec![ - PackagedFile::text(dir.join("weaveffi.go"), go_src), - PackagedFile::text( - dir.join("go.mod"), - render_go_mod(&module_path, input_basename), - ), - PackagedFile::text( - dir.join("README.md"), - render_packaged_readme(ctx, input_basename), - ), - ]; - for (nb, _, _) in bundled(ctx) { - let dest = dir - .join("lib") - .join(nb.platform.id()) - .join(ctx.binaries.bundled_filename(nb.platform)); - files.push(PackagedFile::copy(dest, nb.source.clone())); - } - files -} diff --git a/crates/weaveffi-gen-go/src/runtime.rs b/crates/weaveffi-gen-go/src/runtime.rs deleted file mode 100644 index 37185f41..00000000 --- a/crates/weaveffi-gen-go/src/runtime.rs +++ /dev/null @@ -1,436 +0,0 @@ -//! The emitted Go runtime prelude: bool conversion helpers, the shared -//! error plumbing, the value-buffer reader/writer pair, and the foreign-error -//! reporter callback-interface trampolines use. - -use weaveffi_core::cabi::ABI_VERSION; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::ERROR_BRAND; - -/// The runtime error code a callback-interface trampoline reports when the -/// Go implementation panicked (`FOREIGN_ERROR_CODE` in the ABI contract). -pub(crate) const FOREIGN_ERROR_CODE: i32 = -4; - -/// 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) { - // cgo models C `_Bool` as a distinct Go type whose underlying kind is - // bool, so convert with the type itself rather than integer literals. - out.push_str("func boolToC(b bool) C._Bool {\n"); - out.push_str("\treturn C._Bool(b)\n"); - out.push_str("}\n\n"); - out.push_str("func cToBool(b C._Bool) bool {\n"); - out.push_str("\treturn bool(b)\n"); - out.push_str("}\n\n"); -} - -/// The shared error plumbing: the generic [`ERROR_BRAND`] struct implementing -/// `error` (unknown codes, marshalling failures, producer panics, and foreign -/// callback failures), plus the `wvTakeError` slot reader (returning code, -/// message, and a copy of the structured payload buffer), the `wvBrandError` -/// constructor, and the `wvTrap` panic helper non-throwing wrappers check -/// their slot with (it panics with a `*WeaveFFIError` value rather than a -/// bare string so a `recover()` site can still read the code). -/// -/// The runtime's negative codes (`-1` generic, `-2` producer panic, `-3` -/// marshalling failure, `-4` a Go callback-interface implementation -/// panicked) all surface through the brand error: the code is preserved on -/// the value so a caller can still tell them apart. -pub(crate) fn render_error_infra(out: &mut String) { - let mut w = CodeWriter::tabs(); - w.line(format!( - "// {ERROR_BRAND} reports a failure crossing the C boundary that no typed" - )); - w.line("// error domain claims: an unknown code, a marshalling failure, a"); - w.line("// producer panic (code -2), or a Go callback-interface implementation"); - w.line("// that panicked (code -4)."); - w.block(format!("type {ERROR_BRAND} struct {{"), "}", |w| { - w.line("// Code is the numeric ABI error code."); - w.line("Code int32"); - w.line("// Message is the human-readable error message."); - w.line("Message string"); - }); - w.blank(); - w.block( - format!("func (e *{ERROR_BRAND}) Error() string {{"), - "}", - |w| { - w.line("return fmt.Sprintf(\"weaveffi: %s (code %d)\", e.Message, e.Code)"); - }, - ); - w.blank(); - - w.line("// wvTakeError reads and clears a non-zero C error slot, returning its"); - w.line("// code, message, and a copy of its structured payload buffer (nil when"); - w.line("// the code declares no payload fields)."); - w.block( - "func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) {", - "}", - |w| { - w.line("code := int32(cErr.code)"); - w.line("msg := \"\""); - w.block("if cErr.message != nil {", "}", |w| { - w.line("msg = C.GoString(cErr.message)"); - }); - w.line("var payload []byte"); - w.block("if cErr.payload_ptr != nil {", "}", |w| { - w.line( - "payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len))", - ); - }); - w.line("C.weaveffi_error_clear(cErr)"); - w.line("return code, msg, payload"); - }, - ); - w.blank(); - - w.line("// wvTakeBoxedError reads a heap-boxed error delivered to an async"); - w.line("// completion callback and releases the box with weaveffi_error_free."); - w.block( - "func wvTakeBoxedError(cErr *C.weaveffi_error) (int32, string, []byte) {", - "}", - |w| { - w.line("code := int32(cErr.code)"); - w.line("msg := \"\""); - w.block("if cErr.message != nil {", "}", |w| { - w.line("msg = C.GoString(cErr.message)"); - }); - w.line("var payload []byte"); - w.block("if cErr.payload_ptr != nil {", "}", |w| { - w.line( - "payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len))", - ); - }); - w.line("C.weaveffi_error_free(cErr)"); - w.line("return code, msg, payload"); - }, - ); - w.blank(); - - w.block( - "func wvBrandError(code int32, message string, _ []byte) error {", - "}", - |w| { - w.line(format!( - "return &{ERROR_BRAND}{{Code: code, Message: message}}" - )); - }, - ); - w.blank(); - - w.line("// wvTrap panics when the C error slot reports a failure. Non-throwing"); - w.line("// wrappers check their slot with it: a non-zero code there can only be"); - w.line("// a producer panic, a marshalling failure, or a callback-interface"); - w.line(format!( - "// implementation that panicked. The panic value is a *{ERROR_BRAND}, so a" - )); - w.line("// recover() site can still inspect the code."); - w.block("func wvTrap(cErr *C.weaveffi_error) {", "}", |w| { - w.block("if cErr.code != 0 {", "}", |w| { - w.line("panic(wvBrandError(wvTakeError(cErr)))"); - }); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// The private writer/reader pair implementing the WeaveFFI value-buffer -/// wire format (little-endian, packed, `u32` length prefixes), plus the two -/// buffer copy helpers (`wvCopyBuffer` for owned return and async result -/// buffers released with `weaveffi_free_bytes`, `wvBorrowBuffer` for borrowed -/// callback argument buffers the producer frees). -/// -/// The reader panics on malformed input: a bad buffer is a producer bug (a -/// contract violation), not a recoverable domain error, so it surfaces -/// through the same panic channel a trapped producer error does. -pub(crate) fn render_buffer_runtime(out: &mut String) { - let mut w = CodeWriter::tabs(); - w.line("// wvWriter serializes values into the WeaveFFI value-buffer format:"); - w.line("// little-endian, packed, u32 length prefixes."); - w.block("type wvWriter struct {", "}", |w| { - w.line("buf []byte"); - }); - w.blank(); - w.block("func (w *wvWriter) writeBool(v bool) {", "}", |w| { - w.line("if v {"); - w.indent(); - w.line("w.buf = append(w.buf, 1)"); - w.dedent(); - w.line("} else {"); - w.indent(); - w.line("w.buf = append(w.buf, 0)"); - w.dedent(); - w.line("}"); - }); - w.blank(); - w.block("func (w *wvWriter) writeI8(v int8) {", "}", |w| { - w.line("w.buf = append(w.buf, byte(v))"); - }); - w.blank(); - w.block("func (w *wvWriter) writeU8(v uint8) {", "}", |w| { - w.line("w.buf = append(w.buf, v)"); - }); - w.blank(); - w.block("func (w *wvWriter) writeI16(v int16) {", "}", |w| { - w.line("w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v))"); - }); - w.blank(); - w.block("func (w *wvWriter) writeU16(v uint16) {", "}", |w| { - w.line("w.buf = binary.LittleEndian.AppendUint16(w.buf, v)"); - }); - w.blank(); - w.block("func (w *wvWriter) writeI32(v int32) {", "}", |w| { - w.line("w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v))"); - }); - w.blank(); - w.block("func (w *wvWriter) writeU32(v uint32) {", "}", |w| { - w.line("w.buf = binary.LittleEndian.AppendUint32(w.buf, v)"); - }); - w.blank(); - w.block("func (w *wvWriter) writeI64(v int64) {", "}", |w| { - w.line("w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v))"); - }); - w.blank(); - w.block("func (w *wvWriter) writeU64(v uint64) {", "}", |w| { - w.line("w.buf = binary.LittleEndian.AppendUint64(w.buf, v)"); - }); - w.blank(); - w.block("func (w *wvWriter) writeF32(v float32) {", "}", |w| { - w.line("w.writeU32(math.Float32bits(v))"); - }); - w.blank(); - w.block("func (w *wvWriter) writeF64(v float64) {", "}", |w| { - w.line("w.writeU64(math.Float64bits(v))"); - }); - w.blank(); - w.block("func (w *wvWriter) writeLen(n int) {", "}", |w| { - w.block("if n < 0 || uint64(n) > uint64(^uint32(0)) {", "}", |w| { - w.line("panic(\"weaveffi: value-buffer length exceeds u32 range\")"); - }); - w.line("w.writeU32(uint32(n))"); - }); - w.blank(); - w.block("func (w *wvWriter) writeString(v string) {", "}", |w| { - w.line("w.writeLen(len(v))"); - w.line("w.buf = append(w.buf, v...)"); - }); - w.blank(); - w.block("func (w *wvWriter) writeBytes(v []byte) {", "}", |w| { - w.line("w.writeLen(len(v))"); - w.line("w.buf = append(w.buf, v...)"); - }); - w.blank(); - w.block( - "func (w *wvWriter) writeOptionFlag(present bool) {", - "}", - |w| { - w.line("w.writeBool(present)"); - }, - ); - w.blank(); - - w.line("// wvReader decodes values from the WeaveFFI value-buffer format. A"); - w.line("// malformed buffer is a producer/consumer contract violation, so every"); - w.line("// read panics (the same channel a trapped producer error uses) instead"); - w.line("// of returning a typed domain error."); - w.block("type wvReader struct {", "}", |w| { - w.line("buf []byte"); - w.line("pos int"); - }); - w.blank(); - w.block("func wvMalformed(context string) {", "}", |w| { - w.line("panic(\"weaveffi: malformed value buffer: \" + context)"); - }); - w.blank(); - w.block( - "func (r *wvReader) take(n int, context string) []byte {", - "}", - |w| { - w.block("if n < 0 || len(r.buf)-r.pos < n {", "}", |w| { - w.line("wvMalformed(context)"); - }); - w.line("b := r.buf[r.pos : r.pos+n]"); - w.line("r.pos += n"); - w.line("return b"); - }, - ); - w.blank(); - w.block("func (r *wvReader) readBool() bool {", "}", |w| { - w.line("switch r.take(1, \"bool\")[0] {"); - w.line("case 0:"); - w.indent(); - w.line("return false"); - w.dedent(); - w.line("case 1:"); - w.indent(); - w.line("return true"); - w.dedent(); - w.line("}"); - w.line("wvMalformed(\"bool byte out of range\")"); - w.line("return false"); - }); - w.blank(); - w.block("func (r *wvReader) readI8() int8 {", "}", |w| { - w.line("return int8(r.take(1, \"i8\")[0])"); - }); - w.blank(); - w.block("func (r *wvReader) readU8() uint8 {", "}", |w| { - w.line("return r.take(1, \"u8\")[0]"); - }); - w.blank(); - w.block("func (r *wvReader) readI16() int16 {", "}", |w| { - w.line("return int16(binary.LittleEndian.Uint16(r.take(2, \"i16\")))"); - }); - w.blank(); - w.block("func (r *wvReader) readU16() uint16 {", "}", |w| { - w.line("return binary.LittleEndian.Uint16(r.take(2, \"u16\"))"); - }); - w.blank(); - w.block("func (r *wvReader) readI32() int32 {", "}", |w| { - w.line("return int32(binary.LittleEndian.Uint32(r.take(4, \"i32\")))"); - }); - w.blank(); - w.block("func (r *wvReader) readU32() uint32 {", "}", |w| { - w.line("return binary.LittleEndian.Uint32(r.take(4, \"u32\"))"); - }); - w.blank(); - w.block("func (r *wvReader) readI64() int64 {", "}", |w| { - w.line("return int64(binary.LittleEndian.Uint64(r.take(8, \"i64\")))"); - }); - w.blank(); - w.block("func (r *wvReader) readU64() uint64 {", "}", |w| { - w.line("return binary.LittleEndian.Uint64(r.take(8, \"u64\"))"); - }); - w.blank(); - w.block("func (r *wvReader) readF32() float32 {", "}", |w| { - w.line("return math.Float32frombits(r.readU32())"); - }); - w.blank(); - w.block("func (r *wvReader) readF64() float64 {", "}", |w| { - w.line("return math.Float64frombits(r.readU64())"); - }); - w.blank(); - w.block("func (r *wvReader) readLen() int {", "}", |w| { - w.line("n := int(r.readU32())"); - w.block("if n > len(r.buf)-r.pos {", "}", |w| { - w.line("wvMalformed(\"length prefix exceeds remaining buffer\")"); - }); - w.line("return n"); - }); - w.blank(); - w.block("func (r *wvReader) readString() string {", "}", |w| { - w.line("b := r.take(r.readLen(), \"string bytes\")"); - w.block("if !utf8.Valid(b) {", "}", |w| { - w.line("wvMalformed(\"string is not valid UTF-8\")"); - }); - w.line("return string(b)"); - }); - w.blank(); - w.block("func (r *wvReader) readBytes() []byte {", "}", |w| { - w.line("b := r.take(r.readLen(), \"byte buffer\")"); - w.line("out := make([]byte, len(b))"); - w.line("copy(out, b)"); - w.line("return out"); - }); - w.blank(); - w.block("func (r *wvReader) readOptionFlag() bool {", "}", |w| { - w.line("switch r.take(1, \"option flag\")[0] {"); - w.line("case 0:"); - w.indent(); - w.line("return false"); - w.dedent(); - w.line("case 1:"); - w.indent(); - w.line("return true"); - w.dedent(); - w.line("}"); - w.line("wvMalformed(\"option flag byte out of range\")"); - w.line("return false"); - }); - w.blank(); - w.block("func (r *wvReader) expectEnd() {", "}", |w| { - w.block("if r.pos != len(r.buf) {", "}", |w| { - w.line("wvMalformed(\"trailing bytes after value\")"); - }); - }); - w.blank(); - - w.line("// wvCopyBuffer copies an owned, producer-allocated value buffer into Go"); - w.line("// memory and releases it with weaveffi_free_bytes."); - w.block( - "func wvCopyBuffer(ptr *C.uint8_t, length C.size_t) []byte {", - "}", - |w| { - w.block("if ptr == nil {", "}", |w| { - w.line("return nil"); - }); - w.line("out := C.GoBytes(unsafe.Pointer(ptr), C.int(length))"); - w.line("C.weaveffi_free_bytes(ptr, length)"); - w.line("return out"); - }, - ); - w.blank(); - w.line("// wvBorrowBuffer copies a borrowed value buffer into Go memory. The"); - w.line("// producer keeps ownership and frees it after the borrowing call returns."); - w.block( - "func wvBorrowBuffer(ptr *C.uint8_t, length C.size_t) []byte {", - "}", - |w| { - w.block("if ptr == nil {", "}", |w| { - w.line("return nil"); - }); - w.line("return C.GoBytes(unsafe.Pointer(ptr), C.int(length))"); - }, - ); - w.blank(); - out.push_str(&w.finish()); -} - -/// The `wvForeignError` reporter every callback-interface trampoline calls -/// from its `recover` path: it formats the recovered panic value and hands a -/// borrowed C string to `weaveffi_error_set` with [`FOREIGN_ERROR_CODE`]. -/// The producer copies the message, so the C string is freed before -/// returning, and the trampoline itself returns its zero value; the panic -/// never unwinds through the C frame. Like every other runtime symbol the -/// bindings call, the helper keeps its canonical `weaveffi_` spelling. -pub(crate) fn render_foreign_error(out: &mut String) { - let mut w = CodeWriter::tabs(); - w.line("// wvForeignError reports a panic recovered inside a callback-interface"); - w.line("// trampoline through the producer's error slot (code -4). The message is"); - w.line("// borrowed for the call: the producer copies it with its own allocator."); - w.block( - "func wvForeignError(outErr *C.weaveffi_error, recovered any) {", - "}", - |w| { - w.line("msg := C.CString(fmt.Sprint(recovered))"); - w.line("defer C.free(unsafe.Pointer(msg))"); - w.line(format!( - "C.weaveffi_error_set(outErr, {FOREIGN_ERROR_CODE}, msg)" - )); - }, - ); - w.blank(); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-kotlin/Cargo.toml b/crates/weaveffi-gen-kotlin/Cargo.toml deleted file mode 100644 index 45db416e..00000000 --- a/crates/weaveffi-gen-kotlin/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "weaveffi-gen-kotlin" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "Kotlin binding generator (JNI shim, Android and desktop JVM) for WeaveFFI" -keywords = ["ffi", "codegen", "bindings", "kotlin", "jni"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -camino = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-kotlin/src/calls.rs b/crates/weaveffi-gen-kotlin/src/calls.rs deleted file mode 100644 index 0eba56cc..00000000 --- a/crates/weaveffi-gen-kotlin/src/calls.rs +++ /dev/null @@ -1,392 +0,0 @@ -//! Per-callable Kotlin wrappers: free functions, `suspend fun` shims, and the -//! interface-member native declarations. - -use std::fmt::Write as _; - -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, ModuleBinding, ParamBinding}; -use weaveffi_core::plan::ErrorStrategy; -use weaveffi_core::utils::local_type_name; - -use crate::codec::{kt_decode_expr, kt_encode_expr}; -use crate::entities::kotlin_exception_name; -use crate::types::{ - camel_params, kotlin_fn_name, kotlin_iterator_class_name, kotlin_jni_type, kotlin_type, - kt_param, needs_wrapper_split, -}; - -/// The Kotlin lambda mapping an async error `(code, message, payload)` triple -/// to the exception the continuation resumes with: the typed domain exception -/// (which decodes the payload) when the callable's -/// [`ErrorStrategy`] is [`Throws`](ErrorStrategy::Throws), the generic brand -/// exception otherwise, so the runtime's reserved negative codes always trap -/// generically. -pub(crate) fn kotlin_error_mapper(f: &FnBinding, error: Option<&ErrorBinding>) -> String { - match (error, f.error_strategy()) { - (Some(eb), ErrorStrategy::Throws) => { - format!( - "{{ code, message, payload -> {}.fromCode(code, message, payload) }}", - kotlin_exception_name(eb) - ) - } - _ => format!( - "{{ code, message, _ -> {}(code, message) }}", - errors::EXCEPTION_BRAND - ), - } -} - -/// Render one free function into the `WeaveFFI` companion: a bare `external -/// fun` when every type crosses JNI unchanged (callback interface -/// implementations included: they cross as the object itself), otherwise a -/// private `{name}Jni` external plus a public wrapper that unwraps handles and -/// enums on the way in and re-wraps class returns on the way out. -pub(crate) fn render_kotlin_free_fn( - out: &mut String, - m: &ModuleBinding, - f: &FnBinding, - strip: bool, - c_prefix: &str, -) { - let func_name = kotlin_fn_name(&m.path, &f.name, strip); - crate::docs::emit_fn_doc(out, &f.doc, &camel_params(&f.params), " "); - if f.is_async { - let native = format!("{func_name}Async"); - let mapper = kotlin_error_mapper(f, m.error.as_ref()); - render_kotlin_async_fun( - out, - f, - &func_name, - &native, - false, - "@JvmStatic ", - true, - 2, - &mapper, - ); - } else if needs_wrapper_split(f) { - let native_params: Vec = f - .params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), kotlin_jni_type(&p.ty))) - .collect(); - let native_ret = f - .ret - .as_ref() - .map(kotlin_jni_type) - .unwrap_or_else(|| "Unit".to_string()); - let _ = writeln!( - out, - " @JvmStatic private external fun {}Jni({}): {}", - func_name, - native_params.join(", "), - native_ret - ); - let call_args: Vec = f.params.iter().map(kotlin_unwrap_arg).collect(); - let call = format!("{}Jni({})", func_name, call_args.join(", ")); - let mut w = CodeWriter::four_space().with_depth(2); - write_kotlin_sync_wrapper( - &mut w, - f, - &format!("@JvmStatic fun {func_name}"), - &call, - c_prefix, - ); - out.push_str(&w.finish()); - } else { - let params_sig: Vec = f - .params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), kotlin_type(&p.ty))) - .collect(); - let ret = f - .ret - .as_ref() - .map(kotlin_type) - .unwrap_or_else(|| "Unit".to_string()); - if let Some(msg) = &f.deprecated { - let _ = writeln!(out, " @Deprecated(\"{}\")", msg.replace('"', "\\\"")); - } - let _ = writeln!( - out, - " @JvmStatic external fun {}({}): {}", - func_name, - params_sig.join(", "), - ret - ); - } -} - -/// The Kotlin expression that lowers one public argument for a JNI call: -/// buffered values pack into a `ByteArray` (cloning any object they carry), -/// enums pass `.value`, interfaces lend the raw `.handle` (nullable via -/// `?.`), and a callback interface implementation passes as the object the -/// JNI shim pins. -pub(crate) fn kotlin_unwrap_arg(p: &ParamBinding) -> String { - let n = kt_param(&p.name); - if p.ty.is_buffered() { - return kt_encode_expr(&p.ty, &n); - } - match &p.ty { - Ty::Enum(_) => format!("{n}.value"), - Ty::Interface(_) => format!("{n}.handle"), - // Only `Interface?` reaches here (every other optional is buffered). - Ty::Optional(_) => format!("{n}?.handle"), - _ => n, - } -} - -/// The Kotlin expression re-wrapping a lowered JNI value `expr` into the -/// public return type, or `None` when the lowered value already is the public -/// type: buffered returns decode the `ByteArray`, enums round-trip through -/// `fromValue`, interfaces through the companion's `fromHandle` (nullable via -/// `?.let`). -pub(crate) fn kotlin_wrap_return(ret: Option<&Ty>, expr: &str) -> Option { - let ret = ret?; - if ret.is_buffered() { - return Some(kt_decode_expr(ret, expr)); - } - match ret { - Ty::Enum(name) => Some(format!("{}.fromValue({expr})", local_type_name(name))), - Ty::Interface(name) => Some(format!("{}.fromHandle({expr})", local_type_name(name))), - // Only `Interface?` reaches here (every other optional is buffered). - Ty::Optional(inner) => match inner.as_ref() { - Ty::Interface(name) => Some(format!( - "{expr}?.let {{ {}.fromHandle(it) }}", - local_type_name(name) - )), - _ => unreachable!("buffered optionals are handled above"), - }, - _ => None, - } -} - -/// Write the public wrapper for a sync callable whose lowered JNI call is -/// `call`. `decl` carries everything before the parameter list (annotations -/// resolved by the caller, e.g. `"@JvmStatic fun createContact"` or -/// `"operator fun invoke"`). -pub(crate) fn write_kotlin_sync_wrapper( - w: &mut CodeWriter, - f: &FnBinding, - decl: &str, - call: &str, - c_prefix: &str, -) { - let params_sig: Vec = f - .params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), kotlin_type(&p.ty))) - .collect(); - let public_ret = f - .ret - .as_ref() - .map(kotlin_type) - .unwrap_or_else(|| "Unit".to_string()); - if let Some(msg) = &f.deprecated { - w.line(format!("@Deprecated(\"{}\")", msg.replace('"', "\\\""))); - } - // An iterator callable's native launcher returns the raw handle; the - // public wrapper adopts it into the generated lazy iterator class. - if let CallShape::Iterator(it) = &f.shape { - let class = kotlin_iterator_class_name(it, c_prefix); - w.line(format!( - "{decl}({}): {public_ret} = {class}({call})", - params_sig.join(", ") - )); - return; - } - match kotlin_wrap_return(f.ret.as_ref(), call) { - Some(wrapped) => { - w.line(format!( - "{decl}({}): {public_ret} = {wrapped}", - params_sig.join(", ") - )); - } - None if f.ret.is_some() => { - w.line(format!( - "{decl}({}): {public_ret} = {call}", - params_sig.join(", ") - )); - } - None => { - w.line(format!("{decl}({}) {{ {call} }}", params_sig.join(", "))); - } - } -} - -/// The `external` JNI launcher parameter list for an async callable: the raw -/// `handle` receiver for methods, lowered input slots, the optional cancel -/// token, then the boxed continuation. -pub(crate) fn kotlin_async_native_params(f: &FnBinding, has_self: bool) -> Vec { - let mut chain: Vec = Vec::new(); - if has_self { - chain.push("selfHandle: Long".to_string()); - } - chain.extend( - f.params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), kotlin_jni_type(&p.ty))), - ); - if f.cancellable { - chain.push("cancelToken: Long".to_string()); - } - chain.push("callback: Any".to_string()); - chain -} - -/// Render an async callable: the private `external` launcher declaration -/// (unless the caller declares it elsewhere, as interface companions do) plus -/// the public `suspend fun` wrapper that resumes through `WeaveContinuation` -/// and maps error codes to exceptions via `error_mapper`. -/// -/// The external launcher crosses into JNI C, which declares raw JNI types -/// (`jlong` for handles/structs/interfaces, `jint` for enums), so its -/// signature uses the lowered types and the suspend wrapper unwraps -/// (`.handle` / `.value`) exactly like the sync path. Passing a wrapper object -/// where the C side reads a `jlong` is undefined behaviour (the pointer-sized -/// register holds a JVM reference). -#[allow(clippy::too_many_arguments)] -pub(crate) fn render_kotlin_async_fun( - out: &mut String, - f: &FnBinding, - public_name: &str, - native_name: &str, - has_self: bool, - modifier: &str, - emit_native: bool, - depth: usize, - error_mapper: &str, -) { - let mut w = CodeWriter::four_space().with_depth(depth); - if emit_native { - w.line(format!( - "@JvmStatic private external fun {}({})", - native_name, - kotlin_async_native_params(f, has_self).join(", ") - )); - } - - let params_sig: Vec = f - .params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), kotlin_type(&p.ty))) - .collect(); - let public_ret = f - .ret - .as_ref() - .map(kotlin_type) - .unwrap_or_else(|| "Unit".to_string()); - // The continuation resumes with the value the JNI callback boxes (the - // lowered type); buffered/enum/class returns are re-wrapped after the - // await. - let jni_ret = f - .ret - .as_ref() - .map(kotlin_jni_type) - .unwrap_or_else(|| "Unit".to_string()); - let mut call_args: Vec = Vec::new(); - if has_self { - call_args.push("handle".to_string()); - } - call_args.extend(f.params.iter().map(kotlin_unwrap_arg)); - if f.cancellable { - call_args.push("0L".to_string()); - } - call_args.push(format!("WeaveContinuation(cont) {error_mapper}")); - if let Some(msg) = &f.deprecated { - w.line(format!("@Deprecated(\"{}\")", msg.replace('"', "\\\""))); - } - - // Map the resumed (lowered) value back to the public type. - match kotlin_wrap_return(f.ret.as_ref(), "raw") { - Some(wrap) => { - w.line(format!( - "{modifier}suspend fun {public_name}({}): {public_ret} {{", - params_sig.join(", ") - )); - w.scope(|w| { - w.line(format!( - "val raw: {jni_ret} = suspendCancellableCoroutine {{ cont ->" - )); - w.scope(|w| { - w.line(format!("{}({})", native_name, call_args.join(", "))); - }); - w.line("}"); - w.line(format!("return {wrap}")); - }); - w.line("}"); - } - None => { - w.line(format!( - "{modifier}suspend fun {public_name}({}): {public_ret} = suspendCancellableCoroutine {{ cont ->", - params_sig.join(", ") - )); - w.scope(|w| { - w.line(format!("{}({})", native_name, call_args.join(", "))); - }); - w.line("}"); - } - } - out.push_str(&w.finish()); -} - -/// The Kotlin `external` declaration name for an interface member: `native` + -/// the member's PascalCase name, with an `Async` suffix for async members -/// (`nativeAdd`, `nativeFetchAsync`). The JNI C bridge exports the matching -/// `Java___` symbol. -pub(crate) fn interface_native_name(f: &FnBinding) -> String { - let base = format!("native{}", pascal_case(&f.name)); - if f.is_async { - format!("{base}Async") - } else { - base - } -} - -/// The full `external fun` declaration line for one interface member. Instance -/// methods take the raw receiver as a leading `selfHandle: Long`; every slot -/// uses the lowered JNI type, matching the C bridge exactly. -pub(crate) fn interface_native_decl(f: &FnBinding, has_self: bool) -> String { - if f.is_async { - return format!( - "@JvmStatic private external fun {}({})", - interface_native_name(f), - kotlin_async_native_params(f, has_self).join(", ") - ); - } - let mut params: Vec = Vec::new(); - if has_self { - params.push("selfHandle: Long".to_string()); - } - params.extend( - f.params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), kotlin_jni_type(&p.ty))), - ); - let ret = f - .ret - .as_ref() - .map(kotlin_jni_type) - .unwrap_or_else(|| "Unit".to_string()); - format!( - "@JvmStatic private external fun {}({}): {}", - interface_native_name(f), - params.join(", "), - ret - ) -} - -/// The lowered call expression for one interface member: the native name -/// applied to the receiver handle (when `self_arg` is set) and the unwrapped -/// public arguments. -pub(crate) fn interface_native_call(f: &FnBinding, self_arg: Option<&str>) -> String { - let mut args: Vec = Vec::new(); - if let Some(s) = self_arg { - args.push(s.to_string()); - } - args.extend(f.params.iter().map(kotlin_unwrap_arg)); - format!("{}({})", interface_native_name(f), args.join(", ")) -} diff --git a/crates/weaveffi-gen-kotlin/src/codec.rs b/crates/weaveffi-gen-kotlin/src/codec.rs deleted file mode 100644 index 2ba6858f..00000000 --- a/crates/weaveffi-gen-kotlin/src/codec.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Value-buffer codec expressions: the Kotlin read and write expression for -//! every wire shape, dispatched on the shared [`Ty::wire`] classification. - -use weaveffi_core::model::Ty; -use weaveffi_core::model::{Prim, WireType}; -use weaveffi_core::utils::local_type_name; - -use crate::types::kt_escape; - -/// The Kotlin statement writing `expr` (typed as the public Kotlin type of -/// `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. -/// -/// An object inside a buffer is written as a token carrying one strong -/// reference: the wrapper's `cloneHandle()` calls the interface's `_clone` -/// symbol, so the encoding never captures the pointer the wrapper still -/// owns. -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) => 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::Prim(Prim::String) => format!("{w}.writeString({expr})"), - WireType::Prim(Prim::Bytes) => format!("{w}.writeBytes({expr})"), - WireType::Object(_) => format!("{w}.writeI64({expr}.cloneHandle())"), - WireType::User(name) => format!("pack{}({w}, {expr})", local_type_name(name)), - WireType::Optional(inner) => { - let v = format!("v{depth}"); - format!( - "{w}.writeOptional({expr}) {{ {v} -> {} }}", - kt_write_expr(inner, w, &v, depth + 1) - ) - } - WireType::List(inner) => { - let v = format!("v{depth}"); - format!( - "{w}.writeList({expr}) {{ {v} -> {} }}", - kt_write_expr(inner, w, &v, depth + 1) - ) - } - WireType::Map(k, v) => { - let kv = format!("k{depth}"); - let vv = format!("v{depth}"); - format!( - "{w}.writeMap({expr}, {{ {kv} -> {} }}, {{ {vv} -> {} }})", - kt_write_expr(k, w, &kv, depth + 1), - kt_write_expr(v, w, &vv, depth + 1) - ) - } - } -} - -/// The Kotlin expression reading a value of type `t` from the value-buffer -/// reader named `r`. The inverse of [`kt_write_expr`]. An object token is -/// adopted into a new wrapper instance, which owes the reference's `_destroy`. -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) => 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::Prim(Prim::String) => format!("{r}.readString()"), - WireType::Prim(Prim::Bytes) => format!("{r}.readBytes()"), - WireType::Object(name) => { - format!("{}.fromHandle({r}.readObject())", local_type_name(name)) - } - WireType::User(name) => format!("unpack{}({r})", local_type_name(name)), - WireType::Optional(inner) => { - format!("{r}.readOptional {{ {} }}", kt_read_expr(inner, r)) - } - WireType::List(inner) => format!("{r}.readList {{ {} }}", kt_read_expr(inner, r)), - WireType::Map(k, v) => format!( - "{r}.readMap({{ {} }}, {{ {} }})", - kt_read_expr(k, r), - kt_read_expr(v, r) - ), - } -} - -/// The Kotlin expression packing the public value `expr` of type `t` into a -/// freshly encoded `ByteArray`. -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: &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: &Ty, w: &str, field: &str) -> String { - kt_write_expr(ty, w, &format!("v.{}", kt_escape(field)), 0) -} diff --git a/crates/weaveffi-gen-kotlin/src/entities.rs b/crates/weaveffi-gen-kotlin/src/entities.rs deleted file mode 100644 index 72b684ec..00000000 --- a/crates/weaveffi-gen-kotlin/src/entities.rs +++ /dev/null @@ -1,694 +0,0 @@ -//! Kotlin entity rendering: plain and rich enums, record data classes, the -//! exception hierarchy, `Cleaner`-backed interface wrapper classes, the lazy -//! iterator classes, and callback interfaces with their JNI dispatch objects, -//! plus the per-type buffer codecs. - -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, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, - IteratorBinding, StructBinding, -}; -use weaveffi_core::utils::local_type_name; - -use crate::calls::{ - interface_native_call, interface_native_decl, interface_native_name, kotlin_error_mapper, - render_kotlin_async_fun, write_kotlin_sync_wrapper, -}; -use crate::codec::{kt_decode_expr, kt_read_expr, kt_write_field}; -use crate::docs::{splice, writer_doc, writer_fn_doc}; -use crate::types::{ - camel_params, kotlin_callback_dispatch_name, kotlin_iterator_class_name, - kotlin_shim_param_type, kotlin_type, kt_escape, kt_param, -}; - -/// The Kotlin exception type for an error domain: the shared exception brand -/// naming, so `KvError` becomes `KvException`. -pub(crate) fn kotlin_exception_name(eb: &ErrorBinding) -> String { - errors::exception_type_name(&eb.name) -} - -/// Render a C-style enum as a Kotlin `enum class` with a raw `value` and a -/// `fromValue` companion factory; rich (algebraic) enums divert to -/// [`render_kotlin_rich_enum`]. -pub(crate) fn render_kotlin_enum(out: &mut String, e: &EnumBinding) { - // A rich (algebraic) enum is a value type crossing the ABI in a value - // buffer, so it is emitted as a sealed class with per-variant subtypes, - // never as a plain `enum class`. - if e.is_rich() { - render_kotlin_rich_enum(out, e); - return; - } - let mut w = CodeWriter::four_space(); - w.blank(); - writer_doc(&mut w, &e.doc); - w.line(format!("enum class {}(val value: Int) {{", e.name)); - w.scope(|w| { - for (i, v) in e.variants.iter().enumerate() { - writer_doc(w, &v.doc); - let comma = if i < e.variants.len() - 1 { "," } else { ";" }; - w.line(format!("{}({}){}", kt_escape(&v.name), v.value, comma)); - } - w.blank(); - w.line("companion object {"); - w.scope(|w| { - w.line(format!( - "fun fromValue(value: Int): {} = entries.first {{ it.value == value }}", - e.name - )); - }); - w.line("}"); - }); - w.line("}"); - out.push_str(&w.finish()); -} - -/// Render a rich (algebraic) enum as a sealed class with one `data class` -/// per data-carrying variant and one `object` per unit variant, plus the -/// internal `pack{Name}`/`unpack{Name}` buffer codecs. Values cross the ABI -/// serialized in value buffers (an `i32` tag followed by the active variant's -/// fields in declaration order); no C symbols exist for a rich enum. -pub(crate) fn render_kotlin_rich_enum(out: &mut String, e: &EnumBinding) { - let name = &e.name; - let mut w = CodeWriter::four_space(); - w.blank(); - writer_doc(&mut w, &e.doc); - w.line(format!("sealed class {name} {{")); - w.scope(|w| { - for v in &e.variants { - writer_doc(w, &v.doc); - let vn = pascal_case(&v.name); - if v.fields.is_empty() { - w.line(format!("object {vn} : {name}()")); - } else if v.fields.iter().any(|f| f.doc.is_some()) { - w.line(format!("data class {vn}(")); - w.scope(|w| { - for f in &v.fields { - writer_doc(w, &f.doc); - w.line(format!( - "val {}: {},", - kt_escape(&f.name), - kotlin_type(&f.ty) - )); - } - }); - w.line(format!(") : {name}()")); - } else { - let fields: Vec = v - .fields - .iter() - .map(|f| format!("val {}: {}", kt_escape(&f.name), kotlin_type(&f.ty))) - .collect(); - w.line(format!("data class {vn}({}) : {name}()", fields.join(", "))); - } - } - }); - w.line("}"); - out.push_str(&w.finish()); - render_kotlin_rich_enum_codecs(out, e); -} - -/// Render the internal buffer codecs for one rich enum: `pack{Name}` writes -/// the `i32` tag then the active variant's fields; `unpack{Name}` dispatches -/// on the tag and rejects unknown values. -pub(crate) fn render_kotlin_rich_enum_codecs(out: &mut String, e: &EnumBinding) { - let name = &e.name; - let mut w = CodeWriter::four_space(); - w.blank(); - w.line(format!( - "internal fun pack{name}(w: WeaveBufferWriter, v: {name}) {{" - )); - w.scope(|w| { - w.line("when (v) {"); - w.scope(|w| { - for v in &e.variants { - let vn = pascal_case(&v.name); - if v.fields.is_empty() { - w.line(format!("is {name}.{vn} -> w.writeI32({})", v.value)); - } else { - w.line(format!("is {name}.{vn} -> {{")); - w.scope(|w| { - w.line(format!("w.writeI32({})", v.value)); - for f in &v.fields { - w.line(kt_write_field(&f.ty, "w", &f.name)); - } - }); - w.line("}"); - } - } - }); - w.line("}"); - }); - w.line("}"); - w.blank(); - w.line(format!( - "internal fun unpack{name}(r: WeaveBufferReader): {name} = when (val tag = r.readI32()) {{" - )); - w.scope(|w| { - for v in &e.variants { - let vn = pascal_case(&v.name); - if v.fields.is_empty() { - w.line(format!("{} -> {name}.{vn}", v.value)); - } else { - let args: Vec = v.fields.iter().map(|f| kt_read_expr(&f.ty, "r")).collect(); - w.line(format!("{} -> {name}.{vn}({})", v.value, args.join(", "))); - } - } - w.line(format!( - "else -> throw {}(-2, \"malformed WeaveFFI value buffer: unknown {name} tag $tag\")", - errors::EXCEPTION_BRAND - )); - }); - w.line("}"); - out.push_str(&w.finish()); -} - -/// Render the exception surface: the open generic brand exception plus one -/// sealed exception class per *declared* error domain, each with a per-code -/// subclass and a `fromCode` factory mapping raw ABI codes (and the optional -/// serialized payload) to typed instances. Codes that declare payload fields -/// expose them as constructor properties, decoded from the value buffer. -/// -/// Domain codes are validated positive-only, and the negative range is -/// reserved for the runtime (generic error, producer panic, marshalling -/// failure, a callback interface implementation raised), so `fromCode` maps -/// only the declared codes and every other code (all negatives included) -/// falls through to the generic branded exception. -pub(crate) fn render_kotlin_error_types(out: &mut String, model: &BindingModel) { - let mut w = CodeWriter::four_space(); - w.blank(); - w.line("/** Generic WeaveFFI failure: panics, marshalling errors, callback implementation failures (code -4), and unknown codes. */"); - w.line(format!( - "open class {}(val code: Int, message: String) : Exception(message)", - errors::EXCEPTION_BRAND - )); - for m in &model.modules { - let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) else { - continue; - }; - let exc = kotlin_exception_name(eb); - w.blank(); - w.line(format!( - "/** Typed error domain `{}` declared by module `{}`. */", - eb.name, eb.owner_path - )); - w.line(format!( - "sealed class {exc}(code: Int, message: String) : {}(code, message) {{", - errors::EXCEPTION_BRAND - )); - w.scope(|w| { - for ec in &eb.codes { - writer_doc(w, &ec.doc); - let default_msg = ec.message.replace('"', "\\\""); - if ec.fields.is_empty() { - w.line(format!( - "class {}(message: String = \"{default_msg}\") : {exc}({}, message)", - errors::pascal(&ec.name), - ec.value - )); - } else { - // Payload fields become constructor properties after the - // message, in declaration (and wire) order. - let fields: Vec = ec - .fields - .iter() - .map(|f| format!("val {}: {}", kt_escape(&f.name), kotlin_type(&f.ty))) - .collect(); - w.line(format!( - "class {}(message: String = \"{default_msg}\", {}) : {exc}({}, message)", - errors::pascal(&ec.name), - fields.join(", "), - ec.value - )); - } - } - w.blank(); - w.line("companion object {"); - w.scope(|w| { - w.line(format!( - "/** Map a raw `{}` code and payload to the typed exception; unknown codes yield the generic [{}]. */", - eb.name, - errors::EXCEPTION_BRAND - )); - w.line(format!( - "@JvmStatic fun fromCode(code: Int, message: String, payload: ByteArray?): {} = when (code) {{", - errors::EXCEPTION_BRAND - )); - w.scope(|w| { - for ec in &eb.codes { - let ctor = errors::pascal(&ec.name); - if ec.fields.is_empty() { - w.line(format!("{} -> {ctor}(message)", ec.value)); - } else { - // A missing payload violates the contract for a - // code with declared fields; fall back to the - // generic exception rather than fabricate values. - let reads: Vec = ec - .fields - .iter() - .map(|f| kt_read_expr(&f.ty, "r")) - .collect(); - w.line(format!( - "{} -> if (payload != null) weaveDecode(payload) {{ r -> {ctor}(message, {}) }} else {}(code, message)", - ec.value, - reads.join(", "), - errors::EXCEPTION_BRAND - )); - } - } - w.line(format!( - "else -> {}(code, message)", - errors::EXCEPTION_BRAND - )); - }); - w.line("}"); - }); - w.line("}"); - }); - w.line("}"); - } - out.push_str(&w.finish()); -} - -/// Render a record as a plain Kotlin `data class` with typed properties, plus -/// the internal `pack{Name}`/`unpack{Name}` buffer codecs. Records are value -/// types crossing the ABI serialized in value buffers (fields in declaration -/// order); they have no C symbols, native handles, or disposal. -pub(crate) fn render_kotlin_struct(out: &mut String, s: &StructBinding) { - let mut w = CodeWriter::four_space(); - w.blank(); - writer_doc(&mut w, &s.doc); - if s.fields.is_empty() { - w.line(format!("class {}", s.name)); - } else if s.fields.iter().any(|f| f.doc.is_some()) { - w.line(format!("data class {}(", s.name)); - w.scope(|w| { - for f in &s.fields { - writer_doc(w, &f.doc); - w.line(format!( - "val {}: {},", - kt_escape(&f.name), - kotlin_type(&f.ty) - )); - } - }); - w.line(")"); - } else { - let fields: Vec = s - .fields - .iter() - .map(|f| format!("val {}: {}", kt_escape(&f.name), kotlin_type(&f.ty))) - .collect(); - w.line(format!("data class {}({})", s.name, fields.join(", "))); - } - out.push_str(&w.finish()); - render_kotlin_struct_codecs(out, s); -} - -/// Render the internal buffer codecs for one record: `pack{Name}` writes the -/// fields in declaration order; `unpack{Name}` reads them back. -pub(crate) fn render_kotlin_struct_codecs(out: &mut String, s: &StructBinding) { - let name = &s.name; - let mut w = CodeWriter::four_space(); - w.blank(); - if s.fields.is_empty() { - w.line(format!( - "internal fun pack{name}(w: WeaveBufferWriter, v: {name}) {{}}" - )); - w.blank(); - w.line(format!( - "internal fun unpack{name}(r: WeaveBufferReader): {name} = {name}()" - )); - out.push_str(&w.finish()); - return; - } - w.line(format!( - "internal fun pack{name}(w: WeaveBufferWriter, v: {name}) {{" - )); - w.scope(|w| { - for f in &s.fields { - w.line(kt_write_field(&f.ty, "w", &f.name)); - } - }); - w.line("}"); - w.blank(); - w.line(format!( - "internal fun unpack{name}(r: WeaveBufferReader): {name} = {name}(" - )); - w.scope(|w| { - for f in &s.fields { - w.line(format!("{},", kt_read_expr(&f.ty, "r"))); - } - }); - w.line(")"); - out.push_str(&w.finish()); -} - -/// 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: &Ty) -> String { - // A buffered element crosses as a packed `ByteArray`: decode it into the - // idiomatic Kotlin value. - if elem.is_buffered() { - return kt_decode_expr(elem, "(raw as ByteArray)"); - } - match elem { - Ty::Enum(name) => format!("{}.fromValue(raw as Int)", local_type_name(name)), - Ty::Interface(name) => format!("{}.fromHandle(raw as Long)", local_type_name(name)), - // Only `Interface?` reaches here: 0L crosses for none. - Ty::Optional(inner) => { - let Ty::Interface(name) = inner.as_ref() else { - unreachable!("non-interface optionals are buffered") - }; - format!( - "(raw as Long).takeIf {{ it != 0L }}?.let {{ {}.fromHandle(it) }}", - local_type_name(name) - ) - } - other => format!("raw as {}", kotlin_type(other)), - } -} - -/// Render the lazy Kotlin iterator wrapper class for one `iter` callable. -/// The class implements `Iterator` with a lookahead slot (one producer -/// `next` per consumer step) and `AutoCloseable` disposal through a -/// `WeaveNativeRef` registered with the shared `Cleaner`, so an abandoned -/// iterator's native handle is still destroyed exactly once. -pub(crate) fn render_kotlin_iterator_class( - out: &mut String, - f: &FnBinding, - it: &IteratorBinding, - c_prefix: &str, -) { - let class = kotlin_iterator_class_name(it, c_prefix); - let elem_pub = kotlin_type(&it.elem); - let convert = kotlin_iter_elem_convert(&it.elem); - let mut w = CodeWriter::four_space(); - w.blank(); - w.line("/**"); - w.line(format!( - " * A lazy iterator over the `{}` elements streamed by [{}]. Each step pulls", - elem_pub, - kt_param(&f.name) - )); - w.line(" * exactly one element from the native producer. The native handle is"); - w.line(" * released when the producer is exhausted, when [close] is called, or by"); - w.line(" * the `Cleaner` if the iterator is abandoned, whichever comes first."); - w.line(" */"); - w.line(format!( - "class {class} internal constructor(handle: Long) : Iterator<{elem_pub}>, AutoCloseable {{" - )); - w.scope(|w| { - w.line("private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) }"); - w.line("private val cleanable = weaveCleaner.register(this, ref)"); - w.line("private var nextSlot: Array? = null"); - w.blank(); - w.line("override fun hasNext(): Boolean {"); - w.scope(|w| { - w.line("if (nextSlot != null) return true"); - w.line("if (ref.peek() == 0L) return false"); - w.line("val slot = nativeNext(ref.get())"); - w.line("if (slot == null) {"); - w.scope(|w| { - w.line("close()"); - w.line("return false"); - }); - w.line("}"); - w.line("nextSlot = slot"); - w.line("return true"); - }); - w.line("}"); - w.blank(); - w.line(format!("override fun next(): {elem_pub} {{")); - w.scope(|w| { - w.line("if (!hasNext()) throw NoSuchElementException()"); - w.line("val raw = nextSlot!![0]"); - w.line("nextSlot = null"); - w.line(format!("return {convert}")); - }); - w.line("}"); - w.blank(); - w.line("/** Releases the native iterator; safe to call more than once. */"); - w.line("override fun close() {"); - w.scope(|w| { - w.line("cleanable.clean()"); - }); - w.line("}"); - w.blank(); - w.line("companion object {"); - w.scope(|w| { - w.line("init { WeaveNativeLibrary.ensureLoaded() }"); - w.blank(); - w.line("@JvmStatic private external fun nativeNext(handle: Long): Array?"); - w.line("@JvmStatic private external fun nativeDestroy(handle: Long)"); - }); - w.line("}"); - }); - w.line("}"); - out.push_str(&w.finish()); -} - -/// Render the Kotlin class for one interface: the wrapper owns one strong -/// native reference through a `WeaveNativeRef` registered with the shared -/// `Cleaner`, so `destroy` runs exactly once, from `close()` (`AutoCloseable`) -/// or from the cleaner backstop. `cloneHandle()` exposes the `_clone` symbol -/// for the value-buffer codec. Companion factories wrap constructors (the -/// `new` constructor becomes `operator fun invoke`) and statics; instance -/// methods pass the borrowed handle as the leading native argument. Async -/// members become `suspend fun`s resuming through `WeaveContinuation` with -/// `error`-typed exception mapping. -pub(crate) fn render_kotlin_interface( - out: &mut String, - i: &InterfaceBinding, - error: Option<&ErrorBinding>, - c_prefix: &str, -) { - let mut w = CodeWriter::four_space(); - w.blank(); - writer_doc(&mut w, &i.doc); - // The raw-handle constructor is private (not internal) so that a `new` - // constructor taking one integer, exposed as `operator fun invoke(Long)`, - // can't be shadowed by it when a consumer is compiled in the same module: - // Kotlin prefers a visible constructor over the invoke convention, and an - // `internal` one would silently adopt the consumer's integer as a pointer. - w.line(format!( - "class {} private constructor(handle: Long) : AutoCloseable {{", - i.name - )); - w.scope(|w| { - w.line("private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) }"); - w.line("private val cleanable = weaveCleaner.register(this, ref)"); - w.blank(); - w.line("/** The native pointer, borrowed for one call; throws after [close]. */"); - w.line("internal val handle: Long get() = ref.get()"); - w.blank(); - w.line("/** A new strong reference, written into value buffers as an object token. */"); - w.line("internal fun cloneHandle(): Long = nativeClone(ref.get())"); - w.blank(); - w.line("companion object {"); - w.scope(|w| { - w.line("init { WeaveNativeLibrary.ensureLoaded() }"); - w.blank(); - w.line("/** Adopts one strong native reference (a constructor return, a `_clone`, or a buffer token) into a wrapper that owes its `_destroy`. */"); - w.line(format!( - "internal fun fromHandle(handle: Long): {} = {}(handle)", - i.name, i.name - )); - for f in i.constructors.iter().chain(i.statics.iter()) { - w.line(interface_native_decl(f, false)); - } - for f in &i.methods { - w.line(interface_native_decl(f, true)); - } - w.line("@JvmStatic private external fun nativeClone(handle: Long): Long"); - w.line("@JvmStatic private external fun nativeDestroy(handle: Long)"); - - // Constructors are never async (validation rejects that), so each - // is a plain factory; `new` becomes `operator fun invoke` so - // construction reads as `Store(...)`. - for c in &i.constructors { - w.blank(); - writer_fn_doc(w, &c.doc, &camel_params(&c.params)); - let decl = if c.name == "new" { - "operator fun invoke".to_string() - } else { - format!("fun {}", kt_param(&c.name)) - }; - let call = interface_native_call(c, None); - write_kotlin_sync_wrapper(w, c, &decl, &call, c_prefix); - } - for f in &i.statics { - w.blank(); - writer_fn_doc(w, &f.doc, &camel_params(&f.params)); - if f.is_async { - let mapper = kotlin_error_mapper(f, error); - splice(w, |o| { - render_kotlin_async_fun( - o, - f, - &kt_param(&f.name), - &interface_native_name(f), - false, - "", - false, - 2, - &mapper, - ) - }); - } else { - let decl = format!("fun {}", kt_param(&f.name)); - let call = interface_native_call(f, None); - write_kotlin_sync_wrapper(w, f, &decl, &call, c_prefix); - } - } - }); - w.line("}"); - - for f in &i.methods { - w.blank(); - writer_fn_doc(w, &f.doc, &camel_params(&f.params)); - if f.is_async { - let mapper = kotlin_error_mapper(f, error); - splice(w, |o| { - render_kotlin_async_fun( - o, - f, - &kt_param(&f.name), - &interface_native_name(f), - true, - "", - false, - 1, - &mapper, - ) - }); - } else { - let decl = format!("fun {}", kt_param(&f.name)); - let call = interface_native_call(f, Some("handle")); - write_kotlin_sync_wrapper(w, f, &decl, &call, c_prefix); - } - } - w.blank(); - - w.line("/** Releases this wrapper's native reference; safe to call more than once. */"); - w.line("override fun close() {"); - w.scope(|w| { - w.line("cleanable.clean()"); - }); - w.line("}"); - }); - w.line("}"); - out.push_str(&w.finish()); -} - -/// The Kotlin expression converting one callback-method argument as the JNI -/// trampoline delivers it (see [`kotlin_shim_param_type`]) into the public -/// type the implementation receives: buffers decode, enums round-trip through -/// `fromValue`, and object pointers are adopted into a new wrapper (`0L` -/// becomes `null` for `Interface?`). -fn kotlin_shim_arg_convert(t: &Ty, expr: &str) -> String { - if t.is_buffered() { - return kt_decode_expr(t, expr); - } - match t { - Ty::Enum(name) => format!("{}.fromValue({expr})", local_type_name(name)), - Ty::Interface(name) => format!("{}.fromHandle({expr})", local_type_name(name)), - Ty::Optional(inner) => { - let Ty::Interface(name) = inner.as_ref() else { - unreachable!("non-interface optionals are buffered") - }; - format!( - "{expr}.takeIf {{ it != 0L }}?.let {{ {}.fromHandle(it) }}", - local_type_name(name) - ) - } - _ => expr.to_string(), - } -} - -/// Render one callback interface: the public Kotlin `interface` the consumer -/// implements, plus the internal `{Name}Jni` dispatch object whose -/// `@JvmStatic` shims the native vtable trampolines call through cached -/// `jmethodID`s. Each shim takes the implementing object first, then every -/// argument in its trampoline form, converts them (decoding buffers and -/// adopting object references), and forwards to the implementation; an enum -/// return is lowered back to its raw `Int`. -pub(crate) fn render_kotlin_callback_interface(out: &mut String, cb: &CallbackInterfaceBinding) { - let name = &cb.name; - let dispatch = kotlin_callback_dispatch_name(name); - let mut w = CodeWriter::four_space(); - w.blank(); - writer_doc(&mut w, &cb.doc); - if let Some(msg) = &cb.deprecated { - w.line(format!("@Deprecated(\"{}\")", msg.replace('"', "\\\""))); - } - w.line(format!("interface {name} {{")); - w.scope(|w| { - for m in &cb.methods { - writer_fn_doc(w, &m.doc, &camel_params(&m.params)); - if let Some(msg) = &m.deprecated { - w.line(format!("@Deprecated(\"{}\")", msg.replace('"', "\\\""))); - } - let params: Vec = m - .params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), kotlin_type(&p.ty))) - .collect(); - let ret = m - .ret - .as_ref() - .map(|t| format!(": {}", kotlin_type(t))) - .unwrap_or_default(); - w.line(format!( - "fun {}({}){ret}", - kt_param(&m.name), - params.join(", ") - )); - } - }); - w.line("}"); - w.blank(); - w.line(format!( - "/** JNI dispatch shims for [{name}]; the native vtable trampolines call these, nothing else does. */" - )); - w.line(format!("internal object {dispatch} {{")); - w.scope(|w| { - for m in &cb.methods { - let mut params = vec![format!("impl: {name}")]; - params.extend( - m.params - .iter() - .map(|p| format!("{}: {}", kt_param(&p.name), kotlin_shim_param_type(&p.ty))), - ); - let args: Vec = m - .params - .iter() - .map(|p| kotlin_shim_arg_convert(&p.ty, &kt_param(&p.name))) - .collect(); - let call = format!("impl.{}({})", kt_param(&m.name), args.join(", ")); - let line = match &m.ret { - None => format!( - "@JvmStatic fun {}({}) {{ {call} }}", - kt_param(&m.name), - params.join(", ") - ), - Some(Ty::Enum(_)) => format!( - "@JvmStatic fun {}({}): Int = {call}.value", - kt_param(&m.name), - params.join(", ") - ), - Some(t) => format!( - "@JvmStatic fun {}({}): {} = {call}", - kt_param(&m.name), - params.join(", "), - kotlin_shim_param_type(t) - ), - }; - w.line(line); - } - }); - w.line("}"); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-kotlin/src/jni.rs b/crates/weaveffi-gen-kotlin/src/jni.rs deleted file mode 100644 index f066b834..00000000 --- a/crates/weaveffi-gen-kotlin/src/jni.rs +++ /dev/null @@ -1,1100 +0,0 @@ -//! JNI C bridge emission: sync and async exports, interface natives -//! (`nativeClone`/`nativeDestroy` included), the callback-interface vtable -//! trampolines and static vtables, iterator natives, and the parameter and -//! return marshalling helpers they share. - -use std::fmt::Write as _; - -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{ - CallShape, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, InterfaceBinding, - IteratorBinding, ModuleBinding, ParamBinding, -}; -use weaveffi_core::plan::{self, ArgPass, Free, RetPass}; -use weaveffi_core::utils::c_abi_struct_name; - -use crate::calls::interface_native_name; -use crate::docs::splice; -use crate::runtime::{jni_cb_class_var, jni_cb_method_var, jni_thrower_for}; -use crate::types::{ - c_default_value, c_local, c_type_for_return, jni_call_kind, jni_cast_for, jni_default_return, - jni_mangle, jni_param_type, jni_ret_type, kotlin_fn_name, kotlin_iterator_class_name, -}; - -/// The C identifier of the process-wide static vtable the shim passes for a -/// callback interface whose C tag is `c_tag`. -pub(crate) fn jni_vtable_var(c_tag: &str) -> String { - format!("{c_tag}_jni_vtable") -} - -/// Emit one synchronous JNI export (`Java___`). Interface -/// methods pass `self_cast` (the C expression casting `selfHandle` back to the -/// receiver pointer), which becomes the leading C call argument. -#[allow(clippy::too_many_arguments)] -pub(crate) fn render_jni_sync_export( - jni_c: &mut String, - f: &FnBinding, - class_name: &str, - jni_method: &str, - self_cast: Option<&str>, - thrower: &str, - jni_prefix: &str, - module_path: &str, - c_prefix: &str, -) { - let jret = jni_ret_type(f.ret.as_ref()); - let mut jparams: Vec = vec!["JNIEnv* env".into(), "jclass clazz".into()]; - if self_cast.is_some() { - jparams.push("jlong selfHandle".into()); - } - for p in &f.params { - jparams.push(format!("{} {}", jni_param_type(&p.ty), c_local(&p.name))); - } - let _ = writeln!( - jni_c, - "JNIEXPORT {} JNICALL Java_{}_{}_{}({}) {{", - jret, - jni_prefix, - class_name, - jni_mangle(jni_method), - jparams.join(", ") - ); - let _ = writeln!(jni_c, " weaveffi_error err = {{0, NULL, NULL, 0}};"); - - for p in &f.params { - write_param_acquire(jni_c, p); - } - - let c_sym = &f.c_base; - let mut call_args: Vec = Vec::new(); - if let Some(cast) = self_cast { - call_args.push(cast.to_string()); - } - for p in &f.params { - build_c_call_args(&mut call_args, p, module_path, c_prefix); - } - - // An iterator-returning callable launches the C iterator and hands the - // opaque handle back as a `jlong`; the Kotlin wrapper class then pulls one - // element per `nativeNext` call (see `render_jni_iterator_natives`). - if let CallShape::Iterator(it) = &f.shape { - write_iterator_launch(jni_c, it, &call_args, &f.params, thrower); - let _ = writeln!(jni_c, "}}\n"); - return; - } - - // Bytes and buffered returns share the `const uint8_t*` + trailing - // `size_t* out_len` shape. - let needs_out_len = matches!( - plan::ret_pass(f.ret.as_ref(), module_path, c_prefix), - RetPass::Bytes | RetPass::Buffer - ); - if needs_out_len { - let _ = writeln!(jni_c, " size_t out_len = 0;"); - } - - if f.ret.is_some() { - write_return_handling( - jni_c, - c_sym, - &call_args, - f.ret.as_ref(), - &f.params, - module_path, - c_prefix, - thrower, - ); - } else { - let args_str = call_args.join(", "); - let _ = writeln!( - jni_c, - " {}({});", - c_sym, - join_call_args(&args_str, "&err") - ); - release_jni_resources(jni_c, &f.params); - write_error_check(jni_c, f.ret.as_ref(), thrower); - let _ = writeln!(jni_c, " return;"); - } - - let _ = writeln!(jni_c, "}}\n"); -} - -/// Emit the JNI bridge for one interface: constructor, static, and method -/// exports named `Java___native` (methods take the -/// leading `selfHandle`), plus the `nativeClone` and `nativeDestroy` exports -/// wrapping the interface's reference-count pair. -pub(crate) fn render_jni_interface( - jni_c: &mut String, - m: &ModuleBinding, - i: &InterfaceBinding, - jni_prefix: &str, - c_prefix: &str, -) { - let self_cast = format!("(const {}*)(intptr_t)selfHandle", i.c_tag); - for f in i.constructors.iter().chain(i.statics.iter()) { - let thrower = jni_thrower_for(f, m.error.as_ref()); - if f.is_async { - render_jni_async_function( - jni_c, - &m.path, - f, - &i.name, - &interface_native_name(f), - None, - jni_prefix, - c_prefix, - ); - } else { - render_jni_sync_export( - jni_c, - f, - &i.name, - &interface_native_name(f), - None, - &thrower, - jni_prefix, - &m.path, - c_prefix, - ); - } - } - for f in &i.methods { - let thrower = jni_thrower_for(f, m.error.as_ref()); - if f.is_async { - render_jni_async_function( - jni_c, - &m.path, - f, - &i.name, - &interface_native_name(f), - Some(&self_cast), - jni_prefix, - c_prefix, - ); - } else { - render_jni_sync_export( - jni_c, - f, - &i.name, - &interface_native_name(f), - Some(&self_cast), - &thrower, - jni_prefix, - &m.path, - c_prefix, - ); - } - } - let mut w = CodeWriter::four_space(); - w.line(format!( - "JNIEXPORT jlong JNICALL Java_{}_{}_nativeClone(JNIEnv* env, jclass clazz, jlong handle) {{", - jni_prefix, i.name - )); - w.scope(|w| { - w.line(format!( - "return (jlong)(intptr_t){}((const {}*)(intptr_t)handle);", - i.clone_symbol, i.c_tag - )); - }); - w.line("}"); - w.blank(); - w.line(format!( - "JNIEXPORT void JNICALL Java_{}_{}_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) {{", - jni_prefix, i.name - )); - w.scope(|w| { - w.line(format!( - "{}(({}*)(intptr_t)handle);", - i.destroy_symbol, i.c_tag - )); - }); - w.line("}"); - w.blank(); - jni_c.push_str(&w.finish()); -} - -/// Box one owned async result into the JVM local `boxed` for delivery to -/// the pinned `WeaveContinuation`, dispatched on the [`RetPass`] plan. -/// 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`; an object result is one strong reference boxed -/// as a `Long` for the wrapper to adopt. -fn write_jni_box_result(out: &mut String, ret: Option<&Ty>, module: &str, c_prefix: &str) { - let mut w = CodeWriter::four_space().with_depth(2); - match plan::ret_pass(ret, module, c_prefix) { - RetPass::Void => { - w.line("jobject boxed = NULL;"); - } - RetPass::Buffer => { - 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);"); - } - RetPass::Bytes => { - 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); }"); - w.line("weaveffi_free_bytes((uint8_t*)result, result_len);"); - } - RetPass::String => { - w.line("jobject boxed = (jobject)weaveffi_jni_utf8_to_string(env, result);"); - w.line("weaveffi_free_string(result);"); - } - // One strong reference: the boxed `Long` carries the pointer bits for - // the wrapper to adopt; a null nullable result crosses as `null`. - RetPass::Object { nullable, .. } => { - if nullable { - w.line("jobject boxed = NULL;"); - w.block("if (result != NULL) {", "}", |w| { - splice(w, |o| { - write_boxed_scalar(o, &Ty::I64, "_opt", "(intptr_t)result", " ") - }); - w.line("boxed = _opt;"); - }); - } else { - splice(&mut w, |o| { - write_boxed_scalar(o, &Ty::I64, "boxed", "(intptr_t)result", " ") - }); - } - } - RetPass::Direct => { - let ty = ret.expect("direct results have a type"); - splice(&mut w, |o| { - write_boxed_scalar(o, ty, "boxed", "result", " ") - }); - } - } - w.line("jclass cls = (*env)->GetObjectClass(env, ctx->callback);"); - w.line( - "jmethodID mid = (*env)->GetMethodID(env, cls, \"onSuccess\", \"(Ljava/lang/Object;)V\");", - ); - w.line("(*env)->CallVoidMethod(env, ctx->callback, mid, boxed);"); - out.push_str(&w.finish()); -} - -/// Emit one async JNI export: the completion callback trampoline (delivering -/// `onError(code, message, payload)` or the boxed result to the pinned -/// `WeaveContinuation`) plus the `Java___` launcher. -/// Interface methods pass `self_cast` as the leading C launch argument. -#[allow(clippy::too_many_arguments)] -pub(crate) fn render_jni_async_function( - out: &mut String, - module_name: &str, - f: &FnBinding, - class_name: &str, - jni_method: &str, - self_cast: Option<&str>, - jni_prefix: &str, - c_prefix: &str, -) { - let c_sym = &f.c_base; - let cb_name = format!("{c_sym}_jni_cb"); - let CallShape::Async(ab) = &f.shape else { - unreachable!("render_jni_async_function requires an async call shape"); - }; - // The result-field slots come from the lowered callback signature itself - // (skipping the leading `context`/`err` pair, which the glue spells out), - // so the trampoline matches the ABI typedef exactly. - let cb_result_params: String = ab - .callback_params - .iter() - .skip(2) - .map(|slot| format!(", {} {}", slot.ty.render_c(c_prefix), slot.name)) - .collect(); - - let mut w = CodeWriter::four_space(); - w.line(format!( - "static void {cb_name}(void* context, weaveffi_error* err{cb_result_params}) {{" - )); - w.scope(|w| { - w.line("weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context;"); - // The producer invokes this from its own worker thread, which usually - // is not a JVM thread: attach if needed and detach before returning. - w.line("JNIEnv* env = NULL;"); - w.line("int attached = weaveffi_jni_attach(&env);"); - w.line("if (env == NULL) { free(ctx); return; }"); - w.line("if (err != NULL && err->code != 0) {"); - w.scope(|w| { - // The raw `(code, message, payload)` triple crosses to Kotlin, - // where the continuation's mapper picks the typed or generic - // exception (decoding payload fields when declared); producer - // threads cannot `FindClass` app classes themselves. The boxed - // error is owned: copy its fields, then release it with - // `weaveffi_error_free`. - w.line("const char* msg = err->message ? err->message : \"WeaveFFI error\";"); - w.line("jstring jmsg = weaveffi_jni_utf8_to_string(env, msg);"); - w.line("jbyteArray jpayload = NULL;"); - w.block("if (err->payload_ptr != NULL) {", "}", |w| { - w.line("jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len);"); - w.line("if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); }"); - }); - w.line("jint jcode = (jint)err->code;"); - w.line("weaveffi_error_free(err);"); - w.line("jclass cls = (*env)->GetObjectClass(env, ctx->callback);"); - w.line("jmethodID mid = (*env)->GetMethodID(env, cls, \"onError\", \"(ILjava/lang/String;[B)V\");"); - w.line("(*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload);"); - }); - w.line("} else {"); - w.scope(|w| { - splice(w, |o| { - write_jni_box_result(o, f.ret.as_ref(), module_name, c_prefix) - }); - }); - w.line("}"); - // An exception thrown by the continuation's resume path has no Kotlin - // caller on this producer thread: route it to the installed handler, - // or log it via ExceptionDescribe before clearing. - w.line("weaveffi_jni_handle_uncaught(env);"); - w.line("(*env)->DeleteGlobalRef(env, ctx->callback);"); - w.line("free(ctx);"); - w.line("weaveffi_jni_detach(attached);"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); - - let mut jparams: Vec = vec!["JNIEnv* env".into(), "jclass clazz".into()]; - if self_cast.is_some() { - jparams.push("jlong selfHandle".into()); - } - for p in &f.params { - jparams.push(format!("{} {}", jni_param_type(&p.ty), c_local(&p.name))); - } - if f.cancellable { - jparams.push("jlong cancelToken".to_string()); - } - jparams.push("jobject callback".to_string()); - - let mut w = CodeWriter::four_space(); - w.line(format!( - "JNIEXPORT void JNICALL Java_{}_{}_{}({}) {{", - jni_prefix, - class_name, - jni_mangle(jni_method), - jparams.join(", ") - )); - w.scope(|w| { - w.line("weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx));"); - w.line("ctx->callback = (*env)->NewGlobalRef(env, callback);"); - - for p in &f.params { - splice(w, |o| write_param_acquire(o, p)); - } - - let mut call_args: Vec = Vec::new(); - if let Some(cast) = self_cast { - call_args.push(cast.to_string()); - } - for p in &f.params { - build_c_call_args(&mut call_args, p, module_name, c_prefix); - } - if f.cancellable { - call_args.push("(weaveffi_cancel_token*)(intptr_t)cancelToken".to_string()); - } - call_args.push(cb_name.clone()); - call_args.push("ctx".to_string()); - - w.line(format!("{c_sym}_async({});", call_args.join(", "))); - - splice(w, |o| release_jni_resources(o, &f.params)); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the C local `var` a callback trampoline hands to the Kotlin dispatch -/// shim for one method argument, received like a return ([`RetPass`]): -/// strings, bytes, and buffers are borrowed for the call and deep-copied into -/// JVM values (the trampoline frees nothing); an object argument is one strong -/// reference passed as its raw pointer bits for the shim to adopt (`0` for a -/// null `Interface?`); direct scalars are cast to their JNI type. -fn write_jni_cb_arg(w: &mut CodeWriter, p: &ParamBinding, var: &str) { - match p.arg_pass() { - ArgPass::Direct { slot } => { - let src = c_local(&slot.name); - let jt = jni_param_type(&p.ty); - if matches!(p.ty, Ty::Bool) { - w.line(format!("{jt} {var} = {src} ? JNI_TRUE : JNI_FALSE;")); - } else { - w.line(format!("{jt} {var} = ({jt}){src};")); - } - } - ArgPass::String { slot } => { - let src = c_local(&slot.name); - w.line(format!( - "jstring {var} = weaveffi_jni_utf8_to_string(env, {src});" - )); - } - ArgPass::Bytes { ptr, len } | ArgPass::Buffer { ptr, len } => { - let (ptr, len) = (c_local(&ptr.name), c_local(&len.name)); - w.line(format!( - "jbyteArray {var} = (*env)->NewByteArray(env, (jsize){len});" - )); - w.line(format!( - "if ({var} && {ptr}) {{ (*env)->SetByteArrayRegion(env, {var}, 0, (jsize){len}, (const jbyte*){ptr}); }}" - )); - } - ArgPass::Object { slot, .. } => { - w.line(format!( - "jlong {var} = (jlong)(intptr_t){};", - c_local(&slot.name) - )); - } - ArgPass::Callback { .. } => { - unreachable!("validation rejects callback interfaces inside callback methods") - } - } -} - -/// Emit the producer-thread trampoline for one callback-interface method: -/// attach to the `JavaVM` when the calling thread isn't one, convert every C -/// argument, invoke the cached dispatch shim with the global-ref `ctx` as the -/// implementing object, and, when the JVM raised, report through -/// `{prefix}_error_set(out_err, -4, throwable.toString())` and return the -/// default value. Nothing ever unwinds through the C frame. -fn render_jni_cb_trampoline( - out: &mut String, - cb: &CallbackInterfaceBinding, - m: &CallbackMethodBinding, - c_prefix: &str, -) { - let decls: Vec = m - .abi_params - .iter() - .map(|slot| format!("{} {}", slot.ty.render_c(c_prefix), c_local(&slot.name))) - .collect(); - let out_err = m - .abi_params - .last() - .map(|s| c_local(&s.name)) - .unwrap_or_else(|| "out_err".to_string()); - let (call_kind, jret) = jni_call_kind(m.ret.as_ref()); - let default = c_default_value(m.ret.as_ref()); - let ret_stmt = if m.ret.is_some() { - format!("return {default};") - } else { - "return;".to_string() - }; - let cls = jni_cb_class_var(cb); - let mid = jni_cb_method_var(cb, &m.name); - - let mut w = CodeWriter::four_space(); - w.line(format!( - "static {} {}_jni_{}({}) {{", - m.abi_ret.render_c(c_prefix), - cb.c_tag, - m.name, - decls.join(", ") - )); - w.scope(|w| { - w.line("JNIEnv* env = NULL;"); - w.line("int attached = weaveffi_jni_attach(&env);"); - w.block("if (env == NULL) {", "}", |w| { - w.line(format!( - "{c_prefix}_error_set({out_err}, -4, \"WeaveFFI: could not attach the calling thread to the JavaVM\");" - )); - w.line(&ret_stmt); - }); - // A local frame bounds every reference created while converting, so - // bursts of calls on a long-lived producer thread cannot exhaust the - // local-ref table. - w.block("if ((*env)->PushLocalFrame(env, 16) != 0) {", "}", |w| { - w.line("(*env)->ExceptionClear(env);"); - w.line(format!( - "{c_prefix}_error_set({out_err}, -4, \"WeaveFFI: JNI local frame exhausted\");" - )); - w.line("weaveffi_jni_detach(attached);"); - w.line(&ret_stmt); - }); - let mut arg_vars: Vec = vec!["(jobject)ctx".to_string()]; - for (i, p) in m.params.iter().enumerate() { - let var = format!("_a{i}"); - write_jni_cb_arg(w, p, &var); - arg_vars.push(var); - } - let call = format!( - "(*env)->CallStatic{call_kind}Method(env, {cls}, {mid}, {})", - arg_vars.join(", ") - ); - if m.ret.is_some() { - w.line(format!("{jret} _rv = {call};")); - } else { - w.line(format!("{call};")); - } - w.block("if ((*env)->ExceptionCheck(env)) {", "}", |w| { - w.line(format!("weaveffi_jni_report_foreign(env, {out_err});")); - if m.ret.is_some() { - w.line(format!("_rv = ({jret}){default};")); - } - }); - w.line("(*env)->PopLocalFrame(env, NULL);"); - w.line("weaveffi_jni_detach(attached);"); - if m.ret.is_some() { - let cast = if matches!(m.ret, Some(Ty::Bool)) { - "_rv == JNI_TRUE".to_string() - } else { - format!("({})_rv", m.abi_ret.render_c(c_prefix)) - }; - w.line(format!("return {cast};")); - } - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the vtable surface for one callback interface: one trampoline per -/// method, the `free` entry that deletes the global reference pinning the -/// Kotlin implementation, and the process-wide static vtable the exports pass -/// alongside that global reference as `ctx`. -pub(crate) fn render_jni_callback_interface( - out: &mut String, - cb: &CallbackInterfaceBinding, - c_prefix: &str, -) { - for m in &cb.methods { - render_jni_cb_trampoline(out, cb, m, c_prefix); - } - let mut w = CodeWriter::four_space(); - w.line(format!("static void {}_jni_free(void* ctx) {{", cb.c_tag)); - w.scope(|w| { - w.line("if (ctx == NULL) { return; }"); - w.line("JNIEnv* env = NULL;"); - w.line("int attached = weaveffi_jni_attach(&env);"); - w.line("if (env != NULL) { (*env)->DeleteGlobalRef(env, (jobject)ctx); }"); - w.line("weaveffi_jni_detach(attached);"); - }); - w.line("}"); - w.blank(); - let mut entries: Vec = cb - .methods - .iter() - .map(|m| format!("{}_jni_{}", cb.c_tag, m.name)) - .collect(); - entries.push(format!("{}_jni_free", cb.c_tag)); - w.line(format!( - "static const {} {} = {{ {} }};", - cb.vtable_tag, - jni_vtable_var(&cb.c_tag), - entries.join(", ") - )); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the acquisition statements one parameter needs before the C call, -/// dispatched on its [`ArgPass`] plan: buffered and bytes parameters pin -/// their `jbyteArray` elements, strings pin UTF-8 chars, a nullable object -/// unboxes its `java.lang.Long` into the raw pointer value (0 = none), and a -/// callback interface pins the implementing object with a global reference -/// that becomes the vtable `ctx`. Direct scalars and non-null objects need -/// nothing. -fn write_param_acquire(out: &mut String, p: &ParamBinding) { - let name = c_local(&p.name); - let mut w = CodeWriter::four_space().with_depth(1); - match p.arg_pass() { - // A buffered parameter crosses as a packed `jbyteArray`: pin the - // elements for the borrowed `(ptr, len)` pair the callee decodes and - // never frees. - ArgPass::Buffer { .. } => { - w.line(format!( - "jbyte* {n}_elems = (*env)->GetByteArrayElements(env, {n}, NULL);", - n = name - )); - w.line(format!( - "jsize {n}_len = (*env)->GetArrayLength(env, {n});", - n = name - )); - } - ArgPass::String { .. } => { - w.line(format!( - "char* {n}_chars = weaveffi_jni_string_to_utf8(env, {n});", - n = name - )); - } - ArgPass::Bytes { .. } => { - w.line(format!("jboolean {n}_is_copy = 0;", n = name)); - w.line(format!( - "jbyte* {n}_elems = (*env)->GetByteArrayElements(env, {n}, &{n}_is_copy);", - n = name - )); - w.line(format!( - "jsize {n}_len = (*env)->GetArrayLength(env, {n});", - n = name - )); - } - // Only `Interface?` is a nullable object: unbox the `java.lang.Long` - // into the raw pointer value (0 = none). - ArgPass::Object { nullable: true, .. } => { - w.line(format!("int64_t {n}_val = 0;", n = name)); - w.block(format!("if ({n} != NULL) {{", n = name), "}", |w| { - w.line(format!( - "jclass {n}_cls = (*env)->FindClass(env, \"java/lang/Long\");", - n = name - )); - w.line(format!( - "jmethodID {n}_mid = (*env)->GetMethodID(env, {n}_cls, \"longValue\", \"()J\");", - n = name - )); - w.line(format!( - "{n}_val = (int64_t)(*env)->CallLongMethod(env, {n}, {n}_mid);", - n = name - )); - }); - } - // The global reference keeps the implementation alive for as long as - // the producer holds the callback; the vtable's `free` deletes it. - ArgPass::Callback { .. } => { - w.line(format!( - "jobject {n}_ref = (*env)->NewGlobalRef(env, {n});", - n = name - )); - } - ArgPass::Direct { .. } - | ArgPass::Object { - nullable: false, .. - } => {} - } - out.push_str(&w.finish()); -} - -/// Append the C call arguments one parameter lowers to, dispatched on its -/// [`ArgPass`] plan: buffered and bytes parameters pass their pinned -/// `(ptr, len)` pair, strings their pinned chars, objects the borrowed -/// pointer cast to the interface's C struct type, callback interfaces the -/// global reference as `ctx` plus the interface's static vtable, and direct -/// scalars a value cast matching the C header's declaration type. -fn build_c_call_args(args: &mut Vec, p: &ParamBinding, module: &str, c_prefix: &str) { - let name = c_local(&p.name); - match p.arg_pass() { - // A buffered parameter crosses as one borrowed `(ptr, len)` pair - // pinned from the packed `jbyteArray` by `write_param_acquire`. - ArgPass::Buffer { .. } | ArgPass::Bytes { .. } => { - args.push(format!("(const uint8_t*){n}_elems", n = name)); - args.push(format!("(size_t){n}_len", n = name)); - } - ArgPass::String { .. } => { - args.push(format!("{n}_chars", n = name)); - } - // An interface argument crosses as a borrowed `const {c_tag}*`: the - // Kotlin wrapper keeps ownership and only lends the pointer. The - // nullable spelling passes the unboxed pointer value acquired above. - ArgPass::Object { nullable, .. } => { - let iname = - p.ty.interface_name() - .expect("object-passed parameters are interfaces"); - let c_struct = c_abi_struct_name(iname, module, c_prefix); - if nullable { - args.push(format!("(const {}*)(intptr_t){}_val", c_struct, name)); - } else { - args.push(format!("(const {}*)(intptr_t){}", c_struct, name)); - } - } - ArgPass::Callback { .. } => { - let Ty::CallbackInterface(cname) = &p.ty else { - unreachable!("callback-passed parameters are callback interfaces") - }; - let c_tag = c_abi_struct_name(cname, module, c_prefix); - args.push(format!("(void*){name}_ref")); - args.push(format!("&{}", jni_vtable_var(&c_tag))); - } - ArgPass::Direct { .. } => match &p.ty { - 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::Enum(_) => args.push(format!("(int32_t){}", name)), - other => unreachable!("{other:?} is not passed directly"), - }, - } -} - -/// Format a C call argument list joined by `", "` and append the -/// out-parameter `extras` (e.g. `"&err"` or `"&out_len, &err"`). -/// -/// When `args_str` is empty (the wrapped C function takes only the -/// implicit out-params) the leading comma that would otherwise split -/// the empty user args from `extras` is suppressed, so we emit -/// `f(&err)` rather than the malformed `f(, &err)`. -fn join_call_args(args_str: &str, extras: &str) -> String { - if args_str.is_empty() { - extras.to_string() - } else { - format!("{}, {}", args_str, extras) - } -} - -/// Emit the call, release, error-check, and return statements for one -/// value-returning sync export, dispatched on the shared [`RetPass`] plan: -/// buffered returns copy and decode a producer `(ptr, len)` buffer, strings -/// and bytes copy then free through the runtime, objects hand the owned -/// pointer (nullable ones boxed as `java.lang.Long`) to the wrapper to -/// adopt, and direct scalars cast straight to their JNI type. -#[allow(clippy::too_many_arguments)] -fn write_return_handling( - jni_c: &mut String, - c_sym: &str, - call_args: &[String], - returns: Option<&Ty>, - params: &[ParamBinding], - module: &str, - c_prefix: &str, - thrower: &str, -) { - let ret_type = returns.expect("write_return_handling requires a return type"); - let args_str = call_args.join(", "); - let call_with_err = join_call_args(&args_str, "&err"); - let call_with_out_len_err = join_call_args(&args_str, "&out_len, &err"); - // Borrowed JNI parameter resources are released immediately after the C - // call, *before* the error check, so an error path cannot leak them. - let mut w = CodeWriter::four_space().with_depth(1); - match plan::ret_pass(returns, module, c_prefix) { - RetPass::Void => unreachable!("void returns are handled by the caller"), - // A buffered return is a producer-allocated `(ptr, len)` pair: copy - // it into a `jbyteArray` for the Kotlin wrapper to decode, then free - // the producer allocation with `weaveffi_free_bytes`. - RetPass::Buffer | RetPass::Bytes => { - w.line(format!( - "const uint8_t* rv = {}({});", - c_sym, call_with_out_len_err - )); - splice(&mut w, |o| release_jni_resources(o, params)); - splice(&mut w, |o| write_error_check(o, returns, thrower)); - w.line("jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len);"); - w.line("if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); }"); - w.line("weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len);"); - w.line("return out;"); - } - RetPass::String => { - w.line(format!("const char* 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)); - w.line("jstring out = weaveffi_jni_utf8_to_string(env, rv);"); - w.line("weaveffi_free_string(rv);"); - w.line("return out;"); - } - // One strong reference the wrapper adopts. Only `Interface?` is a - // nullable object return: the C function returns a nullable owned - // pointer, boxed for Kotlin's `Long?` as a `java.lang.Long` or NULL. - RetPass::Object { nullable, .. } => { - let iname = ret_type - .interface_name() - .expect("object returns are interfaces"); - let c_ty = c_abi_struct_name(iname, module, c_prefix); - w.line(format!("{}* rv = {}({});", c_ty, c_sym, call_with_err)); - splice(&mut w, |o| release_jni_resources(o, params)); - splice(&mut w, |o| write_error_check(o, returns, thrower)); - if nullable { - w.line("if (rv == NULL) { return NULL; }"); - w.line("jclass box_cls = (*env)->FindClass(env, \"java/lang/Long\");"); - w.line("jmethodID box_mid = (*env)->GetStaticMethodID(env, box_cls, \"valueOf\", \"(J)Ljava/lang/Long;\");"); - w.line( - "return (*env)->CallStaticObjectMethod(env, box_cls, box_mid, (jlong)(intptr_t)rv);", - ); - } else { - w.line("return (jlong)(intptr_t)rv;"); - } - } - RetPass::Direct => match ret_type { - 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)); - w.line("return rv ? JNI_TRUE : JNI_FALSE;"); - } - ret_type => { - let c_ty = c_type_for_return(ret_type); - let jcast = jni_cast_for(ret_type); - w.line(format!("{} rv = {}({});", c_ty, c_sym, call_with_err)); - splice(&mut w, |o| release_jni_resources(o, params)); - splice(&mut w, |o| write_error_check(o, returns, thrower)); - w.line(format!("return {} rv;", jcast)); - } - }, - } - jni_c.push_str(&w.finish()); -} - -/// 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: &Ty, module: &str, c_prefix: &str) -> String { - weaveffi_core::model::iterator_item_ctype(elem, module).render_c(c_prefix) -} - -/// Box one scalar or pointer `src` (a plain lvalue or cast expression) into a -/// JVM reference `var`. Interfaces box their pointer bits as a `Long` the -/// wrapper adopts. Only bootstrap classes are looked up, so this is safe on -/// attached producer threads. -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 { - Ty::StringUtf8 => { - w.line(format!( - "jstring {v} = weaveffi_jni_utf8_to_string(env, {s});", - v = var, - s = src - )); - } - 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)); - } - 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)); - } - 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)); - } - 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)); - } - 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)); - } - 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)); - } - 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)); - } - Ty::Bool => { - w.line(format!( - "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Boolean\");", - v = var - )); - w.line(format!("jobject {v} = (*env)->CallStaticObjectMethod(env, {v}_cls, (*env)->GetStaticMethodID(env, {v}_cls, \"valueOf\", \"(Z)Ljava/lang/Boolean;\"), {s} ? JNI_TRUE : JNI_FALSE);", v = var, s = src)); - } - other => unreachable!("{other:?} is never boxed as a scalar"), - } - out.push_str(&w.finish()); -} - -/// Emit the body of an iterator-returning JNI export: launch the C iterator -/// and hand the opaque handle back as a `jlong` for the generated Kotlin -/// iterator class to adopt. Launch errors follow the callable's -/// `ErrorStrategy` via `thrower`. -fn write_iterator_launch( - out: &mut String, - it: &IteratorBinding, - call_args: &[String], - params: &[ParamBinding], - thrower: &str, -) { - let args_str = call_args.join(", "); - let launch_call = join_call_args(&args_str, "&err"); - let iter_ret = Ty::Iterator(Box::new(it.elem.clone())); - - let mut w = CodeWriter::four_space().with_depth(1); - w.line(format!( - "{tag}* _iter = {sym}({call});", - tag = it.iter_tag, - sym = it.launch.symbol, - call = launch_call - )); - splice(&mut w, |o| release_jni_resources(o, params)); - splice(&mut w, |o| write_error_check(o, Some(&iter_ret), thrower)); - w.line("return (jlong)(intptr_t)_iter;"); - out.push_str(&w.finish()); -} - -/// Emit the per-iterator `nativeNext`/`nativeDestroy` JNI exports backing one -/// generated Kotlin iterator class. `nativeNext` pulls exactly one element: -/// it returns a one-slot `Object[]` holding the boxed element, or `NULL` when -/// the producer is exhausted (a pending JNI exception distinguishes the error -/// case). Each element is received like a return and freed per its [`Free`] -/// plan: strings are released with `weaveffi_free_string` after conversion -/// to a `jstring`; bytes and buffered elements are copied into a `jbyteArray` -/// and released with `weaveffi_free_bytes`; an object element's strong -/// reference is boxed as a `Long` the wrapper adopts (`0L` for a null -/// `Interface?`). -pub(crate) fn render_jni_iterator_natives( - out: &mut String, - it: &IteratorBinding, - thrower: &str, - jni_prefix: &str, - module: &str, - c_prefix: &str, -) { - let class = kotlin_iterator_class_name(it, c_prefix); - let item_c = iter_item_c_type(&it.elem, module, c_prefix); - let free = plan::elem_free(&it.elem); - // Bytes and buffered elements carry a trailing `size_t* out_len` slot. - let has_len = it.next.params.iter().any(|p| p.name == "out_len"); - // Only `Interface?` elements stay a nullable pointer (boxed as 0L for - // none); every other optional is buffered. - let leaf = match &it.elem { - Ty::Optional(inner) => inner.as_ref(), - other => other, - }; - - let mut w = CodeWriter::four_space(); - w.line(format!( - "JNIEXPORT jobjectArray JNICALL Java_{}_{}_nativeNext(JNIEnv* env, jclass clazz, jlong handle) {{", - jni_prefix, class - )); - w.scope(|w| { - w.line(format!( - "{tag}* _iter = ({tag}*)(intptr_t)handle;", - tag = it.iter_tag - )); - w.line(format!("{ty} _item = ({ty})0;", ty = item_c)); - if has_len { - w.line("size_t _item_len = 0;"); - } - w.line("weaveffi_error err = {0, NULL, NULL, 0};"); - let next_call = if has_len { - format!( - "int32_t _has = {next}(_iter, &_item, &_item_len, &err);", - next = it.next.symbol - ) - } else { - format!( - "int32_t _has = {next}(_iter, &_item, &err);", - next = it.next.symbol - ) - }; - w.line(next_call); - w.block("if (err.code != 0) {", "}", |w| { - w.line(format!("{thrower}(env, &err);")); - w.line("return NULL;"); - }); - w.line("if (_has == 0) { return NULL; }"); - match free { - Free::Bytes => { - w.line("jbyteArray _jitem = (*env)->NewByteArray(env, (jsize)_item_len);"); - w.line("if (_jitem && _item) { (*env)->SetByteArrayRegion(env, _jitem, 0, (jsize)_item_len, (const jbyte*)_item); }"); - w.line("weaveffi_free_bytes((uint8_t*)_item, _item_len);"); - } - Free::String => { - splice(w, |o| write_boxed_scalar(o, leaf, "_jitem", "_item", " ")); - w.line("weaveffi_free_string(_item);"); - } - Free::None => { - splice(w, |o| write_boxed_scalar(o, leaf, "_jitem", "_item", " ")); - } - } - w.line("jclass _obj_cls = (*env)->FindClass(env, \"java/lang/Object\");"); - w.line("jobjectArray _slot = (*env)->NewObjectArray(env, 1, _obj_cls, NULL);"); - w.line("(*env)->SetObjectArrayElement(env, _slot, 0, _jitem);"); - w.line("return _slot;"); - }); - w.line("}"); - w.blank(); - w.line(format!( - "JNIEXPORT void JNICALL Java_{}_{}_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) {{", - jni_prefix, class - )); - w.scope(|w| { - w.line(format!( - "{destroy}(({tag}*)(intptr_t)handle);", - destroy = it.destroy_symbol, - tag = it.iter_tag - )); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// 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<&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);")); - // The default-return statement may be empty (void functions), in which - // case the original emitted an indented blank line (" \n"), so - // splice the indent verbatim rather than via `line` (which would drop - // the indentation for an empty argument). - w.raw(format!( - "{}{}\n", - w.indent_str(), - jni_default_return(ret_type) - )); - }); - out.push_str(&w.finish()); -} - -/// Release the borrowed JNI resources acquired for `params`, dispatched on -/// each parameter's [`ArgPass`] plan: pinned buffer encodings are released -/// with `JNI_ABORT` (the callee never mutates them), pinned byte arrays with -/// copy-back, and the UTF-8 copy of a string parameter with `free`. Direct -/// and object parameters pinned nothing, and a callback interface's global -/// reference stays alive until the producer calls the vtable's `free`. -fn release_jni_resources(out: &mut String, params: &[ParamBinding]) { - let mut w = CodeWriter::four_space().with_depth(1); - for p in params { - let name = c_local(&p.name); - match p.arg_pass() { - // A buffered parameter's pinned encoding is read-only for the - // callee, so JNI_ABORT skips the pointless copy-back. - ArgPass::Buffer { .. } => { - w.line(format!( - "(*env)->ReleaseByteArrayElements(env, {n}, {n}_elems, JNI_ABORT);", - n = name - )); - } - ArgPass::String { .. } => { - w.line(format!("free({n}_chars);", n = name)); - } - ArgPass::Bytes { .. } => { - w.line(format!( - "(*env)->ReleaseByteArrayElements(env, {n}, {n}_elems, 0);", - n = name - )); - } - ArgPass::Direct { .. } | ArgPass::Object { .. } | ArgPass::Callback { .. } => {} - } - } - out.push_str(&w.finish()); -} - -/// The JNI export name of a free function's Kotlin external: the public name -/// when it is a bare `external fun`, `{name}Jni` when it sits behind a -/// wrapper, `{name}Async` for an async launcher. -pub(crate) fn free_fn_jni_name(module_path: &str, f: &FnBinding, strip: bool) -> String { - let func_name = kotlin_fn_name(module_path, &f.name, strip); - if f.is_async { - format!("{func_name}Async") - } else if crate::types::needs_wrapper_split(f) { - format!("{func_name}Jni") - } else { - func_name - } -} diff --git a/crates/weaveffi-gen-kotlin/src/lib.rs b/crates/weaveffi-gen-kotlin/src/lib.rs deleted file mode 100644 index c3a809cf..00000000 --- a/crates/weaveffi-gen-kotlin/src/lib.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! Kotlin binding generator for WeaveFFI. -//! -//! Emits a Gradle library module (`kotlin/`) holding a Kotlin wrapper over a -//! JNI C shim that calls the C ABI. Android is the primary runtime (the module -//! is an Android library and the shim builds through the NDK's CMake), and the -//! same sources run on a desktop JVM when the shim is built against a JDK. -//! -//! The wrapper renders records and rich enums as value classes decoded from -//! value buffers, interfaces as `AutoCloseable` wrapper classes whose native -//! reference is released exactly once (from `close()` or a -//! `java.lang.ref.Cleaner` backstop), `iter` callables as lazy `Iterator` -//! classes, async callables as `suspend fun`s, and callback interfaces as -//! Kotlin `interface`s whose implementations are pinned by a JNI global -//! reference and driven through a process-wide static vtable. -//! -//! Implements [`LanguageBackend`]; the shared driver bridges it into the -//! generator pipeline, and [`KotlinGenerator::package`] assembles an -//! AAR-style module bundling prebuilt native libraries. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod calls; -mod codec; -mod docs; -mod entities; -mod jni; -mod package; -mod runtime; -mod types; - -use camino::Utf8Path; -use serde::{Deserialize, Serialize}; -use std::fmt::Write as _; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::{BindingModel, CallShape}; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::pkg; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::calls::render_kotlin_free_fn; -use crate::entities::{ - render_kotlin_callback_interface, render_kotlin_enum, render_kotlin_error_types, - render_kotlin_interface, render_kotlin_iterator_class, render_kotlin_struct, -}; -use crate::jni::{ - free_fn_jni_name, render_jni_async_function, render_jni_callback_interface, - render_jni_interface, render_jni_iterator_natives, render_jni_sync_export, -}; -use crate::package::{ - build_gradle_kts, cmake_lists, packaged_build_gradle_kts, packaged_cmake_lists, - packaged_header, packaged_readme, settings_gradle_kts, -}; -use crate::runtime::{ - domain_thrower_used, jni_thrower_for, render_jni_domain_thrower, - render_jni_foreign_error_support, render_jni_generic_thrower, render_jni_onload, - render_jni_string_helpers, render_jni_thread_helpers, render_jni_uncaught_support, - render_kotlin_buffer_runtime, render_kotlin_exception_handler_api, - render_kotlin_native_library, render_kotlin_object_runtime, render_weave_continuation, - LibraryLoading, -}; - -/// Per-target configuration for [`KotlinGenerator`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct KotlinConfig { - /// JVM package for the generated Kotlin wrapper (default - /// `"com.weaveffi"`). - pub package: Option, - /// When `true` (the default), strip the IR module name prefix from - /// emitted Kotlin function names. Set to `false` to keep the prefixed - /// spelling (`contactsCreateContact` rather than `createContact`). - pub strip_module_prefix: bool, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the JNI shim calls the same - /// exported symbols the producer emits. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with. - #[serde(skip)] - pub input_basename: Option, -} - -impl Default for KotlinConfig { - /// The default configuration strips module prefixes; every other field - /// falls back to `None` and resolves through its accessor. - fn default() -> Self { - Self { - package: None, - strip_module_prefix: true, - prefix: None, - input_basename: None, - } - } -} - -impl KotlinConfig { - /// Returns the configured JVM package, falling back to `"com.weaveffi"`. - pub fn package(&self) -> &str { - self.package.as_deref().unwrap_or("com.weaveffi") - } - - /// Returns the configured C ABI symbol prefix, falling back to - /// `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// Kotlin backend: emits a Gradle library module with a Kotlin wrapper over a -/// JNI shim that calls into the C ABI, for Android and desktop JVMs. -pub struct KotlinGenerator; - -impl LanguageBackend for KotlinGenerator { - type Config = KotlinConfig; - - fn name(&self) -> &'static str { - "kotlin" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let package = config.package(); - let strip = config.strip_module_prefix; - let input_basename = config.input_basename(); - let dir = out_dir.join("kotlin"); - let pkg_path = package.replace('.', "/"); - let src_dir = dir.join(format!("src/main/kotlin/{pkg_path}")); - let jni_dir = dir.join("src/main/cpp"); - let project_name = pkg::resolve(api, None, config.input_basename.as_deref()).name; - // The generate layout builds the shim as `lib{prefix}.so` from the - // sibling `c/` output; the consumer links the producer library in. - let loading = LibraryLoading::SystemLibrary(model.prefix.clone()); - vec![ - OutputFile::new( - dir.join("settings.gradle.kts"), - settings_gradle_kts(&project_name, input_basename), - ), - OutputFile::new( - dir.join("build.gradle.kts"), - build_gradle_kts(package, input_basename), - ), - OutputFile::new( - src_dir.join("WeaveFFI.kt"), - render_kotlin(model, package, strip, input_basename, &loading), - ), - OutputFile::new( - jni_dir.join("CMakeLists.txt"), - cmake_lists(&model.prefix, input_basename), - ), - OutputFile::new( - jni_dir.join("weaveffi_jni.c"), - render_jni_c(model, package, strip, input_basename), - ), - ] - } - - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let package = config.package(); - let strip = config.strip_module_prefix; - let input_basename = config.input_basename(); - let dir = out_dir.join("kotlin"); - let pkg_path = package.replace('.', "/"); - let src_dir = dir.join(format!("src/main/kotlin/{pkg_path}")); - let jni_dir = dir.join("src/main/cpp"); - let project_name = pkg::resolve(api, None, config.input_basename.as_deref()).name; - let lib = ctx.binaries.lib_name.as_str(); - let loading = LibraryLoading::Packaged { - lib_name: lib.to_string(), - }; - let header_name = format!("{}.h", model.prefix); - - let mut files = vec![ - PackagedFile::text( - dir.join("settings.gradle.kts"), - settings_gradle_kts(&project_name, input_basename), - ), - PackagedFile::text( - dir.join("build.gradle.kts"), - packaged_build_gradle_kts(package, input_basename), - ), - PackagedFile::text( - dir.join("README.md"), - packaged_readme( - &project_name, - package, - lib, - &model.prefix, - ctx, - input_basename, - ), - ), - PackagedFile::text( - src_dir.join("WeaveFFI.kt"), - render_kotlin(model, package, strip, input_basename, &loading), - ), - PackagedFile::text( - jni_dir.join("CMakeLists.txt"), - packaged_cmake_lists(lib, &model.prefix, input_basename), - ), - PackagedFile::text( - jni_dir.join("weaveffi_jni.c"), - render_jni_c(model, package, strip, input_basename), - ), - PackagedFile::text( - jni_dir.join("include").join(&header_name), - packaged_header(model, input_basename, &header_name), - ), - ]; - // Android binaries land in the AAR's `jniLibs//` so the packaged - // shim links against them; desktop JVM binaries are classpath - // resources the Kotlin loader extracts at runtime. WebAssembly builds - // belong to another package format. - for nb in &ctx.binaries.binaries { - let filename = ctx.binaries.bundled_filename(nb.platform); - if let Some(abi) = nb.platform.android_abi() { - let dest = dir.join("src/main/jniLibs").join(abi).join(filename); - files.push(PackagedFile::copy(dest, nb.source.clone())); - } else if nb.platform.is_desktop() { - let dest = dir - .join("src/main/resources/natives") - .join(nb.platform.id()) - .join(filename); - files.push(PackagedFile::copy(dest, nb.source.clone())); - } - } - Some(files) - } -} - -/// Render the complete `WeaveFFI.kt`: the `WeaveFFI` entry class (free -/// functions inside the companion), then the entity surface (enums, records, -/// callback interfaces, interfaces, iterator classes), the exception -/// hierarchy, and the native-library loader, object, buffer, and continuation -/// runtimes as the model needs them. -fn render_kotlin( - model: &BindingModel, - package: &str, - strip_module_prefix: bool, - input_basename: &str, - loading: &LibraryLoading, -) -> String { - let c_prefix = model.prefix.as_str(); - let has_async = model.has_async(); - let has_objects = model.modules.iter().any(|m| !m.interfaces.is_empty()); - let mut kotlin = render_prelude(CommentStyle::DoubleSlash, input_basename); - kotlin.push_str(&format!("package {package}\n\n")); - if has_async { - kotlin.push_str("import kotlinx.coroutines.suspendCancellableCoroutine\n"); - kotlin.push_str("import kotlin.coroutines.resume\n"); - kotlin.push_str("import kotlin.coroutines.resumeWithException\n\n"); - } - kotlin.push_str( - "class WeaveFFI {\n companion object {\n init { WeaveNativeLibrary.ensureLoaded() }\n\n", - ); - if has_async { - render_kotlin_exception_handler_api(&mut kotlin); - } - for m in &model.modules { - for f in &m.functions { - render_kotlin_free_fn(&mut kotlin, m, f, strip_module_prefix, c_prefix); - } - } - kotlin.push_str(" }\n}\n"); - for m in &model.modules { - for e in &m.enums { - render_kotlin_enum(&mut kotlin, e); - } - for s in &m.structs { - render_kotlin_struct(&mut kotlin, s); - } - for cb in &m.callback_interfaces { - render_kotlin_callback_interface(&mut kotlin, cb); - } - for i in &m.interfaces { - render_kotlin_interface(&mut kotlin, i, m.error.as_ref(), c_prefix); - } - // One lazy iterator wrapper class per `iter` callable, streaming - // one producer `next` per consumer step. - for f in m.callables() { - if let CallShape::Iterator(it) = &f.shape { - render_kotlin_iterator_class(&mut kotlin, f, it, c_prefix); - } - } - } - render_kotlin_error_types(&mut kotlin, model); - render_kotlin_native_library(&mut kotlin, loading); - if has_objects || model.has_iterators() { - render_kotlin_object_runtime(&mut kotlin); - } - if model.has_buffers() { - render_kotlin_buffer_runtime(&mut kotlin); - } - if has_async { - render_weave_continuation(&mut kotlin); - } - kotlin.push('\n'); - kotlin.push_str(&render_trailer(CommentStyle::DoubleSlash, "WeaveFFI.kt")); - kotlin -} - -/// Render the complete `weaveffi_jni.c`: the error throwers, `JNI_OnLoad` -/// with its cached VM, classes, and method IDs, the thread-attach, uncaught -/// exception, and foreign-error helpers, the callback-interface trampolines -/// and static vtables, then one JNI export per callable (sync, async, -/// interface member, and iterator native). -fn render_jni_c( - model: &BindingModel, - package: &str, - strip_module_prefix: bool, - input_basename: &str, -) -> String { - let c_prefix = model.prefix.as_str(); - let jni_prefix = package.replace('.', "_"); - let jni_pkg_path = package.replace('.', "/"); - let mut jni_c = render_prelude(CommentStyle::DoubleSlash, input_basename); - jni_c.push_str("#include \n#include \n#include \n#include \n#include \n#include \n"); - let _ = writeln!(jni_c, "#include \"{c_prefix}.h\"\n"); - - render_jni_string_helpers(&mut jni_c); - render_jni_generic_thrower(&mut jni_c, &jni_pkg_path); - for m in &model.modules { - if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { - if domain_thrower_used(model, &eb.c_tag) { - render_jni_domain_thrower(&mut jni_c, eb, &jni_pkg_path); - } - } - } - - let has_async = model.has_async(); - let callback_interfaces: Vec<_> = model.callback_interfaces().collect(); - render_jni_onload( - &mut jni_c, - &jni_pkg_path, - c_prefix, - has_async, - &callback_interfaces, - ); - if has_async || !callback_interfaces.is_empty() { - render_jni_thread_helpers(&mut jni_c); - } - if has_async { - jni_c.push_str("typedef struct {\n"); - jni_c.push_str(" jobject callback;\n"); - jni_c.push_str("} weaveffi_jni_async_ctx;\n\n"); - render_jni_uncaught_support(&mut jni_c); - } - if !callback_interfaces.is_empty() { - render_jni_foreign_error_support(&mut jni_c, c_prefix); - for (_, cb) in &callback_interfaces { - render_jni_callback_interface(&mut jni_c, cb, c_prefix); - } - } - - for m in &model.modules { - for f in &m.functions { - let thrower = jni_thrower_for(f, m.error.as_ref()); - let jni_name = free_fn_jni_name(&m.path, f, strip_module_prefix); - if f.is_async { - render_jni_async_function( - &mut jni_c, - &m.path, - f, - "WeaveFFI", - &jni_name, - None, - &jni_prefix, - c_prefix, - ); - continue; - } - render_jni_sync_export( - &mut jni_c, - f, - "WeaveFFI", - &jni_name, - None, - &thrower, - &jni_prefix, - &m.path, - c_prefix, - ); - } - } - for m in &model.modules { - // Records and rich enums are value types with no C symbols: their - // packing and decoding happens entirely in Kotlin, so no JNI bridge - // is emitted for them. Plain C-style enums need no natives either. - for i in &m.interfaces { - render_jni_interface(&mut jni_c, m, i, &jni_prefix, c_prefix); - } - // The `nativeNext`/`nativeDestroy` exports backing each generated - // lazy iterator class, covering free functions, interface methods, - // and statics returning `iter`. - for f in m.callables() { - if let CallShape::Iterator(it) = &f.shape { - let thrower = jni_thrower_for(f, m.error.as_ref()); - render_jni_iterator_natives( - &mut jni_c, - it, - &thrower, - &jni_prefix, - &m.path, - c_prefix, - ); - } - } - } - jni_c.push('\n'); - jni_c.push_str(&render_trailer(CommentStyle::DoubleSlash, "weaveffi_jni.c")); - jni_c -} - -#[cfg(test)] -mod tests; diff --git a/crates/weaveffi-gen-kotlin/src/package.rs b/crates/weaveffi-gen-kotlin/src/package.rs deleted file mode 100644 index 8e5c879f..00000000 --- a/crates/weaveffi-gen-kotlin/src/package.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! Gradle module scaffolding: `settings.gradle.kts`, `build.gradle.kts`, the -//! CMake build script compiling the JNI shim, and the packaged layout's -//! README and bundled C header. - -use std::fmt::Write as _; - -use weaveffi_core::cabi; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::PackageContext; -use weaveffi_core::utils::{ - render_abi_prefix_aliases, render_prelude, render_trailer, CommentStyle, -}; - -use crate::runtime::jni_lib_name; - -/// Escape `s` for placement inside a double-quoted Kotlin (Gradle Kotlin DSL) -/// string literal: backslashes, double quotes, and `$` gain a backslash, so a -/// package name or project name containing any of them can't terminate the -/// literal or start a template. -pub(crate) fn kts_quote(s: &str) -> String { - s.replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('$', "\\$") -} - -/// Render `settings.gradle.kts`, naming the root project after the resolved -/// package identity. -pub(crate) fn settings_gradle_kts(project_name: &str, input_basename: &str) -> String { - format!( - "{}rootProject.name = \"{}\"\n\n{}", - render_prelude(CommentStyle::DoubleSlash, input_basename), - kts_quote(project_name), - render_trailer(CommentStyle::DoubleSlash, "settings.gradle.kts"), - ) -} - -/// The shared body of the Android library module's `build.gradle.kts`: the -/// Kotlin plugin, the JVM `namespace`, the SDK levels the generated code -/// needs (`java.lang.ref.Cleaner` is API 33), and the JNI shim wired through -/// `externalNativeBuild`. `extra` is spliced inside the `android {}` block. -fn gradle_kts_body(namespace: &str, extra: &str) -> String { - let namespace = kts_quote(namespace); - format!( - r#"plugins {{ - id("com.android.library") - id("org.jetbrains.kotlin.android") version "1.9.22" -}} - -android {{ - namespace = "{namespace}" - compileSdk = 34 - defaultConfig {{ - // java.lang.ref.Cleaner, which backs every wrapper's disposal, is API 33. - minSdk = 33 - externalNativeBuild {{ - cmake {{ - cppFlags("") - }} - }} - }} - externalNativeBuild {{ - cmake {{ - path = file("src/main/cpp/CMakeLists.txt") - }} - }} -{extra}}} - -dependencies {{ - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") -}} -"# - ) -} - -/// Render `build.gradle.kts` for the generated (source-only) module. -pub(crate) fn build_gradle_kts(namespace: &str, input_basename: &str) -> String { - format!( - "{}{}\n{}", - render_prelude(CommentStyle::DoubleSlash, input_basename), - gradle_kts_body(namespace, ""), - render_trailer(CommentStyle::DoubleSlash, "build.gradle.kts"), - ) -} - -/// Render `build.gradle.kts` for the packaged module: the same Android -/// library, plus the bundled `jniLibs/` directory as a JNI source set. -pub(crate) fn packaged_build_gradle_kts(namespace: &str, input_basename: &str) -> String { - let extra = " sourceSets {\n getByName(\"main\") {\n jniLibs.srcDirs(\"src/main/jniLibs\")\n }\n }\n"; - format!( - "{}{}\n{}", - render_prelude(CommentStyle::DoubleSlash, input_basename), - gradle_kts_body(namespace, extra), - render_trailer(CommentStyle::DoubleSlash, "build.gradle.kts"), - ) -} - -/// Render `CMakeLists.txt` for the generated layout: one shared library -/// `lib{prefix}.so` built from the JNI shim, with the sibling `c/` output -/// (the generated `{prefix}.h`) on the include path. Linking the producer -/// library is left to the consumer's build. -pub(crate) fn cmake_lists(c_prefix: &str, input_basename: &str) -> String { - let body = format!( - "cmake_minimum_required(VERSION 3.22)\n\ - project({c_prefix}_jni C)\n\ - add_library({c_prefix} SHARED weaveffi_jni.c)\n\ - target_include_directories({c_prefix} PRIVATE ../../../../c)\n" - ); - format!( - "{}{body}\n{}", - render_prelude(CommentStyle::Hash, input_basename), - render_trailer(CommentStyle::Hash, "CMakeLists.txt"), - ) -} - -/// Render `CMakeLists.txt` for the packaged layout: builds `lib{lib}_jni` from -/// the shim against the bundled `include/{prefix}.h`, and links the bundled -/// producer library for the current Android ABI (from `jniLibs//`) or, -/// on a desktop JVM build, from `src/main/resources/natives//` -/// when the caller passes `WEAVEFFI_PLATFORM_ID`. -pub(crate) fn packaged_cmake_lists(lib: &str, c_prefix: &str, input_basename: &str) -> String { - let jni = jni_lib_name(lib); - let body = format!( - r#"cmake_minimum_required(VERSION 3.22) -project({jni} C) - -# The shim compiles against the bundled include/{c_prefix}.h. -add_library({jni} SHARED weaveffi_jni.c) -target_include_directories({jni} PRIVATE include) - -if(ANDROID) - # The AAR bundles the producer library per ABI under jniLibs/. - add_library({lib} SHARED IMPORTED) - set_target_properties({lib} PROPERTIES IMPORTED_LOCATION - ${{CMAKE_CURRENT_SOURCE_DIR}}/../jniLibs/${{ANDROID_ABI}}/lib{lib}.so) - target_link_libraries({jni} PRIVATE {lib}) -else() - # Desktop JVM: build against a JDK and the bundled desktop binary. - find_package(JNI REQUIRED) - target_include_directories({jni} PRIVATE ${{JNI_INCLUDE_DIRS}}) - if(NOT DEFINED WEAVEFFI_PLATFORM_ID) - message(FATAL_ERROR "set -DWEAVEFFI_PLATFORM_ID= to pick the bundled producer library") - endif() - set(WEAVEFFI_NATIVES ${{CMAKE_CURRENT_SOURCE_DIR}}/../resources/natives/${{WEAVEFFI_PLATFORM_ID}}) - target_link_directories({jni} PRIVATE ${{WEAVEFFI_NATIVES}}) - target_link_libraries({jni} PRIVATE {lib}) - set_target_properties({jni} PROPERTIES - LIBRARY_OUTPUT_DIRECTORY ${{WEAVEFFI_NATIVES}} - RUNTIME_OUTPUT_DIRECTORY ${{WEAVEFFI_NATIVES}}) -endif() -"# - ); - format!( - "{}{body}\n{}", - render_prelude(CommentStyle::Hash, input_basename), - render_trailer(CommentStyle::Hash, "CMakeLists.txt"), - ) -} - -/// Render the `{prefix}.h` the packaged shim compiles against, so the module -/// is self-contained without the sibling `c/` output: the same include -/// guard, runtime declarations, prefix aliases, and per-module declarations -/// the C generator emits, composed from [`weaveffi_core::cabi`]. -pub(crate) fn packaged_header( - model: &BindingModel, - input_basename: &str, - filename: &str, -) -> String { - let prefix = model.prefix.as_str(); - let guard = format!("{}_H", prefix.to_uppercase()); - let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); - let _ = write!(out, "#ifndef {guard}\n#define {guard}\n\n"); - out.push_str("#include \n#include \n#include \n\n"); - cabi::render_visibility_macros(&mut out, prefix); - out.push_str(&render_abi_prefix_aliases(prefix)); - out.push_str("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n"); - cabi::render_runtime_decls(&mut out, prefix); - 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"); - out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); - out -} - -/// Render the packaged module's README: the layout, how Android consumers -/// depend on the module, how a desktop JVM build compiles the shim against -/// the bundled binary, and the list of bundled platforms. -pub(crate) fn packaged_readme( - project_name: &str, - package: &str, - lib: &str, - c_prefix: &str, - ctx: &PackageContext, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let jni = jni_lib_name(lib); - let mut android: Vec = Vec::new(); - let mut desktop: Vec = Vec::new(); - for nb in &ctx.binaries.binaries { - let filename = ctx.binaries.bundled_filename(nb.platform); - if let Some(abi) = nb.platform.android_abi() { - android.push(format!("- `src/main/jniLibs/{abi}/{filename}`")); - } else if nb.platform.is_desktop() { - desktop.push(format!( - "- `src/main/resources/natives/{}/{filename}`", - nb.platform.id() - )); - } - } - let android_list = if android.is_empty() { - "- (none bundled)".to_string() - } else { - android.join("\n") - }; - let desktop_list = if desktop.is_empty() { - "- (none bundled)".to_string() - } else { - desktop.join("\n") - }; - format!( - r#"{prelude}# {project_name} (Kotlin) - -An Android library module (Gradle, Kotlin DSL) exposing the `{package}` API -over a JNI shim. The Kotlin sources live under `src/main/kotlin/`, the shim -under `src/main/cpp/` with the C header it compiles against in -`src/main/cpp/include/{c_prefix}.h`, and the prebuilt producer libraries under -`src/main/jniLibs//` (Android) and `src/main/resources/natives//` -(desktop JVMs). - -## Android - -Add the module to your project (`include(":{project_name}")` in -`settings.gradle.kts`, or publish it as an AAR) and depend on it. The NDK's -CMake builds `lib{jni}.so` against the bundled `lib{lib}.so` for each ABI, and -the Kotlin wrapper loads it with `System.loadLibrary("{jni}")`. `minSdk` is -33 because the wrappers' disposal backstop is `java.lang.ref.Cleaner`. - -## Desktop JVM - -Build the shim once per platform against a JDK, next to the bundled producer -binary, so the wrapper can extract both from the classpath at runtime: - -```bash -cmake -S src/main/cpp -B build -DWEAVEFFI_PLATFORM_ID=darwin-arm64 -cmake --build build -``` - -The wrapper picks `natives/-/` from `os.name` and `os.arch`, loads -the producer library first and the shim second, and falls back to -`System.loadLibrary("{jni}")` (the `java.library.path`) when the resources -are absent. - -## Bundled Android ABIs - -{android_list} - -## Bundled desktop platforms - -{desktop_list} - -{trailer}"# - ) -} diff --git a/crates/weaveffi-gen-kotlin/src/runtime.rs b/crates/weaveffi-gen-kotlin/src/runtime.rs deleted file mode 100644 index 09860f89..00000000 --- a/crates/weaveffi-gen-kotlin/src/runtime.rs +++ /dev/null @@ -1,798 +0,0 @@ -//! Once-per-file runtime support: the Kotlin native-library loader, the -//! native-reference holder and its `Cleaner`, the value-buffer writer/reader, -//! the async exception hook and continuation shim, and the JNI `JNI_OnLoad`, -//! thread-attach helpers, foreign-error reporter, and error throwers. - -use std::fmt::Write as _; - -use weaveffi_core::cabi::ABI_VERSION; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors; -use weaveffi_core::model::{ - BindingModel, CallbackInterfaceBinding, ErrorBinding, FnBinding, ModuleBinding, -}; -use weaveffi_core::plan::ErrorStrategy; - -use crate::entities::kotlin_exception_name; -use crate::types::{jni_descriptor, kotlin_callback_dispatch_name, kt_param}; - -/// The reserved runtime error code a callback trampoline reports when the -/// Kotlin implementation threw: "a consumer callback failed". -pub(crate) const FOREIGN_ERROR_CODE: i32 = -4; - -/// How the generated Kotlin loads the JNI shim (and, when packaged, the -/// producer library it links against). -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum LibraryLoading { - /// `System.loadLibrary(name)`: the shim is on the JVM's library path or - /// bundled as an Android `jniLibs` entry. - SystemLibrary(String), - /// The packaged layout: on Android, `System.loadLibrary("{lib}_jni")` - /// resolves the AAR's `jniLibs`; on a desktop JVM the producer library - /// and the shim are extracted from `natives//` classpath - /// resources and loaded with `System.load`, falling back to - /// `System.loadLibrary` when the resources are absent. - Packaged { - /// The logical producer library base name (`libcontacts.so`). - lib_name: String, - }, -} - -/// The JNI shim library base name the packaged layout builds and loads: -/// `{lib}_jni`, so it never collides with the producer library itself. -pub(crate) fn jni_lib_name(lib_name: &str) -> String { - format!("{lib_name}_jni") -} - -/// Render the `WeaveNativeLibrary` object every class with `external` -/// members touches from its companion `init`, so the shim is loaded exactly -/// once before any native declaration is bound. -pub(crate) fn render_kotlin_native_library(out: &mut String, loading: &LibraryLoading) { - let mut w = CodeWriter::four_space(); - w.blank(); - w.line("/** Loads the native library once; every companion `init` calls [ensureLoaded]. */"); - w.line("internal object WeaveNativeLibrary {"); - w.scope(|w| match loading { - LibraryLoading::SystemLibrary(name) => { - w.line(format!( - "init {{ System.loadLibrary(\"{}\") }}", - name.replace('"', "\\\"") - )); - w.blank(); - w.line("fun ensureLoaded() {}"); - } - LibraryLoading::Packaged { lib_name } => { - let lib = lib_name.replace('"', "\\\""); - let jni = jni_lib_name(&lib); - w.line(format!("private const val LIB = \"{lib}\"")); - w.line(format!("private const val JNI_LIB = \"{jni}\"")); - w.blank(); - w.line("init {"); - w.scope(|w| { - w.line("val vendor = System.getProperty(\"java.vm.vendor\") ?: \"\""); - w.line("if (vendor.contains(\"Android\")) {"); - w.scope(|w| { - w.line("System.loadLibrary(JNI_LIB)"); - }); - w.line("} else {"); - w.scope(|w| { - w.line("loadFromResources()"); - }); - w.line("}"); - }); - w.line("}"); - w.blank(); - w.line("fun ensureLoaded() {}"); - w.blank(); - w.line("private fun loadFromResources() {"); - w.scope(|w| { - w.line("val id = platformId()"); - w.line("val dir = java.nio.file.Files.createTempDirectory(\"weaveffi-natives\")"); - w.line("dir.toFile().deleteOnExit()"); - w.line("// The producer library first, so the shim's dependency resolves."); - w.line("extract(id, fileName(LIB), dir)?.let { System.load(it) }"); - w.line("val shim = extract(id, fileName(JNI_LIB), dir)"); - w.line("if (shim != null) System.load(shim) else System.loadLibrary(JNI_LIB)"); - }); - w.line("}"); - w.blank(); - w.line("private fun platformId(): String {"); - w.scope(|w| { - w.line("val os = System.getProperty(\"os.name\").lowercase()"); - w.line("val arch = System.getProperty(\"os.arch\").lowercase()"); - w.line("val osId = when {"); - w.scope(|w| { - w.line("os.contains(\"mac\") || os.contains(\"darwin\") -> \"darwin\""); - w.line("os.contains(\"win\") -> \"windows\""); - w.line("else -> \"linux\""); - }); - w.line("}"); - w.line("val archId = if (arch == \"aarch64\" || arch == \"arm64\") \"arm64\" else \"x64\""); - w.line("return \"$osId-$archId\""); - }); - w.line("}"); - w.blank(); - w.line("private fun fileName(base: String): String {"); - w.scope(|w| { - w.line("val os = System.getProperty(\"os.name\").lowercase()"); - w.line("return when {"); - w.scope(|w| { - w.line("os.contains(\"mac\") || os.contains(\"darwin\") -> \"lib$base.dylib\""); - w.line("os.contains(\"win\") -> \"$base.dll\""); - w.line("else -> \"lib$base.so\""); - }); - w.line("}"); - }); - w.line("}"); - w.blank(); - w.line("private fun extract(id: String, name: String, dir: java.nio.file.Path): String? {"); - w.scope(|w| { - w.line("val stream = WeaveNativeLibrary::class.java.getResourceAsStream(\"/natives/$id/$name\") ?: return null"); - w.line("val target = dir.resolve(name)"); - w.line("stream.use { java.nio.file.Files.copy(it, target) }"); - w.line("target.toFile().deleteOnExit()"); - w.line("return target.toAbsolutePath().toString()"); - }); - w.line("}"); - } - }); - w.line("}"); - out.push_str(&w.finish()); -} - -/// Render the native-reference holder shared by every object wrapper and -/// iterator class: a `Runnable` owning one strong reference that is released -/// exactly once, either from the wrapper's `close()` or from the -/// `java.lang.ref.Cleaner` backstop when the wrapper becomes unreachable -/// without being closed. The holder never references the wrapper, which is -/// what lets the `Cleaner` collect it. -pub(crate) fn render_kotlin_object_runtime(out: &mut String) { - let _ = write!( - out, - r#" -/** - * One strong native reference, released exactly once. Wrappers register an - * instance with [weaveCleaner] so an unreachable, never-closed wrapper still - * releases its reference; an explicit `close()` runs the same action first - * and the cleaner then does nothing. - */ -internal class WeaveNativeRef(handle: Long, private val release: (Long) -> Unit) : Runnable {{ - private val handle = java.util.concurrent.atomic.AtomicLong(handle) - - /** The live pointer, borrowed for the duration of one native call. */ - fun get(): Long {{ - val h = handle.get() - check(h != 0L) {{ "WeaveFFI object used after close()" }} - return h - }} - - /** The live pointer, or `0L` once released. */ - fun peek(): Long = handle.get() - - override fun run() {{ - val h = handle.getAndSet(0L) - if (h != 0L) release(h) - }} -}} - -/** The process-wide cleaner backing every generated wrapper's disposal backstop. */ -internal val weaveCleaner: java.lang.ref.Cleaner = java.lang.ref.Cleaner.create() -"# - ); -} - -/// Render the settable uncaught-exception hook into the `WeaveFFI` companion. -/// Async continuation resume paths run on native producer threads with no -/// Kotlin caller up-stack, so a thrown exception has nowhere to propagate; -/// the JNI glue routes it here. When no handler is installed, -/// `dispatchCallbackException` rethrows, and the glue falls back to -/// `ExceptionDescribe` (logging the stack trace) before clearing. -pub(crate) fn render_kotlin_exception_handler_api(out: &mut String) { - let mut w = CodeWriter::four_space().with_depth(2); - w.line("@Volatile private var callbackExceptionHandler: ((Throwable) -> Unit)? = null"); - w.blank(); - w.line("/**"); - w.line(" * Installs a handler for exceptions thrown while an async result is"); - w.line(" * delivered on a native producer thread. These exceptions have no Kotlin"); - w.line(" * caller to propagate to; when no handler is installed, they are logged"); - w.line(" * with their stack trace and dropped. Pass `null` to restore the default"); - w.line(" * logging behavior. Exceptions thrown by callback interface methods are"); - w.line(" * not routed here: they abort the producer call that invoked them and"); - w.line(" * surface to the original caller as a [WeaveFFIException] with code -4."); - w.line(" */"); - w.line("@JvmStatic fun setCallbackExceptionHandler(handler: ((Throwable) -> Unit)?) {"); - w.scope(|w| { - w.line("callbackExceptionHandler = handler"); - }); - w.line("}"); - w.blank(); - w.line("// Invoked from the JNI glue; rethrowing signals \"no handler\" so the"); - w.line("// glue falls back to ExceptionDescribe."); - w.line("@JvmStatic private fun dispatchCallbackException(t: Throwable) {"); - w.scope(|w| { - w.line("callbackExceptionHandler?.invoke(t) ?: throw t"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the private Kotlin value-buffer runtime: a growable little-endian -/// writer, a validating reader (rejecting truncated buffers, invalid -/// bool/flag bytes, zero object tokens, oversized length prefixes, and -/// trailing bytes), and the `weaveEncode`/`weaveDecode` entry points the -/// generated wrappers call. -pub(crate) fn render_kotlin_buffer_runtime(out: &mut String) { - let brand = errors::EXCEPTION_BRAND; - let _ = write!( - out, - r#" -/** 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 {brand}(-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()) - /** An object token: one strong reference the caller adopts. Zero is never valid here. */ - fun readObject(): Long {{ - val token = readI64() - if (token == 0L) malformed("null object token") - return token - }} - 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 -}} -"# - ); -} - -/// Render the `WeaveContinuation` shim boxing a cancellable continuation and -/// its error mapper for delivery through the JNI async callback. -pub(crate) fn render_weave_continuation(out: &mut String) { - out.push_str("\ninternal class WeaveContinuation(\n"); - out.push_str(" private val cont: kotlinx.coroutines.CancellableContinuation,\n"); - out.push_str(" private val mapError: (Int, String, ByteArray?) -> Throwable\n"); - out.push_str(") {\n"); - out.push_str(" @Suppress(\"UNCHECKED_CAST\")\n"); - out.push_str(" fun onSuccess(result: Any?) { cont.resume(result as T) }\n"); - out.push_str(" fun onError(code: Int, message: String, payload: ByteArray?) { cont.resumeWithException(mapError(code, message, payload)) }\n"); - out.push_str("}\n"); -} - -/// The C identifier of the cached `jclass` global reference for a callback -/// interface's Kotlin dispatch object. -pub(crate) fn jni_cb_class_var(cb: &CallbackInterfaceBinding) -> String { - format!("{}_jni_cls", cb.c_tag) -} - -/// The C identifier of the cached `jmethodID` for one dispatch shim of a -/// callback interface. -pub(crate) fn jni_cb_method_var(cb: &CallbackInterfaceBinding, method: &str) -> String { - format!("{}_jni_mid_{method}", cb.c_tag) -} - -/// Emit the file-scope JNI state and `JNI_OnLoad`: the cached `JavaVM*` every -/// producer-thread entry attaches through, the ABI revision check, and, as -/// the model needs them, the cached `WeaveFFI` entry class for async -/// exception routing and one cached class plus per-method `jmethodID` set for -/// each callback interface's Kotlin dispatch object. Everything is resolved -/// here because `System.loadLibrary` runs `JNI_OnLoad` on an app thread whose -/// class loader can see the generated classes; producer threads cannot -/// `FindClass` them later. -pub(crate) fn render_jni_onload( - out: &mut String, - jni_pkg_path: &str, - c_prefix: &str, - has_async: bool, - callback_interfaces: &[(&ModuleBinding, &CallbackInterfaceBinding)], -) { - let mut w = CodeWriter::four_space(); - w.line("static JavaVM* weaveffi_jni_vm = NULL;"); - if has_async { - w.line("static jclass weaveffi_jni_entry_class = NULL;"); - w.line("static jmethodID weaveffi_jni_dispatch_exc = NULL;"); - } - if !callback_interfaces.is_empty() { - w.line("static jmethodID weaveffi_jni_throwable_to_string = NULL;"); - for (_, cb) in callback_interfaces { - w.line(format!("static jclass {} = NULL;", jni_cb_class_var(cb))); - for m in &cb.methods { - w.line(format!( - "static jmethodID {} = NULL;", - jni_cb_method_var(cb, &m.name) - )); - } - } - } - w.blank(); - w.line("static jint weaveffi_jni_load_error(JNIEnv* env, const char* message) {"); - w.scope(|w| { - w.line("jclass cls = (*env)->FindClass(env, \"java/lang/UnsatisfiedLinkError\");"); - w.line("if (cls != NULL) { (*env)->ThrowNew(env, cls, message); }"); - w.line("return JNI_ERR;"); - }); - w.line("}"); - w.blank(); - w.line("JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {"); - w.scope(|w| { - w.line("(void)reserved;"); - w.line("weaveffi_jni_vm = vm;"); - w.line("JNIEnv* env = NULL;"); - w.line("if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; }"); - w.block( - format!("if ({c_prefix}_abi_version() != {ABI_VERSION}u) {{"), - "}", - |w| { - w.line(format!( - "return weaveffi_jni_load_error(env, \"WeaveFFI: the native library implements a different C ABI revision than these bindings (expected {ABI_VERSION})\");" - )); - }, - ); - if has_async { - w.line(format!( - "jclass entry = (*env)->FindClass(env, \"{jni_pkg_path}/WeaveFFI\");" - )); - w.line("if (entry == NULL) { return JNI_ERR; }"); - w.line("weaveffi_jni_entry_class = (jclass)(*env)->NewGlobalRef(env, entry);"); - w.line("weaveffi_jni_dispatch_exc = (*env)->GetStaticMethodID(env, weaveffi_jni_entry_class, \"dispatchCallbackException\", \"(Ljava/lang/Throwable;)V\");"); - w.line("if (weaveffi_jni_dispatch_exc == NULL) { (*env)->ExceptionClear(env); }"); - } - if !callback_interfaces.is_empty() { - w.line("jclass throwable = (*env)->FindClass(env, \"java/lang/Throwable\");"); - w.line("if (throwable == NULL) { return JNI_ERR; }"); - w.line("weaveffi_jni_throwable_to_string = (*env)->GetMethodID(env, throwable, \"toString\", \"()Ljava/lang/String;\");"); - w.line("if (weaveffi_jni_throwable_to_string == NULL) { return JNI_ERR; }"); - for (_, cb) in callback_interfaces { - let cls = jni_cb_class_var(cb); - let dispatch = kotlin_callback_dispatch_name(&cb.name); - w.line(format!( - "jclass {cls}_local = (*env)->FindClass(env, \"{jni_pkg_path}/{dispatch}\");" - )); - w.line(format!("if ({cls}_local == NULL) {{ return JNI_ERR; }}")); - w.line(format!( - "{cls} = (jclass)(*env)->NewGlobalRef(env, {cls}_local);" - )); - for m in &cb.methods { - let sig = jni_cb_shim_descriptor(cb, m, jni_pkg_path); - let mid = jni_cb_method_var(cb, &m.name); - w.line(format!( - "{mid} = (*env)->GetStaticMethodID(env, {cls}, \"{}\", \"{sig}\");", - kt_param(&m.name) - )); - w.line(format!("if ({mid} == NULL) {{ return JNI_ERR; }}")); - } - } - } - w.line("return JNI_VERSION_1_6;"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// The JNI method descriptor of a callback interface's dispatch shim: the -/// implementing object first, then every parameter as the shim receives it, -/// then the direct-family (or `V`) return. -pub(crate) fn jni_cb_shim_descriptor( - cb: &CallbackInterfaceBinding, - m: &weaveffi_core::model::CallbackMethodBinding, - jni_pkg_path: &str, -) -> String { - let mut sig = format!("(L{jni_pkg_path}/{};", cb.name); - for p in &m.params { - sig.push_str(jni_descriptor(Some(&p.ty))); - } - sig.push(')'); - sig.push_str(jni_descriptor(m.ret.as_ref())); - sig -} - -/// Emit the two string converters every `string` crossing goes through. -/// JNI's own `GetStringUTFChars`/`NewStringUTF` speak *modified* UTF-8, which -/// encodes supplementary characters (emoji, for instance) as CESU-8 surrogate -/// pairs: a producer expecting standard UTF-8 rejects such a parameter as -/// invalid, and a standard 4-byte sequence handed to `NewStringUTF` is -/// mangled. The converters go through UTF-16 (`GetStringChars`/`NewString`) -/// instead, so every Kotlin `String` round-trips byte-exact. -pub(crate) fn render_jni_string_helpers(out: &mut String) { - let mut w = CodeWriter::four_space(); - w.line("/* Copies `s` as a NUL-terminated standard UTF-8 string (JNI's modified"); - w.line(" UTF-8 would encode supplementary characters as surrogate pairs the"); - w.line(" producer rejects). Lone surrogates become U+FFFD. Release with free()."); - w.line(" Returns NULL for a NULL string, for a string holding U+0000 (which no"); - w.line(" C string can carry), or on allocation failure; the producer then"); - w.line(" reports the null argument through its marshalling error. */"); - w.line("static char* weaveffi_jni_string_to_utf8(JNIEnv* env, jstring s) {"); - w.scope(|w| { - w.line("if (s == NULL) { return NULL; }"); - w.line("jsize n = (*env)->GetStringLength(env, s);"); - w.line("const jchar* u = (*env)->GetStringChars(env, s, NULL);"); - w.line("if (u == NULL) { return NULL; }"); - w.line("char* out = (char*)malloc((size_t)n * 4u + 1u);"); - w.line("if (out == NULL) { (*env)->ReleaseStringChars(env, s, u); return NULL; }"); - w.line("size_t o = 0;"); - w.block("for (jsize i = 0; i < n; i++) {", "}", |w| { - w.line("uint32_t c = u[i];"); - w.line("if (c == 0u) { free(out); (*env)->ReleaseStringChars(env, s, u); return NULL; }"); - w.block( - "if (c >= 0xD800u && c <= 0xDBFFu && i + 1 < n && u[i + 1] >= 0xDC00u && u[i + 1] <= 0xDFFFu) {", - "}", - |w| { - w.line("c = 0x10000u + ((c - 0xD800u) << 10) + ((uint32_t)u[i + 1] - 0xDC00u);"); - w.line("i++;"); - }, - ); - w.line("else if (c >= 0xD800u && c <= 0xDFFFu) { c = 0xFFFDu; }"); - w.line("if (c < 0x80u) { out[o++] = (char)c; }"); - w.line("else if (c < 0x800u) { out[o++] = (char)(0xC0u | (c >> 6)); out[o++] = (char)(0x80u | (c & 0x3Fu)); }"); - w.line("else if (c < 0x10000u) { out[o++] = (char)(0xE0u | (c >> 12)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); }"); - w.line("else { out[o++] = (char)(0xF0u | (c >> 18)); out[o++] = (char)(0x80u | ((c >> 12) & 0x3Fu)); out[o++] = (char)(0x80u | ((c >> 6) & 0x3Fu)); out[o++] = (char)(0x80u | (c & 0x3Fu)); }"); - }); - w.line("out[o] = 0;"); - w.line("(*env)->ReleaseStringChars(env, s, u);"); - w.line("return out;"); - }); - w.line("}"); - w.blank(); - w.line("/* Builds a jstring from standard UTF-8 (NewStringUTF expects modified UTF-8"); - w.line(" and mangles supplementary characters). NULL yields the empty string and"); - w.line(" a malformed sequence yields U+FFFD, so this never fails on producer"); - w.line(" output; it returns NULL only when the JVM could not allocate. */"); - w.line("static jstring weaveffi_jni_utf8_to_string(JNIEnv* env, const char* s) {"); - w.scope(|w| { - w.line("if (s == NULL) { s = \"\"; }"); - w.line("size_t n = strlen(s);"); - w.line("jchar* buf = (jchar*)malloc((n + 1u) * sizeof(jchar));"); - w.line("if (buf == NULL) { return NULL; }"); - w.line("size_t i = 0, o = 0;"); - w.block("while (i < n) {", "}", |w| { - w.line("unsigned char b = (unsigned char)s[i];"); - w.line("uint32_t c; size_t len;"); - w.line("if (b < 0x80u) { c = b; len = 1; }"); - w.line("else if ((b & 0xE0u) == 0xC0u) { c = b & 0x1Fu; len = 2; }"); - w.line("else if ((b & 0xF0u) == 0xE0u) { c = b & 0x0Fu; len = 3; }"); - w.line("else if ((b & 0xF8u) == 0xF0u) { c = b & 0x07u; len = 4; }"); - w.line("else { c = 0xFFFDu; len = 1; }"); - w.line("if (i + len > n) { c = 0xFFFDu; len = 1; }"); - w.block("for (size_t k = 1; k < len; k++) {", "}", |w| { - w.line("unsigned char cb = (unsigned char)s[i + k];"); - w.line("if ((cb & 0xC0u) != 0x80u) { c = 0xFFFDu; len = k; break; }"); - w.line("c = (c << 6) | (cb & 0x3Fu);"); - }); - w.line("i += len;"); - w.line("if (c > 0x10FFFFu) { c = 0xFFFDu; }"); - w.line("if (c >= 0x10000u) { c -= 0x10000u; buf[o++] = (jchar)(0xD800u + (c >> 10)); buf[o++] = (jchar)(0xDC00u + (c & 0x3FFu)); }"); - w.line("else { buf[o++] = (jchar)c; }"); - }); - w.line("jstring out = (*env)->NewString(env, buf, (jsize)o);"); - w.line("free(buf);"); - w.line("return out;"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the thread-attach helpers every producer-thread entry point (async -/// completion callbacks, callback-interface trampolines, and vtable `free`) -/// uses. A producer thread is usually not a JVM thread: attach when needed -/// and detach before returning, because a thread that dies while still -/// attached leaves the JVM with a zombie attachment record that hangs -/// process shutdown. -pub(crate) fn render_jni_thread_helpers(out: &mut String) { - let mut w = CodeWriter::four_space(); - w.line("/* Returns 1 when this call attached the thread (the caller must detach),"); - w.line(" 0 otherwise. *env is NULL when no JNIEnv could be obtained. */"); - w.line("static int weaveffi_jni_attach(JNIEnv** env) {"); - w.scope(|w| { - w.line("*env = NULL;"); - w.line("if (weaveffi_jni_vm == NULL) { return 0; }"); - w.line("if ((*weaveffi_jni_vm)->GetEnv(weaveffi_jni_vm, (void**)env, JNI_VERSION_1_6) == JNI_OK) { return 0; }"); - // `AttachCurrentThread` takes `JNIEnv**` on Android and `void**` - // elsewhere; a `void*` converts implicitly to either in C. - w.line("if ((*weaveffi_jni_vm)->AttachCurrentThread(weaveffi_jni_vm, (void*)env, NULL) != JNI_OK) { *env = NULL; return 0; }"); - w.line("return 1;"); - }); - w.line("}"); - w.blank(); - w.line("static void weaveffi_jni_detach(int attached) {"); - w.scope(|w| { - w.line("if (attached) { (*weaveffi_jni_vm)->DetachCurrentThread(weaveffi_jni_vm); }"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the uncaught-exception helper for async continuation resumes: a -/// pending exception is routed to the settable Kotlin handler through the -/// cached `WeaveFFI` class. When no handler is installed (the dispatcher -/// rethrows) or the handler itself throws, the helper falls back to -/// `ExceptionDescribe`, so the exception is logged with its stack trace -/// before being cleared; it is never silently swallowed. -pub(crate) fn render_jni_uncaught_support(out: &mut String) { - let mut w = CodeWriter::four_space(); - w.line("static void weaveffi_jni_handle_uncaught(JNIEnv* env) {"); - w.scope(|w| { - w.line("if (!(*env)->ExceptionCheck(env)) { return; }"); - w.line("jthrowable ex = (*env)->ExceptionOccurred(env);"); - w.line("(*env)->ExceptionClear(env);"); - w.block( - "if (weaveffi_jni_entry_class != NULL && weaveffi_jni_dispatch_exc != NULL) {", - "}", - |w| { - w.line("(*env)->CallStaticVoidMethod(env, weaveffi_jni_entry_class, weaveffi_jni_dispatch_exc, ex);"); - w.line("if (!(*env)->ExceptionCheck(env)) { (*env)->DeleteLocalRef(env, ex); return; }"); - w.line("(*env)->ExceptionClear(env);"); - }, - ); - w.line("(*env)->Throw(env, ex);"); - w.line("(*env)->ExceptionDescribe(env);"); - w.line("(*env)->ExceptionClear(env);"); - w.line("(*env)->DeleteLocalRef(env, ex);"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the foreign-failure reporter callback trampolines call when the -/// Kotlin implementation (or the decoding shim in front of it) threw: the -/// pending exception is cleared, its `toString()` is borrowed, and -/// `{prefix}_error_set(out_err, -4, message)` hands the producer a copy. The -/// exception never unwinds through the C frame. -pub(crate) fn render_jni_foreign_error_support(out: &mut String, c_prefix: &str) { - let mut w = CodeWriter::four_space(); - w.line("static void weaveffi_jni_report_foreign(JNIEnv* env, weaveffi_error* out_err) {"); - w.scope(|w| { - w.line("jthrowable ex = (*env)->ExceptionOccurred(env);"); - w.line("(*env)->ExceptionClear(env);"); - w.line("jstring jmsg = NULL;"); - w.block( - "if (ex != NULL && weaveffi_jni_throwable_to_string != NULL) {", - "}", - |w| { - w.line("jmsg = (jstring)(*env)->CallObjectMethod(env, ex, weaveffi_jni_throwable_to_string);"); - w.line("if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); jmsg = NULL; }"); - }, - ); - w.line("char* msg = weaveffi_jni_string_to_utf8(env, jmsg);"); - w.line(format!( - "{c_prefix}_error_set(out_err, {FOREIGN_ERROR_CODE}, msg != NULL ? msg : \"callback interface implementation threw\");" - )); - w.line("free(msg);"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the generic thrower: constructs the brand exception with the raw -/// `(code, message)` pair via `NewObject` (so unknown codes keep their numeric -/// code) and throws it. Every non-throwing callable dispatches here, and it -/// is the trap channel for the runtime's reserved negative codes (`-1` -/// generic, `-2` producer panic, `-3` marshalling failure, `-4` a callback -/// interface implementation raised). -pub(crate) fn render_jni_generic_thrower(out: &mut String, jni_pkg_path: &str) { - let mut w = CodeWriter::four_space(); - w.line("static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) {"); - w.scope(|w| { - w.line("const char* msg = err->message ? err->message : \"WeaveFFI error\";"); - w.line(format!( - "jclass exClass = (*env)->FindClass(env, \"{}/{}\");", - jni_pkg_path, - errors::EXCEPTION_BRAND - )); - w.block("if (exClass != NULL) {", "}", |w| { - w.line("jmethodID ctor = (*env)->GetMethodID(env, exClass, \"\", \"(ILjava/lang/String;)V\");"); - w.line("jstring jmsg = weaveffi_jni_utf8_to_string(env, msg);"); - w.line("jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg);"); - w.line("if (ex != NULL) { (*env)->Throw(env, ex); }"); - }); - w.line("weaveffi_error_clear(err);"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the thrower for one declared error domain: the raw `(code, message)` -/// pair and the serialized payload (copied to a `jbyteArray`, or `NULL` when -/// absent) are handed to the sealed exception's static `fromCode` factory, -/// which decodes payload fields into the typed subclass; unknown codes fall -/// back to the generic exception inside `fromCode` itself. Both the message -/// and the payload buffer are released via `weaveffi_error_clear`. -pub(crate) fn render_jni_domain_thrower(out: &mut String, eb: &ErrorBinding, jni_pkg_path: &str) { - let exc = kotlin_exception_name(eb); - let brand = errors::EXCEPTION_BRAND; - let mut w = CodeWriter::four_space(); - w.line(format!( - "static void throw_{}(JNIEnv* env, weaveffi_error* err) {{", - eb.c_tag - )); - w.scope(|w| { - w.line(format!( - "jclass exClass = (*env)->FindClass(env, \"{jni_pkg_path}/{exc}\");" - )); - w.line(format!( - "jmethodID fromCode = exClass ? (*env)->GetStaticMethodID(env, exClass, \"fromCode\", \"(ILjava/lang/String;[B)L{jni_pkg_path}/{brand};\") : NULL;" - )); - w.block("if (fromCode == NULL) {", "}", |w| { - w.line("(*env)->ExceptionClear(env);"); - w.line("throw_weaveffi_error(env, err);"); - w.line("return;"); - }); - w.line("const char* msg = err->message ? err->message : \"WeaveFFI error\";"); - w.line("jstring jmsg = weaveffi_jni_utf8_to_string(env, msg);"); - w.line("jbyteArray jpayload = NULL;"); - w.block("if (err->payload_ptr != NULL) {", "}", |w| { - w.line("jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len);"); - w.line("if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); }"); - }); - w.line("jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, exClass, fromCode, (jint)err->code, jmsg, jpayload);"); - // A pending exception from fromCode itself (e.g. a malformed payload - // buffer) is left in place; otherwise the mapped exception is thrown. - w.line("if (ex != NULL && !(*env)->ExceptionCheck(env)) { (*env)->Throw(env, ex); }"); - w.line("weaveffi_error_clear(err);"); - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// The C thrower a sync callable's error check dispatches to: the typed -/// domain thrower when the callable's [`ErrorStrategy`] is -/// [`Throws`](ErrorStrategy::Throws) inside a module with an error domain, -/// the generic thrower otherwise (the trap channel). -pub(crate) fn jni_thrower_for(f: &FnBinding, error: Option<&ErrorBinding>) -> String { - match (error, f.error_strategy()) { - (Some(eb), ErrorStrategy::Throws) => format!("throw_{}", eb.c_tag), - _ => "throw_weaveffi_error".to_string(), - } -} - -/// Whether any sync or iterator callable dispatches to the domain thrower for -/// `c_tag`, counting inheriting submodules. Async errors bypass the C -/// throwers (they resume the continuation), so an async-only domain emits no -/// thrower. -pub(crate) fn domain_thrower_used(model: &BindingModel, c_tag: &str) -> bool { - model.modules.iter().any(|m| { - m.error.as_ref().is_some_and(|e| e.c_tag == c_tag) - && m.callables().any(|f| f.throws && !f.is_async) - }) -} diff --git a/crates/weaveffi-gen-kotlin/src/tests.rs b/crates/weaveffi-gen-kotlin/src/tests.rs deleted file mode 100644 index fdf69c6d..00000000 --- a/crates/weaveffi-gen-kotlin/src/tests.rs +++ /dev/null @@ -1,400 +0,0 @@ -//! Unit tests rendering an IR fixture through the generator: the fixture -//! covers an interface (constructor, methods, statics), a nullable interface -//! parameter and return, a record carrying an interface, iterators over -//! objects and records, a callback interface with buffered, object, nullable -//! object, and enum traffic, and an async callable. - -use camino::{Utf8Path, Utf8PathBuf}; -use weaveffi_core::backend::LanguageBackend; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::{FileContent, PackageContext}; -use weaveffi_core::platform::{BinarySet, NativeBinary, Platform}; -use weaveffi_core::resolved::ResolvedApi; - -use crate::{KotlinConfig, KotlinGenerator}; - -const FIXTURE: &str = r#" -version: "0.9.0" -modules: - - name: bus - enums: - - name: Level - variants: - - { name: Low, value: 0 } - - { name: High, value: 1 } - structs: - - name: Event - doc: A record carrying an object - fields: - - { name: id, type: i64 } - - { name: source, type: Store } - - { name: parent, type: "Store?" } - callback_interfaces: - - name: Subscriber - doc: Consumer-implemented event sink - methods: - - name: on_event - params: - - { name: event, type: Event } - - { name: store, type: Store } - - { name: maybe, type: "Store?" } - - { name: note, type: string } - - { name: level, type: Level } - return: bool - - name: pick_level - params: - - { name: hint, type: i32 } - return: Level - - name: on_close - params: [] - errors: - name: BusError - codes: - - { name: Closed, code: 1, message: "Bus is closed" } - interfaces: - - name: Store - doc: A reference-counted object - constructors: - - name: new - params: - - { name: name, type: string } - methods: - - name: describe - params: [] - return: string - - name: parent - params: [] - return: "Store?" - - name: adopt - params: - - { name: other, type: "Store?" } - - name: subscribe - params: - - { name: sub, type: Subscriber } - throws: true - - name: events - params: [] - return: "iter" - - name: children - params: [] - return: "iter" - statics: - - name: open_async - params: - - { name: name, type: string } - return: Store - async: true - functions: - - name: publish - params: - - { name: store, type: Store } - - { name: sub, type: Subscriber } - return: Event -"#; - -fn api() -> ResolvedApi { - let api = weaveffi_ir::parse::parse_api_str(FIXTURE, "yaml").expect("fixture parses"); - weaveffi_core::validate::validate_api(api, None).expect("fixture validates") -} - -fn render() -> (String, String) { - let api = api(); - let model = BindingModel::build(&api, "weaveffi"); - let files = KotlinGenerator.files(&api, &model, Utf8Path::new("out"), &KotlinConfig::default()); - let find = |name: &str| { - files - .iter() - .find(|f| f.path.as_str().ends_with(name)) - .unwrap_or_else(|| { - panic!( - "{name} missing from {:?}", - files.iter().map(|f| &f.path).collect::>() - ) - }) - .contents - .clone() - }; - (find("WeaveFFI.kt"), find("weaveffi_jni.c")) -} - -#[test] -fn layout_is_a_kotlin_gradle_module() { - let api = api(); - let model = BindingModel::build(&api, "weaveffi"); - let files = KotlinGenerator.files(&api, &model, Utf8Path::new("out"), &KotlinConfig::default()); - // `files()` joins with the host separator; the orchestrator's - // `output_files` normalizes to `/` later, so do the same here for Windows. - let paths: Vec = files - .iter() - .map(|f| f.path.as_str().replace('\\', "/")) - .collect(); - assert_eq!( - paths, - [ - "out/kotlin/settings.gradle.kts", - "out/kotlin/build.gradle.kts", - "out/kotlin/src/main/kotlin/com/weaveffi/WeaveFFI.kt", - "out/kotlin/src/main/cpp/CMakeLists.txt", - "out/kotlin/src/main/cpp/weaveffi_jni.c", - ] - ); - assert_eq!(KotlinGenerator.name(), "kotlin"); -} - -#[test] -fn interface_wrapper_is_cleaner_backed_and_exposes_clone() { - let (kt, c) = render(); - // The raw-handle constructor is private so a `new(Long)` exposed as - // `operator fun invoke(Long)` can't be shadowed by it inside the module; - // the generated code adopts handles through `fromHandle` instead. - assert!(kt.contains("class Store private constructor(handle: Long) : AutoCloseable {")); - assert!(kt.contains("internal fun fromHandle(handle: Long): Store = Store(handle)")); - assert!(!kt.contains("internal constructor(handle: Long) : AutoCloseable")); - assert!(kt.contains("private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) }")); - assert!(kt.contains("private val cleanable = weaveCleaner.register(this, ref)")); - assert!(kt.contains("internal fun cloneHandle(): Long = nativeClone(ref.get())")); - assert!(kt.contains("override fun close() {\n cleanable.clean()\n }")); - assert!(kt.contains("@JvmStatic private external fun nativeClone(handle: Long): Long")); - assert!(kt.contains( - "internal val weaveCleaner: java.lang.ref.Cleaner = java.lang.ref.Cleaner.create()" - )); - assert!(!kt.contains("finalize()")); - assert!( - c.contains("Java_com_weaveffi_Store_nativeClone(JNIEnv* env, jclass clazz, jlong handle)") - ); - assert!(c.contains("return (jlong)(intptr_t)weaveffi_bus_Store_clone((const weaveffi_bus_Store*)(intptr_t)handle);")); - assert!(c.contains("weaveffi_bus_Store_destroy((weaveffi_bus_Store*)(intptr_t)handle);")); -} - -#[test] -fn nullable_interface_maps_to_nullable_wrapper() { - let (kt, c) = render(); - // Return: the JNI layer boxes the owned pointer as `Long?`; the wrapper adopts it. - assert!(kt.contains("@JvmStatic private external fun nativeParent(selfHandle: Long): Long?")); - assert!( - kt.contains("fun parent(): Store? = nativeParent(handle)?.let { Store.fromHandle(it) }") - ); - assert!(c.contains("if (rv == NULL) { return NULL; }")); - // Parameter: the wrapper lends its handle, null crosses as null. - assert!(kt.contains("fun adopt(other: Store?) { nativeAdopt(handle, other?.handle) }")); - assert!(c.contains("int64_t other_val = 0;")); - assert!(c.contains("(const weaveffi_bus_Store*)(intptr_t)other_val")); -} - -#[test] -fn record_with_object_field_uses_object_tokens() { - let (kt, _) = render(); - assert!(kt.contains("data class Event(val id: Long, val source: Store, val parent: Store?)")); - // Encoding clones so the wrapper keeps its own reference; decoding adopts. - assert!(kt.contains("w.writeI64(v.source.cloneHandle())")); - assert!(kt.contains("w.writeOptional(v.parent) { v0 -> w.writeI64(v0.cloneHandle()) }")); - assert!(kt.contains("Store.fromHandle(r.readObject())")); - assert!(kt.contains("r.readOptional { Store.fromHandle(r.readObject()) }")); - assert!(kt.contains("fun readObject(): Long {")); -} - -#[test] -fn iterators_are_lazy_cleaner_backed_classes() { - let (kt, c) = render(); - assert!(kt.contains("class BusStoreEventsIterator internal constructor(handle: Long) : Iterator, AutoCloseable {")); - assert!(kt.contains("class BusStoreChildrenIterator internal constructor(handle: Long) : Iterator, AutoCloseable {")); - assert!(kt.contains("val slot = nativeNext(ref.get())")); - assert!(kt.contains("return Store.fromHandle(raw as Long)")); - assert!(kt.contains("return weaveDecode((raw as ByteArray)) { r -> unpackEvent(r) }")); - assert!( - kt.contains("fun events(): Iterator = BusStoreEventsIterator(nativeEvents(handle))") - ); - assert!(c.contains("Java_com_weaveffi_BusStoreChildrenIterator_nativeNext")); - assert!(c.contains("weaveffi_bus_Store_EventsIterator_destroy")); -} - -#[test] -fn callback_interface_renders_interface_and_dispatch_object() { - let (kt, _) = render(); - assert!(kt.contains("interface Subscriber {")); - assert!(kt.contains( - "fun onEvent(event: Event, store: Store, maybe: Store?, note: String, level: Level): Boolean" - )); - assert!(kt.contains("fun pickLevel(hint: Int): Level")); - assert!(kt.contains("fun onClose()")); - assert!(kt.contains("internal object SubscriberJni {")); - // The shim decodes the buffer, adopts both object references, and lowers the enum. - assert!(kt.contains( - "@JvmStatic fun onEvent(impl: Subscriber, event: ByteArray, store: Long, maybe: Long, note: String, level: Int): Boolean = impl.onEvent(weaveDecode(event) { r -> unpackEvent(r) }, Store.fromHandle(store), maybe.takeIf { it != 0L }?.let { Store.fromHandle(it) }, note, Level.fromValue(level))" - )); - assert!(kt.contains( - "@JvmStatic fun pickLevel(impl: Subscriber, hint: Int): Int = impl.pickLevel(hint).value" - )); - assert!(kt.contains("@JvmStatic fun onClose(impl: Subscriber) { impl.onClose() }")); - // Passing an implementation needs no wrapper split: the object crosses as itself. - assert!(kt.contains("fun subscribe(sub: Subscriber) { nativeSubscribe(handle, sub) }")); - assert!(kt.contains( - "@JvmStatic private external fun publishJni(store: Long, sub: Subscriber): ByteArray" - )); -} - -#[test] -fn callback_interface_shim_pins_global_ref_and_uses_static_vtable() { - let (_, c) = render(); - // JNI_OnLoad caches the VM, the dispatch class, and every method ID. - assert!(c.contains("static JavaVM* weaveffi_jni_vm = NULL;")); - assert!(c.contains("JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {")); - assert!(c.contains("if (weaveffi_abi_version() != 2u) {")); - assert!(c.contains("FindClass(env, \"com/weaveffi/SubscriberJni\")")); - assert!(c.contains( - "GetStaticMethodID(env, weaveffi_bus_Subscriber_jni_cls, \"onEvent\", \"(Lcom/weaveffi/Subscriber;[BJJLjava/lang/String;I)Z\");" - )); - assert!(c.contains( - "GetStaticMethodID(env, weaveffi_bus_Subscriber_jni_cls, \"pickLevel\", \"(Lcom/weaveffi/Subscriber;I)I\");" - )); - assert!(c.contains( - "GetStaticMethodID(env, weaveffi_bus_Subscriber_jni_cls, \"onClose\", \"(Lcom/weaveffi/Subscriber;)V\");" - )); - // Trampolines attach, convert, call, and report foreign failures. - // The trampoline signature is the header's vtable entry: object arguments - // arrive as owned (non-const) pointers the shim adopts. - assert!(c.contains("static bool weaveffi_bus_Subscriber_jni_on_event(void* ctx, const uint8_t* event_ptr, size_t event_len, weaveffi_bus_Store* store, weaveffi_bus_Store* maybe, const char* note, weaveffi_bus_Level level, weaveffi_error* out_err) {")); - assert!(c.contains("int attached = weaveffi_jni_attach(&env);")); - assert!(c.contains("jbyteArray _a0 = (*env)->NewByteArray(env, (jsize)event_len);")); - assert!(c.contains("jlong _a1 = (jlong)(intptr_t)store;")); - assert!(c.contains("jlong _a2 = (jlong)(intptr_t)maybe;")); - assert!(c.contains("jstring _a3 = weaveffi_jni_utf8_to_string(env, note);")); - assert!(c.contains("jint _a4 = (jint)level;")); - assert!(c.contains("jboolean _rv = (*env)->CallStaticBooleanMethod(env, weaveffi_bus_Subscriber_jni_cls, weaveffi_bus_Subscriber_jni_mid_on_event, (jobject)ctx, _a0, _a1, _a2, _a3, _a4);")); - assert!(c.contains("weaveffi_jni_report_foreign(env, out_err);")); - assert!(c.contains("weaveffi_error_set(out_err, -4, msg != NULL ? msg : \"callback interface implementation threw\");")); - assert!(c.contains("return _rv == JNI_TRUE;")); - assert!(c.contains("jint _rv = (*env)->CallStaticIntMethod(env, weaveffi_bus_Subscriber_jni_cls, weaveffi_bus_Subscriber_jni_mid_pick_level, (jobject)ctx, _a0);")); - assert!(c.contains("weaveffi_jni_detach(attached);")); - // `free` deletes the global ref; the vtable is one static value. - assert!(c.contains("static void weaveffi_bus_Subscriber_jni_free(void* ctx) {")); - assert!(c.contains("(*env)->DeleteGlobalRef(env, (jobject)ctx);")); - assert!(c.contains("static const weaveffi_bus_Subscriber_vtable weaveffi_bus_Subscriber_jni_vtable = { weaveffi_bus_Subscriber_jni_on_event, weaveffi_bus_Subscriber_jni_pick_level, weaveffi_bus_Subscriber_jni_on_close, weaveffi_bus_Subscriber_jni_free };")); - // The export pins the implementation and passes ctx + vtable. - assert!(c.contains("jobject sub_ref = (*env)->NewGlobalRef(env, sub);")); - assert!(c.contains("weaveffi_bus_Store_subscribe((const weaveffi_bus_Store*)(intptr_t)selfHandle, (void*)sub_ref, &weaveffi_bus_Subscriber_jni_vtable, &err);")); - // The throwing method dispatches to the domain thrower. - assert!(c.contains("throw_weaveffi_bus_BusError(env, &err);")); -} - -#[test] -fn strings_cross_as_standard_utf8_not_modified_utf8() { - let (_, c) = render(); - // Both converters are emitted once, ahead of every user. - assert!(c.contains("#include ")); - assert!(c.contains("static char* weaveffi_jni_string_to_utf8(JNIEnv* env, jstring s) {")); - assert!(c.contains("static jstring weaveffi_jni_utf8_to_string(JNIEnv* env, const char* s) {")); - assert!( - c.find("weaveffi_jni_string_to_utf8").unwrap() < c.find("throw_weaveffi_error").unwrap() - ); - // A string parameter is copied as standard UTF-8 and freed after the call; - // a string return is rebuilt from UTF-8. JNI's modified-UTF-8 entry points - // never appear, since they mangle supplementary characters. - assert!(c.contains("char* name_chars = weaveffi_jni_string_to_utf8(env, name);")); - assert!(c.contains("free(name_chars);")); - assert!(c.contains("jstring out = weaveffi_jni_utf8_to_string(env, rv);")); - assert!(c.contains("jstring jmsg = weaveffi_jni_utf8_to_string(env, msg);")); - assert!(c.contains("char* msg = weaveffi_jni_string_to_utf8(env, jmsg);")); - assert!(!c.contains("->GetStringUTFChars(")); - assert!(!c.contains("->ReleaseStringUTFChars(")); - assert!(!c.contains("->NewStringUTF(")); -} - -#[test] -fn async_uses_cached_vm_and_adopts_object_result() { - let (kt, c) = render(); - assert!(kt.contains("suspend fun openAsync(name: String): Store {")); - assert!(kt.contains("return Store.fromHandle(raw)")); - assert!(c.contains("static void weaveffi_bus_Store_open_async_jni_cb(void* context, weaveffi_error* err, weaveffi_bus_Store* result) {")); - assert!(c.contains("weaveffi_jni_handle_uncaught(env);")); - assert!(c.contains("(jlong)(intptr_t)result);")); - assert!(!c.contains("GetJavaVM")); -} - -#[test] -fn no_arena_or_listener_bindings_remain() { - let (kt, c) = render(); - for stale in ["arena", "listener", "Listener", "pthread"] { - assert!(!kt.contains(stale), "Kotlin still mentions {stale}"); - assert!(!c.contains(stale), "C shim still mentions {stale}"); - } - // Every class touching natives goes through the single loader object. - assert!(kt.contains("init { WeaveNativeLibrary.ensureLoaded() }")); - assert_eq!(kt.matches("System.loadLibrary(").count(), 1); - assert!(kt.contains("init { System.loadLibrary(\"weaveffi\") }")); -} - -#[test] -fn package_lays_out_an_aar_style_module() { - let api = api(); - let model = BindingModel::build(&api, "weaveffi"); - let mut binaries = BinarySet::new("bus"); - binaries.binaries.push(NativeBinary { - platform: Platform::AndroidArm64, - source: Utf8PathBuf::from("/prebuilt/android-arm64/libbus.so"), - }); - binaries.binaries.push(NativeBinary { - platform: Platform::MacosArm64, - source: Utf8PathBuf::from("/prebuilt/darwin-arm64/libbus.dylib"), - }); - binaries.binaries.push(NativeBinary { - platform: Platform::Wasm32, - source: Utf8PathBuf::from("/prebuilt/wasm32/bus.wasm"), - }); - let ctx = PackageContext { - binaries: &binaries, - input_basename: Some("bus.yml"), - }; - let files = KotlinGenerator - .package( - &api, - &model, - &ctx, - Utf8Path::new("out"), - &KotlinConfig::default(), - ) - .expect("kotlin supports packaging"); - let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect(); - assert_eq!( - paths, - [ - "out/kotlin/settings.gradle.kts", - "out/kotlin/build.gradle.kts", - "out/kotlin/README.md", - "out/kotlin/src/main/kotlin/com/weaveffi/WeaveFFI.kt", - "out/kotlin/src/main/cpp/CMakeLists.txt", - "out/kotlin/src/main/cpp/weaveffi_jni.c", - "out/kotlin/src/main/cpp/include/weaveffi.h", - "out/kotlin/src/main/jniLibs/arm64-v8a/libbus.so", - "out/kotlin/src/main/resources/natives/darwin-arm64/libbus.dylib", - ] - ); - let text = |name: &str| match &files - .iter() - .find(|f| f.path.as_str().ends_with(name)) - .unwrap() - .content - { - FileContent::Text(t) => t.clone(), - FileContent::Copy(_) => panic!("{name} is a binary"), - }; - let kt = text("WeaveFFI.kt"); - assert!(kt.contains("private const val LIB = \"bus\"")); - assert!(kt.contains("private const val JNI_LIB = \"bus_jni\"")); - assert!(kt.contains("System.loadLibrary(JNI_LIB)")); - assert!(kt.contains("getResourceAsStream(\"/natives/$id/$name\")")); - let cmake = text("CMakeLists.txt"); - assert!(cmake.contains("add_library(bus_jni SHARED weaveffi_jni.c)")); - assert!(cmake.contains("../jniLibs/${ANDROID_ABI}/libbus.so")); - let gradle = text("build.gradle.kts"); - assert!(gradle.contains("namespace = \"com.weaveffi\"")); - assert!(gradle.contains("minSdk = 33")); - assert!(gradle.contains("jniLibs.srcDirs(\"src/main/jniLibs\")")); - let header = text("weaveffi.h"); - assert!(header.contains("typedef struct weaveffi_bus_Subscriber_vtable {")); - assert!(header.contains("weaveffi_bus_Store_clone")); - let readme = text("README.md"); - assert!(readme.contains("- `src/main/jniLibs/arm64-v8a/libbus.so`")); - assert!(readme.contains("- `src/main/resources/natives/darwin-arm64/libbus.dylib`")); - assert!(!readme.contains("wasm32")); -} diff --git a/crates/weaveffi-gen-kotlin/src/types.rs b/crates/weaveffi-gen-kotlin/src/types.rs deleted file mode 100644 index 61ae361d..00000000 --- a/crates/weaveffi-gen-kotlin/src/types.rs +++ /dev/null @@ -1,360 +0,0 @@ -//! Type mapping and naming policy: the Kotlin surface type, the lowered JNI -//! type, the JNI method descriptor, and the C declaration type for every IR -//! type, plus identifier casing, JNI name mangling, and reserved-word -//! escaping. - -use std::fmt::Write as _; - -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}; - -/// Escape a user-chosen identifier for a Kotlin declaration or expression -/// position: a reserved word gains the shared trailing underscore -/// ([`lang::escape_ident`] over [`lang::KOTLIN_KEYWORDS`]). -pub(crate) fn kt_escape(name: &str) -> String { - lang::escape_ident(name, lang::KOTLIN_KEYWORDS) -} - -/// The Kotlin spelling of one IR parameter name: lowerCamelCased, then -/// escaped when the result is a Kotlin reserved word. -pub(crate) fn kt_param(name: &str) -> String { - kt_escape(&lower_camel(name)) -} - -/// Escape a user-chosen identifier for a C declaration in the JNI shim: a C -/// reserved word gains the shared trailing underscore. JNI export names are -/// unaffected (linkage uses the mangled Kotlin method name, not C parameter -/// names). -pub(crate) fn c_local(name: &str) -> String { - lang::escape_ident(name, lang::C_KEYWORDS) -} - -/// The idiomatic (public) Kotlin type for an IR type: records and rich enums -/// surface as their generated value classes, interfaces as their wrapper -/// classes, callback interfaces as the generated Kotlin `interface`, lists -/// and maps as `List`/`Map`, and optionals as nullable types. -pub(crate) fn kotlin_type(t: &Ty) -> String { - match t { - 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 => "String".to_string(), - Ty::Bytes => "ByteArray".to_string(), - // An interface surfaces as its generated Kotlin wrapper class; the - // JNI layer carries the raw `Long` pointer. A callback interface is - // the generated Kotlin `interface` the consumer implements. - // 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. - Ty::Interface(name) - | Ty::CallbackInterface(name) - | Ty::Record(name) - | Ty::RichEnum(name) - | 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, and callback interfaces as the -/// implementing object itself. Everything else matches [`kotlin_type`]. -pub(crate) fn kotlin_jni_type(t: &Ty) -> String { - if t.is_buffered() { - return "ByteArray".to_string(); - } - match t { - 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?`. - 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. - Ty::Iterator(_) => "Long".to_string(), - other => kotlin_type(other), - } -} - -/// The Kotlin parameter type of a callback-interface dispatch shim (the -/// static method the native vtable trampoline calls): buffered arguments -/// arrive as a borrowed `ByteArray` copy, enums as their raw `Int`, and -/// objects (nullable or not) as the raw pointer `Long` the shim adopts -/// (`0L` is none). Everything else matches [`kotlin_type`]. -pub(crate) fn kotlin_shim_param_type(t: &Ty) -> String { - if t.is_buffered() { - return "ByteArray".to_string(); - } - match t { - Ty::Enum(_) => "Int".to_string(), - // Only `Interface?` reaches the optional arm (every other optional is - // buffered); the trampoline passes the raw pointer, `0` for none. - Ty::Interface(_) | Ty::Optional(_) => "Long".to_string(), - other => kotlin_type(other), - } -} - -/// The JNI C parameter type (`jint`, `jbyteArray`, ...) an IR type crosses -/// the JNI export boundary as. Buffered values arrive as one packed -/// `jbyteArray`; a callback interface arrives as the implementing `jobject`. -pub(crate) fn jni_param_type(t: &Ty) -> String { - if t.is_buffered() { - return "jbyteArray".to_string(); - } - match t { - 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::Interface(_) => "jlong".to_string(), - Ty::F32 => "jfloat".to_string(), - Ty::F64 => "jdouble".to_string(), - Ty::Bool => "jboolean".to_string(), - Ty::StringUtf8 => "jstring".to_string(), - Ty::Bytes => "jbyteArray".to_string(), - // Only `Interface?` reaches here: Kotlin's `Long?` boxes to - // `java.lang.Long`, so the slot is a nullable `jobject`. A callback - // interface is the implementing object, pinned by the export. - Ty::Optional(_) | Ty::CallbackInterface(_) => "jobject".to_string(), - // An iterator return crosses as the raw handle (`jlong`); it is never - // a parameter (validation rejects that position). - Ty::Iterator(_) => "jlong".to_string(), - Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { - unreachable!("buffered types are handled above") - } - } -} - -/// 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<&Ty>) -> String { - match t { - None => "void".to_string(), - Some(t) => jni_param_type(t), - } -} - -/// The JNI type descriptor of a callback-interface dispatch shim parameter -/// or return, matching [`kotlin_shim_param_type`]: `Ljava/lang/String;` for -/// strings, `[B` for bytes and buffered values, `J` for object pointers, and -/// the primitive letters for the direct family. `None` is `V`. -pub(crate) fn jni_descriptor(t: Option<&Ty>) -> &'static str { - let Some(t) = t else { - return "V"; - }; - if t.is_buffered() { - return "[B"; - } - match t { - Ty::I8 | Ty::U8 => "B", - Ty::I16 | Ty::U16 => "S", - Ty::I32 | Ty::Enum(_) => "I", - Ty::U32 | Ty::I64 | Ty::U64 | Ty::Interface(_) | Ty::Optional(_) => "J", - Ty::F32 => "F", - Ty::F64 => "D", - Ty::Bool => "Z", - Ty::StringUtf8 => "Ljava/lang/String;", - Ty::Bytes => "[B", - Ty::CallbackInterface(_) | Ty::Iterator(_) => { - unreachable!("validation rejects {t} inside a callback method") - } - Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { - unreachable!("buffered types are handled above") - } - } -} - -/// The `CallStatic{X}Method` suffix and matching `j{x}` C type used to invoke -/// a callback-interface dispatch shim returning `t` (a direct-family type or -/// `None` for void). -pub(crate) fn jni_call_kind(t: Option<&Ty>) -> (&'static str, &'static str) { - match t { - None => ("Void", "void"), - Some(Ty::I8 | Ty::U8) => ("Byte", "jbyte"), - Some(Ty::I16 | Ty::U16) => ("Short", "jshort"), - Some(Ty::I32 | Ty::Enum(_)) => ("Int", "jint"), - Some(Ty::U32 | Ty::I64 | Ty::U64) => ("Long", "jlong"), - Some(Ty::F32) => ("Float", "jfloat"), - Some(Ty::F64) => ("Double", "jdouble"), - Some(Ty::Bool) => ("Boolean", "jboolean"), - Some(other) => unreachable!("callback methods return direct values only, not {other}"), - } -} - -/// The C default value a trampoline returns when the consumer implementation -/// raised: `0`, `false`, or `0.0`, matching the vtable entry's C return type. -pub(crate) fn c_default_value(t: Option<&Ty>) -> &'static str { - match t { - None => "", - Some(Ty::Bool) => "false", - Some(Ty::F32) => "0.0f", - Some(Ty::F64) => "0.0", - Some(_) => "0", - } -} - -/// The C declaration type for the scalar-shaped returns handled by the -/// generic fallthrough in `write_return_handling`. Buffered, string, bytes, -/// object, and iterator returns have dedicated emitters. -pub(crate) fn c_type_for_return(t: &Ty) -> &'static str { - match t { - 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", - 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<&Ty>) -> &'static str { - let Some(t) = t else { - return ""; - }; - if t.is_buffered() { - return "return NULL;"; - } - match t { - Ty::I8 | Ty::U8 | Ty::I16 | Ty::U16 => "return 0;", - Ty::I32 | Ty::Enum(_) => "return 0;", - Ty::U32 | Ty::I64 | Ty::U64 => "return 0;", - Ty::F32 => "return 0.0f;", - Ty::F64 => "return 0.0;", - Ty::Bool => "return JNI_FALSE;", - Ty::StringUtf8 | Ty::Bytes => "return NULL;", - Ty::Interface(_) => "return 0;", - // Only `Interface?` reaches here (a nullable boxed `Long`). - Ty::Optional(_) => "return NULL;", - // The iterator launcher returns the handle as a `jlong`; 0 = failed. - Ty::Iterator(_) => "return 0;", - Ty::CallbackInterface(_) => unreachable!("callback interfaces are never returned"), - Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { - unreachable!("buffered types are handled above") - } - } -} - -/// 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: &Ty) -> &'static str { - match t { - Ty::I8 | Ty::U8 => "(jbyte)", - Ty::I16 | Ty::U16 => "(jshort)", - Ty::I32 | Ty::Enum(_) => "(jint)", - Ty::U32 | Ty::I64 | Ty::U64 => "(jlong)", - Ty::F32 => "(jfloat)", - Ty::F64 => "(jdouble)", - _ => "", - } -} - -/// JNI exports map a Java identifier to a C symbol by escaping `_` to `_1` -/// (plus `;`->`_2`, `[`->`_3`, and non-ASCII to `_0xxxx`). Our function names -/// are `snake_case`, so the runtime lookup of `Java___` only -/// resolves when the `` component is mangled this way. -pub(crate) fn jni_mangle(ident: &str) -> String { - let mut out = String::with_capacity(ident.len()); - for c in ident.chars() { - match c { - '_' => out.push_str("_1"), - ';' => out.push_str("_2"), - '[' => out.push_str("_3"), - c if c.is_ascii_alphanumeric() => out.push(c), - c => { - let _ = write!(out, "_0{:04x}", c as u32); - } - } - } - out -} - -/// Lower-camelCase an identifier (e.g. a PascalCase variant name) for use as a -/// Kotlin factory method / property-prefix. Reuses [`pascal_case`] (which also -/// normalizes `snake_case`) and then lowercases only the leading character, so -/// `Circle` becomes `circle` and `rich_variant` becomes `richVariant`. -pub(crate) fn lower_camel(s: &str) -> String { - let pascal = pascal_case(s); - let mut chars = pascal.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_lowercase().chain(chars).collect(), - } -} - -/// The Kotlin name of a free function: the module prefix is applied (or -/// stripped) first, then the result is lowerCamelCased and keyword-escaped, -/// so `contacts` + `create_contact` is `createContact` when stripping (the -/// default) and `contactsCreateContact` otherwise. -pub(crate) fn kotlin_fn_name(module_path: &str, name: &str, strip_module_prefix: bool) -> String { - kt_escape(&lower_camel(&wrapper_name( - module_path, - name, - strip_module_prefix, - ))) -} - -/// Clone `params` with camelCased (and keyword-escaped) names so KDoc -/// `@param` tags match the emitted Kotlin parameter spelling. -pub(crate) fn camel_params(params: &[ParamBinding]) -> Vec { - params - .iter() - .map(|p| ParamBinding { - name: kt_param(&p.name), - ..p.clone() - }) - .collect() -} - -/// Whether a function needs the private-`Jni` + public-wrapper split rather -/// than a bare `external fun`. This is required when any param or the return -/// crosses the JNI boundary as a *different* type than its public Kotlin -/// type: buffered values (pack/decode a `ByteArray`), enums -/// (`.value`/`fromValue`), interfaces (`.handle` / re-wrap into the class), -/// and iterator returns (the raw `Long` handle is adopted into the generated -/// iterator class). Callback interfaces cross as the implementing object and -/// need no split. -pub(crate) fn needs_wrapper_split(f: &FnBinding) -> bool { - 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(Ty::Iterator(_))) - || f.ret.as_ref().is_some_and(differs) -} - -/// The Kotlin class name of the lazy iterator wrapper for one `iter` -/// callable, derived from the unique C iterator tag with the business prefix -/// stripped (`weaveffi_contacts_ListContactsIterator` becomes -/// `ContactsListContactsIterator`). -pub(crate) fn kotlin_iterator_class_name(it: &IteratorBinding, c_prefix: &str) -> String { - let prefix = format!("{c_prefix}_"); - let stripped = it.iter_tag.strip_prefix(&prefix).unwrap_or(&it.iter_tag); - stripped.split('_').map(pascal_case).collect() -} - -/// The Kotlin name of the dispatch object that adapts a callback interface -/// implementation to the JNI trampolines: `{Name}Jni`. -pub(crate) fn kotlin_callback_dispatch_name(cb_name: &str) -> String { - format!("{}Jni", local_type_name(cb_name)) -} diff --git a/crates/weaveffi-gen-node/Cargo.toml b/crates/weaveffi-gen-node/Cargo.toml deleted file mode 100644 index 286a0e8c..00000000 --- a/crates/weaveffi-gen-node/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "weaveffi-gen-node" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "Node.js N-API and TypeScript binding generator for WeaveFFI" -keywords = ["ffi", "codegen", "bindings", "nodejs", "typescript"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-node/src/addon.rs b/crates/weaveffi-gen-node/src/addon.rs deleted file mode 100644 index 6e354cca..00000000 --- a/crates/weaveffi-gen-node/src/addon.rs +++ /dev/null @@ -1,1678 +0,0 @@ -//! The native N-API addon (`weaveffi_addon.c`): one C entry point per -//! callable, the per-interface `clone`/`destroy` entry points, the lazy -//! iterator and async machinery, and one static vtable plus trampolines per -//! callback interface. -//! -//! Marshalling dispatch is driven by the shared plan layer: -//! [`ParamBinding::arg_pass`] decides how each incoming JS argument crosses -//! into its ABI slots, [`plan::ret_pass`] decides what the entry point does -//! with a result, and [`CallbackInterfaceBinding::protocol`] decides how a -//! trampoline receives each argument; only the N-API spellings live here. -//! -//! Object handles cross the addon boundary as `bigint`s holding the pointer -//! value, 64-bit integers as `bigint`s (never `number`, so nothing above -//! 2^53 is rounded), and every other direct value as the obvious JS type. - -use weaveffi_core::abi::{self, split_qualified}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{ - iterator_item_ctype, BindingModel, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, - FnBinding, InterfaceBinding, IteratorBinding, ModuleBinding, ParamBinding, -}; -use weaveffi_core::plan::{self, elem_free, ArgPass, Free, RetPass}; -use weaveffi_core::utils::{render_prelude, render_trailer, wrapper_name, CommentStyle}; - -use crate::types::{iface_lifecycle_base, iface_member_base, js_fn_name}; - -/// The ABI error code a callback trampoline writes when the consumer's -/// implementation raised (the producer runtime's `FOREIGN_ERROR_CODE`). -const FOREIGN_ERROR_CODE: i32 = -4; - -/// 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: &Ty, module: &str, prefix: &str) -> String { - if matches!(ty, Ty::Iterator(_)) { - return "void*".into(); - } - abi::lower_return(ty, module).ret.render_c(prefix) -} - -/// The `{c_tag}` stem of a (possibly dot-qualified) callback interface name -/// referenced from `module`; its vtable is `{c_tag}_vtable` and its addon -/// helpers hang off the same stem. -fn callback_c_tag(name: &str, module: &str, prefix: &str) -> String { - let (m, n) = split_qualified(name, module); - format!("{prefix}_{m}_{n}") -} - -/// The C statement that creates napi value `target` from a direct-family C -/// expression `expr` (scalars, bools, C-style enums). 64-bit integers become -/// `bigint`s. -fn napi_create_leaf(ty: &Ty, expr: &str, target: &str) -> String { - match ty { - Ty::I8 | Ty::I16 | Ty::I32 => format!("napi_create_int32(env, {expr}, &{target});"), - Ty::U8 | Ty::U16 | Ty::U32 => format!("napi_create_uint32(env, {expr}, &{target});"), - Ty::I64 => format!("napi_create_bigint_int64(env, {expr}, &{target});"), - Ty::U64 => format!("napi_create_bigint_uint64(env, {expr}, &{target});"), - Ty::F32 | Ty::F64 => 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});"), - other => unreachable!("direct leaf with non-direct type {other:?}"), - } -} - -/// Emit the statements reading a direct-family JS value `val` into the C -/// lvalue `target` of type `ty`, recording the N-API status in `status`. -/// N-API only exposes 32/64-bit int and `double` getters, so narrower -/// scalars are read into a wider temporary and narrowed with an explicit -/// cast; 64-bit integers are read from `bigint`s losslessly (a `number` is -/// accepted too, and an out-of-range `bigint` is a failure). -fn emit_leaf_read(out: &mut String, indent: &str, ty: &Ty, val: &str, target: &str, status: &str) { - let tmp = format!("{}_tmp", target.replace(['.', '>', '-', '[', ']'], "_")); - match ty { - Ty::I32 => out.push_str(&format!( - "{indent}{status} = napi_get_value_int32(env, {val}, &{target});\n" - )), - Ty::U32 => out.push_str(&format!( - "{indent}{status} = napi_get_value_uint32(env, {val}, &{target});\n" - )), - Ty::I8 | Ty::I16 => { - out.push_str(&format!("{indent}int32_t {tmp} = 0;\n")); - out.push_str(&format!( - "{indent}{status} = napi_get_value_int32(env, {val}, &{tmp});\n" - )); - out.push_str(&format!( - "{indent}{target} = ({}){tmp};\n", - c_scalar_type(ty) - )); - } - Ty::U8 | Ty::U16 => { - out.push_str(&format!("{indent}uint32_t {tmp} = 0;\n")); - out.push_str(&format!( - "{indent}{status} = napi_get_value_uint32(env, {val}, &{tmp});\n" - )); - out.push_str(&format!( - "{indent}{target} = ({}){tmp};\n", - c_scalar_type(ty) - )); - } - Ty::I64 => out.push_str(&format!( - "{indent}{status} = weaveffi_napi_get_i64(env, {val}, &{target});\n" - )), - Ty::U64 => out.push_str(&format!( - "{indent}{status} = weaveffi_napi_get_u64(env, {val}, &{target});\n" - )), - Ty::F64 => out.push_str(&format!( - "{indent}{status} = napi_get_value_double(env, {val}, &{target});\n" - )), - Ty::F32 => { - out.push_str(&format!("{indent}double {tmp} = 0;\n")); - out.push_str(&format!( - "{indent}{status} = napi_get_value_double(env, {val}, &{tmp});\n" - )); - out.push_str(&format!("{indent}{target} = (float){tmp};\n")); - } - Ty::Bool => out.push_str(&format!( - "{indent}{status} = napi_get_value_bool(env, {val}, &{target});\n" - )), - Ty::Enum(_) => { - out.push_str(&format!("{indent}int32_t {tmp} = 0;\n")); - out.push_str(&format!( - "{indent}{status} = napi_get_value_int32(env, {val}, &{tmp});\n" - )); - out.push_str(&format!("{indent}{target} = {tmp};\n")); - } - other => unreachable!("direct leaf read with non-direct type {other:?}"), - } -} - -/// The bare C type of a scalar parameter temporary. -fn c_scalar_type(ty: &Ty) -> &'static str { - match ty { - 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"), - } -} - -/// Emit the shared value helpers every entry point uses: lossless 64-bit -/// readers (a `bigint` that does not fit is a `RangeError`, a non-integer a -/// `TypeError`, so no value is ever silently truncated), and the object -/// handle codec (`bigint` holding the pointer value; `null`/`undefined` read -/// as `NULL` and a `NULL` pointer surfaces as `null`). All `static inline` -/// so an addon that never needs one compiles without an unused warning. -fn render_value_helpers_c(out: &mut String) { - out.push_str( - r#"static inline napi_status weaveffi_napi_get_i64(napi_env env, napi_value v, int64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_int64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - return napi_get_value_int64(env, v, out); - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_get_u64(napi_env env, napi_value v, uint64_t* out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_bigint) { - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, out, &lossless); - if (s == napi_ok && !lossless) { - napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); - return napi_generic_failure; - } - return s; - } - if (t == napi_number) { - double d = 0; - napi_status s = napi_get_value_double(env, v, &d); - if (s == napi_ok && (d < 0 || d != d)) { - napi_throw_range_error(env, NULL, "negative number for an unsigned 64-bit integer"); - return napi_generic_failure; - } - *out = (uint64_t)d; - return s; - } - napi_throw_type_error(env, NULL, "expected a bigint"); - return napi_bigint_expected; -} - -// An object handle is the pointer value carried as a bigint. null and -// undefined read as NULL (the absent case of a nullable object). -static inline napi_status weaveffi_napi_get_handle(napi_env env, napi_value v, void** out) { - napi_valuetype t; - napi_typeof(env, v, &t); - if (t == napi_null || t == napi_undefined) { - *out = NULL; - return napi_ok; - } - if (t == napi_bigint) { - uint64_t raw = 0; - bool lossless = false; - napi_status s = napi_get_value_bigint_uint64(env, v, &raw, &lossless); - *out = (void*)(uintptr_t)raw; - return s; - } - napi_throw_type_error(env, NULL, "expected an object handle"); - return napi_bigint_expected; -} - -static inline napi_status weaveffi_napi_make_handle(napi_env env, const void* p, napi_value* out) { - if (p == NULL) { - return napi_get_null(env, out); - } - return napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, out); -} - -"#, - ); -} - -/// Emit `{prefix}_napi_error_value`, the shared constructor of the JS error -/// object every failure path produces: a plain `Error` carrying the numeric -/// ABI code as a `code` property and, when the producer attached one, the -/// structured payload buffer as a `payload` property. The JS loader rebrands -/// it as the generic `WeaveFFIError` or the module's typed domain class and -/// decodes the payload fields there. -fn render_error_value_helper_c(out: &mut String, prefix: &str) { - out.push_str(&format!( - "static napi_value {prefix}_napi_error_value(napi_env env, int32_t code, const char* message, const uint8_t* payload_ptr, size_t payload_len) {{\n" - )); - out.push_str(" napi_value msg;\n"); - out.push_str( - " napi_create_string_utf8(env, message ? message : \"\", NAPI_AUTO_LENGTH, &msg);\n", - ); - out.push_str(" napi_value err;\n"); - out.push_str(" napi_create_error(env, NULL, msg, &err);\n"); - out.push_str(" napi_value code_val;\n"); - out.push_str(" napi_create_int32(env, code, &code_val);\n"); - out.push_str(" napi_set_named_property(env, err, \"code\", code_val);\n"); - out.push_str(" if (payload_ptr != NULL) {\n"); - out.push_str(" napi_value payload_val;\n"); - out.push_str( - " napi_create_buffer_copy(env, payload_len, payload_ptr, NULL, &payload_val);\n", - ); - out.push_str(" napi_set_named_property(env, err, \"payload\", payload_val);\n"); - out.push_str(" }\n"); - out.push_str(" return err;\n"); - out.push_str("}\n\n"); -} - -/// Emit the post-call `out_err` check: throw the code-carrying JS error (with -/// the borrowed payload buffer copied in) and bail on a non-zero slot, then -/// clear the error, which releases both the message and the payload. The JS -/// loader maps the `code` property to the module's typed domain class -/// (throwing callables) or the generic brand. -fn emit_error_check_c(out: &mut String, prefix: &str) { - out.push_str(" if (err.code != 0) {\n"); - out.push_str(&format!( - " napi_throw(env, {prefix}_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len));\n" - )); - out.push_str(&format!(" {prefix}_error_clear(&err);\n")); - out.push_str(" return NULL;\n"); - out.push_str(" }\n"); -} - -/// Emit the shared state cell every lazy iterator external wraps. The cell -/// owns the native iterator handle; `next` on exhaustion, the JS wrapper's -/// `return()`, and the external's finalizer all null it before destroying, -/// so the handle is destroyed exactly once no matter which path runs first. -fn render_iter_state_c(out: &mut String, prefix: &str) { - out.push_str("typedef struct {\n"); - out.push_str(" void* iter;\n"); - out.push_str(&format!("}} {prefix}_napi_iter_state;\n\n")); -} - -/// Read the iterator state cell back out of the external in `args[0]`. -fn emit_iter_state_read(out: &mut String, prefix: &str) { - out.push_str(" size_t argc = 1;\n"); - out.push_str(" napi_value args[1];\n"); - out.push_str(" napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n"); - out.push_str(" void* iter_data = NULL;\n"); - out.push_str(" napi_get_value_external(env, args[0], &iter_data);\n"); - out.push_str(&format!( - " {prefix}_napi_iter_state* state = ({prefix}_napi_iter_state*)iter_data;\n" - )); -} - -/// Emit one iterator-returning callable's lazy machinery: the external's -/// finalizer (the safety net for abandoned iterators), the per-step `next` -/// entry point, and the explicit `destroy` entry point the JS wrapper's -/// `return()` calls on early exit. -/// -/// `next` issues exactly one native pull. When the producer reports done (or -/// faults), the native handle is destroyed eagerly and the cell nulled; a -/// per-step fault then throws the code-carrying error, which the JS wrapper -/// maps per the callable's error strategy. A produced element is converted -/// and released per its element plan: strings are freed with -/// `{prefix}_free_string` after the JS string is created, byte or buffered -/// elements are copied into a JS `Buffer` and released with -/// `{prefix}_free_bytes` (the JS wrapper decodes buffered elements), and an -/// object element's strong reference is surfaced as a handle the JS wrapper -/// adopts. -fn render_iterator_napi_fns( - out: &mut String, - f: &FnBinding, - ib: &IteratorBinding, - module: &str, - prefix: &str, -) { - let c_name = &f.c_base; - let tag = &ib.iter_tag; - let next_sym = &ib.next.symbol; - let destroy_sym = &ib.destroy_symbol; - let ef = elem_free(&ib.elem); - - // Finalizer: reclaim abandoned iterators when the external is collected. - out.push_str(&format!( - "static void {c_name}_napi_iter_finalize(napi_env env, void* data, void* hint) {{\n" - )); - out.push_str(" (void)env;\n"); - out.push_str(" (void)hint;\n"); - out.push_str(&format!( - " {prefix}_napi_iter_state* state = ({prefix}_napi_iter_state*)data;\n" - )); - out.push_str(" if (state->iter != NULL) {\n"); - out.push_str(&format!(" {destroy_sym}(({tag}*)state->iter);\n")); - out.push_str(" state->iter = NULL;\n"); - out.push_str(" }\n"); - out.push_str(" free(state);\n"); - out.push_str("}\n\n"); - - // One pull per call; `undefined` signals exhaustion to the JS wrapper. - out.push_str(&format!( - "static napi_value Napi_{next_sym}(napi_env env, napi_callback_info info) {{\n" - )); - emit_iter_state_read(out, prefix); - out.push_str(" napi_value ret;\n"); - out.push_str(" if (state == NULL || state->iter == NULL) {\n"); - out.push_str(" napi_get_undefined(env, &ret);\n"); - out.push_str(" return ret;\n"); - out.push_str(" }\n"); - let et = iterator_item_ctype(&ib.elem, module).render_c(prefix); - out.push_str(&format!(" {et} iter_item;\n")); - if ef == Free::Bytes { - out.push_str(" size_t iter_item_len = 0;\n"); - } - out.push_str(&format!(" {prefix}_error iter_err = {{0}};\n")); - let next_args = if ef == Free::Bytes { - format!("({tag}*)state->iter, &iter_item, &iter_item_len, &iter_err") - } else { - format!("({tag}*)state->iter, &iter_item, &iter_err") - }; - out.push_str(&format!(" if (!{next_sym}({next_args})) {{\n")); - out.push_str(&format!(" {destroy_sym}(({tag}*)state->iter);\n")); - out.push_str(" state->iter = NULL;\n"); - out.push_str(" if (iter_err.code != 0) {\n"); - out.push_str(&format!( - " napi_throw(env, {prefix}_napi_error_value(env, iter_err.code, iter_err.message, iter_err.payload_ptr, iter_err.payload_len));\n" - )); - out.push_str(&format!(" {prefix}_error_clear(&iter_err);\n")); - out.push_str(" return NULL;\n"); - out.push_str(" }\n"); - out.push_str(" napi_get_undefined(env, &ret);\n"); - out.push_str(" return ret;\n"); - out.push_str(" }\n"); - match ef { - Free::String => { - out.push_str( - " napi_create_string_utf8(env, iter_item ? iter_item : \"\", NAPI_AUTO_LENGTH, &ret);\n", - ); - out.push_str(&format!(" {prefix}_free_string((char*)iter_item);\n")); - } - Free::Bytes => { - out.push_str(" napi_create_buffer_copy(env, iter_item_len, iter_item, NULL, &ret);\n"); - out.push_str(&format!( - " {prefix}_free_bytes((uint8_t*)iter_item, iter_item_len);\n" - )); - } - Free::None => { - if ib.elem.interface_name().is_some() { - // One strong reference per element; the JS wrapper adopts it. - out.push_str(" weaveffi_napi_make_handle(env, iter_item, &ret);\n"); - } else { - out.push_str(&format!( - " {}\n", - napi_create_leaf(&ib.elem, "iter_item", "ret") - )); - } - } - } - out.push_str(" return ret;\n"); - out.push_str("}\n\n"); - - // Explicit destroy, guarded so destroy-after-exhaustion (or a double - // `return()`) is a no-op rather than a double free. - out.push_str(&format!( - "static napi_value Napi_{destroy_sym}(napi_env env, napi_callback_info info) {{\n" - )); - emit_iter_state_read(out, prefix); - out.push_str(" if (state != NULL && state->iter != NULL) {\n"); - out.push_str(&format!(" {destroy_sym}(({tag}*)state->iter);\n")); - out.push_str(" state->iter = NULL;\n"); - out.push_str(" }\n"); - out.push_str(" napi_value ret;\n"); - out.push_str(" napi_get_undefined(env, &ret);\n"); - out.push_str(" return ret;\n"); - out.push_str("}\n\n"); -} - -/// Emit one callable's `Napi_*` entry point (plus its async or iterator -/// machinery when needed) and register its JS export(s). `self_tag` is the -/// interface `c_tag` for an instance method, whose wrapped handle arrives as -/// `args[0]`. An iterator-returning callable additionally exports its -/// per-iterator `next`/`destroy` entry points under `{js_name}_iterNext` and -/// `{js_name}_iterDestroy`, which the JS wrapper drives lazily. -fn render_callable_napi( - out: &mut String, - all_exports: &mut Vec<(String, String)>, - f: &FnBinding, - js_name: String, - module: &str, - prefix: &str, - self_tag: Option<&str>, -) { - let c_name = &f.c_base; - let napi_name = format!("Napi_{c_name}"); - - if f.is_async { - render_async_machinery(out, f, c_name, module, prefix); - } - if let CallShape::Iterator(ib) = &f.shape { - render_iterator_napi_fns(out, f, ib, module, prefix); - all_exports.push(( - format!("{js_name}_iterNext"), - format!("Napi_{}", ib.next.symbol), - )); - all_exports.push(( - format!("{js_name}_iterDestroy"), - format!("Napi_{}", ib.destroy_symbol), - )); - } - all_exports.push((js_name, napi_name.clone())); - - out.push_str(&format!( - "static napi_value {napi_name}(napi_env env, napi_callback_info info) {{\n" - )); - if f.is_async { - render_async_napi_body(out, f, module, prefix, self_tag); - } else { - render_napi_body(out, f, module, prefix, self_tag); - } - out.push_str("}\n\n"); -} - -/// Render the complete native addon source (`weaveffi_addon.c`). -pub(crate) fn render_addon_c( - model: &BindingModel, - strip_module_prefix: bool, - input_basename: &str, -) -> String { - let prefix = model.prefix.as_str(); - let has_callbacks = model.has_callback_interfaces(); - let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); - // The bigint and threadsafe-function APIs need Node-API version 6 or - // later; pin the version the addon is written against unless the build - // already chose one. - out.push_str("#ifndef NAPI_VERSION\n#define NAPI_VERSION 8\n#endif\n"); - out.push_str(&format!( - "#include \n#include \"{prefix}.h\"\n#include \n#include \n#include \n#include \n#include \n" - )); - if has_callbacks { - out.push_str("#include \n"); - } - out.push('\n'); - - let mut all_exports: Vec<(String, String)> = Vec::new(); - - render_value_helpers_c(&mut out); - - // Every error path (sync throws, iterator faults, async rejections) - // funnels through one code-and-payload-carrying error constructor. - let has_error_paths = model - .modules - .iter() - .any(|m| !m.functions.is_empty() || !m.interfaces.is_empty()); - if has_error_paths { - render_error_value_helper_c(&mut out, prefix); - } - - if model.has_iterators() { - render_iter_state_c(&mut out, prefix); - } - - if has_callbacks { - render_callback_runtime_c(&mut out, prefix); - } - - for m in &model.modules { - // Callback interfaces get their frames, trampolines, dispatcher, and - // static vtable before any callable that may take one. - for cb in &m.callback_interfaces { - render_callback_interface_c(&mut out, m, cb, prefix, &model.prefix); - } - // Records and rich enums are value types crossing the ABI serialized - // in value buffers, so they need no native helpers here; the JS - // loader packs and unpacks them. Interfaces get one native entry - // point per member (constructors and statics marshal like free - // functions; methods additionally read the wrapped handle from the - // leading argument) plus the `clone` and `destroy` entry points the - // JS class's codec and disposal paths call. - for i in &m.interfaces { - for f in i.constructors.iter().chain(i.statics.iter()) { - render_callable_napi( - &mut out, - &mut all_exports, - f, - wrapper_name( - &m.path, - &iface_member_base(&i.name, &f.name), - strip_module_prefix, - ), - &m.path, - prefix, - None, - ); - } - for f in &i.methods { - render_callable_napi( - &mut out, - &mut all_exports, - f, - wrapper_name( - &m.path, - &iface_member_base(&i.name, &f.name), - strip_module_prefix, - ), - &m.path, - prefix, - Some(&i.c_tag), - ); - } - render_interface_lifecycle_napi(&mut out, i); - all_exports.push(( - wrapper_name( - &m.path, - &iface_lifecycle_base(&i.name, "destroy"), - strip_module_prefix, - ), - format!("Napi_{}", i.destroy_symbol), - )); - all_exports.push(( - wrapper_name( - &m.path, - &iface_lifecycle_base(&i.name, "clone"), - strip_module_prefix, - ), - format!("Napi_{}", i.clone_symbol), - )); - } - for f in &m.functions { - render_callable_napi( - &mut out, - &mut all_exports, - f, - js_fn_name(&m.path, &f.name, strip_module_prefix), - &m.path, - prefix, - None, - ); - } - } - - 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 has_callbacks { - // Trampolines compare against this to decide whether they may call - // into JS directly or must hop through the threadsafe function. - out.push_str(&format!(" {prefix}_napi_js_thread = uv_thread_self();\n")); - } - if !all_exports.is_empty() { - out.push_str(" napi_property_descriptor props[] = {\n"); - for (js_name, napi_fn) in &all_exports { - out.push_str(&format!( - " {{ \"{js_name}\", NULL, {napi_fn}, NULL, NULL, NULL, napi_default, NULL }},\n" - )); - } - out.push_str(" };\n"); - out.push_str(&format!( - " napi_define_properties(env, exports, {}, props);\n", - all_exports.len() - )); - } - out.push_str(" return exports;\n"); - out.push_str("}\n\n"); - out.push_str("NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)\n\n"); - out.push_str(&render_trailer( - CommentStyle::DoubleSlash, - "weaveffi_addon.c", - )); - out -} - -/// Read `args[0]` as an object handle and bind it to a typed `self` pointer. -/// Used by the interface lifecycle entry points. -fn emit_self_handle_read(out: &mut String, c_tag: &str) { - out.push_str(" size_t argc = 1;\n"); - out.push_str(" napi_value args[1];\n"); - out.push_str(" napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n"); - out.push_str(" void* self_raw = NULL;\n"); - out.push_str(" if (weaveffi_napi_get_handle(env, args[0], &self_raw) != napi_ok || self_raw == NULL) {\n"); - out.push_str(" return NULL;\n"); - out.push_str(" }\n"); - out.push_str(&format!(" {c_tag}* self = ({c_tag}*)self_raw;\n")); -} - -/// The `Napi_*` lifecycle entry points for one interface. `destroy` reads -/// the wrapped handle from `args[0]` and releases the strong reference it -/// carries; the JS class's `close()` and its `FinalizationRegistry` backstop -/// call it exactly once per wrapper. `clone` produces a second strong -/// reference (as a fresh handle) for the value-buffer codec to write as an -/// object token. -fn render_interface_lifecycle_napi(out: &mut String, i: &InterfaceBinding) { - out.push_str(&format!( - "static napi_value Napi_{}(napi_env env, napi_callback_info info) {{\n", - i.destroy_symbol - )); - emit_self_handle_read(out, &i.c_tag); - out.push_str(&format!(" {}(self);\n", i.destroy_symbol)); - out.push_str(" napi_value ret;\n"); - out.push_str(" napi_get_undefined(env, &ret);\n"); - out.push_str(" return ret;\n}\n\n"); - - out.push_str(&format!( - "static napi_value Napi_{}(napi_env env, napi_callback_info info) {{\n", - i.clone_symbol - )); - emit_self_handle_read(out, &i.c_tag); - out.push_str(&format!( - " {}* cloned = {}(self);\n", - i.c_tag, i.clone_symbol - )); - out.push_str(" napi_value ret;\n"); - out.push_str(" weaveffi_napi_make_handle(env, cloned, &ret);\n"); - out.push_str(" return ret;\n}\n\n"); -} - -/// Emit the callback-interface runtime shared by every interface: the JS -/// thread identity captured at addon init, the handle-table entry -/// (`{prefix}_napi_cb_ctx`: the `napi_ref` holding the consumer's adapter -/// object plus the threadsafe function used to reach the JS thread), the -/// cross-thread request cell, and the helpers that register an entry, -/// release it, hop a blocking request to the JS thread, and report a JS -/// exception through `out_err` with [`FOREIGN_ERROR_CODE`]. -/// -/// A trampoline that finds itself on the JS thread calls the adapter -/// directly. From any other thread it queues a request through the -/// threadsafe function and waits on a condition variable until the JS thread -/// has run the call and filled in the result. `free` follows the same -/// discipline: the `napi_ref` can only be deleted on the JS thread, so an -/// off-thread `free` queues a release request instead. -fn render_callback_runtime_c(out: &mut String, prefix: &str) { - let mut w = CodeWriter::two_space(); - w.line(format!("static uv_thread_t {prefix}_napi_js_thread;")); - w.blank(); - w.block( - format!("static inline bool {prefix}_napi_on_js_thread(void) {{"), - "}", - |w| { - w.line("uv_thread_t self = uv_thread_self();"); - w.line(format!( - "return uv_thread_equal(&self, &{prefix}_napi_js_thread) != 0;" - )); - }, - ); - w.blank(); - w.line("// One handle-table entry per registered callback-interface implementation;"); - w.line("// its address is the `ctx` the producer passes back to every method."); - w.block( - "typedef struct {", - format!("}} {prefix}_napi_cb_ctx;"), - |w| { - w.line("napi_env env;"); - w.line("napi_ref ref;"); - w.line("napi_threadsafe_function tsfn;"); - }, - ); - w.blank(); - w.line("// Every method frame starts with this header so the dispatcher can reach"); - w.line("// `out_err` without knowing the method."); - w.block( - "typedef struct {", - format!("}} {prefix}_napi_cb_frame_hdr;"), - |w| { - w.line(format!("{prefix}_error* out_err;")); - }, - ); - w.blank(); - w.line("// A request hopped from a producer thread to the JS thread. `method` is the"); - w.line("// vtable index to run, or -1 to release the context."); - w.block( - "typedef struct {", - format!("}} {prefix}_napi_cb_req;"), - |w| { - w.line(format!("{prefix}_napi_cb_ctx* ctx;")); - w.line("int method;"); - w.line("void* frame;"); - w.line("uv_mutex_t mu;"); - w.line("uv_cond_t cv;"); - w.line("bool done;"); - }, - ); - w.blank(); - w.block( - format!("static {prefix}_napi_cb_ctx* {prefix}_napi_cb_register(napi_env env, napi_value target, const char* name, napi_threadsafe_function_call_js dispatch) {{"), - "}", - |w| { - w.line(format!( - "{prefix}_napi_cb_ctx* ctx = ({prefix}_napi_cb_ctx*)calloc(1, sizeof({prefix}_napi_cb_ctx));" - )); - w.line("ctx->env = env;"); - w.line("napi_create_reference(env, target, 1, &ctx->ref);"); - w.line("napi_value resource_name;"); - w.line("napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource_name);"); - w.line("napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, dispatch, &ctx->tsfn);"); - w.line("// A live implementation must not pin the event loop by itself."); - w.line("napi_unref_threadsafe_function(env, ctx->tsfn);"); - w.line("return ctx;"); - }, - ); - w.blank(); - w.line("// Release one entry on the JS thread (env is NULL only during teardown, when"); - w.line("// the reference is already gone)."); - w.block( - format!("static void {prefix}_napi_cb_release(napi_env env, {prefix}_napi_cb_ctx* ctx) {{"), - "}", - |w| { - w.line("if (env != NULL) {"); - w.line(" napi_delete_reference(env, ctx->ref);"); - w.line("}"); - w.line("napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);"); - w.line("free(ctx);"); - }, - ); - w.blank(); - w.line("// The vtable's `free` entry: the producer is done with `ctx`."); - w.block( - format!("static void {prefix}_napi_cb_free(void* ctx_) {{"), - "}", - |w| { - w.line(format!( - "{prefix}_napi_cb_ctx* ctx = ({prefix}_napi_cb_ctx*)ctx_;" - )); - w.line("if (ctx == NULL) {"); - w.line(" return;"); - w.line("}"); - w.line(format!("if ({prefix}_napi_on_js_thread()) {{")); - w.line(format!(" {prefix}_napi_cb_release(ctx->env, ctx);")); - w.line(" return;"); - w.line("}"); - w.line(format!( - "{prefix}_napi_cb_req* req = ({prefix}_napi_cb_req*)calloc(1, sizeof({prefix}_napi_cb_req));" - )); - w.line("req->ctx = ctx;"); - w.line("req->method = -1;"); - w.line("if (napi_call_threadsafe_function(ctx->tsfn, req, napi_tsfn_nonblocking) != napi_ok) {"); - w.line(" free(req);"); - w.line("}"); - }, - ); - w.blank(); - w.line("// Run `req` on the JS thread and block until it has completed."); - w.block( - format!("static void {prefix}_napi_cb_hop({prefix}_napi_cb_req* req) {{"), - "}", - |w| { - w.line("uv_mutex_init(&req->mu);"); - w.line("uv_cond_init(&req->cv);"); - w.line("req->done = false;"); - w.line("if (napi_call_threadsafe_function(req->ctx->tsfn, req, napi_tsfn_blocking) == napi_ok) {"); - w.line(" uv_mutex_lock(&req->mu);"); - w.line(" while (!req->done) {"); - w.line(" uv_cond_wait(&req->cv, &req->mu);"); - w.line(" }"); - w.line(" uv_mutex_unlock(&req->mu);"); - w.line("} else {"); - w.line(format!( - " {prefix}_error_set((({prefix}_napi_cb_frame_hdr*)req->frame)->out_err, {FOREIGN_ERROR_CODE}, \"callback interface implementation is no longer reachable\");" - )); - w.line("}"); - w.line("uv_cond_destroy(&req->cv);"); - w.line("uv_mutex_destroy(&req->mu);"); - }, - ); - w.blank(); - w.block( - format!("static void {prefix}_napi_cb_finish({prefix}_napi_cb_req* req) {{"), - "}", - |w| { - w.line("uv_mutex_lock(&req->mu);"); - w.line("req->done = true;"); - w.line("uv_cond_signal(&req->cv);"); - w.line("uv_mutex_unlock(&req->mu);"); - }, - ); - w.blank(); - w.line("// Report a JS exception raised by a callback implementation (or a return"); - w.line("// value of the wrong type) through `out_err` as a foreign error, clearing"); - w.line("// the pending exception so nothing unwinds through the C frame."); - w.block( - format!("static void {prefix}_napi_cb_report(napi_env env, {prefix}_error* out_err, const char* fallback) {{"), - "}", - |w| { - w.line("char msg[512];"); - w.line("msg[0] = 0;"); - w.line("bool pending = false;"); - w.line("napi_is_exception_pending(env, &pending);"); - w.line("if (pending) {"); - w.line(" napi_value exc;"); - w.line(" napi_get_and_clear_last_exception(env, &exc);"); - w.line(" napi_valuetype t;"); - w.line(" napi_typeof(env, exc, &t);"); - w.line(" napi_value text = exc;"); - w.line(" if (t == napi_object) {"); - w.line(" napi_get_named_property(env, exc, \"message\", &text);"); - w.line(" }"); - w.line(" napi_value str;"); - w.line(" if (napi_coerce_to_string(env, text, &str) == napi_ok) {"); - w.line(" napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL);"); - w.line(" }"); - w.line(" napi_is_exception_pending(env, &pending);"); - w.line(" if (pending) {"); - w.line(" napi_get_and_clear_last_exception(env, &exc);"); - w.line(" }"); - w.line("}"); - w.line(format!( - "{prefix}_error_set(out_err, {FOREIGN_ERROR_CODE}, msg[0] ? msg : fallback);" - )); - }, - ); - w.blank(); - out.push_str(&w.finish()); -} - -/// The frame struct name of one callback method. -fn cb_frame_name(c_tag: &str, m: &CallbackMethodBinding) -> String { - format!("{c_tag}_{}_frame", m.name) -} - -/// Emit one callback interface's native side: a frame struct per method (the -/// C arguments as the producer passed them plus the result slot), the -/// JS-thread invoker per method (convert arguments, call the adapter's -/// method by its IDL name, convert the return or report the exception), the -/// trampoline per method that the vtable points at (call the invoker -/// directly on the JS thread, or hop and wait), the dispatcher the -/// interface's threadsafe functions run, and the one process-wide static -/// vtable. -/// -/// Inside an invoker, strings, bytes, and buffers are borrowed (copied into -/// JS values, never freed), object arguments carry one strong reference each -/// (surfaced as a handle the JS adapter adopts), and 64-bit integers cross -/// as `bigint`s. -fn render_callback_interface_c( - out: &mut String, - m: &ModuleBinding, - cb: &CallbackInterfaceBinding, - prefix: &str, - model_prefix: &str, -) { - let c_tag = &cb.c_tag; - let protocol = cb.protocol(&m.path, model_prefix); - - for (idx, method) in cb.methods.iter().enumerate() { - let frame = cb_frame_name(c_tag, method); - let invoke = format!("{c_tag}_{}_invoke", method.name); - let tramp = format!("{c_tag}_{}_tramp", method.name); - let ret_c = method.abi_ret.render_c(prefix); - let is_void = method.ret.is_none(); - let slots = &method.abi_params[1..method.abi_params.len() - 1]; - let out_err_name = &method - .abi_params - .last() - .expect("callback method has an out_err slot") - .name; - - // -- frame -- - out.push_str("typedef struct {\n"); - out.push_str(&format!(" {prefix}_napi_cb_frame_hdr hdr;\n")); - for slot in slots { - out.push_str(&format!( - " {} {};\n", - slot.ty.render_c(prefix), - slot.name - )); - } - if !is_void { - out.push_str(&format!(" {ret_c} result;\n")); - } - out.push_str(&format!("}} {frame};\n\n")); - - // -- JS-thread invoker -- - out.push_str(&format!( - "static void {invoke}(napi_env env, {prefix}_napi_cb_ctx* ctx, {frame}* f) {{\n" - )); - out.push_str(" napi_handle_scope scope;\n"); - out.push_str(" napi_open_handle_scope(env, &scope);\n"); - out.push_str(" napi_value target;\n"); - out.push_str(" napi_get_reference_value(env, ctx->ref, &target);\n"); - out.push_str(" napi_value fn;\n"); - out.push_str(&format!( - " napi_get_named_property(env, target, \"{}\", &fn);\n", - method.name - )); - let argc = method.params.len(); - if argc > 0 { - out.push_str(&format!(" napi_value argv[{argc}];\n")); - } - for (i, (p, pass)) in method - .params - .iter() - .zip(&protocol.method_args[idx]) - .enumerate() - { - emit_cb_arg_to_napi(out, p, pass, i); - } - out.push_str(" napi_value result;\n"); - out.push_str(" napi_valuetype fn_type;\n"); - out.push_str(" napi_typeof(env, fn, &fn_type);\n"); - let argv = if argc > 0 { "argv" } else { "NULL" }; - out.push_str(" if (fn_type != napi_function) {\n"); - out.push_str(&format!( - " {prefix}_napi_cb_report(env, f->hdr.out_err, \"{} implementation has no {} method\");\n", - cb.name, method.name - )); - out.push_str(&format!( - " }} else if (napi_call_function(env, target, fn, {argc}, {argv}, &result) != napi_ok) {{\n" - )); - out.push_str(&format!( - " {prefix}_napi_cb_report(env, f->hdr.out_err, \"{}.{} threw\");\n", - cb.name, method.name - )); - if let Some(ret) = &method.ret { - out.push_str(" } else {\n"); - out.push_str(" napi_status rs;\n"); - emit_leaf_read(out, " ", ret, "result", "f->result", "rs"); - out.push_str(" if (rs != napi_ok) {\n"); - out.push_str(&format!( - " {prefix}_napi_cb_report(env, f->hdr.out_err, \"{}.{} returned a value of the wrong type\");\n", - cb.name, method.name - )); - out.push_str(" }\n"); - } - out.push_str(" }\n"); - out.push_str(" napi_close_handle_scope(env, scope);\n"); - out.push_str("}\n\n"); - - // -- trampoline (any producer thread) -- - let decls: Vec = method - .abi_params - .iter() - .map(|slot| format!("{} {}", slot.ty.render_c(prefix), slot.name)) - .collect(); - out.push_str(&format!( - "static {ret_c} {tramp}({}) {{\n", - decls.join(", ") - )); - out.push_str(&format!(" {frame} f;\n")); - out.push_str(" memset(&f, 0, sizeof f);\n"); - out.push_str(&format!(" f.hdr.out_err = {out_err_name};\n")); - for slot in slots { - out.push_str(&format!(" f.{0} = {0};\n", slot.name)); - } - out.push_str(&format!( - " {prefix}_napi_cb_ctx* c = ({prefix}_napi_cb_ctx*){};\n", - method.abi_params[0].name - )); - out.push_str(&format!(" if ({prefix}_napi_on_js_thread()) {{\n")); - out.push_str(&format!(" {invoke}(c->env, c, &f);\n")); - out.push_str(" } else {\n"); - out.push_str(&format!(" {prefix}_napi_cb_req req;\n")); - out.push_str(" req.ctx = c;\n"); - out.push_str(&format!(" req.method = {idx};\n")); - out.push_str(" req.frame = &f;\n"); - out.push_str(&format!(" {prefix}_napi_cb_hop(&req);\n")); - out.push_str(" }\n"); - if !is_void { - out.push_str(" return f.result;\n"); - } - out.push_str("}\n\n"); - } - - // -- dispatcher: runs queued requests on the JS thread -- - out.push_str(&format!( - "static void {c_tag}_napi_dispatch(napi_env env, napi_value js_cb, void* context, void* data) {{\n" - )); - out.push_str(" (void)js_cb;\n"); - out.push_str(" (void)context;\n"); - out.push_str(&format!( - " {prefix}_napi_cb_req* req = ({prefix}_napi_cb_req*)data;\n" - )); - out.push_str(" if (req->method < 0) {\n"); - out.push_str(&format!(" {prefix}_napi_cb_release(env, req->ctx);\n")); - out.push_str(" free(req);\n"); - out.push_str(" return;\n"); - out.push_str(" }\n"); - out.push_str(" if (env == NULL) {\n"); - out.push_str(&format!( - " {prefix}_error_set((({prefix}_napi_cb_frame_hdr*)req->frame)->out_err, {FOREIGN_ERROR_CODE}, \"Node environment is shutting down\");\n" - )); - out.push_str(" } else {\n"); - out.push_str(" switch (req->method) {\n"); - for (idx, method) in cb.methods.iter().enumerate() { - let frame = cb_frame_name(c_tag, method); - out.push_str(&format!( - " case {idx}: {c_tag}_{}_invoke(env, req->ctx, ({frame}*)req->frame); break;\n", - method.name - )); - } - out.push_str(" default: break;\n"); - out.push_str(" }\n"); - out.push_str(" }\n"); - out.push_str(&format!(" {prefix}_napi_cb_finish(req);\n")); - out.push_str("}\n\n"); - - // -- the one static vtable -- - let entries: Vec = cb - .methods - .iter() - .map(|m| format!("{c_tag}_{}_tramp", m.name)) - .chain(std::iter::once(format!("{prefix}_napi_cb_free"))) - .collect(); - out.push_str(&format!( - "static const {} {c_tag}_napi_vtable = {{ {} }};\n\n", - cb.vtable_tag, - entries.join(", ") - )); -} - -/// Emit the conversion of one callback-method argument from its frame slots -/// to `argv[idx]`, per its receiving plan. -fn emit_cb_arg_to_napi(out: &mut String, p: &ParamBinding, pass: &RetPass, idx: usize) { - let n0 = &p.abi[0].name; - let target = format!("argv[{idx}]"); - match pass { - RetPass::Void => unreachable!("callback parameters are never void"), - RetPass::Direct => { - let leaf = napi_create_leaf(&p.ty, &format!("f->{n0}"), &target); - out.push_str(&format!(" {leaf}\n")); - } - RetPass::String => out.push_str(&format!( - " napi_create_string_utf8(env, f->{n0} ? f->{n0} : \"\", NAPI_AUTO_LENGTH, &{target});\n" - )), - RetPass::Bytes | RetPass::Buffer => { - let n1 = &p.abi[1].name; - out.push_str(&format!( - " napi_create_buffer_copy(env, f->{n1}, f->{n0} ? (const void*)f->{n0} : (const void*)\"\", NULL, &{target});\n" - )); - } - RetPass::Object { .. } => out.push_str(&format!( - " weaveffi_napi_make_handle(env, f->{n0}, &{target});\n" - )), - } -} - -/// The classified shape of an async function's result, driving what the -/// completion callback copies and what the settle marshaller creates. -enum AsyncResultShape { - /// No result: the promise resolves `undefined`. - None, - /// A by-value scalar, bool, or C-style enum. - Value, - /// An owned `const char*` string (nullable). - Str, - /// A `ptr` + `len` pair: an owned `bytes` result (slot named `result`). - Bytes, - /// A borrowed value-buffer pair (slots `result_ptr` + `result_len`); the - /// callback must copy it before returning, and the JS wrapper decodes it. - Buffered, - /// An owned object reference the JS wrapper adopts (`Interface` or - /// `Interface?`). - Object, -} - -/// Classify an async result type into its marshalling shape via the shared -/// receiving plan. -fn async_result_shape(ret: Option<&Ty>, module: &str, prefix: &str) -> AsyncResultShape { - match plan::ret_pass(ret, module, prefix) { - RetPass::Void => AsyncResultShape::None, - RetPass::Direct => AsyncResultShape::Value, - RetPass::String => AsyncResultShape::Str, - RetPass::Bytes => AsyncResultShape::Bytes, - RetPass::Buffer => AsyncResultShape::Buffered, - RetPass::Object { .. } => AsyncResultShape::Object, - } -} - -/// 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<&Ty>, module: &str, prefix: &str) -> String { - match ret { - None => String::new(), - Some(ty) => abi::callback_result_params(ty, module) - .iter() - .map(|p| format!(", {} {}", p.ty.render_c(prefix), p.name)) - .collect(), - } -} - -/// Emit the per-async-function machinery: a context struct carrying the -/// promise + threadsafe function + deep-copied results, the producer-thread -/// completion callback (which only copies and queues), and the JS-thread -/// marshaller (which settles the promise). -/// -/// The completion callback may fire on any thread, so it must never touch -/// `napi_env`; the ref'd threadsafe function also keeps the event loop alive -/// until the promise settles. Owned results (strings, bytes, and buffered -/// values) are deep-copied inside the callback and released with the runtime -/// free symbols; owned object results are adopted (the pointer stays valid -/// across the thread hop). The error's message and payload are copied for -/// the same reason. -fn render_async_machinery( - out: &mut String, - f: &FnBinding, - c_name: &str, - module: &str, - prefix: &str, -) { - let actx = format!("{c_name}_napi_actx"); - let cb_name = format!("{c_name}_napi_cb"); - let calljs = format!("{c_name}_napi_settle"); - let cb_result = async_cb_result_params_node(f.ret.as_ref(), module, prefix); - let shape = async_result_shape(f.ret.as_ref(), module, prefix); - - // -- context struct -- - out.push_str("typedef struct {\n"); - out.push_str(" napi_deferred deferred;\n"); - out.push_str(" napi_threadsafe_function tsfn;\n"); - out.push_str(" int32_t err_code;\n"); - out.push_str(" char* err_msg;\n"); - out.push_str(" uint8_t* err_payload;\n"); - out.push_str(" size_t err_payload_len;\n"); - match &shape { - AsyncResultShape::None => {} - AsyncResultShape::Value => { - let ct = c_ret_type_str( - f.ret.as_ref().expect("value shape has a type"), - module, - prefix, - ); - out.push_str(&format!(" {ct} result;\n")); - } - AsyncResultShape::Str => { - out.push_str(" char* result;\n"); - out.push_str(" int result_null;\n"); - } - AsyncResultShape::Bytes | AsyncResultShape::Buffered => { - out.push_str(" uint8_t* result;\n"); - out.push_str(" size_t result_len;\n"); - } - AsyncResultShape::Object => { - out.push_str(" void* result;\n"); - } - } - out.push_str(&format!("}} {actx};\n\n")); - - // -- producer-thread completion callback: deep-copy + queue -- - out.push_str(&format!( - "static void {cb_name}(void* context, {prefix}_error* err{cb_result}) {{\n" - )); - out.push_str(&format!(" {actx}* ctx = ({actx}*)context;\n")); - out.push_str(" if (err != NULL && err->code != 0) {\n"); - out.push_str(" ctx->err_code = err->code;\n"); - out.push_str( - " ctx->err_msg = err->message ? strdup(err->message) : strdup(\"unknown error\");\n", - ); - out.push_str(" ctx->err_payload_len = err->payload_len;\n"); - out.push_str( - " 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); }\n", - ); - out.push_str(" } else {\n"); - match &shape { - AsyncResultShape::None => {} - AsyncResultShape::Value => { - out.push_str(" ctx->result = result;\n"); - } - // The string result is owned: copy, then release the producer - // allocation. - AsyncResultShape::Str => { - out.push_str(" ctx->result_null = result == NULL;\n"); - out.push_str(" ctx->result = result ? strdup(result) : NULL;\n"); - out.push_str(&format!(" {prefix}_free_string(result);\n")); - } - AsyncResultShape::Bytes => { - out.push_str(" ctx->result_len = result_len;\n"); - out.push_str( - " if (result != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result, result_len); }\n", - ); - out.push_str(&format!( - " {prefix}_free_bytes((uint8_t*)result, result_len);\n" - )); - } - // The value buffer is owned: copy, then release the producer - // allocation; the JS wrapper decodes the copy after the promise - // resolves. - AsyncResultShape::Buffered => { - out.push_str(" ctx->result_len = result_len;\n"); - out.push_str( - " if (result_ptr != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result_ptr, result_len); }\n", - ); - out.push_str(&format!( - " {prefix}_free_bytes((uint8_t*)result_ptr, result_len);\n" - )); - } - // The strong reference is adopted by the receiver, so the pointer - // stays valid across the thread hop. - AsyncResultShape::Object => { - out.push_str(" ctx->result = (void*)result;\n"); - } - } - out.push_str(" }\n"); - // The heap-boxed error transfers ownership too; a null or zero-code - // error is a safe no-op to free. - out.push_str(&format!(" {prefix}_error_free(err);\n")); - out.push_str(" napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking);\n"); - out.push_str("}\n\n"); - - // -- JS-thread marshaller: settle the promise, free, release -- - out.push_str(&format!( - "static void {calljs}(napi_env env, napi_value js_cb, void* context, void* data) {{\n" - )); - out.push_str(" (void)js_cb;\n"); - out.push_str(" (void)context;\n"); - out.push_str(&format!(" {actx}* ctx = ({actx}*)data;\n")); - out.push_str(" if (env != NULL) {\n"); - out.push_str(" if (ctx->err_code != 0) {\n"); - out.push_str(&format!( - " napi_value err_obj = {prefix}_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len);\n" - )); - out.push_str(" napi_reject_deferred(env, ctx->deferred, err_obj);\n"); - out.push_str(" } else {\n"); - out.push_str(" napi_value val;\n"); - match &shape { - AsyncResultShape::None => out.push_str(" napi_get_undefined(env, &val);\n"), - AsyncResultShape::Value => { - let ty = f.ret.as_ref().expect("value shape has a type"); - out.push_str(&format!( - " {}\n", - napi_create_leaf(ty, "ctx->result", "val") - )); - } - AsyncResultShape::Str => { - out.push_str( - " if (ctx->result_null) napi_get_null(env, &val); else napi_create_string_utf8(env, ctx->result ? ctx->result : \"\", NAPI_AUTO_LENGTH, &val);\n", - ); - } - AsyncResultShape::Bytes | AsyncResultShape::Buffered => { - out.push_str( - " napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)\"\", NULL, &val);\n", - ); - } - // A nullable interface result resolves `null` for the absent case; - // otherwise the strong reference is surfaced as the handle the JS - // class adopts. - AsyncResultShape::Object => { - out.push_str(" weaveffi_napi_make_handle(env, ctx->result, &val);\n"); - } - } - out.push_str(" napi_resolve_deferred(env, ctx->deferred, val);\n"); - out.push_str(" }\n"); - out.push_str(" }\n"); - out.push_str(" free(ctx->err_msg);\n"); - out.push_str(" free(ctx->err_payload);\n"); - match &shape { - AsyncResultShape::Str | AsyncResultShape::Bytes | AsyncResultShape::Buffered => { - out.push_str(" free(ctx->result);\n"); - } - _ => {} - } - out.push_str(" napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);\n"); - out.push_str(" free(ctx);\n"); - out.push_str("}\n\n"); -} - -/// The marshalled arguments of one entry point: the C argument expressions -/// in slot order, the cleanups to run after the call, and whether any read -/// can fail (in which case the entry point bails before calling the -/// producer). -struct MarshalledArgs { - c_args: Vec, - cleanups: Vec, - /// Extra releases that only run when marshalling bails before the call - /// (a registered callback context the producer never received). - fail_cleanups: Vec, - checked: bool, -} - -/// Read `argc`/`args` for a callable with `n` incoming JS arguments -/// (including the leading handle of an instance method), then marshal each -/// argument into its C slots. Instance methods carry the implicit `self` -/// slot in their [`AbiFn`](weaveffi_core::model::AbiFn) signatures; the JS -/// class passes its own handle there. -fn emit_args( - out: &mut String, - f: &FnBinding, - module: &str, - prefix: &str, - self_tag: Option<&str>, -) -> MarshalledArgs { - let offset = usize::from(self_tag.is_some()); - let n = f.params.len() + offset; - if n > 0 { - out.push_str(&format!(" size_t argc = {n};\n")); - out.push_str(&format!(" napi_value args[{n}];\n")); - out.push_str(" napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n"); - } else { - out.push_str(" size_t argc = 0;\n"); - out.push_str(" napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n"); - } - - let checked = self_tag.is_some() - || f.params.iter().any(|p| { - matches!( - p.arg_pass(), - ArgPass::Object { .. } | ArgPass::Direct { .. } | ArgPass::Callback { .. } - ) - }); - if checked { - out.push_str(" napi_status arg_status = napi_ok;\n"); - } - - let mut m = MarshalledArgs { - c_args: Vec::new(), - cleanups: Vec::new(), - fail_cleanups: Vec::new(), - checked, - }; - if let Some(tag) = self_tag { - out.push_str(" void* self_raw = NULL;\n"); - out.push_str(" arg_status = weaveffi_napi_get_handle(env, args[0], &self_raw);\n"); - out.push_str(" if (arg_status == napi_ok && self_raw == NULL) {\n"); - out.push_str(" napi_throw_type_error(env, NULL, \"object used after close()\");\n"); - out.push_str(" arg_status = napi_invalid_arg;\n"); - out.push_str(" }\n"); - m.c_args.push(format!("(const {tag}*)self_raw")); - } - for (i, p) in f.params.iter().enumerate() { - emit_param(out, &mut m, p, i + offset, module, prefix); - } - m -} - -/// Emit the bail-out that runs after argument marshalling when any read -/// failed (a pending JS exception is already set): release what was -/// allocated so far and return without calling the producer. -fn emit_arg_check(out: &mut String, m: &MarshalledArgs) { - if !m.checked { - return; - } - out.push_str(" if (arg_status != napi_ok) {\n"); - for cleanup in m.cleanups.iter().chain(&m.fail_cleanups) { - out.push_str(" "); - out.push_str(cleanup); - } - out.push_str(" return NULL;\n"); - out.push_str(" }\n"); -} - -/// The body of an async callable's `Napi_*` entry point: marshal arguments, -/// allocate the context, create the promise and threadsafe function, launch, -/// and return the pending promise. -fn render_async_napi_body( - out: &mut String, - f: &FnBinding, - module: &str, - prefix: &str, - self_tag: Option<&str>, -) { - let c_name = &f.c_base; - let CallShape::Async(ab) = &f.shape else { - unreachable!("async body rendered for a non-async callable"); - }; - let mut m = emit_args(out, f, module, prefix, self_tag); - emit_arg_check(out, &m); - - let actx = format!("{c_name}_napi_actx"); - out.push_str(&format!( - " {actx}* ctx = ({actx}*)calloc(1, sizeof({actx}));\n" - )); - out.push_str(" napi_value promise;\n"); - out.push_str(" napi_create_promise(env, &ctx->deferred, &promise);\n"); - out.push_str(" napi_value resource_name;\n"); - out.push_str(&format!( - " napi_create_string_utf8(env, \"{c_name}\", NAPI_AUTO_LENGTH, &resource_name);\n" - )); - // Ref'd (unlike callback interfaces): a pending promise must keep the - // loop alive. - out.push_str(&format!( - " napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, {c_name}_napi_settle, &ctx->tsfn);\n" - )); - - if f.cancellable { - m.c_args.push("NULL".into()); - } - - let cb_name = format!("{c_name}_napi_cb"); - m.c_args.push(cb_name); - m.c_args.push("ctx".into()); - let args_str = m.c_args.join(", "); - out.push_str(&format!(" {}({args_str});\n", ab.launch.symbol)); - - for cleanup in &m.cleanups { - out.push_str(cleanup); - } - - out.push_str(" return promise;\n"); -} - -/// The body of a sync (or iterator-launching) callable's `Napi_*` entry -/// point: marshal arguments, call the C symbol, run cleanups, check the -/// error slot, and convert the result. -fn render_napi_body( - out: &mut String, - f: &FnBinding, - module: &str, - prefix: &str, - self_tag: Option<&str>, -) { - // The launcher symbol comes from the lowered shape rather than being - // rebuilt from the name, so interface members call the right entry point. - let symbol = match &f.shape { - CallShape::Sync(abi) => &abi.symbol, - CallShape::Iterator(ib) => &ib.launch.symbol, - CallShape::Async(_) => unreachable!("sync body rendered for an async callable"), - }; - let mut m = emit_args(out, f, module, prefix, self_tag); - emit_arg_check(out, &m); - - out.push_str(&format!(" {prefix}_error err = {{0}};\n")); - - if let Some(ret) = &f.ret { - emit_ret_out_params(out, &mut m.c_args, ret, module, prefix); - } - m.c_args.push("&err".to_string()); - - let args_str = m.c_args.join(", "); - match &f.ret { - Some(ret) => { - let rt = c_ret_type_str(ret, module, prefix); - out.push_str(&format!(" {rt} result = {symbol}({args_str});\n")); - } - None => { - out.push_str(&format!(" {symbol}({args_str});\n")); - } - } - - for cleanup in &m.cleanups { - out.push_str(cleanup); - } - - emit_error_check_c(out, prefix); - - match &f.ret { - Some(ret) => emit_ret_to_napi(out, ret, module, prefix, f), - None => { - out.push_str(" napi_value ret;\n"); - out.push_str(" napi_get_undefined(env, &ret);\n"); - out.push_str(" return ret;\n"); - } - } -} - -/// Marshal one incoming JS argument into its C ABI slot(s), dispatching on -/// the parameter's passing contract. A buffered parameter arrives as a -/// `Buffer` the JS loader packed; it lowers to the borrowed -/// `(const uint8_t*, size_t)` pair the callee decodes and never frees. An -/// object arrives as the handle the JS class borrowed from its instance -/// (`null` for an absent `Interface?`). A callback interface arrives as the -/// adapter object the JS loader built; it is registered in the handle table -/// and passed as the entry's address plus the interface's static vtable. -fn emit_param( - out: &mut String, - m: &mut MarshalledArgs, - p: &ParamBinding, - idx: usize, - module: &str, - prefix: &str, -) { - let name = p.name.as_str(); - match p.arg_pass() { - ArgPass::Buffer { .. } | ArgPass::Bytes { .. } => { - out.push_str(&format!(" void* {name}_raw = NULL;\n")); - out.push_str(&format!(" size_t {name}_len = 0;\n")); - out.push_str(&format!( - " napi_get_buffer_info(env, args[{idx}], &{name}_raw, &{name}_len);\n" - )); - m.c_args.push(format!("(const uint8_t*){name}_raw")); - m.c_args.push(format!("{name}_len")); - } - ArgPass::String { .. } => { - out.push_str(&format!(" size_t {name}_len = 0;\n")); - out.push_str(&format!( - " napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n" - )); - out.push_str(&format!( - " char* {name} = (char*)malloc({name}_len + 1);\n" - )); - out.push_str(&format!( - " napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n" - )); - m.c_args.push(name.into()); - m.cleanups.push(format!(" free({name});\n")); - } - // The callee borrows the pointer for the call; the JS wrapper keeps - // its own reference. `null` (an absent `Interface?`) passes NULL. - ArgPass::Object { slot, .. } => { - let ptr_ty = slot.ty.render_c(prefix); - out.push_str(&format!(" void* {name}_raw = NULL;\n")); - out.push_str(&format!( - " if (arg_status == napi_ok) arg_status = weaveffi_napi_get_handle(env, args[{idx}], &{name}_raw);\n" - )); - m.c_args.push(format!("({ptr_ty}){name}_raw")); - } - ArgPass::Callback { .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback-passed parameter names a callback interface"); - let c_tag = callback_c_tag(cb, module, prefix); - out.push_str(&format!(" napi_valuetype {name}_type;\n")); - out.push_str(&format!(" napi_typeof(env, args[{idx}], &{name}_type);\n")); - out.push_str(&format!(" {prefix}_napi_cb_ctx* {name}_ctx = NULL;\n")); - out.push_str(&format!( - " if ({name}_type == napi_object || {name}_type == napi_function) {{\n" - )); - out.push_str(&format!( - " {name}_ctx = {prefix}_napi_cb_register(env, args[{idx}], \"{c_tag}\", {c_tag}_napi_dispatch);\n" - )); - out.push_str(" } else {\n"); - out.push_str(&format!( - " napi_throw_type_error(env, NULL, \"expected a {} implementation\");\n", - cb - )); - out.push_str(" arg_status = napi_object_expected;\n"); - out.push_str(" }\n"); - m.c_args.push(format!("(void*){name}_ctx")); - m.c_args.push(format!("&{c_tag}_napi_vtable")); - m.fail_cleanups.push(format!( - " if ({name}_ctx != NULL) {prefix}_napi_cb_release(env, {name}_ctx);\n" - )); - } - ArgPass::Direct { slot } => { - let is_enum = matches!(p.ty, Ty::Enum(_)); - let ct = if is_enum { - "int32_t" - } else { - c_scalar_type(&p.ty) - }; - out.push_str(&format!(" {ct} {name} = 0;\n")); - out.push_str(" if (arg_status == napi_ok) {\n"); - emit_leaf_read( - out, - " ", - &p.ty, - &format!("args[{idx}]"), - name, - "arg_status", - ); - out.push_str(" }\n"); - if is_enum { - m.c_args - .push(format!("({}){name}", slot.ty.render_c(prefix))); - } else { - m.c_args.push(name.into()); - } - } - } -} - -/// Declare and thread the trailing out-parameters a return type needs. Bytes -/// and buffered returns share the single `size_t* out_len` slot; iterator -/// returns follow the iterator protocol and take none. -fn emit_ret_out_params( - out: &mut String, - c_args: &mut Vec, - ty: &Ty, - module: &str, - prefix: &str, -) { - if matches!(ty, Ty::Iterator(_)) { - return; - } - if matches!( - plan::ret_pass(Some(ty), module, prefix), - RetPass::Bytes | RetPass::Buffer - ) { - out.push_str(" size_t out_len = 0;\n"); - c_args.push("&out_len".into()); - } -} - -/// Convert the C `result` (plus `out_len` when present) into the JS return -/// value and release what the consumer owes, dispatching on the shared -/// receiving plan. A buffered return is copied into a JS `Buffer` and -/// released with `{prefix}_free_bytes`; the JS loader decodes it into the -/// idiomatic value. A returned object is one strong reference surfaced as a -/// handle the JS class adopts (and eventually destroys); the addon never -/// releases it here. -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, 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 - // points one element at a time; the external's finalizer is the - // safety net for abandoned iterators. - let c_name = &f.c_base; - out.push_str(&format!( - " {prefix}_napi_iter_state* iter_state = ({prefix}_napi_iter_state*)calloc(1, sizeof({prefix}_napi_iter_state));\n" - )); - out.push_str(" iter_state->iter = (void*)result;\n"); - out.push_str(&format!( - " napi_create_external(env, iter_state, {c_name}_napi_iter_finalize, NULL, &ret);\n" - )); - out.push_str(" return ret;\n"); - return; - } - match plan::ret_pass(Some(ty), module, prefix) { - RetPass::Void => unreachable!("a present return type is never void"), - // Copy the owned encoding into a JS Buffer, then release it. Byte - // and buffered returns share the shape; only the JS-side decode - // differs. - RetPass::Bytes | RetPass::Buffer => { - out.push_str(" napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n"); - out.push_str(&format!( - " {prefix}_free_bytes((uint8_t*)result, out_len);\n" - )); - } - RetPass::String => { - out.push_str(" napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n"); - out.push_str(&format!(" {prefix}_free_string(result);\n")); - } - // A NULL result (the absent case of `Interface?`) surfaces as null. - RetPass::Object { .. } => { - out.push_str(" weaveffi_napi_make_handle(env, result, &ret);\n"); - } - RetPass::Direct => { - out.push_str(&format!(" {}\n", napi_create_leaf(ty, "result", "ret"))); - } - } - out.push_str(" return ret;\n"); -} diff --git a/crates/weaveffi-gen-node/src/codec.rs b/crates/weaveffi-gen-node/src/codec.rs deleted file mode 100644 index 4fe66515..00000000 --- a/crates/weaveffi-gen-node/src/codec.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! Value-buffer codec emitters for the JS loader. -//! -//! Buffered values (records, rich enums, optionals, lists, maps, and error -//! payloads) cross the addon boundary as `Buffer`s holding the WeaveFFI value -//! buffer encoding. This module emits the JS expressions that read and write -//! that encoding: generic combinators for optionals, lists, and maps, and one -//! generated pack/unpack function per record and rich enum. Every dispatch -//! goes through [`Ty::wire`], so the wire-shape classification (records and -//! rich enums as one user-codec shape, interfaces as object tokens) lives in -//! `weaveffi-core`, not here. The composition is fixed at generation time -//! from the IR; no runtime type dispatch happens. -//! -//! Object tokens follow the reference-counting contract: writing an interface -//! field calls the wrapper's `_cloneHandle()` (which invokes the producer's -//! `_clone` symbol) so the buffer carries a fresh strong reference, and -//! reading one adopts the token into a new wrapper via `_adopt`. - -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::ERROR_BRAND; -use weaveffi_core::model::ModuleBinding; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{Prim, WireType}; -use weaveffi_core::utils::local_type_name; - -/// The reader/writer method of the private buffer runtime for a scalar wire -/// shape (including strings and bytes, whose length-prefixed reads live in -/// the runtime too). The 64-bit methods read and write JS `bigint`s. -/// -/// # Panics -/// -/// Panics on a composite wire shape; callers dispatch enums, objects, user -/// codecs, and containers before falling through to a scalar method. -fn scalar_method(wt: WireType<'_>) -> &'static str { - match wt { - 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:?}"), - } -} - -/// The JS expression writing the object token of wrapper `val` (an instance -/// of the interface's class): a freshly cloned strong reference, never the -/// handle the wrapper keeps for itself. -fn js_object_token_expr(val: &str) -> String { - format!("w.u64({val}._cloneHandle())") -} - -/// The JS expression adopting an object token read from `r` into a new -/// wrapper of interface `name` (possibly dot-qualified; the class is named by -/// its local type name). -fn js_object_adopt_expr(name: &str) -> String { - format!("{}._adopt(r.u64())", local_type_name(name)) -} - -/// 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; interfaces -/// write a cloned object token. -pub(crate) fn js_writer_fn(ty: &Ty) -> String { - match ty.wire() { - WireType::Enum(_) => "(w, v) => w.i32(v)".into(), - WireType::Object(_) => format!("(w, v) => {}", js_object_token_expr("v")), - 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)), - WireType::Map(k, v) => format!( - "(w, v) => __wMap(w, v, {}, {})", - js_map_key_writer_fn(k), - js_writer_fn(v) - ), - leaf => format!("(w, v) => w.{}(v)", scalar_method(leaf)), - } -} - -/// 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: &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) => "(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)), - } -} - -/// A JS function expression `(r) => value` reading one value of `ty` from the -/// wire format. 64-bit integers surface as `bigint`s (matching the TS -/// surface); object tokens are adopted into wrapper instances. -pub(crate) fn js_reader_fn(ty: &Ty) -> String { - match ty.wire() { - WireType::Object(n) => format!("(r) => {}", js_object_adopt_expr(n)), - 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)), - WireType::List(inner) => format!("(r) => __rList(r, {})", js_reader_fn(inner)), - WireType::Map(k, v) => { - format!("(r) => __rMap(r, {}, {})", js_reader_fn(k), js_reader_fn(v)) - } - leaf => format!("(r) => r.{}()", scalar_method(leaf)), - } -} - -/// 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: &Ty, val: &str) -> String { - match ty.wire() { - WireType::Enum(_) => format!("w.i32({val})"), - WireType::Object(_) => js_object_token_expr(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)), - WireType::Map(k, v) => format!( - "__wMap(w, {val}, {}, {})", - js_map_key_writer_fn(k), - js_writer_fn(v) - ), - leaf => format!("w.{}({val})", scalar_method(leaf)), - } -} - -/// The JS expression reading one value of type `ty` from reader `r`. -pub(crate) fn js_read_expr(ty: &Ty) -> String { - match ty.wire() { - WireType::Object(n) => js_object_adopt_expr(n), - 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)), - WireType::List(inner) => format!("__rList(r, {})", js_reader_fn(inner)), - WireType::Map(k, v) => format!("__rMap(r, {}, {})", js_reader_fn(k), js_reader_fn(v)), - leaf => format!("r.{}()", scalar_method(leaf)), - } -} - -/// Emit one module's pack/unpack functions: one pair per record and one pair -/// per rich enum, with fields written and read in declaration (wire) order. -pub(crate) fn render_pack_fns_js(out: &mut String, m: &ModuleBinding) { - let mut w = CodeWriter::two_space(); - for s in &m.structs { - w.block(format!("function __pack{}(w, v) {{", s.name), "}", |w| { - for field in &s.fields { - w.line(format!( - "{};", - js_write_expr(&field.ty, &format!("v.{}", field.name)) - )); - } - }); - w.block(format!("function __unpack{}(r) {{", s.name), "}", |w| { - if s.fields.is_empty() { - w.line("return {};"); - } else { - w.line("return {"); - for field in &s.fields { - w.line(format!(" {}: {},", field.name, js_read_expr(&field.ty))); - } - w.line("};"); - } - }); - } - for e in &m.enums { - if !e.is_rich() { - continue; - } - let name = &e.name; - // Pack: string tag selects the variant; the i32 discriminant plus the - // variant's fields go on the wire. - w.block(format!("function __pack{name}(w, v) {{"), "}", |w| { - w.block("switch (v.tag) {", "}", |w| { - for v in &e.variants { - w.line(format!("case '{}':", v.name)); - w.line(format!(" w.i32({});", v.value)); - for field in &v.fields { - w.line(format!( - " {};", - js_write_expr(&field.ty, &format!("v.{}", field.name)) - )); - } - w.line(" break;"); - } - w.line("default:"); - w.line(format!( - " throw new {ERROR_BRAND}(-2, 'unknown {name} tag: ' + (v && v.tag));" - )); - }); - }); - // Unpack: the i32 discriminant selects the variant; fields decode in - // order and land next to the string tag. - w.block(format!("function __unpack{name}(r) {{"), "}", |w| { - w.line("const tag = r.i32();"); - w.block("switch (tag) {", "}", |w| { - for v in &e.variants { - let fields: String = v - .fields - .iter() - .map(|f| format!(", {}: {}", f.name, js_read_expr(&f.ty))) - .collect(); - w.line(format!( - "case {}: return {{ tag: '{}'{fields} }};", - v.value, v.name - )); - } - w.line(format!( - "default: throw new {ERROR_BRAND}(-2, 'unknown {name} tag: ' + tag);" - )); - }); - }); - } - let text = w.finish(); - if !text.is_empty() { - out.push_str(&text); - out.push('\n'); - } -} diff --git a/crates/weaveffi-gen-node/src/entities.rs b/crates/weaveffi-gen-node/src/entities.rs deleted file mode 100644 index 4ca4ef51..00000000 --- a/crates/weaveffi-gen-node/src/entities.rs +++ /dev/null @@ -1,568 +0,0 @@ -//! The idiomatic JS surface layered over the addon: error classes, interface -//! classes, callback-interface adapters, function wrappers, and the -//! `index.js` assembler that composes them with the runtime prelude. - -use heck::ToLowerCamelCase; -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, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, - ModuleBinding, ParamBinding, -}; -use weaveffi_core::plan::{ArgPass, RetPass}; -use weaveffi_core::utils::{ - local_type_name, render_prelude, render_trailer, wrapper_name, CommentStyle, -}; - -use crate::codec::{js_read_expr, js_reader_fn, js_writer_fn, render_pack_fns_js}; -use crate::runtime::{ - render_error_brand_js, render_iterator_class_js, render_loader_js, render_object_helpers_js, - BUFFER_RUNTIME_JS, -}; -use crate::types::{ - iface_lifecycle_base, iface_member_base, js_adapter_name, js_fn_name, js_method_name, - js_param_name, js_str_literal, -}; - -/// Recognize an interface-typed value carried directly or as `Interface?` -/// (the only optional that stays a nullable pointer). Buffered values are -/// handled separately by the wrapper body. -struct ObjectShape { - /// The local JS class name. - cls: String, - /// `true` for `Interface?`: the addon surfaces `null` for the absent case. - nullable: bool, -} - -/// Recognize a class-typed (interface) value, direct or optional. -fn js_object_shape(ty: Option<&Ty>) -> Option { - let ty = ty?; - let nullable = matches!(ty, Ty::Optional(_)); - ty.interface_name().map(|n| ObjectShape { - cls: local_type_name(n).to_string(), - nullable, - }) -} - -/// The JS expression adopting one raw addon value `expr` (an object handle, -/// or `null` for an absent `Interface?`) into a wrapper instance. -fn js_adopt_expr(shape: &ObjectShape, expr: &str) -> String { - let cls = &shape.cls; - if shape.nullable { - format!("({expr} == null ? null : {cls}._adopt({expr}))") - } else { - format!("{cls}._adopt({expr})") - } -} - -/// The JS expression converting one raw addon value `expr` of type `ty` -/// received *from* the producer (a return, an async result, an iterator -/// element, or a callback-method argument) into its idiomatic form: buffers -/// decode, object handles are adopted, everything else passes through. -fn js_receive_expr(ty: &Ty, expr: &str) -> String { - if ty.is_buffered() { - return format!("__decode({}, {expr})", js_reader_fn(ty)); - } - match js_object_shape(Some(ty)) { - Some(shape) => js_adopt_expr(&shape, expr), - None => expr.to_string(), - } -} - -/// The addon-argument expression for one logical parameter, dispatching on -/// its passing contract. Buffered values pack into a `Buffer` via the -/// generated writer; interface instances are borrowed to their raw handle -/// (the callee never takes ownership; `Interface?` passes `null` for none); -/// callback-interface implementations are wrapped in the interface's adapter -/// so the addon can hand them raw values; everything else passes through. -fn js_arg_expr(js_name: &str, p: &ParamBinding) -> String { - match p.arg_pass() { - ArgPass::Buffer { .. } => format!("__encode({}, {js_name})", js_writer_fn(&p.ty)), - ArgPass::Object { nullable, .. } => { - let cls = local_type_name( - p.ty.interface_name() - .expect("object-passed parameter names an interface"), - ); - if nullable { - format!("({js_name} == null ? null : __borrow({js_name}, {cls}))") - } else { - format!("__borrow({js_name}, {cls})") - } - } - ArgPass::Callback { .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback-passed parameter names a callback interface"); - format!("{}({js_name})", js_adapter_name(cb)) - } - ArgPass::Direct { .. } | ArgPass::String { .. } | ArgPass::Bytes { .. } => { - js_name.to_string() - } - } -} - -/// The rebranding factory a callable's failures route through: the declaring -/// module's domain factory when the callable `throws`, the generic -/// [`ERROR_BRAND`] constructor otherwise. Negative runtime codes (a producer -/// panic, a marshalling failure, a callback implementation that raised) never -/// match a domain table, so both paths surface them as the generic brand. -fn js_error_map_expr(f: &FnBinding, error: Option<&ErrorBinding>) -> String { - match error { - Some(eb) if f.throws => js_error_factory_name(eb), - _ => "__generic".to_string(), - } -} - -/// `__kvErrorFrom`, the code-to-class factory of the domain declared by -/// `owner_path`. Derived from the owner so inheriting submodules name the -/// same function. -fn js_error_factory_name(eb: &ErrorBinding) -> String { - format!("__{}ErrorFrom", eb.owner_path.to_lower_camel_case()) -} - -/// Emit one C-style enum as a frozen runtime object on `wv`, the value a -/// `types.d.ts` `export enum` declaration promises: every variant name maps -/// to its discriminant and, as TypeScript's own `enum` lowering does, every -/// discriminant maps back to its variant name. Rich enums are tagged unions -/// with no runtime value and are skipped. -fn render_enum_js(out: &mut String, e: &EnumBinding) { - if e.is_rich() { - return; - } - let mut w = CodeWriter::two_space(); - w.block(format!("wv.{} = Object.freeze({{", e.name), "});", |w| { - for v in &e.variants { - w.line(format!("{}: {},", v.name, v.value)); - } - for v in &e.variants { - w.line(format!("{}: '{}',", v.value, v.name)); - } - }); - out.push_str(&w.finish()); -} - -/// Emit one declaring module's typed error surface onto `wv`: the domain -/// class extending the generic brand, one subclass per code carrying its -/// stable `CODE` and default message, and the factory mapping a raw ABI code -/// (plus the raw payload buffer) to the matching class. Codes that declare -/// payload fields get a decoder that unpacks the buffer and attaches the -/// fields as properties on the error instance; unknown codes fall back to -/// the generic brand. -/// -/// Negative codes are reserved by the runtime (generic error, producer -/// panic, marshalling failure, foreign callback failure) and domain codes are -/// validated positive-only, so the table lookup never matches a negative code -/// and the trap idiom (fall back to [`ERROR_BRAND`]) holds by construction. -fn render_error_classes_js(out: &mut String, eb: &ErrorBinding) { - let domain = &eb.type_name; - let factory = js_error_factory_name(eb); - let table = format!("__{}ErrorCodes", eb.owner_path.to_lower_camel_case()); - let payloads = format!("__{}ErrorPayloads", eb.owner_path.to_lower_camel_case()); - let has_payloads = eb.codes.iter().any(|c| !c.fields.is_empty()); - - let mut w = CodeWriter::two_space(); - w.block( - format!("class {domain} extends {ERROR_BRAND} {{"), - "}", - |w| { - w.block("constructor(code, message) {", "}", |w| { - w.line("super(code, message);"); - w.line(format!("this.name = '{domain}';")); - }); - }, - ); - w.line(format!("wv.{domain} = {domain};")); - for c in &eb.codes { - let class = error_type_name(&c.name, "Error"); - let default_msg = js_str_literal(&c.message); - w.block(format!("class {class} extends {domain} {{"), "}", |w| { - w.block("constructor(message) {", "}", |w| { - w.line(format!("super({}, message || '{default_msg}');", c.value)); - w.line(format!("this.name = '{class}';")); - }); - }); - w.line(format!("{class}.CODE = {};", c.value)); - w.line(format!("wv.{class} = {class};")); - } - let entries: Vec = eb - .codes - .iter() - .map(|c| format!("{}: {}", c.value, error_type_name(&c.name, "Error"))) - .collect(); - w.line(format!( - "const {table} = Object.freeze({{ {} }});", - entries.join(", ") - )); - if has_payloads { - // One payload decoder per code that declares fields, reading the - // code's fields in declaration (wire) order. - let decoders: Vec = eb - .codes - .iter() - .filter(|c| !c.fields.is_empty()) - .map(|c| { - let fields: Vec = c - .fields - .iter() - .map(|f| format!("{}: {}", f.name, js_read_expr(&f.ty))) - .collect(); - format!("{}: (r) => ({{ {} }})", c.value, fields.join(", ")) - }) - .collect(); - w.line(format!( - "const {payloads} = Object.freeze({{ {} }});", - decoders.join(", ") - )); - } - w.block( - format!("function {factory}(code, message, payload) {{"), - "}", - |w| { - w.line(format!("const _cls = {table}[code];")); - w.line(format!( - "const _err = _cls === undefined ? new {ERROR_BRAND}(code, message) : new _cls(message);" - )); - if has_payloads { - w.line(format!("const _decode = {payloads}[code];")); - w.block( - "if (_decode !== undefined && payload != null) {", - "}", - |w| { - w.line("Object.assign(_err, __decode(_decode, payload));"); - }, - ); - } - w.line("return _err;"); - }, - ); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit one wrapper callable's body: pack buffered arguments, borrow -/// class-typed ones, and adapt callback implementations; invoke the addon -/// binding through the rebranding helper; then decode a buffered result or -/// adopt an object-typed one. Iterator-returning callables launch the native -/// iterator and hand its external to the shared lazy iterator class, decoding -/// or adopting each element per step. Shared by free functions and interface -/// members (`self_expr` supplies the leading handle of an instance method). -fn emit_wrapper_body_js( - w: &mut CodeWriter, - f: &FnBinding, - addon_name: &str, - self_expr: Option<&str>, - map_expr: &str, -) { - let mut args: Vec = Vec::new(); - if let Some(s) = self_expr { - args.push(s.to_string()); - } - for p in &f.params { - args.push(js_arg_expr(&js_param_name(&p.name), p)); - } - let args = args.join(", "); - let invoke = if f.is_async { - "__invokeAsync" - } else { - "__invoke" - }; - let call = format!("{invoke}(addon.{addon_name}, [{args}], {map_expr})"); - - 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; - // object elements arrive as owned handles adopted per step. - let received = js_receive_expr(inner, "_e"); - let wrap_elem = if received == "_e" { - "null".to_string() - } else { - format!("(_e) => {received}") - }; - w.line(format!("const _it = {call};")); - w.line(format!( - "return new WeaveFFIIterator(_it, addon.{addon_name}_iterNext, addon.{addon_name}_iterDestroy, {map_expr}, {wrap_elem});" - )); - return; - } - - let Some(ret) = f.ret.as_ref() else { - w.line(format!("return {call};")); - return; - }; - let received = js_receive_expr(ret, "_r"); - if received == "_r" { - w.line(format!("return {call};")); - } else if f.is_async { - w.line(format!("return {call}.then((_r) => {received});")); - } else { - w.line(format!("const _r = {call};")); - w.line(format!("return {received};")); - } -} - -/// Emit one interface's JS class onto `wv`. The class holds the object's -/// native handle (one strong reference, as a `bigint`) and releases it -/// exactly once: through `close()`, through `Symbol.dispose` (which aliases -/// `close`, for `using` declarations), or through a `FinalizationRegistry` -/// backstop when the wrapper is collected unclosed. `_adopt` wraps a handle -/// received from the producer without running the public constructor, and -/// `_cloneHandle` produces a second strong reference (through the producer's -/// `_clone` symbol) for the value-buffer codec to write as an object token. -/// -/// A sync constructor named `new` becomes the JS `constructor`; every other -/// constructor becomes a static factory; methods pass the wrapped handle as -/// the leading addon argument; statics are static methods. -fn render_interface_class_js( - out: &mut String, - i: &InterfaceBinding, - m: &ModuleBinding, - strip: bool, -) { - let name = &i.name; - let destroy_js = wrapper_name(&m.path, &iface_lifecycle_base(name, "destroy"), strip); - let clone_js = wrapper_name(&m.path, &iface_lifecycle_base(name, "clone"), strip); - let error = m.error.as_ref(); - - let mut w = CodeWriter::two_space(); - w.block(format!("class {name} {{"), "}", |w| { - let canonical = i - .constructors - .iter() - .find(|c| c.name == "new" && !c.is_async); - if let Some(c) = canonical { - let addon_name = wrapper_name(&m.path, &iface_member_base(name, &c.name), strip); - let params: Vec = c.params.iter().map(|p| js_param_name(&p.name)).collect(); - let args: Vec = c - .params - .iter() - .map(|p| js_arg_expr(&js_param_name(&p.name), p)) - .collect(); - let map = js_error_map_expr(c, error); - w.block(format!("constructor({}) {{", params.join(", ")), "}", |w| { - w.line(format!( - "this._handle = __invoke(addon.{addon_name}, [{}], {map});", - args.join(", ") - )); - w.line(format!( - "{name}._cleanup.register(this, this._handle, this);" - )); - }); - } - for c in &i.constructors { - if canonical.is_some_and(|canon| std::ptr::eq(canon, c)) { - continue; - } - let addon_name = wrapper_name(&m.path, &iface_member_base(name, &c.name), strip); - let factory = c.name.to_lower_camel_case(); - let params: Vec = c.params.iter().map(|p| js_param_name(&p.name)).collect(); - let map = js_error_map_expr(c, error); - w.block( - format!("static {factory}({}) {{", params.join(", ")), - "}", - |w| { - emit_wrapper_body_js(w, c, &addon_name, None, &map); - }, - ); - } - for f in &i.methods { - let addon_name = wrapper_name(&m.path, &iface_member_base(name, &f.name), strip); - let method = f.name.to_lower_camel_case(); - let params: Vec = f.params.iter().map(|p| js_param_name(&p.name)).collect(); - let map = js_error_map_expr(f, error); - w.block(format!("{method}({}) {{", params.join(", ")), "}", |w| { - emit_wrapper_body_js( - w, - f, - &addon_name, - Some(&format!("__borrow(this, {name})")), - &map, - ); - }); - } - for f in &i.statics { - let addon_name = wrapper_name(&m.path, &iface_member_base(name, &f.name), strip); - let method = f.name.to_lower_camel_case(); - let params: Vec = f.params.iter().map(|p| js_param_name(&p.name)).collect(); - let map = js_error_map_expr(f, error); - w.block( - format!("static {method}({}) {{", params.join(", ")), - "}", - |w| { - emit_wrapper_body_js(w, f, &addon_name, None, &map); - }, - ); - } - // Explicit release; guarded so a double `close()` (or close-then-GC) - // is a no-op rather than a double release. - w.block("close() {", "}", |w| { - w.block("if (this._handle) {", "}", |w| { - w.line(format!("{name}._cleanup.unregister(this);")); - w.line(format!("addon.{destroy_js}(this._handle);")); - w.line("this._handle = 0n;"); - }); - }); - // A second strong reference for the value-buffer codec: the object - // token written into a buffer must never be the handle this wrapper - // still owns. - w.block("_cloneHandle() {", "}", |w| { - w.line(format!("return addon.{clone_js}(__borrow(this, {name}));")); - }); - }); - - // Adopt an owned handle received from the addon without running the - // public constructor (which would invoke the native constructor again). - w.block(format!("{name}._adopt = function (handle) {{"), "};", |w| { - w.line(format!("const _o = Object.create({name}.prototype);")); - w.line("_o._handle = handle;"); - w.line(format!("{name}._cleanup.register(_o, handle, _o);")); - w.line("return _o;"); - }); - w.block( - format!("{name}._cleanup = new FinalizationRegistry((handle) => {{"), - "});", - |w| { - w.line(format!("if (handle) {{ addon.{destroy_js}(handle); }}")); - }, - ); - w.block("if (typeof Symbol.dispose === 'symbol') {", "}", |w| { - w.line(format!( - "{name}.prototype[Symbol.dispose] = {name}.prototype.close;" - )); - }); - w.line(format!("wv.{name} = {name};")); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the adapter for one callback interface. The consumer passes any -/// object with the interface's camelCase methods; the adapter is what the -/// addon actually holds (behind a `napi_ref`) and calls by raw IDL method -/// name with raw values: strings, numbers, `bigint`s, booleans, and `Buffer`s -/// for buffered arguments (borrowed and already copied by the addon), and -/// `bigint` handles (or `null` for an absent `Interface?`) for object -/// arguments, each carrying one strong reference the adapter adopts into a -/// wrapper before invoking the implementation. Return values pass straight -/// back; the addon converts them to the method's C return type. An exception -/// thrown by the implementation propagates to the addon's trampoline, which -/// reports it through `out_err` as a foreign error. -fn render_callback_adapter_js( - out: &mut String, - m: &ModuleBinding, - cb: &CallbackInterfaceBinding, - prefix: &str, -) { - let adapter = js_adapter_name(&cb.name); - let protocol = cb.protocol(&m.path, prefix); - let mut w = CodeWriter::two_space(); - w.block(format!("function {adapter}(impl) {{"), "}", |w| { - w.block("if (impl === null || impl === undefined) {", "}", |w| { - w.line(format!( - "throw new {ERROR_BRAND}(-3, '{} implementation must be an object');", - cb.name - )); - }); - w.block("return {", "};", |w| { - for (method, args) in cb.methods.iter().zip(&protocol.method_args) { - let params: Vec = method - .params - .iter() - .map(|p| js_param_name(&p.name)) - .collect(); - let converted: Vec = method - .params - .iter() - .zip(args) - .map(|(p, pass)| { - let n = js_param_name(&p.name); - match pass { - RetPass::Buffer | RetPass::Object { .. } => js_receive_expr(&p.ty, &n), - _ => n, - } - }) - .collect(); - w.block( - format!("{}({}) {{", method.name, params.join(", ")), - "},", - |w| { - w.line(format!( - "return impl.{}({});", - js_method_name(&method.name), - converted.join(", ") - )); - }, - ); - } - }); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// The JS loader (`index.js`). Re-exports the native addon's bindings, then -/// layers the idiomatic surface on top: the generic error brand plus one -/// typed error class per declared domain, the private value-buffer runtime -/// with one pack/unpack pair per record and rich enum, wrapper classes for -/// interfaces, one adapter per callback interface, and one wrapper per module -/// function so failures rebrand as the right error class and value types -/// cross as plain objects rather than raw buffers. -pub(crate) fn render_node_index(model: &BindingModel, strip: bool, input_basename: &str) -> String { - let dbl = CommentStyle::DoubleSlash; - let mut out = render_prelude(dbl, input_basename); - render_loader_js(&mut out); - render_error_brand_js(&mut out); - - if model.has_interfaces() { - render_object_helpers_js(&mut out); - } - - if model.has_buffers() { - out.push_str(BUFFER_RUNTIME_JS); - for m in &model.modules { - render_pack_fns_js(&mut out, m); - } - } - - if model.has_iterators() { - render_iterator_class_js(&mut out); - } - - for m in &model.modules { - if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { - render_error_classes_js(&mut out, eb); - } - for e in &m.enums { - render_enum_js(&mut out, e); - } - for cb in &m.callback_interfaces { - render_callback_adapter_js(&mut out, m, cb, &model.prefix); - } - for i in &m.interfaces { - render_interface_class_js(&mut out, i, m, strip); - } - } - - // One wrapper per module function, so every failure is rebranded and - // buffered or class-typed values cross as idiomatic values. - for m in &model.modules { - for f in &m.functions { - let js = js_fn_name(&m.path, &f.name, strip); - let params: Vec = f.params.iter().map(|p| js_param_name(&p.name)).collect(); - let map = js_error_map_expr(f, m.error.as_ref()); - let mut w = CodeWriter::two_space(); - w.block( - format!("wv.{js} = function ({}) {{", params.join(", ")), - "};", - |w| { - emit_wrapper_body_js(w, f, &js, None, &map); - }, - ); - out.push_str(&w.finish()); - } - } - - out.push_str("\nmodule.exports = wv;\n\n"); - out.push_str(&render_trailer(dbl, "index.js")); - out -} diff --git a/crates/weaveffi-gen-node/src/lib.rs b/crates/weaveffi-gen-node/src/lib.rs deleted file mode 100644 index d8107fdd..00000000 --- a/crates/weaveffi-gen-node/src/lib.rs +++ /dev/null @@ -1,730 +0,0 @@ -//! Node.js (N-API) binding generator for WeaveFFI. -//! -//! Emits a JavaScript loader plus TypeScript type definitions for the -//! companion N-API addon (ABI revision 2). Records and rich enums are value -//! types: they cross the ABI serialized in WeaveFFI value buffers, so records -//! surface as plain JS objects, rich enums as tagged unions, and the loader -//! carries a small private buffer writer/reader plus one pack and one unpack -//! function per type. 64-bit integers are `bigint`s everywhere (parameters, -//! returns, record fields, callback arguments), so nothing above 2^53 is -//! silently rounded. -//! -//! Interfaces surface as JS classes holding one strong reference to a -//! reference-counted native object: `close()` (or `Symbol.dispose`, for -//! `using` declarations) releases it, and a `FinalizationRegistry` backstop -//! releases a wrapper that is collected unclosed, so the producer's -//! `destroy` runs exactly once. Object-typed parameters are borrowed for the -//! call; returned, awaited, and iterated objects are adopted into fresh -//! wrappers; `Interface?` maps to `Wrapper | null`; and objects inside value -//! buffers cross as freshly cloned object tokens. Callback interfaces surface -//! as TypeScript `interface`s any object with the right methods satisfies: -//! the addon keeps the implementation behind a `napi_ref`, installs one -//! static vtable per interface, and its trampolines call the implementation -//! directly on the JS thread or hop through a `napi_threadsafe_function` and -//! wait when the producer calls from another thread. Async functions surface -//! as `Promise`-returning functions, `iter` functions as lazy -//! `IterableIterator` wrappers that pull one element per step, and each -//! declared error domain as an `Error` subclass extending the generic -//! `WeaveFFIError` brand. Implements [`LanguageBackend`]; the shared driver -//! bridges it into the generator pipeline. -//! -//! The crate is split by emitted artifact: `types` holds JS/TS naming and -//! type mapping, `codec` the value-buffer codec emitters, `runtime` the -//! fixed JS runtime prelude, `entities` the idiomatic JS surface and the -//! `index.js` assembler, `addon` the native C addon, and `package` the -//! manifests, `types.d.ts`, and the packaged layout. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod addon; -mod codec; -mod entities; -mod package; -mod runtime; -mod types; - -use camino::Utf8Path; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::{BindingModel, CallbackInterfaceBinding, ModuleBinding}; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::pkg; -use weaveffi_core::platform::Platform; -use weaveffi_core::resolved::ResolvedApi; - -use crate::addon::render_addon_c; -use crate::entities::render_node_index; -use crate::package::{ - node_platform_tokens, render_binding_gyp, render_callback_interface_dts, render_node_dts, - render_package_json, render_packaged_binding_gyp, render_packaged_package_json, - render_packaged_readme, render_platform_package_json, -}; - -/// Per-target configuration for [`NodeGenerator`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct NodeConfig { - /// npm package name (default `"weaveffi"`). - pub package_name: Option, - /// When `true` (the default), strip the IR module name prefix from - /// emitted JS/TS function names, so module `kv`'s `open_store` exports as - /// `openStore` rather than `kvOpenStore`. Set to `false` to keep - /// module-prefixed names. - pub strip_module_prefix: bool, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the native addon calls the same - /// exported symbols the producer emits. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with. - #[serde(skip)] - pub input_basename: Option, -} - -impl Default for NodeConfig { - fn default() -> Self { - Self { - package_name: None, - strip_module_prefix: true, - prefix: None, - input_basename: None, - } - } -} - -impl NodeConfig { - /// Returns the configured npm package name, falling back to `"weaveffi"`. - pub fn package_name(&self) -> &str { - self.package_name.as_deref().unwrap_or("weaveffi") - } - - /// Returns the configured C ABI symbol prefix, falling back to - /// `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// Node.js backend: emits a JavaScript loader and TypeScript declarations for -/// the companion N-API addon that wraps the C ABI. -pub struct NodeGenerator; - -impl LanguageBackend for NodeGenerator { - type Config = NodeConfig; - - fn name(&self) -> &'static str { - "node" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - /// The consumer-facing TypeScript `interface` for one callback interface, - /// the same declaration `types.d.ts` carries. The JS adapter and the - /// native vtable are emitted by the `index.js` and addon assemblers. - fn render_callback_interface( - &self, - out: &mut String, - _module: &ModuleBinding, - cb: &CallbackInterfaceBinding, - _config: &Self::Config, - ) { - render_callback_interface_dts(out, cb); - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let dir = out_dir.join("node"); - let input_basename = config.input_basename(); - let strip = config.strip_module_prefix; - vec![ - OutputFile::new( - dir.join("index.js"), - render_node_index(model, strip, input_basename), - ), - OutputFile::new( - dir.join("types.d.ts"), - render_node_dts(model, strip, input_basename), - ), - OutputFile::new( - dir.join("package.json"), - render_package_json( - &pkg::resolve( - api, - config.package_name.as_deref(), - config.input_basename.as_deref(), - ), - input_basename, - ), - ), - OutputFile::new(dir.join("binding.gyp"), render_binding_gyp(input_basename)), - OutputFile::new( - dir.join("weaveffi_addon.c"), - render_addon_c(model, strip, input_basename), - ), - ] - } - - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let dir = out_dir.join("node"); - let input_basename = config.input_basename(); - let strip = config.strip_module_prefix; - let package = pkg::resolve( - api, - config.package_name.as_deref(), - config.input_basename.as_deref(), - ); - let lib = &ctx.binaries.lib_name; - - // The per-platform package names follow the esbuild/swc convention: - // `--` constrained by npm `os`/`cpu`, so npm - // installs only the matching one. Platforms Node does not run on - // (Android, Wasm) have no npm tokens and are skipped. - let platform_pkgs: Vec<(Platform, (&str, &str), String)> = ctx - .binaries - .platforms() - .filter_map(|p| { - let tokens = node_platform_tokens(p)?; - let (os, cpu) = tokens; - Some((p, tokens, format!("{}-{os}-{cpu}", package.name))) - }) - .collect(); - let dep_pkgs: Vec<(Platform, String)> = platform_pkgs - .iter() - .map(|(p, _, name)| (*p, name.clone())) - .collect(); - - let mut files = vec![ - PackagedFile::text( - dir.join("index.js"), - render_node_index(model, strip, input_basename), - ), - PackagedFile::text( - dir.join("types.d.ts"), - render_node_dts(model, strip, input_basename), - ), - PackagedFile::text( - dir.join("package.json"), - render_packaged_package_json(&package, &dep_pkgs, input_basename), - ), - PackagedFile::text( - dir.join("binding.gyp"), - render_packaged_binding_gyp(&package.name, lib, input_basename), - ), - PackagedFile::text( - dir.join("weaveffi_addon.c"), - render_addon_c(model, strip, input_basename), - ), - PackagedFile::text( - dir.join("README.md"), - render_packaged_readme(&package, ctx, input_basename), - ), - ]; - - // Each platform package bundles its prebuilt library and is gated by - // npm `os`/`cpu` so only the matching one installs. - for (platform, tokens, pkg_name) in &platform_pkgs { - let pkg_dir = dir.join("npm").join(pkg_name); - files.push(PackagedFile::text( - pkg_dir.join("package.json"), - render_platform_package_json(pkg_name, &package.version, *tokens), - )); - let nb = ctx.binaries.get(*platform).expect("platform has a binary"); - files.push(PackagedFile::copy( - pkg_dir.join(ctx.binaries.bundled_filename(*platform)), - nb.source.clone(), - )); - } - Some(files) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use weaveffi_ir::ir::{ - Api, CallbackInterfaceDef, EnumDef, EnumVariant, Function, InterfaceDef, Module, Param, - StructDef, StructField, TypeRef, - }; - - fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } - } - - fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } - } - - fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } - } - - fn named(name: &str) -> TypeRef { - TypeRef::Named(name.into()) - } - - fn variant(name: &str, value: i32, fields: Vec) -> EnumVariant { - EnumVariant { - name: name.into(), - value, - doc: None, - fields, - } - } - - /// One module exercising every revision-2 shape: an interface with a - /// constructor and a method, `Store?` in and out, a record holding a - /// `Store` and a `[Store]`, an iterator of `Store`, an async function - /// returning a `Store`, a 64-bit round trip, a callback interface with - /// string, i32, record, and object parameters (one method returning - /// `bool`, one void), a function taking that callback, a C-style enum, - /// and a rich enum. - fn fixture() -> BindingModel { - let mut fetch = func( - "fetch", - vec![param("path", TypeRef::StringUtf8)], - Some(named("Store")), - ); - fetch.r#async = true; - let kv = Module { - name: "kv".into(), - doc: None, - functions: vec![ - func( - "maybe_store", - vec![param("store", TypeRef::Optional(Box::new(named("Store"))))], - Some(TypeRef::Optional(Box::new(named("Store")))), - ), - func( - "scan", - vec![], - Some(TypeRef::Iterator(Box::new(named("Store")))), - ), - func( - "subscribe", - vec![param("listener", named("Listener"))], - None, - ), - func( - "describe", - vec![param("bundle", named("Bundle"))], - Some(named("Bundle")), - ), - func( - "widen", - vec![param("n", TypeRef::I64), param("m", TypeRef::U64)], - Some(TypeRef::U64), - ), - fetch, - ], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store.".into()), - deprecated: None, - constructors: vec![func("new", vec![param("path", TypeRef::StringUtf8)], None)], - methods: vec![func( - "get", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - )], - statics: vec![], - }], - callback_interfaces: vec![CallbackInterfaceDef { - name: "Listener".into(), - doc: Some("Receives store events.".into()), - deprecated: None, - methods: vec![ - func( - "on_message", - vec![ - param("text", TypeRef::StringUtf8), - param("weight", TypeRef::I32), - ], - Some(TypeRef::Bool), - ), - func( - "on_bundle", - vec![ - param("bundle", named("Bundle")), - param("store", named("Store")), - param("alt", TypeRef::Optional(Box::new(named("Store")))), - ], - None, - ), - ], - }], - structs: vec![StructDef { - name: "Bundle".into(), - doc: None, - deprecated: None, - fields: vec![ - field("primary", named("Store")), - field("extras", TypeRef::List(Box::new(named("Store")))), - field("stamp", TypeRef::I64), - ], - }], - enums: vec![ - EnumDef { - name: "Mode".into(), - doc: None, - deprecated: None, - variants: vec![variant("Fast", 0, vec![]), variant("Safe", 7, vec![])], - }, - EnumDef { - name: "Shape".into(), - doc: None, - deprecated: None, - variants: vec![ - variant("Empty", 0, vec![]), - variant("Circle", 1, vec![field("radius", TypeRef::F64)]), - ], - }, - ], - errors: None, - modules: vec![], - }; - let api = ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![kv], - }); - BindingModel::build(&api, "weaveffi") - } - - fn index_js() -> String { - render_node_index(&fixture(), true, "weaveffi.yml") - } - - fn addon_c() -> String { - render_addon_c(&fixture(), true, "weaveffi.yml") - } - - fn dts() -> String { - render_node_dts(&fixture(), true, "weaveffi.yml") - } - - fn assert_has(src: &str, needle: &str) { - assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); - } - - fn assert_lacks(src: &str, needle: &str) { - assert!(!src.contains(needle), "unexpected `{needle}` in:\n{src}"); - } - - #[test] - fn addon_checks_abi_revision_and_binds_runtime_symbols() { - let c = addon_c(); - assert_has(&c, "WEAVEFFI_ABI_VERSION"); - assert_has(&c, "weaveffi_error_set("); - assert_has(&c, "weaveffi_error_clear(&err);"); - assert_lacks(&c, "arena"); - assert_lacks(&c, "weaveffi_handle_t"); - } - - #[test] - fn interface_wrapper_closes_exactly_once_and_exposes_clone() { - let js = index_js(); - assert_has(&js, "class Store {"); - assert_has( - &js, - "this._handle = __invoke(addon.Store_new, [path], __generic);", - ); - assert_has(&js, "Store._cleanup.register(this, this._handle, this);"); - assert_has(&js, "close() {"); - assert_has(&js, "Store._cleanup.unregister(this);"); - assert_has(&js, "addon.Store__destroy(this._handle);"); - assert_has(&js, "this._handle = 0n;"); - assert_has( - &js, - "Store.prototype[Symbol.dispose] = Store.prototype.close;", - ); - assert_has(&js, "Store._cleanup = new FinalizationRegistry("); - assert_has(&js, "return addon.Store__clone(__borrow(this, Store));"); - assert_has(&js, "Store._adopt = function (handle) {"); - assert_has( - &js, - "__invoke(addon.Store_get, [__borrow(this, Store), key], __generic)", - ); - - let c = addon_c(); - assert_has(&c, "static napi_value Napi_weaveffi_kv_Store_destroy("); - assert_has(&c, "weaveffi_kv_Store_destroy(self);"); - assert_has(&c, "static napi_value Napi_weaveffi_kv_Store_clone("); - assert_has( - &c, - "weaveffi_kv_Store* cloned = weaveffi_kv_Store_clone(self);", - ); - assert_has( - &c, - "{ \"Store__destroy\", NULL, Napi_weaveffi_kv_Store_destroy,", - ); - assert_has( - &c, - "{ \"Store__clone\", NULL, Napi_weaveffi_kv_Store_clone,", - ); - // Methods borrow the wrapped handle as the leading const pointer. - assert_has(&c, "(const weaveffi_kv_Store*)self_raw"); - - let d = dts(); - assert_has(&d, "export class Store {"); - assert_has(&d, "close(): void;"); - assert_has(&d, "[Symbol.dispose](): void;"); - assert_lacks(&d, "destroy(): void;"); - } - - #[test] - fn nullable_objects_map_to_wrapper_or_null() { - let js = index_js(); - assert_has( - &js, - "__invoke(addon.maybeStore, [(store == null ? null : __borrow(store, Store))], __generic)", - ); - assert_has(&js, "return (_r == null ? null : Store._adopt(_r));"); - - let c = addon_c(); - assert_has(&c, "weaveffi_napi_get_handle(env, args[0], &store_raw)"); - assert_has(&c, "(const weaveffi_kv_Store*)store_raw"); - assert_has(&c, "weaveffi_napi_make_handle(env, result, &ret);"); - assert_has(&c, "if (p == NULL) {\n return napi_get_null(env, out);"); - - let d = dts(); - assert_has( - &d, - "export function maybeStore(store: Store | null): Store | null", - ); - } - - #[test] - fn objects_inside_records_clone_on_write_and_adopt_on_read() { - let js = index_js(); - assert_has(&js, "function __packBundle(w, v) {"); - assert_has(&js, "w.u64(v.primary._cloneHandle());"); - assert_has( - &js, - "__wList(w, v.extras, (w, v) => w.u64(v._cloneHandle()));", - ); - assert_has(&js, "w.i64(v.stamp);"); - assert_has(&js, "primary: Store._adopt(r.u64()),"); - assert_has(&js, "extras: __rList(r, (r) => Store._adopt(r.u64())),"); - assert_has(&js, "stamp: r.i64(),"); - - let d = dts(); - assert_has(&d, "primary: Store;"); - assert_has(&d, "extras: Store[];"); - assert_has(&d, "stamp: bigint;"); - } - - #[test] - fn iterator_and_async_object_results_are_adopted() { - let js = index_js(); - assert_has( - &js, - "new WeaveFFIIterator(_it, addon.scan_iterNext, addon.scan_iterDestroy, __generic, (_e) => Store._adopt(_e));", - ); - assert_has( - &js, - "__invokeAsync(addon.fetch, [path], __generic).then((_r) => Store._adopt(_r));", - ); - - let c = addon_c(); - assert_has(&c, "weaveffi_napi_make_handle(env, iter_item, &ret);"); - assert_has(&c, "weaveffi_napi_make_handle(env, ctx->result, &val);"); - - let d = dts(); - assert_has(&d, "export function scan(): IterableIterator"); - assert_has(&d, "export function fetch(path: string): Promise"); - } - - #[test] - fn sixty_four_bit_integers_are_bigints() { - let c = addon_c(); - assert_has(&c, "napi_get_value_bigint_int64(env, v, out, &lossless)"); - assert_has(&c, "napi_get_value_bigint_uint64(env, v, out, &lossless)"); - assert_has( - &c, - "napi_throw_range_error(env, NULL, \"bigint does not fit", - ); - assert_has(&c, "arg_status = weaveffi_napi_get_i64(env, args[0], &n);"); - assert_has(&c, "arg_status = weaveffi_napi_get_u64(env, args[1], &m);"); - assert_has(&c, "napi_create_bigint_uint64(env, result, &ret);"); - assert_lacks(&c, "napi_create_int64("); - assert_lacks(&c, "napi_get_value_int64(env, args"); - - let d = dts(); - assert_has(&d, "export function widen(n: bigint, m: bigint): bigint"); - } - - #[test] - fn plain_enums_have_a_runtime_value_matching_their_declaration() { - let js = index_js(); - // The `.d.ts` promises `export enum Mode`, so `index.js` must export - // the value, with TypeScript's forward and reverse mappings. - assert_has( - &js, - "wv.Mode = Object.freeze({\n Fast: 0,\n Safe: 7,\n 0: 'Fast',\n 7: 'Safe',\n});", - ); - // A rich enum is a tagged union: pack/unpack functions, no value. - assert_has(&js, "function __packShape(w, v) {"); - assert_lacks(&js, "wv.Shape ="); - - let d = dts(); - assert_has(&d, "export enum Mode {\n Fast = 0,\n Safe = 7,\n}"); - assert_has(&d, "export type Shape ="); - } - - #[test] - fn callback_interface_renders_ts_interface_and_js_adapter() { - let d = dts(); - assert_has(&d, "export interface Listener {"); - assert_has(&d, "onMessage(text: string, weight: number): boolean;"); - assert_has( - &d, - "onBundle(bundle: Bundle, store: Store, alt: Store | null): void;", - ); - assert_has(&d, "export function subscribe(listener: Listener): void"); - - let mut via_trait = String::new(); - let model = fixture(); - let m = &model.modules[0]; - NodeGenerator.render_callback_interface( - &mut via_trait, - m, - &m.callback_interfaces[0], - &NodeConfig::default(), - ); - assert_has(&via_trait, "export interface Listener {"); - - let js = index_js(); - assert_has(&js, "function __adaptListener(impl) {"); - assert_has(&js, "on_message(text, weight) {"); - assert_has(&js, "return impl.onMessage(text, weight);"); - assert_has(&js, "on_bundle(bundle, store, alt) {"); - assert_has( - &js, - "return impl.onBundle(__decode(__unpackBundle, bundle), Store._adopt(store), (alt == null ? null : Store._adopt(alt)));", - ); - assert_has( - &js, - "__invoke(addon.subscribe, [__adaptListener(listener)], __generic)", - ); - } - - #[test] - fn callback_interface_renders_static_vtable_and_thread_hopping_trampolines() { - let c = addon_c(); - // Handle table entry, thread identity, and the hop machinery. - assert_has(&c, "#include "); - assert_has(&c, "weaveffi_napi_js_thread = uv_thread_self();"); - assert_has(&c, "napi_create_reference(env, target, 1, &ctx->ref);"); - assert_has(&c, "napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, dispatch, &ctx->tsfn);"); - assert_has(&c, "uv_cond_wait(&req->cv, &req->mu);"); - // Trampolines match the vtable signatures and route by thread. - assert_has( - &c, - "static bool weaveffi_kv_Listener_on_message_tramp(void* ctx, const char* text, int32_t weight, weaveffi_error* out_err) {", - ); - assert_has( - &c, - "static void weaveffi_kv_Listener_on_bundle_tramp(void* ctx, const uint8_t* bundle_ptr, size_t bundle_len, weaveffi_kv_Store* store, weaveffi_kv_Store* alt, weaveffi_error* out_err) {", - ); - assert_has(&c, "if (weaveffi_napi_on_js_thread()) {"); - assert_has(&c, "weaveffi_napi_cb_hop(&req);"); - // The invoker looks the method up by IDL name, borrows buffers, and - // hands object references over as handles. - assert_has( - &c, - "napi_get_named_property(env, target, \"on_bundle\", &fn);", - ); - assert_has( - &c, - "napi_create_buffer_copy(env, f->bundle_len, f->bundle_ptr ?", - ); - assert_has(&c, "weaveffi_napi_make_handle(env, f->store, &argv[1]);"); - assert_has(&c, "weaveffi_napi_make_handle(env, f->alt, &argv[2]);"); - assert_has(&c, "napi_get_value_bool(env, result, &f->result);"); - // Failure path: a JS exception becomes a foreign error, never an unwind. - assert_has( - &c, - "weaveffi_error_set(out_err, -4, msg[0] ? msg : fallback);", - ); - assert_has( - &c, - "weaveffi_napi_cb_report(env, f->hdr.out_err, \"Listener.on_message threw\");", - ); - // One static vtable per interface, methods in order then `free`. - assert_has( - &c, - "static const weaveffi_kv_Listener_vtable weaveffi_kv_Listener_napi_vtable = { weaveffi_kv_Listener_on_message_tramp, weaveffi_kv_Listener_on_bundle_tramp, weaveffi_napi_cb_free };", - ); - // The entry point registers the adapter and passes ctx plus vtable. - assert_has( - &c, - "listener_ctx = weaveffi_napi_cb_register(env, args[0], \"weaveffi_kv_Listener\", weaveffi_kv_Listener_napi_dispatch);", - ); - assert_has( - &c, - "weaveffi_kv_subscribe((void*)listener_ctx, &weaveffi_kv_Listener_napi_vtable, &err);", - ); - } - - #[test] - fn package_skips_platforms_node_does_not_run_on() { - assert_eq!( - node_platform_tokens(Platform::MacosArm64), - Some(("darwin", "arm64")) - ); - assert_eq!(node_platform_tokens(Platform::AndroidArm64), None); - assert_eq!(node_platform_tokens(Platform::Wasm32), None); - } - - #[test] - fn legacy_features_are_gone() { - for src in [index_js(), dts(), addon_c()] { - assert_lacks(&src, "_napi_listener"); - assert_lacks(&src, "weaveffi_handle_t"); - assert_lacks(&src, "arena"); - assert_lacks(&src, "destroy()"); - } - } -} diff --git a/crates/weaveffi-gen-node/src/package.rs b/crates/weaveffi-gen-node/src/package.rs deleted file mode 100644 index 46e06697..00000000 --- a/crates/weaveffi-gen-node/src/package.rs +++ /dev/null @@ -1,486 +0,0 @@ -//! Packaging output: `package.json` and `binding.gyp` manifests (dev and -//! packaged layouts), the packaged README, and the `types.d.ts` assembly. -//! -//! JSON manifests are built with [`manifest::JsonObject`] so quoting and -//! escaping are centralized; gyp's conventional inline spellings (one-line -//! arrays and condition rows) are spliced as [`JsonValue::Raw`] fragments to -//! keep the rendered files byte-identical to the established output. - -use heck::ToLowerCamelCase; -use weaveffi_core::cache::CLI_VERSION; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::{type_name as error_type_name, ERROR_BRAND}; -use weaveffi_core::manifest::{json_escape, JsonObject, JsonValue}; -use weaveffi_core::model::{ - BindingModel, CallbackInterfaceBinding, EnumBinding, ErrorBinding, InterfaceBinding, -}; -use weaveffi_core::package::PackageContext; -use weaveffi_core::pkg::ResolvedPackage; -use weaveffi_core::platform::Platform; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::types::{ - emit_doc, emit_fn_doc, js_fn_name, js_method_name, js_param_name, ts_fn_tags, ts_params, - ts_ret, ts_type_for, -}; - -/// Prepend the generated-file prelude entries (`//`, `//warning`, -/// `//regenerate`) npm tolerates at the top of `package.json`. Mirrors -/// [`render_json_prelude`](weaveffi_core::utils::render_json_prelude), whose -/// version constant ([`CLI_VERSION`]) it shares. -fn json_prelude_entries(obj: JsonObject, input_basename: &str) -> JsonObject { - obj.str_entry( - "//", - format!("Generated by WeaveFFI {CLI_VERSION} from {input_basename}"), - ) - .str_entry( - "//warning", - "DO NOT EDIT. Your changes will be overwritten.", - ) - .str_entry( - "//regenerate", - format!("To regenerate: weaveffi generate {input_basename} -o "), - ) -} - -/// Append the optional npm metadata fields shared by the dev and packaged -/// `package.json` layouts. npm's `repository` shorthand keeps its inline -/// object spelling, with the URL escaped. -fn npm_metadata_entries(obj: JsonObject, package: &ResolvedPackage) -> JsonObject { - let mut obj = obj - .str_entry("name", &package.name) - .str_entry("version", &package.version) - .str_entry("description", package.description_or_default()) - .opt_str_entry("license", package.license.as_deref()); - if let Some(author) = package.authors.first() { - obj = obj.str_entry("author", author); - } - obj = obj.opt_str_entry("homepage", package.homepage.as_deref()); - if let Some(repository) = &package.repository { - obj = obj.entry( - "repository", - JsonValue::Raw(format!( - "{{ \"type\": \"git\", \"url\": \"{}\" }}", - json_escape(repository) - )), - ); - } - obj -} - -/// Append the fixed addon fields: entry points, the gyp flag, and the -/// install-time rebuild script. -fn addon_entries(obj: JsonObject) -> JsonObject { - obj.str_entry("main", "index.js") - .str_entry("types", "types.d.ts") - .entry("gypfile", JsonValue::Bool(true)) - .entry( - "scripts", - JsonValue::Object(JsonObject::new().str_entry("install", "node-gyp rebuild")), - ) -} - -/// Render the dev-layout `package.json`. -pub(crate) fn render_package_json(package: &ResolvedPackage, input_basename: &str) -> String { - let obj = json_prelude_entries(JsonObject::new(), input_basename); - addon_entries(npm_metadata_entries(obj, package)).render() -} - -/// Render the main package's `package.json` with `optionalDependencies` on the -/// per-platform native packages. -pub(crate) fn render_packaged_package_json( - package: &ResolvedPackage, - platform_pkgs: &[(Platform, String)], - input_basename: &str, -) -> String { - let obj = json_prelude_entries(JsonObject::new(), input_basename); - let mut deps = JsonObject::new(); - for (_, pkg_name) in platform_pkgs { - deps = deps.str_entry(pkg_name, &package.version); - } - addon_entries(npm_metadata_entries(obj, package)) - .entry("optionalDependencies", JsonValue::Object(deps)) - .render() -} - -/// The npm `os`/`cpu` pair of a platform, or `None` for a platform Node does -/// not run on (Android, Wasm), which the packaged layout skips. -pub(crate) fn node_platform_tokens(platform: Platform) -> Option<(&'static str, &'static str)> { - Some((platform.node_os()?, platform.node_cpu()?)) -} - -/// Render a per-platform native package's `package.json`, gated by npm `os` and -/// `cpu` so npm installs only the matching one. The single-element `os` and -/// `cpu` arrays keep npm's conventional inline spelling. -pub(crate) fn render_platform_package_json( - pkg_name: &str, - version: &str, - (os, cpu): (&str, &str), -) -> String { - JsonObject::new() - .str_entry("name", pkg_name) - .str_entry("version", version) - .str_entry( - "description", - format!("Prebuilt WeaveFFI native library for {os}/{cpu}"), - ) - .entry("os", JsonValue::Raw(format!("[\"{os}\"]"))) - .entry("cpu", JsonValue::Raw(format!("[\"{cpu}\"]"))) - .render() -} - -/// The shared gyp target skeleton of both `binding.gyp` layouts. Source and -/// include lists keep gyp's conventional one-line array spelling. -fn gyp_target_base() -> JsonObject { - JsonObject::new() - .str_entry("target_name", "weaveffi") - .entry( - "sources", - JsonValue::Raw("[\"weaveffi_addon.c\"]".to_string()), - ) - .entry("include_dirs", JsonValue::Raw("[\"../c\"]".to_string())) -} - -/// Render one `binding.gyp` document: hash-comment prelude, the object with -/// its `targets` list, a blank separator line, and the trailer. -fn render_gyp_document(body: JsonObject, input_basename: &str) -> String { - let mut out = render_prelude(CommentStyle::Hash, input_basename); - out.push_str(&body.render()); - out.push('\n'); - out.push_str(&render_trailer(CommentStyle::Hash, "binding.gyp")); - out -} - -/// Render the dev-layout `binding.gyp`: compile the addon against the -/// generated C header and link the locally built `weaveffi` library. -pub(crate) fn render_binding_gyp(input_basename: &str) -> String { - let target = - gyp_target_base().entry("libraries", JsonValue::Raw("[\"-lweaveffi\"]".to_string())); - let body = - JsonObject::new().entry("targets", JsonValue::Array(vec![JsonValue::Object(target)])); - render_gyp_document(body, input_basename) -} - -/// Render the packaged `binding.gyp`: it links the prebuilt library resolved -/// from the installed per-platform package (selected by npm `os`/`cpu`) and -/// sets an rpath so the addon finds it at runtime. The ` String { - let resolve = format!( - " String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = &package.name; - let platforms: Vec = ctx - .binaries - .platforms() - .filter_map(node_platform_tokens) - .map(|(os, cpu)| format!("- `{name}-{os}-{cpu}`")) - .collect(); - let platform_list = platforms.join("\n"); - format!( - r#"{prelude}# {name} (Node.js) - -Auto-generated N-API bindings. The prebuilt native library is published as a set -of per-platform packages and selected automatically through -`optionalDependencies` (npm installs only the package matching the host -`os`/`cpu`): - -{platform_list} - -The thin N-API addon is compiled at install time (`node-gyp rebuild`) and links -the prebuilt library from the selected platform package, so no Rust toolchain is -needed. A C compiler and the generated C header (`../c`) are required to build -the addon. - -{trailer}"#, - ) -} - -/// `.d.ts` for a rich (algebraic) enum: a discriminated union with a string -/// `tag` property naming the active variant, plus the variant's fields. -/// Mirrors the tagged-union objects the loader's unpack function produces. -fn render_rich_enum_dts(out: &mut String, e: &EnumBinding) { - emit_doc(out, &e.doc, ""); - out.push_str(&format!("export type {} =", e.name)); - for v in &e.variants { - out.push('\n'); - if v.doc.is_some() { - let mut d = String::new(); - emit_doc(&mut d, &v.doc, " "); - out.push_str(&d); - } - let fields: String = v - .fields - .iter() - .map(|f| format!("; {}: {}", f.name, ts_type_for(&f.ty))) - .collect(); - out.push_str(&format!(" | {{ tag: '{}'{fields} }}", v.name)); - } - out.push_str(";\n"); -} - -/// `.d.ts` for one declaring module's error surface: the domain class -/// extending the generic brand plus one subclass per code carrying its -/// stable `CODE` and, for codes with structured payloads, the decoded -/// payload fields as readonly properties. -fn render_error_dts(out: &mut String, eb: &ErrorBinding) { - let domain = &eb.type_name; - out.push_str(&format!( - "/** Typed errors reported by the `{}` module's throwing functions. */\n", - eb.owner_path - )); - out.push_str(&format!("export class {domain} extends {ERROR_BRAND} {{\n")); - out.push_str(" constructor(code: number, message: string);\n"); - out.push_str("}\n"); - for c in &eb.codes { - let class = error_type_name(&c.name, "Error"); - emit_doc(out, &c.doc, ""); - out.push_str(&format!("export class {class} extends {domain} {{\n")); - out.push_str(&format!(" static readonly CODE: {};\n", c.value)); - for field in &c.fields { - emit_doc(out, &field.doc, " "); - out.push_str(&format!( - " readonly {}: {};\n", - field.name, - ts_type_for(&field.ty) - )); - } - out.push_str(" constructor(message?: string);\n"); - out.push_str("}\n"); - } -} - -/// `.d.ts` for one interface: a class whose canonical `new` constructor, -/// static factories, methods, and statics mirror the JS class in -/// [`render_interface_class_js`](crate::entities). -fn render_interface_dts(out: &mut String, i: &InterfaceBinding, error: Option<&ErrorBinding>) { - let name = &i.name; - let mut w = CodeWriter::two_space(); - { - let mut d = String::new(); - emit_doc(&mut d, &i.doc, ""); - w.raw(d); - } - w.block(format!("export class {name} {{"), "}", |w| { - let canonical = i - .constructors - .iter() - .find(|c| c.name == "new" && !c.is_async); - if let Some(c) = canonical { - let mut d = String::new(); - emit_fn_doc(&mut d, &c.doc, &c.params, " ", &ts_fn_tags(c, error)); - w.raw(d); - w.line(format!("constructor({});", ts_params(c))); - } - for c in &i.constructors { - if canonical.is_some_and(|canon| std::ptr::eq(canon, c)) { - continue; - } - let mut d = String::new(); - emit_fn_doc(&mut d, &c.doc, &c.params, " ", &ts_fn_tags(c, error)); - w.raw(d); - let ret = if c.is_async { - format!("Promise<{name}>") - } else { - name.to_string() - }; - w.line(format!( - "static {}({}): {ret};", - c.name.to_lower_camel_case(), - ts_params(c) - )); - } - for f in &i.methods { - let mut d = String::new(); - emit_fn_doc(&mut d, &f.doc, &f.params, " ", &ts_fn_tags(f, error)); - w.raw(d); - w.line(format!( - "{}({}): {};", - f.name.to_lower_camel_case(), - ts_params(f), - ts_ret(f) - )); - } - for f in &i.statics { - let mut d = String::new(); - emit_fn_doc(&mut d, &f.doc, &f.params, " ", &ts_fn_tags(f, error)); - w.raw(d); - w.line(format!( - "static {}({}): {};", - f.name.to_lower_camel_case(), - ts_params(f), - ts_ret(f) - )); - } - w.line("/**"); - w.line(" * Release this wrapper's reference to the native object. Safe to call"); - w.line(" * more than once; a wrapper that is never closed is released when it is"); - w.line(" * garbage collected. Using the wrapper after `close()` throws."); - w.line(" */"); - w.line("close(): void;"); - w.line("/** Alias of `close()` for `using` declarations. */"); - w.line("[Symbol.dispose](): void;"); - }); - out.push_str(&w.finish()); -} - -/// `.d.ts` for one callback interface: a TS `interface` with one camelCase -/// method per IDL method. Any object with those methods (a class instance, a -/// literal) can be passed wherever the interface is expected; 64-bit -/// integers arrive as `bigint`s and object arguments as wrapper instances -/// the implementation owns (and should `close()` when done). -pub(crate) fn render_callback_interface_dts(out: &mut String, cb: &CallbackInterfaceBinding) { - let mut w = CodeWriter::two_space(); - { - let mut d = String::new(); - emit_doc(&mut d, &cb.doc, ""); - w.raw(d); - } - w.block(format!("export interface {} {{", cb.name), "}", |w| { - for m in &cb.methods { - let mut d = String::new(); - emit_fn_doc(&mut d, &m.doc, &m.params, " ", &[]); - w.raw(d); - let params: Vec = m - .params - .iter() - .map(|p| format!("{}: {}", js_param_name(&p.name), ts_type_for(&p.ty))) - .collect(); - let ret = m.ret.as_ref().map_or("void".to_string(), ts_type_for); - w.line(format!( - "{}({}): {ret};", - js_method_name(&m.name), - params.join(", ") - )); - } - }); - out.push_str(&w.finish()); -} - -/// Render the TypeScript declarations (`types.d.ts`): the generic error -/// brand, then per module the typed error surface, record interfaces, enums -/// (numeric or tagged-union), callback interfaces, interface classes, and one -/// function signature per exported wrapper. -pub(crate) fn render_node_dts( - model: &BindingModel, - strip_module_prefix: bool, - input_basename: &str, -) -> String { - let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); - out.push_str("// Generated types for WeaveFFI functions\n"); - out.push_str("/**\n"); - out.push_str(" * Base class of every error thrown by these bindings. Non-throwing\n"); - out.push_str(" * functions reject or throw it directly for runtime traps (code -2 a\n"); - out.push_str(" * producer panic, -3 a marshalling failure, -4 a callback interface\n"); - out.push_str(" * implementation that threw); throwing functions surface a module\n"); - out.push_str(" * domain subclass for their declared codes.\n"); - out.push_str(" */\n"); - out.push_str(&format!("export class {ERROR_BRAND} extends Error {{\n")); - out.push_str(" /** The numeric ABI error code. */\n"); - out.push_str(" code: number;\n"); - out.push_str(" /** The raw producer message, without the code prefix. */\n"); - out.push_str(" errorMessage: string;\n"); - out.push_str(" constructor(code: number, message: string);\n"); - out.push_str("}\n"); - for m in &model.modules { - if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { - render_error_dts(&mut out, eb); - } - // Records are plain value types: an interface with typed fields, no - // handle wrapping, no builders, no destroy. - for s in &m.structs { - emit_doc(&mut out, &s.doc, ""); - out.push_str(&format!("export interface {} {{\n", s.name)); - for field in &s.fields { - emit_doc(&mut out, &field.doc, " "); - out.push_str(&format!(" {}: {};\n", field.name, ts_type_for(&field.ty))); - } - out.push_str("}\n"); - } - for e in &m.enums { - // A rich (algebraic) enum is a tagged union, not a plain numeric - // `enum`. - if e.is_rich() { - render_rich_enum_dts(&mut out, e); - continue; - } - emit_doc(&mut out, &e.doc, ""); - out.push_str(&format!("export enum {} {{\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"); - } - out.push_str(&format!("// module {}\n", m.path)); - for cb in &m.callback_interfaces { - render_callback_interface_dts(&mut out, cb); - } - for i in &m.interfaces { - render_interface_dts(&mut out, i, m.error.as_ref()); - } - for f in &m.functions { - let ts_name = js_fn_name(&m.path, &f.name, strip_module_prefix); - emit_fn_doc( - &mut out, - &f.doc, - &f.params, - "", - &ts_fn_tags(f, m.error.as_ref()), - ); - out.push_str(&format!( - "export function {}({}): {}\n", - ts_name, - ts_params(f), - ts_ret(f) - )); - } - } - out.push('\n'); - out.push_str(&render_trailer(CommentStyle::DoubleSlash, "types.d.ts")); - out -} diff --git a/crates/weaveffi-gen-node/src/runtime.rs b/crates/weaveffi-gen-node/src/runtime.rs deleted file mode 100644 index c9b22d96..00000000 --- a/crates/weaveffi-gen-node/src/runtime.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! The emitted JS runtime prelude: addon loading, the generic error brand -//! and invoke helpers, the object borrow helper, the private value-buffer -//! reader/writer, and the shared lazy iterator class. -//! -//! Everything here is fixed text (or near-fixed text) embedded at the top of -//! `index.js`; the per-entity wrappers in [`crate::entities`] call into it. - -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::ERROR_BRAND; - -/// The private buffer writer/reader runtime embedded in `index.js` whenever -/// the model uses value buffers. Little-endian, packed, no alignment; decode -/// failures throw the generic error brand (a malformed buffer is a contract -/// violation, not a typed domain error). The 64-bit methods take and return -/// `bigint`s (the writers also accept numbers and decimal strings, which is -/// what map keys arrive as). -pub(crate) const BUFFER_RUNTIME_JS: &str = r#"// --- 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; -} - -"#; - -/// Emit the addon-loading preamble of `index.js`: resolve the built `.node` -/// file (env override, node-gyp output, prebuilt fallback) and re-export -/// every native binding onto the wrapper namespace `wv`. -pub(crate) fn render_loader_js(out: &mut String) { - out.push_str( - "// The WEAVEFFI_ADDON environment variable overrides the addon location\n\ - // (an absolute path to the built .node file); otherwise prefer the\n\ - // default node-gyp output path and fall back to a prebuilt index.node\n\ - // placed next to this file.\n\ - let addon;\n\ - if (process.env.WEAVEFFI_ADDON) {\n addon = require(process.env.WEAVEFFI_ADDON);\n} else {\n try {\n addon = require('./build/Release/weaveffi.node');\n } catch (e) {\n addon = require('./index.node');\n }\n}\n", - ); - - // The native bindings are defined as non-enumerable properties, so copy - // them by explicit own-name lookup before layering the idiomatic wrappers. - out.push_str( - "\n// Re-export every native binding, then layer the idiomatic wrappers\n\ - // (error classes, interface classes, buffer pack/unpack, function\n\ - // wrappers) on top.\n\ - const wv = {};\n\ - for (const _name of Object.getOwnPropertyNames(addon)) {\n wv[_name] = addon[_name];\n}\n\n", - ); -} - -/// Emit the generic error brand and the shared invoke helpers. Every wrapper -/// funnels addon failures (JS errors carrying the numeric ABI `code` and, for -/// structured errors, the raw `payload` buffer) through a mapping factory: -/// the module domain's for throwing callables, the generic constructor -/// otherwise. -pub(crate) fn render_error_brand_js(out: &mut String) { - out.push_str(&format!( - "class {ERROR_BRAND} extends Error {{\n \ - constructor(code, message) {{\n \ - super('(' + code + ') ' + (message || ''));\n \ - this.name = '{ERROR_BRAND}';\n \ - this.code = code;\n \ - this.errorMessage = message || '';\n \ - }}\n\ - }}\n\ - wv.{ERROR_BRAND} = {ERROR_BRAND};\n\ - function __generic(code, message) {{\n \ - return new {ERROR_BRAND}(code, message);\n\ - }}\n\ - function __rebrand(e, map) {{\n \ - return e && typeof e.code === 'number' ? map(e.code, e.message, e.payload) : e;\n\ - }}\n\ - function __invoke(fn, args, map) {{\n \ - try {{\n \ - return fn.apply(null, args);\n \ - }} catch (e) {{\n \ - throw __rebrand(e, map);\n \ - }}\n\ - }}\n\ - function __invokeAsync(fn, args, map) {{\n \ - return fn.apply(null, args).catch((e) => {{\n \ - throw __rebrand(e, map);\n \ - }});\n\ - }}\n\n" - )); -} - -/// Emit the shared object helper every interface wrapper and every -/// object-typed argument routes through. `__borrow` unwraps a live wrapper -/// to the handle the callee borrows for the call (the wrapper keeps its own -/// reference); a closed wrapper or a foreign value is a programming error -/// reported through the generic brand with the marshalling code. -pub(crate) fn render_object_helpers_js(out: &mut String) { - out.push_str(&format!( - "// Borrow a live object wrapper's native handle for one call. The wrapper\n\ - // keeps its own reference; the producer clones if it retains the object.\n\ - function __borrow(o, cls) {{\n \ - if (!(o instanceof cls)) {{\n \ - throw new {ERROR_BRAND}(-3, 'expected an instance of ' + cls.name);\n \ - }}\n \ - if (!o._handle) {{\n \ - throw new {ERROR_BRAND}(-3, cls.name + ' used after close()');\n \ - }}\n \ - return o._handle;\n\ - }}\n\n" - )); -} - -/// Emit the shared lazy iterator class the JS loader hands out for every -/// `iter` callable. It implements the iterator protocol over the addon's -/// per-iterator `next`/`destroy` entry points: one native pull per `next()`, -/// eager release on exhaustion (the addon destroys the handle when the -/// producer reports done), and `return()` releases the handle on early exit -/// so `for...of` breaks clean up deterministically. Abandoned iterators are -/// reclaimed by the external's native finalizer. -pub(crate) fn render_iterator_class_js(out: &mut String) { - let mut w = CodeWriter::two_space(); - w.raw( - "// Lazy iterator over a native producer: one native `next` per step.\n\ - // The native handle is released on exhaustion, by `return()` on early\n\ - // exit, or by the external's finalizer if the iterator is abandoned.\n", - ); - w.block("class WeaveFFIIterator {", "}", |w| { - w.block( - "constructor(ext, nextFn, destroyFn, map, wrapElem) {", - "}", - |w| { - w.line("this._ext = ext;"); - w.line("this._nextFn = nextFn;"); - w.line("this._destroyFn = destroyFn;"); - w.line("this._map = map;"); - w.line("this._wrapElem = wrapElem;"); - w.line("this._done = false;"); - }, - ); - w.block("next() {", "}", |w| { - w.block("if (this._done) {", "}", |w| { - w.line("return { done: true, value: undefined };"); - }); - w.line("const _v = __invoke(this._nextFn, [this._ext], this._map);"); - w.block("if (_v === undefined) {", "}", |w| { - w.line("this._done = true;"); - w.line("return { done: true, value: undefined };"); - }); - w.line("return { done: false, value: this._wrapElem ? this._wrapElem(_v) : _v };"); - }); - w.block("return(value) {", "}", |w| { - w.block("if (!this._done) {", "}", |w| { - w.line("this._done = true;"); - w.line("this._destroyFn(this._ext);"); - }); - w.line("return { done: true, value };"); - }); - w.block("[Symbol.iterator]() {", "}", |w| { - w.line("return this;"); - }); - }); - w.blank(); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-node/src/types.rs b/crates/weaveffi-gen-node/src/types.rs deleted file mode 100644 index 01e0b800..00000000 --- a/crates/weaveffi-gen-node/src/types.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! JS/TS naming, type mapping, and doc-comment emission. -//! -//! Everything here answers "how is this IDL name or type spelled in -//! JavaScript and TypeScript?": exported function names, escaped parameter -//! identifiers, TS type annotations, and the JSDoc blocks both emitted files -//! share. - -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}; - -/// The exported JS name of a free function: [`wrapper_name`] -/// (module-prefixed or stripped per config) converted to lowerCamelCase, so -/// module `kv`'s `open_store` exports as `openStore` (stripped, the default) -/// or `kvOpenStore`. -pub(crate) fn js_fn_name(module: &str, func: &str, strip: bool) -> String { - wrapper_name(module, func, strip).to_lower_camel_case() -} - -/// The camelCase JS spelling of a callback-interface method as the consumer -/// implements it (`on_message` becomes `onMessage`). The addon looks methods -/// up on the generated adapter object by their raw IDL name, so only the -/// consumer-facing spelling routes through here. -pub(crate) fn js_method_name(name: &str) -> String { - name.to_lower_camel_case() -} - -/// The JS name of the adapter that bridges a consumer's callback-interface -/// implementation to the raw values the addon delivers (`__adaptListener`). -/// Cross-module references (`events.Listener`) use the local type name, the -/// same flat namespace the interface classes share. -pub(crate) fn js_adapter_name(cb_name: &str) -> String { - format!("__adapt{}", local_type_name(cb_name)) -} - -/// The addon-internal JS export of one interface's lifecycle entry point -/// (`{Interface}__clone` or `{Interface}__destroy`). The double underscore -/// keeps these clear of any user member named `clone` or `destroy`. -pub(crate) fn iface_lifecycle_base(iface: &str, op: &str) -> String { - format!("{iface}__{op}") -} - -/// The camelCase JS spelling of an IDL parameter name, escaped when the -/// camel-cased form collides with a JS/TS reserved word (a parameter named -/// `import` becomes `import_`; property and method positions never need the -/// escape, so only parameters route through here). -pub(crate) fn js_param_name(name: &str) -> String { - lang::escape_ident(&name.to_lower_camel_case(), lang::JS_KEYWORDS) -} - -/// The addon-internal JS export base of an interface member -/// (`{Interface}_{member}`). These names are wiring between the addon and the -/// generated classes, not public API, so they keep the raw member spelling. -pub(crate) fn iface_member_base(iface: &str, member: &str) -> String { - format!("{iface}_{member}") -} - -/// Escape a string for embedding in a single-quoted JS literal. -pub(crate) fn js_str_literal(s: &str) -> String { - s.replace('\\', "\\\\") - .replace('\'', "\\'") - .replace('\n', "\\n") -} - -/// The TS annotation of one IR type. 64-bit integers are `bigint` everywhere -/// (parameters, returns, record fields, callback arguments) so no value above -/// 2^53 is silently rounded. -pub(crate) fn ts_type_for(ty: &Ty) -> String { - match ty { - Ty::I8 | Ty::I16 | Ty::U8 | Ty::U16 | Ty::I32 | Ty::U32 | Ty::F32 | Ty::F64 => { - "number".into() - } - Ty::I64 | Ty::U64 => "bigint".into(), - Ty::Bool => "boolean".into(), - Ty::StringUtf8 => "string".into(), - Ty::Bytes => "Buffer".into(), - // Records, rich enums, plain enums, interfaces, and callback - // interfaces surface as bare local TS names. A cross-module reference - // (e.g. `kv.Store`) must annotate the *local* type `Store`; the - // qualified IR name is not a declared TS type in this module. - Ty::Record(name) - | Ty::RichEnum(name) - | Ty::Interface(name) - | Ty::CallbackInterface(name) - | 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(), Ty::Optional(_)) { - format!("({inner_ts})[]") - } else { - format!("{inner_ts}[]") - } - } - Ty::Map(k, v) => format!("Record<{}, {}>", ts_map_key_type(k), ts_type_for(v)), - // `iter` is a lazy pull stream, not a materialized array. - Ty::Iterator(inner) => { - let t = ts_type_for(inner); - format!("IterableIterator<{t}>") - } - } -} - -/// The TS key type of a map. JS object keys are always strings at runtime; -/// `Record` and `Record` are the idiomatic spellings TS -/// accepts for numeric keys, but `Record` is not a valid TS type, -/// so 64-bit keys are annotated as their decimal string form (which is also -/// what `Object.keys` yields after decoding). -fn ts_map_key_type(ty: &Ty) -> String { - match ty { - Ty::I64 | Ty::U64 => "string".into(), - other => ts_type_for(other), - } -} - -/// Emits a JSDoc comment at `indent`. Single-line docs collapse to -/// `/** text */`; multi-line docs expand to a block with ` * ` prefixed lines. -pub(crate) fn emit_doc(out: &mut String, doc: &Option, indent: &str) { - common_emit_doc(out, doc, indent, DocCommentStyle::Javadoc); -} - -/// Emits a JSDoc block for a function: function doc, `@param name desc` for -/// each documented parameter, and an optional trailing tag list. -pub(crate) fn emit_fn_doc( - out: &mut String, - doc: &Option, - params: &[ParamBinding], - indent: &str, - extra_tags: &[String], -) { - let has_param_docs = params.iter().any(|p| p.doc.is_some()); - let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty()); - if trimmed_doc.is_none() && !has_param_docs && extra_tags.is_empty() { - return; - } - out.push_str(indent); - out.push_str("/**\n"); - if let Some(d) = trimmed_doc { - for line in d.lines() { - out.push_str(indent); - if line.is_empty() { - out.push_str(" *\n"); - } else { - out.push_str(" * "); - out.push_str(line); - out.push('\n'); - } - } - } - for p in params { - if let Some(pdoc) = &p.doc { - let pdoc = pdoc.trim(); - if pdoc.is_empty() { - continue; - } - let mut lines = pdoc.lines(); - if let Some(first) = lines.next() { - out.push_str(indent); - out.push_str(&format!(" * @param {} {}\n", js_param_name(&p.name), first)); - } - for line in lines { - out.push_str(indent); - if line.is_empty() { - out.push_str(" *\n"); - } else { - out.push_str(" * "); - out.push_str(line); - out.push('\n'); - } - } - } - } - for tag in extra_tags { - out.push_str(indent); - out.push_str(" * "); - out.push_str(tag); - out.push('\n'); - } - out.push_str(indent); - out.push_str(" */\n"); -} - -/// The TS parameter list of a callable, camel-cased. -pub(crate) fn ts_params(f: &FnBinding) -> String { - f.params - .iter() - .map(|p| format!("{}: {}", js_param_name(&p.name), ts_type_for(&p.ty))) - .collect::>() - .join(", ") -} - -/// The TS return annotation of a callable (`Promise`-wrapped when async). -pub(crate) fn ts_ret(f: &FnBinding) -> String { - let base = match &f.ret { - Some(ty) => ts_type_for(ty), - None => "void".into(), - }; - if f.is_async { - format!("Promise<{base}>") - } else { - base - } -} - -/// The standard JSDoc tag list of a callable: the C mapping, a `@throws` tag -/// naming the module's domain class for throwing callables, and any -/// deprecation notice. -pub(crate) fn ts_fn_tags(f: &FnBinding, error: Option<&ErrorBinding>) -> Vec { - let mut tags = vec![format!("Maps to C function: {}", f.c_base)]; - if let (true, Some(eb)) = (f.throws, error) { - tags.push(format!("@throws {{{}}}", eb.type_name)); - } - if let Some(msg) = &f.deprecated { - tags.push(format!("@deprecated {}", msg)); - } - tags -} diff --git a/crates/weaveffi-gen-python/Cargo.toml b/crates/weaveffi-gen-python/Cargo.toml deleted file mode 100644 index c4cbbace..00000000 --- a/crates/weaveffi-gen-python/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "weaveffi-gen-python" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "WeaveFFI Python ctypes binding generator" -keywords = ["ffi", "codegen", "bindings", "python", "ctypes"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-python/src/calls.rs b/crates/weaveffi-gen-python/src/calls.rs deleted file mode 100644 index 4a865f0a..00000000 --- a/crates/weaveffi-gen-python/src/calls.rs +++ /dev/null @@ -1,1155 +0,0 @@ -//! Call rendering: sync, async, and iterator wrappers, plus the -//! consumer-facing abstract base class, static vtable, and trampolines of -//! each callback interface. -//! -//! Marshalling dispatch is driven by the shared plans: [`ArgPass`] decides -//! how each parameter crosses, [`RetPass`] how a result (or a trampoline -//! argument) is received, [`plan::elem_free`] what an iterator step owes, and -//! [`CallbackProtocol`] how each trampoline receives its arguments, so this -//! module never 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, CallbackInterfaceBinding, CallbackMethodBinding, ErrorBinding, FnBinding, - ModuleBinding, ParamBinding, -}; -use weaveffi_core::model::{Prim, WireType}; -use weaveffi_core::plan::{self, ArgPass, CallbackProtocol, ErrorStrategy, Free, RetPass}; -use weaveffi_core::utils::local_type_name; - -use crate::codec::{ - py_decode_borrowed_expr, py_decode_owned_expr, py_pack_fn_name, py_read_expr, - py_unpack_fn_name, py_write_stmts, -}; -use crate::docs::{emit_docstring, emit_fn_docstring}; -use crate::entities::{py_checker_name, py_error_factory_name}; -use crate::types::{ - py_ctype, py_ctypes_scalar, py_member_name, py_name, py_param_argtypes, py_return_info, - py_type_hint, py_vtable_class_name, py_vtable_instance_name, py_wrapper_fn_name, -}; - -/// How a rendered callable is scoped and spelled in the generated Python. -#[derive(Clone, Copy)] -pub(crate) enum FnScope<'a> { - /// A module-level free function. - Free { - /// The owning module's underscore-joined path. - module_path: &'a str, - /// Whether the emitted name drops the module-path prefix. - strip_module_prefix: bool, - }, - /// An instance method on an interface wrapper: leading `self` parameter, - /// `self._ptr` passed as the leading C argument. Carries the wrapper - /// class name, which qualifies the member's iterator helper class. - Method { - /// The wrapper class name. - class: &'a str, - }, - /// A `@staticmethod` member; carries the wrapper class name, which - /// qualifies the member's iterator helper class. - Static { - /// The wrapper class name. - class: &'a str, - }, - /// A `@classmethod` constructor factory returning a new wrapper instance. - Factory, - /// The canonical `new` constructor, emitted as `__init__`. - Init, -} - -impl FnScope<'_> { - /// True for every scope rendered inside a class body (depth 1). - fn is_member(&self) -> bool { - !matches!(self, FnScope::Free { .. }) - } - - /// True when the C call receives `self._ptr` as its leading argument. - fn has_self_slot(&self) -> bool { - matches!(self, FnScope::Method { .. }) - } - - /// The owner stem of a member's iterator helper class name: - /// `{Interface}_{member}` for members, the bare function name otherwise. - fn iterator_owner(&self, fn_name: &str) -> String { - match self { - FnScope::Method { class } | FnScope::Static { class } => { - format!("{class}_{fn_name}") - } - _ => fn_name.to_string(), - } - } - - /// Indentation depth of the `def` line (0 at module scope, 1 in a class). - fn depth(&self) -> usize { - usize::from(self.is_member()) - } -} - -/// The emitted Python name for a callable in `scope`: `__init__` for the -/// canonical constructor, otherwise the snake_case member name, with the -/// module-path prefix applied to free functions when configured. -fn py_fn_name(f: &FnBinding, scope: &FnScope) -> String { - match scope { - FnScope::Free { - module_path, - strip_module_prefix, - } => py_wrapper_fn_name(module_path, &f.name, *strip_module_prefix), - FnScope::Init => "__init__".to_string(), - _ => py_member_name(&f.name), - } -} - -/// Render one callable: a free function or an interface member. `error` is -/// the module's error domain (used when the callable throws); `scope` picks -/// the def spelling, receiver, indent, and result handling. Sync, async, and -/// iterator shapes all route through here so members reuse the free-function -/// marshalling paths. -pub(crate) fn render_callable( - out: &mut String, - f: &FnBinding, - error: Option<&ErrorBinding>, - scope: &FnScope, -) { - let func_name = py_fn_name(f, scope); - let depth = scope.depth(); - let ind = " ".repeat(depth + 1); - let checker = py_checker_name(f, error); - let raises = error.filter(|_| f.throws).map(|eb| eb.type_name.as_str()); - - let receiver = match scope { - FnScope::Method { .. } | FnScope::Init => Some("self"), - FnScope::Factory => Some("cls"), - _ => None, - }; - let mut params_sig: Vec = Vec::new(); - if let Some(r) = receiver { - params_sig.push(r.to_string()); - } - params_sig.extend( - f.params - .iter() - .map(|p| format!("{}: {}", py_name(&p.name), py_type_hint(&p.ty))), - ); - let ret_hint = match scope { - FnScope::Init => "None".to_string(), - _ => f - .ret - .as_ref() - .map(py_type_hint) - .unwrap_or_else(|| "None".to_string()), - }; - - let is_iterator_ret = matches!(f.shape, CallShape::Iterator(_)); - - // 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(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); - } - } - - let decorator = match scope { - FnScope::Static { .. } => Some("@staticmethod"), - FnScope::Factory => Some("@classmethod"), - _ => None, - }; - - let mut w = CodeWriter::four_space().with_depth(depth); - w.blank().blank(); - if let Some(d) = decorator { - w.line(d); - } - w.line(format!( - "{}def {}({}) -> {}:", - if f.is_async { "async " } else { "" }, - func_name, - params_sig.join(", "), - ret_hint - )); - w.indent(); - - // An iterator-returning wrapper documents the streaming contract next to - // whatever the IDL says about the function itself. - let doc = if is_iterator_ret { - let streaming = "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)." - .to_string(); - Some(match &f.doc { - Some(d) => format!("{}\n\n{streaming}", d.trim()), - None => streaming, - }) - } else { - f.doc.clone() - }; - let mut fdoc = String::new(); - emit_fn_docstring(&mut fdoc, &doc, &f.params, &ind, raises); - w.raw(fdoc); - - // Set before any fallible statement so `__del__` never sees a - // half-constructed instance when the constructor raises. - if matches!(scope, FnScope::Init) { - w.line("self._ptr = None"); - } - - if let Some(msg) = &f.deprecated { - w.line("import warnings"); - w.line(format!( - "warnings.warn(\"{}\", DeprecationWarning, stacklevel=2)", - msg.replace('"', "\\\"") - )); - } - - if f.is_async { - // The async FFI call body is rendered at the function-body indent and - // spliced in verbatim. - let mut body = String::new(); - render_async_ffi_call_body(&mut body, f, error, &ind, scope.has_self_slot()); - w.raw(body); - out.push_str(&w.finish()); - } else { - let sym = match &f.shape { - CallShape::Sync(abi) => abi.symbol.as_str(), - CallShape::Iterator(it) => it.launch.symbol.as_str(), - CallShape::Async(_) => unreachable!("async handled above"), - }; - w.line(format!("_fn = _lib.{sym}")); - - let mut argtypes: Vec = Vec::new(); - if scope.has_self_slot() { - argtypes.push("ctypes.c_void_p".into()); - } - for p in &f.params { - argtypes.extend(py_param_argtypes(p)); - } - let mut out_ret_argtypes = Vec::new(); - let restype; - if let Some(ret_ty) = &f.ret { - let (rt, oat) = py_return_info(ret_ty); - argtypes.extend(oat.iter().cloned()); - restype = rt; - out_ret_argtypes = oat; - } else { - restype = "None".to_string(); - } - argtypes.push("ctypes.POINTER(_WeaveFFIErrorStruct)".into()); - - w.line(format!("_fn.argtypes = [{}]", argtypes.join(", "))); - w.line(format!("_fn.restype = {restype}")); - - for p in &f.params { - for line in py_param_conversion(p, &ind) { - w.raw(&line).raw("\n"); - } - } - - w.line("_err = _WeaveFFIErrorStruct()"); - - // Both bytes and buffered returns carry one trailing `size_t* - // out_len` slot beside the returned pointer. - let has_out_len = !out_ret_argtypes.is_empty(); - if has_out_len { - w.line("_out_len = ctypes.c_size_t(0)"); - } - - let mut call_args: Vec = Vec::new(); - if scope.has_self_slot() { - call_args.push("_borrow(self)".into()); - } - for p in &f.params { - call_args.extend(py_param_call_args(p)); - } - if has_out_len { - call_args.push("ctypes.byref(_out_len)".into()); - } - call_args.push("ctypes.byref(_err)".into()); - - let call_expr = format!("_fn({})", call_args.join(", ")); - if f.ret.is_some() { - w.line(format!("_result = {call_expr}")); - } else { - w.line(call_expr); - } - - w.line(format!("{checker}(_err)")); - - match scope { - // Constructors receive the owned pointer directly rather than - // routing through the generic return path. - FnScope::Init => { - w.line("if _result is None:"); - w.scope(|w| { - w.line("raise WeaveFFIError(-1, \"null pointer\")"); - }); - w.line("self._ptr = _result"); - out.push_str(&w.finish()); - } - FnScope::Factory => { - w.line("if _result is None:"); - w.scope(|w| { - w.line("raise WeaveFFIError(-1, \"null pointer\")"); - }); - w.line("return cls._from_ptr(_result)"); - out.push_str(&w.finish()); - } - _ => { - if is_iterator_ret { - // Lazy: hand the caller the iterator wrapper; each step - // pulls one element and the wrapper owns the handle. - let class = py_iterator_class_name(&scope.iterator_owner(&f.name)); - w.line(format!("return {class}(_result)")); - out.push_str(&w.finish()); - } else { - out.push_str(&w.finish()); - if let Some(ret_ty) = &f.ret { - render_return_value(out, ret_ty, &ind); - } - } - } - } - } -} - -// ── Param helpers ── - -/// The statements preparing one parameter's locals ahead of the C call: a -/// packed `_{name}_buf` for buffered types, a `_{name}_arr` element array -/// for bytes, a `_{name}_ctx` handle-table key for a callback interface. -/// Direct, string, and object types need no preparation. -fn py_param_conversion(p: &ParamBinding, ind: &str) -> Vec { - let name = py_name(&p.name); - match p.arg_pass() { - // Register the implementation in the handle table; the integer key - // is what the producer receives as `ctx` and hands back to every - // trampoline and, finally, to `free`. - ArgPass::Callback { .. } => vec![format!("{ind}_{name}_ctx = _cb_register({name})")], - // Records and rich enums pack through their dedicated helpers; other - // 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 p.ty.wire() { - WireType::User(n) => { - vec![format!("{ind}_{name}_buf = {}({name})", py_pack_fn_name(n))] - } - _ => { - let mut w = CodeWriter::four_space().with_depth(ind.len() / 4); - let writer = format!("_{name}_w"); - w.line(format!("{writer} = _BufferWriter()")); - py_write_stmts(&mut w, &writer, &name, &p.ty, 0); - w.line(format!("_{name}_buf = {writer}.finish()")); - w.finish().lines().map(str::to_string).collect() - } - }, - ArgPass::Bytes { .. } => { - vec![format!( - "{ind}_{name}_arr = (ctypes.c_uint8 * len({name}))(*{name})" - )] - } - _ => vec![], - } -} - -/// The C argument expressions one parameter contributes, in slot order. -fn py_param_call_args(p: &ParamBinding) -> Vec { - let name = py_name(&p.name); - match p.arg_pass() { - // A buffered parameter fills its `(ptr, len)` slot pair with the - // packed bytes; ctypes passes `bytes` directly for a `c_char_p` - // argtype. - ArgPass::Buffer { .. } => vec![format!("_{name}_buf"), format!("len(_{name}_buf)")], - ArgPass::Bytes { .. } => vec![format!("_{name}_arr"), format!("len({name})")], - ArgPass::String { .. } => vec![format!("_string_to_bytes({name})")], - // Interface parameters are borrowed: lend the wrapper's raw pointer - // and keep the wrapper's own reference; the producer clones if it - // retains the object. A nullable `Interface?` maps `None` onto a - // null pointer. - ArgPass::Object { - nullable: false, .. - } => vec![format!("_borrow({name})")], - ArgPass::Object { nullable: true, .. } => { - vec![format!("(_borrow({name}) if {name} is not None else None)")] - } - // The handle-table key as `ctx`, and the address of the interface's - // one static vtable. - ArgPass::Callback { .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback family names a callback interface"); - vec![ - format!("_{name}_ctx"), - format!("ctypes.byref({})", py_vtable_instance_name(cb)), - ] - } - ArgPass::Direct { .. } => match &p.ty { - Ty::Bool => vec![format!("1 if {name} else 0")], - Ty::Enum(_) => vec![format!("{name}.value")], - // Scalars are already the raw slot value the C signature wants. - _ => vec![name], - }, - } -} - -// ── Return helpers ── - -/// The interface name behind a direct or optional interface return. -fn object_interface_name(ty: &Ty) -> &str { - ty.interface_name() - .expect("object returns are direct or optional interfaces") -} - -/// 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: &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. - match plan::ret_pass(Some(ty), "", "") { - // A buffered return hands over one owned encoded value: copy the - // bytes, release them with `weaveffi_free_bytes`, then decode. - RetPass::Buffer => { - w.line("_data = _take_buffer(_result, _out_len.value)"); - match ty.wire() { - WireType::User(name) => { - w.line(format!("return {}(_data)", py_unpack_fn_name(name))); - } - _ => { - w.line(format!( - "return _decode_buffer(_data, lambda _r: {})", - py_read_expr(ty, 0) - )); - } - } - } - // Owned string: copy, then release via `weaveffi_free_string`. - RetPass::String => { - w.line("return _take_string(_result) or \"\""); - } - // Owned buffer: copy, then release via `weaveffi_free_bytes`. - RetPass::Bytes => { - w.line("if not _result:"); - w.scope(|w| { - w.line("return b\"\""); - }); - w.line("_val = bytes(_result[:_out_len.value])"); - w.line("_lib.weaveffi_free_bytes(_result, ctypes.c_size_t(_out_len.value))"); - w.line("return _val"); - } - // A returned interface is a new owned reference: wrap it without - // re-running the class's FFI constructor. A nullable return maps - // null to `None` instead of raising. - RetPass::Object { nullable, .. } => { - let name = local_type_name(object_interface_name(ty)); - w.line("if _result is None:"); - if nullable { - w.scope(|w| { - w.line("return None"); - }); - } else { - w.scope(|w| { - w.line("raise WeaveFFIError(-1, \"null pointer\")"); - }); - } - w.line(format!("return {name}._from_ptr(_result)")); - } - RetPass::Direct => match ty { - Ty::Bool => { - w.line("return bool(_result)"); - } - Ty::Enum(name) => { - let name = local_type_name(name); - w.line(format!("return {name}(_result)")); - } - _ => { - w.line("return _result"); - } - }, - // Callers only render a value for non-void returns. - RetPass::Void => unreachable!("void returns render no value"), - } - out.push_str(&w.finish()); -} - -// ── 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)> { - match ret { - None => vec![], - Some(ty) => abi::callback_result_params(ty, "") - .into_iter() - .map(|p| { - // An owned string result must keep its raw address (a - // `c_char_p` slot would auto-convert to `bytes` and lose the - // pointer `weaveffi_free_string` needs). - let cty = match py_ctype(&p.ty).as_str() { - "ctypes.c_char_p" => "ctypes.c_void_p".to_string(), - other => other.to_string(), - }; - (p.name, cty) - }) - .collect(), - } -} - -/// Append the success branch of an async completion trampoline: convert the -/// owned `result` slots into the idiomatic value and store it in -/// `_state["val"]`. Async results transfer ownership to the consumer: strings -/// 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) { - let Some(ty) = ret else { - out.push_str(&format!("{ind}_state[\"val\"] = None\n")); - return; - }; - match plan::ret_pass(Some(ty), "", "") { - // The owned `(result_ptr, result_len)` pair holds one encoded value; - // `_take_buffer` copies it and frees the producer allocation. - RetPass::Buffer => { - out.push_str(&format!( - "{ind}_state[\"val\"] = {}\n", - py_decode_owned_expr("result_ptr", "result_len", ty) - )); - } - RetPass::String => { - // The owned C string is copied and freed by `_take_string` (the - // callback slot is typed `c_void_p` so the address survives). - out.push_str(&format!( - "{ind}_state[\"val\"] = _take_string(result) or \"\"\n" - )); - } - RetPass::Bytes => { - // Copy the owned buffer, then release the producer allocation. - out.push_str(&format!("{ind}if not result:\n")); - out.push_str(&format!("{ind} _state[\"val\"] = b\"\"\n")); - out.push_str(&format!("{ind}else:\n")); - out.push_str(&format!("{ind} _n = int(result_len)\n")); - out.push_str(&format!( - "{ind} _state[\"val\"] = _take_buffer(ctypes.cast(result, ctypes.c_void_p).value, _n)\n" - )); - } - // A returned interface transfers ownership of a new object reference; - // wrap it without re-running the class's FFI constructor. A nullable - // return maps null to `None` instead of an error. - RetPass::Object { nullable, .. } => { - let name = local_type_name(object_interface_name(ty)); - if nullable { - out.push_str(&format!("{ind}if not result:\n")); - out.push_str(&format!("{ind} _state[\"val\"] = None\n")); - out.push_str(&format!("{ind}else:\n")); - out.push_str(&format!( - "{ind} _state[\"val\"] = {name}._from_ptr(result)\n" - )); - } else { - out.push_str(&format!("{ind}if result is None:\n")); - out.push_str(&format!( - "{ind} _state[\"err\"] = WeaveFFIError(-1, \"null pointer\")\n" - )); - out.push_str(&format!("{ind}else:\n")); - out.push_str(&format!( - "{ind} _state[\"val\"] = {name}._from_ptr(result)\n" - )); - } - } - RetPass::Direct => match ty { - Ty::Bool => { - out.push_str(&format!("{ind}_state[\"val\"] = bool(result)\n")); - } - Ty::Enum(name) => { - let name = local_type_name(name); - out.push_str(&format!("{ind}_state[\"val\"] = {name}(result)\n")); - } - _ => { - out.push_str(&format!("{ind}_state[\"val\"] = result\n")); - } - }, - // The `let Some(ty)` guard above already returned for void. - RetPass::Void => unreachable!("void handled above"), - } -} - -/// Render the callback-driven body of an `async def` wrapper at the body -/// indent `ind`. -/// -/// The wrapper creates a future on the running `asyncio` loop, builds the -/// `CFUNCTYPE` completion trampoline for the launcher's callback typedef, -/// pins it in `_async_pending` until completion, invokes the launcher (which -/// returns immediately), and awaits the future. The trampoline runs on an -/// arbitrary producer thread: it takes ownership of the result (freeing -/// strings and value buffers through the runtime symbols; owned object -/// pointers are adopted), then resolves the future via -/// `call_soon_threadsafe`. A throwing callable maps the completion -/// error through the module domain's factory (from `error`); a non-throwing -/// one traps with the generic `WeaveFFIError`. When `has_self` is set (an -/// instance method), the launcher receives `self._ptr` as its leading -/// argument. -fn render_async_ffi_call_body( - out: &mut String, - f: &FnBinding, - error: Option<&ErrorBinding>, - ind: &str, - has_self: bool, -) { - let CallShape::Async(a) = &f.shape else { - unreachable!("render_async_ffi_call_body requires an async call shape"); - }; - // A throwing callable routes the completion error through the domain - // factory, including the borrowed payload buffer (copied before the - // producer reclaims it). Traps carry no payload, so the generic error - // skips it. - let (err_expr, wants_payload) = match (f.error_strategy(), error) { - (ErrorStrategy::Throws, Some(eb)) => ( - format!("{}(_code, _msg, _payload)", py_error_factory_name(eb)), - true, - ), - _ => ("WeaveFFIError(_code, _msg)".to_string(), false), - }; - - out.push_str(&format!("{ind}_fn = _lib.{}\n", a.launch.symbol)); - out.push_str(&format!("{ind}_loop = asyncio.get_running_loop()\n")); - out.push_str(&format!("{ind}_fut = _loop.create_future()\n")); - - let trailing = py_async_cb_trailing_fields(&f.ret); - let mut cb_param_list: Vec = vec!["context".into(), "err".into()]; - cb_param_list.extend(trailing.iter().map(|(n, _)| n.clone())); - let cb_params_joined = cb_param_list.join(", "); - - out.push('\n'); - out.push_str(&format!("{ind}def _cb_impl({cb_params_joined}):\n")); - out.push_str(&format!( - "{ind} # Fires exactly once, on a producer thread: take ownership of\n" - )); - out.push_str(&format!( - "{ind} # the result here, then hop back to the event loop.\n" - )); - out.push_str(&format!( - "{ind} _state = {{\"err\": None, \"val\": None}}\n" - )); - out.push_str(&format!("{ind} if err and err.contents.code != 0:\n")); - out.push_str(&format!("{ind} _code = err.contents.code\n")); - out.push_str(&format!( - "{ind} _msg = err.contents.message.decode(\"utf-8\") if err.contents.message else \"\"\n" - )); - if wants_payload { - out.push_str(&format!( - "{ind} _payload = ctypes.string_at(err.contents.payload_ptr, err.contents.payload_len) if err.contents.payload_ptr else b\"\"\n" - )); - } - out.push_str(&format!("{ind} _lib.weaveffi_error_free(err)\n")); - out.push_str(&format!("{ind} _state[\"err\"] = {err_expr}\n")); - out.push_str(&format!("{ind} else:\n")); - // Decoding a malformed result buffer raises; surface that through the - // future rather than letting the exception escape the C callback. - out.push_str(&format!("{ind} try:\n")); - append_async_success_handler(out, &f.ret, &format!("{ind} ")); - out.push_str(&format!("{ind} except Exception as _exc:\n")); - out.push_str(&format!("{ind} _state[\"err\"] = _exc\n")); - out.push('\n'); - out.push_str(&format!("{ind} def _resolve():\n")); - out.push_str(&format!("{ind} _async_pending.pop(_token, None)\n")); - out.push_str(&format!( - "{ind} # A cancelled future must not be resolved.\n" - )); - out.push_str(&format!("{ind} if _fut.cancelled():\n")); - out.push_str(&format!("{ind} return\n")); - out.push_str(&format!("{ind} if _state[\"err\"] is not None:\n")); - out.push_str(&format!( - "{ind} _fut.set_exception(_state[\"err\"])\n" - )); - out.push_str(&format!("{ind} else:\n")); - out.push_str(&format!( - "{ind} _fut.set_result(_state[\"val\"])\n" - )); - out.push('\n'); - out.push_str(&format!("{ind} _loop.call_soon_threadsafe(_resolve)\n")); - out.push('\n'); - - let mut cf_parts: Vec = vec![ - "ctypes.c_void_p".into(), - "ctypes.POINTER(_WeaveFFIErrorStruct)".into(), - ]; - cf_parts.extend(trailing.iter().map(|(_, t)| t.clone())); - out.push_str(&format!( - "{ind}_cb_type = ctypes.CFUNCTYPE(None, {})\n", - cf_parts.join(", ") - )); - out.push_str(&format!("{ind}_cb = _cb_type(_cb_impl)\n")); - out.push_str(&format!( - "{ind}_token = _async_register(_cb) # pinned until completion\n" - )); - - let mut argtypes: Vec = Vec::new(); - if has_self { - argtypes.push("ctypes.c_void_p".into()); - } - for p in &f.params { - argtypes.extend(py_param_argtypes(p)); - } - if f.cancellable { - argtypes.push("ctypes.c_void_p".into()); - } - argtypes.push("_cb_type".into()); - argtypes.push("ctypes.c_void_p".into()); - - out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", "))); - out.push_str(&format!("{ind}_fn.restype = None\n")); - - for p in &f.params { - for line in py_param_conversion(p, ind) { - out.push_str(&line); - out.push('\n'); - } - } - - let mut call_args: Vec = Vec::new(); - if has_self { - call_args.push("_borrow(self)".into()); - } - for p in &f.params { - call_args.extend(py_param_call_args(p)); - } - if f.cancellable { - call_args.push("None".into()); - } - call_args.push("_cb".into()); - call_args.push("None".into()); - - out.push_str(&format!("{ind}_fn({})\n", call_args.join(", "))); - if f.ret.is_some() { - out.push_str(&format!("{ind}return await _fut\n")); - } else { - out.push_str(&format!("{ind}await _fut\n")); - } -} - -// ── Iterator helpers ── - -/// The `_out_item` ctypes local for one iterator element slot, plus whether -/// 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: &Ty) -> (String, bool) { - match plan::elem_free(inner) { - Free::Bytes => ("ctypes.c_void_p".into(), true), - Free::String => ("ctypes.c_void_p".into(), false), - Free::None => (py_ctypes_scalar(inner).into(), false), - } -} - -/// 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: &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. - Free::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) - ), - _ => format!( - "_decode_buffer(_take_buffer(_out_item.value, _out_len.value), lambda _r: {})", - py_read_expr(inner, 0) - ), - }, - // Owned string element: copy, then `weaveffi_free_string`. The out - // slot is a raw `c_void_p`, so the address survives to be freed. - Free::String => "_take_string(_out_item.value)".into(), - Free::None => match inner { - Ty::Enum(name) => { - format!("{}(_out_item.value)", local_type_name(name)) - } - // An owned interface element transfers one strong reference, - // adopted by its wrapper class. A nullable element maps null to - // `None`. - Ty::Interface(name) => { - format!("{}._from_ptr(_out_item.value)", local_type_name(name)) - } - Ty::Optional(obj) if matches!(obj.as_ref(), Ty::Interface(_)) => format!( - "({}._from_ptr(_out_item.value) if _out_item.value else None)", - local_type_name(object_interface_name(inner)) - ), - Ty::Bool => "bool(_out_item.value)".into(), - _ => "_out_item.value".into(), - }, - } -} - -/// The module-level helper class name for one iterator-returning callable. -/// `owner` is the function name for a free function, or -/// `{Interface}_{member}` for an interface member. -fn py_iterator_class_name(owner: &str) -> String { - format!("_{}Iterator", pascal_case(owner)) -} - -/// Render the module-level `_...Iterator` helper class for one -/// iterator-returning callable, satisfying the pull contract of -/// [`weaveffi_core::plan::IteratorProtocol`]: one producer `next` call per -/// `__next__`, per-element releases after copying, and exactly one `destroy` -/// (eagerly on exhaustion, or from `close()`/`__del__` when iteration is -/// abandoned early). `checker` is the error-check helper the `next` calls -/// route their out-err slot through (typed for a throwing callable, generic -/// otherwise). -pub(crate) fn render_iterator_class( - out: &mut String, - iter_tag: &str, - func_name: &str, - inner: &Ty, - checker: &str, -) { - let class_name = py_iterator_class_name(func_name); - let (item_scalar, has_out_len) = py_iter_slots(inner); - let read_expr = py_read_iter_item(inner); - let mut next_argtypes = vec![ - "ctypes.c_void_p".to_string(), - format!("ctypes.POINTER({item_scalar})"), - ]; - if has_out_len { - next_argtypes.push("ctypes.POINTER(ctypes.c_size_t)".into()); - } - next_argtypes.push("ctypes.POINTER(_WeaveFFIErrorStruct)".into()); - let mut next_args = vec!["self._ptr".to_string(), "ctypes.byref(_out_item)".into()]; - if has_out_len { - next_args.push("ctypes.byref(_out_len)".into()); - } - next_args.push("ctypes.byref(_err)".into()); - - out.push_str(&format!("\n\nclass {class_name}:")); - out.push_str("\n \"\"\"Lazy iterator over a producer stream: each step pulls one element"); - out.push_str("\n across the C boundary. The native handle is released exactly once, on"); - out.push_str("\n exhaustion, on close(), or when the iterator is garbage collected.\"\"\""); - out.push_str("\n\n def __init__(self, ptr):"); - out.push_str("\n self._ptr = ptr"); - out.push_str("\n self._done = False"); - - out.push_str("\n\n def __iter__(self):"); - out.push_str("\n return self"); - - out.push_str("\n\n def __next__(self):"); - out.push_str("\n if self._done:"); - out.push_str("\n raise StopIteration"); - out.push_str(&format!("\n _next_fn = _lib.{iter_tag}_next")); - out.push_str(&format!( - "\n _next_fn.argtypes = [{}]", - next_argtypes.join(", ") - )); - out.push_str("\n _next_fn.restype = ctypes.c_int32"); - out.push_str(&format!("\n _out_item = {item_scalar}()")); - if has_out_len { - out.push_str("\n _out_len = ctypes.c_size_t(0)"); - } - out.push_str("\n _err = _WeaveFFIErrorStruct()"); - out.push_str(&format!( - "\n _has = _next_fn({})", - next_args.join(", ") - )); - out.push_str(&format!("\n {checker}(_err)")); - out.push_str("\n if not _has:"); - out.push_str("\n self._done = True"); - out.push_str("\n self._destroy()"); - out.push_str("\n raise StopIteration"); - out.push_str(&format!("\n return {read_expr}")); - - out.push_str("\n\n def close(self):"); - out.push_str("\n \"\"\"Release the native iterator without draining it.\"\"\""); - out.push_str("\n self._done = True"); - out.push_str("\n self._destroy()"); - - out.push_str("\n\n def _destroy(self):"); - out.push_str("\n if self._ptr is not None:"); - out.push_str(&format!( - "\n _destroy_fn = _lib.{iter_tag}_destroy" - )); - out.push_str("\n _destroy_fn.argtypes = [ctypes.c_void_p]"); - out.push_str("\n _destroy_fn.restype = None"); - out.push_str("\n _destroy_fn(self._ptr)"); - out.push_str("\n self._ptr = None"); - - out.push_str("\n\n def __del__(self):"); - out.push_str("\n self._destroy()"); - out.push('\n'); -} - -// ── Callback interfaces ── - -/// The Python spelling of a trampoline slot name: the ABI slot name, -/// keyword-escaped. Derived slots (`{name}_ptr`, `out_err`) never collide -/// with a keyword, so only single-slot parameters can change spelling. -fn py_slot_name(slot: &str) -> String { - lang::escape_ident(slot, lang::PYTHON_KEYWORDS) -} - -/// The expression converting one trampoline parameter's borrowed (or, for -/// objects, transferred) C slots into the idiomatic value handed to the -/// implementation, per the receiving contract in `pass`. -fn py_trampoline_arg_expr(p: &ParamBinding, pass: &RetPass) -> String { - let n = &p.name; - let esc = py_slot_name(n); - match pass { - // A buffered argument is a borrowed `({n}_ptr, {n}_len)` pair valid - // only during the dispatch: copy and decode before returning. Object - // tokens inside are adopted by the decoded wrappers. - RetPass::Buffer => py_decode_borrowed_expr(&format!("{n}_ptr"), &format!("{n}_len"), &p.ty), - RetPass::Bytes => { - format!("(ctypes.string_at({n}_ptr, {n}_len) if {n}_ptr else b\"\")") - } - // `c_char_p` already delivered a `bytes` copy of the borrowed string. - RetPass::String => format!("_bytes_to_string({esc})"), - // An object argument transfers one strong reference: adopt it into a - // wrapper whose disposal owes the destroy symbol. Null is `None` for - // an `Interface?` slot. - RetPass::Object { nullable, .. } => { - let class = local_type_name(object_interface_name(&p.ty)); - if *nullable { - format!("({class}._from_ptr({esc}) if {esc} else None)") - } else { - format!("{class}._from_ptr({esc})") - } - } - RetPass::Direct => match &p.ty { - Ty::Bool => format!("bool({esc})"), - Ty::Enum(name) => format!("{}({esc})", local_type_name(name)), - _ => esc, - }, - RetPass::Void => unreachable!("parameters are never void"), - } -} - -/// `(coercion, default)` for a callback method's direct return: the Python -/// conversion applied to the implementation's result before it is written -/// into the C return slot, and the value returned after a failure has been -/// reported through `out_err`. `None` for a void method. -fn py_trampoline_return(ret: Option<&Ty>) -> Option<(&'static str, &'static str)> { - match ret { - None => None, - Some(Ty::Bool) => Some(("1 if {} else 0", "0")), - Some(Ty::F32 | Ty::F64) => Some(("float({})", "0.0")), - // Integers and C-style enums (an `IntEnum` is an `int`). - Some(_) => Some(("int({})", "0")), - } -} - -/// The module-level stems of one callback method's trampoline objects: -/// `_{Name}_{method}_cfunctype`, `_{Name}_{method}_trampoline`, and -/// `_{Name}_{method}_cfunc`. -fn py_trampoline_stem(cb: &CallbackInterfaceBinding, method: &str) -> String { - format!("_{}_{method}", cb.name) -} - -/// Render one callback interface: the abstract base class the consumer -/// subclasses, then the ABI side that satisfies every clause of -/// [`CallbackProtocol`]: one `CFUNCTYPE` per method plus the trailing `free`, -/// the `ctypes.Structure` mirroring the C vtable, one trampoline per method, -/// and the single process-wide static vtable instance whose function objects -/// are pinned at module scope for the process lifetime. -/// -/// Each trampoline looks its implementation up by the integer `ctx`, decodes -/// borrowed string, bytes, and buffer arguments (freeing nothing), adopts -/// object arguments, and calls the method. Any exception is reported through -/// `{prefix}_error_set(out_err, -4, message)` and a default value is -/// returned, so nothing ever unwinds through the C frame. `free` removes the -/// handle-table entry. `ctypes` acquires the GIL on entry, so the producer -/// may call from any thread. -pub(crate) fn render_callback_interface( - out: &mut String, - module: &ModuleBinding, - cb: &CallbackInterfaceBinding, - prefix: &str, -) { - let protocol: CallbackProtocol<'_> = cb.protocol(&module.path, prefix); - let name = &cb.name; - let vtable_class = py_vtable_class_name(name); - let vtable_instance = py_vtable_instance_name(name); - let free_stem = format!("_{name}_vtable_free"); - - let mut w = CodeWriter::four_space(); - - // The consumer-facing abstract base class. - w.blank().blank(); - w.line(format!("class {name}(abc.ABC):")); - w.indent(); - let base_doc = "Consumer-implemented callback interface. Subclass it, implement every \ - abstract method, and pass an instance wherever the API takes a \ - `{name}`; the producer may call the methods from any thread until it \ - releases the instance. An exception raised by a method is reported to \ - the producer as WeaveFFIError.FOREIGN_ERROR_CODE (-4) and aborts the \ - call that was in progress." - .replace("{name}", name); - let class_doc = match &cb.doc { - Some(d) if !d.trim().is_empty() => format!("{}\n\n{base_doc}", d.trim()), - _ => base_doc, - }; - let mut doc = String::new(); - emit_docstring(&mut doc, &Some(class_doc), &w.indent_str()); - w.raw(doc); - for m in &cb.methods { - let mut sig: Vec = vec!["self".into()]; - sig.extend( - m.params - .iter() - .map(|p| format!("{}: {}", py_name(&p.name), py_type_hint(&p.ty))), - ); - let ret_hint = m - .ret - .as_ref() - .map(py_type_hint) - .unwrap_or_else(|| "None".to_string()); - w.blank(); - w.line("@abc.abstractmethod"); - w.line(format!( - "def {}({}) -> {ret_hint}:", - py_member_name(&m.name), - sig.join(", ") - )); - w.indent(); - let mut mdoc = String::new(); - emit_fn_docstring(&mut mdoc, &m.doc, &m.params, &w.indent_str(), None); - if mdoc.is_empty() { - w.line("..."); - } else { - w.raw(mdoc); - } - w.dedent(); - } - w.dedent(); - - // One CFUNCTYPE per vtable entry, in declaration order, then `free`. - w.blank().blank(); - for m in &cb.methods { - let parts: Vec = std::iter::once(py_ctype(&m.abi_ret)) - .chain(m.abi_params.iter().map(|p| py_ctype(&p.ty))) - .collect(); - w.line(format!( - "{}_cfunctype = ctypes.CFUNCTYPE({})", - py_trampoline_stem(cb, &m.name), - parts.join(", ") - )); - } - w.line(format!( - "{free_stem}_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p)" - )); - - // The vtable layout: the C struct `{vtable_tag}`. - w.blank().blank(); - w.line(format!("class {vtable_class}(ctypes.Structure):")); - w.scope(|w| { - w.line(format!("\"\"\"The C vtable `{}`.\"\"\"", cb.vtable_tag)); - w.blank(); - w.line("_fields_ = ["); - w.scope(|w| { - for m in &cb.methods { - w.line(format!( - "(\"{}\", {}_cfunctype),", - m.name, - py_trampoline_stem(cb, &m.name) - )); - } - w.line(format!("(\"free\", {free_stem}_cfunctype),")); - }); - w.line("]"); - }); - - // One trampoline per method. - for (m, args) in cb.methods.iter().zip(&protocol.method_args) { - render_callback_trampoline(&mut w, cb, m, args); - } - - w.blank().blank(); - w.line(format!("def {free_stem}_trampoline(ctx):")); - w.scope(|w| { - w.line("# The producer's last reference is gone; it never touches `ctx` again."); - w.line("_cb_impls.pop(ctx, None)"); - }); - - // The single static vtable, with every function object pinned. - w.blank().blank(); - w.line(format!( - "# The one process-wide vtable for {name}. The CFUNCTYPE objects are held at" - )); - w.line("# module scope so their function pointers stay valid for the process lifetime."); - for m in &cb.methods { - let stem = py_trampoline_stem(cb, &m.name); - w.line(format!( - "{stem}_cfunc = {stem}_cfunctype({stem}_trampoline)" - )); - } - w.line(format!( - "{free_stem}_cfunc = {free_stem}_cfunctype({free_stem}_trampoline)" - )); - let entries: Vec = cb - .methods - .iter() - .map(|m| format!("{}_cfunc", py_trampoline_stem(cb, &m.name))) - .chain(std::iter::once(format!("{free_stem}_cfunc"))) - .collect(); - w.line(format!( - "{vtable_instance} = {vtable_class}({})", - entries.join(", ") - )); - - out.push_str(&w.finish()); -} - -/// Render the trampoline for one callback method: the `def` whose parameters -/// are the vtable entry's C slots (`ctx`, the parameter slots, `out_err`), -/// receiving each argument per `args` and reporting any exception through -/// `error_set` with the foreign error code. -fn render_callback_trampoline( - w: &mut CodeWriter, - cb: &CallbackInterfaceBinding, - m: &CallbackMethodBinding, - args: &[RetPass], -) { - let stem = py_trampoline_stem(cb, &m.name); - let slots: Vec = m.abi_params.iter().map(|p| py_slot_name(&p.name)).collect(); - let call_args: Vec = m - .params - .iter() - .zip(args) - .map(|(p, pass)| py_trampoline_arg_expr(p, pass)) - .collect(); - let call = format!( - "_impl.{}({})", - py_member_name(&m.name), - call_args.join(", ") - ); - let ret = py_trampoline_return(m.ret.as_ref()); - - w.blank().blank(); - w.line(format!("def {stem}_trampoline({}):", slots.join(", "))); - w.scope(|w| { - w.line("try:"); - w.scope(|w| { - w.line("_impl = _cb_impls[ctx]"); - match ret { - Some((coerce, _)) => { - w.line(format!("_ret = {call}")); - w.line(format!("return {}", coerce.replace("{}", "_ret"))); - } - None => { - w.line(call); - } - } - }); - w.line("except Exception as _exc:"); - w.scope(|w| { - w.line("# Never unwind through the C frame: report the failure and return a"); - w.line("# default; the producer aborts its call with FOREIGN_ERROR_CODE."); - w.line( - "_lib.weaveffi_error_set(out_err, -4, str(_exc).encode(\"utf-8\", \"replace\"))", - ); - if let Some((_, default)) = ret { - w.line(format!("return {default}")); - } - }); - }); -} diff --git a/crates/weaveffi-gen-python/src/codec.rs b/crates/weaveffi-gen-python/src/codec.rs deleted file mode 100644 index abffaee9..00000000 --- a/crates/weaveffi-gen-python/src/codec.rs +++ /dev/null @@ -1,329 +0,0 @@ -//! Value-buffer codec emitters: the inline read expressions and write -//! statements for any wire shape, plus the per-record and per-rich-enum -//! codec functions (`_write_X`, `_read_X`, `_pack_X`, `_unpack_X`). -//! -//! Every dispatch here goes through [`Ty::wire`], so this module never -//! re-derives the wire folds (interfaces as `u64` object tokens carrying one -//! strong reference, 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 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 -/// the bare local class name. -pub(crate) fn py_write_fn_name(name: &str) -> String { - format!("_write_{}", local_type_name(name)) -} - -/// `_read_{Name}`, the reader consuming one encoded value from a -/// `_BufferReader`. -pub(crate) fn py_read_fn_name(name: &str) -> String { - format!("_read_{}", local_type_name(name)) -} - -/// `_pack_{Name}`, encoding one value to standalone `bytes`. -pub(crate) fn py_pack_fn_name(name: &str) -> String { - format!("_pack_{}", local_type_name(name)) -} - -/// `_unpack_{Name}`, decoding one value from standalone `bytes` (rejecting -/// trailing data). -pub(crate) fn py_unpack_fn_name(name: &str) -> String { - format!("_unpack_{}", local_type_name(name)) -} - -/// The Python expression reading one `ty` value from the reader `_r`, -/// following the value-buffer wire format. `depth` uniquifies comprehension -/// loop variables when composites nest. -/// -/// Expressions are used (rather than statements) so composite reads compose: -/// 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: &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(), - // An object token carries one strong reference: adopt it into a new - // wrapper, whose disposal owes the interface's destroy symbol. A - // decode that fails later in the buffer drops the wrapper, and its - // finalizer releases the adopted reference. - WireType::Object(name) => { - format!("{}._from_ptr(_r.read_object())", local_type_name(name)) - } - 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!( - "({} if _r.read_option_flag() else None)", - py_read_expr(inner, depth) - ), - WireType::List(inner) => format!( - "[{} for _i{depth} in range(_r.read_len())]", - py_read_expr(inner, depth + 1) - ), - WireType::Map(k, v) => format!( - "dict(({}, {}) for _i{depth} in range(_r.read_len()))", - py_read_expr(k, depth + 1), - py_read_expr(v, depth + 1) - ), - } -} - -/// 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: &Ty, depth: usize) { - match ty.wire() { - WireType::Prim(Prim::Bool) => { - w.line(format!("{writer}.write_bool({expr})")); - } - WireType::Prim(Prim::I8) => { - w.line(format!("{writer}.write_i8({expr})")); - } - WireType::Prim(Prim::I16) => { - w.line(format!("{writer}.write_i16({expr})")); - } - WireType::Prim(Prim::I32) => { - w.line(format!("{writer}.write_i32({expr})")); - } - WireType::Prim(Prim::I64) => { - w.line(format!("{writer}.write_i64({expr})")); - } - WireType::Prim(Prim::U8) => { - w.line(format!("{writer}.write_u8({expr})")); - } - WireType::Prim(Prim::U16) => { - w.line(format!("{writer}.write_u16({expr})")); - } - WireType::Prim(Prim::U32) => { - w.line(format!("{writer}.write_u32({expr})")); - } - WireType::Prim(Prim::U64) => { - w.line(format!("{writer}.write_u64({expr})")); - } - WireType::Prim(Prim::F32) => { - w.line(format!("{writer}.write_f32({expr})")); - } - WireType::Prim(Prim::F64) => { - w.line(format!("{writer}.write_f64({expr})")); - } - // The token written must be a reference the reader can own: the - // writer clones the wrapper's pointer (a new strong reference to the - // same object) when the buffer is finished, never writing the - // pointer the wrapper still holds. - WireType::Object(_) => { - w.line(format!("{writer}.write_object({expr})")); - } - WireType::Prim(Prim::String) => { - w.line(format!("{writer}.write_string({expr})")); - } - WireType::Prim(Prim::Bytes) => { - w.line(format!("{writer}.write_bytes({expr})")); - } - // IntEnum members are ints, so the discriminant packs directly. - WireType::Enum(_) => { - w.line(format!("{writer}.write_i32({expr})")); - } - WireType::User(name) => { - w.line(format!("{}({writer}, {expr})", py_write_fn_name(name))); - } - WireType::Optional(inner) => { - w.line(format!("if {expr} is None:")); - w.scope(|w| { - w.line(format!("{writer}.write_option_flag(False)")); - }); - w.line("else:"); - w.scope(|w| { - w.line(format!("{writer}.write_option_flag(True)")); - py_write_stmts(w, writer, expr, inner, depth); - }); - } - WireType::List(inner) => { - w.line(format!("{writer}.write_len(len({expr}))")); - w.line(format!("for _e{depth} in {expr}:")); - w.scope(|w| { - py_write_stmts(w, writer, &format!("_e{depth}"), inner, depth + 1); - }); - } - WireType::Map(k, v) => { - w.line(format!("{writer}.write_len(len({expr}))")); - w.line(format!("for _k{depth}, _v{depth} in {expr}.items():")); - w.scope(|w| { - py_write_stmts(w, writer, &format!("_k{depth}"), k, depth + 1); - py_write_stmts(w, writer, &format!("_v{depth}"), v, depth + 1); - }); - } - } -} - -/// The expression decoding a borrowed `(ptr, len)` buffer pair (a -/// callback-interface method argument) into its idiomatic value. The producer -/// owns the buffer for the dispatch, so the bytes are copied before decoding; -/// any object tokens inside still transfer one strong reference, which the -/// decoded wrappers adopt. -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 ty.wire() { - WireType::User(name) => format!("{}({data})", py_unpack_fn_name(name)), - _ => format!("_decode_buffer({data}, lambda _r: {})", py_read_expr(ty, 0)), - } -} - -/// The expression decoding an *owned* `(ptr, len)` buffer pair (an async -/// 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: &Ty) -> String { - let data = format!("_take_buffer(ctypes.cast({ptr}, ctypes.c_void_p).value, {len})"); - match ty.wire() { - WireType::User(name) => format!("{}({data})", py_unpack_fn_name(name)), - _ => format!("_decode_buffer({data}, lambda _r: {})", py_read_expr(ty, 0)), - } -} - -/// Append a record's buffer codec functions: the statement writer, the -/// reader, and the standalone pack/unpack pair. -pub(crate) fn render_record_codecs(w: &mut CodeWriter, s: &StructBinding) { - let name = &s.name; - let write_fn = py_write_fn_name(name); - let read_fn = py_read_fn_name(name); - let pack_fn = py_pack_fn_name(name); - let unpack_fn = py_unpack_fn_name(name); - - w.blank().blank(); - w.line(format!( - "def {write_fn}(_w: _BufferWriter, value: \"{name}\") -> None:" - )); - w.scope(|w| { - if s.fields.is_empty() { - w.line("pass"); - } else { - for f in &s.fields { - py_write_stmts(w, "_w", &format!("value.{}", py_field(&f.name)), &f.ty, 0); - } - } - }); - - w.blank().blank(); - w.line(format!("def {read_fn}(_r: _BufferReader) -> \"{name}\":")); - w.scope(|w| { - if s.fields.is_empty() { - w.line(format!("return {name}()")); - } else { - // Keyword arguments evaluate left to right, matching the wire - // order of the record's fields. - w.line(format!("return {name}(")); - w.scope(|w| { - for f in &s.fields { - w.line(format!("{}={},", py_field(&f.name), py_read_expr(&f.ty, 0))); - } - }); - w.line(")"); - } - }); - - render_pack_unpack(w, name, &write_fn, &read_fn, &pack_fn, &unpack_fn); -} - -/// Append a rich enum's buffer codec functions: the statement writer, the -/// reader, and the standalone pack/unpack pair. -pub(crate) fn render_rich_enum_codecs(w: &mut CodeWriter, e: &EnumBinding) { - let name = &e.name; - let write_fn = py_write_fn_name(name); - let read_fn = py_read_fn_name(name); - let pack_fn = py_pack_fn_name(name); - let unpack_fn = py_unpack_fn_name(name); - - w.blank().blank(); - w.line(format!( - "def {write_fn}(_w: _BufferWriter, value: \"{name}\") -> None:" - )); - w.scope(|w| { - for v in &e.variants { - 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)); - for f in &v.fields { - py_write_stmts(w, "_w", &format!("value.{}", py_field(&f.name)), &f.ty, 0); - } - w.line("return"); - }); - } - w.line(format!( - "raise WeaveFFIError(-1, \"unknown {name} variant\")" - )); - }); - - w.blank().blank(); - w.line(format!("def {read_fn}(_r: _BufferReader) -> \"{name}\":")); - w.scope(|w| { - w.line("_tag = _r.read_i32()"); - for v in &e.variants { - let class = format!("{name}{}", py_variant(&v.name)); - w.line(format!("if _tag == {}:", v.value)); - w.scope(|w| { - if v.fields.is_empty() { - w.line(format!("return {class}()")); - } else { - // Keyword arguments evaluate left to right, matching the - // wire order of the variant's fields. - w.line(format!("return {class}(")); - w.scope(|w| { - for f in &v.fields { - w.line(format!("{}={},", py_field(&f.name), py_read_expr(&f.ty, 0))); - } - }); - w.line(")"); - } - }); - } - w.line(format!( - "raise WeaveFFIError(-1, f\"malformed value buffer: unknown {name} tag {{_tag}}\")" - )); - }); - - render_pack_unpack(w, name, &write_fn, &read_fn, &pack_fn, &unpack_fn); -} - -/// Append the standalone `_pack_X`/`_unpack_X` pair bridging one value type -/// to and from `bytes`. -fn render_pack_unpack( - w: &mut CodeWriter, - name: &str, - write_fn: &str, - read_fn: &str, - pack_fn: &str, - unpack_fn: &str, -) { - w.blank().blank(); - w.line(format!("def {pack_fn}(value: \"{name}\") -> bytes:")); - w.scope(|w| { - w.line("_w = _BufferWriter()"); - w.line(format!("{write_fn}(_w, value)")); - w.line("return _w.finish()"); - }); - - w.blank().blank(); - w.line(format!("def {unpack_fn}(data: bytes) -> \"{name}\":")); - w.scope(|w| { - w.line(format!("return _decode_buffer(data, {read_fn})")); - }); -} diff --git a/crates/weaveffi-gen-python/src/lib.rs b/crates/weaveffi-gen-python/src/lib.rs deleted file mode 100644 index e8433920..00000000 --- a/crates/weaveffi-gen-python/src/lib.rs +++ /dev/null @@ -1,329 +0,0 @@ -//! Python (`ctypes`) binding generator for WeaveFFI. -//! -//! Emits a pip-installable package containing `ctypes`-based bindings and -//! `.pyi` type stubs over the C ABI (revision 2). Records and rich enums are -//! dataclasses crossing the boundary as value buffers; interfaces are -//! reference-counted wrapper classes with `close()` and a `__del__` backstop; -//! callback interfaces are abstract base classes the consumer subclasses, -//! backed by one static vtable of `ctypes` trampolines per interface; async -//! functions surface as `async def` wrappers and `iter` returns as lazy -//! Python iterators. Implements [`LanguageBackend`]; the shared driver bridges -//! it into the generator pipeline. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod calls; -mod codec; -mod docs; -mod entities; -mod package; -mod runtime; -mod stubs; -#[cfg(test)] -mod tests; -mod types; - -use camino::Utf8Path; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::{ - BindingModel, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, - ModuleBinding, StructBinding, -}; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::pkg; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::calls::{render_callable, render_callback_interface, FnScope}; -use crate::entities::{render_enum, render_error, render_interface, render_struct}; -use crate::package::{ - render_packaged_readme, render_packaged_setup_py, render_py_typed, render_pyproject_toml, - render_readme, render_setup_py, -}; -use crate::runtime::{ - py_loader_packaged, render_feature_runtime, render_preamble, PY_LOADER_ORIGINAL, -}; -use crate::stubs::render_pyi_module; - -/// Per-target configuration for [`PythonGenerator`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct PythonConfig { - /// pip-installable Python package name (default `"weaveffi"`). Also - /// determines the on-disk package directory inside `python/`. - pub package_name: Option, - /// When `true` (the default), strip the IR module name prefix from - /// emitted Python function names, so a `contacts` module exports - /// `create_contact` rather than `contacts_create_contact`. Set to - /// `false` to restore module-prefixed names. - pub strip_module_prefix: bool, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the ctypes bindings call the same - /// exported symbols the producer emits. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with. - #[serde(skip)] - pub input_basename: Option, -} - -impl Default for PythonConfig { - fn default() -> Self { - Self { - package_name: None, - strip_module_prefix: true, - prefix: None, - input_basename: None, - } - } -} - -impl PythonConfig { - /// Returns the configured Python package name, falling back to `"weaveffi"`. - pub fn package_name(&self) -> &str { - self.package_name.as_deref().unwrap_or("weaveffi") - } - - /// Returns the configured C ABI symbol prefix, falling back to `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// Python backend: emits a pip-installable package of `ctypes` bindings and -/// `.pyi` type stubs over the C ABI exposed by the underlying cdylib. -pub struct PythonGenerator; - -impl PythonGenerator { - /// Render the primary `weaveffi.py` source by composing the shared - /// [`LanguageBackend::emit_members`] walk over every module. - fn render_py_source(&self, model: &BindingModel, config: &PythonConfig) -> String { - let mut out = render_prelude(CommentStyle::Hash, config.input_basename()); - render_preamble(&mut out); - render_feature_runtime(&mut out, model.has_async(), model.has_callback_interfaces()); - // The model is a flat, pre-order list of modules, each carrying its - // joined symbol path, the same traversal order the recursive walk - // produced. - for m in &model.modules { - out.push_str(&format!("\n\n# === Module: {} ===", m.path)); - self.emit_members(&mut out, m, config); - } - out.push('\n'); - out.push_str(&render_trailer(CommentStyle::Hash, "weaveffi.py")); - out - } -} - -impl LanguageBackend for PythonGenerator { - type Config = PythonConfig; - - fn name(&self) -> &'static str { - "python" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn render_error( - &self, - out: &mut String, - module: &ModuleBinding, - e: &ErrorBinding, - _config: &Self::Config, - ) { - render_error(out, module, e); - } - - fn render_enum(&self, out: &mut String, e: &EnumBinding, _config: &Self::Config) { - render_enum(out, e); - } - - fn render_interface( - &self, - out: &mut String, - module: &ModuleBinding, - i: &InterfaceBinding, - _config: &Self::Config, - ) { - render_interface(out, module, i); - } - - fn render_struct( - &self, - out: &mut String, - _module: &ModuleBinding, - s: &StructBinding, - _config: &Self::Config, - ) { - render_struct(out, s); - } - - fn render_callback_interface( - &self, - out: &mut String, - module: &ModuleBinding, - cb: &CallbackInterfaceBinding, - config: &Self::Config, - ) { - render_callback_interface(out, module, cb, config.prefix()); - } - - fn render_function( - &self, - out: &mut String, - module: &ModuleBinding, - f: &FnBinding, - config: &Self::Config, - ) { - render_callable( - out, - f, - module.error.as_ref(), - &FnScope::Free { - module_path: &module.path, - strip_module_prefix: config.strip_module_prefix, - }, - ); - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let package = pkg::resolve( - api, - config.package_name.as_deref(), - config.input_basename.as_deref(), - ); - let import_name = package.ident_name(); - let input_basename = config.input_basename(); - let dir = out_dir.join("python"); - let pkg_dir = dir.join(&import_name); - let hash = CommentStyle::Hash; - vec![ - OutputFile::new( - pkg_dir.join("__init__.py"), - format!( - "{}from .weaveffi import * # noqa: F401,F403\n\n{}", - render_prelude(hash, input_basename), - render_trailer(hash, "__init__.py"), - ), - ), - OutputFile::new( - pkg_dir.join("weaveffi.py"), - self.render_py_source(model, config), - ), - OutputFile::new( - pkg_dir.join("weaveffi.pyi"), - render_pyi_module(model, config.strip_module_prefix, input_basename), - ), - OutputFile::new(pkg_dir.join("py.typed"), render_py_typed(input_basename)), - OutputFile::new( - dir.join("pyproject.toml"), - render_pyproject_toml(&package, &import_name, input_basename), - ), - OutputFile::new( - dir.join("setup.py"), - render_setup_py(&package, &import_name, input_basename), - ), - OutputFile::new( - dir.join("README.md"), - render_readme(&package, input_basename), - ), - ] - } - - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let package = pkg::resolve( - api, - config.package_name.as_deref(), - config.input_basename.as_deref(), - ); - let import_name = package.ident_name(); - let input_basename = config.input_basename(); - let hash = CommentStyle::Hash; - - // Render the binding source once with the bundled-first loader, then - // reuse it across every per-platform wheel tree. - let py_source = self.render_py_source(model, config).replace( - PY_LOADER_ORIGINAL, - &py_loader_packaged(&ctx.binaries.lib_name), - ); - let init_py = format!( - "{}from .weaveffi import * # noqa: F401,F403\n\n{}", - render_prelude(hash, input_basename), - render_trailer(hash, "__init__.py"), - ); - let pyi = render_pyi_module(model, config.strip_module_prefix, input_basename); - let setup_py = render_packaged_setup_py(&package, &import_name, input_basename); - let pyproject = render_pyproject_toml(&package, &import_name, input_basename); - - let py_dir = out_dir.join("python"); - let mut files = Vec::new(); - // Wheels exist only for the platforms that have a wheel platform tag; - // a binary for any other platform (Android, wasm32) has no wheel to - // land in and is skipped. - for nb in &ctx.binaries.binaries { - let platform = nb.platform; - let Some(tag) = platform.python_platform_tag() else { - continue; - }; - let tree = py_dir.join(platform.id()); - let pkg_dir = tree.join(&import_name); - files.push(PackagedFile::text( - pkg_dir.join("__init__.py"), - init_py.clone(), - )); - files.push(PackagedFile::text( - pkg_dir.join("weaveffi.py"), - py_source.clone(), - )); - files.push(PackagedFile::text( - pkg_dir.join("weaveffi.pyi"), - pyi.clone(), - )); - files.push(PackagedFile::text( - pkg_dir.join("py.typed"), - render_py_typed(input_basename), - )); - files.push(PackagedFile::copy( - pkg_dir.join(ctx.binaries.bundled_filename(platform)), - nb.source.clone(), - )); - files.push(PackagedFile::text( - tree.join("pyproject.toml"), - pyproject.clone(), - )); - files.push(PackagedFile::text(tree.join("setup.py"), setup_py.clone())); - files.push(PackagedFile::text( - tree.join("README.md"), - render_packaged_readme(&package, &import_name, platform, tag, input_basename), - )); - } - Some(files) - } -} diff --git a/crates/weaveffi-gen-python/src/package.rs b/crates/weaveffi-gen-python/src/package.rs deleted file mode 100644 index 7b0ed050..00000000 --- a/crates/weaveffi-gen-python/src/package.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! Packaging manifests: `pyproject.toml`, `setup.py`, and `README.md` for -//! both the plain generate tree and the per-platform packaged wheel trees. -//! All of these are TOML, Python, or Markdown, so none route through the -//! shared JSON manifest builder. - -use weaveffi_core::pkg::ResolvedPackage; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -/// Render the `pyproject.toml` for the generated package. -pub(crate) fn render_pyproject_toml( - package: &ResolvedPackage, - import_name: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Hash, input_basename); - let trailer = render_trailer(CommentStyle::Hash, "pyproject.toml"); - let name = &package.name; - let version = &package.version; - let description = package.description_or_default(); - let mut extra = String::new(); - if let Some(license) = &package.license { - extra.push_str(&format!("license = {{ text = \"{license}\" }}\n")); - } - if !package.authors.is_empty() { - let authors = package - .authors - .iter() - .map(|a| format!("{{ name = \"{a}\" }}")) - .collect::>() - .join(", "); - extra.push_str(&format!("authors = [{authors}]\n")); - } - if let Some(homepage) = &package.homepage { - extra.push_str(&format!("[project.urls]\nHomepage = \"{homepage}\"\n")); - } else if let Some(repository) = &package.repository { - extra.push_str(&format!("[project.urls]\nRepository = \"{repository}\"\n")); - } - format!( - r#"{prelude}[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "{name}" -version = "{version}" -description = "{description}" -requires-python = ">=3.8" -{extra} -[tool.setuptools] -packages = ["{import_name}"] - -[tool.setuptools.package-data] -"{import_name}" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] - -{trailer}"#, - ) -} - -/// Render the PEP 561 `py.typed` marker. Type checkers only look for the -/// file's presence (its content is ignored unless it says `partial`), so it -/// carries the standard prelude like every other generated file. -pub(crate) fn render_py_typed(input_basename: &str) -> String { - render_prelude(CommentStyle::Hash, input_basename) -} - -/// Render the plain `setup.py` for a generated (non-packaged) tree. -pub(crate) fn render_setup_py( - package: &ResolvedPackage, - import_name: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Hash, input_basename); - let trailer = render_trailer(CommentStyle::Hash, "setup.py"); - let name = &package.name; - let version = &package.version; - format!( - r#"{prelude}from setuptools import setup - -setup( - name="{name}", - version="{version}", - packages=["{import_name}"], -) - -{trailer}"#, - ) -} - -/// Render the `README.md` for a generated (non-packaged) tree. -pub(crate) fn render_readme(package: &ResolvedPackage, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = &package.name; - let import_name = package.ident_name(); - format!( - r#"{prelude}# {name} (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 {import_name} import * -``` - -{trailer}"# - ) -} - -/// Render a `setup.py` for a packaged wheel: it ships the bundled library as -/// package data and forces a non-pure (platform-tagged) wheel. -pub(crate) fn render_packaged_setup_py( - package: &ResolvedPackage, - import_name: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Hash, input_basename); - let trailer = render_trailer(CommentStyle::Hash, "setup.py"); - let name = &package.name; - let version = &package.version; - format!( - r#"{prelude}from setuptools import setup -from setuptools.dist import Distribution - - -class _BinaryDistribution(Distribution): - # Force a non-pure, platform-tagged wheel: the package bundles a native - # shared library, so it is not portable across platforms. - def has_ext_modules(self): - return True - - -setup( - name="{name}", - version="{version}", - packages=["{import_name}"], - package_data={{"{import_name}": ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"]}}, - include_package_data=True, - distclass=_BinaryDistribution, -) - -{trailer}"#, - ) -} - -/// README for a packaged per-platform Python wheel tree. `tag` is the -/// platform's wheel tag (`platform.python_platform_tag()`), which the caller -/// has already established exists. -pub(crate) fn render_packaged_readme( - package: &ResolvedPackage, - import_name: &str, - platform: weaveffi_core::platform::Platform, - tag: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = &package.name; - format!( - r#"{prelude}# {name} (Python, {plat}) - -Auto-generated Python bindings with the native library bundled for `{plat}`. -The library loads automatically; no external setup is required. - -## Build the wheel - -```bash -python -m build --wheel -``` - -Tag the resulting wheel for this platform with `{tag}` (for example via -`wheel tags --platform-tag {tag} dist/*.whl`) before publishing. - -## Usage - -```python -from {import_name} import * -``` - -{trailer}"#, - plat = platform.id(), - ) -} diff --git a/crates/weaveffi-gen-python/src/runtime.rs b/crates/weaveffi-gen-python/src/runtime.rs deleted file mode 100644 index b63e710e..00000000 --- a/crates/weaveffi-gen-python/src/runtime.rs +++ /dev/null @@ -1,476 +0,0 @@ -//! The emitted Python runtime prelude: error base classes, library loading, -//! the ABI-revision check, the runtime symbol bindings (`error_set`, -//! `error_clear`, `error_free`, `free_string`, `free_bytes`), the -//! owned-pointer helpers, the `_BufferWriter`/`_BufferReader` pair -//! implementing the value-buffer wire format (including object tokens), and -//! the feature-gated async and callback-interface registries. - -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. -pub(crate) fn render_preamble(out: &mut String) { - out.push_str( - r#""""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): - """An error reported through the C ABI. - - Positive codes are a module's declared error codes (raised as the - module's typed subclasses); negative codes are runtime traps: a generic - producer error (-1), a producer panic (-2), a marshalling failure (-3), - or an exception raised by a callback-interface implementation (-4). - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - - 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() -"#, - ); - render_abi_version_check(out); - out.push_str( - r#"# Callback-interface trampolines report a failed implementation through -# weaveffi_error_set, which copies the message with the producer's allocator. -_lib.weaveffi_error_set.argtypes = [ - ctypes.POINTER(_WeaveFFIErrorStruct), - ctypes.c_int32, - ctypes.c_char_p, -] -_lib.weaveffi_error_set.restype = None -_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 - - -def _borrow(obj): - """The raw pointer of a live object wrapper, lent to the producer for the - duration of one call. Raises if the wrapper was already closed.""" - _p = obj._ptr - if _p is None: - raise WeaveFFIError(-1, f"{type(obj).__name__} used after close()") - return _p - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - self._objects = [] - - def finish(self) -> bytes: - # Mint the object tokens last: an encoding that raised part-way - # (a closed wrapper, a field of the wrong type) never reaches here, - # so it leaks no strong references. - for _pos, _obj in self._objects: - struct.pack_into(" 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) - - def write_object(self, obj) -> None: - """Write an object token: one strong reference to `obj` the reader - will adopt. The wrapper is checked to be live now (a closed one - raises), but the reference itself is cloned in finish().""" - _borrow(obj) - self._objects.append((len(self._buf), obj)) - self._buf += bytes(8) - - -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 read_object(self) -> int: - """Read an object token: one strong reference the caller adopts into - a wrapper. Zero never appears in a non-optional position.""" - _p = struct.unpack(" 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 -"#, - ); -} - -/// Append the feature-gated runtime pieces after the fixed preamble: the -/// `asyncio` completion registry when the API has async callables, and the -/// callback-interface handle table (plus the `abc` import the abstract base -/// classes need) when it declares callback interfaces. Both registries share -/// one `threading` import. -pub(crate) fn render_feature_runtime(out: &mut String, has_async: bool, has_callbacks: bool) { - if !has_async && !has_callbacks { - return; - } - out.push('\n'); - if has_callbacks { - out.push_str("import abc\n"); - } - if has_async { - out.push_str("import asyncio\n"); - } - out.push_str("import threading\n"); - if has_async { - out.push_str( - r#" - -# 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 -"#, - ); - } - if has_callbacks { - out.push_str( - r#" - -# Live callback-interface implementations, keyed by the integer the producer -# receives as `ctx`. An entry is added when an implementation is passed to a -# call and removed when the producer invokes the vtable's `free(ctx)`, so the -# implementation stays alive exactly as long as the producer may call it and -# the producer never holds a raw reference to a Python object. -_cb_impls: Dict[int, object] = {} -_cb_lock = threading.Lock() -_cb_next_ctx = 0 - - -def _cb_register(impl) -> int: - global _cb_next_ctx - with _cb_lock: - _cb_next_ctx += 1 - _ctx = _cb_next_ctx - _cb_impls[_ctx] = impl - return _ctx -"#, - ); - } -} - -/// The exact `_load_library` block [`render_preamble`] emits in `generate` -/// mode, so the packager can swap it for a bundled-first variant. -pub(crate) const PY_LOADER_ORIGINAL: &str = r#"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)"#; - -/// The packaged `_load_library` for `lib`: prefer the per-platform library -/// bundled next to the module, then `WEAVEFFI_LIBRARY`, then the system path. -pub(crate) fn py_loader_packaged(lib: &str) -> String { - format!( - r#"def _load_library() -> ctypes.CDLL: - # A bundled per-platform library ships next to this module; prefer it so the - # package works with no external setup. WEAVEFFI_LIBRARY still overrides. - override = os.environ.get("WEAVEFFI_LIBRARY") - if override: - return ctypes.CDLL(override) - here = os.path.dirname(os.path.abspath(__file__)) - system = platform.system() - if system == "Darwin": - name = "lib{lib}.dylib" - elif system == "Windows": - name = "{lib}.dll" - else: - name = "lib{lib}.so" - bundled = os.path.join(here, name) - if os.path.exists(bundled): - return ctypes.CDLL(bundled) - return ctypes.CDLL(name)"# - ) -} diff --git a/crates/weaveffi-gen-python/src/tests.rs b/crates/weaveffi-gen-python/src/tests.rs deleted file mode 100644 index b37ad6fb..00000000 --- a/crates/weaveffi-gen-python/src/tests.rs +++ /dev/null @@ -1,471 +0,0 @@ -//! Unit tests: render a small API that exercises every ABI revision 2 shape -//! (reference-counted objects, nullable objects, objects inside buffers, -//! iterators of objects, and callback interfaces) and assert the generated -//! Python carries the pieces each contract clause requires. - -use camino::Utf8Path; -use weaveffi_core::backend::LanguageBackend; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::PackageContext; -use weaveffi_core::platform::{BinarySet, Platform}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::validate::validate_api; -use weaveffi_ir::ir::{ - Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, - TypeRef, CURRENT_SCHEMA_VERSION, -}; - -use crate::{PythonConfig, PythonGenerator}; - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } -} - -fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn named(name: &str) -> TypeRef { - TypeRef::Named(name.into()) -} - -fn module(name: &str) -> Module { - Module { - name: name.into(), - doc: None, - functions: vec![], - interfaces: vec![], - callback_interfaces: vec![], - structs: vec![], - enums: vec![], - errors: None, - modules: vec![], - } -} - -/// The fixture the brief asks for: an interface with a constructor and a -/// method; a function taking and returning `Interface?`; a record with an -/// `Interface` field and a `[Interface]` field; an iterator over interface -/// elements; a callback interface whose methods take a string, an `i32`, a -/// record, and an object, one returning `bool` and one returning void; and a -/// function taking that callback interface. Routed through validation so the -/// fixture is known to be a legal IDL. -fn kv_api() -> ResolvedApi { - let kv = Module { - structs: vec![StructDef { - name: "Entry".into(), - doc: None, - deprecated: None, - fields: vec![ - field("key", TypeRef::StringUtf8), - field("store", named("Store")), - field("mirrors", TypeRef::List(Box::new(named("Store")))), - field("backup", TypeRef::Optional(Box::new(named("Store")))), - ], - }], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store.".into()), - deprecated: None, - constructors: vec![func("new", vec![param("path", TypeRef::StringUtf8)], None)], - methods: vec![ - func( - "get", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - ), - func( - "sibling", - vec![], - Some(TypeRef::Optional(Box::new(named("Store")))), - ), - ], - statics: vec![], - }], - callback_interfaces: vec![CallbackInterfaceDef { - name: "Watcher".into(), - doc: Some("Observes store changes.".into()), - deprecated: None, - methods: vec![ - func( - "on_change", - vec![ - param("text", TypeRef::StringUtf8), - param("weight", TypeRef::I32), - param("entry", named("Entry")), - param("store", named("Store")), - ], - None, - ), - func( - "should_stop", - vec![param("count", TypeRef::I32)], - Some(TypeRef::Bool), - ), - ], - }], - functions: vec![ - func( - "pick", - vec![param("store", TypeRef::Optional(Box::new(named("Store"))))], - Some(TypeRef::Optional(Box::new(named("Store")))), - ), - func("watch", vec![param("watcher", named("Watcher"))], None), - func( - "all_stores", - vec![], - Some(TypeRef::Iterator(Box::new(named("Store")))), - ), - Function { - r#async: true, - ..func( - "fetch", - vec![param("id", TypeRef::I64)], - Some(named("Store")), - ) - }, - ], - ..module("kv") - }; - let api = Api { - version: CURRENT_SCHEMA_VERSION.into(), - modules: vec![kv], - }; - validate_api(api, None).unwrap_or_else(|d| panic!("fixture must validate: {d:?}")) -} - -/// Render the fixture and return `(weaveffi.py, weaveffi.pyi)`. -fn render(api: &ResolvedApi) -> (String, String) { - let config = PythonConfig::default(); - let model = BindingModel::build(api, config.prefix()); - let files = PythonGenerator.files(api, &model, Utf8Path::new("out"), &config); - let find = |suffix: &str| { - files - .iter() - .find(|f| f.path.as_str().ends_with(suffix)) - .unwrap_or_else(|| panic!("missing {suffix}")) - .contents - .clone() - }; - (find("weaveffi.py"), find("weaveffi.pyi")) -} - -fn assert_has(hay: &str, needle: &str) { - assert!(hay.contains(needle), "missing `{needle}` in:\n{hay}"); -} - -#[test] -fn runtime_binds_error_set_and_drops_legacy_symbols() { - let (py, pyi) = render(&kv_api()); - assert_has(&py, "_ABI_VERSION = 2\n"); - assert_has(&py, "_lib.weaveffi_error_set.argtypes = ["); - assert_has(&py, "FOREIGN_ERROR_CODE = -4"); - for legacy in ["arena", "handle_t", "listener", "register_"] { - assert!(!py.contains(legacy), "legacy `{legacy}` survives in:\n{py}"); - assert!( - !pyi.contains(legacy), - "legacy `{legacy}` survives in:\n{pyi}" - ); - } - assert_has(&pyi, " FOREIGN_ERROR_CODE: int\n"); -} - -#[test] -fn objects_are_reference_counted_wrappers() { - let (py, pyi) = render(&kv_api()); - // The clone/destroy pair is bound once at module scope. - assert_has( - &py, - "_lib.weaveffi_kv_Store_clone.argtypes = [ctypes.c_void_p]", - ); - assert_has( - &py, - "_lib.weaveffi_kv_Store_clone.restype = ctypes.c_void_p", - ); - assert_has( - &py, - "_lib.weaveffi_kv_Store_destroy.argtypes = [ctypes.c_void_p]", - ); - // Adoption, a second reference, and single disposal. - assert_has(&py, "def _from_ptr(cls, ptr) -> \"Store\":"); - assert_has(&py, "return _lib.weaveffi_kv_Store_clone(_borrow(self))"); - assert_has(&py, " def close(self) -> None:\n"); - assert_has(&py, " _lib.weaveffi_kv_Store_destroy(_p)"); - assert_has(&py, " def __del__(self) -> None:\n self.close()"); - assert_has( - &py, - " def __exit__(self, *exc) -> bool:\n self.close()", - ); - // The constructor adopts the returned reference; methods lend `self`. - assert_has(&py, "def __init__(self, path: str) -> None:"); - assert_has(&py, "_fn = _lib.weaveffi_kv_Store_new"); - assert_has(&py, "self._ptr = _result"); - assert_has( - &py, - "_result = _fn(_borrow(self), _string_to_bytes(key), ctypes.byref(_err))", - ); - assert_has(&py, "return _take_string(_result) or \"\""); - // Stubs mirror the disposal surface. - assert_has(&pyi, " def close(self) -> None: ...\n"); - assert_has(&pyi, " def __enter__(self) -> \"Store\": ...\n"); -} - -#[test] -fn nullable_objects_map_none_to_null_in_both_directions() { - let (py, pyi) = render(&kv_api()); - assert_has( - &py, - "def pick(store: Optional[\"Store\"]) -> Optional[\"Store\"]:", - ); - assert_has( - &py, - "_result = _fn((_borrow(store) if store is not None else None), ctypes.byref(_err))", - ); - assert_has( - &py, - " if _result is None:\n return None\n return Store._from_ptr(_result)", - ); - // A non-nullable object return traps on null instead. - assert_has( - &py, - "if _result is None:\n raise WeaveFFIError(-1, \"null pointer\")", - ); - assert_has(&py, "def sibling(self) -> Optional[\"Store\"]:"); - assert_has( - &pyi, - "def pick(store: Optional[\"Store\"]) -> Optional[\"Store\"]: ...", - ); -} - -#[test] -fn objects_inside_buffers_are_cloned_on_write_and_adopted_on_read() { - let (py, _) = render(&kv_api()); - // Record field: hand the wrapper to the writer (which clones it when the - // buffer is finished), adopt the token on read. - assert_has(&py, "_w.write_object(value.store)"); - assert_has(&py, "store=Store._from_ptr(_r.read_object()),"); - // List field. - assert_has( - &py, - "for _e0 in value.mirrors:\n _w.write_object(_e0)", - ); - assert_has( - &py, - "mirrors=[Store._from_ptr(_r.read_object()) for _i0 in range(_r.read_len())],", - ); - // Optional field keeps the ordinary flag byte. - assert_has( - &py, - "backup=(Store._from_ptr(_r.read_object()) if _r.read_option_flag() else None),", - ); - assert_has(&py, "def read_object(self) -> int:"); - assert_has(&py, "def write_object(self, obj) -> None:"); - // Tokens are minted in finish(), so a failed encoding leaks no reference. - assert_has( - &py, - "struct.pack_into(\" \"Store\":"); - assert_has(&py, "_state[\"val\"] = Store._from_ptr(result)"); -} - -#[test] -fn callback_interfaces_render_abc_vtable_and_trampolines() { - let (py, pyi) = render(&kv_api()); - assert_has(&py, "import abc\n"); - assert_has(&py, "_cb_impls: Dict[int, object] = {}"); - - // The consumer-facing abstract base class. - assert_has(&py, "class Watcher(abc.ABC):"); - assert_has(&py, "Observes store changes."); - assert_has( - &py, - " @abc.abstractmethod\n def on_change(self, text: str, weight: int, entry: \"Entry\", store: \"Store\") -> None:", - ); - assert_has( - &py, - " @abc.abstractmethod\n def should_stop(self, count: int) -> bool:", - ); - - // CFUNCTYPEs mirror the vtable entry signatures: ctx, slots, out_err. - assert_has( - &py, - "_Watcher_on_change_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct))", - ); - assert_has( - &py, - "_Watcher_should_stop_cfunctype = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct))", - ); - assert_has( - &py, - "_Watcher_vtable_free_cfunctype = ctypes.CFUNCTYPE(None, ctypes.c_void_p)", - ); - - // The vtable struct: methods in declaration order, then `free`. - assert_has(&py, "class _WatcherVtable(ctypes.Structure):"); - assert_has(&py, "The C vtable `weaveffi_kv_Watcher_vtable`."); - assert_has( - &py, - " (\"on_change\", _Watcher_on_change_cfunctype),\n (\"should_stop\", _Watcher_should_stop_cfunctype),\n (\"free\", _Watcher_vtable_free_cfunctype),\n", - ); - - // Trampolines decode borrowed args, adopt objects, and never unwind. - assert_has( - &py, - "def _Watcher_on_change_trampoline(ctx, text, weight, entry_ptr, entry_len, store, out_err):", - ); - assert_has(&py, "_impl = _cb_impls[ctx]"); - assert_has( - &py, - "_impl.on_change(_bytes_to_string(text), weight, _unpack_Entry(ctypes.string_at(entry_ptr, entry_len) if entry_ptr else b\"\"), Store._from_ptr(store))", - ); - assert_has( - &py, - "def _Watcher_should_stop_trampoline(ctx, count, out_err):", - ); - assert_has( - &py, - "_ret = _impl.should_stop(count)\n return 1 if _ret else 0", - ); - assert_has(&py, "except Exception as _exc:"); - assert_has( - &py, - "_lib.weaveffi_error_set(out_err, -4, str(_exc).encode(\"utf-8\", \"replace\"))\n return 0\n", - ); - assert_has( - &py, - "def _Watcher_vtable_free_trampoline(ctx):\n # The producer's last reference is gone; it never touches `ctx` again.\n _cb_impls.pop(ctx, None)", - ); - - // Exactly one static vtable whose function objects are pinned. - assert_has( - &py, - "_Watcher_on_change_cfunc = _Watcher_on_change_cfunctype(_Watcher_on_change_trampoline)", - ); - assert_has( - &py, - "_Watcher_vtable = _WatcherVtable(_Watcher_on_change_cfunc, _Watcher_should_stop_cfunc, _Watcher_vtable_free_cfunc)", - ); - assert_eq!(py.matches("_Watcher_vtable = _WatcherVtable(").count(), 1); - - // Passing an implementation: register, then ctx + vtable address. - assert_has(&py, "def watch(watcher: \"Watcher\") -> None:"); - assert_has( - &py, - "_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WatcherVtable), ctypes.POINTER(_WeaveFFIErrorStruct)]", - ); - assert_has(&py, "_watcher_ctx = _cb_register(watcher)"); - assert_has( - &py, - "_fn(_watcher_ctx, ctypes.byref(_Watcher_vtable), ctypes.byref(_err))", - ); - - // Stubs. - assert_has(&pyi, "from abc import ABC, abstractmethod\n"); - assert_has(&pyi, "class Watcher(ABC):"); - assert_has( - &pyi, - " @abstractmethod\n def on_change(self, text: str, weight: int, entry: \"Entry\", store: \"Store\") -> None: ...\n", - ); - assert_has(&pyi, "def watch(watcher: \"Watcher\") -> None: ..."); -} - -#[test] -fn feature_runtime_is_emitted_only_when_used() { - let plain = Module { - functions: vec![func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - )], - ..module("math") - }; - let api = validate_api( - Api { - version: CURRENT_SCHEMA_VERSION.into(), - modules: vec![plain], - }, - None, - ) - .unwrap(); - let (py, _) = render(&api); - assert!(!py.contains("import abc")); - assert!(!py.contains("import asyncio")); - assert!(!py.contains("_cb_impls")); - assert_has(&py, "def add(a: int, b: int) -> int:"); -} - -#[test] -fn generated_source_is_deterministic() { - let api = kv_api(); - assert_eq!(render(&api), render(&api)); -} - -#[test] -fn package_skips_platforms_without_a_wheel_tag() { - let api = kv_api(); - let config = PythonConfig::default(); - let model = BindingModel::build(&api, config.prefix()); - let mut binaries = BinarySet::new("kv"); - for p in Platform::ALL { - binaries.insert( - p, - format!("/tmp/{}/{}", p.id(), binaries.bundled_filename(p)), - ); - } - let ctx = PackageContext { - binaries: &binaries, - input_basename: Some("kv.yml"), - }; - let files = PythonGenerator - .package(&api, &model, &ctx, Utf8Path::new("out"), &config) - .expect("python packages"); - let trees: std::collections::BTreeSet<&str> = files - .iter() - .filter_map(|f| f.path.as_str().strip_prefix("out/python/")) - .filter_map(|rest| rest.split('/').next()) - .collect(); - let expected: std::collections::BTreeSet<&str> = - Platform::DESKTOP.iter().map(|p| p.id()).collect(); - assert_eq!(trees, expected); - for p in [ - Platform::AndroidArm64, - Platform::AndroidX64, - Platform::Wasm32, - ] { - assert!(!files.iter().any(|f| f.path.as_str().contains(p.id()))); - } -} diff --git a/crates/weaveffi-gen-python/src/types.rs b/crates/weaveffi-gen-python/src/types.rs deleted file mode 100644 index db1c21a9..00000000 --- a/crates/weaveffi-gen-python/src/types.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! Python type mapping and naming: the `ctypes` vocabulary for ABI slots, -//! typing hints for signatures, and the identifier policy applied to -//! user-chosen IDL names before they land in generated Python. - -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}; - -/// 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: &Ty) -> &'static str { - match ty { - 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 => "ctypes.c_char_p", - // Interfaces and iterators cross as opaque pointers; `Interface?` is - // the same slot with null meaning none. - Ty::Interface(_) | Ty::Iterator(_) => "ctypes.c_void_p", - Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => "ctypes.c_void_p", - Ty::Bytes => "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") - } - Ty::CallbackInterface(_) => { - unreachable!("callback interfaces occupy a ctx + vtable slot pair") - } - } -} - -/// The Python typing hint for `ty` as it appears in signatures and stubs. -pub(crate) fn py_type_hint(ty: &Ty) -> String { - match ty { - Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::I64 | Ty::U64 => "int".into(), - Ty::F32 | Ty::F64 => "float".into(), - Ty::Bool => "bool".into(), - Ty::StringUtf8 => "str".into(), - Ty::Bytes => "bytes".into(), - // Records, rich enums, plain enums, interfaces, and callback - // 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. - Ty::Enum(name) => format!("\"{}\"", local_type_name(name)), - Ty::Record(name) | Ty::RichEnum(name) | Ty::Interface(name) => { - format!("\"{}\"", local_type_name(name)) - } - Ty::CallbackInterface(name) => format!("\"{}\"", local_type_name(name)), - 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)), - } -} - -/// Maps a shared ABI [`CType`] onto its `ctypes` spelling. The structural -/// lowering (which slots exist, in what order) comes from -/// [`weaveffi_core::abi`]; this is the Python-specific vocabulary applied to -/// each slot. Opaque object and vtable pointers collapse to `c_void_p`; -/// `char*` becomes the `c_char_p` convenience type; the error struct is the -/// emitted `_WeaveFFIErrorStruct` so `error*` slots are typed pointers. -pub(crate) fn py_ctype(ty: &CType) -> String { - match ty { - CType::Int8 => "ctypes.c_int8".into(), - CType::Int16 => "ctypes.c_int16".into(), - CType::Int32 => "ctypes.c_int32".into(), - CType::Uint16 => "ctypes.c_uint16".into(), - CType::Uint32 => "ctypes.c_uint32".into(), - CType::Int64 => "ctypes.c_int64".into(), - CType::Uint64 => "ctypes.c_uint64".into(), - CType::Float => "ctypes.c_float".into(), - CType::Double => "ctypes.c_double".into(), - CType::Bool => "ctypes.c_int32".into(), - CType::Size => "ctypes.c_size_t".into(), - CType::Char => "ctypes.c_char".into(), - CType::Uint8 => "ctypes.c_uint8".into(), - CType::Void => "None".into(), - CType::Enum { .. } => "ctypes.c_int32".into(), - CType::Error => "_WeaveFFIErrorStruct".into(), - CType::CancelToken - | CType::StructTag { .. } - | CType::VtableTag { .. } - | CType::Named(_) => "ctypes.c_void_p".into(), - CType::Ptr { pointee, .. } => match pointee.as_ref() { - CType::Char => "ctypes.c_char_p".into(), - CType::StructTag { .. } - | CType::VtableTag { .. } - | CType::CancelToken - | CType::Void - | CType::Named(_) => "ctypes.c_void_p".into(), - other => format!("ctypes.POINTER({})", py_ctype(other)), - }, - } -} - -/// The module-level `ctypes.Structure` subclass describing one callback -/// interface's vtable. `name` may be a qualified IR reference; the emitted -/// class uses the bare local name. -pub(crate) fn py_vtable_class_name(name: &str) -> String { - format!("_{}Vtable", local_type_name(name)) -} - -/// The module-level static vtable instance for one callback interface: the -/// single process-wide value whose address every call passing that -/// interface hands to the producer. -pub(crate) fn py_vtable_instance_name(name: &str) -> String { - format!("_{}_vtable", local_type_name(name)) -} - -/// The `ctypes` argtypes one parameter contributes, in slot order, driven by -/// its [`ArgPass`] contract. -pub(crate) fn py_param_argtypes(p: &ParamBinding) -> Vec { - match p.arg_pass() { - // A buffered parameter is passed as an immutable Python `bytes` - // object; `c_char_p` (rather than `POINTER(c_uint8)`) is the ctypes - // argtype that accepts `bytes` directly for a `const uint8_t*` slot. - ArgPass::Buffer { .. } => vec!["ctypes.c_char_p".into(), "ctypes.c_size_t".into()], - ArgPass::Bytes { ptr, len } => vec![py_ctype(&ptr.ty), py_ctype(&len.ty)], - ArgPass::Direct { slot } | ArgPass::String { slot } | ArgPass::Object { slot, .. } => { - vec![py_ctype(&slot.ty)] - } - // The context slot carries the handle-table key; the vtable slot is - // typed against the interface's own Structure so `byref` of the - // static instance is accepted. - ArgPass::Callback { ctx, .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback family names a callback interface"); - vec![ - py_ctype(&ctx.ty), - format!("ctypes.POINTER({})", py_vtable_class_name(cb)), - ] - } - } -} - -/// Returns `(restype, out_param_argtypes)` for a return type. -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, Ty::Iterator(_)) { - return ("ctypes.c_void_p".into(), vec![]); - } - // Module and prefix only shape an object return's destroy symbol, which - // the ctypes signature never names; empty context is fine here. - match plan::ret_pass(Some(ty), "", "") { - // A buffered return keeps its raw address (`c_void_p`) so the wrapper - // can copy the encoded bytes and release them with - // `weaveffi_free_bytes`. - RetPass::Buffer => ( - "ctypes.c_void_p".into(), - vec!["ctypes.POINTER(ctypes.c_size_t)".into()], - ), - // An owned string return keeps its raw address so the wrapper can - // copy it and pass it back to `weaveffi_free_string`; a `c_char_p` - // restype would be auto-converted to `bytes`, losing the pointer. - RetPass::String => ("ctypes.c_void_p".into(), vec![]), - _ => { - let r = abi::lower_return(ty, ""); - let out = r.out_params.iter().map(|p| py_ctype(&p.ty)).collect(); - (py_ctype(&r.ret), out) - } - } -} - -/// The Python spelling of an IDL value identifier (parameter name): -/// snake_case via heck, then keyword-escaped. IDL names are usually already -/// snake, so the case conversion is a safety net for camelCase inputs, and -/// the escape guards names like `class` or `import`. -pub(crate) fn py_name(name: &str) -> String { - lang::escape_ident(&name.to_snake_case(), lang::PYTHON_KEYWORDS) -} - -/// The Python spelling of an IDL field name, emitted verbatim except for -/// keyword escaping (a field named `class` becomes `class_`). -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 { - lang::escape_ident(&name.to_snake_case(), lang::PYTHON_KEYWORDS) -} - -/// The Python spelling of a module-level wrapper (free function): the -/// module-path prefix applied per config, then snake_case, then keyword -/// escaping. -pub(crate) fn py_wrapper_fn_name( - module_path: &str, - name: &str, - strip_module_prefix: bool, -) -> String { - lang::escape_ident( - &wrapper_name(module_path, name, strip_module_prefix).to_snake_case(), - lang::PYTHON_KEYWORDS, - ) -} - -/// Escape a string for embedding in a double-quoted Python literal. -pub(crate) fn py_str_literal(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") -} diff --git a/crates/weaveffi-gen-ruby/Cargo.toml b/crates/weaveffi-gen-ruby/Cargo.toml deleted file mode 100644 index eda91771..00000000 --- a/crates/weaveffi-gen-ruby/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "weaveffi-gen-ruby" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "WeaveFFI Ruby FFI binding generator" -keywords = ["ffi", "codegen", "bindings", "ruby"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-ruby/src/calls.rs b/crates/weaveffi-gen-ruby/src/calls.rs deleted file mode 100644 index 0a826e36..00000000 --- a/crates/weaveffi-gen-ruby/src/calls.rs +++ /dev/null @@ -1,871 +0,0 @@ -//! Callable rendering: FFI attachments, the sync/async/iterator wrapper -//! bodies, and the parameter and return marshalling they share. -//! -//! Marshalling dispatch goes through the shared plan layer ([`ArgPass`], -//! [`RetPass`], [`Free`]) rather than crate-local `Ty` folds, so this -//! backend cannot drift from the others on call-boundary semantics: objects -//! are borrowed as parameters and adopted as returns, and a callback -//! interface crosses as a registry key plus the interface's static vtable. - -use heck::ToSnakeCase; -use weaveffi_core::abi::CType; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{ - AsyncBinding, CallShape, FnBinding, IteratorBinding, ModuleBinding, ParamBinding, -}; -use weaveffi_core::plan::{self, ArgPass, ErrorStrategy, Free, RetPass}; -use weaveffi_core::utils::{local_type_name, wrapper_name}; - -use crate::callbacks::rb_vtable_const; -use crate::docs::{emit_doc, emit_param_docs}; -use crate::entities::{rb_checker_name, rb_error_factory_name}; -use crate::types::{ - rb_abi_types, rb_direct_from_c, rb_ffi_type, rb_mem_type, rb_param_name, rb_read_method, -}; - -/// How a rendered Ruby callable is scoped and spelled in the generated -/// module: at module scope as a singleton method, or inside an interface -/// class as a constructor, instance method, or class method. -pub(crate) enum RbScope<'a> { - /// A module-level free function (`def self.name` on the top-level module). - Free { - /// The owning module's underscore-joined path. - module_path: &'a str, - /// Whether the emitted name drops the module-path prefix. - strip_module_prefix: bool, - }, - /// An instance method on an interface class: `def name`, passing the - /// wrapper's borrowed `handle` as the leading C argument. - Method { - /// The top-level Ruby module name qualifying module singleton calls. - module_name: &'a str, - }, - /// A static member of an interface class (`def self.name`). - Static { - /// The top-level Ruby module name qualifying module singleton calls. - module_name: &'a str, - }, - /// A non-`new` constructor: a class method wrapping the returned owned - /// pointer via `_from_ptr` (never re-running `initialize`). - Factory { - /// The top-level Ruby module name qualifying module singleton calls. - module_name: &'a str, - }, - /// The canonical `new` constructor, emitted as `initialize`. - Init { - /// The top-level Ruby module name qualifying module singleton calls. - module_name: &'a str, - /// The interface's `FFI::AutoPointer` subclass wrapping the handle. - ptr_class: &'a str, - }, -} - -impl<'a> RbScope<'a> { - /// The top-level Ruby module name when calls must be explicitly - /// qualified (inside a class body); `None` at module scope, where the - /// implicit `self` already is the module. - fn module_name(&self) -> Option<&'a str> { - match self { - RbScope::Free { .. } => None, - RbScope::Method { module_name } - | RbScope::Static { module_name } - | RbScope::Factory { module_name } - | RbScope::Init { module_name, .. } => Some(module_name), - } - } - - /// The receiver prefix for module singleton calls (attached C symbols, - /// error checkers, `weaveffi_free_*`): `"{ModuleName}."` inside a class - /// body, empty at module scope. - fn qualifier(&self) -> String { - self.module_name() - .map(|m| format!("{m}.")) - .unwrap_or_default() - } - - /// Two-space indent depth of the `def` line (1 at module scope, 2 inside - /// an interface class). - fn depth(&self) -> usize { - if self.module_name().is_none() { - 1 - } else { - 2 - } - } - - /// The borrowed object pointer instance methods pass as the leading C - /// slot, through the `handle` accessor so a closed wrapper raises instead - /// of passing NULL. - fn self_arg(&self) -> Option<&'static str> { - matches!(self, RbScope::Method { .. }).then_some("handle") - } - - /// The `def` opener for `f` with the given formal parameter names. - fn def_open(&self, f: &FnBinding, params: &[String]) -> String { - let args = params.join(", "); - match self { - RbScope::Free { - module_path, - strip_module_prefix, - } => format!( - "def self.{}({args})", - wrapper_name(module_path, &f.name, *strip_module_prefix).to_snake_case() - ), - RbScope::Method { .. } => format!("def {}({args})", f.name.to_snake_case()), - RbScope::Static { .. } | RbScope::Factory { .. } => { - format!("def self.{}({args})", f.name.to_snake_case()) - } - RbScope::Init { .. } => format!("def initialize({args})"), - } - } -} - -/// Render one callable: a free function or an interface member. `module` -/// supplies the error domain for throwing callables; `scope` picks the def -/// spelling, receiver, indent, and result handling. Sync, async, and -/// iterator shapes all route through here so members reuse the free-function -/// marshalling paths. -pub(crate) fn render_callable( - out: &mut String, - module: &ModuleBinding, - f: &FnBinding, - scope: &RbScope, -) { - match &f.shape { - CallShape::Sync(_) => render_sync_function_wrapper(out, module, f, scope), - CallShape::Async(a) => render_async_function_wrapper(out, module, f, a, scope), - CallShape::Iterator(it) => render_iterator_function_wrapper(out, module, f, it, scope), - } -} - -/// Attach the C symbols for one callable: the plain symbol for a sync shape, -/// the completion callback type plus launcher for an async shape, and the -/// launch/next/destroy triple for an iterator. -pub(crate) fn render_attach_function(out: &mut String, f: &FnBinding) { - let mut w = CodeWriter::two_space().with_depth(1); - let mut d = String::new(); - emit_doc(&mut d, &f.doc, " "); - w.raw(d); - match &f.shape { - CallShape::Sync(abi) => { - w.line(format!( - "attach_function :{}, [{}], {}", - abi.symbol, - rb_abi_types(&abi.params, false).join(", "), - rb_ffi_type(&abi.ret, true) - )); - } - CallShape::Async(a) => { - // Completion callback: result strings/bytes stay `:pointer` - // (the wrapper owns and frees them); the launcher takes the - // declared callback type plus the opaque context. - w.line(format!( - "callback :{}, [{}], :void", - a.callback_type, - rb_abi_types(&a.callback_params, true).join(", ") - )); - let argtypes: Vec = a - .launch - .params - .iter() - .map(|p| match &p.ty { - // The `callback` slot is lowered as a Named C type; bind - // it to the callback symbol declared above. - CType::Named(_) => format!(":{}", a.callback_type), - ty => rb_ffi_type(ty, false).to_string(), - }) - .collect(); - w.line(format!( - "attach_function :{}, [{}], :void", - a.launch.symbol, - argtypes.join(", ") - )); - } - CallShape::Iterator(it) => { - w.line(format!( - "attach_function :{}, [{}], :pointer", - it.launch.symbol, - rb_abi_types(&it.launch.params, false).join(", ") - )); - w.line(format!( - "attach_function :{}, [{}], :int32", - it.next.symbol, - // Every `next` slot is a pointer (iter, out_item, out lens, err). - rb_abi_types(&it.next.params, true).join(", ") - )); - w.line(format!( - "attach_function :{}, [:pointer], :void", - it.destroy_symbol - )); - } - } - out.push_str(&w.finish()); -} - -/// Render the sync wrapper for one callable: convert the parameters, make -/// the C call with a stack `ErrorStruct`, route the out-err slot through the -/// function's checker, then convert the result per the scope. -fn render_sync_function_wrapper( - out: &mut String, - module: &ModuleBinding, - f: &FnBinding, - scope: &RbScope, -) { - let c_sym = &f.c_base; - let depth = scope.depth(); - let ind = " ".repeat(depth + 1); - let doc_ind = " ".repeat(depth); - let q = scope.qualifier(); - let checker = rb_checker_name(f, module.error.as_ref()); - - let params: Vec = f.params.iter().map(|p| rb_param_name(&p.name)).collect(); - let mut w = CodeWriter::two_space().with_depth(depth); - w.blank(); - let mut d = String::new(); - emit_doc(&mut d, &f.doc, &doc_ind); - w.raw(d); - emit_param_docs(&mut w, &f.params); - w.block(scope.def_open(f, ¶ms), "end", |w| { - if let Some(msg) = &f.deprecated { - let escaped = msg.replace('"', "\\\""); - w.line(format!("warn \"[DEPRECATED] {escaped}\"")); - } - - w.line("err = ErrorStruct.new"); - - for p in &f.params { - let mut pc = String::new(); - render_param_conversion(&mut pc, p, &ind, scope.module_name()); - w.raw(pc); - } - - // Bytes and buffered returns carry a trailing `out_len` slot. - let has_out_len = matches!( - plan::ret_pass(f.ret.as_ref(), "", ""), - RetPass::Bytes | RetPass::Buffer - ); - - if has_out_len { - w.line("out_len = FFI::MemoryPointer.new(:size_t)"); - } - - let mut call_args: Vec = Vec::new(); - if let Some(recv) = scope.self_arg() { - call_args.push(recv.into()); - } - for p in &f.params { - call_args.extend(rb_call_args(p)); - } - if has_out_len { - call_args.push("out_len".into()); - } - call_args.push("err".into()); - - let call = format!("{q}{c_sym}({})", call_args.join(", ")); - if f.ret.is_some() { - w.line(format!("result = {call}")); - } else { - w.line(call); - } - - w.line(format!("{q}{checker}(err)")); - - match scope { - // Constructors receive the owned pointer directly rather than - // routing through the generic return path. - RbScope::Init { ptr_class, .. } => { - w.line("raise Error.new(-1, 'null pointer') if result.null?"); - w.line(format!("@handle = {ptr_class}.new(result)")); - } - RbScope::Factory { .. } => { - w.line("raise Error.new(-1, 'null pointer') if result.null?"); - w.line("_from_ptr(result)"); - } - _ => { - if let Some(ret_ty) = &f.ret { - let mut tmp = String::new(); - render_return_code(&mut tmp, ret_ty, &ind, scope.module_name()); - w.raw(tmp); - } - } - } - }); - out.push_str(&w.finish()); -} - -/// Async wrapper: launches the `_async` C symbol with an `FFI::Function` -/// completion trampoline and blocks on a `Queue` until it fires (`Queue#pop` -/// releases the GVL, and the ffi gem delivers cross-thread callbacks safely). -/// Blocking is the idiomatic Ruby surface; callers needing concurrency wrap -/// the call in their own Thread or Fiber scheduler. -/// -/// The trampoline (`callback` local) stays referenced by the wrapper's stack -/// frame until `queue.pop` returns, which happens only after the producer has -/// invoked it, so the GC cannot collect it mid-flight. Per -/// [`weaveffi_core::plan::AsyncProtocol`], everything the trampoline receives -/// is owned by the consumer: it copies result buffers and then releases them -/// through the runtime free symbols, and it releases a reported error with -/// `weaveffi_error_free`; the error slot follows the function's -/// [`ErrorStrategy`]. -fn render_async_function_wrapper( - out: &mut String, - module: &ModuleBinding, - f: &FnBinding, - a: &AsyncBinding, - scope: &RbScope, -) { - let depth = scope.depth(); - let ind = " ".repeat(depth + 1); - let doc_ind = " ".repeat(depth); - let q = scope.qualifier(); - // A completion error raises the typed domain error for throwing - // callables; the generic Error otherwise (panics, marshalling). Typed - // errors also copy the borrowed payload buffer so declared fields decode. - let typed_error = matches!( - (f.error_strategy(), module.error.as_ref()), - (ErrorStrategy::Throws, Some(_)) - ); - let error_expr = match (f.error_strategy(), module.error.as_ref()) { - (ErrorStrategy::Throws, Some(eb)) => { - format!("{q}{}(code, msg, payload)", rb_error_factory_name(eb)) - } - _ => "Error.new(code, msg)".to_string(), - }; - let params: Vec = f.params.iter().map(|p| rb_param_name(&p.name)).collect(); - - let mut w = CodeWriter::two_space().with_depth(depth); - w.blank(); - let mut d = String::new(); - emit_doc(&mut d, &f.doc, &doc_ind); - w.raw(d); - w.line("# Blocks the current thread until the async producer completes; the"); - w.line(format!( - "# result (or error) is delivered through the completion callback{}.", - if f.cancellable { - " (cancellation token not exposed; pass-through is NULL)" - } else { - "" - } - )); - w.block(scope.def_open(f, ¶ms), "end", |w| { - if let Some(msg) = &f.deprecated { - let escaped = msg.replace('"', "\\\""); - w.line(format!("warn \"[DEPRECATED] {escaped}\"")); - } - - w.line("queue = Queue.new"); - - // Completion trampoline: (context, err, ). - let cb_types = rb_abi_types(&a.callback_params, true); - let mut cb_formals: Vec = vec!["_context".into(), "err_ptr".into()]; - cb_formals.extend(a.callback_params.iter().skip(2).map(|p| p.name.clone())); - w.block( - format!( - "callback = FFI::Function.new(:void, [{}]) do |{}|", - cb_types.join(", "), - cb_formals.join(", ") - ), - "end", - |w| { - // Producers pass err = NULL on success, so guard before dereferencing. - w.line("err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr)"); - w.line("if err && err[:code] != 0"); - w.scope(|w| { - w.line("code = err[:code]"); - w.line( - "msg = err[:message].null? ? '' : \ - err[:message].read_string.force_encoding(Encoding::UTF_8)", - ); - if typed_error { - // Copy the payload before releasing the boxed error. - w.line( - "payload = err[:payload_ptr].null? ? nil : \ - err[:payload_ptr].read_string(err[:payload_len])", - ); - } - w.line(format!("{q}weaveffi_error_free(err_ptr)")); - w.line(format!("queue << {error_expr}")); - }); - w.line("else"); - w.scope(|w| { - let mut tmp = String::new(); - render_async_result_push( - &mut tmp, - &f.ret, - &format!("{ind} "), - scope.module_name(), - ); - w.raw(tmp); - }); - w.line("end"); - }, - ); - - for p in &f.params { - let mut pc = String::new(); - render_param_conversion(&mut pc, p, &ind, scope.module_name()); - w.raw(pc); - } - let mut call_args: Vec = Vec::new(); - if let Some(recv) = scope.self_arg() { - call_args.push(recv.into()); - } - for p in &f.params { - call_args.extend(rb_call_args(p)); - } - if f.cancellable { - call_args.push("FFI::Pointer::NULL".into()); - } - call_args.push("callback".into()); - call_args.push("FFI::Pointer::NULL".into()); - w.line(format!("{q}{}({})", a.launch.symbol, call_args.join(", "))); - w.line("value = queue.pop"); - w.line("raise value if value.is_a?(Error)"); - w.line("value"); - }); - out.push_str(&w.finish()); -} - -/// Push the converted async result onto the queue. Result slots are named by -/// the shared ABI lowering: `result` (plus `result_len` for bytes), or -/// `result_ptr`/`result_len` for a buffered value. -/// -/// Per the async completion contract ([`weaveffi_core::plan::AsyncProtocol`]), -/// string, bytes, and buffered result buffers are owned by the consumer: the -/// callback deep-copies or decodes them, then releases them through the -/// runtime free symbols. Owned interface results are adopted by a -/// finalizer-bearing wrapper instead. -fn render_async_result_push( - out: &mut String, - ret: &Option, - 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(ret.as_ref(), "", "") { - RetPass::Void => { - w.line("queue << nil"); - } - RetPass::Buffer => { - // Owned buffer: copy, free, then decode from the copy. A decode - // failure surfaces through the queue so the caller thread raises - // it. - let ty = ret.as_ref().expect("buffered return has a type"); - w.line("begin"); - w.scope(|w| { - w.line( - "_wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : \ - result_ptr.read_string(result_len))", - ); - w.line(format!( - "{m}weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null?" - )); - crate::codec::render_wv_read(w, "_wv_r", "_wv_v", ty, 0, &m); - w.line("_wv_r.expect_end!"); - w.line("queue << _wv_v"); - }); - w.line("rescue Error => e"); - w.scope(|w| { - w.line("queue << e"); - }); - w.line("end"); - } - RetPass::String => { - // Owned by the consumer: copy (tagged UTF-8, since ffi's - // read_string yields BINARY), then free. - w.line( - "_wv_s = result.null? ? '' : result.read_string.force_encoding(Encoding::UTF_8)", - ); - w.line(format!( - "{m}weaveffi_free_string(result) unless result.null?" - )); - w.line("queue << _wv_s"); - } - RetPass::Bytes => { - // Owned by the consumer: copy, then free. - w.line("_wv_b = result.null? ? ''.b : result.read_string(result_len)"); - w.line(format!( - "{m}weaveffi_free_bytes(result, result_len) unless result.null?" - )); - w.line("queue << _wv_b"); - } - // A returned interface transfers ownership of a new object - // reference; wrap it without re-running initialize. A nullable - // return surfaces nil for null instead of trapping. - RetPass::Object { nullable, .. } => { - let name = match ret.as_ref() { - 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"), - }; - let local = local_type_name(name); - if nullable { - w.line(format!( - "queue << (result.null? ? nil : {local}._from_ptr(result))" - )); - } else { - w.line("if result.null?"); - w.scope(|w| { - w.line("queue << Error.new(-1, 'null pointer')"); - }); - w.line("else"); - w.scope(|w| { - w.line(format!("queue << {local}._from_ptr(result)")); - }); - w.line("end"); - } - } - RetPass::Direct => { - let ty = ret.as_ref().expect("direct return has a type"); - w.line(format!("queue << {}", rb_direct_from_c(ty, "result"))); - } - } - out.push_str(&w.finish()); -} - -/// Iterator wrapper: returns a lazy `Enumerator` per the pull contract stated -/// by [`weaveffi_core::plan::IteratorProtocol`]. -/// -/// The producer iterator launches *inside* the enumerator block, on the first -/// pull, so a handle cannot leak when the returned enumerator is never -/// started (launch errors therefore raise on the first pull rather than at -/// call time). Each consumer step issues exactly one C `next` call, each -/// yielded element is received per its element plan (strings and bytes -/// copied then freed, value buffers decoded then freed, object references -/// adopted into finalizer-bearing wrappers), and `destroy` runs exactly once -/// from an `ensure` block, so an early `break` or an error raised -/// mid-iteration still releases the handle. Launch and per-`next` errors -/// follow the function's [`ErrorStrategy`]. -fn render_iterator_function_wrapper( - out: &mut String, - module: &ModuleBinding, - f: &FnBinding, - it: &IteratorBinding, - scope: &RbScope, -) { - let depth = scope.depth(); - let ind = " ".repeat(depth + 1); - let doc_ind = " ".repeat(depth); - let q = scope.qualifier(); - let checker = rb_checker_name(f, module.error.as_ref()); - let params: Vec = f.params.iter().map(|p| rb_param_name(&p.name)).collect(); - let protocol = it.protocol(f, &module.path, ""); - - let mut w = CodeWriter::two_space().with_depth(depth); - w.blank(); - let mut d = String::new(); - emit_doc(&mut d, &f.doc, &doc_ind); - w.raw(d); - w.line("# Returns a lazy Enumerator that streams one element per pull; call"); - w.line("# `.to_a` to collect eagerly. The underlying producer iterator is"); - w.line("# launched on the first pull, so launch errors raise at that point"); - w.line("# rather than when this method returns. The iterator handle is"); - w.line("# released exactly once, when iteration finishes or is abandoned"); - w.line("# early (for example by `break`)."); - w.block(scope.def_open(f, ¶ms), "end", |w| { - if let Some(msg) = &f.deprecated { - let escaped = msg.replace('"', "\\\""); - w.line(format!("warn \"[DEPRECATED] {escaped}\"")); - } - for p in &f.params { - let mut pc = String::new(); - render_param_conversion(&mut pc, p, &ind, scope.module_name()); - w.raw(pc); - } - let mut call_args: Vec = Vec::new(); - if let Some(recv) = scope.self_arg() { - call_args.push(recv.into()); - } - for p in &f.params { - call_args.extend(rb_call_args(p)); - } - call_args.push("err".into()); - // The block closes over the converted argument buffers above, so they - // stay referenced (and un-collected) until the launch call runs. - w.block("Enumerator.new do |y|", "end", |w| { - w.line("err = ErrorStruct.new"); - w.line(format!( - "iter = {q}{}({})", - it.launch.symbol, - call_args.join(", ") - )); - w.line("begin"); - w.scope(|w| { - w.line(format!("{q}{checker}(err)")); - w.line("unless iter.null?"); - w.scope(|w| { - w.block("loop do", "end", |w| { - // `next` params: (iter, out_item, , out_err). - let elem = &it.elem; - // A pointer/length element pair (bytes, or any - // buffered value) carries an extra out-length slot. - let needs_len = matches!(protocol.elem_free, Free::Bytes); - let item_mem = rb_mem_type(elem); - w.line(format!("out_item = FFI::MemoryPointer.new({item_mem})")); - if needs_len { - w.line("out_item_len = FFI::MemoryPointer.new(:size_t)"); - } - w.line("item_err = ErrorStruct.new"); - let next_args = if needs_len { - "iter, out_item, out_item_len, item_err" - } else { - "iter, out_item, item_err" - }; - w.line(format!("has_item = {q}{}({next_args})", it.next.symbol)); - w.line(format!("{q}{checker}(item_err)")); - w.line("break if has_item.zero?"); - let mut tmp = String::new(); - render_iterator_item_yield( - &mut tmp, - elem, - &protocol.elem, - &" ".repeat(depth + 5), - scope.module_name(), - ); - w.raw(tmp); - }); - }); - w.line("end"); - }); - w.line("ensure"); - w.scope(|w| { - // Exactly one destroy per launched handle: this ensure runs - // once whether iteration exhausts, raises, or is abandoned by - // an early break from the consumer. - w.line(format!("{q}{}(iter) unless iter.null?", it.destroy_symbol)); - }); - w.line("end"); - }); - }); - out.push_str(&w.finish()); -} - -/// Convert the value written into `out_item` and yield it to the enumerator's -/// yielder `y`, receiving the element per its [`RetPass`] plan first (copy or -/// decode, free, then yield, so an early `break` during the yield cannot -/// leak the element; an object element is adopted). `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: &Ty, - pass: &RetPass, - 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 pass { - RetPass::Void => unreachable!("iterator elements always have a type"), - RetPass::Buffer => { - // 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"); - w.line("item_len = out_item_len.read(:size_t)"); - w.line("_wv_data = item_ptr.null? ? ''.b : item_ptr.read_string(item_len)"); - w.line(format!( - "{m}weaveffi_free_bytes(item_ptr, item_len) unless item_ptr.null?" - )); - w.line("_wv_r = WvBufferReader.new(_wv_data)"); - crate::codec::render_wv_read(&mut w, "_wv_r", "_wv_item", elem, 0, &m); - w.line("_wv_r.expect_end!"); - w.line("y << _wv_item"); - } - RetPass::String => { - w.line("item_ptr = out_item.read_pointer"); - w.line("if item_ptr.null?"); - w.scope(|w| { - w.line("y << ''"); - }); - w.line("else"); - w.scope(|w| { - w.line("item = item_ptr.read_string.force_encoding(Encoding::UTF_8)"); - w.line(format!("{m}weaveffi_free_string(item_ptr)")); - w.line("y << item"); - }); - w.line("end"); - } - RetPass::Bytes => { - w.line("item_ptr = out_item.read_pointer"); - w.line("item_len = out_item_len.read(:size_t)"); - w.line("if item_ptr.null?"); - w.scope(|w| { - w.line("y << ''.b"); - }); - w.line("else"); - w.scope(|w| { - w.line("item = item_ptr.read_string(item_len)"); - w.line(format!("{m}weaveffi_free_bytes(item_ptr, item_len)")); - w.line("y << item"); - }); - w.line("end"); - } - // A yielded object is one strong reference the wrapper adopts, - // without re-running initialize. A nullable element yields nil for a - // null pointer. - RetPass::Object { nullable, .. } => { - let local = local_type_name( - elem.interface_name() - .expect("object element names an interface"), - ); - w.line("item_ptr = out_item.read_pointer"); - if *nullable { - w.line(format!( - "y << (item_ptr.null? ? nil : {local}._from_ptr(item_ptr))" - )); - } else { - w.line("raise Error.new(-1, 'null pointer') if item_ptr.null?"); - w.line(format!("y << {local}._from_ptr(item_ptr)")); - } - } - RetPass::Direct => { - let read = format!("out_item.{}", rb_read_method(elem)); - w.line(format!("y << {}", rb_direct_from_c(elem, &read))); - } - } - out.push_str(&w.finish()); -} - -/// The Ruby argument expressions one wrapper parameter contributes to the C -/// call, driven by its [`ArgPass`] contract. A buffered parameter contributes -/// its packed `(ptr, len)` pair, bytes contribute a copied native buffer plus -/// its length, an object contributes its borrowed pointer, a callback -/// interface contributes its registry key plus the interface's static -/// vtable, and everything else is a single expression. -fn rb_call_args(p: &ParamBinding) -> Vec { - let name = rb_param_name(&p.name); - match p.arg_pass() { - ArgPass::Buffer { .. } => vec![format!("{name}_buf"), format!("{name}_data.bytesize")], - ArgPass::Bytes { .. } => vec![format!("{name}_buf"), format!("{name}.bytesize")], - ArgPass::String { .. } => vec![name], - // Borrowed for the call: the wrapper keeps its own reference. - ArgPass::Object { - nullable: false, .. - } => vec![format!("{name}.handle")], - // A nullable borrowed object pointer: nil passes as NULL. - ArgPass::Object { nullable: true, .. } => vec![format!("{name}&.handle")], - // The registry key (see `render_param_conversion`) and the one - // process-wide vtable for the interface. - ArgPass::Callback { .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback plan names a callback interface"); - vec![ - format!("{name}_ctx"), - format!("{}.to_ptr", rb_vtable_const(cb)), - ] - } - ArgPass::Direct { .. } => match &p.ty { - Ty::Bool => vec![format!("{name}_c")], - _ => vec![name], - }, - } -} - -/// Emit the statements converting one wrapper parameter into the locals its -/// C call slots reference (see [`rb_call_args`]), per its [`ArgPass`] -/// contract. A buffered parameter is packed into its value-buffer encoding -/// and copied into a `MemoryPointer` the C call borrows for its duration; the -/// caller keeps ownership and the callee never frees it. A callback interface -/// implementation is stored in the module's registry, whose key becomes the -/// `ctx` slot. `qualifier` names the top-level Ruby module when rendering -/// inside a class body. -fn render_param_conversion(out: &mut String, p: &ParamBinding, ind: &str, qualifier: Option<&str>) { - let q = qualifier.map(|q| format!("{q}.")).unwrap_or_default(); - let name = rb_param_name(&p.name); - let mut w = CodeWriter::two_space().with_depth(ind.len() / 2); - match p.arg_pass() { - ArgPass::Buffer { .. } => { - w.line(format!("{name}_w = WvBufferWriter.new")); - crate::codec::render_wv_write(&mut w, &format!("{name}_w"), &name, &p.ty, 0, &q); - w.line(format!("{name}_data = {name}_w.data")); - w.line(format!( - "{name}_buf = FFI::MemoryPointer.new(:uint8, {name}_data.bytesize)" - )); - w.line(format!("{name}_buf.put_bytes(0, {name}_data)")); - } - ArgPass::Bytes { .. } => { - w.line(format!( - "{name}_buf = FFI::MemoryPointer.new(:uint8, {name}.bytesize)" - )); - w.line(format!("{name}_buf.put_bytes(0, {name})")); - } - ArgPass::Callback { .. } => { - w.line(format!("{name}_ctx = {q}_wv_cb_register({name})")); - } - ArgPass::Direct { .. } if matches!(p.ty, Ty::Bool) => { - w.line(format!("{name}_c = {name} ? 1 : 0")); - } - _ => {} - } - out.push_str(&w.finish()); -} - -/// Emit the statements converting the raw C `result` (plus any out-params) -/// into the wrapper's idiomatic Ruby return value, per the return's -/// [`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: &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), "", "") { - RetPass::Void => unreachable!("void returns skip return-code rendering"), - RetPass::Buffer => { - w.line("len = out_len.read(:size_t)"); - w.line("data = result.null? ? ''.b : result.read_string(len)"); - w.line(format!( - "{m}weaveffi_free_bytes(result, len) unless result.null?" - )); - w.line("_wv_r = WvBufferReader.new(data)"); - crate::codec::render_wv_read(&mut w, "_wv_r", "_wv_value", ty, 0, &m); - w.line("_wv_r.expect_end!"); - w.line("_wv_value"); - } - RetPass::String => { - // ffi's read_string yields a BINARY string; the ABI guarantees - // UTF-8, so retag it before handing it to the caller. - w.line("return '' if result.null?"); - w.line("str = result.read_string.force_encoding(Encoding::UTF_8)"); - w.line(format!("{m}weaveffi_free_string(result)")); - w.line("str"); - } - RetPass::Bytes => { - w.line("return ''.b if result.null?"); - w.line("len = out_len.read(:size_t)"); - w.line("data = result.read_string(len)"); - w.line(format!("{m}weaveffi_free_bytes(result, len)")); - w.line("data"); - } - // A returned interface transfers ownership of a new object - // reference; wrap it without re-running initialize. A nullable - // return surfaces nil for null. - RetPass::Object { nullable, .. } => { - let name = match ty { - 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"), - }; - let local = local_type_name(name); - if nullable { - w.line("return nil if result.null?"); - w.line(format!("{local}._from_ptr(result)")); - } else { - w.line("raise Error.new(-1, 'null pointer') if result.null?"); - w.line(format!("{local}._from_ptr(result)")); - } - } - RetPass::Direct => { - w.line(rb_direct_from_c(ty, "result")); - } - } - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-ruby/src/lib.rs b/crates/weaveffi-gen-ruby/src/lib.rs deleted file mode 100644 index 4f4abb5c..00000000 --- a/crates/weaveffi-gen-ruby/src/lib.rs +++ /dev/null @@ -1,717 +0,0 @@ -//! Ruby (FFI gem) binding generator for WeaveFFI. -//! -//! Emits a Ruby gem (`.gemspec` + library) using the `ffi` gem to call -//! into the C ABI (revision 2) exposed by the underlying cdylib. Interfaces -//! become reference-counted wrapper classes (`close` plus a GC finalizer -//! backstop, `dup`/`clone` for a second reference), records and rich enums -//! become value classes packed into value buffers (with interfaces inside -//! them carried as object tokens), and callback interfaces become -//! duck-typed Ruby modules backed by one static vtable of pinned -//! `FFI::Function` trampolines per interface. Implements -//! [`LanguageBackend`]; the shared driver bridges it into the generator -//! pipeline. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod callbacks; -mod calls; -mod codec; -mod docs; -mod entities; -mod package; -mod runtime; -mod types; - -use camino::Utf8Path; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::{BindingModel, CallbackInterfaceBinding, ModuleBinding}; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::pkg; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::calls::{render_attach_function, render_callable, RbScope}; -use crate::entities::{ - render_enum, render_error, render_interface_class, render_interface_ffi, - render_rich_enum_class, render_struct_class, -}; -use crate::package::{ - render_gemspec, render_packaged_gemspec, render_packaged_readme, render_readme, -}; -use crate::runtime::{render_preamble, ruby_loader_packaged, RUBY_LOADER_ORIGINAL}; - -/// Per-target configuration for [`RubyGenerator`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct RubyConfig { - /// Top-level Ruby module name (default `"WeaveFFI"`). - pub module_name: Option, - /// Ruby gem name written into `weaveffi.gemspec` (default `"weaveffi"`). - pub gem_name: Option, - /// When `true` (the default), strip the IR module name prefix from - /// emitted Ruby method names, so a `contacts` module exports - /// `create_contact` rather than `contacts_create_contact`. Set to - /// `false` to restore module-prefixed names. - pub strip_module_prefix: bool, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the FFI bindings call the same - /// exported symbols the producer emits. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with. - #[serde(skip)] - pub input_basename: Option, -} - -impl Default for RubyConfig { - fn default() -> Self { - Self { - module_name: None, - gem_name: None, - strip_module_prefix: true, - prefix: None, - input_basename: None, - } - } -} - -impl RubyConfig { - /// Returns the configured top-level Ruby module name, falling back to - /// `"WeaveFFI"`. - pub fn module_name(&self) -> &str { - self.module_name.as_deref().unwrap_or("WeaveFFI") - } - - /// Returns the configured C ABI symbol prefix, falling back to `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the configured gem name, falling back to `"weaveffi"`. - pub fn gem_name(&self) -> &str { - self.gem_name.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// Ruby backend: emits an `ffi`-gem package (a library module, a `.gemspec`, -/// and a README) binding the C ABI exposed by the underlying cdylib. -pub struct RubyGenerator; - -impl LanguageBackend for RubyGenerator { - type Config = RubyConfig; - - fn name(&self) -> &'static str { - "ruby" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn render_callback_interface( - &self, - out: &mut String, - module: &ModuleBinding, - cb: &CallbackInterfaceBinding, - config: &Self::Config, - ) { - callbacks::render_callback_interface(out, module, cb, config.prefix()); - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let dir = out_dir.join("ruby"); - let lib_dir = dir.join("lib"); - let input_basename = config.input_basename(); - let package = pkg::resolve( - api, - config.gem_name.as_deref(), - config.input_basename.as_deref(), - ); - let lib_file = format!("{}.rb", package.ident_name()); - let gem_file = format!("{}.gemspec", package.name); - vec![ - OutputFile::new( - lib_dir.join(&lib_file), - render_ruby_module( - model, - config.module_name(), - config.strip_module_prefix, - &lib_file, - input_basename, - ), - ), - OutputFile::new( - dir.join(&gem_file), - render_gemspec(&package, &gem_file, input_basename), - ), - OutputFile::new( - dir.join("README.md"), - render_readme(&package, input_basename), - ), - ] - } - - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let input_basename = config.input_basename(); - let package = pkg::resolve( - api, - config.gem_name.as_deref(), - config.input_basename.as_deref(), - ); - let lib_file = format!("{}.rb", package.ident_name()); - let gem_file = format!("{}.gemspec", package.name); - - // Render the FFI module once with the bundled-first loader. - let module_src = render_ruby_module( - model, - config.module_name(), - config.strip_module_prefix, - &lib_file, - input_basename, - ) - .replace( - RUBY_LOADER_ORIGINAL, - &ruby_loader_packaged(&ctx.binaries.lib_name), - ); - let readme = render_packaged_readme(&package, input_basename); - - let ruby_dir = out_dir.join("ruby"); - let mut files = Vec::new(); - for nb in &ctx.binaries.binaries { - let platform = nb.platform; - // RubyGems has no platform string for Android or wasm32 builds, - // so those binaries have no gem to land in. - let Some(ruby_platform) = platform.ruby_platform() else { - continue; - }; - let gem_dir = ruby_dir.join(platform.id()); - let lib_dir = gem_dir.join("lib"); - files.push(PackagedFile::text( - lib_dir.join(&lib_file), - module_src.clone(), - )); - files.push(PackagedFile::copy( - lib_dir - .join("native") - .join(ctx.binaries.bundled_filename(platform)), - nb.source.clone(), - )); - files.push(PackagedFile::text( - gem_dir.join(&gem_file), - render_packaged_gemspec(&package, &gem_file, ruby_platform, input_basename), - )); - files.push(PackagedFile::text( - gem_dir.join("README.md"), - readme.clone(), - )); - } - Some(files) - } -} - -/// Render the primary Ruby library source: the fixed preamble, then each -/// module's typed error surface, entities, codecs, FFI attachments, callback -/// interfaces, and wrappers, in dependency order. -fn render_ruby_module( - model: &BindingModel, - module_name: &str, - strip_module_prefix: bool, - lib_filename: &str, - input_basename: &str, -) -> String { - let mut out = render_prelude(CommentStyle::Hash, input_basename); - render_preamble(&mut out, module_name, model.has_callback_interfaces()); - for m in &model.modules { - out.push_str(&format!("\n # === Module: {} ===\n", m.path)); - // The typed error surface comes first so the domain class exists - // before any wrapper references its checker. - if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { - render_error(&mut out, m, eb); - } - for e in &m.enums { - // A plain C-style enum is a module of integer constants; a rich - // (algebraic) enum is a tagged value-class hierarchy packed into - // value buffers by the codec helpers below. - if e.is_rich() { - render_rich_enum_class(&mut out, e); - } else { - render_enum(&mut out, e); - } - } - for s in &m.structs { - render_struct_class(&mut out, s); - } - // Value-buffer codecs: one pack/unpack pair per record and rich enum. - for s in &m.structs { - codec::render_struct_codec(&mut out, s); - } - for e in &m.enums { - if e.is_rich() { - codec::render_rich_enum_codec(&mut out, e); - } - } - for i in &m.interfaces { - render_interface_ffi(&mut out, i); - } - for f in &m.functions { - render_attach_function(&mut out, f); - } - // Callback interfaces precede interfaces and functions because their - // static vtables are what interface members and free functions pass. - for cb in &m.callback_interfaces { - callbacks::render_callback_interface(&mut out, m, cb, &model.prefix); - } - for i in &m.interfaces { - render_interface_class(&mut out, m, i, module_name); - } - for f in &m.functions { - let scope = RbScope::Free { - module_path: &m.path, - strip_module_prefix, - }; - render_callable(&mut out, m, f, &scope); - } - } - out.push_str("end\n\n"); - out.push_str(&render_trailer(CommentStyle::Hash, lib_filename)); - out -} - -#[cfg(test)] -mod tests { - use super::*; - use weaveffi_core::platform::{BinarySet, Platform}; - use weaveffi_ir::ir::{ - Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, - TypeRef, - }; - - fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } - } - - fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } - } - - fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } - } - - fn named(name: &str) -> TypeRef { - TypeRef::Named(name.into()) - } - - /// One module exercising every revision-2 object shape: an interface - /// with a constructor and a method, `Store?` in and out, a record holding - /// a `Store` and a `[Store]`, an iterator of `Store`, a callback - /// interface with string, i32, record, and object parameters (one method - /// returning `bool`, one void), and a function taking that callback. - fn fixture() -> BindingModel { - let kv = Module { - name: "kv".into(), - doc: None, - functions: vec![ - func( - "maybe_store", - vec![param("store", TypeRef::Optional(Box::new(named("Store"))))], - Some(TypeRef::Optional(Box::new(named("Store")))), - ), - func( - "scan", - vec![], - Some(TypeRef::Iterator(Box::new(named("Store")))), - ), - func( - "subscribe", - vec![param("listener", named("Listener"))], - None, - ), - func( - "describe", - vec![param("bundle", named("Bundle"))], - Some(named("Bundle")), - ), - ], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store.".into()), - deprecated: None, - constructors: vec![func("new", vec![param("path", TypeRef::StringUtf8)], None)], - methods: vec![func( - "get", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - )], - statics: vec![], - }], - callback_interfaces: vec![CallbackInterfaceDef { - name: "Listener".into(), - doc: Some("Receives store events.".into()), - deprecated: None, - methods: vec![ - func( - "on_message", - vec![ - param("text", TypeRef::StringUtf8), - param("weight", TypeRef::I32), - ], - Some(TypeRef::Bool), - ), - func( - "on_bundle", - vec![ - param("bundle", named("Bundle")), - param("store", named("Store")), - param("alt", TypeRef::Optional(Box::new(named("Store")))), - ], - None, - ), - ], - }], - structs: vec![StructDef { - name: "Bundle".into(), - doc: None, - deprecated: None, - fields: vec![ - field("primary", named("Store")), - field("extras", TypeRef::List(Box::new(named("Store")))), - ], - }], - enums: vec![], - errors: None, - modules: vec![], - }; - let api = ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![kv], - }); - BindingModel::build(&api, "weaveffi") - } - - fn render(model: &BindingModel) -> String { - render_ruby_module(model, "WeaveFFI", true, "weaveffi.rb", "weaveffi.yml") - } - - fn assert_has(src: &str, needle: &str) { - assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); - } - - #[test] - fn runtime_binds_error_set_and_drops_legacy_symbols() { - let src = render(&fixture()); - assert_has( - &src, - "attach_function :weaveffi_error_set, [:pointer, :int32, :string], :void", - ); - assert_has(&src, "FOREIGN_ERROR_CODE = -4"); - assert_has( - &src, - &format!("ABI_VERSION = {}", weaveffi_core::cabi::ABI_VERSION), - ); - for legacy in [ - "arena", - "listener_refs", - "handle_t", - "register_", - "unregister_", - ] { - assert!( - !src.contains(legacy), - "legacy `{legacy}` survived in:\n{src}" - ); - } - } - - #[test] - fn interface_wrapper_owns_one_reference_and_can_clone_it() { - let src = render(&fixture()); - assert_has( - &src, - "attach_function :weaveffi_kv_Store_clone, [:pointer], :pointer", - ); - assert_has( - &src, - "attach_function :weaveffi_kv_Store_destroy, [:pointer], :void", - ); - assert_has(&src, "class StorePtr < FFI::AutoPointer"); - assert_has(&src, "WeaveFFI.weaveffi_kv_Store_destroy(ptr)"); - assert_has(&src, "def close\n"); - assert_has(&src, "def closed?\n"); - assert_has(&src, "def _wv_clone_ptr\n"); - assert_has(&src, "WeaveFFI.weaveffi_kv_Store_clone(handle)"); - assert_has(&src, "def initialize_copy(other)"); - assert_has( - &src, - "StorePtr.new(WeaveFFI.weaveffi_kv_Store_clone(other.handle))", - ); - // The constructor adopts the returned reference; the method borrows. - assert_has(&src, "def initialize(path)"); - assert_has(&src, "@handle = StorePtr.new(result)"); - assert_has( - &src, - "result = WeaveFFI.weaveffi_kv_Store_get(handle, key, err)", - ); - } - - #[test] - fn nullable_objects_map_to_nil_in_both_directions() { - let src = render(&fixture()); - assert_has(&src, "def self.maybe_store(store)"); - assert_has(&src, "result = weaveffi_kv_maybe_store(store&.handle, err)"); - assert_has( - &src, - "return nil if result.null?\n Store._from_ptr(result)", - ); - } - - #[test] - fn records_carry_objects_as_cloned_tokens() { - let src = render(&fixture()); - assert_has(&src, "def self._wv_write_bundle(w, v)"); - assert_has(&src, "w.write_u64(v.primary._wv_clone_ptr.address)"); - assert_has(&src, "w.write_u64(_wv_e0._wv_clone_ptr.address)"); - assert_has(&src, "def self._wv_read_bundle(r)"); - assert_has(&src, "_wv_primary = Store._from_ptr(r.read_object_token)"); - assert_has(&src, "_wv_e0 = Store._from_ptr(r.read_object_token)"); - assert_has(&src, "def read_object_token"); - } - - #[test] - fn iterator_elements_adopt_objects() { - let src = render(&fixture()); - assert_has(&src, "def self.scan()"); - assert_has( - &src, - "has_item = weaveffi_kv_ScanIterator_next(iter, out_item, item_err)", - ); - assert_has(&src, "y << Store._from_ptr(item_ptr)"); - assert_has( - &src, - "weaveffi_kv_ScanIterator_destroy(iter) unless iter.null?", - ); - } - - #[test] - fn callback_interface_renders_module_vtable_and_trampolines() { - let src = render(&fixture()); - // Consumer-facing module with NotImplementedError defaults. - assert_has(&src, "module Listener\n"); - assert_has(&src, "def on_message(text, weight)"); - assert_has(&src, "def on_bundle(bundle, store, alt)"); - assert_has(&src, "raise NotImplementedError"); - // Vtable layout: methods in order, then free. - assert_has(&src, "class WvListenerVtable < FFI::Struct"); - assert_has( - &src, - "layout :on_message, :pointer,\n :on_bundle, :pointer,\n :free, :pointer", - ); - // Trampolines match the C vtable entry signatures. - assert_has( - &src, - "WV_LISTENER_ON_MESSAGE = FFI::Function.new(:int32, [:pointer, :string, :int32, :pointer]) do |ctx, text, weight, out_err|", - ); - assert_has( - &src, - "WV_LISTENER_ON_BUNDLE = FFI::Function.new(:void, [:pointer, :pointer, :size_t, :pointer, :pointer, :pointer]) do |ctx, bundle_ptr, bundle_len, store, alt, out_err|", - ); - assert_has(&src, "impl = _wv_cb_lookup(ctx)"); - assert_has(&src, "impl.on_message(text_v, weight_v) ? 1 : 0"); - // Borrowed buffer decoded before the call; objects adopted. - assert_has(&src, "bundle_v = _wv_read_bundle(bundle_r)"); - assert_has(&src, "bundle_r.expect_end!"); - assert_has(&src, "store_v = Store._from_ptr(store)"); - assert_has(&src, "alt_v = alt.null? ? nil : Store._from_ptr(alt)"); - // Failure path: error_set with -4 and a default return. - assert_has( - &src, - "rescue Exception => e\n _wv_cb_fail(out_err, e)\n 0\n", - ); - assert_has( - &src, - "rescue Exception => e\n _wv_cb_fail(out_err, e)\n nil\n", - ); - assert_has( - &src, - "weaveffi_error_set(out_err, FOREIGN_ERROR_CODE, message)", - ); - // Free drops the registry entry. - assert_has( - &src, - "WV_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx|", - ); - assert_has(&src, "_wv_cb_free(ctx)"); - // One static vtable, filled with the pinned trampolines. - assert_has(&src, "WV_LISTENER_VTABLE = WvListenerVtable.new"); - assert_has( - &src, - "WV_LISTENER_VTABLE[:on_message] = WV_LISTENER_ON_MESSAGE", - ); - assert_has( - &src, - "WV_LISTENER_VTABLE[:on_bundle] = WV_LISTENER_ON_BUNDLE", - ); - assert_has(&src, "WV_LISTENER_VTABLE[:free] = WV_LISTENER_FREE"); - assert_eq!(src.matches("= WvListenerVtable.new").count(), 1); - } - - #[test] - fn passing_a_callback_registers_ctx_and_static_vtable() { - let src = render(&fixture()); - assert_has(&src, "def self.subscribe(listener)"); - assert_has(&src, "listener_ctx = _wv_cb_register(listener)"); - assert_has( - &src, - "weaveffi_kv_subscribe(listener_ctx, WV_LISTENER_VTABLE.to_ptr, err)", - ); - assert_has(&src, "def self._wv_cb_register(impl)"); - assert_has(&src, "def self._wv_cb_lookup(ctx)"); - assert_has(&src, "def self._wv_cb_free(ctx)"); - } - - #[test] - fn registry_is_omitted_without_callback_interfaces() { - let 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", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - )], - interfaces: vec![], - callback_interfaces: vec![], - structs: vec![], - enums: vec![], - errors: None, - modules: vec![], - }], - }); - let src = render(&BindingModel::build(&api, "weaveffi")); - assert!(!src.contains("_wv_cb_register"), "{src}"); - assert_has(&src, "attach_function :weaveffi_error_set"); - } - - /// ffi's `Pointer#read_string` and `:string` callback slots produce - /// BINARY-encoded Strings; every C string the ABI guarantees is UTF-8 must - /// be retagged, or non-ASCII text fails to compare equal to a literal. - #[test] - fn c_strings_are_retagged_as_utf8() { - let src = render(&fixture()); - // Direct string return. - assert_has( - &src, - "str = result.read_string.force_encoding(Encoding::UTF_8)", - ); - // Callback-interface string parameter. - assert_has( - &src, - "text_v = text.nil? ? '' : text.force_encoding(Encoding::UTF_8)", - ); - // Error messages (generic checker). - assert_has( - &src, - "msg = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8)", - ); - // Every remaining bare read_string is a length-delimited byte copy. - for line in src.lines().filter(|l| l.contains(".read_string")) { - assert!( - line.contains(".read_string(") || line.contains("force_encoding(Encoding::UTF_8)"), - "untagged C string read in: {line}" - ); - } - } - - #[test] - fn rendering_is_deterministic() { - let model = fixture(); - assert_eq!(render(&model), render(&model)); - } - - #[test] - fn package_skips_platforms_without_a_gem_string() { - let model = fixture(); - let api = ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![], - }); - let mut binaries = BinarySet::new("weaveffi"); - binaries.insert(Platform::MacosArm64, "/tmp/darwin-arm64/libweaveffi.dylib"); - binaries.insert(Platform::AndroidArm64, "/tmp/android-arm64/libweaveffi.so"); - binaries.insert(Platform::Wasm32, "/tmp/wasm32/weaveffi.wasm"); - let ctx = PackageContext { - binaries: &binaries, - input_basename: Some("weaveffi.yml"), - }; - let files = RubyGenerator - .package( - &api, - &model, - &ctx, - Utf8Path::new("out"), - &RubyConfig::default(), - ) - .expect("ruby supports packaging"); - let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect(); - assert!( - paths - .iter() - .all(|p| p.starts_with("out/ruby/darwin-arm64/")), - "{paths:?}" - ); - assert!(paths.iter().any(|p| p.ends_with("weaveffi.gemspec"))); - let gemspec = files - .iter() - .find(|f| f.path.as_str().ends_with("weaveffi.gemspec")) - .unwrap(); - let weaveffi_core::package::FileContent::Text(text) = &gemspec.content else { - panic!("gemspec is text"); - }; - assert_has(text, "s.platform = 'arm64-darwin'"); - } -} diff --git a/crates/weaveffi-gen-ruby/src/package.rs b/crates/weaveffi-gen-ruby/src/package.rs deleted file mode 100644 index 76ba0a67..00000000 --- a/crates/weaveffi-gen-ruby/src/package.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Gem packaging surfaces: the `.gemspec` (plain and per-platform) and the -//! README, for both `generate` and `package` layouts. -//! -//! A gemspec is Ruby source, so every interpolated user string (summary, -//! authors, license, homepage) goes through the single-quote escape in -//! [`rb_str_literal`]; a quote or trailing backslash in package metadata -//! cannot corrupt the emitted spec. - -use weaveffi_core::pkg::ResolvedPackage; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::types::rb_str_literal; - -/// The `s.authors`/`s.license`/`s.homepage` block shared by both gemspec -/// shapes, one line per present optional field. -fn gemspec_extra(package: &ResolvedPackage) -> String { - let mut extra = String::new(); - if !package.authors.is_empty() { - let authors = package - .authors - .iter() - .map(|a| format!("'{}'", rb_str_literal(a))) - .collect::>() - .join(", "); - extra.push_str(&format!(" s.authors = [{authors}]\n")); - } - if let Some(license) = &package.license { - extra.push_str(&format!( - " s.license = '{}'\n", - rb_str_literal(license) - )); - } - if let Some(homepage) = package.homepage.as_ref().or(package.repository.as_ref()) { - extra.push_str(&format!( - " s.homepage = '{}'\n", - rb_str_literal(homepage) - )); - } - extra -} - -/// Render the source-only gemspec emitted by `generate`. -pub(crate) fn render_gemspec( - package: &ResolvedPackage, - gem_file: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Hash, input_basename); - let trailer = render_trailer(CommentStyle::Hash, gem_file); - let name = &package.name; - let version = &package.version; - let summary = rb_str_literal(&package.description_or_default()); - let extra = gemspec_extra(package); - format!( - "{prelude}Gem::Specification.new do |s| - s.name = '{name}' - s.version = '{version}' - s.summary = '{summary}' -{extra} s.files = Dir['lib/**/*.rb'] - s.require_paths = ['lib'] - - s.add_dependency 'ffi', '~> 1.15' -end - -{trailer}" - ) -} - -/// Render a platform gemspec: it stamps `s.platform` with the RubyGems -/// platform string (`Platform::ruby_platform`) and ships the bundled native -/// library alongside the Ruby sources. -pub(crate) fn render_packaged_gemspec( - package: &ResolvedPackage, - gem_file: &str, - ruby_platform: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Hash, input_basename); - let trailer = render_trailer(CommentStyle::Hash, gem_file); - let name = &package.name; - let version = &package.version; - let summary = rb_str_literal(&package.description_or_default()); - let extra = gemspec_extra(package); - format!( - "{prelude}Gem::Specification.new do |s| - s.name = '{name}' - s.version = '{version}' - s.platform = '{ruby_platform}' - s.summary = '{summary}' -{extra} s.files = Dir['lib/**/*.rb'] + Dir['lib/**/*.{{so,dylib,dll}}'] - s.require_paths = ['lib'] - - s.add_dependency 'ffi', '~> 1.15' -end - -{trailer}" - ) -} - -/// README for the source-only gem layout. -pub(crate) fn render_readme(package: &ResolvedPackage, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = &package.name; - let version = &package.version; - let require_name = package.ident_name(); - format!( - r#"{prelude}# {name} (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 {name}.gemspec -gem install {name}-{version}.gem -``` - -## Usage - -```ruby -require '{require_name}' -``` - -{trailer}"# - ) -} - -/// README for a packaged Ruby platform gem. -pub(crate) fn render_packaged_readme(package: &ResolvedPackage, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - let name = &package.name; - let version = &package.version; - let require_name = package.ident_name(); - format!( - r#"{prelude}# {name} (Ruby) - -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem, -with the native library bundled for this platform. The library loads -automatically; no external setup is required. - -## Install - -```bash -gem build {name}.gemspec -gem install {name}-{version}-*.gem -``` - -## Usage - -```ruby -require '{require_name}' -``` - -{trailer}"# - ) -} diff --git a/crates/weaveffi-gen-ruby/src/runtime.rs b/crates/weaveffi-gen-ruby/src/runtime.rs deleted file mode 100644 index 2ea5f569..00000000 --- a/crates/weaveffi-gen-ruby/src/runtime.rs +++ /dev/null @@ -1,367 +0,0 @@ -//! The fixed Ruby runtime the generated module carries: the library loader, -//! the error surface, the value-buffer reader/writer pair, the runtime ABI -//! attachments (`error_set`, `error_clear`, `error_free`, `free_string`, -//! `free_bytes`), guarded by the load-time ABI-revision check, and (when -//! the API declares callback interfaces) the implementation registry the -//! vtable trampolines resolve their `ctx` keys against. - -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. -pub(crate) const RUBY_LOADER_ORIGINAL: &str = r#" # 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 packaged `ffi_lib` loader for `lib`: prefer the per-platform library -/// bundled under `lib/native/`, then `WEAVEFFI_LIBRARY`, then the system path. -pub(crate) fn ruby_loader_packaged(lib: &str) -> String { - format!( - r#" # A bundled per-platform library ships inside this gem; prefer it so the gem - # works with no external setup. WEAVEFFI_LIBRARY still overrides. - _wv_override = ENV['WEAVEFFI_LIBRARY'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - case FFI::Platform::OS - when /darwin/ - _wv_name = 'lib{lib}.dylib' - when /mswin|mingw/ - _wv_name = '{lib}.dll' - else - _wv_name = 'lib{lib}.so' - end - _wv_bundled = File.join(__dir__, 'native', _wv_name) - ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) - end"# - ) -} - -/// Emit the fixed module preamble: the loader, the error struct and generic -/// `Error` class, the buffer runtime, the runtime ABI attachments, the -/// generic `check_error!` trap helper, and (when the API declares callback -/// interfaces) the implementation registry. -pub(crate) fn render_preamble(out: &mut String, module_name: &str, has_callback_interfaces: bool) { - out.push_str(&format!( - "# frozen_string_literal: true -# {module_name} Ruby FFI bindings (auto-generated) - -require 'ffi' - -module {module_name} - extend FFI::Library - -{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, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - # The runtime-reserved error codes. Positive codes belong to a module's - # declared error domain; these negative codes are programming errors the - # wrappers raise as a plain Error regardless of the function's `throws`. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - - class Error < StandardError - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end -" - )); - out.push_str(RUBY_BUFFER_RUNTIME); - out.push_str( - " - attach_function :weaveffi_error_set, [:pointer, :int32, :string], :void - 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.force_encoding(Encoding::UTF_8) - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end -", - ); - if has_callback_interfaces { - out.push_str(RUBY_CALLBACK_REGISTRY); - } -} - -/// The handle table behind every callback-interface parameter: the Ruby -/// implementation object is stored under an incrementing Integer key, and -/// that key (widened to a pointer) is what the producer receives as `ctx`. -/// The producer never sees a Ruby object address, so the GC is free to move -/// or keep the implementation as it likes; the entry is deleted when the -/// producer calls the vtable's `free`. -const RUBY_CALLBACK_REGISTRY: &str = " - # Live callback-interface implementations keyed by the Integer the producer - # holds as `ctx`. Keys start at 1 so a context pointer is never NULL. - @wv_cb_registry = {} - @wv_cb_next_key = 0 - @wv_cb_mutex = Mutex.new - - # @api private - # Registers `impl` and returns the `ctx` pointer to hand the producer. - def self._wv_cb_register(impl) - @wv_cb_mutex.synchronize do - @wv_cb_next_key += 1 - @wv_cb_registry[@wv_cb_next_key] = impl - FFI::Pointer.new(@wv_cb_next_key) - end - end - - # @api private - # Resolves a trampoline's `ctx` back to the registered implementation. - def self._wv_cb_lookup(ctx) - impl = @wv_cb_mutex.synchronize { @wv_cb_registry[ctx.address] } - raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') if impl.nil? - impl - end - - # @api private - # Drops the registry entry when the producer releases its last reference. - def self._wv_cb_free(ctx) - @wv_cb_mutex.synchronize { @wv_cb_registry.delete(ctx.address) } - nil - end - - # @api private - # Reports a callback implementation's exception through `out_err`. The - # producer copies the message, so the Ruby String need not outlive the call. - def self._wv_cb_fail(out_err, exception) - message = exception.message.to_s - message = exception.class.name if message.empty? - weaveffi_error_set(out_err, FOREIGN_ERROR_CODE, message) - nil - end -"; - -/// The private Ruby runtime implementing the value-buffer wire format -/// (little-endian, packed, no alignment): a writer building a binary String -/// and a reader that raises `Error` on any malformed buffer (truncation, bad -/// flag bytes, invalid UTF-8, length prefixes past the end, zero object -/// tokens, trailing bytes). -const RUBY_BUFFER_RUNTIME: &str = r#" - # @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 - - # An object token: a non-zero u64 carrying one strong reference the - # caller adopts into a wrapper. - def read_object_token - addr = read_u64 - raise Error.new(-1, 'malformed value buffer: null object token') if addr.zero? - FFI::Pointer.new(addr) - end - - def expect_end! - return if @pos == @data.bytesize - raise Error.new(-1, 'malformed value buffer: trailing bytes after value') - end - end -"#; diff --git a/crates/weaveffi-gen-ruby/src/types.rs b/crates/weaveffi-gen-ruby/src/types.rs deleted file mode 100644 index 6f4924b4..00000000 --- a/crates/weaveffi-gen-ruby/src/types.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Ruby type mapping and naming: the FFI-gem vocabulary for ABI slots, -//! the memory read/write spellings for out-slots, the identifier policy for -//! user-chosen IDL names, and string escaping for Ruby literals. - -use heck::ToSnakeCase; -use weaveffi_core::abi::{AbiParam, CType}; -use weaveffi_core::lang::{escape_ident, RUBY_KEYWORDS}; -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 `_`). -/// IDL names are usually already snake, so the case conversion is a safety -/// net for camelCase inputs. -pub(crate) fn rb_param_name(name: &str) -> String { - escape_ident(&name.to_snake_case(), RUBY_KEYWORDS) -} - -/// The Ruby spelling of a user-chosen field name: verbatim unless it's a -/// Ruby reserved word, in which case it gains a trailing `_` so keyword -/// arguments and locals derived from it stay parseable. -pub(crate) fn rb_field_name(name: &str) -> String { - escape_ident(name, RUBY_KEYWORDS) -} - -/// Maps a shared ABI [`CType`] onto its Ruby FFI symbol. The structural -/// lowering comes from [`weaveffi_core::abi`]; this is the Ruby vocabulary. -/// `string_as_pointer` distinguishes the two char-pointer conventions: `ffi` -/// auto-marshals `:string` for *input* parameters but owned-return pointers -/// must stay `:pointer` so the caller can free them. -pub(crate) fn rb_ffi_type(ty: &CType, string_as_pointer: bool) -> &'static str { - match ty { - CType::Int8 => ":int8", - CType::Int16 => ":int16", - CType::Int32 | CType::Bool | CType::Enum { .. } => ":int32", - CType::Uint8 => ":uint8", - CType::Uint16 => ":uint16", - CType::Uint32 => ":uint32", - CType::Int64 => ":int64", - CType::Uint64 => ":uint64", - CType::Float => ":float", - CType::Double => ":double", - CType::Size => ":size_t", - CType::Void => ":void", - CType::Ptr { pointee, .. } if matches!(**pointee, CType::Char) && !string_as_pointer => { - ":string" - } - _ => ":pointer", - } -} - -/// Map lowered ABI slots onto Ruby FFI type tokens. `string_as_pointer` -/// applies to top-level `char*` slots (owned returns stay `:pointer` so the -/// wrapper can free them; borrowed inputs use `:string` auto-marshalling). -pub(crate) fn rb_abi_types(params: &[AbiParam], string_as_pointer: bool) -> Vec { - params - .iter() - .map(|p| rb_ffi_type(&p.ty, string_as_pointer).to_string()) - .collect() -} - -/// The `FFI::MemoryPointer` read method for one iterator element out-slot. -/// This is ABI-slot vocabulary, not wire vocabulary: strings, bytes, -/// buffers, and objects all cross a `next` slot as a pointer, so anything -/// non-scalar reads a pointer. -pub(crate) fn rb_read_method(ty: &Ty) -> &'static str { - match ty { - 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", - _ => "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: &Ty) -> &'static str { - match ty { - 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", - _ => ":pointer", - } -} - -/// The Ruby expression converting one direct-family (`Direct`) C value -/// received from the producer (a return, an out-slot read, or a trampoline -/// argument) into its idiomatic Ruby value: a `bool` crosses as an `int32` -/// and becomes `true`/`false`; scalars and C-style enum discriminants pass -/// through. -pub(crate) fn rb_direct_from_c(ty: &Ty, expr: &str) -> String { - match ty { - Ty::Bool => format!("({expr} != 0)"), - _ => expr.to_string(), - } -} - -/// The Ruby literal a callback-interface trampoline returns to the producer -/// after its implementation raised: the zero value of the method's direct -/// return type, or `nil` for a void method. -pub(crate) fn rb_direct_default(ty: Option<&Ty>) -> &'static str { - match ty { - None => "nil", - Some(Ty::F32 | Ty::F64) => "0.0", - Some(_) => "0", - } -} - -/// Escape a string for embedding in a single-quoted Ruby literal (the two -/// characters with meaning there: backslash and the quote itself). -pub(crate) fn rb_str_literal(s: &str) -> String { - s.replace('\\', "\\\\").replace('\'', "\\'") -} diff --git a/crates/weaveffi-gen-swift/Cargo.toml b/crates/weaveffi-gen-swift/Cargo.toml deleted file mode 100644 index a53104b6..00000000 --- a/crates/weaveffi-gen-swift/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "weaveffi-gen-swift" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "Swift package and bridging code generator for WeaveFFI" -keywords = ["ffi", "codegen", "bindings", "swift", "ios"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-swift/src/calls.rs b/crates/weaveffi-gen-swift/src/calls.rs deleted file mode 100644 index b130646f..00000000 --- a/crates/weaveffi-gen-swift/src/calls.rs +++ /dev/null @@ -1,1037 +0,0 @@ -//! Callable rendering: sync and async wrapper functions and the lazy -//! sequence classes backing `iter` returns. -//! -//! Parameter marshalling dispatches on the shared [`ArgPass`] plan and -//! return handling on [`RetPass`], so how a value crosses the ABI is decided -//! centrally; this module only renders the Swift spelling. - -use std::fmt::Write; - -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::ERROR_BRAND; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{CallShape, FnBinding, IteratorBinding, ModuleBinding, ParamBinding}; -use weaveffi_core::plan::{ret_pass, ArgPass, ErrorStrategy, Free, RetPass}; -use weaveffi_core::utils::local_type_name; - -use crate::codec::{fresh, read_value_stmts, write_value_stmts}; -use crate::docs::emit_fn_doc; -use crate::entities::domain_stem; -use crate::types::{ - c_enum_type, callback_box_name, callback_vtable_name, iterator_class_name, swift_ident, - swift_scalar_default, swift_type_ctx, swift_type_for, SwiftCtx, -}; - -/// How a wrapper body reports a non-zero `weaveffi_error` slot. -/// -/// A callable with `throws == true` maps codes through the declaring module's -/// typed checker (`checkKv`) and surfaces marshalling failures as thrown -/// [`ERROR_BRAND`] values; a callable with `throws == false` has a plain -/// signature and traps (`fatalError`) instead, since a reported error can only -/// be a producer panic or an argument-marshalling failure. -#[derive(Clone, Copy)] -pub(crate) struct ErrCtx<'a> { - /// `true` when the wrapper is `throws` and surfaces typed errors. - pub(crate) throws: bool, - /// PascalCase stem of the domain in effect (`Kv` names `checkKv` and - /// `mapKv`); `None` falls back to the generic `check` helper. - pub(crate) domain: Option<&'a str>, -} - -impl<'a> ErrCtx<'a> { - /// Build the error context for `f` from its [`ErrorStrategy`]: - /// [`ErrorStrategy::Throws`] surfaces typed errors, [`ErrorStrategy::Trap`] - /// traps on the (panic-only) error path. - pub(crate) fn for_fn(f: &FnBinding, domain: Option<&'a str>) -> Self { - Self { - throws: f.error_strategy() == ErrorStrategy::Throws, - domain, - } - } - - /// The statement checking the error slot named `slot`. - fn check_stmt(&self, slot: &str) -> String { - if !self.throws { - return format!("trap(&{slot})"); - } - match self.domain { - Some(stem) => format!("try check{stem}(&{slot})"), - None => format!("try check(&{slot})"), - } - } - - /// The statement reporting a marshalling failure (`code`, `msg` are - /// literals): a thrown [`ERROR_BRAND`] for a throwing wrapper, a trap - /// otherwise. - fn fail_stmt(&self, code: i32, msg: &str) -> String { - if self.throws { - format!("throw {ERROR_BRAND}.error(code: {code}, message: \"{msg}\")") - } else { - format!("fatalError(\"{code}: {msg}\")") - } - } - - /// A `guard let {name} = {name} else {{ ... }}` line reporting a - /// marshalling failure through [`Self::fail_stmt`]. - fn guard_stmt(&self, name: &str, code: i32, msg: &str) -> String { - format!( - "guard let {name} = {name} else {{ {} }}", - self.fail_stmt(code, msg) - ) - } - - /// The statements an async completion callback runs (after copying the - /// runtime `code`/`msg` locals) when the error slot reports: copy the - /// payload, release the heap-boxed error with `weaveffi_error_free`, and - /// resume throwing the mapped domain error, resume with the generic brand - /// error, or trap. - fn async_err_lines(&self) -> Vec { - if !self.throws { - return vec![ - "weaveffi_error_free(err)".to_string(), - "fatalError(\"\\(code): \\(msg)\")".to_string(), - ]; - } - match self.domain { - Some(stem) => vec![ - "let payload: [UInt8]? = err.pointee.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.pointee.payload_len)) }".to_string(), - "weaveffi_error_free(err)".to_string(), - format!("contRef.value.resume(throwing: map{stem}(code: code, message: msg, payload: payload))"), - ], - None => vec![ - "weaveffi_error_free(err)".to_string(), - format!( - "contRef.value.resume(throwing: {ERROR_BRAND}.error(code: code, message: msg))" - ), - ], - } - } - - /// The statement an async completion callback uses for a marshalling - /// failure with literal `code`/`msg`. - fn async_fail_stmt(&self, code: i32, msg: &str) -> String { - if self.throws { - format!( - "contRef.value.resume(throwing: {ERROR_BRAND}.error(code: {code}, message: \"{msg}\"))" - ) - } else { - format!("fatalError(\"{code}: {msg}\")") - } - } - - /// The Swift error type parameter of the continuation: `Error` for a - /// throwing wrapper, `Never` for a plain one. - fn continuation_err_ty(&self) -> &'static str { - if self.throws { - "Error" - } else { - "Never" - } - } -} - -/// Clone a callable with its parameter names camel-cased and keyword-escaped, -/// so the Swift argument labels, bound locals, and every staged -/// `_ptr`/`_len` variable derived from them agree (and never collide with a -/// reserved word). -pub(crate) fn camel_params(f: &FnBinding) -> FnBinding { - let mut f = f.clone(); - for p in &mut f.params { - p.name = swift_ident(&p.name); - } - f -} - -/// Prepend an instance receiver's pointer to a rendered C argument list. -fn with_self_arg(call_args: String, self_arg: Option<&str>) -> String { - match self_arg { - Some(recv) if call_args.is_empty() => recv.to_string(), - Some(recv) => format!("{recv}, {call_args}"), - None => call_args, - } -} - -/// `true` when `p` is staged through a pointer-borrowing closure -/// (`withCString` or `withUnsafeBufferPointer`) rather than passed directly. -fn needs_staging_closure(p: &ParamBinding) -> bool { - matches!( - p.arg_pass(), - ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } - ) -} - -/// Render one synchronous (or iterator-returning) callable. `swift_name` is -/// the already-cased wrapper name; `self_arg` is `Some("ptr")` for an -/// instance method, making the wrapper a member `func` that passes its own -/// handle as the leading C argument. -#[allow(clippy::too_many_arguments)] -pub(crate) fn render_swift_function( - out: &mut String, - c_prefix: &str, - module_name: &str, - f: &FnBinding, - swift_name: &str, - self_arg: Option<&str>, - err: ErrCtx, - ctx: SwiftCtx, -) { - let mut w = CodeWriter::four_space().with_depth(1); - { - let mut tmp = String::new(); - emit_fn_doc(&mut tmp, &f.doc, &f.params, " "); - w.raw(tmp); - } - if let CallShape::Iterator(_) = &f.shape { - w.line("/// - Returns: A lazy sequence that pulls one element per step from the"); - w.line("/// producer; the underlying iterator is destroyed when the sequence is"); - w.line("/// exhausted or deinitialized."); - if err.throws { - w.line("/// - Throws: The module's typed error if the launch fails. Mid-stream"); - w.line("/// errors end iteration and are stored in the sequence's `error`"); - w.line("/// property instead of being thrown."); - } - } - if let Some(msg) = &f.deprecated { - w.line(format!( - "@available(*, deprecated, message: \"{}\")", - msg.replace('"', "\\\"") - )); - } - let ret_swift = match &f.shape { - CallShape::Iterator(it) => ctx.ty_name(&iterator_class_name(it, c_prefix)), - _ => f - .ret - .as_ref() - .map(|t| swift_type_ctx(t, ctx)) - .unwrap_or_else(|| "Void".to_string()), - }; - let mut sig = String::new(); - write_swift_params_sig(&mut sig, &f.params, ctx); - let static_kw = if self_arg.is_some() { "" } else { "static " }; - let throws_kw = if err.throws { " throws" } else { "" }; - w.line(format!( - "public {static_kw}func {swift_name}({sig}){throws_kw} -> {ret_swift} {{" - )); - w.indent(); - render_call_body(&mut w, f, c_prefix, module_name, self_arg, err, ctx, false); - w.dedent(); - w.line("}"); - out.push_str(&w.finish()); -} - -/// Render the constructor named `new` as `public init`: the body is the -/// shared call body with an assign-to-`self.ptr` tail instead of a -/// wrapper-returning one. Throwing before `self.ptr` is assigned is legal in -/// a root-class initializer, so the error paths carry over unchanged. -pub(crate) fn render_swift_ctor_init( - out: &mut String, - c_prefix: &str, - module_name: &str, - f: &FnBinding, - err: ErrCtx, - ctx: SwiftCtx, -) { - let mut w = CodeWriter::four_space().with_depth(1); - { - let mut tmp = String::new(); - emit_fn_doc(&mut tmp, &f.doc, &f.params, " "); - w.raw(tmp); - } - let mut sig = String::new(); - write_swift_params_sig(&mut sig, &f.params, ctx); - let throws_kw = if err.throws { " throws" } else { "" }; - w.line(format!("public init({sig}){throws_kw} {{")); - w.indent(); - render_call_body(&mut w, f, c_prefix, module_name, None, err, ctx, true); - w.dedent(); - w.line("}"); - out.push_str(&w.finish()); -} - -/// Write `name: SwiftType, name: SwiftType, ...` directly into `out`, -/// avoiding the per-call `format!` and intermediate `Vec` allocations -/// that `params.iter().map(format!).collect::>().join(", ")` would -/// require. Parameters carry real argument labels (their camel-cased, -/// keyword-escaped names). -fn write_swift_params_sig(out: &mut String, params: &[ParamBinding], ctx: SwiftCtx) { - for (i, p) in params.iter().enumerate() { - if i > 0 { - out.push_str(", "); - } - let _ = write!(out, "{}: {}", p.name, swift_type_ctx(&p.ty, ctx)); - } -} - -/// Render the C argument list for `params`: staged params contribute their -/// `_ptr`/`_len` bindings, direct params their converted expressions. -fn build_c_call_args(params: &[ParamBinding], c_prefix: &str, module_name: &str) -> String { - let mut args: Vec = Vec::new(); - for p in params { - match p.arg_pass() { - // Strings are a single NUL-terminated `const char*`. - ArgPass::String { .. } => args.push(format!("{}_ptr", p.name)), - // Bytes and buffered values pass an explicit (ptr, len) pair. - ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { - args.push(format!("{}_ptr", p.name)); - args.push(format!("{}_len", p.name)); - } - // An interface param borrows the wrapper's handle for the call; - // the receiver stays alive for the call frame. A nullable one - // passes null for "none". - ArgPass::Object { nullable, .. } => { - if nullable { - args.push(format!("{}?.ptr", p.name)); - } else { - args.push(format!("{}.ptr", p.name)); - } - } - // A callback interface passes the retained box (staged as - // `{name}_ctx`) plus the interface's process-wide vtable. - ArgPass::Callback { .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback family names a callback interface"); - args.push(format!("{}_ctx", p.name)); - args.push(format!("{}.pointer", callback_vtable_name(cb))); - } - ArgPass::Direct { .. } => match &p.ty { - Ty::Enum(enum_name) => args.push(format!( - "{}({}.rawValue)", - c_enum_type(enum_name, c_prefix, module_name), - p.name - )), - _ => args.push(p.name.clone()), - }, - } - } - args.join(", ") -} - -/// Emit the pre-call staging statements for `params`: byte params are copied -/// into `[UInt8]`, buffered params are packed into a writer (both handed to -/// the C call via `withUnsafeBufferPointer` later), and callback interfaces -/// are boxed with a +1 retain the producer's `free` entry releases. -fn render_arg_staging( - w: &mut CodeWriter, - params: &[ParamBinding], - ctx: SwiftCtx, - counter: &mut usize, -) { - for p in params { - match p.arg_pass() { - ArgPass::Bytes { .. } => { - w.line(format!("let {n}Bytes = Array({n})", n = p.name)); - } - ArgPass::Buffer { .. } => { - w.line(format!("var {n}Writer = WvWriter()", n = p.name)); - let writer = format!("{}Writer", p.name); - write_value_stmts(w, &p.ty, &p.name, &writer, ctx, counter); - } - ArgPass::Callback { .. } => { - let cb = - p.ty.callback_interface_name() - .expect("callback family names a callback interface"); - w.line(format!( - "let {n}_ctx = Unmanaged.passRetained({}({n})).toOpaque()", - callback_box_name(cb), - n = p.name - )); - } - ArgPass::Direct { .. } | ArgPass::String { .. } | ArgPass::Object { .. } => {} - } - } -} - -/// Open one pointer-staging closure for `p` (a string, bytes, or buffered -/// parameter), binding its `_ptr`/`_len` locals; `bind` is the prefix of the -/// opening line (`let rv: T = `, `return `, or nothing). -fn open_staging_closure(w: &mut CodeWriter, p: &ParamBinding, bind: &str) { - let n = &p.name; - match p.arg_pass() { - ArgPass::String { .. } => { - w.line(format!("{bind}{n}.withCString {{ {n}_ptr in")); - w.indent(); - } - ArgPass::Bytes { .. } => { - w.line(format!( - "{bind}{n}Bytes.withUnsafeBufferPointer {{ {n}_buf in" - )); - w.indent(); - w.line(format!("let {n}_ptr = {n}_buf.baseAddress")); - w.line(format!("let {n}_len = {n}_buf.count")); - } - ArgPass::Buffer { .. } => { - w.line(format!( - "{bind}{n}Writer.bytes.withUnsafeBufferPointer {{ {n}_buf in" - )); - w.indent(); - w.line(format!("let {n}_ptr = {n}_buf.baseAddress")); - w.line(format!("let {n}_len = {n}_buf.count")); - } - ArgPass::Direct { .. } | ArgPass::Object { .. } | ArgPass::Callback { .. } => { - unreachable!("only string, bytes, and buffered parameters are staged") - } - } -} - -/// The Swift spelling of the raw C return value of `f`, used to annotate the -/// binding when the call sits inside multi-statement staging closures (whose -/// return type Swift cannot infer). -fn raw_return_swift( - f: &FnBinding, - rp: Option<&RetPass>, - c_prefix: &str, - module_name: &str, -) -> String { - // An iterator launch returns the opaque iterator handle. - let Some(rp) = rp else { - return "OpaquePointer?".to_string(); - }; - match rp { - RetPass::Void => "Void".to_string(), - RetPass::Buffer | RetPass::Bytes => "UnsafePointer?".to_string(), - RetPass::String => "UnsafePointer?".to_string(), - RetPass::Object { .. } => "OpaquePointer?".to_string(), - RetPass::Direct => match f.ret.as_ref() { - 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"), - }, - } -} - -/// The interface name behind an object-returning callable (direct or -/// nullable). -fn ret_interface_name(f: &FnBinding) -> &str { - match f.ret.as_ref() { - 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"), - } -} - -/// Render the shared body of a synchronous callable: the error slot, input -/// staging (byte copies and buffer packing), the C call wrapped in whatever -/// pointer-staging closures the inputs need, the error check, and the return -/// conversion. With `ctor` set, an interface-returning tail assigns -/// `self.ptr` instead of wrapping the pointer. -#[allow(clippy::too_many_arguments)] -fn render_call_body( - w: &mut CodeWriter, - f: &FnBinding, - c_prefix: &str, - module_name: &str, - self_arg: Option<&str>, - err: ErrCtx, - ctx: SwiftCtx, - ctor: bool, -) { - let mut counter = 0usize; - w.line("var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0)"); - render_arg_staging(w, &f.params, ctx, &mut counter); - - // An iterator launch returns the handle, not a value; its return plan is - // the iterator protocol rather than a `RetPass`. - let rp = match &f.shape { - CallShape::Iterator(_) => None, - _ => Some(ret_pass(f.ret.as_ref(), module_name, c_prefix)), - }; - let needs_out_len = matches!(rp, Some(RetPass::Bytes | RetPass::Buffer)); - if needs_out_len { - w.line("var outLen: Int = 0"); - } - - let c_sym = &f.c_base; - let mut all_args = with_self_arg( - build_c_call_args(&f.params, c_prefix, module_name), - self_arg, - ); - if needs_out_len { - if all_args.is_empty() { - all_args.push_str("&outLen"); - } else { - all_args.push_str(", &outLen"); - } - } - let call = if all_args.is_empty() { - format!("{c_sym}(&err)") - } else { - format!("{c_sym}({all_args}, &err)") - }; - - let closure_params: Vec<&ParamBinding> = f - .params - .iter() - .filter(|p| needs_staging_closure(p)) - .collect(); - let has_ret = f.ret.is_some(); - - if closure_params.is_empty() { - if has_ret { - w.line(format!("let rv = {call}")); - } else { - w.line(call); - } - } else { - let raw_ty = raw_return_swift(f, rp.as_ref(), c_prefix, module_name); - for (i, p) in closure_params.iter().enumerate() { - let bind = if !has_ret { - String::new() - } else if i == 0 { - format!("let rv: {raw_ty} = ") - } else { - "return ".to_string() - }; - open_staging_closure(w, p, &bind); - } - if has_ret { - w.line(format!("return {call}")); - } else { - w.line(call); - } - for _ in 0..closure_params.len() { - w.dedent(); - w.line("}"); - } - } - - w.line(err.check_stmt("err")); - render_return_tail(w, f, rp, err, ctx, ctor, &mut counter); -} - -/// Render the post-check return conversion of a callable body, consuming the -/// raw call result bound as `rv`. `rp` is `None` for an iterator launch, -/// whose handle wraps into the per-function sequence class. -fn render_return_tail( - w: &mut CodeWriter, - f: &FnBinding, - rp: Option, - err: ErrCtx, - ctx: SwiftCtx, - ctor: bool, - counter: &mut usize, -) { - match rp { - None => { - let CallShape::Iterator(it) = &f.shape else { - unreachable!("iterator return implies iterator shape") - }; - let class_name = ctx.ty_name(&iterator_class_name(it, ctx.c_prefix)); - w.line(err.guard_stmt("rv", -1, "null iterator")); - w.line(format!("return {class_name}(handle: rv)")); - } - Some(RetPass::Void) => {} - Some(RetPass::Buffer) => { - let ty = f.ret.as_ref().expect("a buffered return carries a type"); - // Copy the encoding, release the producer buffer, then decode. - w.line(err.guard_stmt("rv", -1, "null buffer")); - w.line("let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen))"); - w.line("weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen)"); - w.line("var rvReader = WvReader(bytes: rvBytes)"); - let v = fresh(counter, "v"); - read_value_stmts(w, ty, &v, "rvReader", ctx, counter); - w.line("rvReader.finish()"); - w.line(format!("return {v}")); - } - Some(RetPass::String) => { - w.line(err.guard_stmt("rv", -1, "null string")); - w.line("defer { weaveffi_free_string(rv) }"); - w.line("return String(cString: rv)"); - } - Some(RetPass::Bytes) => { - w.line("guard let rv = rv else { return Data() }"); - w.line("defer { weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) }"); - w.line("return Data(bytes: rv, count: outLen)"); - } - Some(RetPass::Object { - nullable: false, .. - }) => { - let name = ret_interface_name(f); - w.line(err.guard_stmt("rv", -1, "null pointer")); - if ctor { - w.line("self.ptr = rv"); - } else { - w.line(format!( - "return {}(ptr: rv)", - ctx.ty_name(local_type_name(name)) - )); - } - } - // A nullable owned object pointer: null means none. - Some(RetPass::Object { nullable: true, .. }) => { - let name = ret_interface_name(f); - w.line(format!( - "return rv.map {{ {}(ptr: $0) }}", - ctx.ty_name(local_type_name(name)) - )); - } - Some(RetPass::Direct) => match f.ret.as_ref() { - Some(Ty::Enum(name)) => { - let ty_name = ctx.ty_name(local_type_name(name)); - w.line(format!("return {ty_name}(rawValue: rv.rawValue)!")); - } - _ => { - w.line("return rv"); - } - }, - } -} - -/// Render one async callable as a continuation-backed `async` wrapper. A -/// throwing callable is `async throws` over a throwing continuation resuming -/// the module's typed error; a plain one is `async` over a never-throwing -/// continuation that traps on the (panic-only) error path. `self_arg` works -/// as in [`render_swift_function`]. -#[allow(clippy::too_many_arguments)] -pub(crate) fn render_swift_async_function( - out: &mut String, - c_prefix: &str, - module_name: &str, - f: &FnBinding, - swift_name: &str, - self_arg: Option<&str>, - err: ErrCtx, - ctx: SwiftCtx, -) { - let mut w = CodeWriter::four_space().with_depth(1); - { - let mut tmp = String::new(); - emit_fn_doc(&mut tmp, &f.doc, &f.params, " "); - w.raw(tmp); - } - if let Some(msg) = &f.deprecated { - w.line(format!( - "@available(*, deprecated, message: \"{}\")", - msg.replace('"', "\\\"") - )); - } - let ret_swift = f - .ret - .as_ref() - .map(|t| swift_type_ctx(t, ctx)) - .unwrap_or_else(|| "Void".to_string()); - let err_ty = err.continuation_err_ty(); - let mut sig = String::new(); - write_swift_params_sig(&mut sig, &f.params, ctx); - let static_kw = if self_arg.is_some() { "" } else { "static " }; - if err.throws { - w.line(format!( - "public {static_kw}func {swift_name}({sig}) async throws -> {ret_swift} {{" - )); - w.indent(); - w.line(format!( - "try await withCheckedThrowingContinuation {{ (continuation: CheckedContinuation<{ret_swift}, Error>) in" - )); - } else { - w.line(format!( - "public {static_kw}func {swift_name}({sig}) async -> {ret_swift} {{" - )); - w.indent(); - w.line(format!( - "await withCheckedContinuation {{ (continuation: CheckedContinuation<{ret_swift}, Never>) in" - )); - } - w.indent(); - w.line("let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque()"); - - // Staging: identical to the sync path. The producer copies every input - // synchronously during the launch (and retains the callback pair for as - // long as it needs it), so pointer validity for the launch call's - // duration is sufficient. - let mut counter = 0usize; - render_arg_staging(&mut w, &f.params, ctx, &mut counter); - - // The launch returns void, so the staging closures carry no binding. - let closure_params: Vec<&ParamBinding> = f - .params - .iter() - .filter(|p| needs_staging_closure(p)) - .collect(); - for p in &closure_params { - open_staging_closure(&mut w, p, ""); - } - - let c_sym = format!("{}_async", f.c_base); - let call_args = with_self_arg( - build_c_call_args(&f.params, c_prefix, module_name), - self_arg, - ); - let rp = ret_pass(f.ret.as_ref(), module_name, c_prefix); - let cb_param_names = async_callback_param_names(&rp); - - let mut launch_prefix = String::new(); - if !call_args.is_empty() { - launch_prefix.push_str(&call_args); - launch_prefix.push_str(", "); - } - if f.cancellable { - launch_prefix.push_str("nil, "); - } - w.line(format!("{c_sym}({launch_prefix}{{ {cb_param_names} in")); - w.indent(); - w.line(format!( - "let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue()" - )); - w.line("if let err = err, err.pointee.code != 0 {"); - w.indent(); - w.line("let code = err.pointee.code"); - w.line("let msg = err.pointee.message.flatMap { String(cString: $0) } ?? \"\""); - for line in err.async_err_lines() { - w.line(line); - } - w.dedent(); - w.line("} else {"); - w.indent(); - render_async_resume_result(&mut w, f, &rp, err, ctx, &mut counter); - w.dedent(); - w.line("}"); - w.dedent(); - w.line("}, ctx)"); - - for _ in 0..closure_params.len() { - w.dedent(); - w.line("}"); - } - w.dedent(); - w.line("}"); - w.dedent(); - w.line("}"); - out.push_str(&w.finish()); -} - -/// The formal names of the async completion callback's slots, by return -/// plan: `context` and `err` always, plus the result slot(s) the return -/// shape adds. -fn async_callback_param_names(rp: &RetPass) -> &'static str { - match rp { - RetPass::Void => "context, err", - RetPass::Buffer => "context, err, resultPtr, resultLen", - RetPass::Bytes => "context, err, result, resultLen", - _ => "context, err, result", - } -} - -/// Render the success branch of an async completion callback: convert the -/// callback's result slots and resume the continuation exactly once. -/// -/// Result buffers (strings, bytes, and buffered values) are owned by the -/// consumer: they're deep-copied or decoded here, then released through the -/// runtime free symbols. An owned-object result is instead adopted by its -/// wrapper class. -fn render_async_resume_result( - w: &mut CodeWriter, - f: &FnBinding, - rp: &RetPass, - err: ErrCtx, - ctx: SwiftCtx, - counter: &mut usize, -) { - match rp { - RetPass::Void => { - w.line("contRef.value.resume(returning: ())"); - } - RetPass::Buffer => { - let ty = f.ret.as_ref().expect("a buffered return carries a type"); - // Owned by the consumer: copy the bytes, free the producer - // allocation, then decode from the copy. - w.line("guard let resultPtr = resultPtr else {"); - w.indent(); - w.line(err.async_fail_stmt(-1, "null buffer")); - if err.throws { - w.line("return"); - } - w.dedent(); - w.line("}"); - w.line( - "let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen))", - ); - w.line("weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen)"); - w.line("var resultReader = WvReader(bytes: resultBytes)"); - let v = fresh(counter, "v"); - read_value_stmts(w, ty, &v, "resultReader", ctx, counter); - w.line("resultReader.finish()"); - w.line(format!("contRef.value.resume(returning: {v})")); - } - RetPass::String => { - w.line("guard let result = result else {"); - w.indent(); - w.line(err.async_fail_stmt(-1, "null string")); - // `fatalError` already never returns; only the resuming - // (throwing) flavor needs an explicit exit from the guard. - if err.throws { - w.line("return"); - } - w.dedent(); - w.line("}"); - // Owned by the consumer: copy the string, then free it. - w.line("let resultValue = String(cString: result)"); - w.line("weaveffi_free_string(result)"); - w.line("contRef.value.resume(returning: resultValue)"); - } - RetPass::Bytes => { - w.line("if let result = result {"); - w.scope(|w| { - w.line("let resultValue = Data(bytes: result, count: resultLen)"); - w.line("weaveffi_free_bytes(UnsafeMutablePointer(mutating: result), resultLen)"); - w.line("contRef.value.resume(returning: resultValue)"); - }); - w.line("} else {"); - w.scope(|w| { - w.line("contRef.value.resume(returning: Data())"); - }); - w.line("}"); - } - // An owned interface result is adopted: the consumer owns it and the - // wrapper's deinit calls `_destroy`. - RetPass::Object { - nullable: false, .. - } => { - let ty_name = ctx.ty_name(local_type_name(ret_interface_name(f))); - w.line("guard let result = result else {"); - w.indent(); - w.line(err.async_fail_stmt(-1, "null pointer")); - if err.throws { - w.line("return"); - } - w.dedent(); - w.line("}"); - w.line(format!( - "contRef.value.resume(returning: {ty_name}(ptr: result))" - )); - } - // A nullable owned object pointer: null means none. - RetPass::Object { nullable: true, .. } => { - let ty_name = ctx.ty_name(local_type_name(ret_interface_name(f))); - w.line(format!( - "contRef.value.resume(returning: result.map {{ {ty_name}(ptr: $0) }})" - )); - } - RetPass::Direct => match f.ret.as_ref() { - 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)!)" - )); - } - _ => { - w.line("contRef.value.resume(returning: result)"); - } - }, - } -} - -/// Emit the lazy sequence class backing one `iter` function. -/// -/// The class conforms to `Sequence & IteratorProtocol` and owns the C -/// iterator handle. Each `next()` issues exactly one producer `next` call; -/// the handle is destroyed eagerly on exhaustion (or on a mid-stream error) -/// and again, guarded against double-destroy by the nulled handle, from -/// `deinit` when iteration is abandoned early. Elements are converted and -/// released per the [`weaveffi_core::plan::elem_free`] contract: strings are -/// copied then freed, bytes and buffered elements are copied or decoded then -/// released with `weaveffi_free_bytes`, owned interface pointers are adopted -/// by their wrapper classes, and by-value elements need no release. -/// -/// Errors follow the owning function's [`ErrorStrategy`]. `next()` cannot -/// throw under `IteratorProtocol`, so for a throwing function a mid-stream -/// domain error ends iteration and is stored in the sequence's public -/// `error` property for the caller to inspect; for a non-throwing function -/// a reported error can only be a producer bug and traps via `fatalError`. -pub(crate) fn render_swift_iterator_class( - out: &mut String, - mb: &ModuleBinding, - f: &FnBinding, - it: &IteratorBinding, - ctx: SwiftCtx, -) { - let protocol = it.protocol(f, &mb.path, ctx.c_prefix); - let class_name = iterator_class_name(it, ctx.c_prefix); - let next_fn = &it.next.symbol; - let destroy_fn = &it.destroy_symbol; - let inner = &it.elem; - let elem_swift = swift_type_ctx(inner, ctx); - let stem = domain_stem(mb); - let throws = protocol.error == ErrorStrategy::Throws; - let has_len_slot = protocol.elem_free == Free::Bytes; - - // `out_item` is the slot after the iterator handle; render its pointee as - // the element C type so enum slots get the imported C enum - // (`{prefix}_{module}_{Name}`). - let elem_c_type = it - .next - .params - .get(1) - .map(|p| { - p.ty.render_c(ctx.c_prefix) - .trim_end_matches('*') - .trim() - .to_string() - }) - .unwrap_or_default(); - - // The `out_item` slot declaration. - let (c_var, default): (String, String) = match &protocol.elem { - RetPass::Bytes | RetPass::Buffer => { - ("UnsafePointer?".to_string(), "nil".to_string()) - } - RetPass::String => ("UnsafePointer?".to_string(), "nil".to_string()), - RetPass::Object { .. } => ("OpaquePointer?".to_string(), "nil".to_string()), - RetPass::Direct => match inner { - Ty::Enum(_) => (elem_c_type.clone(), format!("{elem_c_type}(0)")), - _ => (swift_type_for(inner), swift_scalar_default(inner)), - }, - RetPass::Void => unreachable!("an iterator element is never void"), - }; - - let mut w = CodeWriter::four_space(); - w.line(format!( - "/// A lazy sequence over the `{elem_swift}` elements streamed by `{}`.", - it.launch.symbol - )); - w.line("///"); - w.line("/// Each `next()` call pulls exactly one element from the producer. The"); - w.line("/// underlying C iterator is destroyed eagerly on exhaustion and from"); - w.line("/// `deinit` when iteration is abandoned early."); - if throws { - w.line("///"); - w.line("/// If the producer reports an error mid-stream, iteration ends and the"); - w.line("/// error is stored in ``error`` for the caller to inspect after the loop."); - } - w.line(format!( - "public final class {class_name}: Sequence, IteratorProtocol {{" - )); - w.indent(); - w.line("private var handle: OpaquePointer?"); - if throws { - w.line("/// The error that ended iteration early, if any."); - w.line("public private(set) var error: Error?"); - } - w.blank(); - w.line("init(handle: OpaquePointer) {"); - w.scope(|w| { - w.line("self.handle = handle"); - }); - w.line("}"); - w.blank(); - w.line("deinit {"); - w.scope(|w| { - w.line("destroyHandle()"); - }); - w.line("}"); - w.blank(); - w.line("private func destroyHandle() {"); - w.scope(|w| { - w.line("guard let handle = handle else { return }"); - w.line(format!("{destroy_fn}(handle)")); - w.line("self.handle = nil"); - }); - w.line("}"); - w.blank(); - w.line("/// Pulls the next element from the producer, or returns `nil` once the"); - w.line("/// stream is exhausted (destroying the underlying iterator)."); - w.line(format!("public func next() -> {elem_swift}? {{")); - w.indent(); - w.line("guard let handle = handle else { return nil }"); - w.line(format!("var item: {c_var} = {default}")); - if has_len_slot { - w.line("var itemLen: Int = 0"); - } - w.line("var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0)"); - if has_len_slot { - w.line(format!( - "if {next_fn}(handle, &item, &itemLen, &err) == 0 {{" - )); - } else { - w.line(format!("if {next_fn}(handle, &item, &err) == 0 {{")); - } - w.indent(); - w.line("if err.code != 0 {"); - w.indent(); - w.line("let code = err.code"); - w.line("let message = err.message.flatMap { String(cString: $0) } ?? \"\""); - if throws { - match &stem { - Some(stem) => { - w.line("let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) }"); - w.line("weaveffi_error_clear(&err)"); - w.line(format!( - "self.error = map{stem}(code: code, message: message, payload: payload)" - )); - } - None => { - w.line("weaveffi_error_clear(&err)"); - w.line(format!( - "self.error = {ERROR_BRAND}.error(code: code, message: message)" - )); - } - } - } else { - w.line("weaveffi_error_clear(&err)"); - w.line("fatalError(\"\\(code): \\(message)\")"); - } - w.dedent(); - w.line("}"); - w.line("destroyHandle()"); - w.line("return nil"); - w.dedent(); - w.line("}"); - - match &protocol.elem { - RetPass::Buffer => { - // Decode the element buffer, then release it (`Free::Bytes`). - w.line("let itemBytes = [UInt8](UnsafeBufferPointer(start: item, count: itemLen))"); - w.line("weaveffi_free_bytes(UnsafeMutablePointer(mutating: item), itemLen)"); - w.line("var itemReader = WvReader(bytes: itemBytes)"); - let mut counter = 0usize; - let v = fresh(&mut counter, "v"); - read_value_stmts(&mut w, inner, &v, "itemReader", ctx, &mut counter); - w.line("itemReader.finish()"); - w.line(format!("return {v}")); - } - RetPass::Bytes => { - w.line("let element = Data(bytes: item!, count: itemLen)"); - w.line("weaveffi_free_bytes(UnsafeMutablePointer(mutating: item), itemLen)"); - w.line("return element"); - } - RetPass::String => { - w.line("let element = String(cString: item!)"); - w.line("weaveffi_free_string(item)"); - w.line("return element"); - } - // An owned interface element is adopted by the wrapper class, whose - // deinit owes the `_destroy`; a nullable one adopts when non-null. - RetPass::Object { nullable, .. } => { - let wrapper = ctx.ty_name(local_type_name( - inner - .interface_name() - .expect("object element names an interface"), - )); - if *nullable { - w.line(format!("return item.map {{ {wrapper}(ptr: $0) }}")); - } else { - w.line(format!("return {wrapper}(ptr: item!)")); - } - } - RetPass::Direct => match inner { - Ty::Enum(name) => { - w.line(format!( - "return {}(rawValue: item.rawValue)!", - ctx.ty_name(local_type_name(name)) - )); - } - _ => { - w.line("return item"); - } - }, - RetPass::Void => unreachable!("an iterator element is never void"), - } - w.dedent(); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-swift/src/codec.rs b/crates/weaveffi-gen-swift/src/codec.rs deleted file mode 100644 index c14a4402..00000000 --- a/crates/weaveffi-gen-swift/src/codec.rs +++ /dev/null @@ -1,255 +0,0 @@ -//! Value-buffer codec emitters: the Swift statements serializing and -//! decoding one value in the wire format. -//! -//! Both emitters dispatch on the shared [`Ty::wire`] classification, so the -//! non-obvious folds (interfaces as `u64` object tokens carrying one strong -//! reference, records and rich enums through one user codec) are 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 crate::types::{swift_type_ctx, SwiftCtx}; - -/// A fresh generated-variable name (`v0`, `n1`, ...) unique within one -/// rendering scope. -pub(crate) fn fresh(counter: &mut usize, prefix: &str) -> String { - let id = *counter; - *counter += 1; - format!("{prefix}{id}") -} - -/// Emit statements serializing `expr` (of IR type `ty`) into the `WvWriter` -/// variable named `writer`, recursing through optionals, lists, and maps and -/// delegating records and rich enums to their generated `wvWrite*` codecs. -pub(crate) fn write_value_stmts( - w: &mut CodeWriter, - ty: &Ty, - expr: &str, - writer: &str, - ctx: SwiftCtx, - counter: &mut usize, -) { - match ty.wire() { - WireType::Prim(Prim::Bool) => { - w.line(format!("{writer}.writeBool({expr})")); - } - WireType::Prim(Prim::I8) => { - w.line(format!("{writer}.writeInt8({expr})")); - } - WireType::Prim(Prim::U8) => { - w.line(format!("{writer}.writeUInt8({expr})")); - } - WireType::Prim(Prim::I16) => { - w.line(format!("{writer}.writeInt16({expr})")); - } - WireType::Prim(Prim::U16) => { - w.line(format!("{writer}.writeUInt16({expr})")); - } - WireType::Prim(Prim::I32) => { - w.line(format!("{writer}.writeInt32({expr})")); - } - WireType::Prim(Prim::U32) => { - w.line(format!("{writer}.writeUInt32({expr})")); - } - WireType::Prim(Prim::I64) => { - w.line(format!("{writer}.writeInt64({expr})")); - } - WireType::Prim(Prim::U64) => { - w.line(format!("{writer}.writeUInt64({expr})")); - } - WireType::Prim(Prim::F32) => { - w.line(format!("{writer}.writeFloat({expr})")); - } - WireType::Prim(Prim::F64) => { - w.line(format!("{writer}.writeDouble({expr})")); - } - WireType::Prim(Prim::String) => { - w.line(format!("{writer}.writeString({expr})")); - } - WireType::Prim(Prim::Bytes) => { - w.line(format!("{writer}.writeBytes({expr})")); - } - // An object token carries one strong reference, so the wrapper - // writes a freshly cloned pointer and keeps its own. - WireType::Object(_) => { - w.line(format!("{writer}.writeObject({expr}.clonePtr())")); - } - WireType::Enum(name) => { - // C-style enums cross as `i32` on the wire; a `UInt32`-raw Swift - // enum reinterprets its bits. - if ctx.enum_raw(local_type_name(name)) == "Int32" { - w.line(format!("{writer}.writeInt32({expr}.rawValue)")); - } else { - w.line(format!( - "{writer}.writeInt32(Int32(bitPattern: {expr}.rawValue))" - )); - } - } - WireType::User(name) => { - w.line(format!( - "wvWrite{}({expr}, into: &{writer})", - local_type_name(name) - )); - } - WireType::Optional(inner) => { - let v = fresh(counter, "v"); - w.line(format!("if let {v} = {expr} {{")); - w.indent(); - w.line(format!("{writer}.writeOptionFlag(true)")); - write_value_stmts(w, inner, &v, writer, ctx, counter); - w.dedent(); - w.line("} else {"); - w.indent(); - w.line(format!("{writer}.writeOptionFlag(false)")); - w.dedent(); - w.line("}"); - } - WireType::List(inner) => { - let v = fresh(counter, "v"); - w.line(format!("{writer}.writeLen({expr}.count)")); - w.line(format!("for {v} in {expr} {{")); - w.indent(); - write_value_stmts(w, inner, &v, writer, ctx, counter); - w.dedent(); - w.line("}"); - } - WireType::Map(k, val) => { - let kv = fresh(counter, "v"); - let vv = fresh(counter, "v"); - w.line(format!("{writer}.writeLen({expr}.count)")); - w.line(format!("for ({kv}, {vv}) in {expr} {{")); - w.indent(); - write_value_stmts(w, k, &kv, writer, ctx, counter); - write_value_stmts(w, val, &vv, writer, ctx, counter); - w.dedent(); - w.line("}"); - } - } -} - -/// Emit statements deserializing one value of IR type `ty` from the -/// `WvReader` variable named `reader`, binding the result to `out`. -pub(crate) fn read_value_stmts( - w: &mut CodeWriter, - ty: &Ty, - out: &str, - reader: &str, - ctx: SwiftCtx, - counter: &mut usize, -) { - match ty.wire() { - WireType::Prim(Prim::Bool) => { - w.line(format!("let {out} = {reader}.readBool()")); - } - WireType::Prim(Prim::I8) => { - w.line(format!("let {out} = {reader}.readInt8()")); - } - WireType::Prim(Prim::U8) => { - w.line(format!("let {out} = {reader}.readUInt8()")); - } - WireType::Prim(Prim::I16) => { - w.line(format!("let {out} = {reader}.readInt16()")); - } - WireType::Prim(Prim::U16) => { - w.line(format!("let {out} = {reader}.readUInt16()")); - } - WireType::Prim(Prim::I32) => { - w.line(format!("let {out} = {reader}.readInt32()")); - } - WireType::Prim(Prim::U32) => { - w.line(format!("let {out} = {reader}.readUInt32()")); - } - WireType::Prim(Prim::I64) => { - w.line(format!("let {out} = {reader}.readInt64()")); - } - WireType::Prim(Prim::U64) => { - w.line(format!("let {out} = {reader}.readUInt64()")); - } - WireType::Prim(Prim::F32) => { - w.line(format!("let {out} = {reader}.readFloat()")); - } - WireType::Prim(Prim::F64) => { - w.line(format!("let {out} = {reader}.readDouble()")); - } - WireType::Prim(Prim::String) => { - w.line(format!("let {out} = {reader}.readString()")); - } - WireType::Prim(Prim::Bytes) => { - w.line(format!("let {out} = {reader}.readBytes()")); - } - // The token's reference is adopted by a new wrapper, whose deinit - // owes the `_destroy`. - WireType::Object(name) => { - w.line(format!( - "let {out} = {}(ptr: {reader}.readObject())", - ctx.ty_name(local_type_name(name)) - )); - } - WireType::Enum(name) => { - let local = local_type_name(name); - let ty_name = ctx.ty_name(local); - // An unknown discriminant traps, matching the decode-failure - // channel. - if ctx.enum_raw(local) == "Int32" { - w.line(format!( - "let {out} = {ty_name}(rawValue: {reader}.readInt32())!" - )); - } else { - w.line(format!( - "let {out} = {ty_name}(rawValue: UInt32(bitPattern: {reader}.readInt32()))!" - )); - } - } - WireType::User(name) => { - w.line(format!( - "let {out} = wvRead{}(&{reader})", - local_type_name(name) - )); - } - WireType::Optional(inner) => { - let t = swift_type_ctx(inner, ctx); - w.line(format!("var {out}: {t}? = nil")); - w.line(format!("if {reader}.readOptionFlag() {{")); - w.indent(); - let v = fresh(counter, "v"); - read_value_stmts(w, inner, &v, reader, ctx, counter); - w.line(format!("{out} = {v}")); - w.dedent(); - w.line("}"); - } - WireType::List(inner) => { - let t = swift_type_ctx(inner, ctx); - let cnt = fresh(counter, "n"); - w.line(format!("let {cnt} = {reader}.readLen()")); - w.line(format!("var {out}: [{t}] = []")); - w.line(format!("{out}.reserveCapacity({cnt})")); - w.line(format!("for _ in 0..<{cnt} {{")); - w.indent(); - let v = fresh(counter, "v"); - read_value_stmts(w, inner, &v, reader, ctx, counter); - w.line(format!("{out}.append({v})")); - w.dedent(); - w.line("}"); - } - WireType::Map(k, val) => { - let kt = swift_type_ctx(k, ctx); - let vt = swift_type_ctx(val, ctx); - let cnt = fresh(counter, "n"); - w.line(format!("let {cnt} = {reader}.readLen()")); - w.line(format!("var {out}: [{kt}: {vt}] = [:]")); - w.line(format!("{out}.reserveCapacity({cnt})")); - w.line(format!("for _ in 0..<{cnt} {{")); - w.indent(); - let kv = fresh(counter, "v"); - let vv = fresh(counter, "v"); - read_value_stmts(w, k, &kv, reader, ctx, counter); - read_value_stmts(w, val, &vv, reader, ctx, counter); - w.line(format!("{out}[{kv}] = {vv}")); - w.dedent(); - w.line("}"); - } - } -} diff --git a/crates/weaveffi-gen-swift/src/entities.rs b/crates/weaveffi-gen-swift/src/entities.rs deleted file mode 100644 index 8c8a1404..00000000 --- a/crates/weaveffi-gen-swift/src/entities.rs +++ /dev/null @@ -1,653 +0,0 @@ -//! Entity rendering: enums, rich enums, records, interfaces, typed error -//! domains, and the recursive module walk emitting them (callback interfaces -//! render through [`crate::callbacks`]). - -use std::collections::HashMap; - -use heck::ToUpperCamelCase; -use weaveffi_core::codegen::common::DocCommentStyle; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::ERROR_BRAND; -use weaveffi_core::model::{ - CallShape, EnumBinding, ErrorBinding, FieldBinding, InterfaceBinding, ModuleBinding, - StructBinding, -}; -use weaveffi_core::utils::{local_type_name, wrapper_name}; -use weaveffi_ir::ir::Module; - -use crate::callbacks::render_swift_callback_interface; -use crate::calls::{ - camel_params, render_swift_async_function, render_swift_ctor_init, render_swift_function, - render_swift_iterator_class, ErrCtx, -}; -use crate::codec::{fresh, read_value_stmts, write_value_stmts}; -use crate::types::{enum_raw_type, swift_ident, swift_str, swift_type_ctx, SwiftCtx}; - -/// The PascalCase helper stem of the domain in effect for `module`, naming the -/// per-domain `check{Stem}`/`map{Stem}` helpers (derived from the *declaring* -/// module's path, so inheriting submodules reference the ancestor's helper). -pub(crate) fn domain_stem(module: &ModuleBinding) -> Option { - module - .error - .as_ref() - .map(|e| e.owner_path.to_upper_camel_case()) -} - -/// Render a C-style enum as a raw-value Swift enum, one case per variant. -fn render_swift_enum(out: &mut String, e: &EnumBinding) { - let raw = enum_raw_type(e); - let mut w = CodeWriter::four_space(); - w.doc(&e.doc, DocCommentStyle::TripleSlash); - w.line(format!("public enum {}: {} {{", e.name, raw)); - w.scope(|w| { - for v in &e.variants { - w.doc(&v.doc, DocCommentStyle::TripleSlash); - w.line(format!("case {} = {}", swift_ident(&v.name), v.value)); - } - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Render a rich (algebraic) enum as a native Swift enum with associated -/// values: one case per variant, with labeled associated values matching the -/// variant's field names. The value crosses the ABI as a buffer; its codec -/// pair is emitted by [`render_rich_enum_codec`]. -fn render_swift_rich_enum(out: &mut String, e: &EnumBinding, ctx: SwiftCtx) { - let mut w = CodeWriter::four_space(); - w.doc(&e.doc, DocCommentStyle::TripleSlash); - w.line(format!("public enum {} {{", e.name)); - w.scope(|w| { - for v in &e.variants { - w.doc(&v.doc, DocCommentStyle::TripleSlash); - let case_name = swift_ident(&v.name); - if v.fields.is_empty() { - w.line(format!("case {case_name}")); - } else { - let assoc = v - .fields - .iter() - .map(|f| format!("{}: {}", swift_ident(&f.name), swift_type_ctx(&f.ty, ctx))) - .collect::>() - .join(", "); - w.line(format!("case {case_name}({assoc})")); - } - } - }); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the `wvWrite{Name}`/`wvRead{Name}` codec pair for a rich enum: the -/// writer switches on the case, writes the `i32` tag, then the active -/// variant's fields in declaration order; the reader inverts it and traps on -/// an unknown tag. -fn render_rich_enum_codec(out: &mut String, e: &EnumBinding, ctx: SwiftCtx) { - let local = local_type_name(&e.name); - let ty_name = ctx.ty_name(local); - let mut w = CodeWriter::four_space(); - let mut counter = 0usize; - - w.line(format!( - "/// Serializes a `{local}` into the value-buffer wire format." - )); - w.line(format!( - "func wvWrite{local}(_ value: {ty_name}, into w: inout WvWriter) {{" - )); - w.indent(); - w.line("switch value {"); - for v in &e.variants { - let case_name = swift_ident(&v.name); - if v.fields.is_empty() { - w.line(format!("case .{case_name}:")); - w.indent(); - w.line(format!("w.writeInt32({})", v.value)); - w.dedent(); - } else { - let binds: Vec = v.fields.iter().map(|_| fresh(&mut counter, "v")).collect(); - w.line(format!("case let .{case_name}({}):", binds.join(", "))); - w.indent(); - w.line(format!("w.writeInt32({})", v.value)); - for (f, bind) in v.fields.iter().zip(&binds) { - write_value_stmts(&mut w, &f.ty, bind, "w", ctx, &mut counter); - } - w.dedent(); - } - } - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - - w.line(format!( - "/// Deserializes a `{local}` from the value-buffer wire format." - )); - w.line(format!( - "func wvRead{local}(_ r: inout WvReader) -> {ty_name} {{" - )); - w.indent(); - w.line("let tag = r.readInt32()"); - w.line("switch tag {"); - for v in &e.variants { - let case_name = swift_ident(&v.name); - w.line(format!("case {}:", v.value)); - w.indent(); - if v.fields.is_empty() { - w.line(format!("return .{case_name}")); - } else { - let mut labeled = Vec::new(); - for f in &v.fields { - let var = fresh(&mut counter, "v"); - read_value_stmts(&mut w, &f.ty, &var, "r", ctx, &mut counter); - labeled.push(format!("{}: {var}", swift_ident(&f.name))); - } - w.line(format!("return .{case_name}({})", labeled.join(", "))); - } - w.dedent(); - } - w.line("default:"); - w.indent(); - w.line(format!("wvDecodeFailure(\"unknown {local} tag \\(tag)\")")); - w.dedent(); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Render a record as a plain Swift struct: one typed `public var` per field -/// and an explicit public memberwise initializer (the compiler-synthesized -/// one is internal). The value crosses the ABI as a buffer; its codec pair is -/// emitted by [`render_struct_codec`]. -fn render_swift_struct(out: &mut String, s: &StructBinding, ctx: SwiftCtx) { - let mut w = CodeWriter::four_space(); - w.doc(&s.doc, DocCommentStyle::TripleSlash); - w.line(format!("public struct {} {{", s.name)); - w.indent(); - for f in &s.fields { - w.doc(&f.doc, DocCommentStyle::TripleSlash); - w.line(format!( - "public var {}: {}", - swift_ident(&f.name), - swift_type_ctx(&f.ty, ctx) - )); - } - w.blank(); - let params = s - .fields - .iter() - .map(|f| format!("{}: {}", swift_ident(&f.name), swift_type_ctx(&f.ty, ctx))) - .collect::>() - .join(", "); - w.line(format!("/// Creates a `{}` value.", s.name)); - w.line(format!("public init({params}) {{")); - w.scope(|w| { - for f in &s.fields { - let prop = swift_ident(&f.name); - w.line(format!("self.{prop} = {prop}")); - } - }); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the `wvWrite{Name}`/`wvRead{Name}` codec pair for a record: fields in -/// declaration order, delegating nested buffered types recursively. -fn render_struct_codec(out: &mut String, s: &StructBinding, ctx: SwiftCtx) { - let local = local_type_name(&s.name); - let ty_name = ctx.ty_name(local); - let mut w = CodeWriter::four_space(); - let mut counter = 0usize; - - w.line(format!( - "/// Serializes a `{local}` into the value-buffer wire format." - )); - w.line(format!( - "func wvWrite{local}(_ value: {ty_name}, into w: inout WvWriter) {{" - )); - w.indent(); - for f in &s.fields { - let expr = format!("value.{}", swift_ident(&f.name)); - write_value_stmts(&mut w, &f.ty, &expr, "w", ctx, &mut counter); - } - w.dedent(); - w.line("}"); - w.blank(); - - w.line(format!( - "/// Deserializes a `{local}` from the value-buffer wire format." - )); - w.line(format!( - "func wvRead{local}(_ r: inout WvReader) -> {ty_name} {{" - )); - w.indent(); - let mut labeled = Vec::new(); - for f in &s.fields { - let var = fresh(&mut counter, "v"); - read_value_stmts(&mut w, &f.ty, &var, "r", ctx, &mut counter); - labeled.push(format!("{}: {var}", swift_ident(&f.name))); - } - w.line(format!("return {ty_name}({})", labeled.join(", "))); - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Render one declaring module's typed error surface: a `public enum -/// {TypeName}: Error` whose lowerCamel cases carry the runtime message plus, -/// for codes that declare payload fields, one labeled associated value per -/// field. Also emits the file-scope `map{Stem}` and `check{Stem}` helpers -/// that convert a non-zero `weaveffi_error` slot into it, decoding the -/// payload buffer for codes with fields. -/// -/// Only declared codes get typed cases. Domain codes are validated positive, -/// so the mapper's `default` arm is what every reserved negative runtime -/// code (generic `-1`, panic `-2`, marshalling `-3`) falls through to: the -/// generic [`ERROR_BRAND`] error. -fn render_swift_error(out: &mut String, module: &ModuleBinding, eb: &ErrorBinding, ctx: SwiftCtx) { - let stem = eb.owner_path.to_upper_camel_case(); - let ty = &eb.type_name; - - let case_decl = |fields: &[FieldBinding]| -> String { - let mut parts = vec!["message: String".to_string()]; - for f in fields { - parts.push(format!( - "{}: {}", - swift_ident(&f.name), - swift_type_ctx(&f.ty, ctx) - )); - } - parts.join(", ") - }; - - let mut w = CodeWriter::four_space(); - w.doc( - &Some(format!( - "Typed errors reported by the `{}` module.", - module.segments.join(".") - )), - DocCommentStyle::TripleSlash, - ); - w.line(format!("public enum {ty}: Error, LocalizedError {{")); - w.indent(); - for c in &eb.codes { - w.doc(&c.doc, DocCommentStyle::TripleSlash); - w.line(format!( - "case {}({})", - swift_ident(&c.name), - case_decl(&c.fields) - )); - } - w.line("public var errorDescription: String? {"); - w.scope(|w| { - w.line("switch self {"); - for c in &eb.codes { - // Bind only the message; wildcard the payload fields. - let mut binds = vec!["message".to_string()]; - binds.extend(c.fields.iter().map(|_| "_".to_string())); - w.line(format!( - "case let .{}({}): return message", - swift_ident(&c.name), - binds.join(", ") - )); - } - w.line("}"); - }); - w.line("}"); - w.line("/// The numeric ABI code carried by this error."); - w.line("public var errorCode: Int32 {"); - w.scope(|w| { - w.line("switch self {"); - for c in &eb.codes { - w.line(format!( - "case .{}: return {}", - swift_ident(&c.name), - c.value - )); - } - w.line("}"); - }); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - - // `map{Stem}`: code -> typed case (default message when the slot carried - // none), decoding the payload buffer for codes that declare fields. - // Unknown code (including every reserved negative runtime code) -> - // generic brand error. - w.line("@inline(__always)"); - w.line(format!( - "func map{stem}(code: Int32, message: String, payload: [UInt8]?) -> Error {{" - )); - w.indent(); - w.line("switch code {"); - for c in &eb.codes { - let case_name = swift_ident(&c.name); - let message_arg = format!( - "message: message.isEmpty ? \"{}\" : message", - swift_str(&c.message) - ); - if c.fields.is_empty() { - w.line(format!( - "case {}: return {ty}.{case_name}({message_arg})", - c.value - )); - } else { - w.line(format!("case {}:", c.value)); - w.indent(); - w.line("var payloadReader = WvReader(bytes: payload ?? [])"); - let mut counter = 0usize; - let mut args = vec![message_arg]; - for f in &c.fields { - let var = fresh(&mut counter, "v"); - read_value_stmts(&mut w, &f.ty, &var, "payloadReader", ctx, &mut counter); - args.push(format!("{}: {var}", swift_ident(&f.name))); - } - w.line("payloadReader.finish()"); - w.line(format!("return {ty}.{case_name}({})", args.join(", "))); - w.dedent(); - } - } - w.line(format!( - "default: return {ERROR_BRAND}.error(code: code, message: message)" - )); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - - w.line("@inline(__always)"); - w.line(format!( - "func check{stem}(_ err: inout weaveffi_error) throws {{" - )); - w.indent(); - w.line("if err.code != 0 {"); - w.scope(|w| { - w.line("let code = err.code"); - w.line("let message = err.message.flatMap { String(cString: $0) } ?? \"\""); - w.line("let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) }"); - w.line("weaveffi_error_clear(&err)"); - w.line(format!( - "throw map{stem}(code: code, message: message, payload: payload)" - )); - }); - w.line("}"); - w.dedent(); - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Render one interface as a `public final class` owning one strong -/// reference to its object: a stored `ptr`, an internal reference-adopting -/// `init(ptr:)`, a `deinit` that releases the reference exactly once through -/// the destroy symbol, and an internal `clonePtr()` that mints a second -/// reference through the clone symbol for positions that take ownership (an -/// object token inside a value buffer). -/// -/// The constructor named `new` surfaces as `public init` (throwing when the -/// IDL marks it `throws`); every other constructor becomes a `public static -/// func` factory. Methods are instance funcs that pass `ptr` as the leading C -/// argument; statics are plain `public static func`s. Member bodies reuse the -/// free-function marshalling paths. -fn render_swift_interface( - out: &mut String, - c_prefix: &str, - module: &ModuleBinding, - iface: &InterfaceBinding, - ctx: SwiftCtx, -) { - let stem = domain_stem(module); - let class_name = local_type_name(&iface.name); - - let mut w = CodeWriter::four_space(); - w.doc(&iface.doc, DocCommentStyle::TripleSlash); - w.line(format!("public final class {class_name} {{")); - w.indent(); - w.line("let ptr: OpaquePointer"); - w.blank(); - w.line("init(ptr: OpaquePointer) {"); - w.scope(|w| { - w.line("self.ptr = ptr"); - }); - w.line("}"); - w.blank(); - w.line("deinit {"); - w.scope(|w| { - w.line(format!("{}(ptr)", iface.destroy_symbol)); - }); - w.line("}"); - w.blank(); - w.line("/// Returns a new strong reference to the same object, for a position that"); - w.line("/// takes ownership of it (an object token inside a value buffer)."); - w.line("func clonePtr() -> OpaquePointer {"); - w.scope(|w| { - w.line(format!("{}(ptr)!", iface.clone_symbol)); - }); - w.line("}"); - w.dedent(); - - let mut members = String::new(); - for c in &iface.constructors { - let f = camel_params(c); - let err = ErrCtx::for_fn(&f, stem.as_deref()); - if f.name == "new" { - render_swift_ctor_init(&mut members, c_prefix, &module.path, &f, err, ctx); - } else { - let swift_name = swift_ident(&f.name); - render_swift_function( - &mut members, - c_prefix, - &module.path, - &f, - &swift_name, - None, - err, - ctx, - ); - } - } - for m in &iface.methods { - let f = camel_params(m); - let err = ErrCtx::for_fn(&f, stem.as_deref()); - let swift_name = swift_ident(&f.name); - if matches!(f.shape, CallShape::Async(_)) { - render_swift_async_function( - &mut members, - c_prefix, - &module.path, - &f, - &swift_name, - Some("ptr"), - err, - ctx, - ); - } else { - render_swift_function( - &mut members, - c_prefix, - &module.path, - &f, - &swift_name, - Some("ptr"), - err, - ctx, - ); - } - } - for s in &iface.statics { - let f = camel_params(s); - let err = ErrCtx::for_fn(&f, stem.as_deref()); - let swift_name = swift_ident(&f.name); - if matches!(f.shape, CallShape::Async(_)) { - render_swift_async_function( - &mut members, - c_prefix, - &module.path, - &f, - &swift_name, - None, - err, - ctx, - ); - } else { - render_swift_function( - &mut members, - c_prefix, - &module.path, - &f, - &swift_name, - None, - err, - ctx, - ); - } - } - w.raw(members); - - w.line("}"); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit every file-scope type a module (and its submodules, recursively) -/// contributes: the typed error surface, enums and their codecs, records and -/// their codecs, callback-interface protocols with their vtables, interface -/// classes, and the sequence classes backing `iter` callables. -pub(crate) fn render_swift_module_types( - out: &mut String, - c_prefix: &str, - by_path: &HashMap<&str, &ModuleBinding>, - m: &Module, - module_path: &str, - ctx: SwiftCtx, -) { - let mb = by_path[module_path]; - if let Some(eb) = mb.error.as_ref().filter(|e| e.declared_here) { - render_swift_error(out, mb, eb, ctx); - } - for e in &mb.enums { - if e.is_rich() { - // A rich (algebraic) enum is a value type: a native Swift enum - // with associated values plus its buffer codec pair. - render_swift_rich_enum(out, e, ctx); - render_rich_enum_codec(out, e, ctx); - } else { - render_swift_enum(out, e); - } - } - for s in &mb.structs { - // A record is a value type: a plain Swift struct plus its buffer - // codec pair. Records have no C symbols at all. - render_swift_struct(out, s, ctx); - render_struct_codec(out, s, ctx); - } - for cb in &mb.callback_interfaces { - render_swift_callback_interface(out, mb, cb, ctx); - } - for i in &mb.interfaces { - render_swift_interface(out, c_prefix, mb, i, ctx); - } - // One lazy sequence class per `iter` callable (free functions and - // interface members alike), emitted at file scope next to the module's - // other wrapper types. - for f in mb.callables() { - if let CallShape::Iterator(it) = &f.shape { - render_swift_iterator_class(out, mb, f, it, ctx); - } - } - for sub in &m.modules { - let sub_path = format!("{module_path}_{}", sub.name); - render_swift_module_types(out, c_prefix, by_path, sub, &sub_path, ctx); - } -} - -/// Emit the body of one module's namespace `enum`: function wrappers at this -/// depth, then one nested namespace `enum` per submodule, re-indented to its -/// depth. -#[allow(clippy::too_many_arguments)] -pub(crate) fn render_swift_module_body( - out: &mut String, - c_prefix: &str, - by_path: &HashMap<&str, &ModuleBinding>, - m: &Module, - module_path: &str, - depth: usize, - strip_module_prefix: bool, - ctx: SwiftCtx, -) { - let indent = " ".repeat(depth); - let mb = by_path[module_path]; - let stem = domain_stem(mb); - let mut bodies: Vec = Vec::new(); - for f in &mb.functions { - let mut buf = String::new(); - let f = camel_params(f); - let swift_name = swift_ident(&wrapper_name(module_path, &f.name, strip_module_prefix)); - let err = ErrCtx::for_fn(&f, stem.as_deref()); - if matches!(f.shape, CallShape::Async(_)) { - render_swift_async_function( - &mut buf, - c_prefix, - module_path, - &f, - &swift_name, - None, - err, - ctx, - ); - } else { - render_swift_function( - &mut buf, - c_prefix, - module_path, - &f, - &swift_name, - None, - err, - ctx, - ); - } - bodies.push(buf); - } - for buf in bodies { - if depth > 1 { - let extra = " ".repeat(depth - 1); - for line in buf.lines() { - if line.is_empty() { - out.push('\n'); - } else { - out.push_str(&extra); - out.push_str(line); - out.push('\n'); - } - } - } else { - out.push_str(&buf); - } - } - for sub in &m.modules { - let sub_path = format!("{module_path}_{}", sub.name); - let sub_name = sub.name.to_upper_camel_case(); - out.push_str(&format!("{indent}public enum {sub_name} {{\n")); - render_swift_module_body( - out, - c_prefix, - by_path, - sub, - &sub_path, - depth + 1, - strip_module_prefix, - ctx, - ); - out.push_str(&format!("{indent}}}\n")); - } -} diff --git a/crates/weaveffi-gen-swift/src/lib.rs b/crates/weaveffi-gen-swift/src/lib.rs deleted file mode 100644 index 17e401b4..00000000 --- a/crates/weaveffi-gen-swift/src/lib.rs +++ /dev/null @@ -1,619 +0,0 @@ -//! Swift binding generator for WeaveFFI. -//! -//! Emits a SwiftPM package containing a thin Swift wrapper over the C ABI, -//! including module map, `Package.swift`, and Swift `async/await` shims for -//! functions marked `async: true`. Implements [`LanguageBackend`]; the shared -//! driver bridges it into the generator pipeline. -//! -//! Records, rich enums, optionals, lists, and maps cross the C ABI as value -//! buffers (one `const uint8_t*` + `size_t` pair in the WeaveFFI wire format). -//! The generated wrapper ships a small private writer/reader pair -//! (`WvWriter`/`WvReader`) plus one pack and one unpack routine per record and -//! rich enum, so records surface as plain Swift structs and rich enums as -//! native Swift enums with associated values. Objects surface as `final -//! class` wrappers owning one strong reference each, and callback interfaces -//! as Swift protocols whose implementations cross the boundary through a -//! process-wide vtable of `@convention(c)` trampolines. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod callbacks; -mod calls; -mod codec; -mod docs; -mod entities; -mod package; -mod runtime; -mod types; - -use std::collections::{HashMap, HashSet}; - -use camino::Utf8Path; -use heck::ToUpperCamelCase; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::codegen::common::walk_modules; -use weaveffi_core::model::{BindingModel, ModuleBinding}; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Module; - -use crate::entities::{render_swift_module_body, render_swift_module_types}; -use crate::package::{ - render_modulemap, render_package_swift, render_packaged_package_swift, render_packaged_readme, - resolve_module_name, -}; -use crate::runtime::{ - render_buffer_runtime, render_callback_support, render_continuation_ref, render_error_infra, -}; -use crate::types::{enum_raw_type, SwiftCtx}; - -/// Per-target configuration for [`SwiftGenerator`]. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct SwiftConfig { - /// SwiftPM module name (default `"WeaveFFI"`). - pub module_name: Option, - /// When `true` (the default), strip the IR module path from emitted - /// function names, so `enum Kv` exposes `openStore` rather than - /// `kvOpenStore`. Set to `false` to restore the module-prefixed spelling. - pub strip_module_prefix: bool, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the Swift wrappers call the same - /// exported symbols the producer emits. - pub prefix: Option, - /// Basename of the IDL the CLI was invoked with. - /// Populated by the CLI; not user-configurable via `[swift]`. - #[serde(skip)] - pub input_basename: Option, -} - -impl Default for SwiftConfig { - fn default() -> Self { - Self { - module_name: None, - strip_module_prefix: true, - prefix: None, - input_basename: None, - } - } -} - -impl SwiftConfig { - /// Returns the configured SwiftPM module name, falling back to - /// `"WeaveFFI"`. - pub fn module_name(&self) -> &str { - self.module_name.as_deref().unwrap_or("WeaveFFI") - } - - /// Returns the configured C ABI symbol prefix, falling back to - /// `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -/// Each module contributes ~2KB of Swift wrapper text on average (struct -/// declarations, codec routines, async wrappers); pre-allocating from this -/// estimate reduces `String` re-allocations as the wrapper grows past 64 KB. -const SWIFT_BASE_BYTES: usize = 4096; -const SWIFT_BYTES_PER_MODULE: usize = 2048; -const SWIFT_BYTES_PER_FUNCTION: usize = 512; -const SWIFT_BYTES_PER_STRUCT: usize = 512; - -/// Estimate the rendered wrapper size from the module tree, for the initial -/// `String` allocation. -fn estimate_swift_capacity(modules: &[Module]) -> usize { - fn count(modules: &[Module]) -> (usize, usize, usize) { - let mut m = 0; - let mut f = 0; - let mut s = 0; - for module in modules { - m += 1; - f += module.functions.len(); - s += module.structs.len(); - let (sm, sf, ss) = count(&module.modules); - m += sm; - f += sf; - s += ss; - } - (m, f, s) - } - let (mods, funcs, structs) = count(modules); - SWIFT_BASE_BYTES - + mods * SWIFT_BYTES_PER_MODULE - + funcs * SWIFT_BYTES_PER_FUNCTION - + structs * SWIFT_BYTES_PER_STRUCT -} - -/// Swift backend: emits a SwiftPM package with a thin Swift wrapper (module -/// map, `Package.swift`, and `async`/`await` shims) over the C ABI. -pub struct SwiftGenerator; - -impl LanguageBackend for SwiftGenerator { - type Config = SwiftConfig; - - fn name(&self) -> &'static str { - "swift" - } - - fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { - TargetCapabilities::full() - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let module_name_owned = resolve_module_name(api, config); - let module_name = module_name_owned.as_str(); - let prefix = config.prefix(); - let input_basename = config.input_basename(); - let dir = out_dir.join("swift"); - let c_module = format!("C{module_name}"); - // The C shim is a SwiftPM `systemLibrary` target, so its module map - // must live under `Sources//` for `swift build` to find it. - let module_dir = dir.join("Sources").join(&c_module); - - let src_dir = dir.join("Sources").join(module_name); - let swift_filename = format!("{module_name}.swift"); - vec![ - OutputFile::new( - dir.join("Package.swift"), - render_package_swift(module_name, &c_module, input_basename), - ), - OutputFile::new( - module_dir.join("module.modulemap"), - render_modulemap(&c_module, prefix, input_basename), - ), - OutputFile::new( - src_dir.join(&swift_filename), - render_swift_wrapper( - api, - model, - prefix, - config.strip_module_prefix, - input_basename, - &swift_filename, - ), - ), - ] - } - - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let module_name_owned = resolve_module_name(api, config); - let module_name = module_name_owned.as_str(); - let prefix = config.prefix(); - let input_basename = config.input_basename(); - let dir = out_dir.join("swift"); - let c_module = format!("C{module_name}"); - let xcframework = format!("{c_module}.xcframework"); - - let src_dir = dir.join("Sources").join(module_name); - let swift_filename = format!("{module_name}.swift"); - let wrapper = render_swift_wrapper( - api, - model, - prefix, - config.strip_module_prefix, - input_basename, - &swift_filename, - ); - - let mut files = vec![ - PackagedFile::text( - dir.join("Package.swift"), - render_packaged_package_swift(module_name, &c_module, &xcframework, input_basename), - ), - PackagedFile::text(src_dir.join(&swift_filename), wrapper), - PackagedFile::text( - dir.join("README.md"), - render_packaged_readme(module_name, &c_module, prefix, ctx, input_basename), - ), - ]; - // Bundle the prebuilt libraries as xcframework-ready slices. Only - // desktop slices are meaningful to a SwiftPM consumer; mobile and - // WebAssembly builds belong to other package formats. - for nb in ctx - .binaries - .binaries - .iter() - .filter(|nb| nb.platform.is_desktop()) - { - let dest = dir - .join("lib") - .join(nb.platform.id()) - .join(ctx.binaries.bundled_filename(nb.platform)); - files.push(PackagedFile::copy(dest, nb.source.clone())); - } - Some(files) - } -} - -/// 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. -fn render_swift_wrapper( - api: &ResolvedApi, - model: &BindingModel, - c_prefix: &str, - strip_module_prefix: bool, - input_basename: &str, - filename: &str, -) -> String { - let mut out = String::with_capacity(estimate_swift_capacity(&api.modules)); - out.push_str(&render_prelude(CommentStyle::DoubleSlash, input_basename)); - // The C shim target is `C` and the wrapper file is always - // `.swift`, so the system-library module to import is the - // file stem with a `C` prefix. Deriving it here keeps the `import` in sync - // with the module name picked from `[swift] module_name` / the IDL package. - let module_name = filename.strip_suffix(".swift").unwrap_or(filename); - out.push_str(&format!("import C{module_name}\nimport Foundation\n\n")); - - // Index the flat, pre-order model by its underscore-joined symbol path so - // the recursive IR walk below can pull each module's precomputed C symbols - // while still emitting the nested Swift `enum` structure the IR tree drives. - let by_path: HashMap<&str, &ModuleBinding> = - model.modules.iter().map(|m| (m.path.as_str(), m)).collect(); - - let all_mods = walk_modules(&api.modules).collect::>(); - - // Every module becomes a namespace `enum`; a wrapper type whose name - // matches one of these is shadowed inside that namespace and must be - // module-qualified at its use sites. - let module_names: HashSet = all_mods - .iter() - .map(|m| m.name.to_upper_camel_case()) - .collect(); - // Raw-value types of every C-style enum, so buffer codecs can emit the - // right `i32` bit conversion for a referenced enum without re-resolving it. - let enum_raws: HashMap = model - .modules - .iter() - .flat_map(|m| m.enums.iter()) - .filter(|e| !e.is_rich()) - .map(|e| (e.name.clone(), enum_raw_type(e))) - .collect(); - let ctx = SwiftCtx { - c_prefix, - swift_module: module_name, - module_names: &module_names, - enum_raws: &enum_raws, - }; - - render_error_infra(&mut out); - render_buffer_runtime(&mut out); - - // Interface members can be async too, so consult every callable. - let has_async = model - .modules - .iter() - .any(|m| m.callables().any(|f| f.is_async)); - if has_async { - render_continuation_ref(&mut out); - } - - let has_callbacks = model - .modules - .iter() - .any(|m| !m.callback_interfaces.is_empty()); - if has_callbacks { - render_callback_support(&mut out); - } - - for m in &api.modules { - render_swift_module_types(&mut out, c_prefix, &by_path, m, &m.name, ctx); - let type_name = m.name.to_upper_camel_case(); - out.push_str(&format!("public enum {} {{\n", type_name)); - render_swift_module_body( - &mut out, - c_prefix, - &by_path, - m, - &m.name, - 1, - strip_module_prefix, - ctx, - ); - out.push_str("}\n\n"); - } - out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); - out -} - -#[cfg(test)] -mod tests { - use super::*; - use weaveffi_ir::ir::{ - Api, CallbackInterfaceDef, Function, InterfaceDef, Param, StructDef, StructField, TypeRef, - }; - - fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } - } - - fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } - } - - fn named(name: &str) -> TypeRef { - TypeRef::Named(name.into()) - } - - fn opt(ty: TypeRef) -> TypeRef { - TypeRef::Optional(Box::new(ty)) - } - - /// One module exercising every object and callback shape the ABI - /// contract distinguishes: an interface with a constructor and a method, - /// nullable objects in and out, objects inside a record (directly and in - /// a list), an iterator of objects, and a callback interface taking a - /// string, an `i32`, a record, and an object, with `bool` and void - /// returns. - fn fixture() -> ResolvedApi { - ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), - modules: vec![Module { - name: "shop".into(), - doc: None, - functions: vec![ - func( - "find_cart", - vec![param("current", opt(named("Cart")))], - Some(opt(named("Cart"))), - ), - func( - "all_carts", - vec![], - Some(TypeRef::Iterator(Box::new(named("Cart")))), - ), - func("watch", vec![param("watcher", named("Watcher"))], None), - ], - interfaces: vec![InterfaceDef { - name: "Cart".into(), - doc: None, - deprecated: None, - constructors: vec![func( - "new", - vec![param("owner", TypeRef::StringUtf8)], - None, - )], - methods: vec![func( - "add", - vec![param("sku", TypeRef::StringUtf8)], - Some(TypeRef::Bool), - )], - statics: vec![], - }], - callback_interfaces: vec![CallbackInterfaceDef { - name: "Watcher".into(), - doc: None, - deprecated: None, - methods: vec![ - func( - "on_event", - vec![ - param("name", TypeRef::StringUtf8), - param("count", TypeRef::I32), - param("order", named("Order")), - param("cart", named("Cart")), - ], - Some(TypeRef::Bool), - ), - func("on_done", vec![], None), - ], - }], - structs: vec![StructDef { - name: "Order".into(), - doc: None, - deprecated: None, - fields: vec![ - StructField { - name: "cart".into(), - ty: named("Cart"), - doc: None, - }, - StructField { - name: "history".into(), - ty: TypeRef::List(Box::new(named("Cart"))), - doc: None, - }, - ], - }], - enums: vec![], - errors: None, - modules: vec![], - }], - }) - } - - fn render() -> String { - let api = fixture(); - let model = BindingModel::build(&api, "weaveffi"); - render_swift_wrapper( - &api, - &model, - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ) - } - - #[track_caller] - fn assert_has(out: &str, needle: &str) { - assert!(out.contains(needle), "missing {needle:?} in:\n{out}"); - } - - #[test] - fn interface_class_owns_one_reference() { - let out = render(); - assert_has(&out, "public final class Cart {"); - assert_has(&out, "let ptr: OpaquePointer"); - assert_has(&out, "weaveffi_shop_Cart_destroy(ptr)"); - assert_has(&out, "func clonePtr() -> OpaquePointer {"); - assert_has(&out, "weaveffi_shop_Cart_clone(ptr)!"); - // Exactly one destroy site: the deinit. - assert_eq!(out.matches("weaveffi_shop_Cart_destroy(").count(), 1); - // Constructor adopts the returned reference; the method borrows `ptr`. - assert_has(&out, "public init(owner: String) {"); - assert_has(&out, "weaveffi_shop_Cart_new("); - assert_has(&out, "public func add(sku: String) -> Bool {"); - assert_has(&out, "weaveffi_shop_Cart_add(ptr, "); - } - - #[test] - fn nullable_objects_map_to_optional_wrappers() { - let out = render(); - assert_has( - &out, - "public static func findCart(current: Cart?) -> Cart? {", - ); - assert_has(&out, "weaveffi_shop_find_cart(current?.ptr, &err)"); - assert_has(&out, "return rv.map { Cart(ptr: $0) }"); - } - - #[test] - fn records_carry_object_tokens() { - let out = render(); - assert_has(&out, "public struct Order {"); - assert_has(&out, "public var cart: Cart"); - assert_has(&out, "public var history: [Cart]"); - assert_has(&out, "w.writeObject(value.cart.clonePtr())"); - assert_has(&out, "w.writeObject(v0.clonePtr())"); - assert_has(&out, "Cart(ptr: r.readObject())"); - assert_has(&out, "mutating func writeObject(_ p: OpaquePointer)"); - assert_has(&out, "mutating func readObject() -> OpaquePointer {"); - } - - #[test] - fn object_iterator_adopts_each_element() { - let out = render(); - assert_has( - &out, - "public final class ShopAllCartsIterator: Sequence, IteratorProtocol {", - ); - assert_has(&out, "public func next() -> Cart? {"); - assert_has(&out, "var item: OpaquePointer? = nil"); - assert_has( - &out, - "weaveffi_shop_AllCartsIterator_next(handle, &item, &err)", - ); - assert_has(&out, "weaveffi_shop_AllCartsIterator_destroy(handle)"); - assert_has(&out, "return Cart(ptr: item!)"); - } - - #[test] - fn callback_interface_renders_protocol_box_and_vtable() { - let out = render(); - assert_has(&out, "public protocol Watcher {"); - assert_has( - &out, - "func onEvent(name: String, count: Int32, order: Order, cart: Cart) throws -> Bool", - ); - assert_has(&out, "func onDone() throws\n"); - assert_has(&out, "final class WvWatcherBox {"); - assert_has(&out, "let impl: any Watcher"); - assert_has(&out, "enum WvWatcherVtable {"); - assert_has(&out, "static let value = weaveffi_shop_Watcher_vtable("); - assert_has( - &out, - "static let pointer: UnsafePointer = {", - ); - assert_has(&out, "cell.initialize(to: value)"); - assert_has( - &out, - "on_event: { ctx, name, count, order_ptr, order_len, cart, out_err in", - ); - assert_has(&out, "on_done: { ctx, out_err in"); - assert_has(&out, "free: { ctx in"); - assert_has(&out, "Unmanaged.fromOpaque(ctx!).release()"); - assert_has( - &out, - "Unmanaged.fromOpaque(ctx!).takeUnretainedValue()", - ); - // No stray empty doc line when the interface is undocumented. - assert!(!out.contains("\n///\n/// Implement this protocol"), "{out}"); - } - - #[test] - fn callback_trampolines_decode_adopt_and_report_errors() { - let out = render(); - // The record decodes through its codec before the call; the borrowed - // string is copied and the object argument adopted at the call site. - assert_has( - &out, - "[UInt8](UnsafeBufferPointer(start: order_ptr, count: order_len))", - ); - assert_has(&out, "let v2 = wvReadOrder(&r1)"); - assert_has( - &out, - "return try wvBox.impl.onEvent(name: String(cString: name!), count: count, order: v2, cart: Cart(ptr: cart!))", - ); - assert_has(&out, "try wvBox.impl.onDone()"); - // A thrown Swift error is reported as code -4 and a default returned. - assert_has(&out, "weaveffi_error_set(outErr, -4, $0)"); - assert_has(&out, "wvForeignError(out_err, error)"); - assert_has(&out, "return false"); - } - - #[test] - fn passing_a_callback_boxes_it_with_the_static_vtable() { - let out = render(); - assert_has(&out, "public static func watch(watcher: Watcher) -> Void {"); - assert_has( - &out, - "let watcher_ctx = Unmanaged.passRetained(WvWatcherBox(watcher)).toOpaque()", - ); - assert_has( - &out, - "weaveffi_shop_watch(watcher_ctx, WvWatcherVtable.pointer, &err)", - ); - } - - #[test] - fn no_arena_or_listener_surface_remains() { - let out = render(); - assert!(!out.contains("arena"), "{out}"); - assert!(!out.contains("Listener"), "{out}"); - assert!(!out.contains("TypedHandle"), "{out}"); - assert!(!out.contains("weaveffi_arena"), "{out}"); - } -} diff --git a/crates/weaveffi-gen-swift/src/package.rs b/crates/weaveffi-gen-swift/src/package.rs deleted file mode 100644 index 19a70957..00000000 --- a/crates/weaveffi-gen-swift/src/package.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! SwiftPM packaging surfaces: `Package.swift`, the system-library module -//! map, and the packaged-artifact README. -//! -//! `Package.swift` is Swift source, so every interpolated user string (the -//! package name and the target names derived from it) routes through -//! [`swift_str`] so quotes or backslashes in a configured module name can't -//! corrupt the manifest. - -use heck::ToUpperCamelCase; -use weaveffi_core::package::PackageContext; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::types::swift_str; -use crate::SwiftConfig; - -/// The SwiftPM package/module name: an explicit `[swift] module_name` wins; -/// 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() - .and_then(|p| p.name.as_deref()) - .map(ToUpperCamelCase::to_upper_camel_case) - }) - .unwrap_or_else(|| "WeaveFFI".to_string()) -} - -/// Render `Package.swift` for the source layout: a `systemLibrary` target -/// over the generated C header plus the Swift wrapper target. -pub(crate) fn render_package_swift( - module_name: &str, - c_module: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::DoubleSlash, input_basename); - // `swift-tools-version` MUST be the very first line of the manifest - // (Swift 6+ rejects it otherwise), so the WeaveFFI header prelude - // follows it rather than preceding it. - format!( - "// swift-tools-version:5.7\n\ -{prelude}import PackageDescription\n\n\ -let package = Package(\n \ - name: \"{name}\",\n \ - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],\n \ - products: [\n \ - .library(name: \"{name}\", targets: [\"{name}\"]),\n \ - ],\n \ - targets: [\n \ - .systemLibrary(name: \"{c_name}\"),\n \ - .target(name: \"{name}\", dependencies: [\"{c_name}\"]),\n \ - ]\n\ -)\n\n\ -{trailer}", - name = swift_str(module_name), - c_name = swift_str(c_module), - trailer = render_trailer(CommentStyle::DoubleSlash, "Package.swift"), - ) -} - -/// Render `Package.swift` for the packaged layout: the C ABI is consumed -/// through a prebuilt `binaryTarget` xcframework instead of a -/// `systemLibrary`, so installation needs no system lib on the search path. -pub(crate) fn render_packaged_package_swift( - module_name: &str, - c_module: &str, - xcframework: &str, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::DoubleSlash, input_basename); - format!( - "// swift-tools-version:5.7\n\ -{prelude}import PackageDescription\n\n\ -let package = Package(\n \ - name: \"{name}\",\n \ - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],\n \ - products: [\n \ - .library(name: \"{name}\", targets: [\"{name}\"]),\n \ - ],\n \ - targets: [\n \ - .binaryTarget(name: \"{c_name}\", path: \"{xcframework}\"),\n \ - .target(name: \"{name}\", dependencies: [\"{c_name}\"]),\n \ - ]\n\ -)\n\n\ -{trailer}", - name = swift_str(module_name), - c_name = swift_str(c_module), - xcframework = swift_str(xcframework), - trailer = render_trailer(CommentStyle::DoubleSlash, "Package.swift"), - ) -} - -/// Render the `systemLibrary` module map pointing at the generated C header. -/// -/// The module map lives at `swift/Sources/C/module.modulemap`, so -/// the C header generated at `/c/.h` is three levels up. -pub(crate) fn render_modulemap(c_module: &str, prefix: &str, input_basename: &str) -> String { - let prelude = render_prelude(CommentStyle::DoubleSlash, input_basename); - format!( - "{prelude}module {} [system] {{\n header \"../../../c/{prefix}.h\"\n link \"weaveffi\"\n export *\n}}\n\n{trailer}", - c_module, - trailer = render_trailer(CommentStyle::DoubleSlash, "module.modulemap"), - ) -} - -/// README for a packaged Swift artifact: it documents assembling the -/// `binaryTarget` xcframework from the bundled per-platform slices, the one -/// step that requires Apple tooling (`lipo` + `xcodebuild`). -pub(crate) fn render_packaged_readme( - module_name: &str, - c_module: &str, - prefix: &str, - ctx: &PackageContext, - input_basename: &str, -) -> String { - let prelude = render_prelude(CommentStyle::Xml, input_basename); - let trailer = render_trailer(CommentStyle::Xml, "README.md"); - // Match the slices `package()` actually bundles: desktop only. - let platforms: Vec = ctx - .binaries - .platforms() - .filter(|p| p.is_desktop()) - .map(|p| format!("- `lib/{}/`", p.id())) - .collect(); - let platform_list = platforms.join("\n"); - format!( - r#"{prelude}# {module_name} (Swift) - -A SwiftPM package whose C ABI is consumed through a prebuilt `binaryTarget` -xcframework named `{c_module}.xcframework`. - -The prebuilt libraries are bundled under `lib//`. Assembling them into -an xcframework is the one step that needs Apple tooling (run on macOS): - -```bash -# Fuse the macOS arm64 and x86_64 dylibs into one universal binary. -lipo -create \ - lib/darwin-arm64/lib{prefix}.dylib \ - lib/darwin-x64/lib{prefix}.dylib \ - -output lib{prefix}.dylib - -# Headers/ must contain {prefix}.h and a module map naming the module {c_module}. -mkdir -p Headers -cp ../c/include/{prefix}.h Headers/ -printf 'module {c_module} {{\n header "{prefix}.h"\n export *\n}}\n' > Headers/module.modulemap - -xcodebuild -create-xcframework \ - -library lib{prefix}.dylib -headers Headers \ - -output {c_module}.xcframework -``` - -Then `swift build` resolves the binary target with no further setup. - -## Bundled platforms - -{platform_list} - -{trailer}"#, - ) -} diff --git a/crates/weaveffi-gen-swift/src/runtime.rs b/crates/weaveffi-gen-swift/src/runtime.rs deleted file mode 100644 index 20465c5e..00000000 --- a/crates/weaveffi-gen-swift/src/runtime.rs +++ /dev/null @@ -1,241 +0,0 @@ -//! The private Swift runtime prelude: the value-buffer writer/reader pair, -//! the generic brand error with its `check`/`trap` helpers, the boxed -//! continuation async wrappers thread through the C context slot, and the -//! foreign-error helper callback-interface trampolines report through. - -use weaveffi_core::errors::ERROR_BRAND; - -/// The `weaveffi_error.code` a callback-interface trampoline reports when the -/// consumer's implementation throws. Mirrors `weaveffi_abi::FOREIGN_ERROR_CODE`. -const FOREIGN_ERROR_CODE: i32 = -4; - -/// The private Swift buffer runtime implementing the WeaveFFI value-buffer -/// wire format: little-endian, packed, no alignment. `WvWriter` serializes, -/// `WvReader` deserializes and traps (via `wvDecodeFailure`) on malformed -/// input, which the spec routes through the same channel as a producer panic. -/// -/// Object tokens (`writeObject`/`readObject`) are the `u64` widening of an -/// interface pointer; each carries one strong reference, so the writer is -/// handed a freshly cloned pointer and the reader hands its pointer to a -/// wrapper that adopts it. -const BUFFER_RUNTIME: &str = r#"/// 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) } - /// Writes an object token. `p` must be a strong reference the buffer now - /// owns (a freshly cloned pointer), never one a wrapper still holds. - mutating func writeObject(_ p: OpaquePointer) { writeUInt64(UInt64(UInt(bitPattern: p))) } -} - -/// 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") - } - } - /// Reads an object token: one strong reference the caller must adopt - /// into a wrapper whose deinit releases it. - mutating func readObject() -> OpaquePointer { - guard let p = OpaquePointer(bitPattern: UInt(readUInt64())) else { - wvDecodeFailure("null object token") - } - return p - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -"#; - -/// Append the private buffer runtime implementing the value-buffer wire -/// format. -pub(crate) fn render_buffer_runtime(out: &mut String) { - out.push_str(BUFFER_RUNTIME); -} - -/// Append the generic brand error enum plus the `check`/`trap` helpers every -/// wrapper body reports its error slot through. -/// -/// The brand error covers unknown codes, marshalling failures, panics, and -/// foreign callback failures; typed domain errors get one enum per declaring -/// module, emitted alongside that module's types. Domain error codes are -/// validated positive, so the reserved negative runtime codes (generic `-1`, -/// panic `-2`, marshalling `-3`, foreign callback failure `-4`) always reach -/// the brand error on throwing paths and the `fatalError` trap on -/// non-throwing ones. -pub(crate) fn render_error_infra(out: &mut String) { - out.push_str(&format!( - "public enum {ERROR_BRAND}: Error, LocalizedError {{\n" - )); - out.push_str(" case error(code: Int32, message: String)\n"); - out.push_str(" public var errorDescription: String? {\n"); - out.push_str(" switch self {\n"); - out.push_str(" case let .error(_, message): return message\n"); - out.push_str(" }\n"); - out.push_str(" }\n"); - out.push_str(" public var errorCode: Int32 {\n"); - out.push_str(" switch self {\n"); - out.push_str(" case let .error(code, _): return code\n"); - out.push_str(" }\n"); - out.push_str(" }\n"); - out.push_str("}\n\n"); - - out.push_str("@inline(__always)\nfunc check(_ err: inout weaveffi_error) throws {\n"); - out.push_str(" if err.code != 0 {\n"); - out.push_str(" let code = err.code\n"); - out.push_str(" let message = err.message.flatMap { String(cString: $0) } ?? \"\"\n"); - out.push_str(" weaveffi_error_clear(&err)\n"); - out.push_str(&format!( - " throw {ERROR_BRAND}.error(code: code, message: message)\n" - )); - out.push_str(" }\n"); - out.push_str("}\n\n"); - - // The trapping flavor for non-throwing wrappers: a non-zero code here can - // only be a producer panic, an argument-marshalling failure, or a - // consumer callback implementation that threw. - out.push_str("@inline(__always)\nfunc trap(_ err: inout weaveffi_error) {\n"); - out.push_str(" if err.code != 0 {\n"); - out.push_str(" let code = err.code\n"); - out.push_str(" let message = err.message.flatMap { String(cString: $0) } ?? \"\"\n"); - out.push_str(" weaveffi_error_clear(&err)\n"); - out.push_str(" fatalError(\"\\(code): \\(message)\")\n"); - out.push_str(" }\n"); - out.push_str("}\n\n"); -} - -/// Append the boxed-continuation class async wrappers thread through the C -/// `context` slot. -pub(crate) fn render_continuation_ref(out: &mut String) { - // `E` is `Error` for a throwing async wrapper and `Never` for a plain - // one, mirroring the checked-continuation flavor each uses. - out.push_str("private final class ContinuationRef {\n"); - out.push_str(" let value: CheckedContinuation\n"); - out.push_str(" init(_ value: CheckedContinuation) { self.value = value }\n"); - out.push_str("}\n\n"); -} - -/// Append the callback-interface support: the helper every trampoline calls -/// when the consumer's implementation throws. It reports the failure through -/// the producer-owned `out_err` slot with `weaveffi_error_set`, which copies -/// the borrowed message, so no Swift allocation crosses the boundary and no -/// error ever unwinds through the C frame. -pub(crate) fn render_callback_support(out: &mut String) { - out.push_str( - "/// Reports a thrown Swift error to the producer as a foreign callback failure\n\ - /// (code -4); the producer aborts its current call with that code and message.\n\ - func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) {\n\ - \x20 let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error)\n", - ); - out.push_str(&format!( - " message.withCString {{ weaveffi_error_set(outErr, {FOREIGN_ERROR_CODE}, $0) }}\n" - )); - out.push_str("}\n\n"); -} diff --git a/crates/weaveffi-gen-swift/src/types.rs b/crates/weaveffi-gen-swift/src/types.rs deleted file mode 100644 index a9d6b934..00000000 --- a/crates/weaveffi-gen-swift/src/types.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! Swift type spellings and identifier policy: how IR types and user-chosen -//! names render in Swift source. - -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; - -/// The Swift spelling of a user-chosen identifier in a lowerCamel position -/// (parameters, fields, enum cases, wrapper names): camel-cased, then -/// keyword-escaped through the shared rule, so a parameter named `in` -/// becomes `in_` rather than emitting broken Swift. -pub(crate) fn swift_ident(name: &str) -> String { - lang::escape_ident(&name.to_lower_camel_case(), lang::SWIFT_KEYWORDS) -} - -/// Escape a string for embedding inside a Swift double-quoted literal. -pub(crate) fn swift_str(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") -} - -/// The Swift surface type of an IR type reference. -/// -/// # Panics -/// -/// 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: &Ty) -> String { - match t { - 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 => "String".to_string(), - Ty::Bytes => "Data".to_string(), - // An interface surfaces as its wrapper class and a callback interface - // as the protocol the consumer implements; both are file-scope types - // named after the IDL type. - Ty::Enum(name) - | Ty::Record(name) - | Ty::RichEnum(name) - | Ty::Interface(name) - | Ty::CallbackInterface(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. - Ty::Iterator(_) => unreachable!("iterator type is only valid as a function return"), - } -} - -/// Context threaded into the function/return renderers so they can emit the -/// fully-prefixed C symbols (for iterators), disambiguate wrapper types that -/// collide with a module namespace, and look up the raw type of a C-style -/// enum inside buffer codecs. -#[derive(Clone, Copy)] -pub(crate) struct SwiftCtx<'a> { - /// C ABI symbol prefix (e.g. `weaveffi`). - pub(crate) c_prefix: &'a str, - /// SwiftPM module name (e.g. `Kvstore`). - pub(crate) swift_module: &'a str, - /// Every module name in the API, PascalCased, i.e. the set of namespace - /// `enum` names that wrapper-type references can be shadowed by. - pub(crate) module_names: &'a HashSet, - /// Raw-value Swift type (`"Int32"` or `"UInt32"`) of every C-style enum - /// in the API, keyed by its bare type name. - pub(crate) enum_raws: &'a HashMap, -} - -impl SwiftCtx<'_> { - /// Qualify a top-level wrapper type name with the Swift module when its - /// name collides with a namespace `enum`. Inside `enum Kv { enum Stats { … } }` - /// the bare name `Stats` resolves to the namespace, not the top-level - /// type; `Kvstore.Stats` forces the type. Module-qualifying is valid from - /// any scope, so we apply it whenever the name collides. - pub(crate) fn ty_name(&self, local: &str) -> String { - if self.module_names.contains(local) { - format!("{}.{}", self.swift_module, local) - } else { - local.to_string() - } - } - - /// The raw-value Swift type of the C-style enum named `local`. - pub(crate) fn enum_raw(&self, local: &str) -> &'static str { - self.enum_raws.get(local).copied().unwrap_or("UInt32") - } -} - -/// 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: &Ty, ctx: SwiftCtx) -> String { - match t { - Ty::Record(name) - | Ty::RichEnum(name) - | Ty::Enum(name) - | Ty::Interface(name) - | Ty::CallbackInterface(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), - } -} - -/// The raw-value Swift type Swift imports the generated C enum with: a C enum -/// with only non-negative discriminants imports as `UInt32`, otherwise -/// `Int32`. Mirroring the raw type keeps every `.rawValue` round-trip against -/// the C symbols type-correct. -pub(crate) fn enum_raw_type(e: &EnumBinding) -> &'static str { - if e.variants.iter().any(|v| v.value < 0) { - "Int32" - } else { - "UInt32" - } -} - -/// The fully-prefixed C type name of a C-style enum referenced (possibly -/// cross-module) from `module_name`. -pub(crate) fn c_enum_type(name: &str, c_prefix: &str, module_name: &str) -> String { - let (module, local) = split_qualified(name, module_name); - format!("{c_prefix}_{module}_{local}") -} - -/// The Swift name of the lazy sequence class emitted for one `iter` -/// function: the iterator tag minus the C prefix, PascalCased -/// (`weaveffi_kv_ScanIterator` becomes `KvScanIterator`). -pub(crate) fn iterator_class_name(it: &IteratorBinding, c_prefix: &str) -> String { - it.iter_tag - .strip_prefix(&format!("{c_prefix}_")) - .unwrap_or(&it.iter_tag) - .to_upper_camel_case() -} - -/// The internal box class retaining one implementation of the callback -/// interface named `name` (possibly dot-qualified) across the C boundary: -/// `Wv{Name}Box`. -pub(crate) fn callback_box_name(name: &str) -> String { - format!("Wv{}Box", local_type_name(name)) -} - -/// The internal namespace holding the process-wide vtable for the callback -/// interface named `name` (possibly dot-qualified): `Wv{Name}Vtable`. -pub(crate) fn callback_vtable_name(name: &str) -> String { - format!("Wv{}Vtable", local_type_name(name)) -} - -/// 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: &Ty) -> String { - if matches!(ty, Ty::Bool) { - "false".to_string() - } else { - "0".to_string() - } -} diff --git a/crates/weaveffi-gen-wasm/Cargo.toml b/crates/weaveffi-gen-wasm/Cargo.toml deleted file mode 100644 index 08f45ee6..00000000 --- a/crates/weaveffi-gen-wasm/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "weaveffi-gen-wasm" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "WebAssembly binding generator for WeaveFFI" -keywords = ["ffi", "codegen", "bindings", "wasm", "webassembly"] -categories = ["development-tools::ffi"] - -[dependencies] -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } -camino = { workspace = true } -heck = { workspace = true } -serde = { workspace = true } - -[lints] -workspace = true diff --git a/crates/weaveffi-gen-wasm/src/calls.rs b/crates/weaveffi-gen-wasm/src/calls.rs deleted file mode 100644 index bd2f5e5a..00000000 --- a/crates/weaveffi-gen-wasm/src/calls.rs +++ /dev/null @@ -1,1013 +0,0 @@ -//! Callable wrappers: argument staging, return decoding, the sync, iterator, -//! and async surfaces, and the callback-interface vtables. -//! -//! Each parameter's passing contract comes from [`ParamBinding::arg_pass`], -//! each result's receiving contract from [`plan::ret_pass`], each iterator's -//! pull contract from [`IteratorBinding::protocol`], each async completion -//! from [`AsyncBinding::protocol`], and each callback interface's vtable -//! shape from [`CallbackInterfaceBinding::protocol`], so this module only -//! spells those shared contracts in JavaScript; it never re-derives them from -//! `Ty` shapes. - -use weaveffi_core::abi::CType; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{ - AsyncBinding, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, - IteratorBinding, ModuleBinding, ParamBinding, -}; -use weaveffi_core::plan::{self, ArgPass, Free, RetPass}; -use weaveffi_core::utils::local_type_name; - -use crate::codec::{buf_read_expr, emit_buf_write_stmts}; -use crate::types::{ - emscripten_stub, js_cb_method_name, js_checker_name, js_err_factory, js_fn_name, js_param_name, - js_vtable_name, JsDecl, -}; - -/// 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: &Ty) -> u32 { - match plan::elem_free(ty) { - Free::Bytes => 8, - Free::String => 4, - Free::None => match ty { - Ty::Bool | Ty::I8 | Ty::U8 => 1, - Ty::I16 | Ty::U16 => 2, - Ty::I64 | Ty::U64 | Ty::F64 => 8, - _ => 4, - }, - } -} - -/// A JS expression reading one by-value scalar of `ty` from `DataView` `dv` -/// at byte offset `at`. -fn read_scalar_at(ty: &Ty, dv: &str, at: &str) -> String { - match ty { - 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 => format!("{dv}.getBigUint64({at}, true)"), - Ty::F32 => format!("{dv}.getFloat32({at}, true)"), - Ty::F64 => format!("{dv}.getFloat64({at}, true)"), - // Opaque pointers (interfaces) are i32 slots. - _ => format!("{dv}.getUint32({at}, true)"), - } -} - -/// The idiomatic JS value of a by-value scalar `r` that arrived through a -/// wasm `i32` or `i64` slot (a return, an async completion, or a callback -/// parameter). Wasm integers are signed on the JS side, so unsigned widths -/// are reinterpreted (`u32` via `>>> 0`, `u64` via `BigInt.asUintN`) and -/// bool is compared against zero; every other scalar is already idiomatic. -fn js_direct_value(ty: &Ty, r: &str) -> String { - match ty { - Ty::Bool => format!("{r} !== 0"), - Ty::U32 => format!("{r} >>> 0"), - Ty::U64 => format!("BigInt.asUintN(64, {r})"), - _ => r.to_string(), - } -} - -/// A direct JS call argument for a scalar value (coercing bool to 0/1 and -/// 64-bit values to `BigInt` as the wasm calling convention requires). -fn js_arg_scalar(ty: &Ty, val: &str) -> String { - match ty { - Ty::Bool => format!("{val} ? 1 : 0"), - Ty::I64 | Ty::U64 => format!("BigInt({val})"), - _ => val.to_string(), - } -} - -/// The interface name inside an object-family type (`Interface` or -/// `Interface?`). -/// -/// # Panics -/// -/// Panics when `ty` is not an interface or optional interface, which cannot -/// happen for a slot classified as [`ArgPass::Object`] or [`RetPass::Object`]. -fn object_name(ty: &Ty) -> &str { - ty.interface_name() - .expect("only interface types classify as objects") -} - -/// Stage one idiomatic input parameter into the Wasm ABI, dispatching on the -/// shared [`ArgPass`] contract. -/// -/// Pushes any pre-call statements to `out` (at `indent`), the produced call -/// arguments to `args`, and any post-call cleanup statements to `cleanup`. -/// `tmp` is a collision-free local-name base; `module` resolves record and -/// rich-enum codec references. Buffered values (records, rich enums, -/// optionals, lists, maps) are encoded into a value buffer and staged like -/// bytes: allocate, copy, pass `(ptr, len)`, dealloc after the call. Object -/// arguments lend the wrapper's pointer for the call; callback interfaces -/// register the implementation in the loader's handle map and pass its key -/// plus the interface's static vtable. Assumes `wasm` is in scope. -pub(crate) fn emit_stage_input( - out: &mut String, - indent: &str, - p: &ParamBinding, - tmp: &str, - module: &str, - args: &mut Vec, - cleanup: &mut Vec, -) { - let value = js_param_name(p); - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - match p.arg_pass() { - ArgPass::Buffer { .. } => { - w.line(format!("const {tmp}_w = new _BufWriter();")); - let mut n = 0u32; - emit_buf_write_stmts(&mut w, &p.ty, &format!("{tmp}_w"), &value, module, &mut n); - w.line(format!( - "const [{tmp}_p, {tmp}_l] = _bytes(wasm, {tmp}_w.finish());" - )); - args.push(format!("{tmp}_p")); - args.push(format!("{tmp}_l")); - cleanup.push(format!("wasm.weaveffi_dealloc({tmp}_p, {tmp}_l);")); - } - ArgPass::String { .. } => { - w.line(format!("const [{tmp}_p, {tmp}_s] = _cstr(wasm, {value});")); - args.push(format!("{tmp}_p")); - cleanup.push(format!("wasm.weaveffi_dealloc({tmp}_p, {tmp}_s);")); - } - ArgPass::Bytes { .. } => { - w.line(format!("const [{tmp}_p, {tmp}_l] = _bytes(wasm, {value});")); - args.push(format!("{tmp}_p")); - args.push(format!("{tmp}_l")); - cleanup.push(format!("wasm.weaveffi_dealloc({tmp}_p, {tmp}_l);")); - } - // A borrowed object pointer: the wrapper keeps its own reference and - // the producer clones if it retains the object. Null means none for - // the nullable `Interface?` spelling. - ArgPass::Object { nullable: true, .. } => { - args.push(format!( - "({value} === null || {value} === undefined ? 0 : _borrow({value}))" - )); - } - ArgPass::Object { .. } => { - args.push(format!("_borrow({value})")); - } - // The implementation object goes into the handle map under a fresh - // integer key, which crosses as `ctx`; the vtable's `free` entry - // removes it when the producer drops its last reference. - ArgPass::Callback { .. } => { - w.line(format!("const {tmp}_ctx = _nextCbId++;")); - w.line(format!("_callbacks.set({tmp}_ctx, {value});")); - args.push(format!("{tmp}_ctx")); - args.push(js_vtable_name( - p.ty.callback_interface_name() - .expect("callback family names a callback interface"), - )); - } - // By-value slot: scalars coerce per the wasm calling convention. - ArgPass::Direct { .. } => { - args.push(js_arg_scalar(&p.ty, &value)); - } - } - out.push_str(&w.finish()); -} - -/// Emit the guarded producer call every synchronous entry point shares: -/// allocate the error slot (and, when `needs_len`, the `_lp` out-length slot -/// a bytes or buffered return writes through), invoke `symbol` with -/// `in_args` plus those slots, and translate anything thrown while the -/// producer was on the stack through `_trap`, which also releases the slot -/// the failed call never filled. `cleanup` (the staged-input releases) runs -/// in a `finally` so a trap leaks nothing in linear memory. The raw result -/// is bound to `bind` (declared with `let` above the `try`) when given. -/// Leaves the error slot live for the caller's checker. -fn emit_guarded_call( - w: &mut CodeWriter, - symbol: &str, - in_args: &[String], - cleanup: &[String], - needs_len: bool, - bind: Option<&str>, -) { - let mut call_args = in_args.to_vec(); - if needs_len { - w.line("const _lp = wasm.weaveffi_alloc(4);"); - call_args.push("_lp".to_string()); - } - w.line("const _err = _allocErr(wasm);"); - call_args.push("_err".to_string()); - if let Some(b) = bind { - w.line(format!("let {b};")); - } - let call = format!("wasm.{symbol}({})", call_args.join(", ")); - w.block("try {", "} catch (e) {", |w| { - match bind { - Some(b) => w.line(format!("{b} = {call};")), - None => w.line(format!("{call};")), - }; - }); - w.scope(|w| { - if needs_len { - w.line("wasm.weaveffi_dealloc(_lp, 4);"); - } - w.line("throw _trap(wasm, _err, e);"); - }); - if cleanup.is_empty() { - w.line("}"); - } else { - w.line("} finally {"); - w.scope(|w| { - for stmt in cleanup { - w.line(stmt); - } - }); - w.line("}"); - } -} - -/// Emit the body that invokes `symbol` with the already-staged `in_args`, -/// runs `cleanup`, routes the error slot through the `checker` helper, and -/// decodes/returns the idiomatic value for `ret`. A buffered or bytes return -/// allocates the trailing `out_len` slot before the call. Assumes `wasm` is -/// in scope at `indent`. -#[allow(clippy::too_many_arguments)] -pub(crate) fn emit_return_decode( - out: &mut String, - indent: &str, - ret: Option<&Ty>, - symbol: &str, - in_args: &[String], - cleanup: &[String], - checker: &str, - module: &str, - prefix: &str, -) { - let needs_len = matches!( - plan::ret_pass(ret, module, prefix), - RetPass::Buffer | RetPass::Bytes - ); - - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - emit_guarded_call( - &mut w, - symbol, - in_args, - cleanup, - needs_len, - ret.map(|_| "_r"), - ); - w.line(format!("{checker}(wasm, _err);")); - w.line("_freeErr(wasm, _err);"); - out.push_str(&w.finish()); - - emit_decode_value(out, indent, ret, "_r", module, prefix); -} - -/// Emit the guarded call of an interface's canonical constructor at -/// `indent`: the staged inputs, the producer call with trap translation, the -/// `checker`, and the adoption of the returned reference into `this`. -pub(crate) fn emit_constructor_call( - out: &mut String, - indent: &str, - symbol: &str, - in_args: &[String], - cleanup: &[String], - checker: &str, - destroy: &str, -) { - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - emit_guarded_call(&mut w, symbol, in_args, cleanup, false, Some("_r")); - w.line(format!("{checker}(wasm, _err);")); - w.line("_freeErr(wasm, _err);"); - w.line(format!("_adopt(this, _r, {destroy});")); - out.push_str(&w.finish()); -} - -/// Emit the `return ...;` (if any) that converts the raw result `r` (plus the -/// `_lp` out-slot already in scope for a bytes or buffered return) into the -/// idiomatic value, dispatching on the shared [`RetPass`] contract. A -/// buffered return is copied out of linear memory, released with -/// `weaveffi_free_bytes`, and decoded through the buffer reader, which -/// rejects malformed encodings. An object return transfers one strong -/// reference, adopted by the wrapper class's `_wrap`. -fn emit_decode_value( - out: &mut String, - indent: &str, - ret: Option<&Ty>, - r: &str, - module: &str, - prefix: &str, -) { - let Some(ret) = ret else { - return; - }; - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - match plan::ret_pass(Some(ret), module, prefix) { - RetPass::Void => unreachable!("a present return type is never void"), - RetPass::Buffer => { - w.line("const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true);"); - w.line("wasm.weaveffi_dealloc(_lp, 4);"); - w.line(format!( - "const _rd = new _BufReader(_takeBytes(wasm, {r}, _len));" - )); - w.line(format!( - "const _out = {};", - buf_read_expr(ret, module, "_rd") - )); - w.line("_rd.end();"); - w.line("return _out;"); - } - RetPass::Direct => { - w.line(format!("return {};", js_direct_value(ret, r))); - } - RetPass::String => { - w.line(format!("return _takeCStr(wasm, {r});")); - } - RetPass::Bytes => { - w.line("const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true);"); - w.line("wasm.weaveffi_dealloc(_lp, 4);"); - w.line(format!("return _takeBytes(wasm, {r}, _len);")); - } - RetPass::Object { nullable, .. } => { - let cls = local_type_name(object_name(ret)); - if nullable { - w.line(format!("return {r} === 0 ? null : {cls}._wrap({r});")); - } else { - w.line(format!("return {cls}._wrap({r});")); - } - } - } - out.push_str(&w.finish()); -} - -/// Emit one callable in the shape its [`CallShape`] and the mode call for: -/// iterator members return a lazy JS iterator, async members return a -/// `Promise`, and everything else is a plain synchronous wrapper. In -/// Emscripten mode an async callable or one taking a callback interface is an -/// explicit throwing stub instead. `self_arg` threads the instance handle -/// for interface methods; `mb` supplies the module's error domain for the -/// throws split and the module path for codec references. -#[allow(clippy::too_many_arguments)] -pub(crate) fn emit_js_callable( - out: &mut String, - mb: &ModuleBinding, - f: &FnBinding, - decl: JsDecl, - self_arg: Option<&str>, - indent: &str, - prefix: &str, - emscripten: bool, -) { - match &f.shape { - _ if emscripten && emscripten_stub(f) => emit_js_emscripten_stub(out, f, decl, indent), - CallShape::Iterator(ib) => { - emit_js_iterator_function_wrapper(out, mb, f, ib, decl, self_arg, indent, prefix); - } - CallShape::Async(ab) => { - emit_js_async_function_wrapper(out, mb, f, ab, decl, self_arg, indent, prefix); - } - CallShape::Sync(_) => emit_js_function_wrapper(out, mb, f, decl, self_arg, indent, prefix), - } -} - -/// Async functions and callback interfaces are unsupported in Emscripten -/// mode: both rely on `WebAssembly.Function` and a growable -/// `__indirect_function_table` to install trampolines, neither of which an -/// Emscripten module exposes portably. Each affected entry point becomes an -/// explicit stub that throws at call time, so the gap is impossible to miss -/// from JS even though the `.d.ts` deliberately omits it (a compile-time -/// error for TS users). -fn emit_js_emscripten_stub(out: &mut String, f: &FnBinding, decl: JsDecl, indent: &str) { - let js_params: Vec = f.params.iter().map(js_param_name).collect(); - let name = js_fn_name(f); - let what = if f.is_async { - format!("async function '{name}'") - } else { - format!("function '{name}' (it takes a callback interface)") - }; - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - w.block( - format!("{}{name}({}) {{", decl.prefix(), js_params.join(", ")), - decl.close(), - |w| { - w.line(format!( - "throw new Error(\"weaveffi: {what} is not supported in Emscripten mode; \ - use the wasm32-unknown-unknown loader or a native target\");" - )); - }, - ); - out.push_str(&w.finish()); -} - -/// The wasm value type of one C ABI slot: pointers and 32-bit-or-smaller -/// scalars are `i32` on wasm32, 64-bit integers widen to `i64`, and floats -/// keep their width. -fn slot_wasm_type(ty: &CType) -> &'static str { - match ty { - CType::Int64 | CType::Uint64 => "i64", - CType::Float => "f32", - CType::Double => "f64", - _ => "i32", - } -} - -/// The quoted wasm value-type list for a slot sequence, as the -/// `WebAssembly.Function` signature spells it. -fn wasm_type_list<'a>(slots: impl Iterator) -> String { - slots - .map(|t| format!("'{}'", slot_wasm_type(t))) - .collect::>() - .join(", ") -} - -/// Emit the statements decoding one callback-method argument from its raw -/// wasm slot values into the idiomatic JS value (bound to `target`) the -/// implementation sees, dispatching on the shared [`RetPass`] contract the -/// callback protocol assigns to each parameter. -/// -/// Strings, bytes, and buffered values are borrowed for the duration of the -/// dispatch: they're copied or decoded out of linear memory and never freed -/// here. Object arguments transfer one strong reference, adopted by the -/// wrapper class's `_wrap` (null means none for `Interface?`). Assumes `wasm` -/// in scope. -fn emit_cb_param_decode( - w: &mut CodeWriter, - p: &ParamBinding, - pass: &RetPass, - slots: &[String], - target: &str, - module: &str, -) { - let a = &slots[0]; - match pass { - RetPass::Void => unreachable!("a parameter always has a type"), - RetPass::Buffer => { - let b = &slots[1]; - w.line(format!( - "const {target}_b = ({a} === 0 || {b} === 0) ? new Uint8Array(0) : new Uint8Array(wasm.memory.buffer, {a}, {b}).slice();" - )); - w.line(format!("const {target}_r = new _BufReader({target}_b);")); - w.line(format!( - "const {target} = {};", - buf_read_expr(&p.ty, module, &format!("{target}_r")) - )); - w.line(format!("{target}_r.end();")); - } - RetPass::Direct => { - w.line(format!("const {target} = {};", js_direct_value(&p.ty, a))); - } - RetPass::String => { - w.line(format!("const {target} = _readCStr(wasm, {a});")); - } - RetPass::Bytes => { - let b = &slots[1]; - w.line(format!( - "const {target} = ({a} === 0 || {b} === 0) ? new Uint8Array(0) : new Uint8Array(wasm.memory.buffer, {a}, {b}).slice();" - )); - } - RetPass::Object { nullable, .. } => { - let cls = local_type_name(object_name(&p.ty)); - if *nullable { - w.line(format!( - "const {target} = {a} === 0 ? null : {cls}._wrap({a});" - )); - } else { - w.line(format!("const {target} = {cls}._wrap({a});")); - } - } - } -} - -/// The `return` statement handing a callback method's JS result back through -/// the C return slot, and the default returned after a foreign failure. -/// Returns are restricted to the direct family (or void) by validation, so -/// only bool (0/1) and 64-bit integers (`BigInt`) need a coercion. -fn cb_return(ret: Option<&Ty>, call: &str) -> (String, Option<&'static str>) { - match ret { - None => (format!("{call};"), None), - Some(Ty::Bool) => (format!("return {call} ? 1 : 0;"), Some("return 0;")), - Some(Ty::I64 | Ty::U64) => (format!("return BigInt({call});"), Some("return 0n;")), - Some(_) => (format!("return {call};"), Some("return 0;")), - } -} - -/// The JS-side name of the trampoline registered for one vtable entry. -fn js_cb_tramp_name(cb: &CallbackInterfaceBinding, method: &str) -> String { - format!("_cb_{}_{method}", cb.name) -} - -/// Emit one callback method's trampoline at `indent` (loader scope): a -/// `WebAssembly.Function` whose signature mirrors the vtable entry's C slots -/// ([`CallbackMethodBinding::abi_params`] and `abi_ret`). It looks up the -/// implementation by `ctx`, decodes each argument per the borrowing contract, -/// calls the JS method synchronously, and coerces the result into the C -/// return. Any exception is caught, reported through `out_err` as -/// `FOREIGN_ERROR_CODE`, and replaced by a default value, so nothing unwinds -/// through the wasm frame. -fn emit_js_callback_trampoline( - w: &mut CodeWriter, - cb: &CallbackInterfaceBinding, - m: &CallbackMethodBinding, - args: &[RetPass], - module: &str, -) { - let tramp = js_cb_tramp_name(cb, &m.name); - let param_types = wasm_type_list(m.abi_params.iter().map(|p| &p.ty)); - let result_types = match &m.abi_ret { - CType::Void => String::new(), - ret => format!("'{}'", slot_wasm_type(ret)), - }; - // Positional slot names: `_ctx`, one per parameter slot, then `_err`. - let mut slot_names = vec!["_ctx".to_string()]; - slot_names.extend((0..m.abi_params.len() - 2).map(|i| format!("a{i}"))); - slot_names.push("_err".to_string()); - - w.block( - format!( - "const {tramp} = _registerTrampoline(_table, [{param_types}], [{result_types}], ({}) => {{", - slot_names.join(", ") - ), - "});", - |w| { - w.block("try {", "} catch (e) {", |w| { - w.line("const _impl = _callbacks.get(_ctx);"); - let mut slot_idx = 1usize; - let mut call_args: Vec = Vec::new(); - for (i, (p, pass)) in m.params.iter().zip(args).enumerate() { - let n = p.abi.len(); - let slots = &slot_names[slot_idx..slot_idx + n]; - slot_idx += n; - let target = format!("_p{i}"); - emit_cb_param_decode(w, p, pass, slots, &target, module); - call_args.push(target); - } - // A callback that already failed during this producer call - // is not consulted again: the producer can't unwind on - // wasm32, so it keeps running until its thunk reports the - // failure, and the implementation must not observe those - // extra invocations. Arguments were still decoded above so - // that any object reference the producer transferred is - // adopted by a wrapper (and reclaimed by the finalizer - // backstop) rather than leaked. - w.block("if (_pendingForeign !== null) {", "}", |w| { - w.line("_reportForeign(wasm, _err, _pendingForeign);"); - if let Some(default) = cb_return(m.ret.as_ref(), "").1 { - w.line(default); - } else { - w.line("return;"); - } - }); - let call = format!( - "_impl.{}({})", - js_cb_method_name(&m.name), - call_args.join(", ") - ); - w.line(cb_return(m.ret.as_ref(), &call).0); - }); - w.scope(|w| { - w.line("_setForeignError(wasm, _err, e);"); - if let Some(default) = cb_return(m.ret.as_ref(), "").1 { - w.line(default); - } - }); - w.line("}"); - }, - ); -} - -/// Emit the static vtable for one callback interface at `indent` (loader -/// scope): one trampoline per method in declaration order plus the trailing -/// `free`, each installed once in the wasm function table for the life of -/// the instance, then the vtable struct itself allocated in linear memory -/// with the module's allocator and filled with the table indices (wasm32 -/// function pointers are 4-byte table indices, so the struct is packed -/// `4 * (methods + 1)` bytes). The struct's address is what -/// [`emit_stage_input`] passes as `{name}_vtable`. -pub(crate) fn emit_js_callback_vtable( - out: &mut String, - module: &ModuleBinding, - cb: &CallbackInterfaceBinding, - prefix: &str, - indent: &str, -) { - let proto = cb.protocol(&module.path, prefix); - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - w.line(format!( - "// Trampolines for the `{}` callback interface: one WebAssembly.Function", - cb.name - )); - w.line("// per vtable entry, mirroring the C signature of each method. Borrowed"); - w.line("// arguments are decoded before the JS method runs; object arguments"); - w.line("// are adopted; a thrown exception becomes a -4 error on out_err."); - for (m, args) in cb.methods.iter().zip(&proto.method_args) { - emit_js_callback_trampoline(&mut w, cb, m, args, &module.path); - } - let free = js_cb_tramp_name(cb, "free"); - w.line("// `free(ctx)`: the producer dropped its last reference; forget the"); - w.line("// implementation so it can be collected."); - w.line(format!( - "const {free} = _registerTrampoline(_table, ['i32'], [], (_ctx) => {{ _callbacks.delete(_ctx); }});" - )); - let vtable = js_vtable_name(&cb.name); - let size = 4 * (cb.methods.len() + 1); - w.line(format!( - "// The one static `{}` for this instance, filled with the table indices", - cb.vtable_tag - )); - w.line("// above in declaration order, then `free`."); - w.line(format!("const {vtable} = wasm.weaveffi_alloc({size});")); - w.block("{", "}", |w| { - w.line("const _dv = new DataView(wasm.memory.buffer);"); - for (i, m) in cb.methods.iter().enumerate() { - w.line(format!( - "_dv.setUint32({vtable} + {}, {}, true);", - 4 * i, - js_cb_tramp_name(cb, &m.name) - )); - } - w.line(format!( - "_dv.setUint32({vtable} + {}, {free}, true);", - 4 * cb.methods.len() - )); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit a synchronous function as a method `name(params) { ... }` at `indent`, -/// staging idiomatic inputs, calling the C symbol, and decoding the return. -/// `self_arg` (an expression such as `this._handle`) becomes the implicit -/// leading argument for interface methods; the checker selected by -/// [`js_checker_name`] enforces the throws split on the out-err slot. -fn emit_js_function_wrapper( - out: &mut String, - mb: &ModuleBinding, - f: &FnBinding, - decl: JsDecl, - self_arg: Option<&str>, - indent: &str, - prefix: &str, -) { - let body = format!("{indent} "); - let js_params: Vec = f.params.iter().map(js_param_name).collect(); - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - - if let Some(msg) = &f.deprecated { - w.line(format!("/** @deprecated {msg} */")); - } - w.line(format!( - "{}{}({}) {{", - decl.prefix(), - js_fn_name(f), - js_params.join(", ") - )); - - let mut inner = String::new(); - let mut args: Vec = self_arg.iter().map(ToString::to_string).collect(); - let mut cleanup = Vec::new(); - for (i, p) in f.params.iter().enumerate() { - emit_stage_input( - &mut inner, - &body, - p, - &format!("a{i}"), - &mb.path, - &mut args, - &mut cleanup, - ); - } - emit_return_decode( - &mut inner, - &body, - f.ret.as_ref(), - &f.c_base, - &args, - &cleanup, - &js_checker_name(f, mb.error.as_ref()), - &mb.path, - prefix, - ); - w.raw(inner); - w.line(decl.close()); - out.push_str(&w.finish()); -} - -/// The `(w, p) => ...` closure converting one element out of an iterator's -/// `next` slot at pointer `p`, applying the per-element receiving plan -/// ([`IteratorProtocol::elem`](weaveffi_core::plan::IteratorProtocol::elem)): -/// a string is copied out of wasm memory and freed with `free_string`, a -/// bytes or buffered element is copied out of its `ptr` + `len` pair and -/// freed with `free_bytes` (buffered elements are then decoded through the -/// buffer reader), an object pointer is adopted by `_wrap`, and a by-value -/// element is read directly. -fn js_iter_decode_closure(elem: &Ty, pass: &RetPass, module: &str) -> String { - match pass { - RetPass::Void => unreachable!("iterator elements always have a type"), - RetPass::Buffer => { - 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; }}" - ) - } - RetPass::Bytes => { - "(w, p) => { const dv = new DataView(w.memory.buffer); return _takeBytes(w, dv.getUint32(p, true), dv.getUint32(p + 4, true)); }".into() - } - RetPass::String => { - "(w, p) => _takeCStr(w, new DataView(w.memory.buffer).getUint32(p, true))".into() - } - RetPass::Object { nullable, .. } => { - let cls = local_type_name(object_name(elem)); - if *nullable { - format!("(w, p) => {{ const h = new DataView(w.memory.buffer).getUint32(p, true); return h === 0 ? null : {cls}._wrap(h); }}") - } else { - format!("(w, p) => {cls}._wrap(new DataView(w.memory.buffer).getUint32(p, true))") - } - } - RetPass::Direct => { - let read = read_scalar_at(elem, "new DataView(w.memory.buffer)", "p"); - format!("(w, p) => {read}") - } - } -} - -/// Emit an iterator-returning function as a method returning a lazy JS -/// iterator over the producer's iterator handle (the TypeScript type is -/// `IterableIterator`). The wrapper issues one producer `next` call per -/// consumer step, converts and frees (or adopts) each element per its plan, -/// and destroys the handle exactly once: on exhaustion, on a `next` error, or -/// from `return()` when the consumer stops early. Both the launch call and -/// every `next` route their out-err slot through the throws-aware checker, so -/// a throwing function's domain errors keep their typed class. -#[allow(clippy::too_many_arguments)] -fn emit_js_iterator_function_wrapper( - out: &mut String, - mb: &ModuleBinding, - f: &FnBinding, - ib: &IteratorBinding, - decl: JsDecl, - self_arg: Option<&str>, - indent: &str, - prefix: &str, -) { - let body = format!("{indent} "); - let proto = ib.protocol(f, &mb.path, prefix); - let js_params: Vec = f.params.iter().map(js_param_name).collect(); - let checker = js_checker_name(f, mb.error.as_ref()); - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - - if let Some(msg) = &f.deprecated { - w.line(format!("/** @deprecated {msg} */")); - } - w.line(format!( - "{}{}({}) {{", - decl.prefix(), - js_fn_name(f), - js_params.join(", ") - )); - - let mut args: Vec = self_arg.iter().map(ToString::to_string).collect(); - let mut cleanup = Vec::new(); - let mut staged = String::new(); - for (i, p) in f.params.iter().enumerate() { - emit_stage_input( - &mut staged, - &body, - p, - &format!("a{i}"), - &mb.path, - &mut args, - &mut cleanup, - ); - } - if f.cancellable { - args.push("0".to_string()); - } - let slot_size = iter_slot_size(&ib.elem); - // A `ptr` + `len` element (bytes or buffered) writes through two out - // slots; the second lives 4 bytes past the first. - let two_slot = matches!(proto.elem_free, Free::Bytes); - let next_call = if two_slot { - format!( - "(it, slot, ep) => wasm.{}(it, slot, slot + 4, ep),", - ib.next.symbol - ) - } else { - format!("(it, slot, ep) => wasm.{}(it, slot, ep),", ib.next.symbol) - }; - let decode = js_iter_decode_closure(&ib.elem, &proto.elem, &mb.path); - w.scope(|w| { - w.raw(&staged); - emit_guarded_call(w, &f.c_base, &args, &cleanup, false, Some("_it")); - w.line(format!("{checker}(wasm, _err);")); - w.line("_freeErr(wasm, _err);"); - w.line(format!( - "return new _WeaveFFIIterator(wasm, _it, {slot_size}," - )); - w.line(format!(" {next_call}")); - w.line(format!(" (it) => wasm.{}(it),", ib.destroy_symbol)); - w.line(format!(" {checker}, {decode});")); - }); - w.line(decl.close()); - out.push_str(&w.finish()); -} - -/// The wasm value types of an async completion callback's slots, straight -/// from the lowered [`AsyncBinding::callback_params`]: `(context, err, -/// )`, where pointers are `i32` on wasm32, 64-bit integers -/// widen to `i64`, and a bytes or buffered result arrives as an owned -/// `ptr` + `len` pair (two `i32` slots). -pub(crate) fn async_cb_wasm_params(a: &AsyncBinding) -> Vec<&'static str> { - a.callback_params - .iter() - .map(|p| slot_wasm_type(&p.ty)) - .collect() -} - -/// Emit the `unwrap` clause for an async result, or none for a void/raw-scalar -/// result (where `results[0]` is already idiomatic), dispatching on the -/// shared [`RetPass`] contract. Assumes the callback was registered with -/// [`async_cb_wasm_params`] widths. `mk_err` is the domain factory stored as -/// the context's `mkErr` for throwing callables, so the completion callback -/// rejects with the typed error. -/// -/// The unwrap runs inside the completion callback, so it follows the async -/// ownership contract: string, byte, and value buffers are consumer-owned, -/// so they are deep-copied or decoded out of wasm memory and then released -/// through the runtime free symbols. Object results transfer one strong -/// reference, adopted by the wrapper class instead. -fn emit_async_unwrap( - out: &mut String, - indent: &str, - ret: Option<&Ty>, - pass: &RetPass, - mk_err: Option<&str>, - module: &str, -) { - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - let base = match mk_err { - Some(factory) => format!("resolve, reject, mkErr: {factory}"), - None => "resolve, reject".to_string(), - }; - let plain = format!("_asyncContexts.set(ctxId, {{ {base} }});"); - let Some(ret) = ret else { - w.line(plain); - out.push_str(&w.finish()); - return; - }; - let open = format!("_asyncContexts.set(ctxId, {{ {base}, unwrap: "); - match pass { - RetPass::Void => unreachable!("a present return type is never void"), - // Owned: copy the encoding out of wasm memory, free the producer - // allocation, then decode from the copy. - RetPass::Buffer => { - w.block(format!("{open}(w, ptr, len) => {{"), "} });", |w| { - w.line( - "const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice();", - ); - w.line("if (ptr !== 0) w.weaveffi_free_bytes(ptr, len);"); - w.line("const _rd = new _BufReader(_b);"); - w.line(format!("const _v = {};", buf_read_expr(ret, module, "_rd"))); - w.line("_rd.end();"); - w.line("return _v;"); - }); - } - RetPass::Direct if matches!(ret, Ty::Bool | Ty::U32 | Ty::U64) => { - w.line(format!( - "{open}(w, r) => {} }});", - js_direct_value(ret, "r") - )); - } - RetPass::Direct => { - w.line(plain); - } - RetPass::String => { - // Owned: copy out of wasm memory, then free. - w.block(format!("{open}(w, p) => {{"), "} });", |w| { - w.line("const _s = _readCStr(w, p);"); - w.line("if (p !== 0) w.weaveffi_free_string(p);"); - w.line("return _s;"); - }); - } - RetPass::Object { nullable, .. } => { - let cls = local_type_name(object_name(ret)); - if *nullable { - w.line(format!( - "{open}(w, h) => h === 0 ? null : {cls}._wrap(h) }});" - )); - } else { - w.line(format!("{open}(w, h) => {cls}._wrap(h) }});")); - } - } - RetPass::Bytes => { - // Owned: slice() deep-copies out of wasm memory, then free. - w.block(format!("{open}(w, ptr, len) => {{"), "} });", |w| { - w.line( - "const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice();", - ); - w.line("if (ptr !== 0) w.weaveffi_free_bytes(ptr, len);"); - w.line("return _b;"); - }); - } - } - out.push_str(&w.finish()); -} - -/// Emit an async function as a method returning a `Promise` at `indent`. -/// Throwing callables store the domain's error factory in the async context, -/// so the completion callback rejects with the typed error; non-throwing ones -/// reject with the generic brand error only for panics and foreign callback -/// failures. -#[allow(clippy::too_many_arguments)] -fn emit_js_async_function_wrapper( - out: &mut String, - mb: &ModuleBinding, - f: &FnBinding, - ab: &AsyncBinding, - decl: JsDecl, - self_arg: Option<&str>, - indent: &str, - prefix: &str, -) { - let body2 = format!("{indent} "); - let proto = ab.protocol(f, &mb.path, prefix); - let js_params: Vec = f.params.iter().map(js_param_name).collect(); - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - - if let Some(msg) = &f.deprecated { - w.line(format!("/** @deprecated {msg} */")); - } - - // Pre-render the inner-most (depth + 2) fragments that delegate to helpers, - // so the nested blocks below can splice them at the right depth. - let mut unwrap = String::new(); - emit_async_unwrap( - &mut unwrap, - &body2, - f.ret.as_ref(), - &proto.result, - js_err_factory(f, mb.error.as_ref()).as_deref(), - &mb.path, - ); - let mut staged = String::new(); - let mut args: Vec = self_arg.iter().map(ToString::to_string).collect(); - let mut cleanup = Vec::new(); - for (i, p) in f.params.iter().enumerate() { - emit_stage_input( - &mut staged, - &body2, - p, - &format!("a{i}"), - &mb.path, - &mut args, - &mut cleanup, - ); - } - let sig_key = async_cb_wasm_params(ab).join("_"); - if proto.cancellable { - args.push("0".to_string()); - } - args.push(format!("_cbPtr_{sig_key}")); - args.push("ctxId".to_string()); - - w.block( - format!( - "{}{}({}) {{", - decl.prefix(), - js_fn_name(f), - js_params.join(", ") - ), - decl.close(), - |w| { - w.block("return new Promise((resolve, reject) => {", "});", |w| { - w.line("const ctxId = _nextCtxId++;"); - w.raw(&unwrap); - w.raw(&staged); - // The launcher may complete the call inline (the default - // wasm32 spawner drives the future before returning), so a - // producer trap surfaces here rather than in the callback: - // forget the context and reject with the translated error. - w.block("try {", "} catch (e) {", |w| { - w.line(format!("wasm.{}({});", ab.launch.symbol, args.join(", "))); - }); - w.scope(|w| { - w.line("_asyncContexts.delete(ctxId);"); - w.line("reject(_trapError(e));"); - }); - if cleanup.is_empty() { - w.line("}"); - } else { - w.line("} finally {"); - w.scope(|w| { - for stmt in &cleanup { - w.line(stmt); - } - }); - w.line("}"); - } - }); - }, - ); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-wasm/src/codec.rs b/crates/weaveffi-gen-wasm/src/codec.rs deleted file mode 100644 index e69d6a9d..00000000 --- a/crates/weaveffi-gen-wasm/src/codec.rs +++ /dev/null @@ -1,430 +0,0 @@ -//! Value-buffer codegen: the private JS buffer runtime and the per-type -//! codec functions. -//! -//! Buffered values (records, rich enums, optionals, lists, maps, and error -//! payloads) cross the boundary serialized in the WeaveFFI value-buffer wire -//! format. This module emits the JS statements and expressions that encode -//! and decode that format. Every dispatch goes through [`Ty::wire`], so the -//! wire-shape classification (objects as `u64` tokens, records and rich enums -//! as one user-codec shape) lives in `weaveffi-core`, not here. -//! -//! An interface inside a buffer is an object token carrying one strong -//! reference: the writer calls the wrapper's `_clone()` (the interface's -//! `_clone` symbol) and writes the fresh pointer, and the reader adopts the -//! decoded pointer into a new wrapper via the class's `_wrap`. Because the -//! wrapper classes close over the loaded `wasm` instance, the per-type codecs -//! are emitted inside the loader rather than at module scope. - -use weaveffi_core::abi::split_qualified; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::ERROR_BRAND; -use weaveffi_core::model::BindingModel; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{Prim, WireType}; -use weaveffi_core::utils::local_type_name; - -/// The `_write_*`/`_read_*` codec function names for a (possibly -/// `module.Name`-qualified) record or rich enum referenced from -/// `current_module`. -pub(crate) fn buf_codec_names(name: &str, current_module: &str) -> (String, String) { - let (module, local) = split_qualified(name, current_module); - ( - format!("_write_{module}_{local}"), - format!("_read_{module}_{local}"), - ) -} - -/// The buffer writer/reader method encoding one leaf wire shape (including -/// strings and bytes, whose length-prefixed forms live in the runtime too). -/// -/// # Panics -/// -/// Panics on a composite wire shape; callers dispatch enums, user codecs, -/// and containers before falling through to a leaf method. -fn scalar_method(wt: WireType<'_>) -> &'static str { - match wt { - 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:?}"), - } -} - -/// Append the statements serializing `val` (a JS expression of IDL type `ty`) -/// into the buffer writer named `wtr`, resolving record and rich-enum -/// references against `module`. `tmp` supplies collision-free local names. -pub(crate) fn emit_buf_write_stmts( - w: &mut CodeWriter, - ty: &Ty, - wtr: &str, - val: &str, - module: &str, - tmp: &mut u32, -) { - match ty.wire() { - WireType::Enum(_) => { - w.line(format!("{wtr}.i32({val});")); - } - // An object token must carry a reference of its own: `_clone()` calls - // the interface's clone symbol, so the wrapper keeps the pointer it - // already holds. - WireType::Object(_) => { - w.line(format!("{wtr}.obj({val}._clone());")); - } - WireType::User(name) => { - let (write_fn, _) = buf_codec_names(name, module); - w.line(format!("{write_fn}({wtr}, {val});")); - } - WireType::Optional(inner) => { - w.line(format!("if ({val} === null || {val} === undefined) {{")); - w.scope(|w| { - w.line(format!("{wtr}.flag(false);")); - }); - w.line("} else {"); - w.scope(|w| { - w.line(format!("{wtr}.flag(true);")); - emit_buf_write_stmts(w, inner, wtr, val, module, tmp); - }); - w.line("}"); - } - WireType::List(inner) => { - *tmp += 1; - let arr = format!("_a{tmp}"); - let elem = format!("_e{tmp}"); - w.line(format!("const {arr} = {val} || [];")); - w.line(format!("{wtr}.len({arr}.length);")); - w.line(format!("for (const {elem} of {arr}) {{")); - w.scope(|w| { - emit_buf_write_stmts(w, inner, wtr, &elem, module, tmp); - }); - w.line("}"); - } - WireType::Map(k, v) => { - *tmp += 1; - let src = format!("_s{tmp}"); - let ents = format!("_m{tmp}"); - let key = format!("_k{tmp}"); - let value = format!("_v{tmp}"); - w.line(format!("const {src} = {val} || {{}};")); - w.line(format!( - "const {ents} = {src} instanceof Map ? [...{src}.entries()] : Object.entries({src});" - )); - w.line(format!("{wtr}.len({ents}.length);")); - w.line(format!("for (const [{key}, {value}] of {ents}) {{")); - w.scope(|w| { - emit_buf_write_stmts(w, k, wtr, &key, module, tmp); - emit_buf_write_stmts(w, v, wtr, &value, module, tmp); - }); - w.line("}"); - } - leaf => { - w.line(format!("{wtr}.{}({val});", scalar_method(leaf))); - } - } -} - -/// A JS expression decoding one value of IDL type `ty` from the buffer reader -/// 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: &Ty, module: &str, rdr: &str) -> String { - match ty.wire() { - WireType::Enum(_) => format!("{rdr}.i32()"), - // The token carries one strong reference, which the new wrapper - // adopts; the reader rejects a zero token. - WireType::Object(name) => format!("{}._wrap({rdr}.obj())", local_type_name(name)), - WireType::User(name) => { - let (_, read_fn) = buf_codec_names(name, module); - format!("{read_fn}({rdr})") - } - WireType::Optional(inner) => { - format!( - "({rdr}.flag() ? {} : null)", - buf_read_expr(inner, module, rdr) - ) - } - WireType::List(inner) => { - let elem = buf_read_expr(inner, module, rdr); - format!( - "(() => {{ const _n = {rdr}.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push({elem}); return _arr; }})()" - ) - } - WireType::Map(k, v) => { - let key = buf_read_expr(k, module, rdr); - let value = buf_read_expr(v, module, rdr); - format!( - "(() => {{ const _n = {rdr}.len(); const _obj = {{}}; for (let _i = 0; _i < _n; _i++) {{ const _k = {key}; _obj[_k] = {value}; }} return _obj; }})()" - ) - } - leaf => format!("{rdr}.{}()", scalar_method(leaf)), - } -} - -/// Emit the private value-buffer runtime: a growable little-endian writer and -/// a strict reader implementing the WeaveFFI wire format. Malformed input (a -/// producer/consumer contract violation) throws the generic brand error with -/// code `-3`. -pub(crate) fn emit_js_buffer_runtime(out: &mut String) { - let mut w = CodeWriter::two_space(); - w.line("// Growable little-endian byte writer implementing the WeaveFFI value-buffer"); - w.line("// wire format: values are packed back to back with no alignment; lengths and"); - w.line("// counts are u32; strings are u32 byte length + UTF-8 bytes (no NUL)."); - w.block("class _BufWriter {", "}", |w| { - w.block("constructor() {", "}", |w| { - w.line("this._u8 = new Uint8Array(64);"); - w.line("this._dv = new DataView(this._u8.buffer);"); - w.line("this._len = 0;"); - }); - w.block("_need(n) {", "}", |w| { - w.line("if (this._len + n <= this._u8.length) return;"); - w.line("let cap = this._u8.length * 2;"); - w.line("while (cap < this._len + n) cap *= 2;"); - w.line("const u8 = new Uint8Array(cap);"); - w.line("u8.set(this._u8.subarray(0, this._len));"); - w.line("this._u8 = u8;"); - w.line("this._dv = new DataView(u8.buffer);"); - }); - w.line("bool(v) { this._need(1); this._dv.setUint8(this._len, v ? 1 : 0); this._len += 1; }"); - w.line("i8(v) { this._need(1); this._dv.setInt8(this._len, v); this._len += 1; }"); - w.line("u8(v) { this._need(1); this._dv.setUint8(this._len, v); this._len += 1; }"); - w.line("i16(v) { this._need(2); this._dv.setInt16(this._len, v, true); this._len += 2; }"); - w.line("u16(v) { this._need(2); this._dv.setUint16(this._len, v, true); this._len += 2; }"); - w.line("i32(v) { this._need(4); this._dv.setInt32(this._len, v, true); this._len += 4; }"); - w.line("u32(v) { this._need(4); this._dv.setUint32(this._len, v, true); this._len += 4; }"); - w.line("i64(v) { this._need(8); this._dv.setBigInt64(this._len, BigInt(v), true); this._len += 8; }"); - w.line("u64(v) { this._need(8); this._dv.setBigUint64(this._len, BigInt(v), true); this._len += 8; }"); - w.line("f32(v) { this._need(4); this._dv.setFloat32(this._len, v, true); this._len += 4; }"); - w.line("f64(v) { this._need(8); this._dv.setFloat64(this._len, v, true); this._len += 8; }"); - w.line("len(n) { this.u32(n); }"); - w.line("flag(present) { this.u8(present ? 1 : 0); }"); - w.line("// An object token: the pointer (a wasm32 i32, read unsigned) widened to u64."); - w.line("obj(ptr) { this.u64(BigInt(ptr >>> 0)); }"); - w.block("str(v) {", "}", |w| { - w.line("const b = _enc.encode(v);"); - w.line("this.len(b.length);"); - w.line("this._need(b.length);"); - w.line("this._u8.set(b, this._len);"); - w.line("this._len += b.length;"); - }); - w.block("bytes(v) {", "}", |w| { - w.line("const b = v instanceof Uint8Array ? v : new Uint8Array(v);"); - w.line("this.len(b.length);"); - w.line("this._need(b.length);"); - w.line("this._u8.set(b, this._len);"); - w.line("this._len += b.length;"); - }); - w.line("finish() { return this._u8.subarray(0, this._len); }"); - }); - w.blank(); - w.line("const _bufDec = new TextDecoder('utf-8', { fatal: true });"); - w.blank(); - w.line("// Strict little-endian reader for the WeaveFFI value-buffer wire format. A"); - w.line("// malformed buffer (truncation, invalid bool or flag bytes, an oversized"); - w.line("// length prefix, invalid UTF-8, trailing bytes) is a producer/consumer"); - w.line("// contract violation and throws the generic brand error; code -3 marks a"); - w.line("// consumer-side marshalling failure."); - w.block("class _BufReader {", "}", |w| { - w.block("constructor(bytes) {", "}", |w| { - w.line("this._u8 = bytes;"); - w.line("this._dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);"); - w.line("this._pos = 0;"); - }); - w.block("_bad(what) {", "}", |w| { - w.line(format!( - "throw new {ERROR_BRAND}(-3, 'malformed value buffer: ' + what);" - )); - }); - w.block("_take(n, what) {", "}", |w| { - w.line("if (this._pos + n > this._u8.length) this._bad('truncated ' + what);"); - w.line("const at = this._pos;"); - w.line("this._pos += n;"); - w.line("return at;"); - }); - w.block("bool() {", "}", |w| { - w.line("const b = this._u8[this._take(1, 'bool')];"); - w.line("if (b > 1) this._bad('bool byte out of range');"); - w.line("return b === 1;"); - }); - w.line("i8() { return this._dv.getInt8(this._take(1, 'i8')); }"); - w.line("u8() { return this._u8[this._take(1, 'u8')]; }"); - w.line("i16() { return this._dv.getInt16(this._take(2, 'i16'), true); }"); - w.line("u16() { return this._dv.getUint16(this._take(2, 'u16'), true); }"); - w.line("i32() { return this._dv.getInt32(this._take(4, 'i32'), true); }"); - w.line("u32() { return this._dv.getUint32(this._take(4, 'u32'), true); }"); - w.line("i64() { return this._dv.getBigInt64(this._take(8, 'i64'), true); }"); - w.line("u64() { return this._dv.getBigUint64(this._take(8, 'u64'), true); }"); - w.line("f32() { return this._dv.getFloat32(this._take(4, 'f32'), true); }"); - w.line("f64() { return this._dv.getFloat64(this._take(8, 'f64'), true); }"); - w.block("len() {", "}", |w| { - w.line("const n = this.u32();"); - w.line("if (n > this._u8.length - this._pos) this._bad('length prefix exceeds remaining buffer');"); - w.line("return n;"); - }); - w.block("flag() {", "}", |w| { - w.line("const b = this._u8[this._take(1, 'option flag')];"); - w.line("if (b > 1) this._bad('option flag byte out of range');"); - w.line("return b === 1;"); - }); - w.line("// An object token narrowed back to a wasm32 pointer; zero is never a"); - w.line("// valid token (an absent `Interface?` uses the option flag instead)."); - w.block("obj() {", "}", |w| { - w.line("const t = this.u64();"); - w.line("if (t === 0n || t > 0xffffffffn) this._bad('object token out of range');"); - w.line("return Number(t);"); - }); - w.block("str() {", "}", |w| { - w.line("const n = this.len();"); - w.line("const at = this._take(n, 'string bytes');"); - w.block("try {", "} catch (e) {", |w| { - w.line("return _bufDec.decode(this._u8.subarray(at, at + n));"); - }); - w.scope(|w| { - w.line("this._bad('string is not valid UTF-8');"); - }); - w.line("}"); - }); - w.block("bytes() {", "}", |w| { - w.line("const n = this.len();"); - w.line("const at = this._take(n, 'byte buffer');"); - w.line("return this._u8.subarray(at, at + n).slice();"); - }); - w.block("end() {", "}", |w| { - w.line("if (this._pos !== this._u8.length) this._bad('trailing bytes after value');"); - }); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the `_write_*`/`_read_*` codec pair for every record and rich enum in -/// the model, in model (declaration) order, at `indent` (loader scope, so the -/// object-token arms can reach the interface classes). Field order is fixed -/// at generation time, so the codecs are direct straight-line code with no -/// runtime dispatch. -pub(crate) fn emit_js_buffer_codecs(out: &mut String, model: &BindingModel, indent: &str) { - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - for m in &model.modules { - for s in &m.structs { - let (write_fn, read_fn) = buf_codec_names(&s.name, &m.path); - w.line(format!( - "// Serialize a `{}.{}` record into the value-buffer wire format.", - m.path, s.name - )); - w.block(format!("function {write_fn}(w, v) {{"), "}", |w| { - let mut tmp = 0u32; - for f in &s.fields { - emit_buf_write_stmts( - w, - &f.ty, - "w", - &format!("v.{}", f.name), - &m.path, - &mut tmp, - ); - } - }); - w.blank(); - w.line(format!( - "// Decode a `{}.{}` record from the value-buffer wire format.", - m.path, s.name - )); - w.block(format!("function {read_fn}(r) {{"), "}", |w| { - w.line("const v = {};"); - for f in &s.fields { - w.line(format!( - "v.{} = {};", - f.name, - buf_read_expr(&f.ty, &m.path, "r") - )); - } - w.line("return v;"); - }); - w.blank(); - } - for e in m.enums.iter().filter(|e| e.is_rich()) { - let (write_fn, read_fn) = buf_codec_names(&e.name, &m.path); - w.line(format!( - "// Serialize a `{}.{}` rich enum into the value-buffer wire format:", - m.path, e.name - )); - w.line("// an i32 tag, then the active variant's fields in order."); - w.block(format!("function {write_fn}(w, v) {{"), "}", |w| { - w.block("switch (v.tag) {", "}", |w| { - for v in &e.variants { - w.block(format!("case \"{}\": {{", v.name), "}", |w| { - w.line(format!("w.i32({});", v.value)); - let mut tmp = 0u32; - for f in &v.fields { - emit_buf_write_stmts( - w, - &f.ty, - "w", - &format!("v.{}", f.name), - &m.path, - &mut tmp, - ); - } - w.line("break;"); - }); - } - w.line("default:"); - w.scope(|w| { - w.line(format!( - "throw new {ERROR_BRAND}(-3, \"unknown {} variant tag: \" + v.tag);", - e.name - )); - }); - }); - }); - w.blank(); - w.line(format!( - "// Decode a `{}.{}` rich enum from the value-buffer wire format.", - m.path, e.name - )); - w.block(format!("function {read_fn}(r) {{"), "}", |w| { - w.line("const _tag = r.i32();"); - w.block("switch (_tag) {", "}", |w| { - for v in &e.variants { - if v.fields.is_empty() { - w.line(format!("case {}:", v.value)); - w.scope(|w| { - w.line(format!("return {{ tag: \"{}\" }};", v.name)); - }); - } else { - w.block(format!("case {}: {{", v.value), "}", |w| { - w.line(format!("const v = {{ tag: \"{}\" }};", v.name)); - for f in &v.fields { - w.line(format!( - "v.{} = {};", - f.name, - buf_read_expr(&f.ty, &m.path, "r") - )); - } - w.line("return v;"); - }); - } - } - w.line("default:"); - w.scope(|w| { - w.line(format!( - "throw new {ERROR_BRAND}(-3, \"malformed value buffer: unknown {} tag \" + _tag);", - e.name - )); - }); - }); - }); - w.blank(); - } - } - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-wasm/src/docs.rs b/crates/weaveffi-gen-wasm/src/docs.rs deleted file mode 100644 index 04e2664d..00000000 --- a/crates/weaveffi-gen-wasm/src/docs.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! JSDoc emission shared by the JS loader and the `.d.ts` declarations. - -use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::ParamBinding; - -use crate::types::js_param_name; - -/// Emits a JSDoc comment at `indent`. Single-line docs collapse to -/// `/** text */`; multi-line docs expand to a block with ` * ` prefixed lines. -pub(crate) fn emit_doc(out: &mut String, doc: &Option, indent: &str) { - common_emit_doc(out, doc, indent, DocCommentStyle::Javadoc); -} - -/// Emits a JSDoc block for a function: function doc, `@param name desc` for -/// each documented parameter (named as the camelCase JS parameter), and an -/// optional trailing tag list. -pub(crate) fn emit_fn_doc( - out: &mut String, - doc: &Option, - params: &[ParamBinding], - indent: &str, - extra_tags: &[String], -) { - let has_param_docs = params.iter().any(|p| p.doc.is_some()); - let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty()); - if trimmed_doc.is_none() && !has_param_docs && extra_tags.is_empty() { - return; - } - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - w.line("/**"); - if let Some(d) = trimmed_doc { - for line in d.lines() { - if line.is_empty() { - w.line(" *"); - } else { - w.line(format!(" * {line}")); - } - } - } - for p in params { - if let Some(pdoc) = &p.doc { - let pdoc = pdoc.trim(); - if pdoc.is_empty() { - continue; - } - let mut lines = pdoc.lines(); - if let Some(first) = lines.next() { - w.line(format!(" * @param {} {}", js_param_name(p), first)); - } - for line in lines { - if line.is_empty() { - w.line(" *"); - } else { - w.line(format!(" * {line}")); - } - } - } - } - for tag in extra_tags { - w.line(format!(" * {tag}")); - } - w.line(" */"); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-wasm/src/dts.rs b/crates/weaveffi-gen-wasm/src/dts.rs deleted file mode 100644 index 4bcf2697..00000000 --- a/crates/weaveffi-gen-wasm/src/dts.rs +++ /dev/null @@ -1,468 +0,0 @@ -//! TypeScript declaration (`.d.ts`) rendering: the error class surface, -//! record and enum type shapes, callback interfaces, ambient interface -//! classes, and the nested module interface the loader's promise resolves to. - -use heck::ToUpperCamelCase; -use weaveffi_core::codegen::common::DocCommentStyle; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::ERROR_BRAND; -use weaveffi_core::model::{ - BindingModel, CallShape, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, - InterfaceBinding, ModuleBinding, -}; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; - -use crate::docs::{emit_doc, emit_fn_doc}; -use crate::types::{ - emscripten_stub, js_cb_method_name, js_code_class_name, js_fn_name, js_param_name, ts_type_for, -}; - -/// Render the `.d.ts` companion: error classes, record -/// interfaces, enum constants, rich-enum unions, callback interfaces, ambient -/// interface classes, the `Module` API shape, and the `load` -/// signature. Async members, callback interfaces, and the functions taking -/// them are omitted in Emscripten mode, turning their runtime stubs into -/// compile-time errors for TS consumers. -pub(crate) fn render_wasm_dts( - model: &BindingModel, - module_name: &str, - input_basename: &str, - filename: &str, - emscripten: bool, -) -> String { - let pascal_name = module_name.to_upper_camel_case(); - let interface_name = format!("{pascal_name}Module"); - let load_fn = format!("load{pascal_name}"); - 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 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 { - emit_doc(&mut out, &field.doc, " "); - out.push_str(&format!(" {}: {};\n", field.name, ts_type_for(&field.ty))); - } - out.push_str("}\n\n"); - } - - 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() { - emit_dts_rich_enum_type(&mut out, e); - 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"); - out.push_str(&format!( - "export type {0} = (typeof {0})[keyof typeof {0}];\n\n", - e.name - )); - } - - // Callback interfaces are unsupported (and never referenced) in - // Emscripten mode, so their shapes are omitted with the functions - // that take them. - if !emscripten { - for cb in &mb.callback_interfaces { - emit_dts_callback_interface(&mut out, cb); - } - } - - for i in &mb.interfaces { - emit_dts_interface_class(&mut out, mb, i, emscripten); - } - } - - out.push_str(&format!("export interface {interface_name} {{\n")); - if model - .modules - .iter() - .any(|m| !m.functions.is_empty() || !m.interfaces.is_empty()) - { - // In Emscripten mode `_raw` is the loader's export-binding object, a - // plain record, not a `WebAssembly.Exports`. - if emscripten { - out.push_str(" _raw: Record;\n"); - } else { - out.push_str(" _raw: WebAssembly.Exports;\n"); - } - for module in model.roots() { - render_dts_module_interface(&mut out, model, module, " ", emscripten); - } - } - out.push_str("}\n\n"); - - if emscripten { - out.push_str(&format!( - "export function {load_fn}(module: object | Promise): Promise<{interface_name}>;\n\n" - )); - } else { - out.push_str(&format!( - "export function {load_fn}(source: string | URL | BufferSource | WebAssembly.Module): Promise<{interface_name}>;\n\n" - )); - } - out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); - out -} - -/// Emit the TypeScript declaration for a rich (algebraic) enum: a -/// 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: &EnumBinding) { - let name = &e.name; - let mut w = CodeWriter::two_space(); - w.doc(&e.doc, DocCommentStyle::Javadoc); - w.line(format!("export type {name} =")); - w.scope(|w| { - let last = e.variants.len().saturating_sub(1); - for (i, v) in e.variants.iter().enumerate() { - let fields: String = v - .fields - .iter() - .map(|f| format!("; {}: {}", f.name, ts_type_for(&f.ty))) - .collect(); - let term = if i == last { ";" } else { "" }; - w.line(format!("| {{ tag: \"{}\"{fields} }}{term}", v.name)); - } - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the TypeScript `interface` a consumer implements for one callback -/// interface: one method per declared method, camelCase, with the same -/// parameter and return typing as any other callable. Object parameters -/// arrive as wrappers the implementation owns (and should `close()`). -fn emit_dts_callback_interface(out: &mut String, cb: &CallbackInterfaceBinding) { - let mut w = CodeWriter::two_space(); - let mut tags = Vec::new(); - if let Some(msg) = &cb.deprecated { - tags.push(format!("@deprecated {msg}")); - } - let doc = cb - .doc - .as_deref() - .map(str::trim) - .filter(|d| !d.is_empty()) - .map_or_else( - || { - "Implemented by the consumer and passed to the producer; the producer \ - calls these methods synchronously. An exception thrown from a method \ - is reported to the producer as an error (code -4) and the method's \ - default value is returned in its place." - .to_string() - }, - |d| { - format!( - "{d}\n\nImplemented by the consumer; the producer calls these methods \ - synchronously. An exception thrown from a method is reported to the \ - producer as an error (code -4)." - ) - }, - ); - let mut rendered = String::new(); - emit_fn_doc(&mut rendered, &Some(doc), &[], "", &tags); - w.raw(rendered); - w.block(format!("export interface {} {{", cb.name), "}", |w| { - let inner = w.indent_str(); - for m in &cb.methods { - let mut tags = Vec::new(); - if let Some(msg) = &m.deprecated { - tags.push(format!("@deprecated {msg}")); - } - let mut doc = String::new(); - emit_fn_doc(&mut doc, &m.doc, &m.params, &inner, &tags); - w.raw(doc); - let params: Vec = m - .params - .iter() - .map(|p| format!("{}: {}", js_param_name(p), ts_type_for(&p.ty))) - .collect(); - let ret = m - .ret - .as_ref() - .map_or_else(|| "void".to_string(), ts_type_for); - w.line(format!( - "{}({}): {ret};", - js_cb_method_name(&m.name), - params.join(", ") - )); - } - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// The TypeScript parameter list for one callable: camelCase names typed by -/// [`ts_type_for`]. -fn dts_params(f: &FnBinding) -> String { - f.params - .iter() - .map(|p| format!("{}: {}", js_param_name(p), ts_type_for(&p.ty))) - .collect::>() - .join(", ") -} - -/// The TypeScript return annotation for one callable (`Promise<...>` when -/// async, `void` for no return). -fn dts_ret(f: &FnBinding) -> String { - let base = f - .ret - .as_ref() - .map(ts_type_for) - .unwrap_or_else(|| "void".into()); - if f.is_async { - format!("Promise<{base}>") - } else { - base - } -} - -/// The JSDoc tag list for one callable: `@deprecated` first when present, a -/// streaming note for iterator-returning callables, an ownership note for -/// object-returning callables, then the `@throws` tag matching the throws -/// split (the typed domain error for throwing callables, the generic brand -/// error otherwise). -fn dts_fn_tags(f: &FnBinding, error: Option<&ErrorBinding>) -> Vec { - let mut tags = Vec::new(); - if let Some(msg) = &f.deprecated { - tags.push(format!("@deprecated {msg}")); - } - match (&f.shape, f.ret.as_ref()) { - (CallShape::Iterator(_), _) => tags.push( - "@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." - .to_string(), - ), - (_, Some(ret)) if ret.interface_name().is_some() => tags.push( - "@returns A wrapper owning one reference; `close()` it (or bind it with \ - `using`) when done." - .to_string(), - ), - _ => {} - } - match error { - Some(eb) if f.throws => tags.push(format!( - "@throws {{{}}} on a domain error code", - eb.type_name - )), - _ => tags.push(format!( - "@throws {{{ERROR_BRAND}}} if the native call fails" - )), - } - tags -} - -/// Whether a callable is declared at all: in Emscripten mode async functions -/// and functions taking callback interfaces are runtime stubs, so leaving -/// them out makes the gap a compile-time error for TS consumers. -fn declared(f: &FnBinding, emscripten: bool) -> bool { - !(emscripten && emscripten_stub(f)) -} - -/// Emit one module's member block inside the `Module` interface: -/// function signatures (stubbed ones omitted in Emscripten mode), `typeof` -/// bindings for the interface classes, and nested submodule blocks, skipping -/// subtrees with no declared content. -fn render_dts_module_interface( - out: &mut String, - model: &BindingModel, - mb: &ModuleBinding, - indent: &str, - emscripten: bool, -) { - fn tree_has_content(model: &BindingModel, mb: &ModuleBinding) -> bool { - !mb.functions.is_empty() - || !mb.interfaces.is_empty() - || model.children(mb).any(|sub| tree_has_content(model, sub)) - } - if !tree_has_content(model, mb) { - return; - } - let error = mb.error.as_ref(); - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - w.block(format!("{}: {{", mb.name), "};", |w| { - let inner = w.indent_str(); - for f in mb.functions.iter().filter(|f| declared(f, emscripten)) { - let mut doc = String::new(); - emit_fn_doc(&mut doc, &f.doc, &f.params, &inner, &dts_fn_tags(f, error)); - w.raw(doc); - w.line(format!( - "{}({}): {};", - js_fn_name(f), - dts_params(f), - dts_ret(f) - )); - } - // The module object carries the interface class itself, so statics, - // factories, and `new` are reachable as `api.kv.Store...`. - for i in &mb.interfaces { - w.line(format!("{}: typeof {};", i.name, i.name)); - } - for sub in model.children(mb) { - let mut tmp = String::new(); - render_dts_module_interface(&mut tmp, model, sub, &inner, emscripten); - w.raw(tmp); - } - }); - out.push_str(&w.finish()); -} - -/// Emit the TypeScript declarations for the error surface: the generic brand -/// error, then one domain class per declaring module with its per-code -/// subclasses (each carrying a literal-typed `CODE` and any declared payload -/// fields) and the static aliases hung on the domain class. -fn emit_dts_error_classes(out: &mut String, model: &BindingModel) { - let mut w = CodeWriter::two_space(); - w.line("/** Base error for WeaveFFI failures: domain errors extend it, and it is"); - w.line(" * thrown directly for unknown codes, marshalling failures, producer"); - w.line(" * panics, and callback-interface implementations that raised (code -4)."); - w.line(" * Carries the stable ABI `code`. */"); - w.block( - format!("export declare class {ERROR_BRAND} extends Error {{"), - "}", - |w| { - w.line("constructor(code: number, message?: string);"); - w.line("code: number;"); - }, - ); - w.blank(); - for m in &model.modules { - let Some(eb) = m.error.as_ref().filter(|eb| eb.declared_here) else { - continue; - }; - let domain = &eb.type_name; - w.line(format!( - "/** Base error for the `{}` module's error domain. */", - m.path - )); - w.block( - format!("export declare class {domain} extends {ERROR_BRAND} {{"), - "}", - |w| { - for c in &eb.codes { - let class = js_code_class_name(&c.name); - w.line(format!("static readonly {class}: typeof {class};")); - } - }, - ); - w.blank(); - for c in &eb.codes { - let class = js_code_class_name(&c.name); - let doc = c - .doc - .clone() - .filter(|d| !d.trim().is_empty()) - .or_else(|| Some(c.message.clone())); - w.doc(&doc, DocCommentStyle::Javadoc); - w.block( - format!("export declare class {class} extends {domain} {{"), - "}", - |w| { - w.line("constructor(message?: string);"); - w.line(format!("static readonly CODE: {};", c.value)); - for f in &c.fields { - w.doc(&f.doc, DocCommentStyle::Javadoc); - w.line(format!("readonly {}: {};", f.name, ts_type_for(&f.ty))); - } - }, - ); - w.blank(); - } - } - out.push_str(&w.finish()); -} - -/// Emit the TypeScript declaration for an interface: an ambient class whose -/// runtime binding is reached through the module object (`api.kv.Store`). The -/// canonical `new` constructor declares `constructor`; other constructors and -/// statics are static members; stubbed members are omitted in Emscripten mode. -/// Every class declares `close()`; the `Symbol.dispose` method is present at -/// runtime but left undeclared so the file type-checks without the -/// `esnext.disposable` lib. -fn emit_dts_interface_class( - out: &mut String, - mb: &ModuleBinding, - i: &InterfaceBinding, - emscripten: bool, -) { - let error = mb.error.as_ref(); - let mut w = CodeWriter::two_space(); - let doc = i - .doc - .as_deref() - .map(str::trim) - .filter(|d| !d.is_empty()) - .map_or_else( - || "Wraps one reference to a producer object; release it with `close()`.".to_string(), - |d| { - format!( - "{d}\n\nWraps one reference to a producer object; release it with `close()`." - ) - }, - ); - w.doc(&Some(doc), DocCommentStyle::Javadoc); - w.block(format!("export declare class {} {{", i.name), "}", |w| { - let inner = w.indent_str(); - if let Some(c) = i.constructors.iter().find(|c| c.name == "new") { - let mut doc = String::new(); - emit_fn_doc(&mut doc, &c.doc, &c.params, &inner, &dts_fn_tags(c, error)); - w.raw(doc); - w.line(format!("constructor({});", dts_params(c))); - } - for c in i.constructors.iter().filter(|c| c.name != "new") { - let mut doc = String::new(); - emit_fn_doc(&mut doc, &c.doc, &c.params, &inner, &dts_fn_tags(c, error)); - w.raw(doc); - w.line(format!( - "static {}({}): {};", - js_fn_name(c), - dts_params(c), - dts_ret(c) - )); - } - for f in i.methods.iter().filter(|f| declared(f, emscripten)) { - let mut doc = String::new(); - emit_fn_doc(&mut doc, &f.doc, &f.params, &inner, &dts_fn_tags(f, error)); - w.raw(doc); - w.line(format!( - "{}({}): {};", - js_fn_name(f), - dts_params(f), - dts_ret(f) - )); - } - for f in i.statics.iter().filter(|f| declared(f, emscripten)) { - let mut doc = String::new(); - emit_fn_doc(&mut doc, &f.doc, &f.params, &inner, &dts_fn_tags(f, error)); - w.raw(doc); - w.line(format!( - "static {}({}): {};", - js_fn_name(f), - dts_params(f), - dts_ret(f) - )); - } - w.line("/** Releases this wrapper's reference exactly once; later calls are"); - w.line(" * no-ops. Also reachable as `[Symbol.dispose]()` for `using`"); - w.line(" * declarations. A wrapper collected unclosed is released by a"); - w.line(" * finalizer where the runtime provides one. */"); - w.line("close(): void;"); - }); - w.blank(); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-wasm/src/entities.rs b/crates/weaveffi-gen-wasm/src/entities.rs deleted file mode 100644 index 98dbab84..00000000 --- a/crates/weaveffi-gen-wasm/src/entities.rs +++ /dev/null @@ -1,622 +0,0 @@ -//! The JS loader assembler: the `weaveffi_wasm.js` stub with its feature-gated -//! runtime, module-scope enum and error surfaces, loader-scoped codecs, -//! callback vtables, and interface classes, and the nested API object the -//! loader returns. - -use heck::ToUpperCamelCase; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{BindingModel, CallShape, InterfaceBinding, ModuleBinding}; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle, ABI_RUNTIME_SYMBOLS}; - -use crate::calls::{ - async_cb_wasm_params, emit_constructor_call, emit_js_callable, emit_js_callback_vtable, - emit_stage_input, -}; -use crate::codec::{emit_js_buffer_codecs, emit_js_buffer_runtime}; -use crate::runtime::{ - emit_abi_version_check, emit_bytes_helpers, emit_check_err_ref, emit_error_slot_helpers, - emit_foreign_error_helper, emit_iterator_class, emit_js_error_checkers, emit_js_error_classes, - emit_js_error_factories, emit_object_helpers, emit_string_helpers, emit_trampoline_helper, -}; -use crate::types::{emscripten_stub, is_string_type, js_checker_name, js_param_name, JsDecl}; - -/// Every producer entry-point symbol the Emscripten glue calls by name: sync -/// and iterator launchers, iterator `next`/destroy pairs, and interface -/// clone/destroy symbols, deduplicated in first-use order. Async launchers -/// and functions taking callback interfaces are excluded; Emscripten mode -/// stubs them out, and this list exists only to build the Emscripten -/// export-binding table. -pub(crate) fn collect_called_symbols(model: &BindingModel) -> Vec { - fn push_unique(syms: &mut Vec, s: &str) { - if !syms.iter().any(|x| x == s) { - syms.push(s.to_string()); - } - } - let mut syms = Vec::new(); - for m in &model.modules { - for f in m.callables() { - if emscripten_stub(f) { - continue; - } - match &f.shape { - CallShape::Iterator(it) => { - push_unique(&mut syms, &f.c_base); - push_unique(&mut syms, &it.next.symbol); - push_unique(&mut syms, &it.destroy_symbol); - } - CallShape::Async(_) => {} - CallShape::Sync(_) => push_unique(&mut syms, &f.c_base), - } - } - for i in &m.interfaces { - push_unique(&mut syms, &i.clone_symbol); - push_unique(&mut syms, &i.destroy_symbol); - } - } - syms -} - -/// Render the `.js` loader: module-scope runtime helpers gated -/// on the features the API actually uses, exported enum objects and error -/// classes, and the async `load` function that instantiates (or, in -/// Emscripten mode, adopts) the wasm module and returns the nested API -/// object. -pub(crate) fn render_wasm_js_stub( - model: &BindingModel, - module_name: &str, - prefix: &str, - input_basename: &str, - filename: &str, - emscripten: bool, -) -> String { - 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); - - // Interface members marshal like free functions, so every callable counts. - let has_functions = model.modules.iter().any(|m| m.callables().next().is_some()); - // In Emscripten mode async functions and callback interfaces are throwing - // stubs, so none of the trampoline machinery (or its helpers) is emitted. - let has_async = !emscripten && model.has_async(); - let has_callbacks = !emscripten && model.has_callback_interfaces(); - let has_interfaces = model.has_interfaces(); - // Records and rich enums are value types packed and unpacked by the - // loader-scope codecs; any of them (or any buffered type, or an error - // payload) pulls in the buffer writer/reader runtime. - let has_codecs = model - .modules - .iter() - .any(|m| !m.structs.is_empty() || m.enums.iter().any(|e| e.is_rich())); - let needs_buf = model.has_buffers(); - // 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; - let has_domains = model.modules.iter().any(ModuleBinding::declares_error); - // 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 || 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 || model.any_type(&|t| matches!(t, Ty::Bytes)); - let has_iterators = model.has_iterators(); - - out.push_str("// WeaveFFI Wasm bindings (auto-generated)\n"); - out.push_str("//\n"); - if emscripten { - out.push_str("// Boundary conventions for an Emscripten build:\n"); - } else { - out.push_str("// Boundary conventions for a wasm32-unknown-unknown build:\n"); - } - out.push_str("//\n"); - out.push_str("// Objects -> i32 pointer into linear memory (0 = null/absent); one\n"); - out.push_str("// strong reference per wrapper, released by close()\n"); - out.push_str("// Enums -> i32 discriminant value\n"); - out.push_str("// i64/u64 -> JavaScript BigInt\n"); - out.push_str("// Strings -> NUL-terminated UTF-8 (const char*); a single i32 pointer\n"); - out.push_str("// Bytes -> i32 data pointer + i32 length (out_len for returns)\n"); - out.push_str("// Buffered -> records, rich enums, optionals, lists, and maps cross\n"); - out.push_str("// as one value buffer: i32 pointer + i32 length\n"); - out.push_str("// Callbacks -> i32 context key + i32 pointer to a static vtable\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); - } - - if needs_strings { - emit_string_helpers(&mut out); - } - - if needs_bytes { - emit_bytes_helpers(&mut out); - } - - if needs_buf { - emit_js_buffer_runtime(&mut out); - } - - if needs_err { - emit_error_slot_helpers(&mut out); - if has_async { - emit_check_err_ref(&mut out); - } - } - - if has_interfaces { - emit_object_helpers(&mut out); - } - - if has_callbacks { - emit_foreign_error_helper(&mut out); - } - - if has_iterators { - emit_iterator_class(&mut out); - } - - if has_async || has_callbacks { - emit_trampoline_helper(&mut out); - } - - 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 - // discriminant object. - if e.is_rich() { - continue; - } - out.push_str(&format!("export const {} = Object.freeze({{\n", e.name)); - for v in &e.variants { - out.push_str(&format!(" {}: {},\n", v.name, v.value)); - } - out.push_str("});\n\n"); - } - } - - out.push_str("/**\n"); - if emscripten { - out.push_str(" * Load a WeaveFFI API from a pre-initialized Emscripten module.\n"); - 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 model.modules.is_empty() { - out.push_str(" * @returns {Promise} The Emscripten module.\n"); - } else { - out.push_str(" * @returns {Promise} The API bindings.\n"); - } - } else { - out.push_str(" * Load a WeaveFFI Wasm module.\n"); - out.push_str(" *\n"); - out.push_str(" * @param {string|URL|BufferSource|WebAssembly.Module} source - A URL to\n"); - out.push_str(" * fetch the `.wasm` from, its bytes, or an already compiled module\n"); - out.push_str(" * (use the bytes form where `fetch` cannot read files, e.g. Node).\n"); - if model.modules.is_empty() { - out.push_str( - " * @returns {Promise} The exported Wasm functions.\n", - ); - } else { - out.push_str(" * @returns {Promise} The API bindings.\n"); - } - } - out.push_str(" *\n"); - out.push_str(" * Exported functions follow the C ABI naming convention:\n"); - out.push_str(&format!( - " * {prefix}_{{module}}_{{function}}(params...) -> result\n" - )); - out.push_str(" *\n"); - out.push_str(" * @example\n"); - if emscripten { - out.push_str(" * import Module from './your_library.js';\n"); - out.push_str(&format!(" * const api = await {load_fn}(Module());\n")); - } else { - out.push_str(&format!(" * const api = await {load_fn}('lib.wasm');\n")); - } - out.push_str(" *\n"); - out.push_str(" * // Primitive: plain numbers in, number out.\n"); - out.push_str(" * const sum = api.math.add(1, 2);\n"); - out.push_str(" *\n"); - out.push_str(" * // Record: plain objects in and out (serialized automatically).\n"); - out.push_str(" * const person = api.contacts.create({ name: 'Ada', age: 36 });\n"); - out.push_str(" *\n"); - out.push_str(" * // Object: a class wrapping one producer reference; close() releases it\n"); - out.push_str(" * // (or let `using` do it). Passing it lends the reference for the call.\n"); - out.push_str(" * const store = new api.kv.Store();\n"); - out.push_str(" * api.kv.inspect(store);\n"); - out.push_str(" * store.close();\n"); - out.push_str(" *\n"); - out.push_str(" * // Enum: pass the integer discriminant.\n"); - out.push_str(" * api.ui.set_color(0); // 0 = first variant\n"); - out.push_str(" *\n"); - out.push_str(" * // Optional: pass null to omit, a value to provide.\n"); - out.push_str(" * api.config.set_timeout(5000); // present\n"); - out.push_str(" * api.config.set_timeout(null); // absent\n"); - out.push_str(" *\n"); - out.push_str(" * // List/Map: pass arrays/objects; receive arrays/objects.\n"); - out.push_str(" * const names = api.data.all_names();\n"); - out.push_str(" */\n"); - if emscripten { - out.push_str(&format!("export async function {load_fn}(module) {{\n")); - out.push_str(" const m = await Promise.resolve(module);\n"); - } else { - out.push_str(&format!("export async function {load_fn}(source) {{\n")); - out.push_str(" let module;\n"); - out.push_str(" if (source instanceof WebAssembly.Module) {\n"); - out.push_str(" module = source;\n"); - out.push_str( - " } else if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {\n", - ); - out.push_str(" module = await WebAssembly.compile(source);\n"); - out.push_str(" } else {\n"); - out.push_str(" const response = await fetch(source);\n"); - out.push_str(" module = await WebAssembly.compile(await response.arrayBuffer());\n"); - out.push_str(" }\n"); - out.push_str(" const instance = await WebAssembly.instantiate(module, {});\n"); - } - - if model.modules.is_empty() { - if emscripten { - out.push_str(" return m;\n"); - } else { - out.push_str(" return instance.exports;\n"); - } - } else { - if emscripten { - emit_emscripten_bindings( - &mut out, - model, - prefix, - needs_strings, - needs_bytes, - needs_err, - ); - } else { - out.push_str(" const wasm = instance.exports;\n\n"); - } - out.push_str(" _checkAbiVersion(wasm);\n\n"); - - // The per-domain factories and checkers, and the value codecs, live in - // the loader because object tokens resolve to the loader-scoped - // interface classes below. - if needs_err && has_domains { - emit_js_error_factories(&mut out, model, " "); - emit_js_error_checkers(&mut out, model, " "); - } - if has_codecs { - emit_js_buffer_codecs(&mut out, model, " "); - } - - if has_async || has_callbacks { - out.push_str(" const _table = wasm.__indirect_function_table;\n\n"); - } - - if has_async { - emit_async_machinery(&mut out, model); - } - - if has_callbacks { - out.push_str(" // Callback-interface implementations, keyed by the integer context\n"); - out.push_str(" // the loader passes as `ctx`; an entry lives until the producer\n"); - out.push_str(" // calls the vtable's `free(ctx)`.\n"); - out.push_str(" let _nextCbId = 1;\n"); - out.push_str(" const _callbacks = new Map();\n\n"); - for (module, cb) in model.callback_interfaces() { - emit_js_callback_vtable(&mut out, module, cb, prefix, " "); - } - } - - // Interface classes close over the loaded `wasm` instance (and the - // async machinery above), so they live inside the loader rather than - // at module scope like the error classes. - for module in &model.modules { - for i in &module.interfaces { - emit_interface_class(&mut out, module, i, " ", prefix, emscripten); - } - } - - out.push_str(" return {\n"); - out.push_str(" _raw: wasm,\n"); - for module in model.roots() { - render_js_module_object(&mut out, model, module, " ", prefix, emscripten); - } - out.push_str(" };\n"); - } - - out.push_str("}\n\n"); - out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); - out -} - -/// Emit the Emscripten export-binding table: the underscore-prefixed exports -/// bound once, up front, to the exact symbol names the glue calls. Module -/// access stays in quoted bracket notation so Closure Compiler's advanced -/// property renaming cannot break it, while the rest of the glue keeps -/// consistent dot access on this locally constructed object. -/// -/// Runtime symbols listed in [`ABI_RUNTIME_SYMBOLS`] keep their `weaveffi_` -/// spelling in the producer (the C header aliases a custom prefix onto them), -/// while `alloc`/`dealloc` are declared under the configured prefix. -fn emit_emscripten_bindings( - out: &mut String, - model: &BindingModel, - prefix: &str, - needs_strings: bool, - needs_bytes: bool, - needs_err: bool, -) { - let runtime = |sym: &str| -> (String, String) { - let export = if ABI_RUNTIME_SYMBOLS.contains(&sym) { - format!("weaveffi_{sym}") - } else { - format!("{prefix}_{sym}") - }; - (format!("weaveffi_{sym}"), export) - }; - let mut bindings: Vec<(String, String)> = - vec![runtime("abi_version"), runtime("alloc"), runtime("dealloc")]; - if needs_strings { - bindings.push(runtime("free_string")); - } - if needs_bytes { - bindings.push(runtime("free_bytes")); - } - if needs_err { - bindings.push(runtime("error_clear")); - } - bindings.extend( - collect_called_symbols(model) - .into_iter() - .map(|s| (s.clone(), s)), - ); - out.push_str(" // Bind the underscore-prefixed Emscripten exports to the symbol\n"); - out.push_str(" // names the glue above calls. Quoted bracket access keeps the\n"); - out.push_str(" // bindings safe under Closure Compiler's property renaming.\n"); - out.push_str(" const wasm = {\n"); - out.push_str(" // Emscripten replaces HEAPU8 when linear memory grows, so the\n"); - out.push_str(" // buffer is re-read on every access instead of captured once.\n"); - out.push_str(" get memory() { return { buffer: m['HEAPU8'].buffer }; },\n"); - for (name, export) in &bindings { - out.push_str(&format!(" {name}: m['_{export}'],\n")); - } - out.push_str(" };\n\n"); -} - -/// Emit the loader-scoped async machinery: the context table, the shared -/// completion handler, and one trampoline per distinct completion-callback -/// signature (deduplicated by wasm slot types, so every async function with -/// the same result shape shares a table entry). -fn emit_async_machinery(out: &mut String, model: &BindingModel) { - out.push_str(" let _nextCtxId = 1;\n"); - out.push_str(" const _asyncContexts = new Map();\n\n"); - out.push_str(" function _asyncHandler(ctxId, errPtr, ...results) {\n"); - out.push_str(" const ctx = _asyncContexts.get(ctxId);\n"); - out.push_str(" if (!ctx) return;\n"); - out.push_str(" _asyncContexts.delete(ctxId);\n"); - out.push_str(" try {\n"); - out.push_str(" if (errPtr !== 0) _checkErrRef(wasm, errPtr, ctx.mkErr);\n"); - out.push_str(" ctx.resolve(ctx.unwrap ? ctx.unwrap(wasm, ...results) : results[0]);\n"); - out.push_str(" } catch (e) {\n"); - out.push_str(" ctx.reject(e);\n"); - out.push_str(" }\n"); - out.push_str(" }\n\n"); - - let mut trampolines: Vec<(String, Vec<&'static str>)> = Vec::new(); - for (_, f) in model.callables() { - if let CallShape::Async(ab) = &f.shape { - let params = async_cb_wasm_params(ab); - let key = params.join("_"); - if !trampolines.iter().any(|(k, _)| k == &key) { - trampolines.push((key, params)); - } - } - } - for (sig_key, params) in &trampolines { - let params_js: Vec = params.iter().map(|p| format!("'{p}'")).collect(); - out.push_str(&format!( - " const _cbPtr_{sig_key} = _registerTrampoline(_table, [{}], [], _asyncHandler);\n", - params_js.join(", ") - )); - } - out.push('\n'); -} - -/// Whether a module subtree exposes anything at runtime (functions or -/// interface classes), so empty namespace objects are not emitted. Records, -/// rich enums, and callback interfaces contribute nothing here: they are -/// plain value shapes (or consumer-implemented shapes) with no runtime -/// members. -fn module_tree_has_content(model: &BindingModel, mb: &ModuleBinding) -> bool { - !mb.functions.is_empty() - || !mb.interfaces.is_empty() - || model - .children(mb) - .any(|sub| module_tree_has_content(model, sub)) -} - -/// Emit one module's namespace object (`math: { ... },`) inside the returned -/// API literal: free-function wrappers (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. -fn render_js_module_object( - out: &mut String, - model: &BindingModel, - mb: &ModuleBinding, - indent: &str, - prefix: &str, - emscripten: bool, -) { - if !module_tree_has_content(model, mb) { - return; - } - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - w.block(format!("{}: {{", mb.name), "},", |w| { - let inner = w.indent_str(); - for f in &mb.functions { - let mut tmp = String::new(); - emit_js_callable( - &mut tmp, - mb, - f, - JsDecl::Object, - None, - &inner, - prefix, - emscripten, - ); - w.raw(tmp); - } - // The interface class itself is exposed on the module object, so - // factories, statics, and `instanceof` checks all reach it. - for i in &mb.interfaces { - w.line(format!("{}: {},", i.name, i.name)); - } - for sub in model.children(mb) { - let mut tmp = String::new(); - render_js_module_object(&mut tmp, model, sub, &inner, prefix, emscripten); - w.raw(tmp); - } - }); - out.push_str(&w.finish()); -} - -/// Emit the loader-scoped `class` for an interface: a wrapper holding one -/// strong reference (the producer-side reference count) and closing over the -/// loaded `wasm` instance. The canonical `new` constructor maps to -/// `constructor`; other constructors and statics are static methods; methods -/// lend `this._handle` as the implicit leading `self` argument. The internal -/// `_wrap(handle)` adopts an owned reference (returns, async results, -/// iterator elements, decoded object tokens, callback arguments) without -/// invoking the constructor; `_clone()` produces a second reference through -/// the clone symbol for object tokens; `close()` and `Symbol.dispose` release -/// the reference exactly once, with a `FinalizationRegistry` backstop for -/// wrappers that are collected unclosed. -fn emit_interface_class( - out: &mut String, - module: &ModuleBinding, - i: &InterfaceBinding, - indent: &str, - prefix: &str, - emscripten: bool, -) { - let cls = &i.name; - let error = module.error.as_ref(); - let destroy = format!("wasm.{}", i.destroy_symbol); - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - if let Some(doc) = i.doc.as_deref().map(str::trim).filter(|d| !d.is_empty()) { - for line in doc.lines() { - w.line(format!("// {line}")); - } - } - w.block(format!("class {cls} {{"), "}", |w| { - let inner = w.indent_str(); - - // Canonical constructor: `new(...)` becomes `constructor(...)`, - // adopting the returned reference rather than returning a wrapped - // value. - if let Some(c) = i.constructors.iter().find(|c| c.name == "new") { - let body = format!("{inner} "); - let js_params: Vec = c.params.iter().map(js_param_name).collect(); - let checker = js_checker_name(c, error); - w.block( - format!("constructor({}) {{", js_params.join(", ")), - "}", - |w| { - let mut staged = String::new(); - let mut args = Vec::new(); - let mut cleanup = Vec::new(); - for (idx, p) in c.params.iter().enumerate() { - emit_stage_input( - &mut staged, - &body, - p, - &format!("a{idx}"), - &module.path, - &mut args, - &mut cleanup, - ); - } - w.raw(staged); - let mut call = String::new(); - emit_constructor_call( - &mut call, &body, &c.c_base, &args, &cleanup, &checker, &destroy, - ); - w.raw(call); - }, - ); - } - - // Internal: adopt one strong reference (a return, an async result, - // an iterator element, a decoded object token, a callback argument) - // without running the constructor. - w.block("static _wrap(handle) {", "}", |w| { - w.line(format!( - "return _adopt(Object.create({cls}.prototype), handle, {destroy});" - )); - }); - - // Internal: a second strong reference to the same object, consumed by - // the value-buffer writer when this object is encoded as a token. - w.block("_clone() {", "}", |w| { - w.line(format!("return wasm.{}(_borrow(this));", i.clone_symbol)); - }); - - // Explicit cleanup: release this wrapper's reference exactly once. - w.block("close() {", "}", |w| { - w.line("_release(this);"); - }); - w.block("[_dispose]() {", "}", |w| { - w.line("_release(this);"); - }); - - for c in i.constructors.iter().filter(|c| c.name != "new") { - let mut tmp = String::new(); - emit_js_callable( - &mut tmp, - module, - c, - JsDecl::Static, - None, - &inner, - prefix, - emscripten, - ); - w.raw(tmp); - } - for m in &i.methods { - let mut tmp = String::new(); - emit_js_callable( - &mut tmp, - module, - m, - JsDecl::Method, - Some("_borrow(this)"), - &inner, - prefix, - emscripten, - ); - w.raw(tmp); - } - for s in &i.statics { - let mut tmp = String::new(); - emit_js_callable( - &mut tmp, - module, - s, - JsDecl::Static, - None, - &inner, - prefix, - emscripten, - ); - w.raw(tmp); - } - }); - w.blank(); - out.push_str(&w.finish()); -} diff --git a/crates/weaveffi-gen-wasm/src/lib.rs b/crates/weaveffi-gen-wasm/src/lib.rs deleted file mode 100644 index 8257f6c6..00000000 --- a/crates/weaveffi-gen-wasm/src/lib.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! WebAssembly binding generator for WeaveFFI. -//! -//! Emits a JavaScript loader stub and TypeScript declarations targeting a -//! `wasm32-unknown-unknown` cdylib build of the same Rust source (or, in -//! Emscripten mode, a pre-initialized Emscripten module), speaking ABI -//! revision 2: reference-counted object wrappers with `close()`, -//! `Symbol.dispose`, and a `FinalizationRegistry` backstop; nullable objects -//! as `Wrapper | null`; object tokens inside value buffers; lazy iterators; -//! `Promise`-returning async functions; and callback interfaces implemented -//! as static vtables of function-table trampolines. Implements -//! [`LanguageBackend`]; the shared driver bridges it into the generator -//! pipeline, and [`LanguageBackend::package`] assembles the npm layout. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -mod calls; -mod codec; -mod docs; -mod dts; -mod entities; -mod package; -mod runtime; -mod types; - -use camino::Utf8Path; -use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::{PackageContext, PackagedFile}; -use weaveffi_core::pkg; -use weaveffi_core::platform::Platform; -use weaveffi_core::resolved::ResolvedApi; - -use crate::dts::render_wasm_dts; -use crate::entities::render_wasm_js_stub; -use crate::package::{ - render_packaged_readme, render_wasm_package_json, render_wasm_readme, PackagedBinary, -}; - -/// WebAssembly backend: emits a JavaScript loader stub and TypeScript -/// declarations targeting a `wasm32-unknown-unknown` cdylib build of the same -/// Rust source. -pub struct WasmGenerator; - -const DEFAULT_MODULE_NAME: &str = "weaveffi_wasm"; - -/// Per-target configuration for [`WasmGenerator`]. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct WasmConfig { - /// Module name used for the emitted `.js` loader and - /// `.d.ts` (default `"weaveffi_wasm"`). - pub module_name: Option, - /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally - /// via `[global] c_prefix`; honored so the wasm glue calls the same - /// exported symbols the producer emits. - pub prefix: Option, - /// Target an Emscripten build instead of a bare `wasm32-unknown-unknown` - /// one. The loader then accepts a pre-initialized Emscripten `Module` - /// object (or the promise returned by its `MODULARIZE` factory) instead - /// of a `.wasm` source, and binds the module's underscore-prefixed - /// exports to the symbol names the glue calls. Async functions and - /// callback interfaces are not supported in this mode (see - /// [`allow_unsupported`](Self::allow_unsupported)); each affected entry - /// point becomes an explicit stub that throws at call time and is omitted - /// from the TypeScript declarations. - pub emscripten: bool, - /// Generate in Emscripten mode even when the API uses async functions or - /// callback interfaces, which that mode cannot support. The orchestrator - /// downgrades the capability failure to a warning and the unsupported - /// entry points are emitted as explicit throwing stubs. Has no effect - /// outside Emscripten mode, where every feature is supported. - pub allow_unsupported: bool, - /// Basename of the IDL the CLI was invoked with. - #[serde(skip)] - pub input_basename: Option, -} - -impl WasmConfig { - /// Returns the configured module name used for the emitted `.js` - /// loader and `.d.ts`, falling back to `"weaveffi_wasm"`. - pub fn module_name(&self) -> &str { - self.module_name.as_deref().unwrap_or(DEFAULT_MODULE_NAME) - } - - /// Returns the configured C ABI symbol prefix, falling back to - /// `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. - pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") - } -} - -impl LanguageBackend for WasmGenerator { - type Config = WasmConfig; - - fn name(&self) -> &'static str { - "wasm" - } - - /// Every gated feature is supported on `wasm32-unknown-unknown`. Callback - /// interfaces and async completions share one mechanism: the loader - /// installs long-lived trampolines in the wasm function table (one per - /// callback method, built into a static vtable in linear memory; one per - /// async result shape) so the producer dispatches straight back into - /// JavaScript. Because the target is single-threaded, delivery is always - /// synchronous: a callback runs only while a call into the module is on - /// the stack (a producer that calls back from a spawned thread cannot run - /// on this target at all). Emscripten mode exposes neither - /// `WebAssembly.Function` nor a growable function table portably, so - /// there async functions and callback interfaces are unsupported and - /// emitted as explicit throwing stubs; see [`WasmConfig::emscripten`]. - fn capabilities(&self, config: &Self::Config) -> TargetCapabilities { - if config.emscripten { - TargetCapabilities { - async_functions: false, - callback_interfaces: false, - iterators: true, - } - } else { - TargetCapabilities::full() - } - } - - fn allows_unsupported(&self, config: &Self::Config) -> bool { - config.allow_unsupported - } - - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - - fn files( - &self, - api: &ResolvedApi, - model: &BindingModel, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Vec { - let wasm_dir = out_dir.join("wasm"); - let module_name = config.module_name(); - let prefix = config.prefix(); - let input_basename = config.input_basename(); - let js_filename = format!("{module_name}.js"); - let dts_filename = format!("{module_name}.d.ts"); - let package = pkg::resolve(api, None, config.input_basename.as_deref()); - vec![ - OutputFile::new( - wasm_dir.join("README.md"), - render_wasm_readme(api, model, prefix, input_basename, config.emscripten), - ), - OutputFile::new( - wasm_dir.join("package.json"), - render_wasm_package_json( - &package, - &js_filename, - &dts_filename, - None, - input_basename, - ), - ), - OutputFile::new( - wasm_dir.join(&js_filename), - render_wasm_js_stub( - model, - module_name, - prefix, - input_basename, - &js_filename, - config.emscripten, - ), - ), - OutputFile::new( - wasm_dir.join(&dts_filename), - render_wasm_dts( - model, - module_name, - input_basename, - &dts_filename, - config.emscripten, - ), - ), - ] - } - - /// The npm package layout under `wasm/`: `package.json` (with the `.wasm` - /// listed in `files` when bundled), the loader, its declarations, a - /// README, and the prebuilt `wasm32-unknown-unknown` binary copied in as - /// `.wasm`. The package is a single pure-JS-plus-wasm artifact, - /// so there is no per-platform split. When no wasm32 binary was supplied - /// (or in Emscripten mode, where the consumer builds their own Emscripten - /// module) the package ships without a binary and the README says so. - fn package( - &self, - api: &ResolvedApi, - model: &BindingModel, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &Self::Config, - ) -> Option> { - let wasm_dir = out_dir.join("wasm"); - let module_name = config.module_name(); - let prefix = config.prefix(); - let input_basename = config.input_basename(); - let js_filename = format!("{module_name}.js"); - let dts_filename = format!("{module_name}.d.ts"); - let package = pkg::resolve(api, None, config.input_basename.as_deref()); - - // Emscripten mode ships glue only (the consumer links the module into - // its own Emscripten build). Otherwise the package exists to carry the - // `.wasm`, so with no wasm32 binary there is nothing to package. - let binary = if config.emscripten { - None - } else { - let b = ctx.binaries.get(Platform::Wasm32)?; - Some(PackagedBinary { - filename: ctx.binaries.bundled_filename(Platform::Wasm32), - source: b.source.clone(), - }) - }; - - let mut files = vec![ - PackagedFile::text( - wasm_dir.join("package.json"), - render_wasm_package_json( - &package, - &js_filename, - &dts_filename, - binary.as_ref().map(|b| b.filename.as_str()), - input_basename, - ), - ), - PackagedFile::text( - wasm_dir.join(&js_filename), - render_wasm_js_stub( - model, - module_name, - prefix, - input_basename, - &js_filename, - config.emscripten, - ), - ), - PackagedFile::text( - wasm_dir.join(&dts_filename), - render_wasm_dts( - model, - module_name, - input_basename, - &dts_filename, - config.emscripten, - ), - ), - PackagedFile::text( - wasm_dir.join("README.md"), - render_packaged_readme( - &package, - module_name, - &js_filename, - binary.as_ref(), - input_basename, - config.emscripten, - ), - ), - ]; - if let Some(b) = &binary { - files.push(PackagedFile::copy( - wasm_dir.join(&b.filename), - b.source.clone(), - )); - } - Some(files) - } -} diff --git a/crates/weaveffi-gen-wasm/src/package.rs b/crates/weaveffi-gen-wasm/src/package.rs deleted file mode 100644 index b0f92f50..00000000 --- a/crates/weaveffi-gen-wasm/src/package.rs +++ /dev/null @@ -1,682 +0,0 @@ -//! Packaging output: the `package.json` manifest, the generated-source README -//! with its boundary-convention notes and API reference, and the packaged -//! README describing the bundled artifact. -//! -//! The manifest is built with [`manifest::JsonObject`] -//! (`weaveffi_core::manifest`) so quoting and escaping are centralized; user -//! strings (name, description, license, author, homepage) can't corrupt the -//! JSON. - -use std::collections::HashMap; - -use camino::Utf8PathBuf; -use weaveffi_core::cache::CLI_VERSION; -use weaveffi_core::errors::ERROR_BRAND; -use weaveffi_core::manifest::{JsonObject, JsonValue}; -use weaveffi_core::model::{ - BindingModel, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, - ModuleBinding, StructBinding, -}; -use weaveffi_core::pkg::ResolvedPackage; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; - -use crate::types::{ - js_cb_method_name, js_code_class_name, ts_type_for, type_display, wasm_type, wasm_type_note, -}; - -/// The prebuilt `wasm32-unknown-unknown` binary a package bundles: the -/// filename it's shipped under (next to the loader) and where to copy it from. -pub(crate) struct PackagedBinary { - /// The bundled filename, `.wasm`. - pub(crate) filename: String, - /// The on-disk source the binary is copied from. - pub(crate) source: Utf8PathBuf, -} - -/// Prepend the generated-file prelude entries (`//`, `//warning`, -/// `//regenerate`) npm tolerates at the top of `package.json`. Mirrors -/// [`render_json_prelude`](weaveffi_core::utils::render_json_prelude), whose -/// version constant ([`CLI_VERSION`]) it shares. -fn json_prelude_entries(obj: JsonObject, input_basename: &str) -> JsonObject { - obj.str_entry( - "//", - format!("Generated by WeaveFFI {CLI_VERSION} from {input_basename}"), - ) - .str_entry( - "//warning", - "DO NOT EDIT. Your changes will be overwritten.", - ) - .str_entry( - "//regenerate", - format!("To regenerate: weaveffi generate {input_basename} -o "), - ) -} - -/// Render the `package.json` manifest: prelude comment entries, npm metadata -/// from the resolved package (optional fields only when present), the fixed -/// ES-module entry points for the loader and its declarations, and, when a -/// binary is bundled, an explicit `files` list so npm publishes exactly the -/// loader, declarations, and `.wasm` (the README is always included by npm). -pub(crate) fn render_wasm_package_json( - package: &ResolvedPackage, - js_filename: &str, - dts_filename: &str, - wasm_filename: Option<&str>, - input_basename: &str, -) -> String { - let mut obj = json_prelude_entries(JsonObject::new(), input_basename) - .str_entry("name", &package.name) - .str_entry("version", &package.version) - .str_entry("description", package.description_or_default()) - .opt_str_entry("license", package.license.as_deref()); - if let Some(author) = package.authors.first() { - obj = obj.str_entry("author", author); - } - obj = obj - .opt_str_entry("homepage", package.homepage.as_deref()) - .str_entry("type", "module") - .str_entry("main", js_filename) - .str_entry("types", dts_filename); - if let Some(wasm) = wasm_filename { - obj = obj.entry( - "files", - JsonValue::Array( - [js_filename, dts_filename, wasm] - .into_iter() - .map(JsonValue::str) - .collect(), - ), - ); - } - obj.render() -} - -/// README for a packaged Wasm artifact: what the package contains, how to -/// load it from the bundled `.wasm` (or, without one, where the binary has to -/// come from), and the lifetime rules a consumer must follow. -pub(crate) fn render_packaged_readme( - package: &ResolvedPackage, - module_name: &str, - js_filename: &str, - binary: Option<&PackagedBinary>, - input_basename: &str, - emscripten: bool, -) -> String { - use heck::ToUpperCamelCase; - let load_fn = format!("load{}", module_name.to_upper_camel_case()); - let name = &package.name; - let mut out = render_prelude(CommentStyle::Xml, input_basename); - out.push_str(&format!("# {name} (WebAssembly)\n\n")); - if emscripten { - out.push_str( - "Auto-generated JavaScript glue and TypeScript declarations for an Emscripten \ - build of this library. The package carries no binary: build the Emscripten \ - module yourself (so you control options like `locateFile`) and hand it to \ - the loader.\n\n", - ); - out.push_str("```js\n"); - out.push_str("import Module from './your_library.js';\n"); - out.push_str(&format!("import {{ {load_fn} }} from '{name}';\n\n")); - out.push_str(&format!("const api = await {load_fn}(Module());\n")); - out.push_str("```\n\n"); - out.push_str( - "Async functions and callback interfaces are not supported in Emscripten \ - mode: each affected entry point throws at call time and is omitted from the \ - TypeScript declarations.\n\n", - ); - } else if let Some(b) = binary { - out.push_str(&format!( - "Auto-generated JavaScript loader and TypeScript declarations with the \ - prebuilt `wasm32-unknown-unknown` module bundled as `{}`. No toolchain is \ - needed: fetch the bundled file (or read it in Node) and hand it to the \ - loader.\n\n", - b.filename - )); - out.push_str("```js\n"); - out.push_str(&format!("import {{ {load_fn} }} from '{name}';\n\n")); - out.push_str("// Browsers and bundlers: resolve the bundled file's URL.\n"); - out.push_str(&format!( - "const api = await {load_fn}(new URL('./{}', import.meta.url));\n\n", - b.filename - )); - out.push_str("// Node: pass the bytes instead of fetching.\n"); - out.push_str("import { readFile } from 'node:fs/promises';\n"); - out.push_str(&format!( - "const api2 = await {load_fn}(await readFile(new URL('./{}', import.meta.url)));\n", - b.filename - )); - out.push_str("```\n\n"); - } else { - out.push_str( - "Auto-generated JavaScript loader and TypeScript declarations for a \ - `wasm32-unknown-unknown` build of this library. No `wasm32` binary was \ - supplied when this package was assembled, so it ships without one: build \ - the library with `cargo build --target wasm32-unknown-unknown --release` \ - and pass the resulting `.wasm` (a URL, its bytes, or a compiled \ - `WebAssembly.Module`) to the loader.\n\n", - ); - out.push_str("```js\n"); - out.push_str(&format!("import {{ {load_fn} }} from '{name}';\n\n")); - out.push_str(&format!( - "const api = await {load_fn}('your_library.wasm');\n" - )); - out.push_str("```\n\n"); - } - out.push_str(&format!( - "`{js_filename}` is an ES module; the package is `\"type\": \"module\"`.\n\n" - )); - out.push_str("## Object lifetimes\n\n"); - out.push_str( - "Interface values are wrapper classes holding one reference to a producer \ - object. Call `close()` (or bind the wrapper with `using`, which calls \ - `Symbol.dispose`) when you're done; a wrapper that is garbage collected \ - unclosed is released by a `FinalizationRegistry` backstop where the runtime \ - provides one. Passing a wrapper to a function lends its reference for the \ - duration of the call; a wrapper returned to you is yours to close.\n\n", - ); - out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); - out -} - -/// Render the `README.md`: build and load instructions for the configured -/// mode, the boundary conventions for every complex type family, the error -/// contract, and (for a non-empty API) the generated ABI-level reference. -pub(crate) fn render_wasm_readme( - api: &Api, - model: &BindingModel, - prefix: &str, - input_basename: &str, - emscripten: bool, -) -> String { - let mut out = render_prelude(CommentStyle::Xml, input_basename); - out.push_str("# WeaveFFI Wasm (experimental)\n\n"); - if emscripten { - out.push_str("This folder contains a minimal stub to help you load an Emscripten build of your WeaveFFI library.\n\n"); - out.push_str("Build (example):\n\n"); - out.push_str("```bash\n"); - out.push_str("emcc your_library.c -o your_library.js \\\n"); - out.push_str(" -sMODULARIZE=1 -sEXPORT_ES6=1 \\\n"); - out.push_str(" -sEXPORTED_RUNTIME_METHODS=HEAPU8 \\\n"); - out.push_str(" -sALLOW_MEMORY_GROWTH=1\n"); - out.push_str("```\n\n"); - out.push_str(&format!( - "The `{prefix}_*` symbols are kept alive and exported automatically: the \ - generated header tags them with `{}_API`, which expands to \ - `__attribute__((used, visibility(\"default\")))` under Emscripten.\n\n", - prefix.to_uppercase() - )); - out.push_str("Then construct the Emscripten module yourself (so you control options like `locateFile`) and pass it to the loader:\n\n"); - out.push_str("```js\n"); - out.push_str("import Module from './your_library.js';\n"); - out.push_str("import { loadWeaveffiWasm } from './weaveffi_wasm.js';\n\n"); - out.push_str("const api = await loadWeaveffiWasm(Module());\n"); - out.push_str("```\n\n"); - if model.has_async() { - out.push_str("## Async Functions\n\n"); - out.push_str( - "Async functions are not supported in Emscripten mode. Each one is \ - generated as an explicit stub that throws at call time and is omitted \ - from the TypeScript declarations. Use the standard \ - `wasm32-unknown-unknown` loader or a native target when you need \ - them.\n\n", - ); - } - if model.has_callback_interfaces() { - out.push_str("## Callback Interfaces\n\n"); - out.push_str( - "Callback interfaces are not supported in Emscripten mode: their \ - trampolines rely on `WebAssembly.Function` and a growable \ - `__indirect_function_table`, neither of which an Emscripten module \ - exposes portably. Each function taking a callback interface is \ - generated as an explicit stub that throws at call time and is omitted \ - from the TypeScript declarations, along with the interface shapes \ - themselves. Use the standard `wasm32-unknown-unknown` loader or a \ - native target when you need them.\n\n", - ); - } - } else { - out.push_str("This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library.\n\n"); - out.push_str("Build (example):\n\n"); - out.push_str("```bash\n"); - out.push_str("cargo build --target wasm32-unknown-unknown --release\n"); - out.push_str("```\n\n"); - out.push_str( - "Then serve the `.wasm` and use `weaveffi_wasm.js` to load it: the loader \ - accepts a URL to fetch, the module's bytes (for Node, where `fetch` cannot \ - read files), or an already compiled `WebAssembly.Module`.\n\n", - ); - } - out.push_str("## Complex Type Handling\n\n"); - out.push_str("Wasm only supports numeric types natively (`i32`, `i64`, `f32`, `f64`). "); - out.push_str("Complex types are encoded at the boundary as follows:\n\n"); - out.push_str("### Records\n\n"); - out.push_str("Records are **plain JavaScript objects**. A record crosses the boundary "); - out.push_str("serialized in the WeaveFFI value-buffer format as one pointer + length "); - out.push_str("pair in linear memory; the glue packs and unpacks it automatically, so "); - out.push_str("there are no handles, no accessor functions, and nothing to free.\n\n"); - out.push_str("### Enums\n\n"); - out.push_str("C-style enums are passed as **`i32` values** corresponding to the variant's integer discriminant. "); - out.push_str("Rich (algebraic) enums are plain objects tagged by variant name "); - out.push_str("(`{ tag: \"Circle\", radius: 2 }`) and cross the boundary serialized in a "); - out.push_str("value buffer, exactly like records.\n\n"); - out.push_str("### Objects\n\n"); - out.push_str("Interfaces are **wrapper classes** holding one reference to a producer "); - out.push_str("object (an `i32` pointer into linear memory). A wrapper you receive is "); - out.push_str("yours: release it with `close()` (or bind it with `using`, which calls "); - out.push_str("`Symbol.dispose`); a wrapper collected unclosed is released by a "); - out.push_str("`FinalizationRegistry` backstop where the runtime provides one, and "); - out.push_str("destroy runs exactly once either way. Passing a wrapper to a function lends "); - out.push_str("its reference for the duration of the call. Inside a value buffer an object "); - out.push_str("crosses as a `u64` token carrying one reference: the writer clones the "); - out.push_str("wrapper's reference and the reader adopts the token into a new wrapper.\n\n"); - out.push_str("### 64-bit Integers\n\n"); - out.push_str("`i64` and `u64` are JavaScript **`bigint`** values everywhere: as arguments, "); - out.push_str("returns, record fields, and callback parameters. Map keys of these types "); - out.push_str("become string property names on the decoded object. A `number` is accepted "); - out.push_str("where a `bigint` is expected (it's widened with `BigInt(v)`, so a fractional "); - out.push_str("value throws a `RangeError`). Values crossing by value follow the wasm "); - out.push_str("calling convention: an out-of-range `bigint` wraps modulo 2^64 rather than "); - out.push_str("throwing, and unsigned results are read back as unsigned (`u64` via "); - out.push_str("`BigInt.asUintN`, `u32` via `>>> 0`).\n\n"); - out.push_str("### Optionals\n\n"); - out.push_str("Optional values map to **`null`** for the absent case. An optional crosses "); - out.push_str("the boundary inside a value buffer as a one-byte presence flag followed by "); - out.push_str("the value when present. The one exception is an optional interface, which "); - out.push_str( - "stays a nullable object pointer (`0` = absent) and maps to `Wrapper | null`.\n\n", - ); - out.push_str("### Lists and Maps\n\n"); - out.push_str("Lists are JS arrays; maps are plain objects (a `Map` instance is also "); - out.push_str("accepted on input). Both cross the boundary serialized in a value buffer "); - out.push_str("as one pointer + length pair, recursing through element types.\n\n"); - out.push_str("### Iterators\n\n"); - out.push_str("`iter` functions return a **lazy JS iterator** (typed "); - out.push_str("`IterableIterator`): each `next()` issues exactly one producer call, so "); - out.push_str("iteration streams in constant memory. The producer handle is destroyed "); - out.push_str("exactly once, on exhaustion or via `return()` when iteration stops early "); - out.push_str("(a `for...of` loop calls `return()` automatically on `break` or `throw`). "); - out.push_str("Abandoning an iterator without exhausting or closing it leaks the handle.\n"); - if !emscripten && model.has_async() { - out.push_str("\n### Async Functions\n\n"); - out.push_str( - "`async` functions return a **`Promise`**. `wasm32-unknown-unknown` has no \ - threads, so the producer's default spawner drives each future to completion \ - inline, inside the launch call: the completion callback fires (through a \ - function-table trampoline) before the wrapper returns, and the Promise is \ - already settled when you `await` it. There is nothing to pump or poll from \ - JS. A future that returns `Pending` can't be woken on this target with the \ - default spawner; a producer whose futures wait on external events needs its \ - own spawner (`weaveffi::set_spawner`), and driving that executor is then the \ - producer's responsibility, not the glue's. A typed error rejects the Promise \ - with the same class the synchronous wrapper would throw.\n", - ); - } - if !emscripten && model.has_callback_interfaces() { - out.push_str("\n### Callback Interfaces\n\n"); - out.push_str( - "A callback interface is a TypeScript `interface`; pass any object with the \ - matching methods where a parameter of that type is expected. The loader \ - keeps one **static vtable per interface** in linear memory, filled with \ - function-table indices of long-lived trampolines, and passes a small \ - integer context key with it; the producer calls `free(ctx)` when it drops \ - the callback, which releases your object. Delivery is **synchronous and \ - same-thread**: `wasm32-unknown-unknown` is single-threaded, so a method \ - runs only while a call into the module is on the stack. A producer that \ - calls back from a spawned thread cannot run on this target at all.\n\n", - ); - out.push_str( - "Method arguments that are strings, bytes, or buffered values are \ - **borrowed for the duration of the call**: they're copied or decoded into \ - JS values before your method runs. Object arguments transfer one reference \ - to you: `close()` the wrapper when you're done with it. An exception thrown \ - from a method is reported to the producer as error code `-4` (with the \ - exception's message) and the method's default value is returned in its \ - place; the producer then aborts the call it was making, and the original \ - caller sees a `WeaveFFIError` with code `-4` carrying your message (see \ - *Traps* below for how that abort reaches the caller on this target).\n", - ); - } - out.push_str("\n### Error Handling\n\n"); - out.push_str("The generated JS wrappers automatically handle errors by passing an error\n"); - out.push_str("pointer as the last argument to each Wasm function. The error struct is\n"); - 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"); - out.push_str("- `weaveffi_error_set(err_ptr: i32, code: i32, msg: i32)`: fill an error slot (used to report callback exceptions)\n"); - out.push_str("- `weaveffi_free_string(ptr: i32)`: free a producer-returned C string\n"); - out.push_str("- `weaveffi_free_bytes(ptr: i32, len: i32)`: free a producer-returned buffer\n"); - out.push_str("\nWrappers of functions declared `throws` raise the declaring module's typed\n"); - out.push_str("error class (a `WeaveFFIError` subclass with a per-code subclass, such as\n"); - out.push_str("`KeyNotFound`); every other wrapper raises the generic `WeaveFFIError` only\n"); - out.push_str("for producer panics, marshalling failures, and callback exceptions\n"); - out.push_str("(code `-4`). When the matched error code declares payload fields, the\n"); - out.push_str("wrapper decodes them from the error's value buffer and attaches them as\n"); - out.push_str("properties on the thrown error.\n"); - if !emscripten { - out.push_str("\n#### Callback failures without unwinding\n\n"); - out.push_str( - "`wasm32-unknown-unknown` builds with `panic = \"abort\"`, so when a callback \ - interface method throws, the producer can't unwind out of the call the way \ - native targets do. Instead the runtime records the failure, the producer's \ - code runs to completion on the method's default return value (`0`, `0n`, \ - or `false`), and the thunk then reports code `-4` with the exception's \ - message in place of the result, so the wrapper throws (or the Promise \ - rejects with) the same `WeaveFFIError` a native build would produce. While \ - that producer call is still on the stack, the glue refuses every further \ - callback invocation with the same error rather than calling your \ - implementation again, so your code observes the failure exactly as it would \ - under unwinding. Producers must tolerate a callback returning its default \ - once (a well-behaved producer already does, because a consumer could return \ - that value on purpose).\n", - ); - out.push_str("\n#### Traps\n\n"); - out.push_str( - "A producer panic can't be caught on the Rust side either: it executes \ - `unreachable`, and the engine throws a `WebAssembly.RuntimeError` out of \ - the call. The glue translates that trap for you: the wrapper throws (or the \ - Promise rejects with) a `WeaveFFIError` with code `-2` (`producer \ - panicked`), or code `-4` if a callback method had already failed during \ - the call. The error slot and every staged argument are released either \ - way. The producer frames between the trap and the call are *not* unwound, \ - so their cleanup does not run: a lock held across the panic stays locked, \ - and allocations those frames owned leak.\n", - ); - } - - if !api.modules.is_empty() { - render_api_reference(&mut out, api, model); - } - - out.push('\n'); - out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); - out -} - -/// Append the README's ABI-level API reference: one section per top-level -/// module covering its error domain, functions, interfaces, structs, and -/// enums. -fn render_api_reference(out: &mut String, api: &Api, model: &BindingModel) { - let by_path: HashMap<&str, &ModuleBinding> = - model.modules.iter().map(|m| (m.path.as_str(), m)).collect(); - out.push_str("\n## API Reference\n"); - for module in &api.modules { - out.push_str(&format!("\n### Module: `{}`\n", module.name)); - let mb = by_path[module.name.as_str()]; - - if let Some(eb) = mb.error.as_ref().filter(|eb| eb.declared_here) { - render_error_ref(out, eb); - } - - if !mb.functions.is_empty() { - out.push_str("\n#### Functions\n"); - for f in &mb.functions { - render_function_ref(out, f); - } - } - - if !mb.interfaces.is_empty() { - out.push_str("\n#### Interfaces\n"); - for i in &mb.interfaces { - render_interface_ref(out, i); - } - } - - if !mb.callback_interfaces.is_empty() { - out.push_str("\n#### Callback Interfaces\n"); - for cb in &mb.callback_interfaces { - render_callback_interface_ref(out, cb); - } - } - - if !mb.structs.is_empty() { - out.push_str("\n#### Structs\n"); - for s in &mb.structs { - render_struct_ref(out, s); - } - } - - if !mb.enums.is_empty() { - out.push_str("\n#### Enums\n"); - for e in &mb.enums { - render_enum_ref(out, e); - } - } - } -} - -/// Document a module's declared error domain: the JS class hierarchy it -/// generates, the stable ABI code of each subclass, and any structured -/// payload fields a code attaches to the thrown error. -fn render_error_ref(out: &mut String, eb: &ErrorBinding) { - out.push_str(&format!("\n#### Error Domain: `{}`\n\n", eb.type_name)); - out.push_str(&format!( - "Throwing wrappers in this module raise `{}` (a `{ERROR_BRAND}` 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.\n\n", - eb.type_name - )); - out.push_str("| Class | Code | Default Message | Payload Fields |\n"); - out.push_str("|-------|------|-----------------|----------------|\n"); - for c in &eb.codes { - let fields = if c.fields.is_empty() { - "(none)".to_string() - } else { - c.fields - .iter() - .map(|f| format!("`{}: {}`", f.name, type_display(&f.ty))) - .collect::>() - .join(", ") - }; - out.push_str(&format!( - "| `{}` | `{}` | {} | {} |\n", - js_code_class_name(&c.name), - c.value, - c.message, - fields - )); - } -} - -/// Document one interface: an object pointer wrapped by a JS class, with the -/// member entry points listed at the ABI level like free functions. -fn render_interface_ref(out: &mut String, i: &InterfaceBinding) { - out.push_str(&format!("\n##### `{}`\n\n", i.name)); - if let Some(doc) = &i.doc { - out.push_str(doc); - out.push_str("\n\n"); - } - out.push_str( - "Passed as an **object pointer** (`i32`), wrapped by a JS class holding one \ - reference. Constructors return an owned reference; methods lend the pointer \ - as the implicit leading `self` argument; `close()` (or `Symbol.dispose`) \ - releases the reference via the destroy symbol exactly once.\n", - ); - for f in i - .constructors - .iter() - .chain(i.methods.iter()) - .chain(i.statics.iter()) - { - render_function_ref(out, f); - } - out.push_str(&format!( - "\n##### `{}`\n\nReturns a second reference to the same object. Used when the \ - wrapper is encoded as an object token inside a value buffer.\n", - i.clone_symbol - )); - out.push_str(&format!( - "\n##### `{}`\n\nReleases one object reference. Called by the wrapper's `close()` \ - (and by the finalizer backstop).\n", - i.destroy_symbol - )); -} - -/// Document one callback interface: the TS shape a consumer implements, its -/// vtable layout, and each method's parameters at the API level. -fn render_callback_interface_ref(out: &mut String, cb: &CallbackInterfaceBinding) { - out.push_str(&format!("\n##### `{}`\n\n", cb.name)); - if let Some(doc) = &cb.doc { - out.push_str(doc); - out.push_str("\n\n"); - } - if let Some(msg) = &cb.deprecated { - out.push_str(&format!("**Deprecated:** {msg}\n\n")); - } - out.push_str(&format!( - "Implemented in JS as an object with the methods below. Passed as an `i32` \ - context key plus an `i32` pointer to the static `{}` vtable; the vtable's \ - trailing `free(ctx)` entry releases the JS object.\n\n", - cb.vtable_tag - )); - out.push_str("| Method | Parameters | Returns |\n"); - out.push_str("|--------|------------|---------|\n"); - for m in &cb.methods { - let params = if m.params.is_empty() { - "(none)".to_string() - } else { - m.params - .iter() - .map(|p| format!("`{}: {}`", p.name, ts_type_for(&p.ty))) - .collect::>() - .join(", ") - }; - let ret = m - .ret - .as_ref() - .map_or_else(|| "`void`".to_string(), |t| format!("`{}`", ts_type_for(t))); - out.push_str(&format!( - "| `{}` | {params} | {ret} |\n", - js_cb_method_name(&m.name) - )); - } -} - -/// Document one callable at the ABI level: its C signature in wasm value -/// types and a per-parameter table of API type, wasm slot type, and boundary -/// note. -fn render_function_ref(out: &mut String, f: &FnBinding) { - let abi_name = &f.c_base; - out.push_str(&format!("\n##### `{abi_name}`\n\n")); - - if let Some(doc) = &f.doc { - out.push_str(doc); - out.push_str("\n\n"); - } - - if let Some(msg) = &f.deprecated { - out.push_str(&format!("**Deprecated:** {msg}\n\n")); - } - - let params_sig: Vec = f - .params - .iter() - .map(|p| format!("{}: {}", p.name, wasm_type(&p.ty))) - .collect(); - let ret_sig = f.ret.as_ref().map_or("void", wasm_type); - out.push_str(&format!( - "`{abi_name}({}) -> {ret_sig}`\n\n", - params_sig.join(", ") - )); - - out.push_str("| Param | API Type | Wasm | Notes |\n"); - out.push_str("|-------|----------|------|-------|\n"); - for param in &f.params { - out.push_str(&format!( - "| `{}` | `{}` | `{}` | {} |\n", - param.name, - type_display(¶m.ty), - wasm_type(¶m.ty), - wasm_type_note(¶m.ty) - )); - } - if let Some(ret) = &f.ret { - out.push_str(&format!( - "| _returns_ | `{}` | `{}` | {} |\n", - type_display(ret), - wasm_type(ret), - wasm_type_note(ret) - )); - } -} - -/// Document one record: a plain JS object serialized in a value buffer, with -/// its field schema in declaration (wire) order. -fn render_struct_ref(out: &mut String, s: &StructBinding) { - out.push_str(&format!("\n##### `{}`\n\n", s.name)); - - if let Some(doc) = &s.doc { - out.push_str(doc); - out.push_str("\n\n"); - } - - out.push_str( - "A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at \ - the boundary. Fields in declaration (wire) order:\n\n", - ); - - if !s.fields.is_empty() { - out.push_str("| Field | Type |\n"); - out.push_str("|-------|------|\n"); - for field in &s.fields { - out.push_str(&format!( - "| `{}` | `{}` |\n", - field.name, - type_display(&field.ty) - )); - } - } -} - -/// Document one enum: a variant/value table for a C-style enum, or the -/// tagged-object union shape for a rich one. -fn render_enum_ref(out: &mut String, e: &EnumBinding) { - out.push_str(&format!("\n##### `{}`\n\n", e.name)); - - if let Some(doc) = &e.doc { - out.push_str(doc); - out.push_str("\n\n"); - } - - if e.is_rich() { - render_rich_enum_ref(out, e); - return; - } - - out.push_str("Passed as `i32` discriminant.\n\n"); - out.push_str("| Variant | Value |\n"); - out.push_str("|---------|-------|\n"); - for v in &e.variants { - out.push_str(&format!("| `{}` | `{}` |\n", v.name, v.value)); - } -} - -/// Document a rich (algebraic) enum: a plain tagged object serialized in a -/// value buffer as an `i32` tag followed by the active variant's fields, not -/// a by-value `i32` discriminant like a plain enum. -fn render_rich_enum_ref(out: &mut String, e: &EnumBinding) { - out.push_str( - "Rich (algebraic) enum: a plain tagged object (`{ tag: \"Variant\", ...fields }`) \ - serialized in a value buffer as an `i32` tag followed by the active variant's \ - fields in declaration order.\n\n", - ); - out.push_str("| Variant | Tag | Fields |\n"); - out.push_str("|---------|-----|--------|\n"); - for v in &e.variants { - let fields = if v.fields.is_empty() { - "(none)".to_string() - } else { - v.fields - .iter() - .map(|f| format!("`{}: {}`", f.name, type_display(&f.ty))) - .collect::>() - .join(", ") - }; - out.push_str(&format!("| `{}` | `{}` | {} |\n", v.name, v.value, fields)); - } -} diff --git a/crates/weaveffi-gen-wasm/src/runtime.rs b/crates/weaveffi-gen-wasm/src/runtime.rs deleted file mode 100644 index cd429e4e..00000000 --- a/crates/weaveffi-gen-wasm/src/runtime.rs +++ /dev/null @@ -1,580 +0,0 @@ -//! The fixed JS runtime the loader embeds: string, byte, and error-slot -//! helpers, the error class hierarchy with its per-domain factories and -//! checkers, the object-wrapper lifecycle helpers, the lazy iterator wrapper, -//! the trampoline registrar, and the foreign-error reporter callback -//! trampolines use. -//! -//! Everything here is emitted conditionally by the loader assembler in -//! [`crate::entities`], keyed off which features the API actually uses. The -//! error classes are exported at module scope; the per-domain factories and -//! checkers decode error payloads through the value-buffer codecs, which live -//! inside the loader (they reference the loader-scoped interface classes for -//! object tokens), so they're emitted there too. - -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}; - -use crate::codec::buf_read_expr; -use crate::types::{ - js_code_class_name, js_error_checker_name, js_error_factory_name, js_str_literal, -}; - -/// Emit the module-scope error classes: the generic `WeaveFFIError` base -/// (unknown codes, marshalling failures, panics, foreign callback failures), -/// then one domain class per declaring module (`class KvError extends -/// WeaveFFIError`) with one subclass per code carrying its stable `CODE` and -/// default message. Each code class is also aliased onto its domain class -/// (`KvError.KeyNotFound`). -/// -/// Domain codes are validated positive-only, so the reserved runtime codes -/// (`-1` generic, `-2` panic, `-3` marshalling, `-4` a callback-interface -/// implementation raised) always fall through to the generic brand error in -/// the factories emitted by [`emit_js_error_factories`]. -pub(crate) fn emit_js_error_classes(out: &mut String, model: &BindingModel) { - let mut w = CodeWriter::two_space(); - w.line("/** Base error for WeaveFFI failures: domain errors extend it, and it is"); - w.line(" * thrown directly for unknown codes, marshalling failures, producer"); - w.line(" * panics, and callback-interface implementations that raised (code -4)."); - w.line(" * Carries the stable ABI `code`. */"); - w.block(format!("export class {ERROR_BRAND} extends Error {{"), "}", |w| { - w.block("constructor(code, message) {", "}", |w| { - w.line("super(message ? `WeaveFFI error ${code}: ${message}` : `WeaveFFI error ${code}`);"); - w.line("this.name = new.target.name;"); - w.line("this.code = code;"); - }); - }); - w.blank(); - - for m in &model.modules { - let Some(eb) = m.error.as_ref().filter(|eb| eb.declared_here) else { - continue; - }; - let domain = &eb.type_name; - w.line(format!( - "/** Base error for the `{}` module's error domain. */", - m.path - )); - w.line(format!("export class {domain} extends {ERROR_BRAND} {{}}")); - w.blank(); - for c in &eb.codes { - let class = js_code_class_name(&c.name); - let message = js_str_literal(&c.message); - let doc = c - .doc - .as_deref() - .map(str::trim) - .filter(|d| !d.is_empty()) - .unwrap_or(&c.message); - for line in doc.lines() { - w.line(format!("// {line}")); - } - w.block( - format!("export class {class} extends {domain} {{"), - "}", - |w| { - w.block( - format!("constructor(message = \"{message}\") {{"), - "}", - |w| { - w.line(format!("super({}, message);", c.value)); - }, - ); - }, - ); - w.line(format!("{class}.CODE = {};", c.value)); - w.line(format!("{domain}.{class} = {class};")); - w.blank(); - } - - let table = js_error_code_table_name(eb); - w.block(format!("const {table} = Object.freeze({{"), "});", |w| { - for c in &eb.codes { - w.line(format!("{}: {},", c.value, js_code_class_name(&c.name))); - } - }); - w.blank(); - } - out.push_str(&w.finish()); -} - -/// `_{TYPE_NAME}_CODES`: the frozen code-to-class table for one domain. -fn js_error_code_table_name(eb: &ErrorBinding) -> String { - format!("_{}_CODES", eb.type_name.to_shouty_snake_case()) -} - -/// Emit one `_{domain}From(wasm, code, message, payloadPtr, payloadLen)` -/// factory per declaring module at `indent` (loader scope): it builds the -/// matching code subclass (or the generic brand error for codes outside the -/// domain) and decodes any declared payload fields from the error's value -/// buffer into properties on the thrown error. -pub(crate) fn emit_js_error_factories(out: &mut String, model: &BindingModel, indent: &str) { - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - for m in &model.modules { - let Some(eb) = m.error.as_ref().filter(|eb| eb.declared_here) else { - continue; - }; - let domain = &eb.type_name; - let table = js_error_code_table_name(eb); - let factory = js_error_factory_name(eb); - let has_payload = eb.codes.iter().any(|c| !c.fields.is_empty()); - w.line(format!( - "// Build the {domain} subclass matching `code`, or a generic" - )); - w.line(format!( - "// {ERROR_BRAND} for codes outside the domain (panics, marshalling," - )); - w.line("// foreign callback failures)."); - if has_payload { - w.line("// Codes that declare payload fields decode them from the error's"); - w.line("// borrowed value buffer into properties on the thrown error."); - } - w.block( - format!("function {factory}(wasm, code, message, payloadPtr, payloadLen) {{"), - "}", - |w| { - w.line(format!("const _cls = {table}[code];")); - w.line(format!( - "const _e = _cls ? (message ? new _cls(message) : new _cls()) : new {ERROR_BRAND}(code, message);" - )); - if has_payload { - w.block("switch (code) {", "}", |w| { - for c in eb.codes.iter().filter(|c| !c.fields.is_empty()) { - w.block(format!("case {}: {{", c.value), "}", |w| { - w.line( - "const _b = payloadPtr === 0 || payloadLen === 0 ? new Uint8Array(0) : new Uint8Array(wasm.memory.buffer, payloadPtr, payloadLen).slice();", - ); - w.line("const _rd = new _BufReader(_b);"); - for f in &c.fields { - w.line(format!( - "_e.{} = {};", - f.name, - buf_read_expr(&f.ty, &eb.owner_path, "_rd") - )); - } - w.line("_rd.end();"); - w.line("break;"); - }); - } - }); - } - w.line("return _e;"); - }, - ); - w.blank(); - } - out.push_str(&w.finish()); -} - -/// Emit one `_check{Domain}(wasm, errPtr)` helper per declaring module at -/// `indent` (loader scope): identical to the generic `_checkErr` except the -/// thrown error is built by the domain's factory, so domain codes surface as -/// their typed subclasses with any declared payload fields decoded and -/// attached. The payload is decoded before `weaveffi_error_clear` releases -/// it. -pub(crate) fn emit_js_error_checkers(out: &mut String, model: &BindingModel, indent: &str) { - let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - for m in &model.modules { - let Some(eb) = m.error.as_ref().filter(|eb| eb.declared_here) else { - continue; - }; - let checker = js_error_checker_name(eb); - let factory = js_error_factory_name(eb); - w.line(format!( - "// Throw the `{}` domain error (and free the slot) if the error slot", - eb.type_name - )); - w.line("// carries a non-zero code."); - w.block(format!("function {checker}(wasm, errPtr) {{"), "}", |w| { - w.line("_pendingForeign = null;"); - w.line("const dv = new DataView(wasm.memory.buffer);"); - w.line("const code = dv.getInt32(errPtr, true);"); - w.block("if (code !== 0) {", "}", |w| { - w.line("const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || '';"); - w.line(format!( - "const _e = {factory}(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true));" - )); - w.line("wasm.weaveffi_error_clear(errPtr);"); - w.line("wasm.weaveffi_dealloc(errPtr, 16);"); - w.line("throw _e;"); - }); - }); - w.blank(); - } - out.push_str(&w.finish()); -} - -/// Emit the text encoder/decoder pair and the C-string helpers: `_cstr` -/// stages a JS string as a NUL-terminated C string, `_readCStr` reads one -/// without freeing, and `_takeCStr` reads then frees a producer-owned one. -pub(crate) fn emit_string_helpers(out: &mut String) { - out.push_str("const _enc = new TextEncoder();\n"); - out.push_str("const _dec = new TextDecoder();\n\n"); - out.push_str("// Stage a JS string as a NUL-terminated C string in linear memory.\n"); - out.push_str("// Returns [ptr, size] (size includes the NUL); release with _free.\n"); - out.push_str("function _cstr(wasm, str) {\n"); - out.push_str(" const bytes = _enc.encode(str);\n"); - out.push_str(" const size = bytes.length + 1;\n"); - out.push_str(" const ptr = wasm.weaveffi_alloc(size);\n"); - out.push_str(" const mem = new Uint8Array(wasm.memory.buffer, ptr, size);\n"); - out.push_str(" mem.set(bytes);\n"); - out.push_str(" mem[bytes.length] = 0;\n"); - out.push_str(" return [ptr, size];\n"); - out.push_str("}\n\n"); - out.push_str("// Read a NUL-terminated C string (0 => null). Does not free.\n"); - out.push_str("function _readCStr(wasm, ptr) {\n"); - out.push_str(" if (ptr === 0) return null;\n"); - out.push_str(" const mem = new Uint8Array(wasm.memory.buffer);\n"); - out.push_str(" let end = ptr;\n"); - out.push_str(" while (mem[end] !== 0) end++;\n"); - out.push_str(" return _dec.decode(mem.subarray(ptr, end));\n"); - out.push_str("}\n\n"); - out.push_str("// Read then free a producer-owned C string.\n"); - out.push_str("function _takeCStr(wasm, ptr) {\n"); - out.push_str(" const s = _readCStr(wasm, ptr);\n"); - out.push_str(" if (ptr !== 0) wasm.weaveffi_free_string(ptr);\n"); - out.push_str(" return s;\n"); - out.push_str("}\n\n"); -} - -/// Emit the byte-buffer helpers: `_bytes` stages a byte (or encoded value) -/// buffer into linear memory, `_takeBytes` copies then frees a -/// producer-owned one. -pub(crate) fn emit_bytes_helpers(out: &mut String) { - out.push_str("// Stage a byte buffer (or an encoded value buffer); returns [ptr, len];\n"); - out.push_str("// release with weaveffi_dealloc(ptr, len).\n"); - out.push_str("function _bytes(wasm, data) {\n"); - out.push_str(" const u8 = data instanceof Uint8Array ? data : new Uint8Array(data);\n"); - out.push_str(" const ptr = wasm.weaveffi_alloc(u8.length);\n"); - out.push_str(" if (u8.length) new Uint8Array(wasm.memory.buffer, ptr, u8.length).set(u8);\n"); - out.push_str(" return [ptr, u8.length];\n"); - out.push_str("}\n\n"); - out.push_str("// Copy then free a producer-owned byte (or value) buffer.\n"); - out.push_str("function _takeBytes(wasm, ptr, len) {\n"); - out.push_str(" if (ptr === 0 || len === 0) return new Uint8Array(0);\n"); - out.push_str(" const copy = new Uint8Array(wasm.memory.buffer, ptr, len).slice();\n"); - out.push_str(" wasm.weaveffi_free_bytes(ptr, len);\n"); - out.push_str(" return copy;\n"); - out.push_str("}\n\n"); -} - -/// Emit the error-slot helpers: `_allocErr` allocates a zeroed 16-byte error -/// struct, `_checkErr` throws the generic brand error (and frees the slot) -/// for a non-zero code on the trap path, and `_freeErr` releases the slot on -/// the success path. -pub(crate) fn emit_error_slot_helpers(out: &mut String) { - out.push_str("// Allocate a zeroed 16-byte error slot:\n"); - out.push_str("// { i32 code, char* message, uint8_t* payload_ptr, size_t payload_len }.\n"); - out.push_str("function _allocErr(wasm) {\n"); - out.push_str(" const ptr = wasm.weaveffi_alloc(16);\n"); - out.push_str(" new Uint8Array(wasm.memory.buffer, ptr, 16).fill(0);\n"); - out.push_str(" return ptr;\n"); - out.push_str("}\n\n"); - out.push_str("// Throw (and free the slot) if the error slot carries a non-zero code.\n"); - out.push_str("// Non-throwing wrappers route here: a non-zero code can only be a\n"); - out.push_str("// producer panic (-2), a marshalling failure (-3), or a callback\n"); - out.push_str("// interface implementation that raised (-4), surfaced as the generic\n"); - out.push_str(&format!("// {ERROR_BRAND}.\n")); - out.push_str("function _checkErr(wasm, errPtr) {\n"); - out.push_str(" _pendingForeign = null;\n"); - out.push_str(" const dv = new DataView(wasm.memory.buffer);\n"); - out.push_str(" const code = dv.getInt32(errPtr, true);\n"); - out.push_str(" if (code !== 0) {\n"); - out.push_str(" const msgPtr = dv.getUint32(errPtr + 4, true);\n"); - out.push_str(" const msg = _readCStr(wasm, msgPtr) || '';\n"); - out.push_str(" wasm.weaveffi_error_clear(errPtr);\n"); - out.push_str(" wasm.weaveffi_dealloc(errPtr, 16);\n"); - out.push_str(&format!(" throw new {ERROR_BRAND}(code, msg);\n")); - out.push_str(" }\n"); - out.push_str("}\n\n"); - out.push_str("// Release an error slot on the success path.\n"); - out.push_str("function _freeErr(wasm, errPtr) {\n"); - out.push_str(" _pendingForeign = null;\n"); - out.push_str(" wasm.weaveffi_dealloc(errPtr, 16);\n"); - out.push_str("}\n\n"); - emit_trap_helpers(out); -} - -/// Emit `_pendingForeign`, `_trapError`, and `_trap`: the trap translation -/// every producer call routes its exceptions through. -/// -/// `wasm32-unknown-unknown` has no unwinding runtime, so a producer panic -/// can't be caught and reported through `out_err` the way native targets do: -/// it executes `unreachable` and the engine throws a `WebAssembly.RuntimeError` -/// out of the call. The glue restores the ABI's contract on the JS side: a -/// trap that follows a callback-interface failure recorded in -/// `_pendingForeign` becomes the `-4` brand error with the consumer's message -/// (the runtime normally reports that failure through `out_err` without -/// trapping; this is the fallback), and any other trap is reported as a -/// producer panic (`-2`). Exceptions that aren't traps (the `-3` from -/// `_borrow`, for example) pass through unchanged. -fn emit_trap_helpers(out: &mut String) { - let mut w = CodeWriter::two_space(); - w.line("// The message of a callback-interface failure a trampoline reported through"); - w.line("// out_err, held until the producer call on the stack returns (every error"); - w.line("// checker and _freeErr clear it) so later callbacks in that call are refused."); - w.line("let _pendingForeign = null;"); - w.blank(); - w.line("// Translate an exception raised while a producer call was on the stack."); - w.line("// wasm32-unknown-unknown has no unwinding runtime: a producer panic executes"); - w.line("// `unreachable`, so the engine throws a WebAssembly.RuntimeError instead of"); - w.line("// the producer writing out_err. A trap that follows a callback-interface"); - w.line("// failure is labelled with that failure (-4); any other trap is a producer"); - w.line("// panic (-2). Non-trap exceptions pass through unchanged. The producer"); - w.line("// frames between the trap and the call are not unwound, so a lock held"); - w.line("// across a callback stays locked; a producer that snapshots its state"); - w.line("// before calling out stays usable."); - w.block("function _trapError(e) {", "}", |w| { - w.line("const foreign = _pendingForeign;"); - w.line("_pendingForeign = null;"); - w.line("if (!(e instanceof WebAssembly.RuntimeError)) return e;"); - w.line(format!( - "if (foreign !== null) return new {ERROR_BRAND}(-4, foreign);" - )); - w.line(format!( - "return new {ERROR_BRAND}(-2, 'producer panicked: ' + e.message);" - )); - }); - w.blank(); - w.line("// Release the error slot a failed call never filled, then translate."); - w.block("function _trap(wasm, errPtr, e) {", "}", |w| { - w.line("_freeErr(wasm, errPtr);"); - w.line("return _trapError(e);"); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// 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 -/// released with `weaveffi_error_free`. -pub(crate) fn emit_check_err_ref(out: &mut String) { - out.push_str("// Throw if a heap-boxed (consumer-owned) error carries a non-zero\n"); - out.push_str("// code. Used by async callbacks: the fields are read, then the box\n"); - out.push_str("// is released with weaveffi_error_free before throwing.\n"); - out.push_str("// `mkErr` maps domain codes (and decodes payload fields) for\n"); - out.push_str(&format!( - "// throwing callables; without it the generic {ERROR_BRAND} is thrown.\n" - )); - out.push_str("function _checkErrRef(wasm, errPtr, mkErr) {\n"); - out.push_str(" _pendingForeign = null;\n"); - out.push_str(" const dv = new DataView(wasm.memory.buffer);\n"); - out.push_str(" const code = dv.getInt32(errPtr, true);\n"); - out.push_str(" if (code === 0) { wasm.weaveffi_error_free(errPtr); return; }\n"); - out.push_str(" const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || '';\n"); - out.push_str(&format!( - " const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new {ERROR_BRAND}(code, msg);\n" - )); - out.push_str(" wasm.weaveffi_error_free(errPtr);\n"); - out.push_str(" throw err;\n"); - out.push_str("}\n\n"); -} - -/// Emit the object-wrapper lifecycle helpers shared by every interface class: -/// `_dispose` (the `Symbol.dispose` well-known symbol, or its registered -/// stand-in on runtimes that predate explicit resource management), the -/// `FinalizationRegistry` backstop, `_adopt` (bind one strong reference to a -/// wrapper and arm the backstop), `_release` (destroy exactly once, disarming -/// the backstop), and `_borrow` (the live pointer of a wrapper passed as an -/// argument, rejecting a closed one). -pub(crate) fn emit_object_helpers(out: &mut String) { - let mut w = CodeWriter::two_space(); - w.line("// `using` declarations (explicit resource management) call this symbol;"); - w.line("// runtimes that predate it get the registered stand-in so the method is"); - w.line("// still reachable by name."); - w.line("const _dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose');"); - w.blank(); - w.line("// Finalization backstop: a wrapper that is garbage collected without"); - w.line("// close() still releases its reference. The held value is the"); - w.line("// [destroy, handle] pair (never the wrapper, which would keep it alive);"); - w.line("// the wrapper is the unregister token so close() disarms the backstop and"); - w.line("// destroy runs exactly once either way. Absent on runtimes without"); - w.line("// FinalizationRegistry, where close() is the only release path."); - w.line("const _finalizer = typeof FinalizationRegistry === 'function'"); - w.line(" ? new FinalizationRegistry(([destroy, handle]) => destroy(handle))"); - w.line(" : null;"); - w.blank(); - w.line("// Bind one strong reference to a wrapper and arm the backstop."); - w.block("function _adopt(obj, handle, destroy) {", "}", |w| { - w.line("obj._handle = handle;"); - w.line("obj._destroy = destroy;"); - w.line("if (_finalizer !== null) _finalizer.register(obj, [destroy, handle], obj);"); - w.line("return obj;"); - }); - w.blank(); - w.line("// Release a wrapper's reference exactly once; later calls are no-ops."); - w.block("function _release(obj) {", "}", |w| { - w.line("if (!obj._handle) return;"); - w.line("if (_finalizer !== null) _finalizer.unregister(obj);"); - w.line("obj._destroy(obj._handle);"); - w.line("obj._handle = 0;"); - }); - w.blank(); - w.line("// The pointer of a live wrapper, lent to the producer for one call (the"); - w.line("// wrapper keeps its own reference). A closed or non-object argument is a"); - w.line("// consumer programming error, reported with the marshalling code."); - w.block("function _borrow(obj) {", "}", |w| { - w.block( - "if (obj === null || obj === undefined || !obj._handle) {", - "}", - |w| { - w.line(format!( - "throw new {ERROR_BRAND}(-3, 'expected a live object wrapper');" - )); - }, - ); - w.line("return obj._handle;"); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit `_reportForeign` and `_setForeignError`, which callback-interface -/// trampolines call when the consumer's implementation throws: the exception -/// message is staged in linear memory only for the duration of the -/// `weaveffi_error_set` call (the producer copies it with its own allocator), -/// then released, and the producer sees `FOREIGN_ERROR_CODE` (`-4`). -/// -/// `wasm32-unknown-unknown` builds with `panic = "abort"`, so the producer -/// can't unwind out of the failed call the way native targets do; the -/// runtime records the failure instead and the producer's code runs to -/// completion on the trampoline's default return value before the thunk -/// reports `-4`. The message is parked in `_pendingForeign` for two reasons: -/// so that every further callback invocation during the same producer call -/// is refused with the same error rather than reaching the consumer's -/// implementation again (matching what unwinding would have done), and so -/// that `_trapError` can still label a trap, should one follow. -pub(crate) fn emit_foreign_error_helper(out: &mut String) { - let mut w = CodeWriter::two_space(); - w.line("// Report a callback-interface failure through the trampoline's out_err"); - w.line("// slot as FOREIGN_ERROR_CODE (-4). weaveffi_error_set copies the message"); - w.line("// with the producer's allocator, so the staged C string is released as"); - w.line("// soon as it returns. The message is parked in _pendingForeign until the"); - w.line("// producer call on the stack returns: the producer can't unwind on wasm32,"); - w.line("// so it keeps running on the trampoline's default return value, and any"); - w.line("// further callback it makes during that call is refused with this error"); - w.line("// instead of reaching the implementation again."); - w.block("function _reportForeign(wasm, errPtr, msg) {", "}", |w| { - w.line("if (_pendingForeign === null) _pendingForeign = msg;"); - w.line("const [p, s] = _cstr(wasm, msg);"); - w.line("wasm.weaveffi_error_set(errPtr, -4, p);"); - w.line("wasm.weaveffi_dealloc(p, s);"); - }); - w.blank(); - w.line("// Report an exception thrown by a callback-interface implementation."); - w.block("function _setForeignError(wasm, errPtr, e) {", "}", |w| { - w.line( - "_reportForeign(wasm, errPtr, e instanceof Error ? (e.message || e.name) : String(e));", - ); - }); - w.blank(); - out.push_str(&w.finish()); -} - -/// Emit the shared `_WeaveFFIIterator` class: a lazy wrapper over a producer -/// iterator handle implementing the JS iterator protocol, destroying the -/// handle exactly once (on exhaustion, on a `next` error, or from -/// `return()`). -pub(crate) fn emit_iterator_class(out: &mut String) { - out.push_str("// Lazy wrapper over a producer iterator handle, implementing the JS\n"); - out.push_str("// iterator protocol: each next() issues exactly one producer next call\n"); - out.push_str("// and yields one converted element, so iteration streams in constant\n"); - out.push_str("// memory. The handle is destroyed exactly once: eagerly on exhaustion,\n"); - out.push_str("// on a next error, or from return() when iteration stops early (a\n"); - out.push_str("// for...of loop calls return() automatically on break or throw).\n"); - out.push_str("// Abandoning an iterator without exhausting or closing it leaks the\n"); - out.push_str("// producer handle: JS has no finalization hook that is reliable across\n"); - out.push_str("// every target this loader supports.\n"); - out.push_str("class _WeaveFFIIterator {\n"); - out.push_str(" constructor(wasm, handle, slotSize, callNext, destroy, check, decode) {\n"); - out.push_str(" this._wasm = wasm;\n"); - out.push_str(" this._handle = handle;\n"); - out.push_str(" this._slotSize = slotSize;\n"); - out.push_str(" this._callNext = callNext;\n"); - out.push_str(" this._destroyFn = destroy;\n"); - out.push_str(" this._check = check;\n"); - out.push_str(" this._decode = decode;\n"); - out.push_str(" this._slot = wasm.weaveffi_alloc(slotSize);\n"); - out.push_str(" }\n"); - out.push_str(" // Destroy the handle and release the element slot exactly once.\n"); - out.push_str(" _close() {\n"); - out.push_str(" if (this._handle === 0) return;\n"); - out.push_str(" this._destroyFn(this._handle);\n"); - out.push_str(" this._handle = 0;\n"); - out.push_str(" this._wasm.weaveffi_dealloc(this._slot, this._slotSize);\n"); - out.push_str(" this._slot = 0;\n"); - out.push_str(" }\n"); - out.push_str(" next() {\n"); - out.push_str(" if (this._handle === 0) return { done: true, value: undefined };\n"); - out.push_str(" const wasm = this._wasm;\n"); - out.push_str(" const _err = _allocErr(wasm);\n"); - out.push_str(" let _has;\n"); - out.push_str(" try {\n"); - out.push_str(" _has = this._callNext(this._handle, this._slot, _err);\n"); - out.push_str(" } catch (e) {\n"); - out.push_str(" // A trap: the slot was never filled, so release it here.\n"); - out.push_str(" this._close();\n"); - out.push_str(" throw _trap(wasm, _err, e);\n"); - out.push_str(" }\n"); - out.push_str(" try {\n"); - out.push_str(" // Throws (and releases the slot) on a non-zero code.\n"); - out.push_str(" this._check(wasm, _err);\n"); - out.push_str(" } catch (e) {\n"); - out.push_str(" this._close();\n"); - out.push_str(" throw e;\n"); - out.push_str(" }\n"); - out.push_str(" _freeErr(wasm, _err);\n"); - out.push_str(" if (_has === 0) {\n"); - out.push_str(" this._close();\n"); - out.push_str(" return { done: true, value: undefined };\n"); - out.push_str(" }\n"); - out.push_str(" return { done: false, value: this._decode(wasm, this._slot) };\n"); - out.push_str(" }\n"); - out.push_str(" // Early-exit cleanup; for...of calls this on break/throw.\n"); - out.push_str(" return(value) {\n"); - out.push_str(" this._close();\n"); - out.push_str(" return { done: true, value };\n"); - out.push_str(" }\n"); - out.push_str(" [Symbol.iterator]() {\n"); - out.push_str(" return this;\n"); - out.push_str(" }\n"); - out.push_str("}\n\n"); -} - -/// Emit `_registerTrampoline`, which grows the wasm function table by one -/// slot and installs a `WebAssembly.Function` wrapping `handler` with the -/// given wasm parameter and result types, returning the new index. Shared by -/// the async completion trampolines and the callback-interface vtable -/// entries. -pub(crate) fn emit_trampoline_helper(out: &mut String) { - out.push_str("function _registerTrampoline(table, paramTypes, resultTypes, handler) {\n"); - out.push_str(" const idx = table.grow(1);\n"); - out.push_str(" table.set(idx, new WebAssembly.Function(\n"); - out.push_str(" { parameters: paramTypes, results: resultTypes },\n"); - out.push_str(" handler\n"); - out.push_str(" ));\n"); - out.push_str(" return idx;\n"); - out.push_str("}\n\n"); -} diff --git a/crates/weaveffi-gen-wasm/src/types.rs b/crates/weaveffi-gen-wasm/src/types.rs deleted file mode 100644 index feba7852..00000000 --- a/crates/weaveffi-gen-wasm/src/types.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! JS/TS naming, type mapping, and API scanning. -//! -//! Everything here answers "how is this IDL name or type spelled in -//! JavaScript and TypeScript?": wasm boundary types for the README tables, -//! TS annotations, exported function and parameter names (escaped against -//! the shared JS keyword table), the error-surface naming policy, and the -//! deep type scans that decide which runtime helpers the loader embeds. - -use heck::ToLowerCamelCase; -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; - -/// 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); a callback interface occupies two `i32` slots (context plus -/// vtable pointer); everything else keeps its scalar or pointer slot. -pub(crate) fn wasm_type(ty: &Ty) -> &'static str { - if ty.is_buffered() { - return "i32, i32"; - } - match ty { - Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::Bool | Ty::Enum(_) => "i32", - Ty::I64 | Ty::U64 => "i64", - Ty::F32 => "f32", - Ty::F64 => "f64", - Ty::StringUtf8 => "i32", - Ty::Bytes | Ty::CallbackInterface(_) => "i32, i32", - Ty::Interface(_) | Ty::Iterator(_) => "i32", - // Only `Interface?` reaches here: a nullable object pointer. - Ty::Optional(_) => "i32", - Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { - unreachable!("buffered type handled above") - } - } -} - -/// 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: &Ty) -> &'static str { - if ty.is_buffered() { - return "value buffer: ptr + len in linear memory"; - } - match ty { - 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 => "NUL-terminated C string pointer", - Ty::Bytes => "ptr + len in linear memory", - Ty::Interface(_) => "borrowed object pointer", - Ty::CallbackInterface(_) => "context key + static vtable 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") - } - } -} - -/// The IDL-flavored display spelling of one type for the README's API -/// reference (`Contact?`, `[string]`, `{string:i32}`). -pub(crate) fn type_display(ty: &Ty) -> String { - match ty { - 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 => "string".into(), - Ty::Bytes => "bytes".into(), - Ty::Record(n) | Ty::RichEnum(n) | Ty::Interface(n) | Ty::CallbackInterface(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)), - } -} - -/// The TS annotation of one IR type. -pub(crate) fn ts_type_for(ty: &Ty) -> String { - match ty { - Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::F32 | Ty::F64 => { - "number".into() - } - Ty::Bool => "boolean".into(), - Ty::StringUtf8 => "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. - Ty::Bytes => "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. - Ty::I64 | Ty::U64 => "bigint".into(), - // Records, rich enums, plain enums, interfaces, and callback - // 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. - Ty::Enum(name) - | Ty::Record(name) - | Ty::RichEnum(name) - | Ty::Interface(name) - | Ty::CallbackInterface(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(), Ty::Optional(_)) { - format!("({inner_ts})[]") - } else { - format!("{inner_ts}[]") - } - } - // `iter` streams lazily; the wrapper is a JS iterator, never a - // drained array. - Ty::Iterator(inner) => { - let t = ts_type_for(inner); - format!("IterableIterator<{t}>") - } - // 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 => "string".to_string(), - other => ts_type_for(other), - }; - format!("Record<{key}, {}>", ts_type_for(v)) - } - } -} - -/// True if `ty` is the UTF-8 string type. -pub(crate) fn is_string_type(ty: &Ty) -> bool { - matches!(ty, Ty::StringUtf8) -} - -// ── Naming and error-surface policy ── - -/// The lowerCamelCase JS name a callable is exposed under (`list_keys` becomes -/// `listKeys`). Functions are namespaced by module object, so exported names -/// never carry a module prefix in the first place. -pub(crate) fn js_fn_name(f: &FnBinding) -> String { - f.name.to_lower_camel_case() -} - -/// The lowerCamelCase JS name of a callback-interface method (`on_message` -/// becomes `onMessage`): the property the trampoline invokes on the consumer's -/// implementation object and the member the TS `interface` declares. -pub(crate) fn js_cb_method_name(name: &str) -> String { - name.to_lower_camel_case() -} - -/// The camelCase JS spelling of one parameter (`ttl_seconds` becomes -/// `ttlSeconds`), escaped when the camel-cased form collides with a JS/TS -/// reserved word (a parameter named `new` becomes `new_`; property and -/// method positions never need the escape, so only parameters route through -/// here). -pub(crate) fn js_param_name(p: &ParamBinding) -> String { - lang::escape_ident(&p.name.to_lower_camel_case(), lang::JS_KEYWORDS) -} - -/// The JS class name for one error code: plain PascalCase with no forced -/// suffix (`KeyNotFound`, not `KeyNotFoundError`). Code names are validated -/// to be globally unique across domains, so the flat name cannot collide. -pub(crate) fn js_code_class_name(name: &str) -> String { - weaveffi_core::errors::pascal(name) -} - -/// `_{typeName}From` (lowerCamel): builds the domain error matching an ABI -/// code, e.g. `_kvErrorFrom`. -pub(crate) fn js_error_factory_name(eb: &ErrorBinding) -> String { - format!("_{}From", eb.type_name.to_lower_camel_case()) -} - -/// `_check{TypeName}`: throws the domain error for a non-zero out-err slot, -/// e.g. `_checkKvError`. -pub(crate) fn js_error_checker_name(eb: &ErrorBinding) -> String { - format!("_check{}", eb.type_name) -} - -/// The error-check helper a callable's out-err slot routes through, per its -/// [`ErrorStrategy`]: the module domain's typed checker for -/// [`ErrorStrategy::Throws`], the generic `_checkErr` (plain `WeaveFFIError`; -/// panics, marshalling failures, and foreign callback failures only) for -/// [`ErrorStrategy::Trap`]. -pub(crate) fn js_checker_name(f: &FnBinding, error: Option<&ErrorBinding>) -> String { - match (f.error_strategy(), error) { - (ErrorStrategy::Throws, Some(eb)) => js_error_checker_name(eb), - _ => "_checkErr".to_string(), - } -} - -/// The rejection factory a throwing async callable stores in its context so -/// the completion callback maps domain codes to the typed error, or `None` -/// for [`ErrorStrategy::Trap`] callables (which reject with the generic -/// brand error). -pub(crate) fn js_err_factory(f: &FnBinding, error: Option<&ErrorBinding>) -> Option { - match (f.error_strategy(), error) { - (ErrorStrategy::Throws, Some(eb)) => Some(js_error_factory_name(eb)), - _ => None, - } -} - -/// The loader-scoped JS constant holding the linear-memory address of the -/// static vtable for the (possibly dot-qualified) callback interface `name`. -/// Callback interface names are unique across the API, so the local name is -/// collision-free. -pub(crate) fn js_vtable_name(name: &str) -> String { - format!("_vtable_{}", local_type_name(name)) -} - -/// True when `f` takes at least one callback-interface parameter, which the -/// Emscripten loader cannot support (see [`crate::calls`]). -pub(crate) fn takes_callback(f: &FnBinding) -> bool { - f.params - .iter() - .any(|p| matches!(p.ty, Ty::CallbackInterface(_))) -} - -/// True when `f` is unsupported in Emscripten mode and rendered as a throwing -/// stub there: an async function or one taking a callback interface. -pub(crate) fn emscripten_stub(f: &FnBinding) -> bool { - f.is_async || takes_callback(f) -} - -/// Escape a string for embedding in a double-quoted JS literal. -pub(crate) fn js_str_literal(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") -} - -/// How a generated JS callable is declared: as a property of a module object -/// literal (`name() {...},`), as an instance member of an interface class -/// (`name() {...}`), or as a static member (`static name() {...}`). -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum JsDecl { - /// Object-literal property (module objects); comma-terminated. - Object, - /// Class instance method; no terminator comma. - Method, - /// Class static method; no terminator comma. - Static, -} - -impl JsDecl { - /// The declaration keyword prefix (`static ` for statics). - pub(crate) fn prefix(self) -> &'static str { - match self { - JsDecl::Static => "static ", - _ => "", - } - } - - /// The block terminator (object-literal members carry a trailing comma). - pub(crate) fn close(self) -> &'static str { - match self { - JsDecl::Object => "},", - _ => "}", - } - } -} diff --git a/crates/weaveffi-gen-wasm/tests/abi2.rs b/crates/weaveffi-gen-wasm/tests/abi2.rs deleted file mode 100644 index 8a018cc0..00000000 --- a/crates/weaveffi-gen-wasm/tests/abi2.rs +++ /dev/null @@ -1,695 +0,0 @@ -//! ABI revision 2 rendering checks: reference-counted object wrappers, -//! nullable objects, object tokens inside value buffers, iterators of objects, -//! callback-interface vtables with their trampolines, the Emscripten -//! capability ceiling, and the npm package layout. - -use camino::Utf8Path; -use weaveffi_core::backend::LanguageBackend; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::BindingModel; -use weaveffi_core::package::{FileContent, PackageContext, PackagedFile}; -use weaveffi_core::platform::{BinarySet, Platform}; -use weaveffi_core::validate::validate_api; -use weaveffi_gen_wasm::{WasmConfig, WasmGenerator}; -use weaveffi_ir::ir::{ - Api, CallbackInterfaceDef, EnumDef, EnumVariant, Function, InterfaceDef, Module, Param, - StructDef, StructField, TypeRef, CURRENT_SCHEMA_VERSION, -}; - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - doc: None, - } -} - -fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn named(name: &str) -> TypeRef { - TypeRef::Named(name.into()) -} - -/// One module exercising every revision-2 shape: an interface with a -/// constructor and a method, `Interface?` in and out, a record with an -/// `Interface` field and a `[Interface]` field, an iterator over interface -/// elements, a callback interface with string, `i32`, record, and object -/// parameters (one `bool` return, one `void`), and a function taking it. -fn fixture() -> Api { - Api { - version: CURRENT_SCHEMA_VERSION.into(), - modules: vec![Module { - name: "bus".into(), - doc: None, - functions: vec![ - func( - "lookup", - vec![param( - "fallback", - TypeRef::Optional(Box::new(named("Ticker"))), - )], - Some(TypeRef::Optional(Box::new(named("Ticker")))), - ), - func( - "tickers", - vec![], - Some(TypeRef::Iterator(Box::new(named("Ticker")))), - ), - func( - "subscribe", - vec![param("listener", named("Subscriber"))], - None, - ), - func( - "describe", - vec![param("env", named("Envelope"))], - Some(named("Envelope")), - ), - Function { - r#async: true, - ..func( - "fetch", - vec![param("id", TypeRef::I64)], - Some(TypeRef::Optional(Box::new(named("Ticker")))), - ) - }, - Function { - r#async: true, - ..func( - "checksum", - vec![param("id", TypeRef::I64)], - Some(TypeRef::U64), - ) - }, - ], - interfaces: vec![InterfaceDef { - name: "Ticker".into(), - doc: Some("A counter.".into()), - deprecated: None, - constructors: vec![func("new", vec![param("start", TypeRef::I64)], None)], - methods: vec![ - func("value", vec![], Some(TypeRef::I64)), - func("mask", vec![], Some(TypeRef::U64)), - func("bits", vec![], Some(TypeRef::U32)), - ], - statics: vec![], - }], - callback_interfaces: vec![CallbackInterfaceDef { - name: "Subscriber".into(), - doc: Some("Receives bus events.".into()), - deprecated: None, - methods: vec![ - func( - "on_message", - vec![ - param("text", TypeRef::StringUtf8), - param("weight", TypeRef::I32), - param("envelope", named("Envelope")), - ], - Some(TypeRef::Bool), - ), - func( - "on_ticker", - vec![ - param("ticker", named("Ticker")), - param("alt", TypeRef::Optional(Box::new(named("Ticker")))), - ], - None, - ), - func( - "classify", - vec![param("weight", TypeRef::I32)], - Some(named("Priority")), - ), - func( - "on_mask", - vec![param("mask", TypeRef::U64), param("bits", TypeRef::U32)], - None, - ), - ], - }], - structs: vec![StructDef { - name: "Envelope".into(), - doc: None, - deprecated: None, - fields: vec![ - field("topic", TypeRef::StringUtf8), - field("primary", named("Ticker")), - field("others", TypeRef::List(Box::new(named("Ticker")))), - ], - }], - enums: vec![EnumDef { - name: "Priority".into(), - doc: None, - deprecated: None, - variants: vec![ - EnumVariant { - name: "Low".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "High".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - errors: None, - modules: vec![], - }], - } -} - -fn render_with(config: &WasmConfig) -> (String, String) { - let api = validate_api(fixture(), None).expect("fixture validates"); - let model = BindingModel::build(&api, WasmGenerator.prefix(config)); - let files = WasmGenerator.files(&api, &model, Utf8Path::new("out"), config); - let pick = |suffix: &str| { - files - .iter() - .find(|f| f.path.as_str().ends_with(suffix)) - .unwrap_or_else(|| panic!("no {suffix} output")) - .contents - .clone() - }; - (pick("weaveffi_wasm.js"), pick("weaveffi_wasm.d.ts")) -} - -fn render() -> (String, String) { - render_with(&WasmConfig::default()) -} - -fn assert_has(src: &str, needle: &str) { - assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); -} - -fn emscripten_config() -> WasmConfig { - WasmConfig { - emscripten: true, - ..WasmConfig::default() - } -} - -#[test] -fn objects_are_reference_counted_wrappers_with_finalizers() { - let (js, dts) = render(); - // The class lives inside the loader and is exposed on the module object. - assert_has(&js, " class Ticker {\n constructor(start) {"); - assert_has(&js, " Ticker: Ticker,"); - // The constructor adopts the returned reference with the destroy symbol. - assert_has( - &js, - "_r = wasm.weaveffi_bus_Ticker_new(BigInt(start), _err);", - ); - assert_has(&js, "_adopt(this, _r, wasm.weaveffi_bus_Ticker_destroy);"); - // Internal adoption path used by returns, tokens, iterators, and callbacks. - assert_has( - &js, - "static _wrap(handle) {\n return _adopt(Object.create(Ticker.prototype), handle, wasm.weaveffi_bus_Ticker_destroy);", - ); - // Clone symbol produces the second reference for object tokens. - assert_has( - &js, - "_clone() {\n return wasm.weaveffi_bus_Ticker_clone(_borrow(this));", - ); - // close() and Symbol.dispose both funnel into the once-only release. - assert_has(&js, "close() {\n _release(this);"); - assert_has(&js, "[_dispose]() {\n _release(this);"); - assert_has( - &js, - "const _dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose');", - ); - assert_has( - &js, - "function _release(obj) {\n if (!obj._handle) return;\n if (_finalizer !== null) _finalizer.unregister(obj);\n obj._destroy(obj._handle);\n obj._handle = 0;\n}", - ); - // The FinalizationRegistry backstop holds [destroy, handle], never the wrapper. - assert_has( - &js, - "new FinalizationRegistry(([destroy, handle]) => destroy(handle))", - ); - assert_has( - &js, - "if (_finalizer !== null) _finalizer.register(obj, [destroy, handle], obj);", - ); - // Methods lend the pointer as the implicit self. - assert_has( - &js, - "_r = wasm.weaveffi_bus_Ticker_value(_borrow(this), _err);", - ); - assert_has( - &js, - "throw new WeaveFFIError(-3, 'expected a live object wrapper');", - ); - - assert_has(&dts, "export declare class Ticker {"); - assert_has(&dts, " constructor(start: bigint);"); - assert_has(&dts, " value(): bigint;"); - assert_has(&dts, " close(): void;"); - assert_has(&dts, "Also reachable as `[Symbol.dispose]()` for `using`"); - assert_has(&dts, " Ticker: typeof Ticker;"); -} - -#[test] -fn nullable_objects_are_nullable_pointers_both_ways() { - let (js, dts) = render(); - assert_has( - &js, - "_r = wasm.weaveffi_bus_lookup((fallback === null || fallback === undefined ? 0 : _borrow(fallback)), _err);", - ); - assert_has(&js, "return _r === 0 ? null : Ticker._wrap(_r);"); - // Async results adopt the same way. - assert_has(&js, "unwrap: (w, h) => h === 0 ? null : Ticker._wrap(h)"); - assert_has(&dts, "lookup(fallback: Ticker | null): Ticker | null;"); - assert_has(&dts, "fetch(id: bigint): Promise;"); -} - -#[test] -fn objects_inside_records_are_cloned_tokens() { - let (js, dts) = render(); - // Encoding writes a fresh clone as a u64 token, never the wrapper's own pointer. - assert_has(&js, "w.obj(v.primary._clone());"); - assert_has(&js, "for (const _e1 of _a1) {\n w.obj(_e1._clone());"); - assert_has(&js, "obj(ptr) { this.u64(BigInt(ptr >>> 0)); }"); - // Decoding adopts each token into a new wrapper and rejects zero tokens. - assert_has(&js, "v.primary = Ticker._wrap(r.obj());"); - assert_has(&js, "_arr.push(Ticker._wrap(r.obj()));"); - assert_has( - &js, - "if (t === 0n || t > 0xffffffffn) this._bad('object token out of range');", - ); - assert_has( - &dts, - "export interface Envelope {\n topic: string;\n primary: Ticker;\n others: Ticker[];\n}", - ); -} - -#[test] -fn iterators_adopt_object_elements() { - let (js, dts) = render(); - assert_has(&js, "_it = wasm.weaveffi_bus_tickers(_err);"); - assert_has(&js, "return new _WeaveFFIIterator(wasm, _it, 4,"); - assert_has( - &js, - "(it, slot, ep) => wasm.weaveffi_bus_TickersIterator_next(it, slot, ep),", - ); - assert_has( - &js, - "(it) => wasm.weaveffi_bus_TickersIterator_destroy(it),", - ); - assert_has( - &js, - "(w, p) => Ticker._wrap(new DataView(w.memory.buffer).getUint32(p, true))", - ); - assert_has(&dts, "tickers(): IterableIterator;"); -} - -#[test] -fn callback_interfaces_render_ts_interface_vtable_and_trampolines() { - let (js, dts) = render(); - // Consumer-facing TS interface. - assert_has( - &dts, - "export interface Subscriber {\n onMessage(text: string, weight: number, envelope: Envelope): boolean;\n onTicker(ticker: Ticker, alt: Ticker | null): void;\n classify(weight: number): Priority;\n onMask(mask: bigint, bits: number): void;\n}", - ); - assert_has(&dts, "subscribe(listener: Subscriber): void;"); - - // The handle map keyed by the integer ctx. - assert_has(&js, "let _nextCbId = 1;\n const _callbacks = new Map();"); - - // One trampoline per method, whose wasm signature mirrors abi_params - // (ctx, params..., out_err) and abi_ret. - assert_has( - &js, - "const _cb_Subscriber_on_message = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], ['i32'], (_ctx, a0, a1, a2, a3, _err) => {", - ); - assert_has( - &js, - "const _cb_Subscriber_on_ticker = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32'], [], (_ctx, a0, a1, _err) => {", - ); - assert_has( - &js, - "const _cb_Subscriber_classify = _registerTrampoline(_table, ['i32', 'i32', 'i32'], ['i32'], (_ctx, a0, _err) => {", - ); - // Borrowed args are decoded (string, i32, record), object args adopted. - assert_has(&js, "const _impl = _callbacks.get(_ctx);"); - assert_has(&js, "const _p0 = _readCStr(wasm, a0);"); - assert_has(&js, "const _p1 = a1;"); - assert_has( - &js, - "const _p2 = _read_bus_Envelope(_p2_r);\n _p2_r.end();", - ); - assert_has(&js, "return _impl.onMessage(_p0, _p1, _p2) ? 1 : 0;"); - assert_has( - &js, - "const _p0 = Ticker._wrap(a0);\n const _p1 = a1 === 0 ? null : Ticker._wrap(a1);\n if (_pendingForeign !== null) {", - ); - assert_has(&js, "return _impl.classify(_p0);"); - // Failure path: error_set with -4 and a default return. - assert_has( - &js, - "} catch (e) {\n _setForeignError(wasm, _err, e);\n return 0;\n }", - ); - assert_has( - &js, - "} catch (e) {\n _setForeignError(wasm, _err, e);\n }", - ); - assert_has( - &js, - "function _reportForeign(wasm, errPtr, msg) {\n if (_pendingForeign === null) _pendingForeign = msg;\n const [p, s] = _cstr(wasm, msg);\n wasm.weaveffi_error_set(errPtr, -4, p);\n wasm.weaveffi_dealloc(p, s);\n}", - ); - assert_has( - &js, - "function _setForeignError(wasm, errPtr, e) {\n _reportForeign(wasm, errPtr, e instanceof Error ? (e.message || e.name) : String(e));\n}", - ); - // Without unwinding the producer keeps calling back after a failure; - // those invocations are refused after their arguments are adopted. - assert_has( - &js, - "if (_pendingForeign !== null) {\n _reportForeign(wasm, _err, _pendingForeign);\n return 0;\n }\n return _impl.classify(_p0);", - ); - assert_has( - &js, - "if (_pendingForeign !== null) {\n _reportForeign(wasm, _err, _pendingForeign);\n return;\n }\n _impl.onTicker(_p0, _p1);", - ); - // Every producer-call completion path clears the parked failure. - assert_has( - &js, - "function _freeErr(wasm, errPtr) {\n _pendingForeign = null;", - ); - assert_has( - &js, - "function _checkErr(wasm, errPtr) {\n _pendingForeign = null;", - ); - assert_has( - &js, - "function _checkErrRef(wasm, errPtr, mkErr) {\n _pendingForeign = null;", - ); - // free deletes the map entry. - assert_has( - &js, - "const _cb_Subscriber_free = _registerTrampoline(_table, ['i32'], [], (_ctx) => { _callbacks.delete(_ctx); });", - ); - - // Exactly one static vtable, allocated once with the module allocator and - // filled with table indices in declaration order, then free. - assert_eq!( - js.matches("const _vtable_Subscriber = wasm.weaveffi_alloc(20);") - .count(), - 1 - ); - assert_has( - &js, - "_dv.setUint32(_vtable_Subscriber + 0, _cb_Subscriber_on_message, true);\n _dv.setUint32(_vtable_Subscriber + 4, _cb_Subscriber_on_ticker, true);\n _dv.setUint32(_vtable_Subscriber + 8, _cb_Subscriber_classify, true);\n _dv.setUint32(_vtable_Subscriber + 12, _cb_Subscriber_on_mask, true);\n _dv.setUint32(_vtable_Subscriber + 16, _cb_Subscriber_free, true);", - ); - - // Passing an implementation: map key as ctx plus the vtable address. - assert_has( - &js, - "subscribe(listener) {\n const a0_ctx = _nextCbId++;\n _callbacks.set(a0_ctx, listener);", - ); - assert_has( - &js, - "wasm.weaveffi_bus_subscribe(a0_ctx, _vtable_Subscriber, _err);", - ); -} - -/// `wasm32-unknown-unknown` can't unwind, so a producer panic reaches JS as a -/// `WebAssembly.RuntimeError` trap. Every producer call is guarded: the trap -/// is translated into the brand error (`-4` if a callback failure was parked -/// during the call, `-2` otherwise), the error slot is released, staged inputs -/// are freed in a `finally`, and async launchers reject their Promise and -/// forget the context. -#[test] -fn traps_are_translated_into_brand_errors() { - let (js, _) = render(); - assert_has(&js, "let _pendingForeign = null;"); - assert_has( - &js, - "function _trapError(e) {\n const foreign = _pendingForeign;\n _pendingForeign = null;\n if (!(e instanceof WebAssembly.RuntimeError)) return e;\n if (foreign !== null) return new WeaveFFIError(-4, foreign);\n return new WeaveFFIError(-2, 'producer panicked: ' + e.message);\n}", - ); - assert_has( - &js, - "function _trap(wasm, errPtr, e) {\n _freeErr(wasm, errPtr);\n return _trapError(e);\n}", - ); - // Sync wrapper: guarded call, then the checker on the untouched slot. - assert_has( - &js, - "const _err = _allocErr(wasm);\n let _r;\n try {\n _r = wasm.weaveffi_bus_lookup((fallback === null || fallback === undefined ? 0 : _borrow(fallback)), _err);\n } catch (e) {\n throw _trap(wasm, _err, e);\n }\n _checkErr(wasm, _err);", - ); - // A buffered return releases its out-length slot on the trap path and - // frees the staged argument in `finally`. - assert_has( - &js, - "const _lp = wasm.weaveffi_alloc(4);\n const _err = _allocErr(wasm);\n let _r;\n try {\n _r = wasm.weaveffi_bus_describe(a0_p, a0_l, _lp, _err);\n } catch (e) {\n wasm.weaveffi_dealloc(_lp, 4);\n throw _trap(wasm, _err, e);\n } finally {\n wasm.weaveffi_dealloc(a0_p, a0_l);\n }", - ); - // Constructor and iterator launch are guarded the same way. - assert_has( - &js, - "let _r;\n try {\n _r = wasm.weaveffi_bus_Ticker_new(BigInt(start), _err);\n } catch (e) {\n throw _trap(wasm, _err, e);\n }", - ); - assert_has( - &js, - "let _it;\n try {\n _it = wasm.weaveffi_bus_tickers(_err);\n } catch (e) {\n throw _trap(wasm, _err, e);\n }", - ); - // Iterator steps release the slot on a trap before rethrowing. - assert_has( - &js, - "_has = this._callNext(this._handle, this._slot, _err);\n } catch (e) {\n // A trap: the slot was never filled, so release it here.\n this._close();\n throw _trap(wasm, _err, e);\n }", - ); - // Async launch: forget the context and reject with the translated error. - assert_has( - &js, - "try {\n wasm.weaveffi_bus_fetch_async(BigInt(id), _cbPtr_i32_i32_i32, ctxId);\n } catch (e) {\n _asyncContexts.delete(ctxId);\n reject(_trapError(e));\n }", - ); -} - -/// Wasm integers are signed on the JS side, so unsigned scalars arriving -/// through a direct slot (sync returns, async completions, callback -/// parameters) are reinterpreted: `u32` via `>>> 0`, `u64` via -/// `BigInt.asUintN(64, ...)`. Signed and 32-bit-or-narrower values need no -/// coercion, and inputs rely on the wasm calling convention's own wrapping. -#[test] -fn unsigned_scalars_are_reinterpreted_on_the_way_out() { - let (js, _) = render(); - assert_has( - &js, - "_r = wasm.weaveffi_bus_Ticker_mask(_borrow(this), _err);", - ); - assert_has(&js, "return BigInt.asUintN(64, _r);"); - assert_has( - &js, - "_r = wasm.weaveffi_bus_Ticker_bits(_borrow(this), _err);", - ); - assert_has(&js, "return _r >>> 0;"); - // Signed 64-bit returns stay as the BigInt wasm hands back. - assert_has( - &js, - "_r = wasm.weaveffi_bus_Ticker_value(_borrow(this), _err);\n } catch (e) {\n throw _trap(wasm, _err, e);\n }\n _checkErr(wasm, _err);\n _freeErr(wasm, _err);\n return _r;", - ); - // Async completion of a u64 result. - assert_has( - &js, - "_asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, r) => BigInt.asUintN(64, r) });", - ); - assert_has( - &js, - "wasm.weaveffi_bus_checksum_async(BigInt(id), _cbPtr_i32_i32_i64, ctxId);", - ); - // Callback parameters. - assert_has( - &js, - "const _p0 = BigInt.asUintN(64, a0);\n const _p1 = a1 >>> 0;\n if (_pendingForeign !== null) {", - ); -} - -#[test] -fn runtime_checks_abi_revision_and_drops_legacy_surfaces() { - let (js, dts) = render(); - assert_has(&js, "const _ABI_VERSION = 2;"); - assert_has(&js, "_checkAbiVersion(wasm);"); - assert_has(&js, "wasm.weaveffi_error_set("); - for gone in [ - "arena", - "weaveffi_handle_t", - "TypedHandle", - "register_", - "_listeners", - "free()", - ".destroy()", - ] { - assert!(!js.contains(gone), "stale `{gone}` in loader"); - assert!(!dts.contains(gone), "stale `{gone}` in declarations"); - } -} - -#[test] -fn emscripten_mode_is_partial_and_stubs_unsupported_entry_points() { - let config = emscripten_config(); - assert_eq!( - WasmGenerator.capabilities(&config), - TargetCapabilities { - async_functions: false, - callback_interfaces: false, - iterators: true, - } - ); - assert_eq!( - WasmGenerator.capabilities(&WasmConfig::default()), - TargetCapabilities::full() - ); - assert!(!WasmGenerator.allows_unsupported(&config)); - assert!(WasmGenerator.allows_unsupported(&WasmConfig { - allow_unsupported: true, - ..emscripten_config() - })); - - let (js, dts) = render_with(&config); - assert_has( - &js, - "subscribe(listener) {\n throw new Error(\"weaveffi: function 'subscribe' (it takes a callback interface) is not supported in Emscripten mode;", - ); - assert_has( - &js, - "fetch(id) {\n throw new Error(\"weaveffi: async function 'fetch' is not supported in Emscripten mode;", - ); - // No trampoline machinery, but objects and iterators still work. - assert!( - !js.contains("_registerTrampoline"), - "trampolines in Emscripten glue" - ); - assert!(!js.contains("_vtable_"), "vtable in Emscripten glue"); - assert_has( - &js, - "get memory() { return { buffer: m['HEAPU8'].buffer }; },", - ); - assert_has( - &js, - "weaveffi_bus_Ticker_clone: m['_weaveffi_bus_Ticker_clone'],", - ); - assert_has( - &js, - "weaveffi_bus_Ticker_destroy: m['_weaveffi_bus_Ticker_destroy'],", - ); - assert_has( - &js, - "weaveffi_bus_TickersIterator_next: m['_weaveffi_bus_TickersIterator_next'],", - ); - assert!( - !js.contains("m['_weaveffi_bus_subscribe']"), - "stubbed symbol bound" - ); - // The declarations omit the stubs and the callback interface entirely. - assert!(!dts.contains("subscribe("), "stubbed function declared"); - assert!(!dts.contains("fetch("), "stubbed async function declared"); - assert!( - !dts.contains("interface Subscriber"), - "callback interface declared" - ); - assert_has(&dts, "lookup(fallback: Ticker | null): Ticker | null;"); - assert_has(&dts, "tickers(): IterableIterator;"); -} - -fn packaged_text<'a>(files: &'a [PackagedFile], suffix: &str) -> &'a str { - files - .iter() - .find(|f| f.path.as_str().ends_with(suffix)) - .and_then(|f| match &f.content { - FileContent::Text(s) => Some(s.as_str()), - FileContent::Copy(_) => None, - }) - .unwrap_or_else(|| panic!("no packaged {suffix}")) -} - -fn package_with(config: &WasmConfig) -> Vec { - let api = validate_api(fixture(), None).expect("fixture validates"); - let model = BindingModel::build(&api, WasmGenerator.prefix(config)); - let mut binaries = BinarySet::new("bus"); - for p in Platform::ALL { - binaries.insert(p, format!("/prebuilt/{}/lib", p.id())); - } - let ctx = PackageContext { - binaries: &binaries, - input_basename: Some("bus.yml"), - }; - WasmGenerator - .package(&api, &model, &ctx, Utf8Path::new("out"), config) - .expect("wasm packages") -} - -#[test] -fn package_bundles_only_the_wasm32_binary() { - let files = package_with(&WasmConfig::default()); - let bundled: Vec<&PackagedFile> = files.iter().filter(|f| f.is_binary()).collect(); - assert_eq!(bundled.len(), 1, "{bundled:?}"); - assert_eq!(bundled[0].path.as_str(), "out/wasm/bus.wasm"); - assert_eq!( - bundled[0].content, - FileContent::Copy("/prebuilt/wasm32/lib".into()) - ); - let mut paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect(); - paths.sort_unstable(); - assert_eq!( - paths, - [ - "out/wasm/README.md", - "out/wasm/bus.wasm", - "out/wasm/package.json", - "out/wasm/weaveffi_wasm.d.ts", - "out/wasm/weaveffi_wasm.js", - ] - ); - let manifest = packaged_text(&files, "package.json"); - assert_has(manifest, "\"main\": \"weaveffi_wasm.js\""); - assert_has(manifest, "\"types\": \"weaveffi_wasm.d.ts\""); - assert_has( - manifest, - "\"weaveffi_wasm.js\",\n \"weaveffi_wasm.d.ts\",\n \"bus.wasm\"", - ); - let readme = packaged_text(&files, "README.md"); - assert_has(readme, "bundled as `bus.wasm`"); - assert_has(readme, "new URL('./bus.wasm', import.meta.url)"); - // The loader and declarations are the same as `generate` emits. - let (js, dts) = render(); - assert_eq!(packaged_text(&files, "weaveffi_wasm.js"), js); - assert_eq!(packaged_text(&files, "weaveffi_wasm.d.ts"), dts); -} - -#[test] -fn emscripten_package_ships_without_a_binary() { - let files = package_with(&emscripten_config()); - assert!( - files.iter().all(|f| !f.is_binary()), - "binary bundled in Emscripten package" - ); - let manifest = packaged_text(&files, "package.json"); - assert!( - !manifest.contains("\"files\""), - "files list without a binary" - ); - let readme = packaged_text(&files, "README.md"); - assert_has(readme, "The package carries no binary"); - assert_has(readme, "const api = await loadWeaveffiWasm(Module());"); -} diff --git a/crates/weaveffi-core/Cargo.toml b/crates/weaveffi-gen/Cargo.toml similarity index 56% rename from crates/weaveffi-core/Cargo.toml rename to crates/weaveffi-gen/Cargo.toml index 80f8def5..2579d4b8 100644 --- a/crates/weaveffi-core/Cargo.toml +++ b/crates/weaveffi-gen/Cargo.toml @@ -1,41 +1,33 @@ [package] -name = "weaveffi-core" -version = "0.23.0" +name = "weaveffi-gen" +version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true -description = "Generator trait, orchestrator, validation, and shared utilities for WeaveFFI" -keywords = ["ffi", "codegen", "bindings", "code-generation", "validation"] +description = "Code-generation orchestrator and the eleven language generators for WeaveFFI" +keywords = ["ffi", "codegen", "bindings", "code-generation", "generator"] categories = ["development-tools::ffi"] [dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } +weaveffi-model = { workspace = true, features = ["idl"] } anyhow = { workspace = true } thiserror = { workspace = true } camino = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } -miette = { workspace = true } rayon = { workspace = true } heck = { workspace = true } [dev-dependencies] tempfile = "3" criterion = { workspace = true } -toml = { workspace = true } [[bench]] name = "codegen_bench" harness = false -[[bin]] -name = "hook_helper" -path = "tests/bin/hook_helper.rs" -test = false -doc = false -bench = false - [lints] workspace = true diff --git a/crates/weaveffi-core/benches/codegen_bench.rs b/crates/weaveffi-gen/benches/codegen_bench.rs similarity index 92% rename from crates/weaveffi-core/benches/codegen_bench.rs rename to crates/weaveffi-gen/benches/codegen_bench.rs index f0cd87f4..54184c58 100644 --- a/crates/weaveffi-core/benches/codegen_bench.rs +++ b/crates/weaveffi-gen/benches/codegen_bench.rs @@ -1,14 +1,14 @@ use std::path::Path; use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use weaveffi_ir::ir::{ +use weaveffi_model::ir::{ Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, }; -use weaveffi_ir::parse::parse_api_str; +use weaveffi_model::parse::parse_api_str; fn calculator_api() -> Api { Api { - version: "0.9.0".to_string(), + version: "0.10.0".to_string(), modules: vec![Module { name: "calculator".to_string(), doc: None, @@ -206,7 +206,7 @@ fn large_api() -> Api { .collect(); Api { - version: "0.9.0".to_string(), + version: "0.10.0".to_string(), modules, } } @@ -215,7 +215,7 @@ fn bench_validate_small_api(c: &mut Criterion) { let api = calculator_api(); c.bench_function("validate_small_api", |b| { b.iter(|| { - weaveffi_core::validate::validate_api(black_box(api.clone()), None).unwrap(); + weaveffi_model::validate::validate_api(black_box(api.clone()), None).unwrap(); }); }); } @@ -224,16 +224,16 @@ fn bench_validate_large_api(c: &mut Criterion) { let api = large_api(); c.bench_function("validate_large_api", |b| { b.iter(|| { - weaveffi_core::validate::validate_api(black_box(api.clone()), None).unwrap(); + weaveffi_model::validate::validate_api(black_box(api.clone()), None).unwrap(); }); }); } fn bench_hash_api(c: &mut Criterion) { - let api = weaveffi_core::validate::validate_api(large_api(), None).unwrap(); + let api = weaveffi_model::validate::validate_api(large_api(), None).unwrap(); c.bench_function("hash_large_api", |b| { b.iter(|| { - weaveffi_core::cache::hash_api(black_box(api.api())); + black_box(weaveffi_gen::cache::hash_api(black_box(api.api()))); }); }); } @@ -254,17 +254,18 @@ fn bench_validate_kitchen_sink(c: &mut Criterion) { let api = load_kitchen_sink_unvalidated(); c.bench_function("validate_kitchen_sink", |b| { b.iter(|| { - weaveffi_core::validate::validate_api(black_box(api.clone()), None).unwrap(); + weaveffi_model::validate::validate_api(black_box(api.clone()), None).unwrap(); }); }); } /// Target: hash_api < 1ms for the kitchen-sink fixture. fn bench_hash_kitchen_sink(c: &mut Criterion) { - let api = weaveffi_core::validate::validate_api(load_kitchen_sink_unvalidated(), None).unwrap(); + let api = + weaveffi_model::validate::validate_api(load_kitchen_sink_unvalidated(), None).unwrap(); c.bench_function("hash_kitchen_sink", |b| { b.iter(|| { - weaveffi_core::cache::hash_api(black_box(api.api())); + black_box(weaveffi_gen::cache::hash_api(black_box(api.api()))); }); }); } diff --git a/crates/weaveffi-core/src/backend/mod.rs b/crates/weaveffi-gen/src/backend/mod.rs similarity index 73% rename from crates/weaveffi-core/src/backend/mod.rs rename to crates/weaveffi-gen/src/backend/mod.rs index deb623b1..7527f9bf 100644 --- a/crates/weaveffi-core/src/backend/mod.rs +++ b/crates/weaveffi-gen/src/backend/mod.rs @@ -7,29 +7,26 @@ //! manifests. //! //! [`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, +//! common structure as hooks; [`ConfiguredBackend`](crate::codegen::ConfiguredBackend) +//! builds the model and exposes the backend to the orchestrator through the +//! object-safe [`Target`](crate::codegen::Target) trait. A backend owns *only* language-specific rendering: type mapping, //! marshalling, 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 +//! [`BindingModel`]: weaveffi_model::model::BindingModel +//! [`CallShape`]: weaveffi_model::model::CallShape -use anyhow::Result; use camino::{Utf8Path, Utf8PathBuf}; use serde::Serialize; use crate::capabilities::TargetCapabilities; -use crate::model::{ +use crate::package::{PackageContext, PackagedFile}; +use weaveffi_model::model::{ BindingModel, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, ModuleBinding, StructBinding, }; -use crate::package::{PackageContext, PackagedFile}; -use crate::resolved::ResolvedApi; +use weaveffi_model::resolved::ResolvedApi; /// A single generated file: its full path (under the output directory) and the /// rendered contents. Backends return these from [`LanguageBackend::files`]; @@ -44,9 +41,19 @@ pub struct OutputFile { impl OutputFile { /// Pair a destination path with its rendered contents. + /// + /// The path is normalized to `/` separators, which every platform's file + /// APIs accept, so listings, cache records, and tests see the same path + /// on Windows as elsewhere. pub fn new(path: impl Into, contents: impl Into) -> Self { + let path: Utf8PathBuf = path.into(); + let path = if path.as_str().contains('\\') { + Utf8PathBuf::from(path.as_str().replace('\\', "/")) + } else { + path + }; Self { - path: path.into(), + path, contents: contents.into(), } } @@ -58,9 +65,8 @@ impl OutputFile { /// [`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`. +/// orchestrator. That alone gives every backend the shared driver and the +/// [`OutputFile`] model (rendering is pure; the orchestrator does the I/O). /// /// Backends whose primary file is a straightforward per-module walk (Python, /// Ruby, Go, Dart, .NET) override the per-entity hooks (`render_enum`, @@ -111,15 +117,6 @@ pub trait LanguageBackend: Send + Sync { false } - /// The C ABI symbol prefix the producer used. The driver builds the - /// [`BindingModel`] with it so every emitted call targets the right - /// exported symbol. Defaults to `"weaveffi"`; override when the config - /// carries a configurable `c_prefix`. - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - let _ = config; - "weaveffi" - } - /// Render one enum (its declaration and any helpers), including doc /// comments. Override when using [`emit_members`](Self::emit_members). fn render_enum(&self, out: &mut String, e: &EnumBinding, config: &Self::Config) { @@ -223,7 +220,7 @@ pub trait LanguageBackend: Send + Sync { } /// Assemble the complete output set. The driver has already built `model` - /// (via [`BindingModel::build`] with [`prefix`](Self::prefix)) and passes + /// (via [`BindingModel::build`], with the API identity's prefix) and passes /// the resolved `api` too, for the rare file (e.g. a `.pyi` stub) that /// needs the full resolved tree. Most backends render a primary source /// file by composing [`emit_members`](Self::emit_members) over @@ -263,89 +260,14 @@ pub trait LanguageBackend: Send + Sync { } } -/// Build the model and write every file a backend produces. This is the body -/// of [`Target::generate`](crate::codegen::Target::generate). -/// -/// # Errors -/// -/// Returns an error if a parent directory cannot be created or any file the -/// backend produced cannot be written. -pub fn run( - backend: &B, - api: &ResolvedApi, - out_dir: &Utf8Path, - config: &B::Config, -) -> Result<()> { - let model = BindingModel::build(api, backend.prefix(config)); - for file in backend.files(api, &model, out_dir, config) { - if let Some(parent) = file.path.parent() { - std::fs::create_dir_all(parent.as_std_path())?; - } - std::fs::write(file.path.as_std_path(), file.contents)?; - } - Ok(()) -} - -/// Render a path for listing with `/` separators on every platform. -/// -/// `Utf8Path::join` emits the platform separator, so on Windows a backend's -/// `out_dir.join("c").join("weaveffi.h")` yields `c\weaveffi.h`. The listing -/// surfaced by `--dry-run` and `weaveffi diff` (and asserted by the snapshot -/// and unit suites) must be OS-independent, so fold `\` back to `/`. A no-op -/// off Windows, where `\` is a legal filename byte we must not rewrite. -fn forward_slashes(path: Utf8PathBuf) -> String { - let s = path.into_string(); - if cfg!(windows) { - s.replace('\\', "/") - } else { - s - } -} - -/// 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, - out_dir: &Utf8Path, - config: &B::Config, -) -> Vec { - let model = BindingModel::build(api, backend.prefix(config)); - let mut paths: Vec = backend - .files(api, &model, out_dir, config) - .into_iter() - .map(|f| forward_slashes(f.path)) - .collect(); - paths.sort(); - paths -} - -/// Build the model and assemble the package a backend produces, the body of -/// [`Target::package`](crate::codegen::Target::package). Returns `None` when -/// the backend does not support packaging. -pub fn package_files( - backend: &B, - api: &ResolvedApi, - ctx: &PackageContext, - out_dir: &Utf8Path, - config: &B::Config, -) -> Option> { - let model = BindingModel::build(api, backend.prefix(config)); - backend.package(api, &model, ctx, out_dir, config) -} - #[cfg(test)] mod tests { use super::*; - use crate::model::CallShape; - use weaveffi_ir::ir::{Api, Function, Module, Param, TypeRef}; + use weaveffi_model::ir::{Api, Function, Module, Param, TypeRef}; + use weaveffi_model::model::CallShape; #[derive(Default, Clone, serde::Serialize)] - struct FakeConfig { - prefix: Option, - } + struct FakeConfig; /// A trivial backend that records the canonical traversal order so we can /// assert the driver walks and dispatches correctly. @@ -362,10 +284,6 @@ mod tests { TargetCapabilities::full() } - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix.as_deref().unwrap_or("weaveffi") - } - fn render_function( &self, out: &mut String, @@ -416,7 +334,7 @@ mod tests { fn api() -> ResolvedApi { ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), modules: vec![Module { name: "math".into(), doc: None, @@ -440,26 +358,22 @@ mod tests { } #[test] - 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(); + fn driver_walks_dispatches_and_honors_the_identity_prefix() { + use crate::codegen::{ConfiguredBackend, Target}; + let out_dir = Utf8Path::new("out"); + let render = |n: &str| { + let api = api().with_identity(weaveffi_model::pkg::Identity::named(n)); + let files = ConfiguredBackend::new(FakeBackend, FakeConfig).render(&api, out_dir); + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, out_dir.join("fake/out.txt")); + files[0].contents.clone() + }; assert_eq!( - body, + render("weaveffi"), "module math\nfn add [sync] weaveffi_math_add\nfn fetch [async] weaveffi_math_fetch\nfn scan [iter] weaveffi_math_scan\n" ); - - 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(); + let body = render("acme"); assert!(body.contains("acme_math_add"), "{body}"); assert!(!body.contains("weaveffi_math_add")); - - let files = output_files(&FakeBackend, &api(), out_dir, &FakeConfig::default()); - assert_eq!(files.len(), 1); - assert!(files[0].ends_with("fake/out.txt")); } } diff --git a/crates/weaveffi-core/src/cabi.rs b/crates/weaveffi-gen/src/cabi.rs similarity index 82% rename from crates/weaveffi-core/src/cabi.rs rename to crates/weaveffi-gen/src/cabi.rs index 469d0c64..7fc88834 100644 --- a/crates/weaveffi-core/src/cabi.rs +++ b/crates/weaveffi-gen/src/cabi.rs @@ -1,7 +1,7 @@ //! Shared rendering of the **C ABI declarations** from a -//! [`BindingModel`](crate::model::BindingModel). +//! [`BindingModel`]. //! -//! Both the C generator (which emits the canonical `{prefix}.h`) and the C++ +//! Both the C generator (which emits the canonical `{library}.h`) and the C++ //! generator (whose idiomatic wrapper opens an `extern "C"` block re-declaring //! the same symbols) render their C declarations through this module, so the //! two can't drift. @@ -11,23 +11,21 @@ use std::fmt::Write; -use crate::abi::AbiParam; 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, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, - InterfaceBinding, ModuleBinding, +use weaveffi_model::abi::AbiParam; +use weaveffi_model::model::{ + checksum_symbol, AbiFn, BindingModel, CallShape, CallbackInterfaceBinding, 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 = 2; +/// The C ABI revision the generated headers declare. Mirrors +/// `weaveffi_abi::ABI_VERSION`; the two are kept equal by a test in the +/// `weaveffi` crate. +pub use weaveffi_model::model::ABI_VERSION; /// Emit a `/** ... */` doc comment at `indent`. pub fn emit_doc(out: &mut String, doc: &Option, indent: &str) { @@ -152,33 +150,29 @@ pub fn fn_decl(out: &mut String, f: &AbiFn, prefix: &str) { ); } -/// Render the runtime typedefs and helper prototypes (`error`, `free_*`, -/// `alloc`/`dealloc`, `cancel_token`) that every WeaveFFI C surface depends -/// on. -/// -/// `alloc`/`dealloc` back the Wasm JavaScript glue, which stages strings, -/// 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` convenience file provides malloc/free-backed defaults. -pub fn render_runtime_decls(out: &mut String, prefix: &str) { +/// Render the runtime surface every producer exports (see the C ABI +/// contract): the ABI revision check, the error struct and its helpers, the +/// allocation release function, cancel tokens, the Wasm staging allocator, +/// and the debug leak counter, followed by one checksum declaration per +/// top-level module. +pub fn render_runtime_decls(out: &mut String, model: &BindingModel) { + let prefix = model.prefix.as_str(); let api = export_macro(prefix); let upper = prefix.to_uppercase(); let _ = write!( out, - "/* 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\ + "/* The C ABI revision this header was generated against. The producer\n \ + exports {prefix}_abi_version() so a consumer can refuse to load a\n \ + library built for a different revision. */\n\ #define {upper}_ABI_VERSION {ABI_VERSION}u\n\ {api} uint32_t {prefix}_abi_version(void);\n\n\ - /* Error slot written by every fallible call. `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 \ - and the payload are released by {prefix}_error_clear. Positive codes are\n \ - the module's declared error codes; negative codes are runtime traps:\n \ + /* Error slot written by every fallible call. `message` is NUL-terminated\n \ + UTF-8. `payload_ptr`/`payload_len` hold the matched error code's fields\n \ + serialized in the value buffer format (null when the code declares no\n \ + fields). Both are released by {prefix}_error_clear. Positive codes are\n \ + the API's declared error codes; negative codes are runtime traps:\n \ -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback\n \ - interface implementation raised. */\n\ + interface implementation failed, -5 cancelled. */\n\ typedef struct {prefix}_error {{\n \ int32_t code;\n \ const char* message;\n \ @@ -186,32 +180,54 @@ pub fn render_runtime_decls(out: &mut String, prefix: &str) { size_t payload_len;\n\ }} {prefix}_error;\n\n\ /* Fill `err` with `code` and a producer-owned copy of `message`. Callback\n \ - interface trampolines call this to report a failure in the consumer's\n \ - implementation (code -4) without allocating with a foreign allocator. */\n\ + interface implementations call this to report a failure (code -4)\n \ + without allocating with a foreign allocator. */\n\ {api} void {prefix}_error_set({prefix}_error* err, int32_t code, const char* message);\n\ {api} void {prefix}_error_clear({prefix}_error* err);\n\n\ - /* Async completion callbacks receive a heap-boxed error the consumer\n \ - owns; {prefix}_error_free releases the message, the payload, and the\n \ - box itself. Passing NULL is a safe no-op. */\n\ - {api} void {prefix}_error_free({prefix}_error* err);\n\ - {api} void {prefix}_free_string(const char* ptr);\n\ + /* Async completions receive a heap-boxed error the consumer owns;\n \ + {prefix}_error_free releases the message, the payload, and the box.\n \ + NULL is a no-op. */\n\ + {api} void {prefix}_error_free({prefix}_error* err);\n\n\ + /* Release a string, bytes, or value buffer the producer returned. */\n\ {api} void {prefix}_free_bytes(uint8_t* ptr, size_t len);\n\n\ - /* Linear-memory allocator used by the Wasm JS glue to stage call\n \ - arguments. Native consumers never call these; producers targeting\n \ - WebAssembly must export them (the generated {prefix}.c provides\n \ - malloc/free-backed defaults). */\n\ - {api} uint8_t* {prefix}_alloc(uint32_t size);\n\ - {api} void {prefix}_dealloc(uint8_t* ptr, uint32_t size);\n\n\ + /* Cancellation for `cancellable` async functions. A token starts with\n \ + one reference owned by the consumer; the producer takes its own for\n \ + each call it is passed to, so the consumer may cancel and destroy its\n \ + reference at any time. A cancelled call completes with code -5. */\n\ typedef struct {prefix}_cancel_token {prefix}_cancel_token;\n\ {api} {prefix}_cancel_token* {prefix}_cancel_token_create(void);\n\ {api} void {prefix}_cancel_token_cancel({prefix}_cancel_token* token);\n\ {api} bool {prefix}_cancel_token_is_cancelled(const {prefix}_cancel_token* token);\n\ - {api} void {prefix}_cancel_token_destroy({prefix}_cancel_token* token);\n\n", + {api} void {prefix}_cancel_token_destroy({prefix}_cancel_token* token);\n\n\ + /* Linear-memory allocator the Wasm glue stages arguments through.\n \ + Exported by wasm32 producers only. */\n\ + {api} uint8_t* {prefix}_alloc(uint32_t size);\n\ + {api} void {prefix}_dealloc(uint8_t* ptr, uint32_t size);\n\n\ + /* Live-allocation counters for leak checks in tests: 0 objects,\n \ + 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned allocations.\n \ + Always 0 unless the producer enables the `leak-check` feature. */\n\ + {api} uint64_t {prefix}_debug_live(int32_t kind);\n\n", ); + let roots: Vec<&ModuleBinding> = model.roots().collect(); + if !roots.is_empty() { + out.push_str( + "/* Contract checksums, one per top-level module. A consumer compares\n \ + each function's result with the constant it was generated against\n \ + before making any other call. */\n", + ); + } + for m in roots { + let symbol = checksum_symbol(prefix, &m.name); + let value = m.checksum.unwrap_or_default(); + let _ = writeln!( + out, + "#define {} 0x{value:016x}ull\n{api} uint64_t {symbol}(void);", + symbol.to_uppercase() + ); + } + out.push('\n'); } -/// Render an enum's discriminant constants as a C `typedef enum` named -/// `type_name`. Multi-line when any variant is documented. fn render_enum_constants(out: &mut String, e: &EnumBinding, type_name: &str) { let mut w = CodeWriter::four_space(); w.doc(&e.doc, DocCommentStyle::Javadoc); @@ -254,12 +270,11 @@ fn render_rich_enum_tag_decl(out: &mut String, e: &EnumBinding) { /// Render an error domain's code constants as a C `typedef enum` named by the /// domain's `c_tag`. These are the values a throwing function stores in /// `{prefix}_error.code`. -fn render_error_domain_decl(out: &mut String, e: &ErrorBinding) { +fn render_error_domain_decl(out: &mut String, e: &ErrorBinding, owner: &str) { let mut w = CodeWriter::four_space(); w.doc( &Some(format!( - "Error codes reported by throwing functions in the `{}` module tree.", - e.owner_path.replace('_', ".") + "Error codes reported by throwing functions in the `{owner}` module tree." )), DocCommentStyle::Javadoc, ); @@ -299,7 +314,7 @@ pub fn render_module_enum_defs(out: &mut String, module: &ModuleBinding) { } if let Some(err) = &module.error { if err.declared_here { - render_error_domain_decl(out, err); + render_error_domain_decl(out, err, &module.dot_path); } } } diff --git a/crates/weaveffi-gen/src/cache.rs b/crates/weaveffi-gen/src/cache.rs new file mode 100644 index 00000000..d53165b8 --- /dev/null +++ b/crates/weaveffi-gen/src/cache.rs @@ -0,0 +1,213 @@ +//! Per-target generation records for skip-if-unchanged builds and stale-file +//! cleanup. +//! +//! After generating a target, the orchestrator writes +//! `{out_dir}/.weaveffi-cache/{target}.json`: a hash of every input that +//! affects the target's output (the canonical IR, the library identity, the +//! target name, its serialized config, and the CLI version) plus the path and +//! content hash of every file it wrote. A later run skips the target only +//! when the input hash matches *and* every recorded file is still on disk +//! unmodified, so deleting or hand-editing a generated file regenerates it. +//! Files recorded by the previous run that the current run no longer +//! produces are removed, so a renamed type never leaves a stale file behind, +//! while files the user added (a `node_modules/`, a build directory) are +//! never touched. + +use std::collections::BTreeMap; + +use anyhow::{Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use weaveffi_model::ir::Api; +use weaveffi_model::resolved::ResolvedApi; + +const CACHE_DIR: &str = ".weaveffi-cache"; + +/// Version string folded into every input hash, so upgrading the CLI +/// regenerates everything. +pub const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// What one generation of one target produced. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Record { + /// [`hash_generator_inputs`] for the run that produced the files. + pub inputs: String, + /// Every file written, relative to the output directory (with `/` + /// separators), mapped to the SHA-256 of its contents. + pub files: BTreeMap, +} + +impl Record { + /// Whether every recorded file still exists under `out_dir` with the + /// recorded contents. + #[must_use] + pub fn files_intact(&self, out_dir: &Utf8Path) -> bool { + self.files.iter().all(|(rel, hash)| { + std::fs::read(out_dir.join(rel).as_std_path()) + .is_ok_and(|bytes| &hash_bytes(&bytes) == hash) + }) + } +} + +/// The SHA-256 hex digest of `bytes`. +#[must_use] +pub fn hash_bytes(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) +} + +/// 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"); + serde_json::to_string(&value).expect("Value serialization should not fail") +} + +/// The SHA-256 hex digest of the API's canonical JSON. +/// +/// # Panics +/// +/// Panics if the API cannot be serialized to JSON, which does not happen for +/// well-formed inputs. +#[must_use] +pub fn hash_api(api: &Api) -> String { + hash_bytes(canonical_json(api).as_bytes()) +} + +/// The SHA-256 hex digest of every input that affects a single target's +/// output: the canonical IR, the library identity, the target's name, the +/// target's serialized config, and the CLI version. +/// +/// # Panics +/// +/// Panics if the API or identity cannot be serialized to JSON, which does not +/// happen for well-formed inputs. +#[must_use] +pub fn hash_generator_inputs(api: &ResolvedApi, target: &str, config_bytes: &[u8]) -> String { + let identity = + serde_json::to_string(api.identity()).expect("identity serialization should not fail"); + let mut hasher = Sha256::new(); + for part in [ + b"v3".as_slice(), + CLI_VERSION.as_bytes(), + target.as_bytes(), + canonical_json(api.api()).as_bytes(), + identity.as_bytes(), + config_bytes, + ] { + hasher.update(part); + hasher.update(b"\0"); + } + format!("{:x}", hasher.finalize()) +} + +fn record_path(out_dir: &Utf8Path, target: &str) -> Utf8PathBuf { + out_dir.join(CACHE_DIR).join(format!("{target}.json")) +} + +/// Read the record of the last generation of `target` under `out_dir`, if +/// any (a missing or unreadable record means "never generated"). +#[must_use] +pub fn read_record(out_dir: &Utf8Path, target: &str) -> Option { + let text = std::fs::read_to_string(record_path(out_dir, target)).ok()?; + serde_json::from_str(&text).ok() +} + +/// Persist the record of a generation of `target`. +/// +/// # Errors +/// +/// Returns an error when the cache directory cannot be created or the record +/// cannot be written. +/// +/// # Panics +/// +/// Panics if the record cannot be serialized, which does not happen. +pub fn write_record(out_dir: &Utf8Path, target: &str, record: &Record) -> Result<()> { + let path = record_path(out_dir, target); + let dir = out_dir.join(CACHE_DIR); + std::fs::create_dir_all(dir.as_std_path()) + .with_context(|| format!("failed to create cache directory: {dir}"))?; + let json = serde_json::to_string_pretty(record).expect("record serializes"); + std::fs::write(path.as_std_path(), json) + .with_context(|| format!("failed to write cache file: {path}")) +} + +/// Forget every target's record, so the next run regenerates everything. +/// +/// # Errors +/// +/// 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.exists() { + std::fs::remove_dir_all(cache_dir.as_std_path()) + .with_context(|| format!("failed to remove cache directory: {cache_dir}"))?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use weaveffi_model::ir::{Module, CURRENT_SCHEMA_VERSION}; + + fn api(name: &str) -> Api { + Api { + version: CURRENT_SCHEMA_VERSION.into(), + modules: vec![Module { + name: name.into(), + doc: None, + functions: vec![], + interfaces: vec![], + callback_interfaces: vec![], + structs: vec![], + enums: vec![], + errors: None, + modules: vec![], + }], + } + } + + #[test] + fn input_hash_covers_api_target_config_and_identity() { + let r = ResolvedApi::assume_valid(api("math")); + 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!( + base, + hash_generator_inputs(&ResolvedApi::assume_valid(api("other")), "swift", b"{}") + ); + let renamed = r + .clone() + .with_identity(weaveffi_model::pkg::Identity::named("kv")); + assert_ne!(base, hash_generator_inputs(&renamed, "swift", b"{}")); + assert_eq!(hash_api(&api("math")), hash_api(&api("math"))); + } + + #[test] + fn records_round_trip_and_detect_tampering() { + let dir = tempfile::tempdir().unwrap(); + let out = Utf8Path::from_path(dir.path()).unwrap(); + assert_eq!(read_record(out, "swift"), None); + std::fs::create_dir_all(out.join("swift")).unwrap(); + std::fs::write(out.join("swift/a.swift"), "a").unwrap(); + let record = Record { + inputs: "abc".into(), + files: [("swift/a.swift".to_string(), hash_bytes(b"a"))].into(), + }; + write_record(out, "swift", &record).unwrap(); + let back = read_record(out, "swift").unwrap(); + assert_eq!(back, record); + assert!(back.files_intact(out)); + std::fs::write(out.join("swift/a.swift"), "edited").unwrap(); + assert!(!back.files_intact(out)); + std::fs::remove_file(out.join("swift/a.swift")).unwrap(); + assert!(!back.files_intact(out)); + invalidate_all(out).unwrap(); + assert_eq!(read_record(out, "swift"), None); + invalidate_all(out).unwrap(); + } +} diff --git a/crates/weaveffi-core/src/capabilities.rs b/crates/weaveffi-gen/src/capabilities.rs similarity index 98% rename from crates/weaveffi-core/src/capabilities.rs rename to crates/weaveffi-gen/src/capabilities.rs index 29b1b581..bd1a23fc 100644 --- a/crates/weaveffi-core/src/capabilities.rs +++ b/crates/weaveffi-gen/src/capabilities.rs @@ -15,7 +15,7 @@ use std::collections::BTreeMap; use std::fmt; -use weaveffi_ir::ir::{Api, Module, TypeRef}; +use weaveffi_model::ir::{Api, Module, TypeRef}; /// An IDL feature whose support varies (or could vary) per target. /// @@ -204,7 +204,7 @@ pub fn check( #[cfg(test)] mod tests { use super::*; - use weaveffi_ir::ir::{CallbackInterfaceDef, Function, Param}; + use weaveffi_model::ir::{CallbackInterfaceDef, Function, Param}; fn func(name: &str, is_async: bool, returns: Option) -> Function { Function { @@ -239,7 +239,7 @@ mod tests { fn api(modules: Vec) -> Api { Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), modules, } } diff --git a/crates/weaveffi-core/src/codegen/common.rs b/crates/weaveffi-gen/src/codegen/common.rs similarity index 99% rename from crates/weaveffi-core/src/codegen/common.rs rename to crates/weaveffi-gen/src/codegen/common.rs index 10698555..42e038ea 100644 --- a/crates/weaveffi-core/src/codegen/common.rs +++ b/crates/weaveffi-gen/src/codegen/common.rs @@ -11,7 +11,7 @@ //! their behaviour is non-uniform; this module deliberately covers //! only the common 80%. -use weaveffi_ir::ir::Module; +use weaveffi_model::ir::Module; /// Doc-comment flavour used by [`emit_doc`]. /// @@ -150,7 +150,7 @@ pub fn pascal_case(s: &str) -> String { #[cfg(test)] mod tests { use super::*; - use weaveffi_ir::ir::Module; + use weaveffi_model::ir::Module; fn leaf(name: &str) -> Module { Module { diff --git a/crates/weaveffi-gen/src/codegen/mod.rs b/crates/weaveffi-gen/src/codegen/mod.rs new file mode 100644 index 00000000..a3326fb5 --- /dev/null +++ b/crates/weaveffi-gen/src/codegen/mod.rs @@ -0,0 +1,562 @@ +//! Target erasure and orchestration. +//! +//! Each language target implements [`LanguageBackend`] with its own typed +//! `Config`. The orchestrator works on the object-safe [`Target`] trait, which +//! erases the concrete config; [`ConfiguredBackend`] is the adapter that pairs +//! a backend with a concrete config value and is what the CLI and tests pass +//! into [`Orchestrator::with_target`]. +//! +//! Rendering is pure: a target returns its files in memory, and the +//! [`Orchestrator`] does every write. That is what lets it skip unchanged +//! targets, rewrite only files whose contents changed, remove files a +//! previous run wrote that the current run no longer produces (see +//! [`crate::cache`]), and lets `weaveffi diff` compare without touching disk. + +use std::collections::BTreeMap; + +use anyhow::{bail, Context, Result}; +use camino::Utf8Path; +use rayon::prelude::*; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::cache::{self, Record}; +use crate::capabilities::{self, TargetCapabilities}; +use crate::package::{PackageContext, PackagedFile}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::resolved::ResolvedApi; + +pub mod common; +pub mod writer; + +pub use writer::CodeWriter; + +/// Object-safe view of a [`LanguageBackend`] paired with a concrete config. +/// +/// The orchestrator stores targets as `&dyn Target` so it can hold a +/// heterogeneous set whose `Config` types differ. [`ConfiguredBackend`] is +/// the canonical adapter. +pub trait Target: Send + Sync { + /// The target's stable short name. Mirrors [`LanguageBackend::name`]. + fn name(&self) -> &'static str; + /// The gated features the target implements. Mirrors + /// [`LanguageBackend::capabilities`]. + fn capabilities(&self) -> TargetCapabilities; + /// See [`LanguageBackend::allows_unsupported`], evaluated against the + /// bound config. + fn allows_unsupported(&self) -> bool; + /// Render every file the target produces for `api`, with paths under + /// `out_dir`. Pure: nothing is written. + fn render(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec; + /// Assemble the distributable package for this target, using the bound + /// config. Returns `None` when the target does not support packaging. + fn package( + &self, + api: &ResolvedApi, + ctx: &PackageContext, + out_dir: &Utf8Path, + ) -> Option>; + /// Canonical-JSON encoding of the bound config, fed into the cache + /// hash so a config-only change invalidates the record. + fn config_hash_input(&self) -> Vec; +} + +/// Binds a [`LanguageBackend`] to a concrete config value so it can be erased +/// to `&dyn Target`. +/// +/// ```ignore +/// let swift = ConfiguredBackend::new(SwiftGenerator, SwiftConfig::default()); +/// orchestrator.with_target(&swift); +/// ``` +pub struct ConfiguredBackend { + inner: B, + config: B::Config, +} + +impl ConfiguredBackend { + /// Pair a backend with the concrete config it should run under. + pub fn new(inner: B, config: B::Config) -> Self { + Self { inner, config } + } +} + +impl Target for ConfiguredBackend { + fn name(&self) -> &'static str { + self.inner.name() + } + + fn capabilities(&self) -> TargetCapabilities { + self.inner.capabilities(&self.config) + } + + fn allows_unsupported(&self) -> bool { + self.inner.allows_unsupported(&self.config) + } + + fn render(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec { + let model = BindingModel::build(api); + self.inner.files(api, &model, out_dir, &self.config) + } + + fn package( + &self, + api: &ResolvedApi, + ctx: &PackageContext, + out_dir: &Utf8Path, + ) -> Option> { + let model = BindingModel::build(api); + self.inner.package(api, &model, ctx, out_dir, &self.config) + } + + fn config_hash_input(&self) -> Vec { + let value = + serde_json::to_value(&self.config).expect("backend config should serialize to JSON"); + serde_json::to_vec(&value).expect("JSON Value should serialize") + } +} + +/// The path of `file` relative to `out_dir`, with `/` separators on every +/// platform, so listings and cache records are OS-independent. +/// +/// # Panics +/// +/// Panics if a backend emitted a path outside `out_dir`, which is a backend +/// bug. +#[must_use] +pub fn relative_path(out_dir: &Utf8Path, file: &Utf8Path) -> String { + let rel = file + .strip_prefix(out_dir) + .unwrap_or_else(|_| panic!("generated file {file} is outside {out_dir}")); + rel.components() + .map(|c| c.as_str()) + .collect::>() + .join("/") +} + +/// Points in a generation run where the caller may act (the CLI runs the +/// project's `pre_generate` and `post_generate` commands here). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Hook { + /// At least one target is out of date; nothing has been written yet. + BeforeWrite, + /// Every out-of-date target has been written. + AfterWrite, +} + +/// What a generation run did. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct GenerateReport { + /// Targets that were regenerated. + pub generated: Vec<&'static str>, + /// Targets whose inputs and files were unchanged, so they were skipped. + pub up_to_date: Vec<&'static str>, + /// Files written because they were new or their contents changed. + pub written: usize, + /// Files a regenerated target produced with unchanged contents. + pub unchanged: usize, + /// Files removed because the previous run wrote them and this run did + /// not (paths relative to the output directory). + pub removed: Vec, + /// Advisory messages, such as a target generating with + /// `allow_unsupported` set. + pub warnings: Vec, +} + +/// Runs a set of targets against one API and output directory. +#[derive(Default)] +pub struct Orchestrator<'a> { + targets: Vec<&'a dyn Target>, +} + +impl<'a> Orchestrator<'a> { + /// An orchestrator with no targets. + pub fn new() -> Self { + Self::default() + } + + /// Add a target. + #[must_use] + pub fn with_target(mut self, target: &'a dyn Target) -> Self { + self.targets.push(target); + self + } + + /// Check every target's capabilities against the features `api` uses. + /// Returns the warnings for targets that proceed under + /// `allow_unsupported`. + /// + /// # Errors + /// + /// Returns an error listing every target that cannot deliver a feature + /// the API uses and did not opt in to generating anyway. + pub fn check_capabilities(&self, api: &ResolvedApi) -> Result> { + let mut violations = Vec::new(); + let mut warnings = Vec::new(); + for t in &self.targets { + let Err(err) = capabilities::check(api.api(), t.name(), &t.capabilities()) else { + continue; + }; + if t.allows_unsupported() { + let mut msg = format!( + "target '{}' does not support every feature this API uses; generating \ + anyway because allow_unsupported is set:", + t.name() + ); + for (feature, locations) in &err.violations { + msg.push_str(&format!( + "\n - {feature} (used by: {})", + locations.join(", ") + )); + } + warnings.push(msg); + } else { + violations.push(err.to_string()); + } + } + if !violations.is_empty() { + bail!("{}", violations.join("\n")); + } + Ok(warnings) + } + + /// Generate every out-of-date target under `out_dir`. + /// + /// A target is up to date when its last record's input hash matches and + /// every file it recorded is still on disk unmodified; `force` treats + /// every target as out of date. Out-of-date targets render in parallel; + /// only files whose contents changed are written, and files the previous + /// run of a target wrote that this run no longer produces are removed. + /// `hook` runs before the first write and after the last, and only when + /// something is out of date. + /// + /// # Errors + /// + /// Returns an error when a capability check fails, a hook fails, or a + /// file cannot be written or removed. + pub fn run( + &self, + api: &ResolvedApi, + out_dir: &Utf8Path, + force: bool, + hook: &mut dyn FnMut(Hook) -> Result<()>, + ) -> Result { + let mut report = GenerateReport { + warnings: self.check_capabilities(api)?, + ..GenerateReport::default() + }; + if force { + cache::invalidate_all(out_dir)?; + } + let mut pending: Vec<(&'a dyn Target, String, Option)> = Vec::new(); + for &t in &self.targets { + let inputs = cache::hash_generator_inputs(api, t.name(), &t.config_hash_input()); + let previous = cache::read_record(out_dir, t.name()); + let fresh = previous + .as_ref() + .is_some_and(|r| r.inputs == inputs && r.files_intact(out_dir)); + if fresh { + report.up_to_date.push(t.name()); + } else { + pending.push((t, inputs, previous)); + } + } + if pending.is_empty() { + return Ok(report); + } + hook(Hook::BeforeWrite)?; + let rendered: Vec> = pending + .par_iter() + .map(|(t, _, _)| t.render(api, out_dir)) + .collect(); + for ((t, inputs, previous), files) in pending.iter().zip(rendered) { + let mut record = Record { + inputs: inputs.clone(), + files: BTreeMap::new(), + }; + for file in files { + let rel = relative_path(out_dir, &file.path); + let hash = cache::hash_bytes(file.contents.as_bytes()); + let current = std::fs::read(file.path.as_std_path()).ok(); + if current.as_deref() == Some(file.contents.as_bytes()) { + report.unchanged += 1; + } else { + if let Some(parent) = file.path.parent() { + std::fs::create_dir_all(parent.as_std_path()) + .with_context(|| format!("failed to create {parent}"))?; + } + std::fs::write(file.path.as_std_path(), &file.contents) + .with_context(|| format!("failed to write {}", file.path))?; + report.written += 1; + } + record.files.insert(rel, hash); + } + for stale in previous + .iter() + .flat_map(|p| p.files.keys()) + .filter(|rel| !record.files.contains_key(*rel)) + { + let path = out_dir.join(stale); + if path.exists() { + std::fs::remove_file(path.as_std_path()) + .with_context(|| format!("failed to remove stale file {path}"))?; + report.removed.push(stale.clone()); + } + } + cache::write_record(out_dir, t.name(), &record)?; + report.generated.push(t.name()); + } + hook(Hook::AfterWrite)?; + Ok(report) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use weaveffi_model::ir::{Api, CallbackInterfaceDef, Function, Module}; + + #[derive(Default, Clone, serde::Serialize)] + struct TestConfig { + knob: Option, + allow_unsupported: bool, + } + + struct Counting { + name: &'static str, + calls: Arc, + caps: TargetCapabilities, + files: Arc>>, + } + + impl LanguageBackend for Counting { + type Config = TestConfig; + + fn name(&self) -> &'static str { + self.name + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + self.caps + } + + fn allows_unsupported(&self, config: &Self::Config) -> bool { + config.allow_unsupported + } + + fn files( + &self, + _api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + _config: &Self::Config, + ) -> Vec { + self.calls.fetch_add(1, Ordering::SeqCst); + self.files + .lock() + .unwrap() + .iter() + .map(|f| { + OutputFile::new( + out_dir.join(self.name).join(f), + format!("{} {}", model.prefix, model.modules[0].name), + ) + }) + .collect() + } + } + + fn module(name: &str) -> Module { + Module { + name: name.into(), + doc: None, + functions: vec![], + interfaces: vec![], + structs: vec![], + enums: vec![], + callback_interfaces: vec![], + errors: None, + modules: vec![], + } + } + + fn api_named(name: &str) -> ResolvedApi { + ResolvedApi::assume_valid(Api { + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![module(name)], + }) + } + + fn listener_api() -> ResolvedApi { + let mut m = module("math"); + m.callback_interfaces = vec![CallbackInterfaceDef { + name: "OnChange".into(), + doc: None, + deprecated: None, + methods: vec![Function { + name: "changed".into(), + params: vec![], + returns: None, + doc: None, + throws: false, + r#async: false, + cancellable: false, + deprecated: None, + }], + }]; + ResolvedApi::assume_valid(Api { + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![m], + }) + } + + fn backend( + name: &'static str, + calls: &Arc, + callbacks: bool, + config: TestConfig, + ) -> ConfiguredBackend { + ConfiguredBackend::new( + Counting { + name, + calls: Arc::clone(calls), + caps: TargetCapabilities { + callback_interfaces: callbacks, + ..TargetCapabilities::full() + }, + files: Arc::new(Mutex::new(vec!["out.txt"])), + }, + config, + ) + } + + fn no_hook(_: Hook) -> Result<()> { + Ok(()) + } + + #[test] + fn capability_gate_blocks_unless_opted_in() { + let dir = tempfile::tempdir().unwrap(); + let out = Utf8Path::from_path(dir.path()).unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let strict = backend("strict", &calls, false, TestConfig::default()); + let opted = backend( + "opted", + &calls, + false, + TestConfig { + knob: None, + allow_unsupported: true, + }, + ); + let err = Orchestrator::new() + .with_target(&strict) + .with_target(&opted) + .run(&listener_api(), out, false, &mut no_hook) + .unwrap_err() + .to_string(); + assert!(err.contains("target 'strict' does not support"), "{err}"); + assert!(!err.contains("target 'opted'"), "{err}"); + assert_eq!(calls.load(Ordering::SeqCst), 0); + let report = Orchestrator::new() + .with_target(&opted) + .run(&listener_api(), out, false, &mut no_hook) + .unwrap(); + assert_eq!(report.generated, ["opted"]); + assert_eq!(report.warnings.len(), 1); + } + + #[test] + fn records_skip_unchanged_targets_and_detect_tampering() { + let dir = tempfile::tempdir().unwrap(); + let out = Utf8Path::from_path(dir.path()).unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let c = backend("c", &calls, true, TestConfig::default()); + let orch = Orchestrator::new().with_target(&c); + let api = api_named("math"); + + let first = orch.run(&api, out, false, &mut no_hook).unwrap(); + assert_eq!((first.generated.len(), first.written), (1, 1)); + let second = orch.run(&api, out, false, &mut no_hook).unwrap(); + assert_eq!(second.up_to_date, ["c"]); + assert_eq!(calls.load(Ordering::SeqCst), 1); + + std::fs::write(out.join("c/out.txt"), "edited").unwrap(); + let third = orch.run(&api, out, false, &mut no_hook).unwrap(); + assert_eq!((third.generated.len(), third.written), (1, 1)); + + let forced = orch.run(&api, out, true, &mut no_hook).unwrap(); + assert_eq!((forced.written, forced.unchanged), (0, 1)); + + let reconfigured = backend( + "c", + &calls, + true, + TestConfig { + knob: Some("changed".into()), + allow_unsupported: false, + }, + ); + let report = Orchestrator::new() + .with_target(&reconfigured) + .run(&api, out, false, &mut no_hook) + .unwrap(); + assert_eq!(report.generated, ["c"]); + } + + #[test] + fn stale_files_are_removed_but_user_files_are_kept() { + let dir = tempfile::tempdir().unwrap(); + let out = Utf8Path::from_path(dir.path()).unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let c = backend("c", &calls, true, TestConfig::default()); + let files = Arc::clone(&c.inner.files); + *files.lock().unwrap() = vec!["a.txt", "b.txt"]; + let orch = Orchestrator::new().with_target(&c); + orch.run(&api_named("math"), out, false, &mut no_hook) + .unwrap(); + std::fs::write(out.join("c/user.txt"), "mine").unwrap(); + + *files.lock().unwrap() = vec!["a.txt"]; + let report = orch + .run(&api_named("math2"), out, false, &mut no_hook) + .unwrap(); + assert_eq!(report.removed, ["c/b.txt"]); + assert!(out.join("c/a.txt").exists()); + assert!(!out.join("c/b.txt").exists()); + assert!(out.join("c/user.txt").exists()); + } + + #[test] + fn hooks_run_only_when_something_is_out_of_date() { + let dir = tempfile::tempdir().unwrap(); + let out = Utf8Path::from_path(dir.path()).unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let c = backend("c", &calls, true, TestConfig::default()); + let orch = Orchestrator::new().with_target(&c); + let mut seen = Vec::new(); + orch.run(&api_named("math"), out, false, &mut |h| { + seen.push(h); + Ok(()) + }) + .unwrap(); + orch.run(&api_named("math"), out, false, &mut |h| { + seen.push(h); + Ok(()) + }) + .unwrap(); + assert_eq!(seen, [Hook::BeforeWrite, Hook::AfterWrite]); + let err = orch + .run(&api_named("other"), out, false, &mut |_| { + bail!("hook failed") + }) + .unwrap_err(); + assert!(err.to_string().contains("hook failed")); + } + + #[test] + fn relative_paths_use_forward_slashes() { + let out = Utf8Path::new("gen"); + assert_eq!(relative_path(out, &out.join("c").join("x.h")), "c/x.h"); + } +} diff --git a/crates/weaveffi-core/src/codegen/writer.rs b/crates/weaveffi-gen/src/codegen/writer.rs similarity index 99% rename from crates/weaveffi-core/src/codegen/writer.rs rename to crates/weaveffi-gen/src/codegen/writer.rs index 628ab4e5..6a4d8745 100644 --- a/crates/weaveffi-core/src/codegen/writer.rs +++ b/crates/weaveffi-gen/src/codegen/writer.rs @@ -14,7 +14,7 @@ //! carry trailing whitespace, and the indent unit is fixed per writer. //! //! ``` -//! use weaveffi_core::codegen::writer::CodeWriter; +//! use weaveffi_gen::codegen::writer::CodeWriter; //! //! let mut w = CodeWriter::new(" "); //! w.line("class Greeter:"); @@ -188,7 +188,7 @@ impl CodeWriter { /// block. /// /// ``` - /// use weaveffi_core::codegen::writer::CodeWriter; + /// use weaveffi_gen::codegen::writer::CodeWriter; /// let mut w = CodeWriter::four_space(); /// w.block("fn main() {", "}", |w| { /// w.line("println!(\"hi\");"); diff --git a/crates/weaveffi-core/src/lang.rs b/crates/weaveffi-gen/src/lang.rs similarity index 100% rename from crates/weaveffi-core/src/lang.rs rename to crates/weaveffi-gen/src/lang.rs diff --git a/crates/weaveffi-gen/src/lib.rs b/crates/weaveffi-gen/src/lib.rs new file mode 100644 index 00000000..f296b451 --- /dev/null +++ b/crates/weaveffi-gen/src/lib.rs @@ -0,0 +1,23 @@ +//! Code generation for WeaveFFI: the `LanguageBackend` trait, the +//! code-generation orchestrator, the C ABI header renderer, and the eleven +//! language generators under [`targets`]. +//! +//! Every generator renders from the resolved API and binding model in +//! [`weaveffi_model`], so symbol names and parameter lowering are computed +//! once and shared. +#![deny(missing_docs)] +#![warn(clippy::missing_errors_doc)] +#![warn(clippy::missing_panics_doc)] +#![warn(clippy::doc_markdown)] + +pub mod backend; +pub mod cabi; +pub mod cache; +pub mod capabilities; +pub mod codegen; +pub mod lang; +pub mod manifest; +pub mod package; +pub mod platform; +pub mod targets; +pub mod utils; diff --git a/crates/weaveffi-core/src/manifest.rs b/crates/weaveffi-gen/src/manifest.rs similarity index 100% rename from crates/weaveffi-core/src/manifest.rs rename to crates/weaveffi-gen/src/manifest.rs diff --git a/crates/weaveffi-core/src/package.rs b/crates/weaveffi-gen/src/package.rs similarity index 98% rename from crates/weaveffi-core/src/package.rs rename to crates/weaveffi-gen/src/package.rs index ab85123e..75db6827 100644 --- a/crates/weaveffi-core/src/package.rs +++ b/crates/weaveffi-gen/src/package.rs @@ -98,8 +98,8 @@ fn normalize_separators(path: Utf8PathBuf) -> Utf8PathBuf { } /// Everything a backend's [`package`](crate::backend::LanguageBackend::package) -/// hook needs beyond the [`Api`](weaveffi_ir::ir::Api) and -/// [`BindingModel`](crate::model::BindingModel) it already receives. +/// hook needs beyond the [`Api`](weaveffi_model::ir::Api) and +/// [`BindingModel`](weaveffi_model::model::BindingModel) it already receives. /// /// The prebuilt libraries to bundle live in [`binaries`](Self::binaries); /// `input_basename` is the IDL file stem, used (as in `generate`) as the diff --git a/crates/weaveffi-core/src/platform.rs b/crates/weaveffi-gen/src/platform.rs similarity index 86% rename from crates/weaveffi-core/src/platform.rs rename to crates/weaveffi-gen/src/platform.rs index ca3961b8..a9e37052 100644 --- a/crates/weaveffi-core/src/platform.rs +++ b/crates/weaveffi-gen/src/platform.rs @@ -28,6 +28,9 @@ pub enum Os { Linux, /// Windows; shared libraries are `.dll`. Windows, + /// iOS devices and simulators; libraries are static (`.a`), assembled + /// into an `XCFramework`. + Ios, /// Android (Bionic); shared libraries are `.so` bundled under `jniLibs`. Android, /// WebAssembly; the "library" is a standalone `.wasm` module. @@ -69,6 +72,12 @@ pub enum Platform { LinuxArm64, /// Windows on x86-64 (`x86_64-pc-windows-msvc`). WindowsX64, + /// iOS on device (`aarch64-apple-ios`), static library. + IosArm64, + /// iOS simulator on Apple silicon (`aarch64-apple-ios-sim`), static library. + IosSimArm64, + /// iOS simulator on Intel (`x86_64-apple-ios`), static library. + IosSimX64, /// Android on ARM64 (`aarch64-linux-android`, jniLibs `arm64-v8a`). AndroidArm64, /// Android on x86-64 (`x86_64-linux-android`, jniLibs `x86_64`), the @@ -80,12 +89,15 @@ pub enum Platform { impl Platform { /// Every supported platform, in a stable order. - pub const ALL: [Platform; 8] = [ + pub const ALL: [Platform; 11] = [ Platform::MacosArm64, Platform::MacosX64, Platform::LinuxX64, Platform::LinuxArm64, Platform::WindowsX64, + Platform::IosArm64, + Platform::IosSimArm64, + Platform::IosSimX64, Platform::AndroidArm64, Platform::AndroidX64, Platform::Wasm32, @@ -101,6 +113,20 @@ impl Platform { Platform::WindowsX64, ]; + /// The platform this process runs on, if it is a packaging platform. + #[must_use] + pub fn host() -> Option { + let triple = match (std::env::consts::OS, std::env::consts::ARCH) { + ("macos", "aarch64") => "aarch64-apple-darwin", + ("macos", "x86_64") => "x86_64-apple-darwin", + ("linux", "x86_64") => "x86_64-unknown-linux-gnu", + ("linux", "aarch64") => "aarch64-unknown-linux-gnu", + ("windows", "x86_64") => "x86_64-pc-windows-msvc", + _ => return None, + }; + Platform::from_rust_target(triple) + } + /// The stable WeaveFFI platform identifier, used as the `--platforms` token /// and the `--binaries` subdirectory name (`darwin-arm64`, `darwin-x64`, /// `linux-x64`, `linux-arm64`, `windows-x64`, `android-arm64`, @@ -112,6 +138,9 @@ impl Platform { Platform::LinuxX64 => "linux-x64", Platform::LinuxArm64 => "linux-arm64", Platform::WindowsX64 => "windows-x64", + Platform::IosArm64 => "ios-arm64", + Platform::IosSimArm64 => "ios-sim-arm64", + Platform::IosSimX64 => "ios-sim-x64", Platform::AndroidArm64 => "android-arm64", Platform::AndroidX64 => "android-x64", Platform::Wasm32 => "wasm32", @@ -135,6 +164,7 @@ impl Platform { Platform::MacosArm64 | Platform::MacosX64 => Os::MacOs, Platform::LinuxX64 | Platform::LinuxArm64 => Os::Linux, Platform::WindowsX64 => Os::Windows, + Platform::IosArm64 | Platform::IosSimArm64 | Platform::IosSimX64 => Os::Ios, Platform::AndroidArm64 | Platform::AndroidX64 => Os::Android, Platform::Wasm32 => Os::Wasm, } @@ -143,11 +173,16 @@ impl Platform { /// The CPU architecture. pub fn arch(self) -> Arch { match self { - Platform::MacosArm64 | Platform::LinuxArm64 | Platform::AndroidArm64 => Arch::Arm64, + Platform::MacosArm64 + | Platform::LinuxArm64 + | Platform::AndroidArm64 + | Platform::IosArm64 + | Platform::IosSimArm64 => Arch::Arm64, Platform::MacosX64 | Platform::LinuxX64 | Platform::WindowsX64 - | Platform::AndroidX64 => Arch::X64, + | Platform::AndroidX64 + | Platform::IosSimX64 => Arch::X64, Platform::Wasm32 => Arch::Wasm32, } } @@ -161,6 +196,9 @@ impl Platform { Platform::LinuxX64 => "x86_64-unknown-linux-gnu", Platform::LinuxArm64 => "aarch64-unknown-linux-gnu", Platform::WindowsX64 => "x86_64-pc-windows-msvc", + Platform::IosArm64 => "aarch64-apple-ios", + Platform::IosSimArm64 => "aarch64-apple-ios-sim", + Platform::IosSimX64 => "x86_64-apple-ios", Platform::AndroidArm64 => "aarch64-linux-android", Platform::AndroidX64 => "x86_64-linux-android", Platform::Wasm32 => "wasm32-unknown-unknown", @@ -179,7 +217,7 @@ impl Platform { /// (including Android), empty on Windows and for a `.wasm` module. pub fn lib_prefix(self) -> &'static str { match self.os() { - Os::MacOs | Os::Linux | Os::Android => "lib", + Os::MacOs | Os::Linux | Os::Android | Os::Ios => "lib", Os::Windows | Os::Wasm => "", } } @@ -189,6 +227,7 @@ impl Platform { pub fn lib_extension(self) -> &'static str { match self.os() { Os::MacOs => "dylib", + Os::Ios => "a", Os::Linux | Os::Android => "so", Os::Windows => "dll", Os::Wasm => "wasm", @@ -223,7 +262,12 @@ impl Platform { Platform::LinuxX64 => Some("linux-x64"), Platform::LinuxArm64 => Some("linux-arm64"), Platform::WindowsX64 => Some("win-x64"), - Platform::AndroidArm64 | Platform::AndroidX64 | Platform::Wasm32 => None, + Platform::AndroidArm64 + | Platform::AndroidX64 + | Platform::Wasm32 + | Platform::IosArm64 + | Platform::IosSimArm64 + | Platform::IosSimX64 => None, } } @@ -234,7 +278,7 @@ impl Platform { Os::MacOs => Some("darwin"), Os::Linux => Some("linux"), Os::Windows => Some("win32"), - Os::Android | Os::Wasm => None, + Os::Android | Os::Wasm | Os::Ios => None, } } @@ -258,7 +302,12 @@ impl Platform { Platform::LinuxX64 => Some("manylinux2014_x86_64"), Platform::LinuxArm64 => Some("manylinux2014_aarch64"), Platform::WindowsX64 => Some("win_amd64"), - Platform::AndroidArm64 | Platform::AndroidX64 | Platform::Wasm32 => None, + Platform::AndroidArm64 + | Platform::AndroidX64 + | Platform::Wasm32 + | Platform::IosArm64 + | Platform::IosSimArm64 + | Platform::IosSimX64 => None, } } @@ -272,7 +321,12 @@ impl Platform { Platform::LinuxX64 => Some("x86_64-linux"), Platform::LinuxArm64 => Some("aarch64-linux"), Platform::WindowsX64 => Some("x64-mingw-ucrt"), - Platform::AndroidArm64 | Platform::AndroidX64 | Platform::Wasm32 => None, + Platform::AndroidArm64 + | Platform::AndroidX64 + | Platform::Wasm32 + | Platform::IosArm64 + | Platform::IosSimArm64 + | Platform::IosSimX64 => None, } } @@ -285,6 +339,9 @@ impl Platform { Platform::LinuxX64 => "Linux x64", Platform::LinuxArm64 => "Linux arm64", Platform::WindowsX64 => "Windows x64", + Platform::IosArm64 => "iOS arm64", + Platform::IosSimArm64 => "iOS simulator arm64", + Platform::IosSimX64 => "iOS simulator x64", Platform::AndroidArm64 => "Android arm64", Platform::AndroidX64 => "Android x64", Platform::Wasm32 => "WebAssembly (wasm32)", diff --git a/crates/weaveffi-gen/src/targets/c/buffer.rs b/crates/weaveffi-gen/src/targets/c/buffer.rs new file mode 100644 index 00000000..116ddeaa --- /dev/null +++ b/crates/weaveffi-gen/src/targets/c/buffer.rs @@ -0,0 +1,875 @@ +//! Rendering of the `{library}_buffer.h` value-buffer helper header. +//! +//! Records, rich enums, optionals, lists, and maps cross the C ABI as +//! serialized value buffers. This header gives a C consumer a plain C struct +//! for every record and rich enum (and for every error code that carries +//! fields), a struct for every list and map shape the API uses, and +//! `static inline` codecs for each: `{name}_write`, `{name}_read`, +//! `{name}_decode`, and `{name}_free`. Optionals are pointers (`NULL` is +//! absent), so they have codecs but no struct. +//! +//! The fixed reader and writer live in `runtime/buffer_runtime.h` and are +//! spliced in with the prefix substituted; everything after them is rendered +//! from the [`BindingModel`]. + +use std::collections::{HashMap, HashSet}; + +use crate::cabi::c_param_name; +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::model::{ + BindingModel, EnumBinding, FieldBinding, InterfaceBinding, Prim, Ty, WireType, +}; + +/// The fixed reader, writer, and string view types. +const RUNTIME: &str = include_str!("runtime/buffer_runtime.h"); + +/// Nesting depth past which the recursive size and ownership queries stop. +/// Only a by-value record cycle (which no producer can build) gets there. +const MAX_DEPTH: usize = 64; + +/// The fields a generated struct is built from. +enum Body<'a> { + /// A record or an error code's payload: fields in wire order. + Fields(&'a [FieldBinding]), + /// A rich enum: an `int32_t` tag, then the active variant's fields. + Variants(&'a EnumBinding), +} + +/// A record, rich enum, or error payload rendered as a named C struct. +struct UserType<'a> { + /// The struct typedef, which is also the stem of its codec functions. + name: String, + doc: Option, + body: Body<'a>, +} + +/// Every type the header renders, indexed for lookup by resolved name. +struct Codecs<'a> { + prefix: &'a str, + /// Records and rich enums in declaration order, then error payloads. + users: Vec>, + /// Absolute dot name (`kv.Entry`) to an index into `users`. + user_index: HashMap, + /// C-style enums by absolute dot name. + enums: HashMap, + /// Interfaces by absolute dot name. + interfaces: HashMap, + /// Optional, list, and map shapes, innermost first, without duplicates. + composites: Vec, +} + +/// The header file name for a library: `{library}_buffer.h`. +#[must_use] +pub fn buffer_header_name(library: &str) -> String { + format!("{library}_buffer.h") +} + +/// Render `{library}_buffer.h`, or `None` when no type in the API crosses +/// the ABI as a value buffer. +/// +/// `header_name` is the main header the helper includes; `file_name` is the +/// helper's own name, used in its trailer. +#[must_use] +pub fn render_buffer_header( + model: &BindingModel, + input_basename: &str, + header_name: &str, + file_name: &str, +) -> Option { + if !model.has_buffers() { + return None; + } + let codecs = Codecs::new(model); + let prefix = model.prefix.as_str(); + let guard = format!("{}_BUFFER_H", prefix.to_uppercase()); + + let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); + out.push_str(&format!("#ifndef {guard}\n#define {guard}\n\n")); + out.push_str(&format!("#include \"{header_name}\"\n\n")); + out.push_str("#include \n#include \n\n"); + out.push_str("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n"); + out.push_str(&RUNTIME.replace("{{p}}", prefix)); + out.push('\n'); + out.push_str(&codecs.render()); + out.push_str("#ifdef __cplusplus\n}\n#endif\n\n"); + out.push_str(&format!("#endif // {guard}\n\n")); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, file_name)); + Some(out) +} + +/// The four codec functions of one named type, as body lines. +struct Unit { + name: String, + /// The C type a decoded value is stored in. + vtype: String, + /// The type of `{name}_write`'s value parameter. + write_param: String, + write: Vec, + read: Vec, + free: Vec, +} + +impl<'a> Codecs<'a> { + fn new(model: &'a BindingModel) -> Self { + let prefix = model.prefix.as_str(); + let mut users = Vec::new(); + let mut user_index = HashMap::new(); + let mut enums = HashMap::new(); + let mut interfaces = HashMap::new(); + for m in &model.modules { + let qualify = |name: &str| format!("{}.{name}", m.dot_path); + for s in &m.structs { + user_index.insert(qualify(&s.name), users.len()); + users.push(UserType { + name: format!("{prefix}_{}_{}", m.path, s.name), + doc: with_deprecation(&s.doc, &s.deprecated), + body: Body::Fields(&s.fields), + }); + } + for e in &m.enums { + if e.rich { + user_index.insert(qualify(&e.name), users.len()); + users.push(UserType { + name: e.c_tag.clone(), + doc: with_deprecation(&e.doc, &e.deprecated), + body: Body::Variants(e), + }); + } else { + enums.insert(qualify(&e.name), e); + } + } + for i in &m.interfaces { + interfaces.insert(qualify(&i.name), i); + } + } + for m in &model.modules { + let Some(err) = m.error.as_ref().filter(|e| e.declared_here) else { + continue; + }; + for code in err.codes.iter().filter(|c| !c.fields.is_empty()) { + users.push(UserType { + name: format!("{}_payload", code.c_const), + doc: Some(format!( + "The fields a `{}` error carries in `payload_ptr`/`payload_len`.", + code.c_const + )), + body: Body::Fields(&code.fields), + }); + } + } + + let mut composites = Vec::new(); + let mut seen = HashSet::new(); + for u in &users { + match u.body { + Body::Fields(fields) => { + for f in fields { + collect(&f.ty, &mut composites, &mut seen); + } + } + Body::Variants(e) => { + for f in e.variants.iter().flat_map(|v| &v.fields) { + collect(&f.ty, &mut composites, &mut seen); + } + } + } + } + let mut boundary = |ty: &Ty| { + let ty = ty.iterator_elem().unwrap_or(ty); + if ty.is_buffered() { + collect(ty, &mut composites, &mut seen); + } + }; + for m in &model.modules { + for f in m.callables() { + f.params.iter().for_each(|p| boundary(&p.ty)); + f.ret.iter().for_each(&mut boundary); + } + for cb in &m.callback_interfaces { + for meth in &cb.methods { + meth.params.iter().for_each(|p| boundary(&p.ty)); + meth.ret.iter().for_each(&mut boundary); + } + } + } + + Codecs { + prefix, + users, + user_index, + enums, + interfaces, + composites, + } + } + + fn user(&self, name: &str) -> &UserType<'a> { + let idx = self + .user_index + .get(name) + .unwrap_or_else(|| panic!("unresolved record or rich enum '{name}'")); + &self.users[*idx] + } + + fn interface(&self, name: &str) -> &InterfaceBinding { + self.interfaces + .get(name) + .unwrap_or_else(|| panic!("unresolved interface '{name}'")) + } + + fn enum_tag(&self, name: &str) -> &str { + &self + .enums + .get(name) + .unwrap_or_else(|| panic!("unresolved enum '{name}'")) + .c_tag + } + + /// The codec stem of a record, rich enum, optional, list, or map. + fn named(&self, ty: &Ty) -> Option { + match ty { + Ty::Record(n) | Ty::RichEnum(n) => Some(self.user(n).name.clone()), + Ty::Optional(_) | Ty::List(_) | Ty::Map(_, _) => { + Some(format!("{}_{}", self.prefix, mangle(ty))) + } + _ => None, + } + } + + /// The C type a decoded value of `ty` is stored in. + fn vtype(&self, ty: &Ty) -> String { + let p = self.prefix; + match ty { + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { + self.named(ty).unwrap_or_default() + } + Ty::Enum(n) => self.enum_tag(n).to_string(), + Ty::Interface(n) => format!("{}*", self.interface(n).c_tag), + Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => self.vtype(inner), + Ty::Optional(inner) => format!("{}*", self.vtype(inner)), + Ty::StringUtf8 => format!("{p}_str"), + Ty::Bytes => format!("{p}_bytes"), + _ => prim_ctype(prim(ty)).to_string(), + } + } + + /// The parameter type of `{name}_write` for a named type: a pointer to + /// the value, except for an optional, which takes the (nullable) pointer + /// it is stored as. + fn write_param(&self, ty: &Ty) -> String { + let target = match ty { + Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => { + return format!("const {}", self.vtype(ty)); + } + Ty::Optional(inner) => self.vtype(inner), + _ => self.vtype(ty), + }; + const_ptr(&target) + } + + /// A statement appending the value at lvalue `lv` to writer `w`. + fn write_stmt(&self, ty: &Ty, lv: &str) -> String { + let p = self.prefix; + match ty { + Ty::Enum(_) => format!("{p}_writer_put_i32(w, (int32_t){lv});"), + Ty::Interface(n) => format!( + "{p}_writer_put_object(w, {}({lv}));", + self.interface(n).clone_symbol + ), + Ty::Optional(_) => format!("{}_write(w, {lv});", self.named(ty).unwrap_or_default()), + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { + format!("{}_write(w, &{lv});", self.named(ty).unwrap_or_default()) + } + _ => format!("{p}_writer_put_{}(w, {lv});", prim(ty).snake()), + } + } + + /// A statement decoding one value from reader `r` into lvalue `lv`. + fn read_stmt(&self, ty: &Ty, lv: &str) -> String { + let p = self.prefix; + match ty { + Ty::Enum(n) => format!("{lv} = ({}){p}_reader_get_i32(r);", self.enum_tag(n)), + Ty::Interface(n) => format!( + "{lv} = ({}*){p}_reader_get_object(r);", + self.interface(n).c_tag + ), + _ => match self.named(ty) { + Some(name) => format!("{name}_read(r, &{lv});"), + None => format!("{lv} = {p}_reader_get_{}(r);", prim(ty).snake()), + }, + } + } + + /// A statement releasing what the decoded value at lvalue `lv` owns, or + /// `None` when it owns nothing. + fn free_stmt(&self, ty: &Ty, lv: &str) -> Option { + if !self.needs_free(ty, 0) { + return None; + } + let p = self.prefix; + Some(match ty { + Ty::StringUtf8 => format!("{p}_str_free(&{lv});"), + Ty::Bytes => format!("{p}_bytes_free(&{lv});"), + Ty::Interface(n) => format!("{}({lv});", self.interface(n).destroy_symbol), + _ => format!("{}_free(&{lv});", self.named(ty).unwrap_or_default()), + }) + } + + /// Whether a decoded value of `ty` owns memory or object references. + fn needs_free(&self, ty: &Ty, depth: usize) -> bool { + match ty { + Ty::StringUtf8 + | Ty::Bytes + | Ty::Interface(_) + | Ty::Optional(_) + | Ty::List(_) + | Ty::Map(_, _) => true, + Ty::Record(n) | Ty::RichEnum(n) => { + depth > MAX_DEPTH || self.body_needs_free(&self.user(n).body, depth + 1) + } + _ => false, + } + } + + fn body_needs_free(&self, body: &Body<'_>, depth: usize) -> bool { + match body { + Body::Fields(fields) => fields.iter().any(|f| self.needs_free(&f.ty, depth)), + Body::Variants(e) => e + .variants + .iter() + .flat_map(|v| &v.fields) + .any(|f| self.needs_free(&f.ty, depth)), + } + } + + /// The fewest bytes one encoded value of `ty` can occupy, which bounds + /// how many elements a list or map count can honestly claim. + fn min_wire(&self, ty: &Ty, depth: usize) -> usize { + match ty { + Ty::Enum(_) => 4, + Ty::Interface(_) => 8, + Ty::Optional(_) => 1, + Ty::List(_) | Ty::Map(_, _) => 4, + Ty::RichEnum(_) => 4, + Ty::Record(n) => match self.user(n).body { + Body::Fields(fields) if depth <= MAX_DEPTH => { + fields.iter().map(|f| self.min_wire(&f.ty, depth + 1)).sum() + } + _ => 0, + }, + _ => match prim(ty) { + Prim::Bool | Prim::I8 | Prim::U8 => 1, + Prim::I16 | Prim::U16 => 2, + Prim::I32 | Prim::U32 | Prim::F32 | Prim::String | Prim::Bytes => 4, + Prim::I64 | Prim::U64 | Prim::F64 => 8, + }, + } + } + + fn render(&self) -> String { + let p = self.prefix; + let mut w = CodeWriter::four_space(); + w.line("/* ---------------------------------------------------------------------------"); + w.line(" * Value types. A record is a struct of its fields; a rich enum is a `tag`"); + w.line(" * plus a union `as` with one struct per payload-carrying variant; a list is"); + w.line(" * `items` + `len`; a map is parallel `keys` and `values` arrays + `len`; an"); + w.line(" * optional is a pointer that is NULL when absent; an object is its pointer;"); + w.line(" * a string or byte string is a view (`ptr` + `len`)."); + w.line(" * ------------------------------------------------------------------------- */"); + w.blank(); + for u in &self.users { + w.line(format!("typedef struct {0} {0};", u.name)); + } + let structs: Vec<&Ty> = self + .composites + .iter() + .filter(|t| matches!(t, Ty::List(_) | Ty::Map(_, _))) + .collect(); + for ty in &structs { + w.line(format!( + "typedef struct {0} {0};", + self.named(ty).unwrap_or_default() + )); + } + w.blank(); + for ty in &structs { + self.render_composite_struct(&mut w, ty); + } + for idx in self.struct_order() { + self.render_user_struct(&mut w, &self.users[idx]); + } + + let units = self.units(); + w.line("/* ---------------------------------------------------------------------------"); + w.line(" * Codecs. Each value type T above, and each optional shape, has four"); + w.line(" * functions:"); + w.line(" *"); + w.line(" * T_write(w, v) appends the value at `v` to writer `w`"); + w.line(" * T_read(r, out) decodes one value from reader `r`"); + w.line(" * T_decode(ptr, len, out) decodes a whole buffer; on a malformed"); + w.line(" * buffer it frees what it built and"); + w.line(" * returns false"); + w.line(" * T_free(v) releases a decoded value"); + w.line(" *"); + w.line(" * An optional's T_write takes the nullable pointer itself. Writing an"); + w.line(" * object stores a fresh `_clone` reference for the reader to adopt;"); + w.line(" * decoding adopts each object reference, and T_free destroys it. T_free"); + w.line(" * releases only what T_read or T_decode allocated; never call it on a"); + w.line(" * value that borrows caller memory."); + w.line(" * ------------------------------------------------------------------------- */"); + w.blank(); + for u in &units { + let n = &u.name; + let v = &u.vtype; + w.line(format!( + "static inline void {n}_write({p}_writer* w, {} v);", + u.write_param + )); + w.line(format!( + "static inline void {n}_read({p}_reader* r, {v}* out);" + )); + w.line(format!( + "static inline bool {n}_decode(const uint8_t* ptr, size_t len, {v}* out);" + )); + w.line(format!("static inline void {n}_free({v}* v);")); + } + for u in &units { + let n = &u.name; + let v = &u.vtype; + w.blank(); + w.block( + format!( + "static inline void {n}_write({p}_writer* w, {} v) {{", + u.write_param + ), + "}", + |w| { + u.write.iter().for_each(|l| { + w.line(l); + }) + }, + ); + w.blank(); + w.block( + format!("static inline void {n}_read({p}_reader* r, {v}* out) {{"), + "}", + |w| { + u.read.iter().for_each(|l| { + w.line(l); + }) + }, + ); + w.blank(); + w.block( + format!( + "static inline bool {n}_decode(const uint8_t* ptr, size_t len, {v}* out) {{" + ), + "}", + |w| { + w.line(format!("{p}_reader r = {p}_reader_of(ptr, len);")); + w.line(format!("{n}_read(&r, out);")); + w.block(format!("if (!{p}_reader_finish(&r)) {{"), "}", |w| { + w.line(format!("{n}_free(out);")); + w.line("return false;"); + }); + w.line("return true;"); + }, + ); + w.blank(); + w.block( + format!("static inline void {n}_free({v}* v) {{"), + "}", + |w| { + u.free.iter().for_each(|l| { + w.line(l); + }) + }, + ); + } + w.blank(); + w.finish() + } + + /// User structs ordered so every by-value field's struct is defined + /// before the struct that embeds it. + fn struct_order(&self) -> Vec { + fn visit(c: &Codecs<'_>, idx: usize, done: &mut Vec, order: &mut Vec) { + if done[idx] { + return; + } + done[idx] = true; + let fields: Vec<&FieldBinding> = match c.users[idx].body { + Body::Fields(fields) => fields.iter().collect(), + Body::Variants(e) => e.variants.iter().flat_map(|v| &v.fields).collect(), + }; + for f in fields { + if let Ty::Record(n) | Ty::RichEnum(n) = &f.ty { + if let Some(dep) = c.user_index.get(n) { + visit(c, *dep, done, order); + } + } + } + order.push(idx); + } + let mut done = vec![false; self.users.len()]; + let mut order = Vec::with_capacity(self.users.len()); + for idx in 0..self.users.len() { + visit(self, idx, &mut done, &mut order); + } + order + } + + fn render_composite_struct(&self, w: &mut CodeWriter, ty: &Ty) { + let name = self.named(ty).unwrap_or_default(); + w.line(format!("/** `{ty}` */")); + w.block(format!("struct {name} {{"), "};", |w| match ty { + Ty::List(elem) => { + w.line(format!("{}* items;", self.vtype(elem))); + w.line("size_t len;"); + } + Ty::Map(k, v) => { + w.line(format!("{}* keys;", self.vtype(k))); + w.line(format!("{}* values;", self.vtype(v))); + w.line("size_t len;"); + } + _ => {} + }); + w.blank(); + } + + fn render_fields(&self, w: &mut CodeWriter, fields: &[FieldBinding]) { + for f in fields { + w.doc(&f.doc, DocCommentStyle::Javadoc); + w.line(format!("{} {};", self.vtype(&f.ty), c_param_name(&f.name))); + } + } + + fn render_user_struct(&self, w: &mut CodeWriter, u: &UserType<'_>) { + w.doc(&u.doc, DocCommentStyle::Javadoc); + w.block(format!("struct {} {{", u.name), "};", |w| match u.body { + Body::Fields([]) => { + w.line("uint8_t empty_;"); + } + Body::Fields(fields) => self.render_fields(w, fields), + Body::Variants(e) => { + w.line(format!("{}_Tag tag;", e.c_tag)); + let payloads: Vec<_> = e.variants.iter().filter(|v| !v.fields.is_empty()).collect(); + if !payloads.is_empty() { + w.block("union {", "} as;", |w| { + for v in payloads { + w.doc(&v.doc, DocCommentStyle::Javadoc); + w.block("struct {", format!("}} {};", c_param_name(&v.name)), |w| { + self.render_fields(w, &v.fields); + }); + } + }); + } + } + }); + w.blank(); + } + + /// The codec surface of every named type: user structs, then the + /// optional, list, and map shapes. + fn units(&self) -> Vec { + let mut units: Vec = self.users.iter().map(|u| self.user_unit(u)).collect(); + units.extend(self.composites.iter().map(|ty| self.composite_unit(ty))); + units + } + + fn user_unit(&self, u: &UserType<'_>) -> Unit { + let p = self.prefix; + let mut write = Vec::new(); + let mut read = vec!["memset(out, 0, sizeof *out);".to_string()]; + let mut free = Vec::new(); + match u.body { + Body::Fields(fields) => { + if fields.is_empty() { + write.push("(void)w;".into()); + write.push("(void)v;".into()); + read.push("(void)r;".into()); + } + for f in fields { + let field = c_param_name(&f.name); + write.push(self.write_stmt(&f.ty, &format!("v->{field}"))); + read.push(self.read_stmt(&f.ty, &format!("out->{field}"))); + free.extend(self.free_stmt(&f.ty, &format!("v->{field}"))); + } + } + Body::Variants(e) => { + let member = + |v: &str, f: &str| format!("as.{}.{}", c_param_name(v), c_param_name(f)); + write.push(format!("{p}_writer_put_i32(w, (int32_t)v->tag);")); + write.push("switch (v->tag) {".into()); + read.push(format!("int32_t tag = {p}_reader_get_i32(r);")); + read.push("switch (tag) {".into()); + let mut free_cases = Vec::new(); + for v in &e.variants { + write.push(format!("case {}:", v.c_const)); + read.push(format!("case {}:", v.c_const)); + read.push(format!(" out->tag = {};", v.c_const)); + let mut frees = Vec::new(); + for f in &v.fields { + let m = member(&v.name, &f.name); + write.push(format!( + " {}", + self.write_stmt(&f.ty, &format!("v->{m}")) + )); + read.push(format!( + " {}", + self.read_stmt(&f.ty, &format!("out->{m}")) + )); + frees.extend(self.free_stmt(&f.ty, &format!("v->{m}"))); + } + write.push(" break;".into()); + read.push(" break;".into()); + if !frees.is_empty() { + free_cases.push(format!("case {}:", v.c_const)); + free_cases.extend(frees.into_iter().map(|l| format!(" {l}"))); + free_cases.push(" break;".into()); + } + } + write.extend( + ["default:", " w->failed = true;", " break;", "}"].map(String::from), + ); + read.extend( + ["default:", " r->failed = true;", " break;", "}"].map(String::from), + ); + if !free_cases.is_empty() { + free.push("switch (v->tag) {".into()); + free.extend(free_cases); + free.extend(["default:", " break;", "}"].map(String::from)); + } + } + } + free.push("memset(v, 0, sizeof *v);".into()); + Unit { + name: u.name.clone(), + vtype: u.name.clone(), + write_param: format!("const {}*", u.name), + write, + read, + free, + } + } + + fn composite_unit(&self, ty: &Ty) -> Unit { + let p = self.prefix; + let mut write = Vec::new(); + let mut read = Vec::new(); + let mut free = Vec::new(); + match ty { + Ty::List(elem) => { + let ev = self.vtype(elem); + write.push(format!("{p}_writer_put_len(w, v->len);")); + write.push("for (size_t i = 0; i < v->len; i++) {".into()); + write.push(format!(" {}", self.write_stmt(elem, "v->items[i]"))); + write.push("}".into()); + read.push("memset(out, 0, sizeof *out);".into()); + read.push(format!("size_t n = {p}_reader_get_u32(r);")); + read.push(format!( + "out->items = ({ev}*){p}_reader_alloc(r, n, sizeof({ev}), {});", + self.min_wire(elem, 0) + )); + read.extend(["if (out->items == NULL) {", " return;", "}"].map(String::from)); + read.push("out->len = n;".into()); + read.push("for (size_t i = 0; i < n; i++) {".into()); + read.push(format!(" {}", self.read_stmt(elem, "out->items[i]"))); + read.push("}".into()); + if let Some(stmt) = self.free_stmt(elem, "v->items[i]") { + free.push("for (size_t i = 0; i < v->len; i++) {".into()); + free.push(format!(" {stmt}")); + free.push("}".into()); + } + free.push("free(v->items);".into()); + free.push("memset(v, 0, sizeof *v);".into()); + } + Ty::Map(k, val) => { + let kv = self.vtype(k); + let vv = self.vtype(val); + write.push(format!("{p}_writer_put_len(w, v->len);")); + write.push("for (size_t i = 0; i < v->len; i++) {".into()); + write.push(format!(" {}", self.write_stmt(k, "v->keys[i]"))); + write.push(format!(" {}", self.write_stmt(val, "v->values[i]"))); + write.push("}".into()); + read.push("memset(out, 0, sizeof *out);".into()); + read.push(format!("size_t n = {p}_reader_get_u32(r);")); + read.push(format!( + "out->keys = ({kv}*){p}_reader_alloc(r, n, sizeof({kv}), {});", + self.min_wire(k, 0) + self.min_wire(val, 0) + )); + read.push(format!( + "out->values = ({vv}*){p}_reader_alloc(r, n, sizeof({vv}), 0);" + )); + read.extend( + [ + "if (out->keys == NULL || out->values == NULL) {", + " free(out->keys);", + " free(out->values);", + " out->keys = NULL;", + " out->values = NULL;", + " return;", + "}", + ] + .map(String::from), + ); + read.push("out->len = n;".into()); + read.push("for (size_t i = 0; i < n; i++) {".into()); + read.push(format!(" {}", self.read_stmt(k, "out->keys[i]"))); + read.push(format!(" {}", self.read_stmt(val, "out->values[i]"))); + read.push("}".into()); + let frees: Vec = [ + self.free_stmt(k, "v->keys[i]"), + self.free_stmt(val, "v->values[i]"), + ] + .into_iter() + .flatten() + .collect(); + if !frees.is_empty() { + free.push("for (size_t i = 0; i < v->len; i++) {".into()); + free.extend(frees.into_iter().map(|l| format!(" {l}"))); + free.push("}".into()); + } + free.push("free(v->keys);".into()); + free.push("free(v->values);".into()); + free.push("memset(v, 0, sizeof *v);".into()); + } + Ty::Optional(inner) => { + if let Ty::Interface(n) = inner.as_ref() { + let iface = self.interface(n); + write.push(format!("{p}_writer_put_bool(w, v != NULL);")); + write.push("if (v != NULL) {".into()); + write.push(format!( + " {p}_writer_put_object(w, {}(v));", + iface.clone_symbol + )); + write.push("}".into()); + read.push("*out = NULL;".into()); + read.push(format!("if ({p}_reader_get_bool(r)) {{")); + read.push(format!( + " *out = ({}*){p}_reader_get_object(r);", + iface.c_tag + )); + read.push("}".into()); + free.push(format!("{}(*v);", iface.destroy_symbol)); + free.push("*v = NULL;".into()); + } else { + let iv = self.vtype(inner); + write.push(format!("{p}_writer_put_bool(w, v != NULL);")); + write.push("if (v != NULL) {".into()); + write.push(format!(" {}", self.write_stmt(inner, "(*v)"))); + write.push("}".into()); + read.push("*out = NULL;".into()); + read.extend([ + format!("if (!{p}_reader_get_bool(r)) {{"), + " return;".into(), + "}".into(), + ]); + read.push(format!("{iv}* value = ({iv}*)calloc(1, sizeof({iv}));")); + read.extend( + [ + "if (value == NULL) {", + " r->failed = true;", + " return;", + "}", + ] + .map(String::from), + ); + read.push(self.read_stmt(inner, "(*value)")); + read.push("*out = value;".into()); + free.extend(["if (*v == NULL) {", " return;", "}"].map(String::from)); + free.extend(self.free_stmt(inner, "(**v)")); + free.push("free(*v);".into()); + free.push("*v = NULL;".into()); + } + } + _ => unreachable!("only optional, list, and map shapes are composites"), + } + Unit { + name: self.named(ty).unwrap_or_default(), + vtype: self.vtype(ty), + write_param: self.write_param(ty), + write, + read, + free, + } + } +} + +/// A type's doc comment with its deprecation note appended. The struct +/// itself carries no deprecation attribute: the codecs in this header use it, +/// and every such use would warn. +fn with_deprecation(doc: &Option, deprecated: &Option) -> Option { + match (doc, deprecated) { + (_, None) => doc.clone(), + (None, Some(msg)) => Some(format!("Deprecated: {msg}")), + (Some(doc), Some(msg)) => Some(format!("{doc}\n\nDeprecated: {msg}")), + } +} + +/// Record every optional, list, and map shape inside `ty`, innermost first. +fn collect(ty: &Ty, out: &mut Vec, seen: &mut HashSet) { + match ty { + Ty::Optional(inner) | Ty::List(inner) => collect(inner, out, seen), + Ty::Map(k, v) => { + collect(k, out, seen); + collect(v, out, seen); + } + _ => return, + } + if seen.insert(ty.clone()) { + out.push(ty.clone()); + } +} + +/// The identifier fragment naming `ty` inside a composite's codec stem: +/// `string`, `kv_Entry`, `opt_i64`, `list_string`, `map_string_i64`. +fn mangle(ty: &Ty) -> String { + match ty { + Ty::Optional(inner) => format!("opt_{}", mangle(inner)), + Ty::List(inner) => format!("list_{}", mangle(inner)), + Ty::Map(k, v) => format!("map_{}_{}", mangle(k), mangle(v)), + _ => match ty.user_name() { + Some(name) => name.replace('.', "_"), + None => prim(ty).snake().to_string(), + }, + } +} + +/// The wire primitive of a scalar, string, or bytes type. +fn prim(ty: &Ty) -> Prim { + match ty.wire() { + WireType::Prim(p) => p, + _ => unreachable!("{ty} is not a wire primitive"), + } +} + +/// The C type a decoded primitive is stored in. +fn prim_ctype(p: Prim) -> &'static str { + match p { + Prim::Bool => "bool", + Prim::I8 => "int8_t", + Prim::I16 => "int16_t", + Prim::I32 => "int32_t", + Prim::I64 => "int64_t", + Prim::U8 => "uint8_t", + Prim::U16 => "uint16_t", + Prim::U32 => "uint32_t", + Prim::U64 => "uint64_t", + Prim::F32 => "float", + Prim::F64 => "double", + Prim::String | Prim::Bytes => unreachable!("strings and bytes are views"), + } +} + +/// A pointer-to-const spelling of `target` that also reads correctly when +/// `target` is itself a pointer (`int64_t* const*`). +fn const_ptr(target: &str) -> String { + if target.ends_with('*') { + format!("{target} const*") + } else { + format!("const {target}*") + } +} diff --git a/crates/weaveffi-gen/src/targets/c/header.rs b/crates/weaveffi-gen/src/targets/c/header.rs new file mode 100644 index 00000000..0214cd45 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/c/header.rs @@ -0,0 +1,75 @@ +//! Rendering of the `{library}.h` header. +//! +//! The per-declaration rendering is shared with the C++ backend via +//! [`crate::cabi`]; this module adds only the header framing (include guard, +//! includes, and the value-buffer convention comment). + +use std::fmt::Write; + +use crate::cabi; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::resolved::ResolvedApi; + +/// Render the complete header for `api`. +/// +/// Thin wrapper over [`render_c_header_from_model`] for tests and callers +/// that only hold a [`ResolvedApi`]; the production path renders directly +/// from the driver-built [`BindingModel`] without re-deriving it. +pub fn render_c_header(api: &ResolvedApi, input_basename: &str, filename: &str) -> String { + render_c_header_from_model(&BindingModel::build(api), input_basename, filename) +} + +/// Render the complete header from the shared [`BindingModel`]. +/// +/// The per-declaration rendering is shared with the C++ backend via +/// [`crate::cabi`]; this function only adds the header framing +/// (include guard, includes, the value-buffer convention comment). The +/// C symbol prefix is read from [`BindingModel::prefix`], so every name already +/// agrees with the symbols baked into the model. Parameter names that collide +/// with a C or C++ keyword are escaped inside `cabi` itself (see +/// [`crate::cabi::c_param_name`]). +pub fn render_c_header_from_model( + model: &BindingModel, + input_basename: &str, + filename: &str, +) -> String { + let prefix = model.prefix.as_str(); + let guard = format!("{}_H", prefix.to_uppercase()); + 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"); + out.push_str("#include \n"); + out.push_str("#include \n\n"); + cabi::render_visibility_macros(&mut out, prefix); + out.push_str("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n"); + cabi::render_runtime_decls(&mut out, model); + let helper = format!( + "{}_buffer.h", + filename.strip_suffix(".h").unwrap_or(filename) + ); + out.push_str("/*\n"); + out.push_str(" * Strings, bytes, and value buffers cross as (ptr, len) pairs. A\n"); + out.push_str(" * parameter named \"v\" expands to a view borrowed for the call:\n"); + out.push_str(" * const uint8_t* v_ptr, size_t v_len\n"); + out.push_str(" * Strings are UTF-8 without a NUL terminator; NULL with length 0 is\n"); + out.push_str(" * the empty value. A returned string, bytes, or buffer is\n"); + out.push_str(" * `const uint8_t*` with a trailing `size_t* out_len`, owned by the\n"); + out.push_str(&format!( + " * caller and released with {prefix}_free_bytes.\n" + )); + out.push_str(" * Records, rich enums, lists, maps, and optionals are serialized\n"); + out.push_str(" * in the little-endian value buffer format; when generated,\n"); + out.push_str(&format!( + " * {helper} declares a C struct and codec for each.\n" + )); + out.push_str(" */\n\n"); + + cabi::render_decls(&mut out, &model.modules, prefix, true); + + out.push_str("\n#ifdef __cplusplus\n}\n#endif\n\n"); + let _ = write!(out, "#endif // {guard}\n\n"); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); + out +} diff --git a/crates/weaveffi-gen/src/targets/c/mod.rs b/crates/weaveffi-gen/src/targets/c/mod.rs new file mode 100644 index 00000000..5ef78eaf --- /dev/null +++ b/crates/weaveffi-gen/src/targets/c/mod.rs @@ -0,0 +1,155 @@ +//! C header generator for WeaveFFI. +//! +//! Emits `{library}.h`, describing the stable C ABI surface of a +//! [`ResolvedApi`]. This is the canonical backend: the header it emits *is* the +//! C ABI every other language binds to, and a C or C++ producer implementing +//! an IDL by hand implements exactly the functions it declares. +//! +//! When the API passes any value buffers and [`CConfig::buffer_helpers`] is +//! on (the default), it also emits `{library}_buffer.h`: C structs for the +//! API's records, rich enums, lists, and maps, with `static inline` codecs +//! that encode and decode them (see [`render_buffer_header`]). +//! +//! Like every WeaveFFI backend it renders from the shared +//! [`weaveffi_model::model::BindingModel`], so symbol names and parameter +//! lowering are computed once and shared, never re-derived here. + +mod buffer; +mod header; +mod package; +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::package::{PackageContext, PackagedFile}; +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::resolved::ResolvedApi; + +pub use buffer::{buffer_header_name, render_buffer_header}; +pub use header::{render_c_header, render_c_header_from_model}; + +use package::{render_packaged_cmake, render_packaged_readme}; + +/// Per-target configuration for [`CGenerator`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct CConfig { + /// Basename of the IDL the CLI was invoked with (e.g. `kvstore.yml`). + /// Embedded in the prelude header of every generated file. Populated + /// by the CLI; not user-configurable via the `[c]` config section. + #[serde(skip)] + pub input_basename: Option, + /// Emit `{library}_buffer.h`, the value-buffer helper header, next to + /// `{library}.h` when the API passes any value buffers. On by default. + pub buffer_helpers: bool, +} + +impl Default for CConfig { + fn default() -> Self { + Self { + input_basename: None, + buffer_helpers: true, + } + } +} + +impl CConfig { + /// The IDL basename for generated-file preludes. + pub fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } +} + +/// The header file name for an API: `{library}.h`. +#[must_use] +pub fn header_name(api: &ResolvedApi) -> String { + format!("{}.h", api.identity().library) +} + +/// The helper header's name and contents, when the configuration asks for it +/// and the API has value buffers to describe. +fn buffer_file( + api: &ResolvedApi, + model: &BindingModel, + config: &CConfig, +) -> Option<(String, String)> { + if !config.buffer_helpers { + return None; + } + let name = buffer_header_name(&api.identity().library); + render_buffer_header(model, config.input_basename(), &header_name(api), &name) + .map(|contents| (name, contents)) +} + +/// The C header backend. +pub struct CGenerator; + +impl LanguageBackend for CGenerator { + type Config = CConfig; + + fn name(&self) -> &'static str { + "c" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + let header = header_name(api); + let mut files = vec![OutputFile::new( + out_dir.join("c").join(&header), + render_c_header_from_model(model, config.input_basename(), &header), + )]; + if let Some((name, contents)) = buffer_file(api, model, config) { + files.push(OutputFile::new(out_dir.join("c").join(name), contents)); + } + files + } + + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let input_basename = config.input_basename(); + let dir = out_dir.join("c"); + let header_name = header_name(api); + let lib = &ctx.binaries.lib_name; + + let mut files = vec![ + PackagedFile::text( + dir.join("include").join(&header_name), + render_c_header_from_model(model, input_basename, &header_name), + ), + PackagedFile::text( + dir.join("CMakeLists.txt"), + render_packaged_cmake(lib, input_basename), + ), + PackagedFile::text( + dir.join("README.md"), + render_packaged_readme(lib, &header_name, ctx, input_basename), + ), + ]; + if let Some((name, contents)) = buffer_file(api, model, config) { + files.push(PackagedFile::text(dir.join("include").join(name), contents)); + } + for nb in &ctx.binaries.binaries { + let dest = dir + .join("lib") + .join(nb.platform.id()) + .join(ctx.binaries.bundled_filename(nb.platform)); + files.push(PackagedFile::copy(dest, nb.source.clone())); + } + Some(files) + } +} diff --git a/crates/weaveffi-gen-c/src/package.rs b/crates/weaveffi-gen/src/targets/c/package.rs similarity index 82% rename from crates/weaveffi-gen-c/src/package.rs rename to crates/weaveffi-gen/src/targets/c/package.rs index b2d929e9..569b3a07 100644 --- a/crates/weaveffi-gen-c/src/package.rs +++ b/crates/weaveffi-gen/src/targets/c/package.rs @@ -2,8 +2,8 @@ //! that exposes the bundled per-platform library as an imported target, and //! the README describing the layout. -use weaveffi_core::package::PackageContext; -use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; +use crate::package::PackageContext; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; /// Render a `CMakeLists.txt` that exposes the bundled per-platform library as /// an `IMPORTED` target (`::`) with the header's include directory, @@ -17,26 +17,26 @@ project(@LIB@ C) # Select the prebuilt library bundled for the host platform and architecture. if(APPLE) if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64") - set(_wv_plat "darwin-arm64") + set(_@LIB@_platform "darwin-arm64") else() - set(_wv_plat "darwin-x64") + set(_@LIB@_platform "darwin-x64") endif() - set(_wv_libfile "lib@LIB@.dylib") + set(_@LIB@_libfile "lib@LIB@.dylib") elseif(WIN32) - set(_wv_plat "windows-x64") - set(_wv_libfile "@LIB@.dll") + set(_@LIB@_platform "windows-x64") + set(_@LIB@_libfile "@LIB@.dll") else() if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") - set(_wv_plat "linux-arm64") + set(_@LIB@_platform "linux-arm64") else() - set(_wv_plat "linux-x64") + set(_@LIB@_platform "linux-x64") endif() - set(_wv_libfile "lib@LIB@.so") + set(_@LIB@_libfile "lib@LIB@.so") endif() add_library(@LIB@ SHARED IMPORTED GLOBAL) set_target_properties(@LIB@ PROPERTIES - IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/lib/${_wv_plat}/${_wv_libfile}" + IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/lib/${_@LIB@_platform}/${_@LIB@_libfile}" INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_LIST_DIR}/include") add_library(@LIB@::@LIB@ ALIAS @LIB@) "# diff --git a/crates/weaveffi-gen/src/targets/c/runtime/buffer_runtime.h b/crates/weaveffi-gen/src/targets/c/runtime/buffer_runtime.h new file mode 100644 index 00000000..d12ed889 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/c/runtime/buffer_runtime.h @@ -0,0 +1,339 @@ +/* --------------------------------------------------------------------------- + * Value buffer runtime. + * + * The wire format is little-endian and packed: fixed-width scalars at their + * natural width, `bool` as one byte, strings and bytes as a `uint32_t` length + * followed by the raw bytes (no NUL terminator), optionals as a presence byte + * followed by the value, lists as a `uint32_t` count followed by the + * elements, maps as a `uint32_t` count followed by alternating keys and + * values, and objects as a `uint64_t` token holding one strong reference. + * ------------------------------------------------------------------------- */ + +/* A UTF-8 string view: `len` bytes at `ptr`, which may contain NUL bytes. + A decoded string owns `ptr` and also NUL-terminates it, so `ptr` can be + passed to C string functions when the text has no interior NUL. */ +typedef struct {{p}}_str { + const char* ptr; + size_t len; +} {{p}}_str; + +/* A byte string view: `len` bytes at `ptr`. A decoded value owns `ptr`. */ +typedef struct {{p}}_bytes { + const uint8_t* ptr; + size_t len; +} {{p}}_bytes; + +/* Borrow a NUL-terminated C string (NULL is the empty string). */ +static inline {{p}}_str {{p}}_str_of(const char* s) { + {{p}}_str v; + v.ptr = s; + v.len = s ? strlen(s) : 0; + return v; +} + +/* Borrow `len` bytes at `ptr`. */ +static inline {{p}}_bytes {{p}}_bytes_of(const uint8_t* ptr, size_t len) { + {{p}}_bytes v; + v.ptr = ptr; + v.len = len; + return v; +} + +/* Release a decoded string. Never call this on a borrowed view. */ +static inline void {{p}}_str_free({{p}}_str* s) { + free((void*)s->ptr); + s->ptr = NULL; + s->len = 0; +} + +/* Release decoded bytes. Never call this on a borrowed view. */ +static inline void {{p}}_bytes_free({{p}}_bytes* b) { + free((void*)b->ptr); + b->ptr = NULL; + b->len = 0; +} + +/* A growable output buffer. Zero-initialize it, write values, pass + `ptr`/`len` to the call, then release it with {{p}}_writer_free. An + allocation failure sets `failed` and turns later writes into no-ops. */ +typedef struct {{p}}_writer { + uint8_t* ptr; + size_t len; + size_t cap; + bool failed; +} {{p}}_writer; + +static inline void {{p}}_writer_free({{p}}_writer* w) { + free(w->ptr); + w->ptr = NULL; + w->len = 0; + w->cap = 0; + w->failed = false; +} + +static inline void {{p}}_writer_put_raw({{p}}_writer* w, const void* data, size_t n) { + if (w->failed || n == 0) { + return; + } + if (n > w->cap - w->len) { + size_t cap = w->cap ? w->cap : 64; + while (cap - w->len < n) { + if (cap > SIZE_MAX / 2) { + w->failed = true; + return; + } + cap *= 2; + } + uint8_t* grown = (uint8_t*)realloc(w->ptr, cap); + if (grown == NULL) { + w->failed = true; + return; + } + w->ptr = grown; + w->cap = cap; + } + memcpy(w->ptr + w->len, data, n); + w->len += n; +} + +static inline void {{p}}_writer_put_u64({{p}}_writer* w, uint64_t v) { + uint8_t b[8]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)(v >> (8 * i)); + } + {{p}}_writer_put_raw(w, b, 8); +} + +static inline void {{p}}_writer_put_u32({{p}}_writer* w, uint32_t v) { + uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)}; + {{p}}_writer_put_raw(w, b, 4); +} + +static inline void {{p}}_writer_put_u16({{p}}_writer* w, uint16_t v) { + uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)}; + {{p}}_writer_put_raw(w, b, 2); +} + +static inline void {{p}}_writer_put_u8({{p}}_writer* w, uint8_t v) { + {{p}}_writer_put_raw(w, &v, 1); +} + +static inline void {{p}}_writer_put_i8({{p}}_writer* w, int8_t v) { + {{p}}_writer_put_u8(w, (uint8_t)v); +} + +static inline void {{p}}_writer_put_i16({{p}}_writer* w, int16_t v) { + {{p}}_writer_put_u16(w, (uint16_t)v); +} + +static inline void {{p}}_writer_put_i32({{p}}_writer* w, int32_t v) { + {{p}}_writer_put_u32(w, (uint32_t)v); +} + +static inline void {{p}}_writer_put_i64({{p}}_writer* w, int64_t v) { + {{p}}_writer_put_u64(w, (uint64_t)v); +} + +static inline void {{p}}_writer_put_bool({{p}}_writer* w, bool v) { + {{p}}_writer_put_u8(w, v ? 1 : 0); +} + +static inline void {{p}}_writer_put_f32({{p}}_writer* w, float v) { + uint32_t bits; + memcpy(&bits, &v, 4); + {{p}}_writer_put_u32(w, bits); +} + +static inline void {{p}}_writer_put_f64({{p}}_writer* w, double v) { + uint64_t bits; + memcpy(&bits, &v, 8); + {{p}}_writer_put_u64(w, bits); +} + +static inline void {{p}}_writer_put_len({{p}}_writer* w, size_t n) { + if (n > UINT32_MAX) { + w->failed = true; + return; + } + {{p}}_writer_put_u32(w, (uint32_t)n); +} + +static inline void {{p}}_writer_put_string({{p}}_writer* w, {{p}}_str v) { + {{p}}_writer_put_len(w, v.len); + {{p}}_writer_put_raw(w, v.ptr, v.len); +} + +static inline void {{p}}_writer_put_bytes({{p}}_writer* w, {{p}}_bytes v) { + {{p}}_writer_put_len(w, v.len); + {{p}}_writer_put_raw(w, v.ptr, v.len); +} + +/* Write an object token. The token carries one strong reference, so pass + a reference the buffer may own (the generated codecs pass a fresh + `_clone`). */ +static inline void {{p}}_writer_put_object({{p}}_writer* w, const void* owned) { + {{p}}_writer_put_u64(w, (uint64_t)(uintptr_t)owned); +} + +/* A cursor over an input buffer. A malformed buffer sets `failed`; every + later read then returns a zero value, so a decoder can always finish and + release what it built. */ +typedef struct {{p}}_reader { + const uint8_t* ptr; + size_t len; + size_t pos; + bool failed; +} {{p}}_reader; + +static inline {{p}}_reader {{p}}_reader_of(const uint8_t* ptr, size_t len) { + {{p}}_reader r; + r.ptr = ptr; + r.len = ptr ? len : 0; + r.pos = 0; + r.failed = false; + return r; +} + +/* `true` when every byte was consumed and nothing was malformed. */ +static inline bool {{p}}_reader_finish(const {{p}}_reader* r) { + return !r->failed && r->pos == r->len; +} + +static inline const uint8_t* {{p}}_reader_take({{p}}_reader* r, size_t n) { + if (r->failed || n > r->len - r->pos) { + r->failed = true; + return NULL; + } + const uint8_t* at = r->ptr + r->pos; + r->pos += n; + return at; +} + +/* Zeroed storage for `n` decoded elements of `size` bytes (NULL when `n` is + zero). `min_wire` is the smallest encoding of one element; a count the + remaining bytes can't hold fails the reader before anything is + allocated. */ +static inline void* {{p}}_reader_alloc({{p}}_reader* r, size_t n, size_t size, size_t min_wire) { + if (r->failed || n == 0) { + return NULL; + } + if (min_wire != 0 && n > (r->len - r->pos) / min_wire) { + r->failed = true; + return NULL; + } + void* p = calloc(n, size); + if (p == NULL) { + r->failed = true; + } + return p; +} + +static inline uint64_t {{p}}_reader_get_u64({{p}}_reader* r) { + const uint8_t* b = {{p}}_reader_take(r, 8); + uint64_t v = 0; + if (b != NULL) { + for (int i = 0; i < 8; i++) { + v |= (uint64_t)b[i] << (8 * i); + } + } + return v; +} + +static inline uint32_t {{p}}_reader_get_u32({{p}}_reader* r) { + const uint8_t* b = {{p}}_reader_take(r, 4); + if (b == NULL) { + return 0; + } + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static inline uint16_t {{p}}_reader_get_u16({{p}}_reader* r) { + const uint8_t* b = {{p}}_reader_take(r, 2); + return b ? (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)) : (uint16_t)0; +} + +static inline uint8_t {{p}}_reader_get_u8({{p}}_reader* r) { + const uint8_t* b = {{p}}_reader_take(r, 1); + return b ? b[0] : (uint8_t)0; +} + +static inline int8_t {{p}}_reader_get_i8({{p}}_reader* r) { + return (int8_t){{p}}_reader_get_u8(r); +} + +static inline int16_t {{p}}_reader_get_i16({{p}}_reader* r) { + return (int16_t){{p}}_reader_get_u16(r); +} + +static inline int32_t {{p}}_reader_get_i32({{p}}_reader* r) { + return (int32_t){{p}}_reader_get_u32(r); +} + +static inline int64_t {{p}}_reader_get_i64({{p}}_reader* r) { + return (int64_t){{p}}_reader_get_u64(r); +} + +static inline bool {{p}}_reader_get_bool({{p}}_reader* r) { + uint8_t v = {{p}}_reader_get_u8(r); + if (v > 1) { + r->failed = true; + return false; + } + return v == 1; +} + +static inline float {{p}}_reader_get_f32({{p}}_reader* r) { + uint32_t bits = {{p}}_reader_get_u32(r); + float v; + memcpy(&v, &bits, 4); + return v; +} + +static inline double {{p}}_reader_get_f64({{p}}_reader* r) { + uint64_t bits = {{p}}_reader_get_u64(r); + double v; + memcpy(&v, &bits, 8); + return v; +} + +/* A `uint32_t` length followed by that many bytes, copied into a fresh + NUL-terminated allocation. */ +static inline uint8_t* {{p}}_reader_get_run({{p}}_reader* r, size_t* out_len) { + size_t n = {{p}}_reader_get_u32(r); + const uint8_t* at = {{p}}_reader_take(r, n); + *out_len = 0; + if (at == NULL) { + return NULL; + } + uint8_t* copy = (uint8_t*)malloc(n + 1); + if (copy == NULL) { + r->failed = true; + return NULL; + } + memcpy(copy, at, n); + copy[n] = 0; + *out_len = n; + return copy; +} + +static inline {{p}}_str {{p}}_reader_get_string({{p}}_reader* r) { + {{p}}_str v; + v.ptr = (const char*){{p}}_reader_get_run(r, &v.len); + return v; +} + +static inline {{p}}_bytes {{p}}_reader_get_bytes({{p}}_reader* r) { + {{p}}_bytes v; + v.ptr = {{p}}_reader_get_run(r, &v.len); + return v; +} + +/* Read an object token and adopt the strong reference it carries. A zero + token in a required position fails the reader. */ +static inline void* {{p}}_reader_get_object({{p}}_reader* r) { + uint64_t token = {{p}}_reader_get_u64(r); + if (token == 0) { + r->failed = true; + } + return (void*)(uintptr_t)token; +} diff --git a/crates/weaveffi-gen-cpp/src/callbacks.rs b/crates/weaveffi-gen/src/targets/cpp/callbacks.rs similarity index 81% rename from crates/weaveffi-gen-cpp/src/callbacks.rs rename to crates/weaveffi-gen/src/targets/cpp/callbacks.rs index 5a994fad..9a145a8d 100644 --- a/crates/weaveffi-gen-cpp/src/callbacks.rs +++ b/crates/weaveffi-gen/src/targets/cpp/callbacks.rs @@ -1,16 +1,16 @@ //! Callback-interface rendering: the abstract class the consumer implements, //! the `extern "C"`-compatible trampolines that adapt an implementation to //! the C vtable, and the process-wide static vtable, per -//! `weaveffi_core::plan::CallbackProtocol`. +//! `weaveffi_model::plan::CallbackProtocol`. -use weaveffi_core::codegen::common::DocCommentStyle; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding, Ty}; -use weaveffi_core::plan::ArgPass; -use weaveffi_core::utils::{c_abi_struct_name, local_type_name}; +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding, Ty}; +use weaveffi_model::plan::ArgPass; -use crate::codec::emit_read_decl; -use crate::types::{ +use crate::targets::cpp::codec::emit_read_decl; +use crate::targets::cpp::types::{ cpp_cb_param_decl, cpp_fn_name, cpp_ident, cpp_type, render_param_decls, slot_name, trampoline_struct, vtable_accessor, }; @@ -20,8 +20,9 @@ const FOREIGN_ERROR_CODE: i32 = -4; /// Render the abstract class a consumer subclasses to implement a callback /// interface: a virtual destructor and one pure virtual method per IDL -/// method, with idiomatic C++ signatures (strings and buffered values by -/// const reference, object arguments by value as the adopted wrapper). +/// method, with idiomatic C++ signatures (strings as `std::string_view`, +/// buffered values by const reference, object arguments by value as the +/// adopted wrapper). pub(crate) fn render_callback_class(out: &mut String, cb: &CallbackInterfaceBinding) { let name = &cb.name; let usage = format!( @@ -72,14 +73,13 @@ pub(crate) fn render_callback_class(out: &mut String, cb: &CallbackInterfaceBind /// `ctx` is a heap-allocated `std::shared_ptr` box created when the /// implementation is passed to the producer; the trailing `free` entry /// deletes it. Each method trampoline receives its arguments per the -/// callback protocol (strings, bytes, and buffers borrowed and copied or -/// decoded; objects adopted), calls the virtual method, and on any exception +/// callback protocol (strings viewed in place, bytes and buffers borrowed and +/// copied or decoded; objects adopted), calls the virtual method, and on any exception /// reports `{prefix}_error_set(out_err, -4, message)` and returns a default /// value, so nothing ever unwinds through the C frame. pub(crate) fn render_callback_trampolines( out: &mut String, cb: &CallbackInterfaceBinding, - module: &str, prefix: &str, ) { let name = &cb.name; @@ -94,7 +94,7 @@ pub(crate) fn render_callback_trampolines( w.line(format!("struct {strukt} {{")); w.scope(|w| { for m in &cb.methods { - render_trampoline(w, cb, m, module, prefix); + render_trampoline(w, cb, m, prefix); } w.line( "/** Releases the implementation box once the producer drops its last reference. */", @@ -142,7 +142,6 @@ fn render_trampoline( w: &mut CodeWriter, cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding, - module: &str, prefix: &str, ) { let iface = &cb.name; @@ -170,9 +169,9 @@ fn render_trampoline( let var = arg_var(p); if nullable { w.line(format!("std::optional<{class}> {var};")); - w.line(format!("if ({slot}) {var}.emplace({slot});")); + w.line(format!("if ({slot}) {var}.emplace(adopt, {slot});")); } else { - w.line(format!("{class} {var}({slot});")); + w.line(format!("{class} {var}(adopt, {slot});")); } } } @@ -184,7 +183,7 @@ fn render_trampoline( let args: Vec = m .params .iter() - .map(|p| emit_trampoline_arg(w, p, module, prefix)) + .map(|p| emit_trampoline_arg(w, p)) .collect(); let call = format!("impl.{method}({})", args.join(", ")); // Validation restricts callback returns to the direct family, so @@ -194,10 +193,9 @@ fn render_trampoline( w.line(format!("{call};")); w.line("return;"); } - Some(Ty::Enum(e)) => { + Some(Ty::Enum(_)) => { w.line(format!( - "return static_cast<{}>(static_cast({call}));", - c_abi_struct_name(e, module, prefix) + "return static_cast<{c_ret}>(static_cast({call}));" )); } Some(_) => { @@ -233,10 +231,10 @@ fn arg_var(p: &ParamBinding) -> String { /// Emit any decode statements for one trampoline argument and return the /// expression handed to the virtual method. Strings, bytes, and buffered -/// values are borrowed for the call, so they are copied or decoded into owned -/// C++ values and nothing is freed. Objects were adopted before the `try` -/// block and are moved into the call. -fn emit_trampoline_arg(w: &mut CodeWriter, p: &ParamBinding, module: &str, prefix: &str) -> String { +/// values are borrowed for the call: a string is viewed in place, bytes and +/// buffers are copied or decoded into owned C++ values, and nothing is freed. +/// Objects were adopted before the `try` block and are moved into the call. +fn emit_trampoline_arg(w: &mut CodeWriter, p: &ParamBinding) -> String { let var = arg_var(p); match p.arg_pass() { ArgPass::Buffer { ptr, len } => { @@ -246,19 +244,24 @@ fn emit_trampoline_arg(w: &mut CodeWriter, p: &ParamBinding, module: &str, prefi slot_name(ptr), slot_name(len) )); - emit_read_decl(w, &p.ty, &var, &rdr, module, prefix); + emit_read_decl(w, &p.ty, &var, &rdr); w.line(format!("{rdr}.expect_end();")); var } - ArgPass::String { slot } => { - let n = slot_name(slot); - w.line(format!("std::string {var}({n} ? {n} : \"\");")); + ArgPass::String { ptr, len } => { + w.line(format!( + "std::string_view {var} = detail::borrow_string({}, {});", + slot_name(ptr), + slot_name(len) + )); var } ArgPass::Bytes { ptr, len } => { - let n0 = slot_name(ptr); - let n1 = slot_name(len); - w.line(format!("std::vector {var}({n0}, {n0} + {n1});")); + w.line(format!( + "std::vector {var} = detail::borrow_bytes({}, {});", + slot_name(ptr), + slot_name(len) + )); var } ArgPass::Object { .. } => format!("std::move({var})"), diff --git a/crates/weaveffi-gen-cpp/src/calls.rs b/crates/weaveffi-gen/src/targets/cpp/calls.rs similarity index 76% rename from crates/weaveffi-gen-cpp/src/calls.rs rename to crates/weaveffi-gen/src/targets/cpp/calls.rs index 94be775d..8897a718 100644 --- a/crates/weaveffi-gen-cpp/src/calls.rs +++ b/crates/weaveffi-gen/src/targets/cpp/calls.rs @@ -8,21 +8,22 @@ //! Free functions are defined inline inside their module namespace, which is //! rendered last. +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; use heck::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, ModuleBinding, ParamBinding, +use weaveffi_model::abi::AbiParam; +use weaveffi_model::model::Ty; +use weaveffi_model::model::{ + iterator_item_ctype, AbiFn, AsyncBinding, CallShape, FnBinding, IteratorBinding, ModuleBinding, + ParamBinding, }; -use weaveffi_core::plan::{ret_pass, ArgPass, ErrorStrategy, RetPass}; -use weaveffi_core::utils::{c_abi_struct_name, local_type_name}; +use weaveffi_model::plan::{ret_pass, ArgPass, ErrorStrategy, RetPass}; -use crate::codec::{emit_read_decl, emit_write_value}; -use crate::types::{ +use crate::targets::cpp::codec::{emit_read_decl, emit_write_value}; +use crate::targets::cpp::types::{ cpp_fn_name, cpp_ident, cpp_namespace_path, cpp_param_decl, cpp_type, render_param_decls, - vtable_accessor, + slot_name, vtable_accessor, }; // ── Error routing ── @@ -30,8 +31,7 @@ use crate::types::{ /// The `detail::check*` helper a wrapper calls after the C call returns, /// selected by the callable's [`ErrorStrategy`]: the per-domain variant /// (throwing the typed exception) for [`ErrorStrategy::Throws`] in a module -/// with an error domain in scope, the generic trap (`WeaveFFIError`) -/// otherwise. +/// with an error domain in scope, the generic trap (`Error`) otherwise. pub(crate) fn check_helper(f: &FnBinding, module: &ModuleBinding) -> String { match (&module.error, f.error_strategy()) { (Some(eb), ErrorStrategy::Throws) => format!("detail::check_{}", eb.owner_path), @@ -68,6 +68,8 @@ fn object_class(ty: &Ty) -> &str { /// expressions its ABI slots receive, dispatching on the parameter's /// [`ArgPass`] plan. /// +/// * A string is passed as its UTF-8 pointer and length (no terminator, so +/// interior NULs survive); bytes likewise. /// * A buffered parameter is encoded into a local `detail::BufferWriter` and /// passed as `(data(), size())`; the caller owns the encoding for the /// duration of the call. @@ -75,13 +77,10 @@ fn object_class(ty: &Ty) -> &str { /// wrapper keeps its own reference. `Interface?` passes null for none. /// * A callback interface moves the caller's `std::shared_ptr` into a heap /// box that becomes `ctx`; the producer releases it through the vtable's -/// `free`. The vtable is the process-wide static for the interface. -fn emit_param_setup( - w: &mut CodeWriter, - p: &ParamBinding, - module: &str, - prefix: &str, -) -> Vec { +/// `free`. The vtable is the process-wide static for the interface. The +/// box is held by a `std::unique_ptr` until the C call's argument list +/// releases it, so a throw while marshalling a later parameter frees it. +fn emit_param_setup(w: &mut CodeWriter, p: &ParamBinding, prefix: &str) -> Vec { let name = cpp_ident(&p.name); match p.arg_pass() { ArgPass::Buffer { .. } => { @@ -90,7 +89,9 @@ fn emit_param_setup( emit_write_value(w, &p.ty, &name, &buf, 0); vec![format!("{buf}.data()"), format!("{buf}.size()")] } - ArgPass::String { .. } => vec![format!("{name}.c_str()")], + ArgPass::String { .. } => { + vec![format!("detail::utf8({name})"), format!("{name}.size()")] + } ArgPass::Bytes { .. } => { vec![format!("{name}.data()"), format!("{name}.size()")] } @@ -110,17 +111,17 @@ fn emit_param_setup( "if (!{name}) throw std::invalid_argument(\"{name}: null callback interface\");" )); w.line(format!( - "auto* {name}_ctx = new std::shared_ptr<{iface}>(std::move({name}));" + "auto {name}_ctx = std::make_unique>(std::move({name}));" )); vec![ - format!("static_cast({name}_ctx)"), + format!("static_cast({name}_ctx.release())"), format!("&detail::{}()", vtable_accessor(iface)), ] } - ArgPass::Direct { .. } => match &p.ty { - Ty::Enum(e) => vec![format!( + ArgPass::Direct { slot } => match &p.ty { + Ty::Enum(_) => vec![format!( "static_cast<{}>(static_cast({name}))", - c_abi_struct_name(e, module, prefix) + slot.ty.render_c(prefix) )], _ => vec![name], }, @@ -129,36 +130,31 @@ fn emit_param_setup( /// Marshal a sync callable's C result (already error-checked) into the C++ /// return value at the writer's current depth, dispatching on the return's -/// [`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. An object return is adopted into the -/// RAII wrapper, which owes the eventual `_destroy`. -fn emit_sync_return(w: &mut CodeWriter, ty: &Ty, module: &str, prefix: &str) { - match ret_pass(Some(ty), module, prefix) { +/// [`RetPass`] plan. Strings, bytes, and buffered returns are copied or +/// decoded and then released with `{prefix}_free_bytes` (by a scope guard +/// for buffers, so the release happens even when decoding throws). An object +/// return is adopted into the RAII wrapper, which owes the eventual +/// `_destroy`. +fn emit_sync_return(w: &mut CodeWriter, ty: &Ty, prefix: &str) { + match ret_pass(Some(ty), prefix) { RetPass::Buffer => { - w.line("detail::BufferGuard result_guard{result, out_len};"); + w.line("detail::BufferGuard result_guard(result, out_len);"); w.line("detail::BufferReader result_r(result, out_len);"); - emit_read_decl(w, ty, "ret", "result_r", module, prefix); + emit_read_decl(w, ty, "ret", "result_r"); w.line("result_r.expect_end();"); w.line("return ret;"); } RetPass::String => { - w.line("std::string ret(result);"); - w.line(format!("{prefix}_free_string(result);")); - w.line("return ret;"); + w.line("return detail::take_string(result, out_len);"); } RetPass::Bytes => { - w.line("std::vector ret(result, result + out_len);"); - w.line(format!( - "{prefix}_free_bytes(const_cast(result), out_len);" - )); - w.line("return ret;"); + w.line("return detail::take_bytes(result, out_len);"); } RetPass::Object { nullable, .. } => { if nullable { w.line("if (!result) return std::nullopt;"); } - w.line(format!("return {}(result);", object_class(ty))); + w.line(format!("return {}(adopt, result);", object_class(ty))); } RetPass::Direct => match ty { Ty::Enum(n) => { @@ -250,18 +246,23 @@ fn return_type(f: &FnBinding, kind: FnKind<'_>) -> Option { }) } -/// The C++ parameter list of a callable. `with_defaults` adds the -/// `= nullptr` default on a cancellable async call's `cancel_token`, which -/// belongs on the declaration only. -fn param_decls(f: &FnBinding, prefix: &str, with_defaults: bool) -> String { +/// The C++ parameter list of a callable. A cancellable async call takes a +/// trailing `const CancelToken&`; `with_defaults` adds its +/// `= CancelToken::none()` default, which belongs on the first declaration +/// only. +fn param_decls(f: &FnBinding, with_defaults: bool) -> String { let mut decls: Vec = f .params .iter() .map(|p| cpp_param_decl(&p.ty, &cpp_ident(&p.name))) .collect(); if f.is_async && f.cancellable { - let default = if with_defaults { " = nullptr" } else { "" }; - decls.push(format!("{prefix}_cancel_token* cancel_token{default}")); + let default = if with_defaults { + " = CancelToken::none()" + } else { + "" + }; + decls.push(format!("const CancelToken& cancel_token{default}")); } decls.join(", ") } @@ -296,13 +297,14 @@ pub(crate) fn render_member_decl( f: &FnBinding, cpp_name: &str, kind: FnKind<'_>, - prefix: &str, ) { let mut w = CodeWriter::four_space().with_depth(1); emit_callable_attrs(&mut w, f, kind); - let params = param_decls(f, prefix, true); + let params = param_decls(f, true); match (kind, return_type(f, kind)) { - (FnKind::Ctor { class }, _) => w.line(format!("{class}({params});")), + // Explicit, so no argument list converts into a new producer object + // behind the caller's back (`std::optional t(5)`). + (FnKind::Ctor { class }, _) => w.line(format!("explicit {class}({params});")), (FnKind::Method { .. }, Some(ret)) => w.line(format!("{ret} {cpp_name}({params}) const;")), (FnKind::Static { .. }, Some(ret)) => w.line(format!("static {ret} {cpp_name}({params});")), _ => unreachable!("free functions are defined inline, never declared"), @@ -317,8 +319,11 @@ pub(crate) fn render_member_decl( /// by [`render_member_decl`]. /// /// Wrappers are deliberately never marked `noexcept`: a callable with -/// `throws == false` still surfaces producer panics as the generic -/// `WeaveFFIError`. +/// `throws == false` still surfaces producer panics as the generic `Error`. +/// +/// Every free function, constructor, and static member begins with +/// `check_library()`, which verifies the linked library once. Instance +/// methods skip it: their receiver could only come from a checked call. pub(crate) fn render_definition( out: &mut String, f: &FnBinding, @@ -331,7 +336,7 @@ pub(crate) fn render_definition( if matches!(kind, FnKind::Free) { emit_callable_attrs(&mut w, f, kind); } - let params = param_decls(f, prefix, false); + let params = param_decls(f, matches!(kind, FnKind::Free)); let ret = return_type(f, kind); let header = match (kind, ret) { (FnKind::Ctor { class }, _) => { @@ -350,10 +355,15 @@ pub(crate) fn render_definition( _ => unreachable!("every non-constructor callable has a return type"), }; w.line(header); - w.scope(|w| match &f.shape { - CallShape::Sync(abi) => emit_sync_body(w, f, abi, kind, module, prefix), - CallShape::Iterator(it) => emit_iterator_launch_body(w, f, it, kind, module, prefix), - CallShape::Async(a) => emit_async_body(w, f, a, kind, module, prefix), + w.scope(|w| { + if kind.self_arg().is_none() { + w.line("check_library();"); + } + match &f.shape { + CallShape::Sync(abi) => emit_sync_body(w, f, abi, kind, module, prefix), + CallShape::Iterator(it) => emit_iterator_launch_body(w, f, it, kind, module, prefix), + CallShape::Async(a) => emit_async_body(w, f, a, kind, module, prefix), + } }); w.line("}"); w.blank(); @@ -379,13 +389,13 @@ fn emit_sync_body( c_args.push(self_arg); } for p in &f.params { - c_args.extend(emit_param_setup(w, p, &module.path, prefix)); + c_args.extend(emit_param_setup(w, p, prefix)); } - // A bytes or buffered return carries a trailing `size_t* out_len`. + // A string, bytes, or buffered return carries a trailing `size_t* out_len`. let has_out_len = matches!( - ret_pass(f.ret.as_ref(), &module.path, prefix), - RetPass::Buffer | RetPass::Bytes + ret_pass(f.ret.as_ref(), prefix), + RetPass::Buffer | RetPass::String | RetPass::Bytes ); if has_out_len { w.line("size_t out_len = 0;"); @@ -405,7 +415,7 @@ fn emit_sync_body( if matches!(kind, FnKind::Ctor { .. }) { w.line("handle_ = result;"); } else if let Some(ret) = &f.ret { - emit_sync_return(w, ret, &module.path, prefix); + emit_sync_return(w, ret, prefix); } } @@ -416,18 +426,18 @@ fn emit_sync_body( /// The C ABI yields an opaque iterator pointer plus `_next`/`_destroy`. The /// range class is move-only, owns the pointer, and pulls exactly one element /// per consumer step, honoring the `iter` streaming contract -/// (`weaveffi_core::plan::IteratorProtocol`): +/// (`weaveffi_model::plan::IteratorProtocol`): /// /// * `begin()`/`end()` expose a single-pass input iterator with a sentinel /// end, so `for (auto&& item : fn())` streams in constant memory. -/// * Each pulled element is received per the plan's `elem` (strings copied -/// then `{prefix}_free_string`; bytes and buffered values copied or decoded -/// then `{prefix}_free_bytes`; objects adopted into their RAII wrapper). +/// * Each pulled element is received per the plan's `elem` (strings, bytes, +/// and buffered values copied or decoded then released with +/// `{prefix}_free_bytes`; objects adopted into their RAII wrapper). /// * `destroy` runs exactly once: eagerly on exhaustion or a `next` error, /// from the destructor otherwise. The pointer is nulled on every path. /// * Launch and per-`next` errors follow the callable's [`ErrorStrategy`] -/// (the typed domain exception for `Throws`, the generic `WeaveFFIError` -/// trap otherwise). +/// (the typed domain exception for `Throws`, the generic `Error` trap +/// otherwise). pub(crate) fn render_iterator_range( out: &mut String, f: &FnBinding, @@ -466,9 +476,9 @@ pub(crate) fn render_iterator_range( }); w.line("public:"); w.scope(|w| { - w.line("/** Adopts ownership of the raw producer iterator. */"); + w.line("/** Adopts ownership of a raw producer iterator. */"); w.line(format!( - "explicit {class_name}({iter_tag}* h) : handle_(h) {{}}" + "explicit {class_name}(adopt_t, {iter_tag}* h) noexcept : handle_(h) {{}}" )); w.blank(); @@ -576,7 +586,7 @@ fn emit_iterator_launch_body( c_args.push(self_arg); } for p in &f.params { - c_args.extend(emit_param_setup(w, p, &module.path, prefix)); + c_args.extend(emit_param_setup(w, p, prefix)); } c_args.push("&err".into()); w.line(format!("{prefix}_error err{{}};")); @@ -587,7 +597,7 @@ fn emit_iterator_launch_body( c_args.join(", ") )); w.line(format!("{check}(err);")); - w.line(format!("return {class_name}(iter);")); + w.line(format!("return {class_name}(adopt, iter);")); } /// Emit the range class's `next()` member: one producer `next` call that @@ -604,11 +614,11 @@ fn render_iterator_next_method( ) { let elem_cpp = cpp_type(&it.elem); let destroy = &it.destroy_symbol; - let item_ret = abi::lower_return(&it.elem, &module.path); - let item_ty = item_ret.ret.render_c(prefix); + let item_ty = iterator_item_ctype(&it.elem, &module.path).render_c(prefix); + let elem_pass = ret_pass(Some(&it.elem), prefix); let strategy_doc = match f.error_strategy() { ErrorStrategy::Throws => "throws the module's typed exception", - ErrorStrategy::Trap => "throws the generic WeaveFFIError", + ErrorStrategy::Trap => "throws the generic Error", }; w.doc( @@ -625,7 +635,10 @@ fn render_iterator_next_method( w.line(format!("{prefix}_error err{{}};")); w.line(format!("{item_ty} item{{}};")); let mut next_args = vec!["handle_".to_string(), "&item".to_string()]; - if !item_ret.out_params.is_empty() { + if matches!( + elem_pass, + RetPass::Buffer | RetPass::String | RetPass::Bytes + ) { w.line("size_t item_len = 0;"); next_args.push("&item_len".to_string()); } @@ -649,28 +662,22 @@ fn render_iterator_next_method( w.line("return std::nullopt;"); }); w.line("}"); - match ret_pass(Some(&it.elem), &module.path, prefix) { + match elem_pass { RetPass::Buffer => { // 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};"); + w.line("detail::BufferGuard item_guard(item, item_len);"); w.line("detail::BufferReader item_r(item, item_len);"); - emit_read_decl(w, &it.elem, "value", "item_r", &module.path, prefix); + emit_read_decl(w, &it.elem, "value", "item_r"); w.line("item_r.expect_end();"); w.line("return value;"); } RetPass::Bytes => { - w.line("std::vector value(item, item + item_len);"); - w.line(format!( - "{prefix}_free_bytes(const_cast(item), item_len);" - )); - w.line("return value;"); + w.line("return detail::take_bytes(item, item_len);"); } RetPass::String => { - w.line("std::string value(item);"); - w.line(format!("{prefix}_free_string(item);")); - w.line("return value;"); + w.line("return detail::take_string(item, item_len);"); } // An object element transfers one strong reference: adopt it. A // nullable element yields an engaged outer optional holding an @@ -679,10 +686,10 @@ fn render_iterator_next_method( let class = object_class(&it.elem); if nullable { w.line(format!("{elem_cpp} value;")); - w.line("if (item) value.emplace(item);"); + w.line("if (item) value.emplace(adopt, item);"); w.line("return value;"); } else { - w.line(format!("return {class}(item);")); + w.line(format!("return {class}(adopt, item);")); } } RetPass::Direct => match &it.elem { @@ -703,14 +710,18 @@ fn render_iterator_next_method( // ── Async ── -/// Emit an asynchronous callable's body as a `std::future` wrapper. The -/// promise is heap-allocated, threaded through the C `context` pointer, -/// settled by the completion callback, and deleted exactly once. A callback -/// error settles the promise with the typed domain exception (payload fields -/// decoded) when the callable throws, or the generic `WeaveFFIError` -/// otherwise. The callback owns everything it receives: the boxed error and -/// any string or buffer result are released through the runtime free symbols -/// after copying, and an object result is adopted. +/// Emit an asynchronous callable's body as a `std::future` wrapper. +/// +/// The parameters are marshalled first (so a throw there leaks nothing), +/// then a heap-allocated promise is threaded through the C `context` pointer +/// and adopted back into a `std::unique_ptr` by the completion callback, +/// which settles it exactly once. A callback error settles the promise with +/// the typed domain exception (payload fields decoded) when the callable +/// throws, `Cancelled` for code -5, or the generic `Error` otherwise. The +/// callback owns everything it receives: the boxed error and any string or +/// buffer result are released after copying, and an object result is +/// adopted. A cancellable call passes its `CancelToken`'s native token (null +/// for `CancelToken::none()`); the producer takes its own reference. fn emit_async_body( w: &mut CodeWriter, f: &FnBinding, @@ -720,24 +731,22 @@ fn emit_async_body( prefix: &str, ) { let cpp_ret = f.ret.as_ref().map_or("void".to_string(), cpp_type); + let promise = format!("std::promise<{cpp_ret}>"); let cb_params = render_param_decls(&a.callback_params, prefix).join(", "); let make_error = make_error_call(f, module); - w.line(format!( - "auto* promise_ptr = new std::promise<{cpp_ret}>();" - )); - w.line("auto future = promise_ptr->get_future();"); - let mut c_args = Vec::new(); if let Some(self_arg) = kind.self_arg() { c_args.push(self_arg); } for p in &f.params { - c_args.extend(emit_param_setup(w, p, &module.path, prefix)); + c_args.extend(emit_param_setup(w, p, prefix)); } if f.cancellable { - c_args.push("cancel_token".to_string()); + c_args.push("cancel_token.handle()".to_string()); } + w.line(format!("auto promise = std::make_unique<{promise}>();")); + w.line("auto future = promise->get_future();"); if c_args.is_empty() { w.line(format!("{}([]({cb_params}) {{", a.launch.symbol)); @@ -750,22 +759,22 @@ fn emit_async_body( } w.scope(|w| { w.line(format!( - "auto* p = static_cast*>(context);" + "std::unique_ptr<{promise}> p(static_cast<{promise}*>(context));" )); // The callback runs on a producer thread inside a C frame, so a // decode failure (of the result or of an error payload) must settle // the promise, never unwind. w.line("try {"); w.scope(|w| { - w.line("if (err && err->code != 0) {"); + w.line("if (err != nullptr && err->code != 0) {"); w.scope(|w| { - w.line("std::string msg(err->message ? err->message : \"unknown error\");"); + w.line("std::string msg = detail::error_message(*err);"); w.line(format!("p->set_exception({make_error});")); }); w.line("} else {"); w.scope(|w| { if let Some(ret) = &f.ret { - emit_async_set_value(w, ret, &module.path, prefix); + emit_async_set_value(w, ret, &a.callback_params[2..], prefix); } else { w.line("p->set_value();"); } @@ -778,66 +787,72 @@ fn emit_async_body( }); w.line("}"); w.line(format!("{prefix}_error_free(err);")); - w.line("delete p;"); }); - w.line("}, static_cast(promise_ptr));"); + w.line("}, static_cast(promise.release()));"); w.line("return future;"); } -/// Settle an async promise from the callback's result slots at the writer's -/// current depth, dispatching on the result's [`RetPass`] plan. +/// Settle an async promise from the callback's result slots (`slots`, the +/// callback parameters after `context` and `err`) at the writer's current +/// depth, dispatching on the result's [`RetPass`] plan. /// -/// Per the async completion contract (`weaveffi_core::plan::AsyncProtocol`), +/// Per the async completion contract (`weaveffi_model::plan::AsyncProtocol`), /// result buffers handed to the callback (strings, bytes, and buffered /// 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: &Ty, module: &str, prefix: &str) { - match ret_pass(Some(ty), module, prefix) { +/// then releases the producer allocation with `{prefix}_free_bytes`. An +/// owned interface result is instead adopted into the RAII wrapper. +fn emit_async_set_value(w: &mut CodeWriter, ty: &Ty, slots: &[AbiParam], prefix: &str) { + let slot = |i: usize| slot_name(&slots[i]); + match ret_pass(Some(ty), prefix) { RetPass::Buffer => { - w.line("detail::BufferGuard result_guard{result_ptr, result_len};"); - w.line("detail::BufferReader result_r(result_ptr, result_len);"); - emit_read_decl(w, ty, "value", "result_r", module, prefix); + let (ptr, len) = (slot(0), slot(1)); + w.line(format!("detail::BufferGuard result_guard({ptr}, {len});")); + w.line(format!("detail::BufferReader result_r({ptr}, {len});")); + emit_read_decl(w, ty, "value", "result_r"); w.line("result_r.expect_end();"); w.line("p->set_value(std::move(value));"); } RetPass::String => { - w.line("std::string value(result ? result : \"\");"); - w.line(format!("{prefix}_free_string(result);")); - w.line("p->set_value(std::move(value));"); + w.line(format!( + "p->set_value(detail::take_string({}, {}));", + slot(0), + slot(1) + )); } RetPass::Bytes => { - w.line("std::vector value(result, result + result_len);"); w.line(format!( - "{prefix}_free_bytes(const_cast(result), result_len);" + "p->set_value(detail::take_bytes({}, {}));", + slot(0), + slot(1) )); - w.line("p->set_value(std::move(value));"); } RetPass::Object { nullable, .. } => { let class = object_class(ty); + let result = slot(0); if nullable { - w.line("if (!result) {"); + w.line(format!("if ({result} == nullptr) {{")); w.scope(|w| { w.line("p->set_value(std::nullopt);"); }); w.line("} else {"); w.scope(|w| { - w.line(format!("p->set_value({class}(result));")); + w.line(format!("p->set_value({class}(adopt, {result}));")); }); w.line("}"); } else { - w.line(format!("p->set_value({class}(result));")); + w.line(format!("p->set_value({class}(adopt, {result}));")); } } RetPass::Direct => match ty { Ty::Enum(n) => { w.line(format!( - "p->set_value(static_cast<{}>(result));", - local_type_name(n) + "p->set_value(static_cast<{}>({}));", + local_type_name(n), + slot(0) )); } _ => { - w.line("p->set_value(result);"); + w.line(format!("p->set_value({});", slot(0))); } }, RetPass::Void => unreachable!("void results settle the promise with no value"), diff --git a/crates/weaveffi-gen-cpp/src/codec.rs b/crates/weaveffi-gen/src/targets/cpp/codec.rs similarity index 70% rename from crates/weaveffi-gen-cpp/src/codec.rs rename to crates/weaveffi-gen/src/targets/cpp/codec.rs index c85446fa..be374e75 100644 --- a/crates/weaveffi-gen-cpp/src/codec.rs +++ b/crates/weaveffi-gen/src/targets/cpp/codec.rs @@ -1,5 +1,5 @@ //! Value-buffer codec emission: the statements and expressions that move C++ -//! values through the WeaveFFI wire format, plus the per-type pack and unpack +//! values through the value-buffer wire format, plus the per-type pack and unpack //! routines. //! //! Every encode/decode decision dispatches on [`Ty::wire`], the shared wire @@ -8,14 +8,14 @@ //! one user-codec shape, and interfaces as `u64` object tokens carrying one //! strong reference. -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 crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::model::Ty; +use weaveffi_model::model::WireType; +use weaveffi_model::model::{EnumBinding, StructBinding}; -use crate::types::{cpp_ident, cpp_type, interface_c_tag}; +use crate::targets::cpp::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 @@ -29,19 +29,7 @@ pub(crate) fn emit_write_value(w: &mut CodeWriter, ty: &Ty, expr: &str, wtr: &st w.line(format!("{wtr}.{method}({expr});")); }; 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::Prim(p) => leaf(w, &format!("write_{}", p.snake())), WireType::Enum(_) => { w.line(format!("{wtr}.write_i32(static_cast({expr}));")); } @@ -85,27 +73,16 @@ pub(crate) fn emit_write_value(w: &mut CodeWriter, ty: &Ty, expr: &str, wtr: &st /// /// An object token decodes into a new wrapper that adopts the strong /// reference the token carries; the wrapper's destructor releases it. -fn read_leaf_expr(ty: &Ty, rdr: &str, module: &str, prefix: &str) -> Option { +fn read_leaf_expr(ty: &Ty, rdr: &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::Prim(p) => format!("{rdr}.read_{}()", p.snake()), WireType::Enum(n) => format!("static_cast<{}>({rdr}.read_i32())", local_type_name(n)), - WireType::Object(n) => format!( - "{}(reinterpret_cast<{}*>(static_cast({rdr}.read_u64())))", - local_type_name(n), - interface_c_tag(n, module, prefix) - ), + WireType::Object(n) => { + let class = local_type_name(n); + format!( + "{class}(adopt, reinterpret_cast<{class}::raw_type*>(static_cast({rdr}.read_u64())))" + ) + } WireType::User(n) => format!("detail::read_{}({rdr})", local_type_name(n)), WireType::Optional(_) | WireType::List(_) | WireType::Map(_, _) => return None, }) @@ -114,16 +91,8 @@ fn read_leaf_expr(ty: &Ty, rdr: &str, module: &str, prefix: &str) -> Option { w.line("{"); w.scope(|w| { - w.line(format!("size_t {tmp}_n = {rdr}.read_len();")); - w.line(format!("{target}.reserve({tmp}_n);")); + w.line(format!("size_t {tmp}_n = {rdr}.read_count();")); + w.line(format!("{target}.reserve({rdr}.reserve_hint({tmp}_n));")); w.line(format!( "for (size_t {tmp}_i = 0; {tmp}_i < {tmp}_n; ++{tmp}_i) {{" )); w.scope(|w| { let var = format!("{tmp}_item"); - emit_read_decl(w, inner, &var, rdr, module, prefix); + emit_read_decl(w, inner, &var, rdr); w.line(format!("{target}.push_back(std::move({var}));")); }); w.line("}"); @@ -157,15 +126,15 @@ pub(crate) fn emit_read_into( WireType::Map(k, v) => { w.line("{"); w.scope(|w| { - w.line(format!("size_t {tmp}_n = {rdr}.read_len();")); + w.line(format!("size_t {tmp}_n = {rdr}.read_count();")); w.line(format!( "for (size_t {tmp}_i = 0; {tmp}_i < {tmp}_n; ++{tmp}_i) {{" )); w.scope(|w| { let key = format!("{tmp}_key"); let val = format!("{tmp}_val"); - emit_read_decl(w, k, &key, rdr, module, prefix); - emit_read_decl(w, v, &val, rdr, module, prefix); + emit_read_decl(w, k, &key, rdr); + emit_read_decl(w, v, &val, rdr); w.line(format!( "{target}.emplace(std::move({key}), std::move({val}));" )); @@ -185,20 +154,13 @@ pub(crate) fn emit_read_into( /// default constructed here: a record holding an interface has no default /// constructor, so record values are only ever produced by their `read_` /// routine. -pub(crate) fn emit_read_decl( - w: &mut CodeWriter, - ty: &Ty, - var: &str, - rdr: &str, - module: &str, - prefix: &str, -) { +pub(crate) fn emit_read_decl(w: &mut CodeWriter, ty: &Ty, var: &str, rdr: &str) { let cpp = cpp_type(ty); - if let Some(expr) = read_leaf_expr(ty, rdr, module, prefix) { + if let Some(expr) = read_leaf_expr(ty, rdr) { w.line(format!("{cpp} {var} = {expr};")); } else { w.line(format!("{cpp} {var}{{}};")); - emit_read_into(w, ty, var, var, rdr, module, prefix); + emit_read_into(w, ty, var, var, rdr); } } @@ -209,12 +171,12 @@ pub(crate) fn emit_read_decl( /// an interface (and therefore has no default constructor) decodes like any /// other. If a later field fails to decode, the locals already holding /// adopted object references are destroyed on unwind, releasing them. -pub(crate) fn render_record_codec(out: &mut String, s: &StructBinding, module: &str, prefix: &str) { +pub(crate) fn render_record_codec(out: &mut String, s: &StructBinding) { let name = &s.name; let mut w = CodeWriter::four_space(); w.doc( &Some(format!( - "Encodes a `{name}` in the WeaveFFI value-buffer format." + "Encodes a `{name}` in the value-buffer wire format." )), DocCommentStyle::Javadoc, ); @@ -235,7 +197,7 @@ pub(crate) fn render_record_codec(out: &mut String, s: &StructBinding, module: & w.doc( &Some(format!( - "Decodes a `{name}` from the WeaveFFI value-buffer format." + "Decodes a `{name}` from the value-buffer wire format." )), DocCommentStyle::Javadoc, ); @@ -247,7 +209,7 @@ pub(crate) fn render_record_codec(out: &mut String, s: &StructBinding, module: & let mut inits = Vec::new(); for f in &s.fields { let var = format!("f_{}", f.name); - emit_read_decl(w, &f.ty, &var, "r", module, prefix); + emit_read_decl(w, &f.ty, &var, "r"); inits.push(format!("std::move({var})")); } w.line(format!("return {name}{{{}}};", inits.join(", "))); @@ -260,17 +222,12 @@ pub(crate) fn render_record_codec(out: &mut String, s: &StructBinding, module: & /// Emit the pack and unpack routines for one rich enum (inside `detail`): /// an `i32` tag followed by the active variant's fields in wire order. Like /// records, variant payloads are aggregate-initialized from decoded locals. -pub(crate) fn render_rich_enum_codec( - out: &mut String, - e: &EnumBinding, - module: &str, - prefix: &str, -) { +pub(crate) fn render_rich_enum_codec(out: &mut String, e: &EnumBinding) { let name = &e.name; let mut w = CodeWriter::four_space(); w.doc( &Some(format!( - "Encodes a `{name}` in the WeaveFFI value-buffer format." + "Encodes a `{name}` in the value-buffer wire format." )), DocCommentStyle::Javadoc, ); @@ -301,7 +258,7 @@ pub(crate) fn render_rich_enum_codec( w.doc( &Some(format!( - "Decodes a `{name}` from the WeaveFFI value-buffer format." + "Decodes a `{name}` from the value-buffer wire format." )), DocCommentStyle::Javadoc, ); @@ -319,7 +276,7 @@ pub(crate) fn render_rich_enum_codec( let mut inits = Vec::new(); for f in &variant.fields { let var = format!("f_{}", f.name); - emit_read_decl(w, &f.ty, &var, "r", module, prefix); + emit_read_decl(w, &f.ty, &var, "r"); inits.push(format!("std::move({var})")); } w.line(format!( @@ -336,7 +293,7 @@ pub(crate) fn render_rich_enum_codec( }); w.line("}"); w.line(format!( - "throw WeaveFFIError(-2, \"malformed WeaveFFI value buffer: unknown {name} tag\");" + "throw Error(-3, \"malformed value buffer: unknown {name} tag\");" )); }); w.line("}"); diff --git a/crates/weaveffi-gen-cpp/src/entities.rs b/crates/weaveffi-gen/src/targets/cpp/entities.rs similarity index 91% rename from crates/weaveffi-gen-cpp/src/entities.rs rename to crates/weaveffi-gen/src/targets/cpp/entities.rs index 44c4cf46..8dc4ae41 100644 --- a/crates/weaveffi-gen-cpp/src/entities.rs +++ b/crates/weaveffi-gen/src/targets/cpp/entities.rs @@ -4,19 +4,19 @@ use std::collections::HashMap; -use weaveffi_core::codegen::common::DocCommentStyle; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{ +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::model::Ty; +use weaveffi_model::model::{ CallShape, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, ModuleBinding, StructBinding, }; -use weaveffi_core::utils::local_type_name; -use crate::calls::{ +use crate::targets::cpp::calls::{ iterator_class_name, render_definition, render_iterator_range, render_member_decl, FnKind, }; -use crate::codec::emit_read_decl; -use crate::types::{cpp_error_class, cpp_fn_name, cpp_ident, cpp_type}; +use crate::targets::cpp::codec::emit_read_decl; +use crate::targets::cpp::types::{cpp_error_class, cpp_fn_name, cpp_ident, cpp_type}; // ── Namespace: enums ── @@ -220,7 +220,7 @@ pub(crate) fn render_cpp_rich_enum(out: &mut String, e: &EnumBinding) { // ── Typed error domains ── /// Emit one module's typed error domain: a domain exception derived from -/// `WeaveFFIError`, one subclass per declared code (with typed members for +/// `Error`, one subclass per declared code (with typed members for /// any payload fields the code declares), and the per-domain /// `detail::make_{path}_error`/`detail::check_{path}` helpers that throwing /// wrappers use to map a nonzero `out_err` to the typed exception, decoding @@ -228,9 +228,10 @@ pub(crate) fn render_cpp_rich_enum(out: &mut String, e: &EnumBinding) { /// /// Domain codes are validated positive-only, and the runtime reserves every /// negative code (generic error, producer panic, marshalling failure, foreign -/// callback failure), so the mapping helper routes negative codes to the -/// generic `WeaveFFIError` before consulting the domain's cases; unknown -/// positive codes fall back to the domain exception itself. +/// callback failure, cancellation), so the mapping helper routes negative +/// codes to `detail::make_error` (the generic `Error`, or `Cancelled` for +/// -5) before consulting the domain's cases; unknown positive codes fall back +/// to the domain exception itself. pub(crate) fn render_domain_error(out: &mut String, eb: &ErrorBinding, prefix: &str) { let domain = &eb.type_name; let path = &eb.owner_path; @@ -240,11 +241,11 @@ pub(crate) fn render_domain_error(out: &mut String, eb: &ErrorBinding, prefix: & "/** Typed errors reported by the `{}` module's throwing functions. */", eb.owner_path )); - w.line(format!("class {domain} : public WeaveFFIError {{")); + w.line(format!("class {domain} : public Error {{")); w.line("public:"); w.scope(|w| { w.line(format!( - "{domain}(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {{}}" + "{domain}(int32_t code, const std::string& msg) : Error(code, msg) {{}}" )); }); w.line("};"); @@ -263,7 +264,7 @@ pub(crate) fn render_domain_error(out: &mut String, eb: &ErrorBinding, prefix: & } if code.fields.is_empty() { w.line(format!( - "{class}(const std::string& msg) : {domain}({}, msg) {{}}", + "explicit {class}(const std::string& msg) : {domain}({}, msg) {{}}", code.value )); } else { @@ -289,7 +290,7 @@ pub(crate) fn render_domain_error(out: &mut String, eb: &ErrorBinding, prefix: & w.line("namespace detail {"); w.blank(); w.line(format!( - "/** Map a `{path}` error code and payload to its typed exception (WeaveFFIError for runtime codes, {domain} for unknown codes). */" + "/** Map a `{path}` error code and payload to its typed exception (the runtime exception for negative codes, {domain} for unknown codes). */" )); w.line(format!( "inline std::exception_ptr make_{path}_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) {{" @@ -297,9 +298,9 @@ pub(crate) fn render_domain_error(out: &mut String, eb: &ErrorBinding, prefix: & w.scope(|w| { // The negative range is reserved for the runtime (-1 generic error, // -2 producer panic, -3 marshalling failure, -4 foreign callback - // failure); those never map to a typed domain error, on the throwing - // path or anywhere else. - w.line("if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg));"); + // failure, -5 cancelled); those never map to a typed domain error, on + // the throwing path or anywhere else. + w.line("if (code < 0) return make_error(code, msg);"); if eb.codes.iter().all(|c| c.fields.is_empty()) { w.line("(void)payload_ptr;"); w.line("(void)payload_len;"); @@ -319,7 +320,7 @@ pub(crate) fn render_domain_error(out: &mut String, eb: &ErrorBinding, prefix: & let mut args = vec!["msg".to_string()]; for fld in &code.fields { let var = format!("f_{}", fld.name); - emit_read_decl(w, &fld.ty, &var, "payload_r", path, prefix); + emit_read_decl(w, &fld.ty, &var, "payload_r"); args.push(format!("std::move({var})")); } w.line("payload_r.expect_end();"); @@ -344,7 +345,7 @@ pub(crate) fn render_domain_error(out: &mut String, eb: &ErrorBinding, prefix: & w.line(format!("inline void check_{path}({prefix}_error& err) {{")); w.scope(|w| { w.line("if (err.code == 0) return;"); - w.line("std::string msg(err.message ? err.message : \"unknown error\");"); + w.line("std::string msg = error_message(err);"); // The payload buffer is owned by the error and released by // error_clear, so the exception (which decodes it) is built first. w.line(format!( @@ -363,7 +364,8 @@ pub(crate) fn render_domain_error(out: &mut String, eb: &ErrorBinding, prefix: & // ── Namespace: interfaces ── /// Emit the reference-counting RAII skeleton of an interface class: the -/// adopted `{c_tag}*`, a destructor releasing one reference, copy operations +/// `raw_type` alias of the C tag, the tagged constructor adopting one +/// strong reference, a destructor releasing one reference, copy operations /// that take a new reference through the `_clone` symbol (so C++ copy /// semantics equal a reference-count clone), move operations that transfer /// the pointer, and the `handle()`/`clone_handle()` readers the marshalling @@ -374,8 +376,16 @@ fn emit_raii_skeleton(w: &mut CodeWriter, i: &InterfaceBinding) { let clone = &i.clone_symbol; let destroy = &i.destroy_symbol; - w.line("/** Adopts one strong reference to a producer object. */"); - w.line(format!("explicit {name}({tag}* h) : handle_(h) {{}}")); + w.line("/** The C type this class wraps. */"); + w.line(format!("using raw_type = {tag};")); + w.blank(); + + w.line(format!( + "/** Adopts one strong reference to a producer object: `{name}(adopt, raw)`. */" + )); + w.line(format!( + "explicit {name}(adopt_t, {tag}* h) noexcept : handle_(h) {{}}" + )); w.blank(); w.line("/** Releases this wrapper's reference; the object is dropped with its last one. */"); @@ -482,7 +492,7 @@ pub(crate) fn render_cpp_interface_forward_decls(out: &mut String, i: &Interface /// [`render_cpp_interface_members`], once every value type and codec they /// marshal through is complete; the class itself must be complete first so /// records can hold it by value. -pub(crate) fn render_cpp_interface_class(out: &mut String, i: &InterfaceBinding, prefix: &str) { +pub(crate) fn render_cpp_interface_class(out: &mut String, i: &InterfaceBinding) { let name = &i.name; let mut w = CodeWriter::four_space(); w.doc(&i.doc, DocCommentStyle::Javadoc); @@ -495,7 +505,7 @@ pub(crate) fn render_cpp_interface_class(out: &mut String, i: &InterfaceBinding, w.scope(|w| emit_raii_skeleton(w, i)); let mut members = String::new(); for (f, cpp_name, kind) in member_kinds(i) { - render_member_decl(&mut members, f, &cpp_name, kind, prefix); + render_member_decl(&mut members, f, &cpp_name, kind); } w.raw(members); w.line("};"); diff --git a/crates/weaveffi-gen-cpp/src/lib.rs b/crates/weaveffi-gen/src/targets/cpp/mod.rs similarity index 53% rename from crates/weaveffi-gen-cpp/src/lib.rs rename to crates/weaveffi-gen/src/targets/cpp/mod.rs index da25b5fb..56ad94da 100644 --- a/crates/weaveffi-gen-cpp/src/lib.rs +++ b/crates/weaveffi-gen/src/targets/cpp/mod.rs @@ -1,63 +1,52 @@ -//! C++ wrapper generator for WeaveFFI. +//! C++ wrapper generator. //! -//! Produces an idiomatic `weaveffi.hpp` header (value structs, `std::variant` -//! sum types, reference-counted RAII object wrappers, `std::optional`, -//! `std::vector`, exception-based error handling) plus a `CMakeLists.txt` -//! skeleton on top of the C ABI emitted by -//! [`weaveffi-gen-c`](../weaveffi_gen_c/index.html). The header inlines the -//! same `extern "C"` declarations through `weaveffi_core::cabi`, so it is -//! self-contained. Implements [`LanguageBackend`]; the shared driver bridges -//! it into the generator pipeline. +//! Produces an idiomatic header-only C++17 library over the C ABI: the C +//! header `{library}.h` (the same file the [`c`](super::c) target emits), a +//! wrapper header `{library}.hpp` that includes it, a `CMakeLists.txt` +//! exporting an INTERFACE target, and a README. Everything lives in +//! `namespace {prefix}` by default. Implements [`LanguageBackend`]; the +//! shared driver bridges it into the generator pipeline. //! -//! The generated surface follows ABI revision 2: +//! The generated surface follows C ABI revision 3: //! +//! * Strings cross as a UTF-8 pointer and length: parameters are +//! `std::string_view`, returns are `std::string` copied out of the +//! producer's allocation and released with `{prefix}_free_bytes`. //! * Records are plain C++ value structs with typed members; rich (algebraic) //! enums are `std::variant`-backed sum types with one payload struct per //! variant. Neither has any C symbols: values cross the ABI serialized in -//! the WeaveFFI value-buffer format as one `(const uint8_t*, size_t)` pair, -//! through a small private reader/writer in `detail` plus one generated -//! pack and unpack routine per type. +//! the value-buffer format as one `(const uint8_t*, size_t)` pair, through +//! a small private reader and writer in `detail` plus one generated pack +//! and unpack routine per type. //! * Interfaces are reference-counted objects owned by the producer. Each //! becomes an RAII class holding one strong reference: the destructor calls -//! the `_destroy` symbol exactly once, copying calls `_clone` (so C++ copy -//! semantics equal a reference-count clone), and moving transfers the -//! pointer. A top-level object parameter is borrowed for the call; a -//! returned object, an async result, and an iterator element are adopted. -//! `Interface?` maps to `std::optional` with a null pointer as -//! `std::nullopt`. An interface inside a record, list, map value, optional, -//! or rich-enum payload is held by value and crosses the buffer as a `u64` -//! token minted with `_clone` on write and adopted on read. -//! * Callback interfaces become abstract classes with one pure virtual method -//! per IDL method. The consumer passes a `std::shared_ptr`; the -//! wrapper boxes it on the heap as `ctx`, hands the producer the one -//! process-wide static vtable for the interface, and the vtable's `free` -//! deletes the box. Trampolines decode borrowed arguments, adopt object -//! arguments, and report any exception through `{prefix}_error_set` with -//! the foreign-error code `-4` instead of unwinding through the C frame. +//! the `_destroy` symbol exactly once, copying calls `_clone`, and moving +//! transfers the pointer. Adopting a raw pointer takes the `adopt` tag, so +//! no integer or pointer converts into an object by accident. A top-level +//! object parameter is borrowed for the call; a returned object, an async +//! result, and an iterator element are adopted. `Interface?` maps to +//! `std::optional`. An interface inside a value buffer crosses as +//! a `u64` token minted with `_clone` on write and adopted on read. +//! * Callback interfaces become abstract classes. The consumer passes a +//! `std::shared_ptr`; the wrapper boxes it on the heap as `ctx` and +//! hands the producer the process-wide static vtable, whose `free` deletes +//! the box. Trampolines report any exception through `{prefix}_error_set` +//! with the foreign-error code -4 instead of unwinding through the C frame. //! * Free functions live in a nested namespace per IDL module -//! (`kv::stats::get_stats`), with bare snake_case names. +//! (`kvstore::kv::stats::get_stats`). //! * An `iter` callable returns a move-only lazy range class that pulls //! one element per iteration step and releases the producer iterator from -//! its destructor (or eagerly on exhaustion), per the `weaveffi_core::plan` -//! iterator contract. Buffered elements are decoded per pull and released -//! with `{prefix}_free_bytes`; object elements are adopted. +//! its destructor (or eagerly on exhaustion). +//! * Async callables return `std::future`. A cancellable one takes a +//! trailing `const CancelToken&` (an RAII wrapper over the native token); +//! a cancelled call settles the future with `Cancelled`. //! * Each declaring module's error domain becomes an exception type derived -//! from the generic `WeaveFFIError`, with one subclass per code. A code -//! that declares payload fields exposes them as typed members on its -//! subclass, decoded from the error's payload buffer. A callable with -//! `throws == true` throws the typed domain exception; a callable with -//! `throws == false` still checks `out_err` and throws the generic -//! `WeaveFFIError`. Domain codes are validated positive-only, so a negative -//! runtime code (generic error, producer panic, marshalling failure, or a -//! callback implementation that threw) always surfaces as the generic -//! `WeaveFFIError`, never a typed domain exception. No wrapper is marked -//! `noexcept` for exactly that reason. -//! * `check_abi_version()` compares the producer's exported ABI revision with -//! the one this header was generated against. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] +//! from `Error`, with one subclass per code. Negative runtime codes surface +//! as the generic `Error` (or `Cancelled` for -5), never a typed domain +//! exception. +//! * `check_library()` verifies the producer's ABI revision and every +//! top-level module's contract checksum once, and throws `LoadError` on a +//! mismatch. Every free function, constructor, and static member calls it. mod callbacks; mod calls; @@ -67,57 +56,61 @@ mod package; mod runtime; mod types; +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::package::{PackageContext, PackagedFile}; +use crate::targets::c::{header_name as c_header_name, render_c_header_from_model}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::backend::{LanguageBackend, OutputFile}; -use weaveffi_core::cabi; -use weaveffi_core::capabilities::TargetCapabilities; -use weaveffi_core::model::{BindingModel, EnumBinding, 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_model::model::{BindingModel, EnumBinding, ModuleBinding}; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; -use crate::callbacks::{render_callback_class, render_callback_trampolines}; -use crate::calls::render_cpp_module_ns; -use crate::entities::{ +use crate::targets::cpp::callbacks::{render_callback_class, render_callback_trampolines}; +use crate::targets::cpp::calls::render_cpp_module_ns; +use crate::targets::cpp::entities::{ render_cpp_enums, render_cpp_interface_class, render_cpp_interface_forward_decls, render_cpp_interface_iterators, render_cpp_interface_members, render_cpp_record, render_cpp_rich_enum, render_domain_error, topo_order, ValueDef, }; -use crate::package::{render_cmake, render_packaged_cmake, render_packaged_readme, render_readme}; -use crate::runtime::{render_buffer_runtime, render_generic_error}; +use crate::targets::cpp::package::{ + render_cmake, render_packaged_cmake, render_packaged_readme, render_readme, CmakeNames, +}; +use crate::targets::cpp::runtime::{render_buffer_runtime, render_prelude_runtime}; /// Per-target configuration for [`CppGenerator`]. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct CppConfig { - /// C++ namespace (default `"weaveffi"`). + /// C++ namespace holding every generated declaration (default: the + /// package's C prefix). pub namespace: Option, - /// Filename of the emitted C++ header (default `"weaveffi.hpp"`). + /// Filename of the wrapper header (default `{library}.hpp`). pub header_name: Option, /// C++ standard advertised in the generated `CMakeLists.txt` (default /// `"17"`). pub standard: Option, - /// C ABI symbol prefix that the C++ wrappers call into. Must match the - /// configured C generator prefix. Defaults to `"weaveffi"`. - pub prefix: Option, /// Basename of the IDL the CLI was invoked with. #[serde(skip)] pub input_basename: Option, } impl CppConfig { - /// Returns the configured C++ namespace, falling back to `"weaveffi"`. - pub fn namespace(&self) -> &str { - self.namespace.as_deref().unwrap_or("weaveffi") + /// Returns the configured C++ namespace, falling back to the identity's + /// C prefix. + pub fn namespace(&self, identity: &Identity) -> String { + self.namespace + .clone() + .unwrap_or_else(|| identity.prefix.clone()) } - /// Returns the emitted header's filename, falling back to - /// `"weaveffi.hpp"`. - pub fn header_name(&self) -> &str { - self.header_name.as_deref().unwrap_or("weaveffi.hpp") + /// Returns the wrapper header's filename, falling back to + /// `{library}.hpp`. + pub fn header_name(&self, identity: &Identity) -> String { + self.header_name + .clone() + .unwrap_or_else(|| format!("{}.hpp", identity.library)) } /// Returns the C++ standard advertised in the generated `CMakeLists.txt`, @@ -126,23 +119,44 @@ impl CppConfig { self.standard.as_deref().unwrap_or("17") } - /// Returns the C ABI symbol prefix the C++ wrappers call into, falling - /// back to `"weaveffi"`. - pub fn prefix(&self) -> &str { - self.prefix.as_deref().unwrap_or("weaveffi") - } - /// Returns the input IDL basename embedded in generated file headers, - /// falling back to `"weaveffi.yml"`. + /// falling back to `"api.yml"`. pub fn input_basename(&self) -> &str { - self.input_basename.as_deref().unwrap_or("weaveffi.yml") + self.input_basename.as_deref().unwrap_or("api.yml") } } -/// C++ backend: emits an idiomatic wrapper header (`weaveffi.hpp` by default) -/// plus a `CMakeLists.txt` skeleton over the C ABI. +/// C++ backend: emits a wrapper header (`{library}.hpp` by default), a copy +/// of the C header it includes, a `CMakeLists.txt`, and a README. pub struct CppGenerator; +/// The names every rendered file agrees on, resolved once per run. +struct Names { + namespace: String, + header: String, + c_header: String, +} + +impl Names { + fn new(api: &ResolvedApi, config: &CppConfig) -> Self { + Self { + namespace: config.namespace(api.identity()), + header: config.header_name(api.identity()), + c_header: c_header_name(api), + } + } + + fn cmake<'a>(&'a self, identity: &'a Identity, config: &'a CppConfig) -> CmakeNames<'a> { + CmakeNames { + identity, + namespace: &self.namespace, + header: &self.header, + standard: config.standard(), + input_basename: config.input_basename(), + } + } +} + impl LanguageBackend for CppGenerator { type Config = CppConfig; @@ -154,10 +168,6 @@ impl LanguageBackend for CppGenerator { TargetCapabilities::full() } - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { - config.prefix() - } - fn files( &self, api: &ResolvedApi, @@ -166,23 +176,28 @@ impl LanguageBackend for CppGenerator { config: &Self::Config, ) -> Vec { let dir = out_dir.join("cpp"); - let header_name = config.header_name(); + let identity = api.identity(); + let names = Names::new(api, config); let input_basename = config.input_basename(); + let cmake = names.cmake(identity, config); vec![ OutputFile::new( - dir.join(header_name), - render_cpp_header(model, config.namespace(), input_basename, header_name), + dir.join(&names.c_header), + render_c_header_from_model(model, input_basename, &names.c_header), ), OutputFile::new( - dir.join("CMakeLists.txt"), - render_cmake( - config.standard(), - &weaveffi_core::pkg::resolve(api, None, config.input_basename.as_deref()) - .version, + dir.join(&names.header), + render_cpp_header( + model, + identity, + &names.namespace, + &names.c_header, input_basename, + &names.header, ), ), - OutputFile::new(dir.join("README.md"), render_readme(input_basename)), + OutputFile::new(dir.join("CMakeLists.txt"), render_cmake(&cmake)), + OutputFile::new(dir.join("README.md"), render_readme(&cmake)), ] } @@ -195,28 +210,34 @@ impl LanguageBackend for CppGenerator { config: &Self::Config, ) -> Option> { let dir = out_dir.join("cpp"); - let header_name = config.header_name(); + let identity = api.identity(); + let names = Names::new(api, config); let input_basename = config.input_basename(); - let version = - weaveffi_core::pkg::resolve(api, None, config.input_basename.as_deref()).version; + let cmake = names.cmake(identity, config); let lib = &ctx.binaries.lib_name; - // The C++ header inlines the `extern "C"` declarations, so the package - // is self-contained: header + prebuilt library + CMake, no separate C - // header needed. + let include = dir.join("include"); let mut files = vec![ PackagedFile::text( - dir.join("include").join(header_name), - render_cpp_header(model, config.namespace(), input_basename, header_name), + include.join(&names.c_header), + render_c_header_from_model(model, input_basename, &names.c_header), ), PackagedFile::text( - dir.join("CMakeLists.txt"), - render_packaged_cmake(lib, &version, config.standard(), input_basename), + include.join(&names.header), + render_cpp_header( + model, + identity, + &names.namespace, + &names.c_header, + input_basename, + &names.header, + ), ), PackagedFile::text( - dir.join("README.md"), - render_packaged_readme(lib, header_name, ctx, input_basename), + dir.join("CMakeLists.txt"), + render_packaged_cmake(&cmake, lib), ), + PackagedFile::text(dir.join("README.md"), render_packaged_readme(&cmake, ctx)), ]; // The packaged CMake selects among the desktop platforms only, so // Android and Wasm binaries (which belong to other ecosystems' @@ -237,13 +258,15 @@ impl LanguageBackend for CppGenerator { } } -/// Render the complete C++ header from the driver-built binding model. +/// Render the complete C++ wrapper header from the driver-built binding +/// model. `c_header` is the C header file it includes. /// /// Layout inside `namespace {namespace}`, in an order that keeps every type /// complete before it is held by value or marshalled: /// -/// 1. the generic error surface and the private value-buffer runtime (when -/// any buffered value crosses the ABI); +/// 1. the runtime: error types, the adopt tag, `CancelToken`, string helpers, +/// `check_library()`, and the private value-buffer codec (when any +/// buffered value crosses the ABI); /// 2. plain enums; /// 3. forward declarations of every value type, interface class, /// callback-interface class, and member iterator range class; @@ -259,7 +282,9 @@ impl LanguageBackend for CppGenerator { /// 10. one nested namespace per module holding its free functions. pub(crate) fn render_cpp_header( model: &BindingModel, + identity: &Identity, namespace: &str, + c_header: &str, input_basename: &str, filename: &str, ) -> String { @@ -273,45 +298,45 @@ pub(crate) fn render_cpp_header( out.push_str(&render_prelude(CommentStyle::DoubleSlash, input_basename)); out.push_str("#pragma once\n\n"); - out.push_str("#include \n"); - out.push_str("#include \n"); - out.push_str("#include \n"); - out.push_str("#include \n"); - out.push_str("#include \n"); - out.push_str("#include \n"); - out.push_str("#include \n"); - out.push_str("#include \n"); - out.push_str("#include \n"); + out.push_str(&format!("#include \"{c_header}\"\n\n")); + let mut includes = vec![ + "cstddef", + "cstdint", + "exception", + "memory", + "new", + "optional", + "stdexcept", + "string", + "string_view", + "unordered_map", + "utility", + "vector", + ]; if has_rich_enums { - out.push_str("#include \n"); + includes.push("variant"); } if needs_buffers { // The buffer runtime needs memcpy for float bits. - out.push_str("#include \n"); + includes.push("cstring"); } if model.has_async() { - out.push_str("#include \n"); + includes.push("future"); } - // The lazy iterator range classes need std::input_iterator_tag and - // std::ptrdiff_t. if model.has_iterators() { - out.push_str("#include \n"); - out.push_str("#include \n"); + // The lazy range classes need std::input_iterator_tag. + includes.push("iterator"); + } + includes.sort_unstable(); + for inc in includes { + out.push_str(&format!("#include <{inc}>\n")); } out.push('\n'); - cabi::render_visibility_macros(&mut out, prefix); - out.push_str(&render_abi_prefix_aliases(prefix)); - out.push_str("extern \"C\" {\n\n"); - cabi::render_runtime_decls(&mut out, prefix); - cabi::render_decls(&mut out, &model.modules, prefix, false); - out.push_str("} // extern \"C\"\n\n"); - out.push_str(&format!("namespace {namespace} {{\n\n")); - - render_generic_error(&mut out, prefix); + out.push_str(&render_prelude_runtime(model, identity)); if needs_buffers { - render_buffer_runtime(&mut out, prefix); + out.push_str(&render_buffer_runtime(prefix)); } // Enums first: they reference no other types and are used by value. @@ -347,7 +372,7 @@ pub(crate) fn render_cpp_header( // follow those. for module in &model.modules { for i in &module.interfaces { - render_cpp_interface_class(&mut out, i, prefix); + render_cpp_interface_class(&mut out, i); } } @@ -356,31 +381,24 @@ pub(crate) fn render_cpp_header( // complete, so nested types are emitted first. The pack/unpack routines // follow in the same order so a codec can call the codecs of the types it // nests. - let value_entries: Vec<(ValueDef, &ModuleBinding)> = model + let value_entries: Vec = model .modules .iter() - .flat_map(|m| { - let records = m.structs.iter().map(move |s| (ValueDef::Record(s), m)); - let rich = m - .enums - .iter() - .filter(|e| e.is_rich()) - .map(move |e| (ValueDef::Rich(e), m)); + .flat_map(|m: &ModuleBinding| { + let records = m.structs.iter().map(ValueDef::Record); + let rich = m.enums.iter().filter(|e| e.is_rich()).map(ValueDef::Rich); records.chain(rich) }) .collect(); let value_order = topo_order( &value_entries .iter() - .map(|(v, _)| v.name().to_string()) - .collect::>(), - &value_entries - .iter() - .map(|(v, _)| v.deps()) + .map(|v| v.name().to_string()) .collect::>(), + &value_entries.iter().map(ValueDef::deps).collect::>(), ); for &idx in &value_order { - match &value_entries[idx].0 { + match &value_entries[idx] { ValueDef::Record(s) => render_cpp_record(&mut out, s), ValueDef::Rich(e) => render_cpp_rich_enum(&mut out, e), } @@ -388,14 +406,9 @@ pub(crate) fn render_cpp_header( if !value_entries.is_empty() { out.push_str("namespace detail {\n\n"); for &idx in &value_order { - let (v, module) = &value_entries[idx]; - match v { - ValueDef::Record(s) => { - codec::render_record_codec(&mut out, s, &module.path, prefix) - } - ValueDef::Rich(e) => { - codec::render_rich_enum_codec(&mut out, e, &module.path, prefix) - } + match &value_entries[idx] { + ValueDef::Record(s) => codec::render_record_codec(&mut out, s), + ValueDef::Rich(e) => codec::render_rich_enum_codec(&mut out, e), } } out.push_str("} // namespace detail\n\n"); @@ -416,7 +429,7 @@ pub(crate) fn render_cpp_header( for m in &model.modules { for cb in &m.callback_interfaces { render_callback_class(&mut out, cb); - render_callback_trampolines(&mut out, cb, &m.path, prefix); + render_callback_trampolines(&mut out, cb, prefix); } } diff --git a/crates/weaveffi-gen/src/targets/cpp/package.rs b/crates/weaveffi-gen/src/targets/cpp/package.rs new file mode 100644 index 00000000..3f6fb573 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/cpp/package.rs @@ -0,0 +1,203 @@ +//! Build-integration files: the source-layout `CMakeLists.txt`, the packaged +//! CMake importing prebuilt libraries, and the READMEs. +//! +//! CMake and Markdown are the only formats emitted here; neither is JSON or +//! XML, so the shared manifest escaping helpers don't apply. The only +//! interpolated values are identity names (C identifiers), the version, and +//! the configured C++ standard. + +use crate::package::PackageContext; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::pkg::Identity; + +/// The names the build files and READMEs interpolate. +pub(crate) struct CmakeNames<'a> { + /// The package identity. + pub(crate) identity: &'a Identity, + /// The C++ namespace. + pub(crate) namespace: &'a str, + /// The wrapper header's file name. + pub(crate) header: &'a str, + /// The C++ standard (`17`). + pub(crate) standard: &'a str, + /// The IDL basename for the generated-file prelude. + pub(crate) input_basename: &'a str, +} + +impl CmakeNames<'_> { + /// Replace the `@NAME@` placeholders shared by the CMake templates. + fn fill(&self, template: &str) -> String { + let id = self.identity; + template + .replace("@LIBRARY@", &id.library) + .replace("@VERSION@", &id.version) + .replace("@ENV@", &id.library_env_var()) + .replace("@STD@", self.standard) + .replace("@HEADER@", self.header) + .replace("@NAMESPACE@", self.namespace) + } +} + +/// Render the source-layout `CMakeLists.txt`: an INTERFACE library +/// `{library}_cpp` (alias `{library}::cpp`) that adds the generated headers +/// to the include path and links the producer library, by the path in the +/// `{PREFIX}_LIBRARY` cache variable or environment variable when set, by +/// the name `{library}` otherwise. +pub(crate) fn render_cmake(names: &CmakeNames<'_>) -> String { + let body = r#"cmake_minimum_required(VERSION 3.14) +project(@LIBRARY@_cpp VERSION @VERSION@ LANGUAGES CXX) + +# The producer library to link: a full path from the @ENV@ cache or +# environment variable, or else the library name `@LIBRARY@` (a CMake target +# of that name, or lib@LIBRARY@ on the linker search path). +set(@ENV@ "$ENV{@ENV@}" CACHE FILEPATH "Path to the @LIBRARY@ native library") + +add_library(@LIBRARY@_cpp INTERFACE) +add_library(@LIBRARY@::cpp ALIAS @LIBRARY@_cpp) +target_include_directories(@LIBRARY@_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_features(@LIBRARY@_cpp INTERFACE cxx_std_@STD@) +if(@ENV@) + target_link_libraries(@LIBRARY@_cpp INTERFACE "${@ENV@}") +else() + target_link_libraries(@LIBRARY@_cpp INTERFACE @LIBRARY@) +endif() +"#; + format!( + "{}{}\n{}", + render_prelude(CommentStyle::Hash, names.input_basename), + names.fill(body), + render_trailer(CommentStyle::Hash, "CMakeLists.txt") + ) +} + +/// Render the packaged `CMakeLists.txt`, which imports the bundled library +/// for the host platform as `{library}` and links it into the +/// `{library}_cpp` INTERFACE library. `lib` is the bundled binaries' base +/// file name. +pub(crate) fn render_packaged_cmake(names: &CmakeNames<'_>, lib: &str) -> String { + let body = r#"cmake_minimum_required(VERSION 3.14) +project(@LIBRARY@_cpp VERSION @VERSION@ LANGUAGES CXX) + +# Select the prebuilt native library bundled for the host platform/arch. +if(APPLE) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64") + set(_plat "darwin-arm64") + else() + set(_plat "darwin-x64") + endif() + set(_libfile "lib@LIB@.dylib") +elseif(WIN32) + set(_plat "windows-x64") + set(_libfile "@LIB@.dll") +else() + if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(_plat "linux-arm64") + else() + set(_plat "linux-x64") + endif() + set(_libfile "lib@LIB@.so") +endif() + +if(NOT TARGET @LIBRARY@) + add_library(@LIBRARY@ SHARED IMPORTED GLOBAL) + set_target_properties(@LIBRARY@ PROPERTIES + IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/lib/${_plat}/${_libfile}") +endif() + +add_library(@LIBRARY@_cpp INTERFACE) +add_library(@LIBRARY@::cpp ALIAS @LIBRARY@_cpp) +target_include_directories(@LIBRARY@_cpp INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) +target_link_libraries(@LIBRARY@_cpp INTERFACE @LIBRARY@) +target_compile_features(@LIBRARY@_cpp INTERFACE cxx_std_@STD@) +"#; + format!( + "{}{}\n{}", + render_prelude(CommentStyle::Hash, names.input_basename), + names.fill(body).replace("@LIB@", lib), + render_trailer(CommentStyle::Hash, "CMakeLists.txt") + ) +} + +/// README for a packaged C++ artifact bundling the headers and the desktop +/// per-platform libraries the packaged CMake can select among. +pub(crate) fn render_packaged_readme(names: &CmakeNames<'_>, ctx: &PackageContext) -> String { + let platforms: Vec = ctx + .binaries + .platforms() + .filter(|p| p.is_desktop()) + .map(|p| format!("- `lib/{}/`", p.id())) + .collect(); + let body = r#"# @LIBRARY@ (C++) + +A header-only C++@STD@ wrapper (`include/@HEADER@`), the C header it +includes, and a prebuilt shared library for each supported platform under +`lib//`. + +## Use with CMake + +```cmake +add_subdirectory(path/to/cpp) +target_link_libraries(your_app PRIVATE @LIBRARY@::cpp) +``` + +`CMakeLists.txt` selects the right library for the host platform and links +it into the `@LIBRARY@::cpp` interface target. + +## Bundled platforms + +"#; + format!( + "{}{}{}\n\n{}", + render_prelude(CommentStyle::Xml, names.input_basename), + names.fill(body), + platforms.join("\n"), + render_trailer(CommentStyle::Xml, "README.md") + ) +} + +/// README for the source layout: prerequisites, CMake usage, and the +/// library-path override. +pub(crate) fn render_readme(names: &CmakeNames<'_>) -> String { + let body = r#"# @LIBRARY@ C++ bindings + +A header-only C++@STD@ wrapper over the `@LIBRARY@` C ABI. `@HEADER@` +includes the C header beside it; every declaration lives in +`namespace @NAMESPACE@`. + +## Prerequisites + +- CMake 3.14 or later +- A C++@STD@ compiler +- The `@LIBRARY@` native library (`lib@LIBRARY@.so`, `lib@LIBRARY@.dylib`, or + `@LIBRARY@.dll`) + +## Use with CMake + +```cmake +add_subdirectory(path/to/cpp) +add_executable(app main.cpp) +target_link_libraries(app PRIVATE @LIBRARY@::cpp) +``` + +`@LIBRARY@::cpp` adds this directory to the include path, requires +C++@STD@, and links the native library: the file named by the `@ENV@` +CMake cache variable or environment variable when set, or else +`@LIBRARY@` by name. + +```cpp +#include "@HEADER@" +``` + +The first free function, constructor, or static member you call checks +that the linked library matches these headers and throws +`@NAMESPACE@::LoadError` if it doesn't; call `@NAMESPACE@::check_library()` +at startup to check eagerly. + +"#; + format!( + "{}{}{}", + render_prelude(CommentStyle::Xml, names.input_basename), + names.fill(body), + render_trailer(CommentStyle::Xml, "README.md") + ) +} diff --git a/crates/weaveffi-gen/src/targets/cpp/runtime.rs b/crates/weaveffi-gen/src/targets/cpp/runtime.rs new file mode 100644 index 00000000..5967ac96 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/cpp/runtime.rs @@ -0,0 +1,46 @@ +//! The fixed runtime of the generated header: the error types, the adopt +//! tag, the cancel token, the string helpers, the library check, and the +//! value-buffer reader and writer. +//! +//! The code lives in real C++ sources under `runtime/`, spliced into the +//! header with `{{PLACEHOLDER}}` substitution. + +use weaveffi_model::model::{checksum_symbol, BindingModel}; +use weaveffi_model::pkg::Identity; + +/// The error types, adopt tag, cancel token, string helpers, and +/// `check_library()`. +const PRELUDE: &str = include_str!("runtime/prelude.hpp"); + +/// The value-buffer reader, writer, and release guard. +const BUFFER: &str = include_str!("runtime/buffer.hpp"); + +/// Render the runtime prelude for `model`, the binding model of the library +/// `identity` describes. `check_library()` compares the producer's ABI revision and +/// every top-level module's contract checksum against the values this header +/// was generated with. +pub(crate) fn render_prelude_runtime(model: &BindingModel, identity: &Identity) -> String { + let prefix = &model.prefix; + let library = &identity.library; + let mut checks = String::new(); + for root in model.roots() { + let Some(sum) = root.checksum else { continue }; + let symbol = checksum_symbol(prefix, &root.path); + checks.push_str(&format!( + " if ({symbol}() != {sum:#018x}ull) {{\n \ + return std::string(\"{library}: module '{name}' does not match the linked library \ + (contract checksum mismatch); regenerate the bindings for this build\");\n }}\n", + name = root.dot_path, + )); + } + PRELUDE + .replace("{{PREFIX}}", prefix) + .replace("{{MACRO}}", &identity.macro_prefix()) + .replace("{{LIBRARY}}", library) + .replace("{{CHECKSUMS}}", &checks) +} + +/// Render the value-buffer runtime. +pub(crate) fn render_buffer_runtime(prefix: &str) -> String { + BUFFER.replace("{{PREFIX}}", prefix) +} diff --git a/crates/weaveffi-gen/src/targets/cpp/runtime/buffer.hpp b/crates/weaveffi-gen/src/targets/cpp/runtime/buffer.hpp new file mode 100644 index 00000000..f72d7698 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/cpp/runtime/buffer.hpp @@ -0,0 +1,192 @@ +namespace detail { + +/** + * Serializes values into the value-buffer wire format: little-endian, packed + * with no alignment, lengths and element counts as u32. + */ +class BufferWriter { + std::vector buf_; + + template + void append_le(T v) { + for (size_t i = 0; i < sizeof(T); ++i) { + buf_.push_back(static_cast(v >> (8 * i))); + } + } + +public: + /** Pointer to the encoded bytes. */ + const uint8_t* data() const noexcept { return buf_.data(); } + + /** Number of encoded bytes. */ + size_t size() const noexcept { return buf_.size(); } + + void write_bool(bool v) { buf_.push_back(v ? 1 : 0); } + void write_i8(int8_t v) { buf_.push_back(static_cast(v)); } + void write_u8(uint8_t v) { buf_.push_back(v); } + void write_i16(int16_t v) { append_le(static_cast(v)); } + void write_u16(uint16_t v) { append_le(v); } + void write_i32(int32_t v) { append_le(static_cast(v)); } + void write_u32(uint32_t v) { append_le(v); } + void write_i64(int64_t v) { append_le(static_cast(v)); } + void write_u64(uint64_t v) { append_le(v); } + + void write_f32(float v) { + uint32_t bits = 0; + std::memcpy(&bits, &v, sizeof(bits)); + append_le(bits); + } + + void write_f64(double v) { + uint64_t bits = 0; + std::memcpy(&bits, &v, sizeof(bits)); + append_le(bits); + } + + /** Writes a byte length or an element count as a u32. */ + void write_len(size_t n) { + if (static_cast(n) > UINT32_MAX) throw Error(-3, "value buffer length exceeds u32"); + append_le(static_cast(n)); + } + + void write_string(std::string_view v) { + write_len(v.size()); + buf_.insert(buf_.end(), v.begin(), v.end()); + } + + void write_bytes(const std::vector& v) { + write_len(v.size()); + buf_.insert(buf_.end(), v.begin(), v.end()); + } + + /** Writes an optional's presence flag: 0 absent, 1 present. */ + void write_option_flag(bool present) { buf_.push_back(present ? 1 : 0); } +}; + +/** + * Decodes values from the value-buffer wire format. A malformed buffer is a + * producer/consumer contract violation (both sides are generated from one + * API), so every decode failure throws the generic Error with the + * marshalling code -3. Object tokens adopted before a failure live in RAII + * wrappers that release them on unwind. + */ +class BufferReader { + const uint8_t* data_; + size_t len_; + size_t pos_; + + [[noreturn]] static void fail(const char* what) { + throw Error(-3, std::string("malformed 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) noexcept : data_(data), len_(data != nullptr ? len : 0), pos_(0) {} + + /** Bytes not yet consumed. */ + size_t remaining() const noexcept { return len_ - pos_; } + + bool read_bool() { + uint8_t b = read_le("bool"); + if (b > 1) fail("bool byte out of range"); + return b != 0; + } + + int8_t read_i8() { return read_le("i8"); } + uint8_t read_u8() { return read_le("u8"); } + int16_t read_i16() { return read_le("i16"); } + uint16_t read_u16() { return read_le("u16"); } + int32_t read_i32() { return read_le("i32"); } + uint32_t read_u32() { return read_le("u32"); } + int64_t read_i64() { return read_le("i64"); } + uint64_t read_u64() { return read_le("u64"); } + + float read_f32() { + uint32_t bits = read_le("f32"); + float v = 0; + std::memcpy(&v, &bits, sizeof(v)); + return v; + } + + double read_f64() { + uint64_t bits = read_le("f64"); + double v = 0; + std::memcpy(&v, &bits, sizeof(v)); + return v; + } + + /** Reads a byte length, rejecting one larger than the bytes remaining. */ + size_t read_len() { + uint32_t n = read_le("length"); + if (static_cast(n) > remaining()) fail("length prefix exceeds remaining buffer"); + return static_cast(n); + } + + /** + * Reads an element count. Elements may encode to zero bytes, so a count + * larger than the bytes remaining is legal; callers cap preallocation + * with reserve_hint(). + */ + size_t read_count() { return static_cast(read_le("count")); } + + /** A preallocation size for `n` elements that a malformed count can't inflate. */ + size_t reserve_hint(size_t n) const noexcept { return n < remaining() ? n : remaining(); } + + std::string read_string() { + size_t n = read_len(); + std::string v(reinterpret_cast(data_) + pos_, n); + pos_ += n; + return v; + } + + std::vector read_bytes() { + size_t n = read_len(); + std::vector v(data_ + pos_, data_ + pos_ + n); + pos_ += n; + return v; + } + + bool read_option_flag() { + uint8_t b = read_le("option flag"); + if (b > 1) fail("option flag byte out of range"); + return b != 0; + } + + /** 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 {{PREFIX}}_free_bytes on scope exit. */ +struct BufferGuard { + /** The producer-allocated buffer, or null when the call reported an error. */ + const uint8_t* ptr; + /** The buffer length in bytes. */ + size_t len; + + BufferGuard(const uint8_t* p, size_t n) noexcept : ptr(p), len(n) {} + BufferGuard(const BufferGuard&) = delete; + BufferGuard& operator=(const BufferGuard&) = delete; + + ~BufferGuard() { + if (ptr != nullptr) {{PREFIX}}_free_bytes(const_cast(ptr), len); + } +}; + +} // namespace detail + diff --git a/crates/weaveffi-gen/src/targets/cpp/runtime/prelude.hpp b/crates/weaveffi-gen/src/targets/cpp/runtime/prelude.hpp new file mode 100644 index 00000000..49b707e7 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/cpp/runtime/prelude.hpp @@ -0,0 +1,190 @@ +/** + * Base exception for every error reported through the C ABI. + * + * Positive codes are the declared error codes (see the typed domain + * exceptions); negative codes are runtime traps: -1 a generic producer + * error, -2 a producer panic, -3 a marshalling failure, -4 a + * callback-interface implementation that threw, and -5 a cancelled call + * (thrown as Cancelled). + */ +class Error : public std::runtime_error { + int32_t code_; + +public: + /** Builds an error carrying `code` and `message`. */ + Error(int32_t code, const std::string& message) : std::runtime_error(message), code_(code) {} + + /** The numeric error code. */ + int32_t code() const noexcept { return code_; } +}; + +/** Thrown by an async call whose CancelToken was cancelled (code -5). */ +class Cancelled : public Error { +public: + /** Builds a cancellation error carrying the producer's message. */ + explicit Cancelled(const std::string& message) : Error(-5, message) {} +}; + +/** + * Thrown on first use when the linked {{LIBRARY}} library was built for a + * different C ABI revision or a different API contract than this header. + */ +class LoadError : public Error { +public: + /** Builds a load error describing the mismatch. */ + explicit LoadError(const std::string& message) : Error(-1, message) {} +}; + +/** + * Tag selecting the constructor that adopts a raw C pointer (one strong + * reference the wrapper then owns), in the style of std::adopt_lock. + */ +struct adopt_t { + explicit adopt_t() = default; +}; + +/** The adopt_t tag value: `Gadget g(adopt, raw)`. */ +inline constexpr adopt_t adopt{}; + +/** + * A cancellation token for cancellable async functions. + * + * Create one, pass it to any number of cancellable calls, and call cancel() + * from any thread. A cancelled call settles its future with Cancelled. Each + * call takes its own reference to the native token, so the token can be + * destroyed at any time; the destructor releases this wrapper's reference. + */ +class CancelToken { + {{PREFIX}}_cancel_token* handle_; + + struct none_t {}; + explicit CancelToken(none_t) noexcept : handle_(nullptr) {} + +public: + /** Creates a new, uncancelled token. */ + CancelToken() : handle_({{PREFIX}}_cancel_token_create()) { + if (handle_ == nullptr) throw std::bad_alloc(); + } + + /** Releases this wrapper's reference to the native token. */ + ~CancelToken() { + if (handle_ != nullptr) {{PREFIX}}_cancel_token_destroy(handle_); + } + + CancelToken(const CancelToken&) = delete; + CancelToken& operator=(const CancelToken&) = delete; + + /** Transfers the native token; `other` becomes empty. */ + CancelToken(CancelToken&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + + /** Releases the current token and takes over `other`'s. */ + CancelToken& operator=(CancelToken&& other) noexcept { + if (this != &other) { + if (handle_ != nullptr) {{PREFIX}}_cancel_token_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /** The empty token a cancellable call uses when none is passed. */ + static const CancelToken& none() noexcept { + static const CancelToken token{none_t{}}; + return token; + } + + /** Requests cancellation of every call this token was passed to. Idempotent and thread-safe. */ + void cancel() const noexcept { + if (handle_ != nullptr) {{PREFIX}}_cancel_token_cancel(handle_); + } + + /** Whether cancel() has been called. */ + bool is_cancelled() const noexcept { + return handle_ != nullptr && {{PREFIX}}_cancel_token_is_cancelled(handle_); + } + + /** The native token, borrowed (null for an empty token). */ + {{PREFIX}}_cancel_token* handle() const noexcept { return handle_; } +}; + +namespace detail { + +/** The exception for a runtime (non-domain) error code. */ +inline std::exception_ptr make_error(int32_t code, const std::string& message) { + if (code == -5) return std::make_exception_ptr(Cancelled(message)); + return std::make_exception_ptr(Error(code, message)); +} + +/** The message of a nonzero error slot. */ +inline std::string error_message(const {{PREFIX}}_error& err) { + return std::string(err.message != nullptr ? err.message : "unknown error"); +} + +/** Throws the generic Error (or Cancelled) if `err` carries a nonzero code. */ +inline void check({{PREFIX}}_error& err) { + if (err.code == 0) return; + std::exception_ptr ex = make_error(err.code, error_message(err)); + {{PREFIX}}_error_clear(&err); + std::rethrow_exception(ex); +} + +/** Copies a producer-returned UTF-8 run into a string, then releases it. */ +inline std::string take_string(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::string(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { {{PREFIX}}_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::string(reinterpret_cast(ptr), len); +} + +/** Copies a producer-returned byte run into a vector, then releases it. */ +inline std::vector take_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr) return std::vector(); + struct Release { + const uint8_t* ptr; + size_t len; + ~Release() { {{PREFIX}}_free_bytes(const_cast(ptr), len); } + } release{ptr, len}; + return std::vector(ptr, ptr + len); +} + +/** Views a borrowed UTF-8 run (null when empty) as a string_view. */ +inline std::string_view borrow_string(const uint8_t* ptr, size_t len) noexcept { + if (ptr == nullptr || len == 0) return std::string_view(); + return std::string_view(reinterpret_cast(ptr), len); +} + +/** Views a borrowed byte run (null when empty) as an owned vector. */ +inline std::vector borrow_bytes(const uint8_t* ptr, size_t len) { + if (ptr == nullptr || len == 0) return std::vector(); + return std::vector(ptr, ptr + len); +} + +/** The data pointer of a string argument, as the C ABI expects it. */ +inline const uint8_t* utf8(std::string_view s) noexcept { + return reinterpret_cast(s.data()); +} + +} // namespace detail + +/** + * Verifies that the linked {{LIBRARY}} library implements the C ABI revision + * and the module contracts this header was generated against, throwing + * LoadError naming the first mismatch. The check runs once; every free + * function, constructor, and static member calls this before its first + * native call, so calling it at startup is optional. + */ +inline void check_library() { + static const std::string failure = [] { + uint32_t abi = {{PREFIX}}_abi_version(); + if (abi != {{MACRO}}_ABI_VERSION) { + return "{{LIBRARY}}: C ABI revision mismatch (header " + std::to_string({{MACRO}}_ABI_VERSION) + + ", library " + std::to_string(abi) + ")"; + } +{{CHECKSUMS}} return std::string(); + }(); + if (!failure.empty()) throw LoadError(failure); +} + diff --git a/crates/weaveffi-gen-cpp/src/tests.rs b/crates/weaveffi-gen/src/targets/cpp/tests.rs similarity index 67% rename from crates/weaveffi-gen-cpp/src/tests.rs rename to crates/weaveffi-gen/src/targets/cpp/tests.rs index c330396a..add27499 100644 --- a/crates/weaveffi-gen-cpp/src/tests.rs +++ b/crates/weaveffi-gen/src/targets/cpp/tests.rs @@ -1,14 +1,18 @@ -//! Unit tests: render a small API that exercises every ABI revision 2 shape -//! and assert the header carries the key pieces of each contract. +//! Unit tests: render a small API that exercises every C ABI revision 3 +//! shape and assert the header carries the key pieces of each contract. -use weaveffi_core::model::{BindingModel, CallShape}; -use weaveffi_core::validate::validate_api; -use weaveffi_ir::ir::{ +use camino::Utf8Path; +use weaveffi_model::ir::{ Api, CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, Module, Param, StructDef, StructField, TypeRef, }; +use weaveffi_model::model::{BindingModel, CallShape}; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; +use weaveffi_model::validate::validate_api; -use crate::render_cpp_header; +use crate::backend::LanguageBackend; +use crate::targets::cpp::{render_cpp_header, CppConfig, CppGenerator}; fn param(name: &str, ty: TypeRef) -> Param { Param { @@ -110,7 +114,7 @@ fn fixture() -> Api { ], }; Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), modules: vec![Module { name: "kv".into(), doc: None, @@ -183,13 +187,26 @@ fn fixture() -> Api { } } -fn render() -> (BindingModel, String) { - let api = validate_api(fixture(), None).expect("fixture validates"); - let model = BindingModel::build(&api, "weaveffi"); - let header = render_cpp_header(&model, "weaveffi", "kv.yml", "weaveffi.hpp"); +/// Render the wrapper header for `api` with the default names (namespace and +/// files named after the identity, `api` for an API built in memory). +fn render_api(api: &ResolvedApi) -> (BindingModel, String) { + let model = BindingModel::build(api); + let id = api.identity(); + let header = render_cpp_header( + &model, + id, + &id.prefix, + &format!("{}.h", id.library), + "kv.yml", + &format!("{}.hpp", id.library), + ); (model, header) } +fn render() -> (BindingModel, String) { + render_api(&validate_api(fixture(), None).expect("fixture validates")) +} + fn assert_contains(header: &str, needle: &str) { assert!( header.contains(needle), @@ -211,46 +228,50 @@ fn assert_before(header: &str, first: &str, second: &str) { fn interface_wrapper_is_reference_counted_raii() { let (_, h) = render(); assert_contains(&h, "class Store {"); - assert_contains(&h, "weaveffi_kv_Store* handle_;"); - assert_contains(&h, "explicit Store(weaveffi_kv_Store* h) : handle_(h) {}"); - assert_contains(&h, "if (handle_) weaveffi_kv_Store_destroy(handle_);"); + assert_contains(&h, "api_kv_Store* handle_;"); + assert_contains(&h, "using raw_type = api_kv_Store;"); assert_contains( &h, - "Store(const Store& other) : handle_(weaveffi_kv_Store_clone(other.handle_)) {}", + "explicit Store(adopt_t, api_kv_Store* h) noexcept : handle_(h) {}", ); + assert_contains(&h, "if (handle_) api_kv_Store_destroy(handle_);"); assert_contains( &h, - "Store(Store&& other) noexcept : handle_(other.handle_) {", + "Store(const Store& other) : handle_(api_kv_Store_clone(other.handle_)) {}", ); assert_contains( &h, - "const weaveffi_kv_Store* handle() const { return handle_; }", + "Store(Store&& other) noexcept : handle_(other.handle_) {", ); + assert_contains(&h, "const api_kv_Store* handle() const { return handle_; }"); assert_contains( &h, - "weaveffi_kv_Store* clone_handle() const { return weaveffi_kv_Store_clone(handle_); }", + "api_kv_Store* clone_handle() const { return api_kv_Store_clone(handle_); }", ); // Constructor adopts the returned reference; methods borrow `handle_`. - assert_contains(&h, " Store(const std::string& path);"); + assert_contains(&h, " explicit Store(std::string_view path);"); assert_contains( &h, - "inline Store::Store(const std::string& path) : handle_(nullptr) {", + "inline Store::Store(std::string_view path) : handle_(nullptr) {\n check_library();", ); assert_contains( &h, - "auto result = weaveffi_kv_Store_new(path.c_str(), &err);", + "auto result = api_kv_Store_new(detail::utf8(path), path.size(), &err);", ); assert_contains(&h, "handle_ = result;"); assert_contains( &h, - "inline std::optional Store::get(const std::string& key) const {", + "inline std::optional Store::get(std::string_view key) const {\n size_t out_len = 0;", ); assert_contains( &h, - "weaveffi_kv_Store_get(handle_, key.c_str(), &out_len, &err);", + "api_kv_Store_get(handle_, detail::utf8(key), key.size(), &out_len, &err);", ); assert_contains(&h, "static Store open_default();"); - assert_contains(&h, "inline Store Store::open_default() {"); + assert_contains( + &h, + "inline Store Store::open_default() {\n check_library();", + ); // The throwing method routes through the typed domain check. assert_contains( &h, @@ -259,7 +280,7 @@ fn interface_wrapper_is_reference_counted_raii() { assert_contains(&h, "detail::check_kv(err);"); // Async object results are adopted. assert_contains(&h, "inline std::future Store::fetch() const {"); - assert_contains(&h, "p->set_value(Store(result));"); + assert_contains(&h, "p->set_value(Store(adopt, result));"); } #[test] @@ -267,14 +288,14 @@ fn nullable_objects_map_to_optional() { let (_, h) = render(); assert_contains( &h, - "inline std::optional lookup(const std::optional& store) {", + "inline std::optional lookup(const std::optional& store) {\n check_library();", ); assert_contains( &h, - "weaveffi_kv_lookup(store.has_value() ? store->handle() : nullptr, &err);", + "api_kv_lookup(store.has_value() ? store->handle() : nullptr, &err);", ); assert_contains(&h, "if (!result) return std::nullopt;"); - assert_contains(&h, "return Store(result);"); + assert_contains(&h, "return Store(adopt, result);"); assert_contains(&h, "std::optional sibling() const;"); } @@ -300,7 +321,7 @@ fn records_hold_objects_as_cloned_tokens() { ); assert_contains( &h, - "Store f_store = Store(reinterpret_cast(static_cast(r.read_u64())));", + "Store f_store = Store(adopt, reinterpret_cast(static_cast(r.read_u64())));", ); assert_contains( &h, @@ -323,7 +344,7 @@ fn iterators_over_objects_adopt_each_element() { assert_contains(&h, &format!("{}* handle_;", it.iter_tag)); assert_contains(&h, &format!("if (handle_) {}(handle_);", it.destroy_symbol)); assert_contains(&h, "std::optional next() {"); - assert_contains(&h, "return Store(item);"); + assert_contains(&h, "return Store(adopt, item);"); assert_contains(&h, "StoreScanIterator scan() const;"); assert_contains(&h, "inline StoreScanIterator Store::scan() const {"); assert_contains(&h, "class AllStoresIterator {"); @@ -342,7 +363,7 @@ fn callback_interface_renders_abstract_class_vtable_and_trampolines() { assert_contains(&h, "virtual ~Listener() = default;"); assert_contains( &h, - "virtual bool on_message(const std::string& text, int32_t weight, const Contact& contact, Store store) = 0;", + "virtual bool on_message(std::string_view text, int32_t weight, const Contact& contact, Store store) = 0;", ); assert_contains(&h, "virtual void on_reset(std::optional alt) = 0;"); assert_contains(&h, "virtual Color pick(Color color) = 0;"); @@ -351,27 +372,30 @@ fn callback_interface_renders_abstract_class_vtable_and_trampolines() { assert_contains(&h, "struct Listener_trampolines {"); assert_contains( &h, - "static bool on_message(void* ctx, const char* text, int32_t weight, const uint8_t* contact_ptr, size_t contact_len, weaveffi_kv_Store* store, weaveffi_error* out_err) {", + "static bool on_message(void* ctx, const uint8_t* text_ptr, size_t text_len, int32_t weight, const uint8_t* contact_ptr, size_t contact_len, api_kv_Store* store, api_error* out_err) {", ); assert_contains( &h, - "static void on_reset(void* ctx, weaveffi_kv_Store* alt, weaveffi_error* out_err) {", + "static void on_reset(void* ctx, api_kv_Store* alt, api_error* out_err) {", ); assert_contains( &h, - "static weaveffi_kv_Color pick(void* ctx, weaveffi_kv_Color color, weaveffi_error* out_err) {", + "static api_kv_Color pick(void* ctx, api_kv_Color color, api_error* out_err) {", ); // Objects are adopted before anything that can throw; buffers and // strings are decoded (borrowed, never freed). - assert_contains(&h, "Store store_val(store);"); + assert_contains(&h, "Store store_val(adopt, store);"); assert_before( &h, - "Store store_val(store);", + "Store store_val(adopt, store);", "Listener& impl = **static_cast*>(ctx);", ); assert_contains(&h, "std::optional alt_val;"); - assert_contains(&h, "if (alt) alt_val.emplace(alt);"); - assert_contains(&h, "std::string text_val(text ? text : \"\");"); + assert_contains(&h, "if (alt) alt_val.emplace(adopt, alt);"); + assert_contains( + &h, + "std::string_view text_val = detail::borrow_string(text_ptr, text_len);", + ); assert_contains( &h, "detail::BufferReader contact_r(contact_ptr, contact_len);", @@ -384,27 +408,27 @@ fn callback_interface_renders_abstract_class_vtable_and_trampolines() { assert_contains(&h, "impl.on_reset(std::move(alt_val));"); assert_contains( &h, - "return static_cast(static_cast(impl.pick(static_cast(static_cast(color)))));", + "return static_cast(static_cast(impl.pick(static_cast(static_cast(color)))));", ); // Failure path: foreign error code -4, default return, no unwinding. assert_contains(&h, "} catch (const std::exception& e) {"); - assert_contains(&h, "weaveffi_error_set(out_err, -4, e.what());"); + assert_contains(&h, "api_error_set(out_err, -4, e.what());"); assert_contains(&h, "return bool{};"); - assert_contains(&h, "return weaveffi_kv_Color{};"); - assert!(!h.contains("weaveffi_free_bytes(const_cast(contact_ptr)")); + assert_contains(&h, "return api_kv_Color{};"); + assert!(!h.contains("api_free_bytes(const_cast(contact_ptr)")); // Exactly one process-wide vtable, methods in declaration order then free. assert_eq!( - h.matches("static const weaveffi_kv_Listener_vtable vtable = {") + h.matches("static const api_kv_Listener_vtable vtable = {") .count(), 1 ); assert_contains( &h, - "inline const weaveffi_kv_Listener_vtable& Listener_vtable() {", + "inline const api_kv_Listener_vtable& Listener_vtable() {", ); let vtable_start = h - .find("static const weaveffi_kv_Listener_vtable vtable = {") + .find("static const api_kv_Listener_vtable vtable = {") .unwrap(); let vtable = &h[vtable_start..]; let order: Vec = [ @@ -437,47 +461,125 @@ fn passing_a_callback_interface_boxes_the_shared_ptr() { ); assert_contains( &h, - "auto* listener_ctx = new std::shared_ptr(std::move(listener));", + "auto listener_ctx = std::make_unique>(std::move(listener));", ); assert_contains( &h, - "weaveffi_kv_subscribe(static_cast(listener_ctx), &detail::Listener_vtable(), &err);", + "api_kv_subscribe(static_cast(listener_ctx.release()), &detail::Listener_vtable(), &err);", ); } #[test] -fn runtime_surface_matches_abi_revision_2() { - let (_, h) = render(); - assert_contains(&h, "#define WEAVEFFI_ABI_VERSION 2u"); - assert_contains(&h, "inline void check_abi_version() {"); +fn runtime_surface_matches_abi_revision_3() { + let (model, h) = render(); + // The C ABI comes from the shipped C header, never re-declared. + assert_contains(&h, "#include \"api.h\""); + assert!( + !h.contains("extern \"C\""), + "C declarations are not inlined" + ); + assert_contains(&h, "namespace api {"); + // The library check covers the ABI revision and every root checksum. + let sum = model.modules[0].checksum.expect("roots carry a checksum"); + assert_contains(&h, "inline void check_library() {"); + assert_contains(&h, "if (abi != API_ABI_VERSION) {"); + assert_contains(&h, &format!("if (api_kv_checksum() != {sum:#018x}ull) {{")); + assert_contains(&h, "module 'kv' does not match the linked library"); + assert_contains(&h, "if (!failure.empty()) throw LoadError(failure);"); + // Errors: the base, cancellation, and load failures. + assert_contains(&h, "class Error : public std::runtime_error {"); + assert_contains(&h, "class Cancelled : public Error {"); + assert_contains(&h, "class LoadError : public Error {"); assert_contains( &h, - "WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message);", + "if (code == -5) return std::make_exception_ptr(Cancelled(message));", ); + // Negative codes, including -4 and -5, route to the runtime exceptions. + assert_contains(&h, "if (code < 0) return make_error(code, msg);"); + // Returned strings are copied by length and released with free_bytes. assert_contains( &h, - "WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_clone(const weaveffi_kv_Store* self);", + "~Release() { api_free_bytes(const_cast(ptr), len); }", ); + assert_contains( + &h, + "return std::string(reinterpret_cast(ptr), len);", + ); + assert_contains(&h, "class CancelToken {"); + assert_contains(&h, "CancelToken() : handle_(api_cancel_token_create()) {"); for legacy in [ + "free_string", + "c_str()", + "WeaveFFI", + "weaveffi", + "check_abi_version", "arena", "handle_t", - "register_", - "unregister_", "std::function", ] { - assert!(!h.contains(legacy), "legacy token {legacy:?} in header"); + let body = h.split_once("#pragma once").expect("prelude").1; + assert!(!body.contains(legacy), "legacy token {legacy:?} in header"); } - // Negative codes, including -4, route to the generic error. +} + +#[test] +fn identity_names_every_file_and_config_overrides_win() { + let api = validate_api(fixture(), None) + .expect("fixture validates") + .with_identity(Identity::named("kv-store")); + let model = BindingModel::build(&api); + let out = Utf8Path::new("/out"); + let files = CppGenerator.files(&api, &model, out, &CppConfig::default()); + let paths: Vec = files.iter().map(|f| f.path.to_string()).collect(); + assert_eq!( + paths, + [ + "/out/cpp/kv_store.h", + "/out/cpp/kv_store.hpp", + "/out/cpp/CMakeLists.txt", + "/out/cpp/README.md", + ] + ); + let hpp = &files[1].contents; + assert_contains(hpp, "#include \"kv_store.h\""); + assert_contains(hpp, "namespace kv_store {"); + assert_contains(hpp, "kv_store: C ABI revision mismatch"); + let cmake = &files[2].contents; + assert_contains(cmake, "add_library(kv_store_cpp INTERFACE)"); + assert_contains(cmake, "add_library(kv_store::cpp ALIAS kv_store_cpp)"); assert_contains( - &h, - "if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg));", + cmake, + "set(KV_STORE_LIBRARY \"$ENV{KV_STORE_LIBRARY}\" CACHE FILEPATH", + ); + assert_contains( + cmake, + "target_link_libraries(kv_store_cpp INTERFACE kv_store)", ); + + let config = CppConfig { + namespace: Some("acme::kv".into()), + header_name: Some("acme_kv.hpp".into()), + ..CppConfig::default() + }; + let files = CppGenerator.files(&api, &model, out, &config); + assert_eq!(files[1].path, "/out/cpp/acme_kv.hpp"); + assert_contains(&files[1].contents, "namespace acme::kv {"); + assert_contains(&files[1].contents, "#include \"kv_store.h\""); +} + +#[test] +fn constructors_and_adoption_are_explicit() { + let (_, h) = render(); + // No implicit conversion from arguments or raw pointers into an object. + assert!(!h.contains("\n Store(std::string_view path);")); + assert!(!h.contains("Store(api_kv_Store* h)")); + assert_contains(&h, "inline constexpr adopt_t adopt{};"); } #[test] fn extended_shapes_render() { let api = Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), modules: vec![Module { name: "ext".into(), doc: None, @@ -589,14 +691,12 @@ fn extended_shapes_render() { modules: vec![], }], }; - let api = validate_api(api, None).expect("fixture validates"); - let model = BindingModel::build(&api, "weaveffi"); - let h = render_cpp_header(&model, "weaveffi", "ext.yml", "weaveffi.hpp"); + let (_, h) = render_api(&validate_api(api, None).expect("fixture validates")); // Nullable iterator elements, object payloads in rich enums and error // fields, maps of objects, and a cancellable async bytes result. assert_contains(&h, "std::optional> next() {"); - assert_contains(&h, "if (item) value.emplace(item);"); + assert_contains(&h, "if (item) value.emplace(adopt, item);"); assert_contains( &h, "return Shape{Shape::Boxed{std::move(f_store), std::move(f_tags)}};", @@ -615,8 +715,21 @@ fn extended_shapes_render() { ); assert_contains( &h, - "inline std::future> slow(int64_t n, weaveffi_cancel_token* cancel_token) {", + "inline std::future> slow(int64_t n, const CancelToken& cancel_token = CancelToken::none()) {", + ); + assert_contains( + &h, + "api_ext_slow(n, cancel_token.handle(), [](void* context, api_error* err, const uint8_t* result_ptr, size_t result_len) {", + ); + assert_contains( + &h, + "std::unique_ptr>> p(static_cast>*>(context));", + ); + assert_contains( + &h, + "p->set_value(detail::take_bytes(result_ptr, result_len));", ); + assert_contains(&h, "}, static_cast(promise.release()));"); assert_contains( &h, "p->set_exception(detail::make_ext_error(err->code, msg, err->payload_ptr, err->payload_len));", @@ -628,7 +741,7 @@ fn extended_shapes_render() { ); assert_contains( &h, - "std::vector blob_val(blob_ptr, blob_ptr + blob_len);", + "std::vector blob_val = detail::borrow_bytes(blob_ptr, blob_len);", ); assert_contains(&h, "Shape shape_val = detail::read_Shape(shape_r);"); assert_contains(&h, "return int64_t{};"); diff --git a/crates/weaveffi-gen-cpp/src/types.rs b/crates/weaveffi-gen/src/targets/cpp/types.rs similarity index 86% rename from crates/weaveffi-gen-cpp/src/types.rs rename to crates/weaveffi-gen/src/targets/cpp/types.rs index 591997a3..27d89c34 100644 --- a/crates/weaveffi-gen-cpp/src/types.rs +++ b/crates/weaveffi-gen/src/targets/cpp/types.rs @@ -1,14 +1,14 @@ //! C++ spellings: identifier escaping, name casing, namespace paths, and the //! IR-to-C++ type mapping. +use crate::cabi::c_param_name; +use crate::lang::{self, CPP_KEYWORDS}; +use crate::utils::local_type_name; use heck::ToSnakeCase; -use weaveffi_core::abi::AbiParam; -use weaveffi_core::cabi::c_param_name; -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_model::abi::AbiParam; +use weaveffi_model::errors; +use weaveffi_model::model::ModuleBinding; +use weaveffi_model::model::Ty; /// Idiomatic C++ exception class name for an error code: PascalCase with a /// single `Error` suffix (`KEY_NOT_FOUND` becomes `KeyNotFoundError`), instead @@ -80,12 +80,6 @@ pub(crate) fn render_param_decls(params: &[AbiParam], prefix: &str) -> Vec String { - c_abi_struct_name(name, module, prefix) -} - /// The `detail` accessor returning the process-wide static vtable for a /// callback interface, named by its local C++ class name. pub(crate) fn vtable_accessor(local_name: &str) -> String { @@ -137,12 +131,14 @@ pub(crate) fn cpp_type(ty: &Ty) -> String { } /// One C++ parameter declaration (` `) for a wrapper signature. -/// Heavier types borrow by const reference; scalars and enums pass by value. -/// A callback interface is taken as a `std::shared_ptr` by value, which the -/// wrapper moves into the heap box it hands the producer as `ctx`. +/// Strings borrow as `std::string_view` (the ABI passes a pointer and a +/// length, so interior NULs survive); heavier types borrow by const +/// reference; scalars and enums pass by value. A callback interface is taken +/// as a `std::shared_ptr` by value, which the wrapper moves into the heap box +/// it hands the producer as `ctx`. pub(crate) fn cpp_param_decl(ty: &Ty, name: &str) -> String { match ty { - Ty::StringUtf8 => format!("const std::string& {name}"), + Ty::StringUtf8 => format!("std::string_view {name}"), Ty::Bytes => format!("const std::vector& {name}"), // Records and rich enums borrow: the wrapper encodes them into a local // buffer, so the value stays with the caller. Interfaces borrow their @@ -158,8 +154,9 @@ pub(crate) fn cpp_param_decl(ty: &Ty, name: &str) -> String { } /// One C++ parameter declaration for a callback-interface method the consumer -/// implements. Borrowed strings, bytes, and buffered values arrive by const -/// reference (the trampoline owns the decoded copy for the call); object +/// implements. Borrowed strings arrive as a `std::string_view` valid for the +/// call; bytes and buffered values arrive by const reference (the trampoline +/// owns the decoded copy for the call); object /// arguments transfer one strong reference, so they arrive by value as the /// RAII wrapper (or `std::optional` of it) the implementation now owns. pub(crate) fn cpp_cb_param_decl(ty: &Ty, name: &str) -> String { diff --git a/crates/weaveffi-gen/src/targets/dart/callbacks.rs b/crates/weaveffi-gen/src/targets/dart/callbacks.rs new file mode 100644 index 00000000..b9f0be7c --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/callbacks.rs @@ -0,0 +1,409 @@ +//! Callback-interface rendering: the abstract class a consumer implements, +//! the `Struct` mirroring the C vtable, one entry per method, the dispatcher +//! that delivers forwarded void methods, and the one process-wide vtable. +//! +//! A method that returns a value is a `NativeCallable.isolateLocal` +//! trampoline: it runs synchronously on the isolate's thread, receives its +//! borrowed arguments in place, adopts object arguments, and reports a thrown +//! exception through `{prefix}_error_set` with the foreign code instead of +//! unwinding. A void method is a `NativeCallable.isolateGroupBound` +//! forwarder: it may run on any thread, copies its arguments into a message +//! for the isolate, and returns at once; the dispatcher then runs the +//! implementation on the event loop. + +use crate::codegen::CodeWriter; +use heck::ToUpperCamelCase; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding, Ty}; +use weaveffi_model::plan::ArgPass; + +use crate::targets::dart::calls::{adopt_expr, decoder}; +use crate::targets::dart::docs::{write_deprecated, write_doc}; +use crate::targets::dart::types::{ + dart_class, dart_ident, dart_type, default_literal, ffi_type, ffi_typedef, object_class, +}; + +/// The private global holding a callback interface's vtable. +pub(crate) fn vtable_var(name: &str) -> String { + format!("_{}Vtable", lower_first(&dart_class(name))) +} + +/// The private dispatcher delivering a callback interface's void methods. +pub(crate) fn dispatch_fn(name: &str) -> String { + format!("_{}Dispatch", lower_first(&dart_class(name))) +} + +/// The `Struct` mirroring a callback interface's C vtable. +fn vtable_struct(class: &str) -> String { + format!("_{class}Vtable") +} + +/// The vtable entry function of one method. +fn entry_fn(class: &str, method: &str) -> String { + format!("_{}{}", lower_first(class), method.to_upper_camel_case()) +} + +fn lower_first(s: &str) -> String { + let mut chars = s.chars(); + chars.next().map_or_else(String::new, |c| { + c.to_lowercase().collect::() + chars.as_str() + }) +} + +/// Render one callback interface. +pub(crate) fn render_callback_interface(out: &mut String, cb: &CallbackInterfaceBinding) { + let class = dart_class(&cb.name); + let mut w = CodeWriter::two_space(); + render_abstract_class(&mut w, cb, &class); + render_vtable_struct(&mut w, cb, &class); + for m in &cb.methods { + if m.ret.is_some() { + render_trampoline(&mut w, cb, m, &class); + } else { + render_forwarder(&mut w, cb, m, &class); + } + } + render_dispatch(&mut w, cb, &class); + render_vtable(&mut w, cb, &class); + out.push_str(&w.finish()); +} + +/// The class a consumer extends or implements. +fn render_abstract_class(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &str) { + w.blank(); + write_doc(w, &cb.doc); + if cb.doc.is_some() { + w.line("///"); + } + w.line("/// A callback interface: implement it and pass an instance to any"); + w.line(format!( + "/// function taking a [{class}]. The library keeps the instance alive" + )); + w.line("/// until the producer releases it."); + w.line("///"); + w.line("/// Methods that return a value run synchronously and must be called by"); + w.line("/// the producer on this isolate's thread during a call from Dart; a"); + w.line("/// thrown exception fails that call with [NativeException.foreignCode]."); + w.line("/// Void methods may be called from any thread: they're delivered"); + w.line("/// asynchronously on this isolate's event loop, in the zone that passed"); + w.line("/// the instance, where a thrown exception is an uncaught error of that"); + w.line("/// zone. Object arguments are owned by the implementation."); + write_deprecated(w, &cb.deprecated); + w.block(format!("abstract class {class} {{"), "}", |w| { + for (i, m) in cb.methods.iter().enumerate() { + if i > 0 { + w.blank(); + } + write_doc(w, &m.doc); + write_deprecated(w, &m.deprecated); + let params: Vec = m + .params + .iter() + .map(|p| format!("{} {}", dart_type(&p.ty), dart_ident(&p.name))) + .collect(); + let ret = m.ret.as_ref().map_or("void".to_string(), dart_type); + w.line(format!( + "{ret} {}({});", + dart_ident(&m.name), + params.join(", ") + )); + } + }); +} + +/// The native typedef of one vtable entry. +fn entry_typedef(cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding) -> String { + ffi_typedef(&format!("{}_{}", cb.c_tag, m.name)) +} + +/// The `Struct` whose layout is the C vtable: one function pointer per +/// method in declaration order, then `free`. +fn render_vtable_struct(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &str) { + w.blank(); + for m in &cb.methods { + let natives: Vec = m.abi_params.iter().map(|p| ffi_type(&p.ty).0).collect(); + w.line(format!( + "typedef {} = {} Function({});", + entry_typedef(cb, m), + ffi_type(&m.abi_ret).0, + natives.join(", ") + )); + } + w.blank(); + w.line(format!("// The C `{}` layout.", cb.vtable_tag)); + w.block( + format!("final class {} extends Struct {{", vtable_struct(class)), + "}", + |w| { + for m in &cb.methods { + w.line(format!( + "external Pointer> {};", + entry_typedef(cb, m), + dart_ident(&m.name) + )); + } + w.line("external Pointer)>> free;"); + }, + ); +} + +/// The `(dart type, dart name)` pairs of one vtable entry's parameters. +fn entry_params(m: &CallbackMethodBinding) -> Vec { + m.abi_params + .iter() + .map(|p| format!("{} {}", ffi_type(&p.ty).1, dart_ident(&p.name))) + .collect() +} + +/// A value-returning method's `isolateLocal` trampoline: adopt object +/// arguments first (so a later failure still leaves each reference owned by +/// a finalizable wrapper), receive the borrowed ones in place, call the +/// implementation, and return its value. A thrown exception is reported +/// through `_foreignError` and the zero value is returned. +fn render_trampoline( + w: &mut CodeWriter, + cb: &CallbackInterfaceBinding, + m: &CallbackMethodBinding, + class: &str, +) { + let ret = m.ret.as_ref().expect("value-returning method"); + let (_, dart_ret) = ffi_type(&m.abi_ret); + w.blank(); + w.line(format!("// Vtable entry `{}.{}`.", cb.vtable_tag, m.name)); + w.block( + format!( + "{dart_ret} {}({}) {{", + entry_fn(class, &m.name), + entry_params(m).join(", ") + ), + "}", + |w| { + w.line("try {"); + w.scope(|w| { + let mut args = Vec::new(); + for p in &m.params { + let base = dart_ident(&p.name); + let slot = dart_ident(&p.abi[0].name); + args.push(match p.arg_pass() { + ArgPass::Direct { .. } => match &p.ty { + Ty::Enum(n) => format!("{}.fromValue({slot})", dart_class(n)), + _ => slot, + }, + ArgPass::String { len, .. } => { + format!("_readString({slot}, {})", dart_ident(&len.name)) + } + ArgPass::Bytes { len, .. } => { + format!("_copyBytes({slot}, {})", dart_ident(&len.name)) + } + ArgPass::Buffer { len, .. } => format!( + "_decode(_copyBytes({slot}, {}), {})", + dart_ident(&len.name), + decoder(&p.ty) + ), + ArgPass::Object { nullable, .. } => { + w.line(format!( + "final _{base} = {};", + adopt_expr(&slot, &p.ty, nullable) + )); + format!("_{base}") + } + ArgPass::Callback { .. } => { + unreachable!("callback methods never take callback interfaces") + } + }); + } + let call = format!( + "(_callbackTarget(ctx) as {class}).{}({})", + dart_ident(&m.name), + args.join(", ") + ); + match ret { + Ty::Enum(_) => w.line(format!("return {call}.value;")), + _ => w.line(format!("return {call};")), + }; + }); + w.line("} catch (e) {"); + w.scope(|w| { + w.line("_foreignError(outErr, e);"); + w.line(format!("return {};", default_literal(ret))); + }); + w.line("}"); + }, + ); +} + +/// A void method's `isolateGroupBound` forwarder: it runs outside the +/// isolate, so it only copies its slots into a message (borrowed bytes are +/// copied by the post) and touches no Dart globals. +fn render_forwarder( + w: &mut CodeWriter, + cb: &CallbackInterfaceBinding, + m: &CallbackMethodBinding, + class: &str, +) { + let index = cb + .methods + .iter() + .position(|x| x.name == m.name) + .expect("method of its interface"); + w.blank(); + w.line(format!( + "// Vtable entry `{}.{}`: forwarded to the isolate.", + cb.vtable_tag, m.name + )); + w.block( + format!( + "void {}({}) {{", + entry_fn(class, &m.name), + entry_params(m).join(", ") + ), + "}", + |w| { + w.line(format!( + "_CallbackMessage(ctx, {index}, {})", + m.params.len() + )); + w.scope(|w| { + w.scope(|w| { + for p in &m.params { + let slot = dart_ident(&p.abi[0].name); + w.line(match p.arg_pass() { + ArgPass::Direct { .. } => match &p.ty { + Ty::Bool => format!("..boolean({slot})"), + Ty::F32 | Ty::F64 => format!("..float64({slot})"), + _ => format!("..int64({slot})"), + }, + ArgPass::String { len, .. } + | ArgPass::Bytes { len, .. } + | ArgPass::Buffer { len, .. } => { + format!("..bytes({slot}, {})", dart_ident(&len.name)) + } + ArgPass::Object { .. } => format!("..pointer({slot})"), + ArgPass::Callback { .. } => { + unreachable!("callback methods never take callback interfaces") + } + }); + } + w.line("..send();"); + }); + }); + }, + ); +} + +/// The dispatcher running forwarded void methods on the isolate: +/// `message` is `[key, method, arguments...]`. +fn render_dispatch(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &str) { + w.blank(); + w.block( + format!( + "void {}(Object impl, int method, List message) {{", + dispatch_fn(&cb.name) + ), + "}", + |w| { + if cb.methods.iter().all(|m| m.ret.is_some()) { + return; + } + w.line(format!("final target = impl as {class};")); + w.block("switch (method) {", "}", |w| { + for (index, m) in cb.methods.iter().enumerate() { + if m.ret.is_some() { + continue; + } + // Object arguments are adopted first, so a failure while + // decoding the rest still leaves each transferred + // reference owned by a finalizable wrapper. + w.block(format!("case {index}: {{"), "}", |w| { + let params: Vec<(usize, &ParamBinding)> = + m.params.iter().enumerate().collect(); + let is_object = + |p: &ParamBinding| matches!(p.arg_pass(), ArgPass::Object { .. }); + for (i, p) in params.iter().filter(|(_, p)| is_object(p)) { + message_arg(w, p, i + 2); + } + for (i, p) in params.iter().filter(|(_, p)| !is_object(p)) { + message_arg(w, p, i + 2); + } + let args: Vec = (0..m.params.len()) + .map(|i| format!("_a{}", i + 2)) + .collect(); + w.line(format!( + "target.{}({});", + dart_ident(&m.name), + args.join(", ") + )); + }); + } + }); + }, + ); +} + +/// Decode forwarded argument `i` of `message` into the local `_a{i}`. +fn message_arg(w: &mut CodeWriter, p: &ParamBinding, i: usize) { + let field = format!("message[{i}]!"); + let local = format!("_a{i}"); + let value = match p.arg_pass() { + ArgPass::Direct { .. } => match &p.ty { + Ty::Enum(n) => format!("{}.fromValue({field} as int)", dart_class(n)), + ty => format!("{field} as {}", dart_type(ty)), + }, + ArgPass::String { .. } => format!("utf8.decode({field} as Uint8List)"), + ArgPass::Bytes { .. } => format!("{field} as Uint8List"), + ArgPass::Buffer { .. } => { + format!("_decode({field} as Uint8List, {})", decoder(&p.ty)) + } + ArgPass::Object { nullable, .. } => { + let address = format!("{local}Address"); + w.line(format!("final {address} = {field} as int;")); + if nullable { + format!( + "{address} == 0 ? null : {}._(Pointer.fromAddress({address}))", + object_class(&p.ty) + ) + } else { + format!( + "{}._(Pointer.fromAddress({address}))", + object_class(&p.ty) + ) + } + } + ArgPass::Callback { .. } => { + unreachable!("callback methods never take callback interfaces") + } + }; + w.line(format!("final {local} = {value};")); +} + +/// The one process-wide vtable every instance passed to the producer +/// shares; the per-instance state travels in `ctx`. +fn render_vtable(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, class: &str) { + let strukt = vtable_struct(class); + w.blank(); + w.block( + format!("final Pointer<{strukt}> {} = () {{", vtable_var(&cb.name)), + "}();", + |w| { + w.line(format!("final vtable = calloc<{strukt}>();")); + w.line("vtable.ref"); + w.scope(|w| { + w.scope(|w| { + for m in &cb.methods { + let td = entry_typedef(cb, m); + let entry = entry_fn(class, &m.name); + let callable = match &m.ret { + Some(ret) => format!( + "NativeCallable<{td}>.isolateLocal({entry}, exceptionalReturn: {})", + default_literal(ret) + ), + None => format!("NativeCallable<{td}>.isolateGroupBound({entry})"), + }; + w.line(format!("..{} = _pin({callable})", dart_ident(&m.name))); + } + w.line("..free = _callbackFree;"); + }); + }); + w.line("return vtable;"); + }, + ); +} diff --git a/crates/weaveffi-gen/src/targets/dart/calls.rs b/crates/weaveffi-gen/src/targets/dart/calls.rs new file mode 100644 index 00000000..5f620851 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/calls.rs @@ -0,0 +1,664 @@ +//! Callable rendering: the `lookupFunction` bindings and idiomatic Dart +//! wrappers for sync, async, and iterator callables. +//! +//! FFI signatures come straight from the model's lowered [`AbiFn`]s, so they +//! match the C header slot for slot. Parameter marshalling dispatches on the +//! shared [`ArgPass`] contract and result handling on [`RetPass`]. + +use crate::codegen::CodeWriter; +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::model::{ + AbiFn, AsyncBinding, CallShape, FnBinding, IteratorBinding, ModuleBinding, ParamBinding, Ty, +}; +use weaveffi_model::plan::{self, ArgPass, ErrorStrategy, RetPass}; + +use crate::targets::dart::callbacks::{dispatch_fn, vtable_var}; +use crate::targets::dart::codec::{read_expr, unpack_fn, write_stmts}; +use crate::targets::dart::docs::emit_wrapper_doc; +use crate::targets::dart::entities::dart_exception_name; +use crate::targets::dart::types::{ + dart_class, dart_fn_name, dart_ident, dart_type, ffi_type, ffi_typedef, ffi_var, object_class, + pointee_ffi, +}; + +/// Error-reporting context for one wrapper: the domain mapper its error +/// checks route through. +/// +/// The split follows [`ErrorStrategy`]: a throwing callable maps codes onto +/// the module's typed domain exceptions, while a non-throwing callable maps +/// every failure onto the runtime exceptions (a reported error there is only +/// ever a producer bug or a runtime trap, never a domain error). +#[derive(Clone, Copy)] +pub(crate) struct ErrCtx<'a> { + /// `true` when the wrapper surfaces typed domain errors. + throws: bool, + /// The domain exception class in effect, if any. + exception: Option<&'a str>, +} + +impl<'a> ErrCtx<'a> { + /// The domain exception this wrapper throws, if it throws one. + pub(crate) fn thrown_exception(&self) -> Option<&'a str> { + self.exception.filter(|_| self.throws) + } + + /// The `_ErrorMapper` the wrapper's checks use. + fn mapper(&self) -> String { + self.thrown_exception() + .map_or_else(|| "_runtimeError".to_string(), mapper_fn) + } + + /// The statement checking the shared error slot after a call. + fn check_stmt(&self) -> String { + match self.thrown_exception() { + Some(exc) => format!("_check(_err, {});", mapper_fn(exc)), + None => "_check(_err);".to_string(), + } + } +} + +/// The `_map{Exception}` mapper of a domain exception class. +pub(crate) fn mapper_fn(exception: &str) -> String { + format!("_map{exception}") +} + +/// The [`ErrCtx`] of one callable, paired with the exception class of the +/// domain in effect (own or inherited). +pub(crate) fn err_ctx<'a>(f: &FnBinding, exception: Option<&'a str>) -> ErrCtx<'a> { + ErrCtx { + throws: matches!(f.error_strategy(), ErrorStrategy::Throws), + exception, + } +} + +/// How one wrapper is declared: a top-level function, or a member of an +/// interface class. +pub(crate) enum DartDecl<'a> { + /// A top-level function. + TopLevel, + /// An instance method: the call borrows the wrapper's own pointer. + Method, + /// A `static` method of an interface class. + Static, + /// A factory constructor; `named` is `false` for the canonical `new`. + Factory { + /// The interface class the factory constructs. + class_name: &'a str, + /// `false` for the canonical `new` constructor. + named: bool, + }, +} + +impl DartDecl<'_> { + /// The declaration's opening line (through the `{`). `ret` is the public + /// return type and `suffix` an optional body modifier (`sync*`). + fn open_line(&self, ret: &str, name: &str, params: &str, suffix: &str) -> String { + let suffix = if suffix.is_empty() { + String::new() + } else { + format!(" {suffix}") + }; + match self { + DartDecl::TopLevel | DartDecl::Method => format!("{ret} {name}({params}){suffix} {{"), + DartDecl::Static => format!("static {ret} {name}({params}){suffix} {{"), + DartDecl::Factory { + class_name, + named: false, + } => format!("factory {class_name}({params}) {{"), + DartDecl::Factory { + class_name, + named: true, + } => format!("factory {class_name}.{name}({params}) {{"), + } + } +} + +/// Render one free function of `module` at top level. +pub(crate) fn render_function( + out: &mut String, + module: &ModuleBinding, + f: &FnBinding, + strip: bool, + leaf: bool, +) { + let exc = module + .error + .as_ref() + .map(|e| dart_exception_name(&e.type_name)); + let mut decl = String::new(); + render_callable( + out, + &mut decl, + f, + &DartDecl::TopLevel, + &dart_fn_name(&module.path, &f.name, strip), + err_ctx(f, exc.as_deref()), + leaf, + ); + out.push_str(&decl); +} + +/// Render one callable: its FFI bindings into `lookups` (always top level) +/// and its wrapper declaration into `decl`. `leaf` marks synchronous calls +/// as leaf calls (only sound when the API declares no callback interfaces, +/// so no call can re-enter Dart). +pub(crate) fn render_callable( + lookups: &mut String, + decl: &mut String, + f: &FnBinding, + kind: &DartDecl, + name: &str, + err: ErrCtx, + leaf: bool, +) { + let mut w = CodeWriter::two_space(); + w.blank(); + emit_wrapper_doc(&mut w, f, err); + match &f.shape { + CallShape::Sync(abi) => { + emit_lookup(lookups, abi, leaf, None); + render_sync(&mut w, f, abi, kind, name, err); + } + CallShape::Async(a) => { + let cb = ffi_typedef(&a.callback_type); + emit_callback_typedef(lookups, &cb, &a.callback_params); + emit_lookup(lookups, &a.launch, false, Some(&cb)); + render_async(&mut w, f, a, &cb, kind, name, err); + } + CallShape::Iterator(ib) => { + emit_lookup(lookups, &ib.launch, false, None); + emit_lookup(lookups, &ib.next, false, None); + emit_iter_destroy(lookups, ib); + render_iterator(&mut w, f, ib, kind, name, err); + } + } + decl.push_str(&w.finish()); +} + +/// The (native, Dart) FFI types of `params`, substituting `callback` for a +/// bare named slot (an async launcher's completion callback). +fn slot_types(params: &[AbiParam], callback: Option<&str>) -> (Vec, Vec) { + params + .iter() + .map(|p| match (&p.ty, callback) { + (CType::Named(_), Some(cb)) => { + let t = format!("Pointer>"); + (t.clone(), t) + } + (ty, _) => ffi_type(ty), + }) + .unzip() +} + +/// Bind one C symbol with `lookupFunction`. +pub(crate) fn emit_lookup(out: &mut String, abi: &AbiFn, leaf: bool, callback: Option<&str>) { + let (natives, darts) = slot_types(&abi.params, callback); + let (ret_n, ret_d) = ffi_type(&abi.ret); + out.push_str(&lookup( + &abi.symbol, + &format!("{ret_n} Function({})", natives.join(", ")), + &format!("{ret_d} Function({})", darts.join(", ")), + leaf, + )); +} + +/// The `final _sym = _lib.lookupFunction<...>('sym');` binding of C symbol +/// `sym` with the given native and Dart function types. +pub(crate) fn lookup(sym: &str, native: &str, dart: &str, leaf: bool) -> String { + let leaf = if leaf { ", isLeaf: true" } else { "" }; + format!( + "\nfinal {} = _lib.lookupFunction<\n {native},\n {dart}>('{sym}'{leaf});\n", + ffi_var(sym) + ) +} + +/// The `NativeFinalizer` over a destroy symbol `sym`, named after its +/// binding. +pub(crate) fn finalizer(sym: &str) -> String { + format!( + "final {}Finalizer = NativeFinalizer(\n _lib.lookup)>>('{sym}'));\n", + ffi_var(sym) + ) +} + +/// The native typedef of an async completion callback. +fn emit_callback_typedef(out: &mut String, name: &str, params: &[AbiParam]) { + let (natives, _) = slot_types(params, None); + out.push_str(&format!( + "\ntypedef {name} = Void Function({});\n", + natives.join(", ") + )); +} + +/// Bind an iterator's destroy symbol and the finalizer that backs abandoned +/// iterations. +fn emit_iter_destroy(out: &mut String, ib: &IteratorBinding) { + let sym = &ib.destroy_symbol; + out.push_str(&lookup( + sym, + "Void Function(Pointer)", + "void Function(Pointer)", + false, + )); + out.push_str(&finalizer(sym)); +} + +/// The wrapper's parameter list: the IDL parameters, plus a named +/// `cancelToken` for a cancellable async call. +fn wrapper_params(f: &FnBinding) -> String { + let mut params: Vec = f + .params + .iter() + .map(|p| format!("{} {}", dart_type(&p.ty), dart_ident(&p.name))) + .collect(); + if f.cancellable { + params.push(format!("{{CancelToken? {}}}", cancel_param(f))); + } + params.join(", ") +} + +/// The name of a cancellable call's token parameter: `cancelToken`, unless +/// an IDL parameter already took it. +fn cancel_param(f: &FnBinding) -> &'static str { + if f.params + .iter() + .any(|p| dart_ident(&p.name) == "cancelToken") + { + "cancelToken_" + } else { + "cancelToken" + } +} + +/// The marshalling of a callable's inputs: staging statements, call +/// arguments, and whether an `Arena` scopes them. +struct Inputs { + /// Statements run before the call (staging, borrowing, registering). + stage: Vec, + /// The call's argument expressions, in ABI order. + args: Vec, + /// Whether the statements allocate from (or borrow through) `_arena`. + arena: bool, +} + +/// Marshal `f`'s inputs. An instance method's call passes `_self` first. +/// Callback registrations run last, after everything that can throw, so a +/// failed staging step never strands a registered implementation. +fn marshal_inputs(f: &FnBinding) -> Inputs { + let mut w = CodeWriter::two_space(); + let mut registrations: Vec = Vec::new(); + let mut args: Vec = Vec::new(); + let mut arena = false; + let mut tmp = 0usize; + if f.has_self { + args.push("_self".into()); + } + for p in &f.params { + args.extend(marshal_param( + &mut w, + &mut registrations, + p, + &mut arena, + &mut tmp, + )); + } + let mut stage: Vec = w.finish().lines().map(str::to_string).collect(); + stage.extend(registrations); + Inputs { stage, args, arena } +} + +/// Marshal one parameter per its [`ArgPass`], returning its argument +/// expressions. +fn marshal_param( + w: &mut CodeWriter, + registrations: &mut Vec, + p: &ParamBinding, + arena: &mut bool, + tmp: &mut usize, +) -> Vec { + let name = dart_ident(&p.name); + match p.arg_pass() { + ArgPass::Direct { .. } => match &p.ty { + Ty::Enum(_) => vec![format!("{name}.value")], + _ => vec![name], + }, + ArgPass::String { .. } => { + *arena = true; + w.line(format!("final _{name}Bytes = utf8.encode({name});")); + vec![ + format!("_stage(_arena, _{name}Bytes)"), + format!("_{name}Bytes.length"), + ] + } + ArgPass::Bytes { .. } => { + *arena = true; + vec![format!("_stage(_arena, {name})"), format!("{name}.length")] + } + ArgPass::Buffer { .. } => { + *arena = true; + let writer = format!("_{name}Writer"); + w.line(format!("final {writer} = _BufferWriter();")); + write_stmts(w, &writer, &name, &p.ty, tmp); + w.line(format!("final _{name}Bytes = {writer}.takeBytes();")); + vec![ + format!("_stage(_arena, _{name}Bytes)"), + format!("_{name}Bytes.length"), + ] + } + // The wrapper keeps its reference and is borrowed for the call. + ArgPass::Object { nullable, .. } => { + *arena = true; + let ptr = format!("_{name}Ptr"); + if nullable { + w.line(format!( + "final {ptr} = {name} == null ? nullptr : _borrow(_arena, {name});" + )); + } else { + w.line(format!("final {ptr} = _borrow(_arena, {name});")); + } + vec![ptr] + } + // The producer owns the registration and releases it through the + // vtable's `free`. + ArgPass::Callback { .. } => { + let cb = + p.ty.callback_interface_name() + .expect("callback family names a callback interface"); + let ctx = format!("_{name}Ctx"); + registrations.push(format!( + "final {ctx} = _registerCallback({name}, {});", + dispatch_fn(cb) + )); + vec![ctx, format!("{}.cast()", vtable_var(cb))] + } + } +} + +/// The expression adopting the owned object pointer `expr` into its wrapper; +/// a null pointer becomes `null` when `nullable`. +pub(crate) fn adopt_expr(expr: &str, ty: &Ty, nullable: bool) -> String { + let class = object_class(ty); + if nullable { + format!("{expr} == nullptr ? null : {class}._({expr})") + } else { + format!("{class}._({expr})") + } +} + +/// The decoder argument `_decode` takes for a buffered `ty`: the record's +/// own unpack function, or a closure over the inline read expression. +pub(crate) fn decoder(ty: &Ty) -> String { + match ty { + Ty::Record(n) | Ty::RichEnum(n) => unpack_fn(n), + _ => format!("(r) => {}", read_expr("r", ty)), + } +} + +/// The expression turning a produced value into its Dart value per its +/// [`RetPass`]: `value` is the pointer or scalar and `len` its length slot. +/// Owned strings, bytes, and buffers are copied and released; objects are +/// adopted. +pub(crate) fn receive_expr(ty: &Ty, pass: &RetPass, value: &str, len: &str) -> String { + match pass { + RetPass::Void => String::new(), + RetPass::Direct => match ty { + Ty::Enum(n) => format!("{}.fromValue({value})", dart_class(n)), + _ => value.to_string(), + }, + RetPass::String => format!("_takeString({value}, {len})"), + RetPass::Bytes => format!("_takeBytes({value}, {len})"), + RetPass::Buffer => format!("_decode(_takeBytes({value}, {len}), {})", decoder(ty)), + RetPass::Object { nullable, .. } => adopt_expr(value, ty, *nullable), + } +} + +/// Write `body` inside the try/finally that scopes `_self` and `_arena`, or +/// bare when the call needs neither. +fn scoped(w: &mut CodeWriter, has_self: bool, arena: bool, body: impl FnOnce(&mut CodeWriter)) { + if has_self { + w.line("final _self = _enter();"); + } + if arena { + w.line("final _arena = Arena();"); + } + if !has_self && !arena { + body(w); + return; + } + w.line("try {"); + w.scope(body); + w.line("} finally {"); + w.scope(|w| { + if arena { + w.line("_arena.releaseAll();"); + } + if has_self { + w.line("_leave();"); + } + }); + w.line("}"); +} + +/// A synchronous wrapper: stage, call, check, receive. +fn render_sync( + w: &mut CodeWriter, + f: &FnBinding, + abi: &AbiFn, + kind: &DartDecl, + name: &str, + err: ErrCtx, +) { + let ret = f.ret.as_ref().map_or("void".to_string(), dart_type); + let pass = plan::ret_pass(f.ret.as_ref(), ""); + let mut inputs = marshal_inputs(f); + if matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer) { + inputs.args.push("_outLen".into()); + } + inputs.args.push("_err".into()); + let call = format!("{}({})", ffi_var(&abi.symbol), inputs.args.join(", ")); + w.block( + kind.open_line(&ret, name, &wrapper_params(f), ""), + "}", + |w| { + scoped(w, f.has_self, inputs.arena, |w| { + for s in &inputs.stage { + w.line(s); + } + match &pass { + RetPass::Void => { + w.line(format!("{call};")); + w.line(err.check_stmt()); + } + _ => { + w.line(format!("final _result = {call};")); + w.line(err.check_stmt()); + let ty = f.ret.as_ref().expect("non-void return"); + w.line(format!( + "return {};", + receive_expr(ty, &pass, "_result", "_outLen.value") + )); + } + } + }); + }, + ); +} + +/// An async wrapper: a `Future` completed from a `NativeCallable.listener` +/// the producer invokes exactly once, from any thread. Results and errors +/// it receives are owned, so decoding them later on the event loop is safe. +/// A cancellable call binds a native token to its `CancelToken` for the +/// call's duration. +fn render_async( + w: &mut CodeWriter, + f: &FnBinding, + a: &AsyncBinding, + cb: &str, + kind: &DartDecl, + name: &str, + err: ErrCtx, +) { + let ret = f.ret.as_ref().map_or("void".to_string(), dart_type); + let pass = plan::ret_pass(f.ret.as_ref(), ""); + let mut inputs = marshal_inputs(f); + if f.cancellable { + inputs.args.push("_cancel?.pointer ?? nullptr".into()); + } + inputs.args.push("_callback.nativeFunction".into()); + inputs.args.push("nullptr".into()); + let call = format!("{}({});", ffi_var(&a.launch.symbol), inputs.args.join(", ")); + + // The listener's parameters, named after the callback's result slots. + let (_, darts) = slot_types(&a.callback_params, None); + let names = ["_", "_error", "_value", "_valueLen"]; + let listener_params: Vec = darts + .iter() + .zip(names) + .map(|(d, n)| format!("{d} {n}")) + .collect(); + let result = match (&pass, f.ret.as_ref()) { + (RetPass::Void, _) | (_, None) => String::new(), + (_, Some(ty)) => receive_expr(ty, &pass, "_value", "_valueLen"), + }; + + let open = kind.open_line(&format!("Future<{ret}>"), name, &wrapper_params(f), ""); + let has_self = f.has_self; + w.block(open, "}", |w| { + if has_self { + w.line("final _self = _enter();"); + } + if inputs.arena { + w.line("final _arena = Arena();"); + } + w.line(format!("final _completer = Completer<{ret}>();")); + if f.cancellable { + w.line(format!( + "final _cancel = _NativeCancel.bind({});", + cancel_param(f) + )); + } + w.line(format!("late final NativeCallable<{cb}> _callback;")); + w.line(format!("_callback = NativeCallable<{cb}>.listener((")); + w.line(format!(" {}) {{", listener_params.join(", "))); + w.scope(|w| { + w.line("_callback.close();"); + if f.cancellable { + w.line("_cancel?.release();"); + } + w.line("try {"); + w.scope(|w| { + w.line(format!( + "if (_error != nullptr) throw _takeAsyncError(_error, {});", + err.mapper() + )); + w.line(format!("_completer.complete({result});")); + }); + w.line("} catch (e, s) {"); + w.scope(|w| { + w.line("_completer.completeError(e, s);"); + }); + w.line("}"); + }); + w.line("});"); + w.line("try {"); + w.scope(|w| { + for s in &inputs.stage { + w.line(s); + } + w.line(&call); + }); + w.line("} catch (_) {"); + w.scope(|w| { + w.line("_callback.close();"); + if f.cancellable { + w.line("_cancel?.release();"); + } + w.line("rethrow;"); + }); + if has_self || inputs.arena { + w.line("} finally {"); + w.scope(|w| { + if inputs.arena { + w.line("_arena.releaseAll();"); + } + if has_self { + w.line("_leave();"); + } + }); + } + w.line("}"); + w.line("return _completer.future;"); + }); +} + +/// An iterator wrapper: a lazy `sync*` body that launches the native +/// iterator on the first pull, issues one `next` per element, and destroys +/// the iterator exactly once (eagerly on completion or failure, or through +/// the finalizer when the iteration is abandoned). +fn render_iterator( + w: &mut CodeWriter, + f: &FnBinding, + ib: &IteratorBinding, + kind: &DartDecl, + name: &str, + err: ErrCtx, +) { + let ret = f.ret.as_ref().map_or("void".to_string(), dart_type); + let elem = plan::ret_pass(Some(&ib.elem), ""); + let mut inputs = marshal_inputs(f); + inputs.args.push("_err".into()); + let launch = format!("{}({})", ffi_var(&ib.launch.symbol), inputs.args.join(", ")); + // `next(iter, out_item, [out_len,] out_err)`: `_outItem` is read as the + // item slot's pointee. + let item_type = pointee_ffi(&ib.next.params[1].ty); + let has_len = ib.next.params.len() == 4; + let next_args = if has_len { + "_iter, _outItem.cast(), _outLen, _err" + } else { + "_iter, _outItem.cast(), _err" + }; + let destroy = ffi_var(&ib.destroy_symbol); + let item = receive_expr( + &ib.elem, + &elem, + &format!("_outItem.cast<{item_type}>().value"), + "_outLen.value", + ); + + w.block( + kind.open_line(&ret, name, &wrapper_params(f), "sync*"), + "}", + |w| { + w.line("final Pointer _iter;"); + scoped(w, f.has_self, inputs.arena, |w| { + for s in &inputs.stage { + w.line(s); + } + w.line(format!("_iter = {launch};")); + w.line(err.check_stmt()); + }); + w.line("final _anchor = _IteratorAnchor();"); + w.line(format!( + "{destroy}Finalizer.attach(_anchor, _iter, detach: _anchor);" + )); + w.line("try {"); + w.scope(|w| { + w.block("while (true) {", "}", |w| { + w.line(format!( + "final _more = {}({next_args});", + ffi_var(&ib.next.symbol) + )); + w.line(err.check_stmt()); + w.line("if (_more == 0) break;"); + w.line(format!("yield {item};")); + }); + }); + w.line("} finally {"); + w.scope(|w| { + w.line(format!("{destroy}Finalizer.detach(_anchor);")); + w.line(format!("{destroy}(_iter);")); + }); + w.line("}"); + }, + ); +} diff --git a/crates/weaveffi-gen/src/targets/dart/codec.rs b/crates/weaveffi-gen/src/targets/dart/codec.rs new file mode 100644 index 00000000..68ca6490 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/codec.rs @@ -0,0 +1,113 @@ +//! Value-buffer codec emitters: the inline read expressions and write +//! statements for any wire shape, naming the per-record and per-rich-enum +//! `_pack{Name}`/`_unpack{Name}` helpers. +//! +//! Every dispatch goes through [`Ty::wire`], and primitives map onto the +//! runtime's `read{Prim}`/`write{Prim}` methods by +//! [`Prim::pascal`](weaveffi_model::model::Prim::pascal), so this +//! module never re-derives the wire format. +//! +//! An object token carries one strong reference. Writing one calls the +//! wrapper's `_cloneRef()` so the encoding owns a fresh reference and the +//! wrapper keeps its own; reading one adopts the pointer into a new wrapper. + +use crate::codegen::CodeWriter; +use weaveffi_model::model::{Ty, WireType}; + +use crate::targets::dart::types::{dart_class, dart_type}; + +/// The `_pack{Name}` helper of a record or rich enum. +pub(crate) fn pack_fn(name: &str) -> String { + format!("_pack{}", dart_class(name)) +} + +/// The `_unpack{Name}` helper of a record or rich enum. +pub(crate) fn unpack_fn(name: &str) -> String { + format!("_unpack{}", dart_class(name)) +} + +/// Mint a fresh `_t{n}` temporary name. +pub(crate) fn fresh(tmp: &mut usize) -> String { + let n = *tmp; + *tmp += 1; + format!("_t{n}") +} + +/// The Dart expression decoding one value of `ty` from the reader `r`. Read +/// expressions evaluate strictly left to right, so composing them preserves +/// the wire order. Collections grow as they decode, so a corrupt count can't +/// force a huge allocation up front. +pub(crate) fn read_expr(r: &str, ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(p) => format!("{r}.read{}()", p.pascal()), + // A token is one adopted strong reference. + WireType::Object(n) => format!( + "{}._(Pointer.fromAddress({r}.readU64()))", + dart_class(n) + ), + WireType::Enum(n) => format!("{}.fromValue({r}.readI32())", dart_class(n)), + WireType::User(n) => format!("{}({r})", unpack_fn(n)), + WireType::Optional(inner) => { + format!("({r}.readFlag() ? {} : null)", read_expr(r, inner)) + } + WireType::List(inner) => format!( + "<{}>[for (var i = {r}.readLength(); i > 0; i--) {}]", + dart_type(inner), + read_expr(r, inner) + ), + WireType::Map(k, v) => format!( + "<{}, {}>{{for (var i = {r}.readLength(); i > 0; i--) {}: {}}}", + dart_type(k), + dart_type(v), + read_expr(r, k), + read_expr(r, v) + ), + } +} + +/// Emit the statements encoding `expr` (a value of `ty`) into the writer +/// `wr`. Optionals, lists, and maps recurse through fresh `_t{n}` +/// temporaries. +pub(crate) fn write_stmts(w: &mut CodeWriter, wr: &str, expr: &str, ty: &Ty, tmp: &mut usize) { + match ty.wire() { + WireType::Prim(p) => { + w.line(format!("{wr}.write{}({expr});", p.pascal())); + } + WireType::Object(_) => { + w.line(format!("{wr}.writeU64({expr}._cloneRef().address);")); + } + WireType::Enum(_) => { + w.line(format!("{wr}.writeI32({expr}.value);")); + } + WireType::User(n) => { + w.line(format!("{}({wr}, {expr});", pack_fn(n))); + } + WireType::Optional(inner) => { + let t = fresh(tmp); + w.line(format!("final {t} = {expr};")); + w.line(format!("{wr}.writeFlag({t} != null);")); + w.block(format!("if ({t} != null) {{"), "}", |w| { + write_stmts(w, wr, &t, inner, tmp); + }); + } + WireType::List(inner) => { + let t = fresh(tmp); + let e = fresh(tmp); + w.line(format!("final {t} = {expr};")); + w.line(format!("{wr}.writeLength({t}.length);")); + w.block(format!("for (final {e} in {t}) {{"), "}", |w| { + write_stmts(w, wr, &e, inner, tmp); + }); + } + WireType::Map(k, v) => { + let t = fresh(tmp); + let e = fresh(tmp); + w.line(format!("final {t} = {expr};")); + w.line(format!("{wr}.writeLength({t}.length);")); + w.block(format!("for (final {e} in {t}.entries) {{"), "}", |w| { + write_stmts(w, wr, &format!("{e}.key"), k, tmp); + write_stmts(w, wr, &format!("{e}.value"), v, tmp); + }); + } + } +} diff --git a/crates/weaveffi-gen/src/targets/dart/docs.rs b/crates/weaveffi-gen/src/targets/dart/docs.rs new file mode 100644 index 00000000..848345df --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/docs.rs @@ -0,0 +1,88 @@ +//! Doc-comment emission: `///` comments carried from the IDL, plus the +//! generated notes (streaming contract, ownership, thrown exception, +//! cancellation, deprecation) a wrapper's declaration carries. + +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use weaveffi_model::model::{CallShape, FnBinding, Ty}; + +use crate::targets::dart::calls::ErrCtx; +use crate::targets::dart::types::dart_str_literal; + +/// Emit `doc` at the writer's current depth. +pub(crate) fn write_doc(w: &mut CodeWriter, doc: &Option) { + w.doc(doc, DocCommentStyle::TripleSlash); +} + +/// Emit `@Deprecated(...)` when `deprecated` carries a message. +pub(crate) fn write_deprecated(w: &mut CodeWriter, deprecated: &Option) { + if let Some(msg) = deprecated { + w.line(format!("@Deprecated('{}')", dart_str_literal(msg))); + } +} + +/// Emit a wrapper's doc comment followed by the generated notes that apply +/// to it, then its `@Deprecated` annotation. +pub(crate) fn emit_wrapper_doc(w: &mut CodeWriter, f: &FnBinding, err: ErrCtx) { + write_doc(w, &f.doc); + let mut notes: Vec<&str> = Vec::new(); + let mut owned: Vec = Vec::new(); + if let CallShape::Iterator(ib) = &f.shape { + notes.extend([ + "Returns a lazy [Iterable]: each step pulls one element from a native", + "iterator, and iterating again starts a new one. The native iterator", + "is released when the iteration ends, fails, or is abandoned and", + "collected.", + ]); + if ib.elem.interface_name().is_some() { + notes.extend([ + "", + "Each element is owned by the caller: call its `dispose()` when", + "you're done with it.", + ]); + } + } else if let Some(ret) = f.ret.as_ref().filter(|r| r.interface_name().is_some()) { + if matches!(ret, Ty::Optional(_)) { + notes.extend([ + "Returns null when the producer reports no object. A returned object", + "is owned by the caller: call its `dispose()` when you're done with it.", + ]); + } else { + notes.extend([ + "The returned object is owned by the caller: call its `dispose()`", + "when you're done with it.", + ]); + } + } + if f.cancellable { + if !notes.is_empty() { + notes.push(""); + } + notes.extend([ + "Cancelling [cancelToken] completes the returned future with a", + "[CancelledException].", + ]); + } + if let Some(exc) = err.thrown_exception() { + if !notes.is_empty() { + notes.push(""); + } + owned.push(format!("Throws [{exc}] on domain errors.")); + } + if !notes.is_empty() || !owned.is_empty() { + if f.doc.is_some() { + w.line("///"); + } + for n in notes { + w.line(if n.is_empty() { + "///".to_string() + } else { + format!("/// {n}") + }); + } + for n in owned { + w.line(format!("/// {n}")); + } + } + write_deprecated(w, &f.deprecated); +} diff --git a/crates/weaveffi-gen/src/targets/dart/entities.rs b/crates/weaveffi-gen/src/targets/dart/entities.rs new file mode 100644 index 00000000..79b8abbe --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/entities.rs @@ -0,0 +1,451 @@ +//! Entity rendering: error domains, C-style enums, rich enums, records, and +//! interface wrapper classes, plus their value-buffer codec helpers. + +use crate::codegen::CodeWriter; +use heck::ToUpperCamelCase; +use weaveffi_model::model::{ + EnumBinding, ErrorBinding, FieldBinding, InterfaceBinding, ModuleBinding, StructBinding, Ty, +}; + +use crate::targets::dart::calls::{ + err_ctx, finalizer, lookup, mapper_fn, render_callable, DartDecl, +}; +use crate::targets::dart::codec::{fresh, pack_fn, read_expr, unpack_fn, write_stmts}; +use crate::targets::dart::docs::{write_deprecated, write_doc}; +use crate::targets::dart::types::{dart_class, dart_ident, dart_str_literal, dart_type, ffi_var}; + +/// The Dart exception class of an error domain or code: the PascalCase name +/// with a trailing `Error` swapped for `Exception` (`KvError` becomes +/// `KvException`, a code `IoError` becomes `IoException`, and a bare +/// `Error` becomes `ErrorException`). +pub(crate) fn dart_exception_name(raw: &str) -> String { + let pascal = raw.to_upper_camel_case(); + let stem = pascal + .strip_suffix("Error") + .filter(|s| !s.is_empty()) + .unwrap_or(&pascal); + if stem.ends_with("Exception") { + dart_class(stem) + } else { + dart_class(&format!("{stem}Exception")) + } +} + +/// The indefinite article for a class name (`an Entry`, `a Contact`). +fn article(class: &str) -> &'static str { + if class.starts_with(['A', 'E', 'I', 'O', 'U']) { + "an" + } else { + "a" + } +} + +/// Render the final fields of a value class, with their docs. +fn render_fields(w: &mut CodeWriter, fields: &[FieldBinding]) { + for f in fields { + write_doc(w, &f.doc); + w.line(format!( + "final {} {};", + dart_type(&f.ty), + dart_ident(&f.name) + )); + } +} + +/// Render one module's declared error domain: the domain exception (a +/// `NativeException` subclass), one subclass per code carrying its code, +/// default message, and decoded payload fields, and the `_map{Domain}` +/// mapper that throwing wrappers route codes through. Only declared codes +/// gain a `case`; every other code, including the negative runtime range, +/// maps onto the runtime exceptions. +pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorBinding) { + let exc = dart_exception_name(&eb.type_name); + let mut w = CodeWriter::two_space(); + w.blank(); + w.line(format!( + "/// The `{}` error domain of module `{}`.", + eb.name, module.dot_path + )); + w.block( + format!("class {exc} extends NativeException {{"), + "}", + |w| { + w.line("/// Creates a domain error carrying [code] and [message]."); + w.line(format!("{exc}(super.code, super.message);")); + }, + ); + + for c in &eb.codes { + let class = dart_exception_name(&c.name); + let message = dart_str_literal(&c.message); + w.blank(); + write_doc(&mut w, &c.doc.clone().or_else(|| Some(c.message.clone()))); + w.block(format!("class {class} extends {exc} {{"), "}", |w| { + if c.fields.is_empty() { + w.line(format!( + "{class}([String message = '{message}']) : super({}, message);", + c.value + )); + } else { + let params: Vec = c + .fields + .iter() + .map(|f| format!("this.{}", dart_ident(&f.name))) + .collect(); + w.line(format!( + "{class}({}, [String message = '{message}'])", + params.join(", ") + )); + w.line(format!(" : super({}, message);", c.value)); + w.blank(); + render_fields(w, &c.fields); + } + }); + } + + w.blank(); + w.block( + format!( + "NativeException {}(int code, String message, Uint8List payload) {{", + mapper_fn(&exc) + ), + "}", + |w| { + w.block("switch (code) {", "}", |w| { + for c in &eb.codes { + let class = dart_exception_name(&c.name); + w.line(format!("case {}:", c.value)); + w.scope(|w| { + if c.fields.is_empty() { + w.line(format!("return {class}(message);")); + } else { + w.line("final r = _BufferReader(payload);"); + let args: Vec = + c.fields.iter().map(|f| read_expr("r", &f.ty)).collect(); + w.line(format!( + "final error = {class}({}, message);", + args.join(", ") + )); + w.line("r.expectEnd();"); + w.line("return error;"); + } + }); + } + w.line("default:"); + w.scope(|w| { + w.line("return _runtimeError(code, message, payload);"); + }); + }); + }, + ); + out.push_str(&w.finish()); +} + +/// Render one interface as a wrapper class over the runtime's +/// `_NativeObject`: the canonical `new` constructor is an unnamed factory, +/// other constructors are named factories, methods borrow the wrapper's +/// pointer, and statics are `static`. Member bindings stay at file scope. +pub(crate) fn render_interface( + out: &mut String, + module: &ModuleBinding, + i: &InterfaceBinding, + leaf: bool, +) { + let class = dart_class(&i.name); + let destroy = ffi_var(&i.destroy_symbol); + let clone = ffi_var(&i.clone_symbol); + out.push_str(&lookup( + &i.clone_symbol, + "Pointer Function(Pointer)", + "Pointer Function(Pointer)", + leaf, + )); + out.push_str(&lookup( + &i.destroy_symbol, + "Void Function(Pointer)", + "void Function(Pointer)", + leaf, + )); + out.push_str(&finalizer(&i.destroy_symbol)); + + let exc = module + .error + .as_ref() + .map(|e| dart_exception_name(&e.type_name)); + let mut members = String::new(); + for c in &i.constructors { + let kind = DartDecl::Factory { + class_name: &class, + named: c.name != "new", + }; + let name = dart_ident(&c.name); + render_callable( + out, + &mut members, + c, + &kind, + &name, + err_ctx(c, exc.as_deref()), + leaf, + ); + } + for m in &i.methods { + let name = dart_ident(&m.name); + let err = err_ctx(m, exc.as_deref()); + render_callable(out, &mut members, m, &DartDecl::Method, &name, err, leaf); + } + for s in &i.statics { + let name = dart_ident(&s.name); + let err = err_ctx(s, exc.as_deref()); + render_callable(out, &mut members, s, &DartDecl::Static, &name, err, leaf); + } + + let mut w = CodeWriter::two_space(); + w.blank(); + write_doc(&mut w, &i.doc); + if i.doc.is_some() { + w.line("///"); + } + w.line("/// A reference-counted native object. Each instance holds one strong"); + w.line("/// reference, released by [dispose] or, when an undisposed instance is"); + w.line("/// collected, by a finalizer."); + write_deprecated(&mut w, &i.deprecated); + w.block( + format!("final class {class} extends _NativeObject {{"), + "}", + |w| { + w.line(format!("{class}._(super._ptr);")); + w.blank(); + w.line("@override"); + w.line(format!( + "NativeFinalizer get _finalizer => {destroy}Finalizer;" + )); + w.blank(); + w.line("@override"); + w.line(format!( + "void _destroy(Pointer ptr) => {destroy}(ptr);" + )); + w.blank(); + w.line("@override"); + w.line(format!( + "Pointer _clone(Pointer ptr) => {clone}(ptr);" + )); + w.block_raw(&members); + }, + ); + out.push_str(&w.finish()); +} + +/// Enum members `enum` declarations already define; a variant spelled like +/// one gains a trailing `_`. +const ENUM_MEMBERS: &[&str] = &["fromValue", "index", "name", "value", "values"]; + +/// Render one enum: a C-style enum becomes an enhanced Dart `enum` carrying +/// its discriminant; a rich enum becomes a sealed class hierarchy. +pub(crate) fn render_enum(out: &mut String, e: &EnumBinding) { + if e.is_rich() { + render_rich_enum(out, e); + return; + } + let name = dart_class(&e.name); + let mut w = CodeWriter::two_space(); + w.blank(); + write_doc(&mut w, &e.doc); + write_deprecated(&mut w, &e.deprecated); + w.block(format!("enum {name} {{"), "}", |w| { + for (i, v) in e.variants.iter().enumerate() { + let mut variant = dart_ident(&v.name); + if ENUM_MEMBERS.contains(&variant.as_str()) { + variant.push('_'); + } + write_doc(w, &v.doc); + let end = if i + 1 == e.variants.len() { ';' } else { ',' }; + w.line(format!("{variant}({}){end}", v.value)); + } + w.blank(); + w.line(format!("const {name}(this.value);")); + w.blank(); + w.line("/// The C discriminant."); + w.line("final int value;"); + w.blank(); + w.line("/// The variant whose discriminant is [value]."); + w.line(format!("static {name} fromValue(int value) => values.firstWhere(")); + w.line(" (e) => e.value == value,"); + w.line(format!( + " orElse: () => throw ArgumentError.value(value, 'value', 'not a {name} discriminant'));" + )); + }); + out.push_str(&w.finish()); +} + +/// Render one record as a plain Dart value class (final fields, one named +/// constructor argument per field, optional fields not required) plus its +/// `_pack{Name}`/`_unpack{Name}` helpers. +pub(crate) fn render_struct(out: &mut String, s: &StructBinding) { + let class = dart_class(&s.name); + let mut w = CodeWriter::two_space(); + w.blank(); + write_doc(&mut w, &s.doc); + write_deprecated(&mut w, &s.deprecated); + w.block(format!("class {class} {{"), "}", |w| { + if s.fields.is_empty() { + w.line(format!("/// Creates {} [{class}].", article(&class))); + w.line(format!("const {class}();")); + return; + } + let params: Vec = s + .fields + .iter() + .map(|f| { + let n = dart_ident(&f.name); + if matches!(f.ty, Ty::Optional(_)) { + format!("this.{n}") + } else { + format!("required this.{n}") + } + }) + .collect(); + w.line(format!("/// Creates {} [{class}].", article(&class))); + w.line(format!("{class}({{{}}});", params.join(", "))); + w.blank(); + render_fields(w, &s.fields); + }); + + w.blank(); + w.block( + format!("void {}(_BufferWriter w, {class} v) {{", pack_fn(&s.name)), + "}", + |w| { + let mut tmp = 0usize; + for f in &s.fields { + let expr = format!("v.{}", dart_ident(&f.name)); + write_stmts(w, "w", &expr, &f.ty, &mut tmp); + } + }, + ); + + // Named arguments evaluate in source order, which is the wire order. + w.blank(); + if s.fields.is_empty() { + w.line(format!( + "{class} {}(_BufferReader r) => const {class}();", + unpack_fn(&s.name) + )); + } else { + w.line(format!( + "{class} {}(_BufferReader r) => {class}(", + unpack_fn(&s.name) + )); + w.scope(|w| { + for f in &s.fields { + w.line(format!( + "{}: {},", + dart_ident(&f.name), + read_expr("r", &f.ty) + )); + } + }); + w.line(");"); + } + out.push_str(&w.finish()); +} + +/// Render one rich enum as a sealed class hierarchy: a sealed base plus one +/// subclass per variant carrying its fields (positional constructor), and +/// `_pack{Name}`/`_unpack{Name}` helpers encoding the `i32` tag followed by +/// the active variant's fields. +fn render_rich_enum(out: &mut String, e: &EnumBinding) { + let base = dart_class(&e.name); + let variant_class = |name: &str| dart_class(&format!("{}{}", e.name, name)); + let mut w = CodeWriter::two_space(); + w.blank(); + write_doc(&mut w, &e.doc); + write_deprecated(&mut w, &e.deprecated); + w.block(format!("sealed class {base} {{"), "}", |w| { + w.line(format!("const {base}();")); + }); + + for v in &e.variants { + let cls = variant_class(&v.name); + w.blank(); + write_doc(&mut w, &v.doc); + if v.fields.is_empty() { + w.block(format!("final class {cls} extends {base} {{"), "}", |w| { + w.line(format!("/// Creates {} [{cls}].", article(&cls))); + w.line(format!("const {cls}();")); + }); + continue; + } + w.block(format!("final class {cls} extends {base} {{"), "}", |w| { + let params: Vec = v + .fields + .iter() + .map(|f| format!("this.{}", dart_ident(&f.name))) + .collect(); + w.line(format!("/// Creates {} [{cls}].", article(&cls))); + w.line(format!("{cls}({});", params.join(", "))); + w.blank(); + render_fields(w, &v.fields); + }); + } + + // Pack: the tag, then the active variant's fields. The sealed base makes + // the switch exhaustive. One temp counter spans every case so names stay + // unique across the switch. + w.blank(); + w.block( + format!("void {}(_BufferWriter w, {base} v) {{", pack_fn(&e.name)), + "}", + |w| { + w.block("switch (v) {", "}", |w| { + let mut tmp = 0usize; + for v in &e.variants { + let cls = variant_class(&v.name); + if v.fields.is_empty() { + w.line(format!("case {cls}():")); + w.scope(|w| { + w.line(format!("w.writeI32({});", v.value)); + }); + continue; + } + let b = fresh(&mut tmp); + w.line(format!("case final {cls} {b}:")); + w.scope(|w| { + w.line(format!("w.writeI32({});", v.value)); + for f in &v.fields { + let expr = format!("{b}.{}", dart_ident(&f.name)); + write_stmts(w, "w", &expr, &f.ty, &mut tmp); + } + }); + } + }); + }, + ); + + // Unpack: positional arguments evaluate left to right, in wire order. + w.blank(); + w.block( + format!("{base} {}(_BufferReader r) {{", unpack_fn(&e.name)), + "}", + |w| { + w.line("final tag = r.readI32();"); + w.block("switch (tag) {", "}", |w| { + for v in &e.variants { + let cls = variant_class(&v.name); + w.line(format!("case {}:", v.value)); + w.scope(|w| { + let args: Vec = + v.fields.iter().map(|f| read_expr("r", &f.ty)).collect(); + let ctor = if v.fields.is_empty() { "const " } else { "" }; + w.line(format!("return {ctor}{cls}({});", args.join(", "))); + }); + } + w.line("default:"); + w.scope(|w| { + w.line(format!("_bufferError('unknown {base} tag $tag');")); + }); + }); + }, + ); + out.push_str(&w.finish()); +} diff --git a/crates/weaveffi-gen/src/targets/dart/mod.rs b/crates/weaveffi-gen/src/targets/dart/mod.rs new file mode 100644 index 00000000..401d9a93 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/mod.rs @@ -0,0 +1,231 @@ +//! Dart (`dart:ffi`) binding generator. +//! +//! Emits a standalone Dart package (`pubspec.yaml`, `README.md`, and +//! `lib/{package}.dart`) whose bindings call the C ABI through `dart:ffi`. +//! The package is named after the identity's C prefix and loads the +//! identity's library, honoring the `{PREFIX}_LIBRARY` override. On first use +//! the library's ABI revision and every top-level module's contract checksum +//! are verified. +//! +//! Records and rich enums are value types: plain Dart classes (a sealed +//! hierarchy for a rich enum) that cross the ABI serialized in the value +//! buffer format. Interfaces are wrapper classes owning one strong reference, +//! released by `dispose()` or a `NativeFinalizer`, and guarded so a +//! `dispose()` during an in-flight call defers the release until the call +//! returns. Async functions return a `Future` completed by a +//! `NativeCallable.listener`; cancellable ones take a `CancelToken`. +//! Callback interfaces are abstract classes: value-returning methods are +//! isolate-local trampolines (callable only on the isolate's thread), and +//! void methods are isolate-group-bound forwarders that may run on any thread +//! and deliver the call on the isolate's event loop. + +mod callbacks; +mod calls; +mod codec; +mod docs; +mod entities; +mod package; +mod runtime; +mod types; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::package::{PackageContext, PackagedFile}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::resolved::ResolvedApi; + +use crate::targets::dart::callbacks::render_callback_interface; +use crate::targets::dart::calls::render_function; +use crate::targets::dart::entities::{render_enum, render_error, render_interface, render_struct}; +use crate::targets::dart::package::{render_packaged_readme, render_pubspec, render_readme}; +use crate::targets::dart::runtime::{bundles_platform, render_runtime, LoaderCandidates}; + +/// Per-target configuration for [`DartGenerator`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct DartConfig { + /// Dart package name: the pubspec `name` and the library file + /// `lib/{package_name}.dart`. Defaults to the identity's C prefix. + pub package_name: Option, + /// When `true` (the default), module-level functions drop their module + /// path, so a `contacts` module exports `createContact` rather than + /// `contactsCreateContact`. + pub strip_module_prefix: bool, + /// Basename of the IDL the CLI was invoked with, for file preludes. + /// Populated by the CLI. + #[serde(skip)] + pub input_basename: Option, +} + +impl Default for DartConfig { + fn default() -> Self { + Self { + package_name: None, + strip_module_prefix: true, + input_basename: None, + } + } +} + +impl DartConfig { + /// The Dart package name for `api`: the configured name, else the + /// identity's C prefix. + pub fn package_name(&self, api: &ResolvedApi) -> String { + self.package_name + .clone() + .unwrap_or_else(|| api.identity().prefix.clone()) + } + + /// The IDL basename for generated-file preludes. + pub fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } +} + +/// The Dart backend. +pub struct DartGenerator; + +impl DartGenerator { + /// Render `lib/{package}.dart`: imports, the runtime sections the API + /// uses, then every module's declarations. + fn render_library( + &self, + api: &ResolvedApi, + model: &BindingModel, + config: &DartConfig, + loader: &LoaderCandidates, + file_name: &str, + ) -> String { + let mut out = render_prelude(CommentStyle::DoubleSlash, config.input_basename()); + out.push_str( + "// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element\n\n", + ); + let callbacks = model.has_callback_interfaces(); + if model.has_async() || callbacks { + out.push_str("import 'dart:async';\n"); + } + out.push_str("import 'dart:convert';\n"); + out.push_str("import 'dart:ffi';\n"); + out.push_str("import 'dart:io' show Platform;\n"); + if callbacks { + out.push_str("import 'dart:isolate';\n"); + } + out.push_str("import 'dart:typed_data';\n\n"); + out.push_str("import 'package:ffi/ffi.dart';\n"); + + render_runtime(&mut out, api.identity(), model, loader); + // Synchronous calls are leaf calls only when no callback interface + // exists, so no call can re-enter Dart. That is a property of the + // whole API, so the walk is done here rather than through the + // per-entity hooks (in `emit_members` order). + let leaf = !callbacks; + for module in &model.modules { + if let Some(e) = module.error.as_ref().filter(|e| e.declared_here) { + render_error(&mut out, module, e); + } + for e in &module.enums { + render_enum(&mut out, e); + } + for s in &module.structs { + render_struct(&mut out, s); + } + for cb in &module.callback_interfaces { + render_callback_interface(&mut out, cb); + } + for i in &module.interfaces { + render_interface(&mut out, module, i, leaf); + } + for f in &module.functions { + render_function(&mut out, module, f, config.strip_module_prefix, leaf); + } + } + out.push('\n'); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, file_name)); + out + } +} + +impl LanguageBackend for DartGenerator { + type Config = DartConfig; + + fn name(&self) -> &'static str { + "dart" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + let package = config.package_name(api); + let dir = out_dir.join("dart"); + let file = format!("{package}.dart"); + let loader = LoaderCandidates::system(api.identity()); + let input = config.input_basename(); + vec![ + OutputFile::new( + dir.join("lib").join(&file), + self.render_library(api, model, config, &loader, &file), + ), + OutputFile::new( + dir.join("pubspec.yaml"), + render_pubspec(api.identity(), &package, input), + ), + OutputFile::new( + dir.join("README.md"), + render_readme(api.identity(), &package, input), + ), + ] + } + + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let package = config.package_name(api); + let dir = out_dir.join("dart"); + let file = format!("{package}.dart"); + let loader = LoaderCandidates::bundled(&ctx.binaries.lib_name); + let input = config.input_basename(); + let mut files = vec![ + PackagedFile::text( + dir.join("lib").join(&file), + self.render_library(api, model, config, &loader, &file), + ), + PackagedFile::text( + dir.join("pubspec.yaml"), + render_pubspec(api.identity(), &package, input), + ), + PackagedFile::text( + dir.join("README.md"), + render_packaged_readme(api.identity(), ctx, input), + ), + ]; + for nb in ctx + .binaries + .binaries + .iter() + .filter(|nb| bundles_platform(nb.platform)) + { + let dest = dir + .join("native") + .join(nb.platform.id()) + .join(ctx.binaries.bundled_filename(nb.platform)); + files.push(PackagedFile::copy(dest, nb.source.clone())); + } + Some(files) + } +} diff --git a/crates/weaveffi-gen/src/targets/dart/package.rs b/crates/weaveffi-gen/src/targets/dart/package.rs new file mode 100644 index 00000000..c1592558 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/package.rs @@ -0,0 +1,115 @@ +//! Package files: `pubspec.yaml` and the README for the generated and +//! packaged layouts. + +use crate::package::PackageContext; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::pkg::Identity; + +use crate::targets::dart::runtime::bundles_platform; + +/// The Dart SDK range the generated code needs: +/// `NativeCallable.isolateGroupBound` (callback interfaces) is verified +/// against 3.12. +pub(crate) const SDK_CONSTRAINT: &str = ">=3.12.0 <4.0.0"; + +/// Quote a YAML scalar. +fn yaml_str(s: &str) -> String { + format!("'{}'", s.replace('\'', "''")) +} + +/// Render `pubspec.yaml` for package `package`. +pub(crate) fn render_pubspec(identity: &Identity, package: &str, input_basename: &str) -> String { + let mut out = render_prelude(CommentStyle::Hash, input_basename); + out.push_str(&format!("name: {package}\n")); + out.push_str(&format!("version: {}\n", identity.version)); + out.push_str(&format!( + "description: {}\n", + yaml_str(&identity.description_or_default()) + )); + if let Some(homepage) = &identity.homepage { + out.push_str(&format!("homepage: {}\n", yaml_str(homepage))); + } + if let Some(repository) = &identity.repository { + out.push_str(&format!("repository: {}\n", yaml_str(repository))); + } + out.push_str(&format!( + "\nenvironment:\n sdk: '{SDK_CONSTRAINT}'\n\ndependencies:\n ffi: ^2.1.0\n\n" + )); + out.push_str(&render_trailer(CommentStyle::Hash, "pubspec.yaml")); + out +} + +/// Render the README of the generated package. +pub(crate) fn render_readme(identity: &Identity, package: &str, input_basename: &str) -> String { + let (macos, linux, windows) = identity.library_files(); + let env = identity.library_env_var(); + let name = &identity.name; + format!( + "{prelude}# {name} (Dart) + +`dart:ffi` bindings for the `{name}` native library. + +## Usage + +Add this package as a dependency (a `path:` dependency works) and import it: + +```dart +import 'package:{package}/{package}.dart'; +``` + +The library loads `{macos}` (macOS), `{linux}` (Linux and Android), or +`{windows}` (Windows) from the platform's search path on first use. Set +`{env}` to a full path to load a specific build. On iOS, and as a macOS +fallback, the bindings look the symbols up in the running executable. + +## Objects and callbacks + +Each interface wrapper holds one native reference: call `dispose()` when +you're done, or let the garbage collector's finalizer release it. + +Callback-interface methods that return a value run synchronously on the +isolate's thread; void methods may be called from any thread and are +delivered on the event loop. + +## Requirements + +- Dart SDK `{SDK_CONSTRAINT}` +- The `ffi` package + +{trailer}", + prelude = render_prelude(CommentStyle::Xml, input_basename), + trailer = render_trailer(CommentStyle::Xml, "README.md"), + ) +} + +/// Render the README of a packaged library that bundles native binaries. +pub(crate) fn render_packaged_readme( + identity: &Identity, + ctx: &PackageContext, + input_basename: &str, +) -> String { + let platforms: Vec = ctx + .binaries + .platforms() + .filter(|p| bundles_platform(*p)) + .map(|p| format!("- `native/{}/`", p.id())) + .collect(); + format!( + "{prelude}# {name} (Dart) + +`dart:ffi` bindings with prebuilt native libraries bundled under +`native//`. The loader tries the bundled library (relative to the +working directory) before the system search path; `{env}` overrides both. + +## Bundled platforms + +{platforms} + +{trailer}", + prelude = render_prelude(CommentStyle::Xml, input_basename), + name = identity.name, + env = identity.library_env_var(), + platforms = platforms.join("\n"), + trailer = render_trailer(CommentStyle::Xml, "README.md"), + ) +} diff --git a/crates/weaveffi-gen/src/targets/dart/runtime.rs b/crates/weaveffi-gen/src/targets/dart/runtime.rs new file mode 100644 index 00000000..d4deb6f4 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime.rs @@ -0,0 +1,133 @@ +//! The fixed Dart runtime every generated library ships, kept as real Dart +//! source under `runtime/` and spliced in with `{{PLACEHOLDER}}` +//! substitution: the loader and load-time contract check, the error and +//! memory helpers, the object base class, the value-buffer codec, async and +//! cancellation glue, the callback-interface machinery, and the iterator +//! anchor. Sections an API doesn't use are left out. + +use crate::platform::{Os, Platform}; +use weaveffi_model::model::{checksum_symbol, BindingModel, ABI_VERSION}; +use weaveffi_model::pkg::Identity; + +use crate::targets::dart::types::dart_str_literal; + +const LOADER: &str = include_str!("runtime/loader.dart"); +const CORE: &str = include_str!("runtime/core.dart"); +const OBJECT: &str = include_str!("runtime/object.dart"); +const CODEC: &str = include_str!("runtime/codec.dart"); +const ASYNC: &str = include_str!("runtime/async.dart"); +const CANCEL: &str = include_str!("runtime/cancel.dart"); +const CALLBACKS: &str = include_str!("runtime/callbacks.dart"); +const ITERATOR: &str = include_str!("runtime/iterator.dart"); + +/// The library file names the loader tries, per OS family, in order. +pub(crate) struct LoaderCandidates { + /// macOS candidates. + pub(crate) macos: Vec, + /// Windows candidates. + pub(crate) windows: Vec, + /// Linux, Android, and every other POSIX candidate. + pub(crate) linux: Vec, +} + +impl LoaderCandidates { + /// The platform file names of `library`, found on the system search + /// path. + pub(crate) fn system(identity: &Identity) -> Self { + let (macos, linux, windows) = identity.library_files(); + Self { + macos: vec![macos], + windows: vec![windows], + linux: vec![linux], + } + } + + /// The bundled `native//` copies of `lib` (relative to the + /// working directory) for every desktop platform of each OS family, then + /// the system file name. + pub(crate) fn bundled(lib: &str) -> Self { + let family = |os: Os| { + let mut names: Vec = Platform::DESKTOP + .iter() + .filter(|p| p.os() == os) + .map(|p| format!("native/{}/{}", p.id(), p.lib_filename(lib))) + .collect(); + if let Some(p) = Platform::DESKTOP.iter().find(|p| p.os() == os) { + names.push(p.lib_filename(lib)); + } + names + }; + Self { + macos: family(Os::MacOs), + windows: family(Os::Windows), + linux: family(Os::Linux), + } + } +} + +/// Whether the packaged library bundles a prebuilt binary for `platform`: +/// the loader resolves `native//` for the desktop matrix only +/// (Android libraries ship through the app's own packaging, and a `.wasm` +/// module can't be opened with `DynamicLibrary`). +pub(crate) fn bundles_platform(platform: Platform) -> bool { + platform.is_desktop() +} + +/// Substitute the placeholders every runtime file may use. +fn splice(source: &str, identity: &Identity) -> String { + source + .replace("{{PREFIX}}", &identity.prefix) + .replace("{{LIBRARY_ENV}}", &identity.library_env_var()) + .replace("{{LIBRARY}}", &dart_str_literal(&identity.library)) + .replace("{{NAME}}", &dart_str_literal(&identity.name)) +} + +/// Render the runtime sections `model` needs, with `loader` choosing where +/// the library is looked for. +pub(crate) fn render_runtime( + out: &mut String, + identity: &Identity, + model: &BindingModel, + loader: &LoaderCandidates, +) { + let quote = |names: &[String]| { + names + .iter() + .map(|n| format!("'{}'", dart_str_literal(n))) + .collect::>() + .join(", ") + }; + let contracts: String = model + .roots() + .map(|m| { + format!( + " ('{}', '{}', 0x{:016x}),\n", + m.name, + checksum_symbol(&model.prefix, &m.name), + m.checksum.expect("top-level modules carry a checksum") + ) + }) + .collect(); + let loader_src = LOADER + .replace("{{MACOS_CANDIDATES}}", "e(&loader.macos)) + .replace("{{WINDOWS_CANDIDATES}}", "e(&loader.windows)) + .replace("{{LINUX_CANDIDATES}}", "e(&loader.linux)) + .replace("{{ABI_VERSION}}", &ABI_VERSION.to_string()) + .replace("{{CONTRACTS}}", &contracts); + + let cancellable = model.callables().any(|(_, f)| f.cancellable); + let sections = [ + (true, loader_src.as_str()), + (true, CORE), + (model.has_interfaces(), OBJECT), + (model.has_buffers(), CODEC), + (model.has_async(), ASYNC), + (cancellable, CANCEL), + (model.has_callback_interfaces(), CALLBACKS), + (model.has_iterators(), ITERATOR), + ]; + for (_, source) in sections.iter().filter(|(used, _)| *used) { + out.push('\n'); + out.push_str(&splice(source, identity)); + } +} diff --git a/crates/weaveffi-gen/src/targets/dart/runtime/async.dart b/crates/weaveffi-gen/src/targets/dart/runtime/async.dart new file mode 100644 index 00000000..72917a15 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime/async.dart @@ -0,0 +1,16 @@ +// ── Async completion ── +// An async call completes through a `NativeCallable.listener` the producer +// invokes exactly once, from any thread. Everything the completion carries is +// owned by these bindings, so it's decoded later on the event loop and then +// released. + +final _errorFree = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + '{{PREFIX}}_error_free'); + +/// Builds the exception a completion reports and frees its boxed error. +NativeException _takeAsyncError(Pointer<_Error> err, _ErrorMapper map) { + final error = _readError(err, map); + _errorFree(err); + return error; +} diff --git a/crates/weaveffi-gen/src/targets/dart/runtime/callbacks.dart b/crates/weaveffi-gen/src/targets/dart/runtime/callbacks.dart new file mode 100644 index 00000000..88bfca02 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime/callbacks.dart @@ -0,0 +1,236 @@ +// ── Callback interfaces ── +// An implementation passed to the producer is parked in [_callbacks] under +// an integer key. The `ctx` the producer receives points at a small native +// [_CallbackContext] holding that key, so the producer never holds a Dart +// object and the GC never sees a raw pointer. The entry lives until the +// producer calls the vtable's `free`. +// +// Methods that return a value are `NativeCallable.isolateLocal` trampolines: +// they run synchronously, so the producer must call them on this isolate's +// thread while a call from Dart is in progress. Void methods and `free` are +// `NativeCallable.isolateGroupBound` forwarders, which may run on any thread: +// they copy their arguments into a message for this isolate (borrowed bytes +// are copied before the forwarder returns) and the implementation runs later +// on the event loop, in the zone that passed the callback. + +/// Where forwarders send messages: the allocator they build a message with +/// and the isolate port that receives it. Forwarders run outside the +/// isolate and can't read Dart globals, so everything they need is native. +final class _CallbackPost extends Struct { + external Pointer Function(Size)>> alloc; + external Pointer)>> release; + external Pointer)>> post; + @Int64() + external int port; +} + +/// The native `ctx` of one registered implementation. +final class _CallbackContext extends Struct { + @Int64() + external int key; + external Pointer<_CallbackPost> post; +} + +// The `Dart_CObject` layout `Dart_PostCObject` reads. +final class _CArray extends Struct { + @IntPtr() + external int length; + external Pointer> values; +} + +final class _CTypedData extends Struct { + @Int32() + external int type; + @IntPtr() + external int length; + external Pointer values; +} + +final class _CValue extends Union { + @Bool() + external bool asBool; + @Int64() + external int asInt64; + @Double() + external double asDouble; + external _CArray asArray; + external _CTypedData asTypedData; +} + +final class _CObject extends Struct { + @Int32() + external int type; + external _CValue value; +} + +typedef _CallbackDispatch = void Function( + Object impl, int method, List message); + +final class _CallbackEntry { + _CallbackEntry(this.impl, this.dispatch, this.context) : zone = Zone.current; + + final Object impl; + final _CallbackDispatch dispatch; + final Pointer<_CallbackContext> context; + final Zone zone; +} + +final Map _callbacks = {}; +int _nextCallbackKey = 1; + +/// Registers [impl] and returns the `ctx` the producer receives. +Pointer _registerCallback(Object impl, _CallbackDispatch dispatch) { + final key = _nextCallbackKey++; + final context = calloc<_CallbackContext>(); + context.ref + ..key = key + ..post = _callbackPost; + _callbacks[key] = _CallbackEntry(impl, dispatch, context); + return context.cast(); +} + +/// The implementation registered under [ctx]. +Object _callbackTarget(Pointer ctx) { + final key = ctx.cast<_CallbackContext>().ref.key; + final entry = _callbacks[key]; + if (entry == null) { + throw StateError('callback context $key is not registered'); + } + return entry.impl; +} + +final Pointer<_CallbackPost> _callbackPost = () { + final port = RawReceivePort(_receiveCallbackMessage, 'callback messages'); + port.keepIsolateAlive = false; + _callbackPort = port; + final allocator = Platform.isWindows + ? DynamicLibrary.open('ole32.dll') + : DynamicLibrary.process(); + final post = calloc<_CallbackPost>(); + post.ref + ..alloc = allocator.lookup(Platform.isWindows ? 'CoTaskMemAlloc' : 'malloc') + ..release = allocator.lookup(Platform.isWindows ? 'CoTaskMemFree' : 'free') + ..post = NativeApi.postCObject.cast() + ..port = port.sendPort.nativePort; + return post; +}(); + +RawReceivePort? _callbackPort; + +void _receiveCallbackMessage(Object? message) { + final fields = message! as List; + final key = fields[0]! as int; + final method = fields[1]! as int; + if (method == _CallbackMessage.free) { + final entry = _callbacks.remove(key); + if (entry != null) calloc.free(entry.context); + return; + } + final entry = _callbacks[key]; + if (entry == null) return; + entry.zone.runGuarded(() => entry.dispatch(entry.impl, method, fields)); +} + +/// One message from a forwarder: `[key, method, arguments...]`, built in +/// native memory and posted with `Dart_PostCObject`, which copies it. Runs +/// outside the isolate, so it touches no Dart globals. +final class _CallbackMessage { + _CallbackMessage(Pointer ctx, int method, int arguments) + : _context = ctx.cast<_CallbackContext>().ref, + _capacity = arguments + 2 { + final post = _context.post.ref; + final alloc = post.alloc.asFunction Function(int)>(); + _objects = alloc(sizeOf<_CObject>() * (_capacity + 1)).cast(); + _slots = alloc(sizeOf>() * _capacity).cast(); + int64(_context.key); + int64(method); + } + + static const int free = -1; + static const int _kBool = 1; + static const int _kInt64 = 3; + static const int _kDouble = 4; + static const int _kArray = 6; + static const int _kTypedData = 7; + static const int _kUint8 = 2; + + final _CallbackContext _context; + final int _capacity; + late final Pointer<_CObject> _objects; + late final Pointer> _slots; + int _count = 0; + + _CObject _next(int type) { + final object = _objects + (_count + 1); + _slots[_count++] = object; + return object.ref..type = type; + } + + void boolean(bool v) => _next(_kBool).value.asBool = v; + + void int64(int v) => _next(_kInt64).value.asInt64 = v; + + void float64(double v) => _next(_kDouble).value.asDouble = v; + + void pointer(Pointer v) => int64(v.address); + + /// A borrowed byte run; posting copies it. + void bytes(Pointer ptr, int len) { + final data = _next(_kTypedData).value.asTypedData; + data + ..type = _kUint8 + ..length = ptr == nullptr ? 0 : len + ..values = ptr; + } + + void send() { + final post = _context.post.ref; + _objects.ref.type = _kArray; + _objects.ref.value.asArray + ..length = _count + ..values = _slots; + post.post.asFunction)>()( + post.port, _objects); + final release = post.release.asFunction)>(); + release(_slots.cast()); + release(_objects.cast()); + } +} + +// The vtable `free` entry every callback interface shares. +void _forwardCallbackFree(Pointer ctx) => + _CallbackMessage(ctx, _CallbackMessage.free, 0).send(); + +final Pointer)>> _callbackFree = + _pin(NativeCallable)>.isolateGroupBound( + _forwardCallbackFree)); + +// Vtable entries live for the process: they're never closed, so they're +// anchored here, and they don't keep an idle isolate alive. +final List> _pinned = >[]; + +Pointer> _pin(NativeCallable callable) { + callable.keepIsolateAlive = false; + _pinned.add(callable); + return callable.nativeFunction; +} + +final _errorSet = _lib.lookupFunction< + Void Function(Pointer<_Error>, Int32, Pointer), + void Function(Pointer<_Error>, int, Pointer)>('{{PREFIX}}_error_set'); + +/// Reports an implementation's exception to the producer through +/// `{{PREFIX}}_error_set` with the foreign code (-4). Never throws: it runs +/// in the catch path of a trampoline the exception must not unwind through. +void _foreignError(Pointer<_Error> outErr, Object error) { + if (outErr == nullptr) return; + Pointer message = nullptr; + try { + message = error.toString().toNativeUtf8(allocator: calloc); + _errorSet(outErr, NativeException.foreignCode, message); + } catch (_) { + _errorSet(outErr, NativeException.foreignCode, nullptr); + } finally { + if (message != nullptr) calloc.free(message); + } +} diff --git a/crates/weaveffi-gen/src/targets/dart/runtime/cancel.dart b/crates/weaveffi-gen/src/targets/dart/runtime/cancel.dart new file mode 100644 index 00000000..0e67d04f --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime/cancel.dart @@ -0,0 +1,63 @@ +// ── Cancellation ── + +/// Cancels the cancellable calls it's passed to. One token can serve any +/// number of calls; once cancelled it stays cancelled, and a call that +/// receives a cancelled token completes with [CancelledException] without +/// waiting for its work. +final class CancelToken { + bool _cancelled = false; + final List _listeners = []; + + /// Whether [cancel] has been called. + bool get isCancelled => _cancelled; + + /// Requests cancellation of every call this token was passed to. Calls + /// that already completed are unaffected. Idempotent. + void cancel() { + if (_cancelled) return; + _cancelled = true; + final listeners = List.of(_listeners); + _listeners.clear(); + for (final listener in listeners) { + listener(); + } + } +} + +final _cancelTokenCreate = + _lib.lookupFunction Function(), Pointer Function()>( + '{{PREFIX}}_cancel_token_create'); +final _cancelTokenCancel = _lib.lookupFunction), + void Function(Pointer)>('{{PREFIX}}_cancel_token_cancel'); +final _cancelTokenDestroy = _lib.lookupFunction), + void Function(Pointer)>('{{PREFIX}}_cancel_token_destroy'); + +/// The native token behind one launch: created for the call, cancelled when +/// the [CancelToken] is, and destroyed once the call completes. The producer +/// holds its own reference, so destroying ours never races the call. +final class _NativeCancel { + _NativeCancel._(this._token) : pointer = _cancelTokenCreate() { + if (_token.isCancelled) { + _cancelTokenCancel(pointer); + } else { + _token._listeners.add(_cancel); + } + } + + /// Binds a native token to [token], or returns null for no token. + static _NativeCancel? bind(CancelToken? token) => + token == null ? null : _NativeCancel._(token); + + final CancelToken _token; + + /// The `{{PREFIX}}_cancel_token*` passed to the launcher. + final Pointer pointer; + + void _cancel() => _cancelTokenCancel(pointer); + + /// Detaches from the [CancelToken] and drops the native reference. + void release() { + _token._listeners.remove(_cancel); + _cancelTokenDestroy(pointer); + } +} diff --git a/crates/weaveffi-gen/src/targets/dart/runtime/codec.dart b/crates/weaveffi-gen/src/targets/dart/runtime/codec.dart new file mode 100644 index 00000000..f58f6e9f --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime/codec.dart @@ -0,0 +1,179 @@ +// ── Value buffers ── +// Records, rich enums, optionals, lists, maps, and error payloads cross the +// C ABI serialized in this little-endian, packed format. A malformed buffer +// is a contract violation between producer and bindings, never a domain +// error. + +Never _bufferError(String context) => + throw StateError('malformed value buffer: $context'); + +final class _BufferWriter { + Uint8List _buf = Uint8List(64); + late ByteData _data = ByteData.sublistView(_buf); + int _len = 0; + + void _reserve(int extra) { + if (_len + extra <= _buf.length) return; + var cap = _buf.length * 2; + while (cap < _len + extra) { + cap *= 2; + } + _buf = Uint8List(cap)..setRange(0, _len, _buf); + _data = ByteData.sublistView(_buf); + } + + Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); + + void writeBool(bool v) => writeU8(v ? 1 : 0); + + void writeFlag(bool present) => writeU8(present ? 1 : 0); + + void writeI8(int v) { + _reserve(1); + _data.setInt8(_len, v); + _len += 1; + } + + void writeU8(int v) { + _reserve(1); + _data.setUint8(_len, v); + _len += 1; + } + + void writeI16(int v) { + _reserve(2); + _data.setInt16(_len, v, Endian.little); + _len += 2; + } + + void writeU16(int v) { + _reserve(2); + _data.setUint16(_len, v, Endian.little); + _len += 2; + } + + void writeI32(int v) { + _reserve(4); + _data.setInt32(_len, v, Endian.little); + _len += 4; + } + + void writeU32(int v) { + _reserve(4); + _data.setUint32(_len, v, Endian.little); + _len += 4; + } + + void writeI64(int v) { + _reserve(8); + _data.setInt64(_len, v, Endian.little); + _len += 8; + } + + // Dart ints are signed 64-bit; a u64 travels as its bit pattern. + void writeU64(int v) => writeI64(v); + + void writeF32(double v) { + _reserve(4); + _data.setFloat32(_len, v, Endian.little); + _len += 4; + } + + void writeF64(double v) { + _reserve(8); + _data.setFloat64(_len, v, Endian.little); + _len += 8; + } + + void writeLength(int v) => writeU32(v); + + void writeString(String v) => writeBytes(utf8.encode(v)); + + void writeBytes(List v) { + writeLength(v.length); + _reserve(v.length); + _buf.setRange(_len, _len + v.length, v); + _len += v.length; + } +} + +final class _BufferReader { + _BufferReader(this._buf) : _data = ByteData.sublistView(_buf); + + final Uint8List _buf; + final ByteData _data; + int _pos = 0; + + int _take(int n, String context) { + if (_buf.length - _pos < n) _bufferError('truncated $context'); + final at = _pos; + _pos += n; + return at; + } + + bool readBool() { + final b = _buf[_take(1, 'bool')]; + if (b > 1) _bufferError('bool byte $b'); + return b == 1; + } + + bool readFlag() { + final b = _buf[_take(1, 'option flag')]; + if (b > 1) _bufferError('option flag $b'); + return b == 1; + } + + int readI8() => _data.getInt8(_take(1, 'i8')); + + int readU8() => _buf[_take(1, 'u8')]; + + int readI16() => _data.getInt16(_take(2, 'i16'), Endian.little); + + int readU16() => _data.getUint16(_take(2, 'u16'), Endian.little); + + int readI32() => _data.getInt32(_take(4, 'i32'), Endian.little); + + int readU32() => _data.getUint32(_take(4, 'u32'), Endian.little); + + int readI64() => _data.getInt64(_take(8, 'i64'), Endian.little); + + // Dart ints are signed 64-bit; a u64 arrives as its bit pattern. + int readU64() => readI64(); + + double readF32() => _data.getFloat32(_take(4, 'f32'), Endian.little); + + double readF64() => _data.getFloat64(_take(8, 'f64'), Endian.little); + + int readLength() => readU32(); + + String readString() { + final n = readLength(); + final at = _take(n, 'string'); + return utf8.decode(Uint8List.sublistView(_buf, at, at + n)); + } + + Uint8List readBytes() { + final n = readLength(); + final at = _take(n, 'bytes'); + return Uint8List.fromList(Uint8List.sublistView(_buf, at, at + n)); + } + + void expectEnd() { + if (_pos != _buf.length) _bufferError('trailing bytes'); + } +} + +/// Encodes one value with [write] into a fresh buffer. +Uint8List _encode(T value, void Function(_BufferWriter, T) write) { + final writer = _BufferWriter(); + write(writer, value); + return writer.takeBytes(); +} + +/// Decodes exactly one value with [read] from [bytes]. +T _decode(Uint8List bytes, T Function(_BufferReader) read) { + final reader = _BufferReader(bytes); + final value = read(reader); + reader.expectEnd(); + return value; +} diff --git a/crates/weaveffi-gen/src/targets/dart/runtime/core.dart b/crates/weaveffi-gen/src/targets/dart/runtime/core.dart new file mode 100644 index 00000000..5bb37d74 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime/core.dart @@ -0,0 +1,123 @@ +// ── Errors and memory ── + +/// The C `{{PREFIX}}_error` slot every fallible call writes. A non-zero +/// `code` carries a producer-owned `message` and, for a domain error with +/// fields, a value-buffer `payload`; `{{PREFIX}}_error_clear` releases both. +final class _Error extends Struct { + @Int32() + external int code; + external Pointer message; + external Pointer payloadPtr; + @Size() + external int payloadLen; +} + +final _errorClear = + _lib.lookupFunction), void Function(Pointer<_Error>)>( + '{{PREFIX}}_error_clear', + isLeaf: true); + +final _freeBytes = _lib.lookupFunction, Size), + void Function(Pointer, int)>('{{PREFIX}}_free_bytes', isLeaf: true); + +// Synchronous calls on this isolate share one error slot and one length +// slot: each call reads (and clears) them right after it returns, before any +// other call can reuse them, including a nested call from a callback. +final Pointer<_Error> _err = calloc<_Error>(); +final Pointer _outLen = calloc(); + +/// A failure the native library reported. Domain errors extend this class +/// with a positive [code]; the negative runtime codes are the constants +/// below. +class NativeException implements Exception { + /// Creates an exception carrying [code] and [message]. + NativeException(this.code, this.message); + + /// The producer reported an untyped error. + static const int genericCode = -1; + + /// The producer panicked; [message] carries the panic text. + static const int panicCode = -2; + + /// An argument couldn't be lifted by the producer. + static const int marshalCode = -3; + + /// A callback implementation threw; [message] carries its text. + static const int foreignCode = -4; + + /// The call was cancelled (see [CancelledException]). + static const int cancelledCode = -5; + + /// The error code: positive for a domain error, negative for a runtime + /// failure. + final int code; + + /// The producer's message. + final String message; + + @override + String toString() => '$runtimeType($code): $message'; +} + +/// A cancellable call completed because its token was cancelled. +class CancelledException extends NativeException { + /// Creates a cancellation with the producer's [message]. + CancelledException([String message = 'cancelled']) + : super(NativeException.cancelledCode, message); +} + +typedef _ErrorMapper = NativeException Function( + int code, String message, Uint8List payload); + +/// Maps a runtime failure (or an undeclared code) onto its exception. +NativeException _runtimeError(int code, String message, Uint8List payload) => + code == NativeException.cancelledCode + ? CancelledException(message) + : NativeException(code, message); + +/// Builds the exception [err] reports, using [map] for domain codes. +NativeException _readError(Pointer<_Error> err, _ErrorMapper map) { + final e = err.ref; + final message = e.message == nullptr ? '' : e.message.toDartString(); + return map(e.code, message, _copyBytes(e.payloadPtr, e.payloadLen)); +} + +/// Throws the error a synchronous call left in [err], clearing the slot. +void _check(Pointer<_Error> err, [_ErrorMapper map = _runtimeError]) { + if (err.ref.code == 0) return; + final error = _readError(err, map); + _errorClear(err); + throw error; +} + +/// Copies a borrowed native byte run into Dart memory. +Uint8List _copyBytes(Pointer ptr, int len) => + ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); + +/// Copies an owned native byte run and releases it. +Uint8List _takeBytes(Pointer ptr, int len) { + if (ptr == nullptr) return Uint8List(0); + final bytes = Uint8List.fromList(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return bytes; +} + +/// Decodes an owned UTF-8 run and releases it. +String _takeString(Pointer ptr, int len) { + if (ptr == nullptr) return ''; + final text = utf8.decode(ptr.asTypedList(len)); + _freeBytes(ptr, len); + return text; +} + +/// Decodes a borrowed UTF-8 run. +String _readString(Pointer ptr, int len) => + ptr == nullptr ? '' : utf8.decode(ptr.asTypedList(len)); + +/// Copies [bytes] into [arena] memory for a borrowed `(ptr, len)` argument. +Pointer _stage(Arena arena, List bytes) { + if (bytes.isEmpty) return nullptr; + final ptr = arena(bytes.length); + ptr.asTypedList(bytes.length).setAll(0, bytes); + return ptr; +} diff --git a/crates/weaveffi-gen/src/targets/dart/runtime/iterator.dart b/crates/weaveffi-gen/src/targets/dart/runtime/iterator.dart new file mode 100644 index 00000000..1c77bbfa --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime/iterator.dart @@ -0,0 +1,11 @@ +// ── Iterators ── + +/// Anchors one live native iterator for its GC-finalizer backstop. A +/// suspended `sync*` body keeps its anchor reachable; abandoning the +/// iteration drops the body, and the finalizer destroys the native iterator. +/// An iteration that runs to completion (or fails) detaches and destroys it +/// eagerly instead, so the iterator is destroyed exactly once either way. +final class _IteratorAnchor implements Finalizable {} + +/// The `out_item` slot iterator steps write, wide enough for any element. +final Pointer _outItem = calloc(); diff --git a/crates/weaveffi-gen/src/targets/dart/runtime/loader.dart b/crates/weaveffi-gen/src/targets/dart/runtime/loader.dart new file mode 100644 index 00000000..1099a61c --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime/loader.dart @@ -0,0 +1,74 @@ +// ── Library loading ── +// The native library opens on first use and is verified before any other +// lookup: its C ABI revision and every module's contract checksum must match +// the values these bindings were generated against. + +/// The native library every binding in this file calls. +final DynamicLibrary _lib = _verifyLibrary(_openLibrary()); + +DynamicLibrary _openLibrary() { + // An explicit path wins, so a caller can point at any build artifact. + final override = Platform.environment['{{LIBRARY_ENV}}']; + if (override != null && override.isNotEmpty) { + return DynamicLibrary.open(override); + } + // iOS links native code into the app binary. + if (Platform.isIOS) return DynamicLibrary.process(); + final candidates = Platform.isMacOS + ? const [{{MACOS_CANDIDATES}}] + : Platform.isWindows + ? const [{{WINDOWS_CANDIDATES}}] + : const [{{LINUX_CANDIDATES}}]; + ArgumentError? failure; + for (final candidate in candidates) { + try { + return DynamicLibrary.open(candidate); + } on ArgumentError catch (e) { + failure ??= e; + } + } + // A macOS app may link the library into its executable instead. + if (Platform.isMacOS) return DynamicLibrary.process(); + throw failure!; +} + +/// The C ABI revision these bindings were generated against. +const int _abiVersion = {{ABI_VERSION}}; + +/// Each top-level module's name, checksum symbol, and expected checksum. +const List<(String, String, int)> _contracts = <(String, String, int)>[ +{{CONTRACTS}}]; + +DynamicLibrary _verifyLibrary(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction( + '{{PREFIX}}_abi_version'); + } on ArgumentError { + throw StateError( + "the native library '{{LIBRARY}}' isn't loaded; set {{LIBRARY_ENV}} to " + 'its path'); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError('{{NAME}}: these bindings expect C ABI revision ' + '$_abiVersion, but the loaded library reports revision $found'); + } + for (final (module, symbol, expected) in _contracts) { + final int actual; + try { + actual = lib.lookupFunction(symbol)(); + } on ArgumentError { + throw StateError( + "{{NAME}}: the loaded library doesn't export module '$module'"); + } + if (actual != expected) { + throw StateError("{{NAME}}: module '$module' doesn't match the loaded " + 'library (contract checksum ${_hex(actual)}, expected ' + '${_hex(expected)}); regenerate the bindings'); + } + } + return lib; +} + +String _hex(int v) => '0x${v.toUnsigned(64).toRadixString(16).padLeft(16, '0')}'; diff --git a/crates/weaveffi-gen/src/targets/dart/runtime/object.dart b/crates/weaveffi-gen/src/targets/dart/runtime/object.dart new file mode 100644 index 00000000..36fe1e04 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/runtime/object.dart @@ -0,0 +1,65 @@ +// ── Objects ── + +/// The lifetime machinery every interface wrapper shares. A wrapper owns one +/// strong reference to its native object and releases it exactly once: in +/// [dispose], or through a GC finalizer when it's collected undisposed. +/// +/// Every call borrows the pointer through [_enter] and [_leave]. A [dispose] +/// that arrives while a call is in flight (from a callback the call made) +/// only marks the wrapper; the reference is released when the last call +/// leaves, so the native object never disappears under a running call. +abstract base class _NativeObject implements Finalizable { + _NativeObject(this._ptr) { + _finalizer.attach(this, _ptr, detach: this); + } + + final Pointer _ptr; + int _calls = 0; + bool _disposed = false; + + NativeFinalizer get _finalizer; + void _destroy(Pointer ptr); + Pointer _clone(Pointer ptr); + + /// Releases this wrapper's native reference. Safe to call more than once; + /// the native object is dropped when its last reference (this one, another + /// wrapper's, or the producer's) goes away. Using the wrapper afterwards + /// throws a [StateError]. + void dispose() { + if (_disposed) return; + _disposed = true; + if (_calls == 0) _release(); + } + + void _release() { + _finalizer.detach(this); + _destroy(_ptr); + } + + Pointer _enter() { + if (_disposed) throw StateError('$runtimeType used after dispose()'); + _calls++; + return _ptr; + } + + void _leave() { + if (--_calls == 0 && _disposed) _release(); + } + + /// A second strong reference, for an object token in a value buffer. + Pointer _cloneRef() { + final ptr = _enter(); + try { + return _clone(ptr); + } finally { + _leave(); + } + } +} + +/// Borrows [object]'s pointer until [arena] is released. +Pointer _borrow(Arena arena, _NativeObject object) { + final ptr = object._enter(); + arena.onReleaseAll(object._leave); + return ptr; +} diff --git a/crates/weaveffi-gen/src/targets/dart/types.rs b/crates/weaveffi-gen/src/targets/dart/types.rs new file mode 100644 index 00000000..2eb595ed --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dart/types.rs @@ -0,0 +1,238 @@ +//! Dart type mapping and naming: the `dart:ffi` vocabulary for C ABI slots, +//! the surface types of wrapper signatures, and the identifier policy applied +//! to user-chosen IDL names before they land in generated Dart. + +use crate::lang; +use crate::utils::{local_type_name, wrapper_name}; +use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use weaveffi_model::abi::CType; +use weaveffi_model::model::Ty; + +/// Type names the generated library declares or uses unqualified. A user +/// type with one of these names would shadow or collide with it, so it gains +/// a trailing `_` like a keyword. +const RESERVED_TYPES: &[&str] = &[ + "Arena", + "ArgumentError", + "Bool", + "ByteData", + "CancelToken", + "CancelledException", + "Completer", + "Double", + "DynamicLibrary", + "Endian", + "Exception", + "Finalizable", + "Float", + "Function", + "Future", + "Int16", + "Int32", + "Int64", + "Int8", + "IntPtr", + "Iterable", + "List", + "Map", + "NativeApi", + "NativeCallable", + "NativeException", + "NativeFinalizer", + "NativeFunction", + "Never", + "Object", + "Platform", + "Pointer", + "RawReceivePort", + "Size", + "StateError", + "String", + "Struct", + "Uint16", + "Uint32", + "Uint64", + "Uint8", + "Uint8List", + "Union", + "Utf8", + "Void", + "Zone", +]; + +/// The Dart spelling of an IDL value identifier (parameter, field, method): +/// lowerCamelCase, then keyword-escaped (`class` becomes `class_`). The +/// result never starts with `_`, so generated locals that do can't collide +/// with it. +pub(crate) fn dart_ident(name: &str) -> String { + lang::escape_ident(&name.to_lower_camel_case(), lang::DART_KEYWORDS) +} + +/// The Dart spelling of a module-level function: the module path applied per +/// `strip_module_prefix`, then lowerCamelCase and keyword escaping. +pub(crate) fn dart_fn_name(module_path: &str, name: &str, strip_module_prefix: bool) -> String { + dart_ident(&wrapper_name(module_path, name, strip_module_prefix)) +} + +/// The Dart class of a (possibly dot-qualified) user type: its local name in +/// UpperCamelCase, escaped when it would collide with a reserved type. +pub(crate) fn dart_class(name: &str) -> String { + let class = local_type_name(name).to_upper_camel_case(); + if RESERVED_TYPES.binary_search(&class.as_str()).is_ok() { + format!("{class}_") + } else { + class + } +} + +/// The idiomatic Dart type a [`Ty`] surfaces as. `u64` values are carried as +/// their two's-complement bit pattern in a Dart `int`. +pub(crate) fn dart_type(ty: &Ty) -> String { + match ty { + Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::U64 => "int".into(), + Ty::F32 | Ty::F64 => "double".into(), + Ty::Bool => "bool".into(), + Ty::StringUtf8 => "String".into(), + Ty::Bytes => "List".into(), + Ty::Enum(n) + | Ty::Record(n) + | Ty::RichEnum(n) + | Ty::Interface(n) + | Ty::CallbackInterface(n) => dart_class(n), + Ty::Optional(inner) => format!("{}?", dart_type(inner)), + Ty::List(inner) => format!("List<{}>", dart_type(inner)), + Ty::Iterator(inner) => format!("Iterable<{}>", dart_type(inner)), + Ty::Map(k, v) => format!("Map<{}, {}>", dart_type(k), dart_type(v)), + } +} + +/// The wrapper class of a bare or optional interface type. +/// +/// # Panics +/// +/// Panics when `ty` names no interface; callers dispatch on the object +/// family first. +pub(crate) fn object_class(ty: &Ty) -> String { + dart_class( + ty.interface_name() + .expect("object positions are (optional) interfaces"), + ) +} + +/// The `dart:ffi` (native, Dart) type pair of one C ABI slot or return. +/// +/// Opaque pointees (objects, iterators, vtables, cancel tokens, `void*`) are +/// `Pointer`; byte runs are `Pointer`; the error slot is +/// `Pointer<_Error>`. A bare [`CType::Named`] (an async completion callback) +/// has no generic spelling, so callers substitute the callback typedef. +pub(crate) fn ffi_type(ct: &CType) -> (String, String) { + let scalar = |native: &str, dart: &str| (native.to_string(), dart.to_string()); + match ct { + CType::Int8 => scalar("Int8", "int"), + CType::Int16 => scalar("Int16", "int"), + CType::Int32 | CType::Enum { .. } => scalar("Int32", "int"), + CType::Int64 => scalar("Int64", "int"), + CType::Uint8 => scalar("Uint8", "int"), + CType::Uint16 => scalar("Uint16", "int"), + CType::Uint32 => scalar("Uint32", "int"), + CType::Uint64 => scalar("Uint64", "int"), + CType::Size => scalar("Size", "int"), + CType::Float => scalar("Float", "double"), + CType::Double => scalar("Double", "double"), + CType::Bool => scalar("Bool", "bool"), + CType::Void => scalar("Void", "void"), + CType::Ptr { pointee, .. } => { + let ptr = format!("Pointer<{}>", pointee_type(pointee)); + (ptr.clone(), ptr) + } + CType::Char + | CType::CancelToken + | CType::Error + | CType::StructTag { .. } + | CType::VtableTag { .. } + | CType::Named(_) => unreachable!("{ct:?} only appears behind a pointer"), + } +} + +/// The `dart:ffi` type a pointer slot points at. +fn pointee_type(ct: &CType) -> String { + match ct { + CType::Char => "Utf8".into(), + CType::Error => "_Error".into(), + CType::Void + | CType::CancelToken + | CType::StructTag { .. } + | CType::VtableTag { .. } + | CType::Named(_) => "Void".into(), + other => ffi_type(other).0, + } +} + +/// The `dart:ffi` type a pointer slot `ct` points at (`Int32` for an +/// `int32_t*` slot, `Pointer` for a `const uint8_t**` slot). +/// +/// # Panics +/// +/// Panics when `ct` is not a pointer. +pub(crate) fn pointee_ffi(ct: &CType) -> String { + let CType::Ptr { pointee, .. } = ct else { + unreachable!("{ct:?} is not a pointer slot") + }; + pointee_type(pointee) +} + +/// The private Dart variable holding the bound function of C symbol `sym`. +pub(crate) fn ffi_var(sym: &str) -> String { + format!("_{}", sym.to_lower_camel_case()) +} + +/// The private Dart typedef name derived from C name `c_name`. +pub(crate) fn ffi_typedef(c_name: &str) -> String { + format!("_{}", c_name.to_upper_camel_case()) +} + +/// The Dart literal a callback trampoline returns when the implementation +/// threw: the zero value of the method's direct return type. +pub(crate) fn default_literal(ty: &Ty) -> &'static str { + match ty { + Ty::Bool => "false", + Ty::F32 | Ty::F64 => "0.0", + _ => "0", + } +} + +/// Escape a string for a single-quoted Dart literal. +pub(crate) fn dart_str_literal(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('\'', "\\'") + .replace('$', "\\$") + .replace('\n', "\\n") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reserved_types_are_sorted_and_escaped() { + assert!(RESERVED_TYPES.windows(2).all(|w| w[0] < w[1])); + assert_eq!(dart_class("kv.Store"), "Store"); + assert_eq!(dart_class("CancelToken"), "CancelToken_"); + assert_eq!(dart_class("app.String"), "String_"); + } + + #[test] + fn pointers_map_to_opaque_or_byte_pointers() { + let ptr = |t| CType::ptr(t); + assert_eq!(ffi_type(&ptr(CType::Error)).0, "Pointer<_Error>"); + assert_eq!( + ffi_type(&ptr(ptr(CType::Uint8))).0, + "Pointer>" + ); + assert_eq!( + ffi_type(&ptr(CType::Named("kv_ScanIterator".into()))).0, + "Pointer" + ); + assert_eq!(ffi_type(&CType::Size), ("Size".into(), "int".into())); + } +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/callbacks.rs b/crates/weaveffi-gen/src/targets/dotnet/callbacks.rs new file mode 100644 index 00000000..b6f072f2 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/callbacks.rs @@ -0,0 +1,193 @@ +//! Callback-interface renderers: the consumer-facing C# `interface` plus the +//! process-wide vtable and its `[UnmanagedCallersOnly]` trampolines, +//! following the shared `CallbackProtocol`. +//! +//! * One static vtable per callback interface, allocated once in native +//! memory and never freed, so its address is stable for the process. +//! * `ctx` is a `GCHandle` to the implementation, released by the vtable's +//! `free` entry, so the implementation lives exactly as long as the +//! producer holds it. +//! * Trampoline arguments are received like returns, except that strings, +//! bytes, and buffers are borrowed (copied, never freed); objects are +//! adopted into wrappers the implementation owns. +//! * A thrown exception never unwinds into native code: the trampoline +//! reports it through `out_err` with the foreign error code and returns a +//! default value. Trampolines run on whatever thread the producer calls +//! from. + +use crate::codegen::CodeWriter; +use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ModuleBinding}; + +use crate::targets::dotnet::calls::{ + direct_to_slot, receive, write_obsolete, Own, UNMANAGED_CALLERS_ONLY, +}; +use crate::targets::dotnet::docs::{write_doc, write_fn_doc}; +use crate::targets::dotnet::types::{ + callback_interface_cs, cs_ctype, cs_type, fn_pointer_type, safe_cs_name, vtable_class_cs, Cx, +}; + +/// Render one callback interface: the `public interface I{Name}` the +/// consumer implements, then the internal class hosting its vtable. +pub(crate) fn render_callback_interface( + w: &mut CodeWriter, + module: &ModuleBinding, + cb: &CallbackInterfaceBinding, + cx: Cx<'_>, +) { + render_consumer_interface(w, cb, cx.base); + render_vtable_class(w, cx, module, cb); +} + +fn method_cs(m: &CallbackMethodBinding) -> String { + m.name.to_upper_camel_case() +} + +/// The consumer-facing interface: one method per callback method. +fn render_consumer_interface(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, base: &str) { + let iface = callback_interface_cs(&cb.name); + write_doc(w, &cb.doc); + w.line("/// The native library may call any method from any thread until"); + w.line("/// it releases its last reference to the implementation. Object arguments"); + w.line("/// belong to the implementation. An exception thrown by a method reaches"); + w.line(format!( + "/// the native caller as ." + )); + write_obsolete(w, &cb.deprecated); + w.line(format!("public interface {iface}")); + w.block("{", "}", |w| { + for m in &cb.methods { + let mut params = m.params.clone(); + for p in &mut params { + p.name = p.name.to_lower_camel_case(); + } + write_fn_doc(w, &m.doc, ¶ms); + write_obsolete(w, &m.deprecated); + let ret = m.ret.as_ref().map(cs_type).unwrap_or_else(|| "void".into()); + let sig: Vec = params + .iter() + .map(|p| format!("{} {}", cs_type(&p.ty), safe_cs_name(&p.name))) + .collect(); + w.line(format!("{ret} {}({});", method_cs(m), sig.join(", "))); + } + }); + w.blank(); +} + +/// The internal class owning the vtable (function pointers in declaration +/// order, then `free`) and the trampolines it points at. +fn render_vtable_class( + w: &mut CodeWriter, + cx: Cx<'_>, + module: &ModuleBinding, + cb: &CallbackInterfaceBinding, +) { + let iface = callback_interface_cs(&cb.name); + let class = vtable_class_cs(&module.path, &cb.name); + w.line(format!( + "/// The process-wide {} and the trampolines behind it,", + cb.vtable_tag + )); + w.line(format!( + "/// adapting an to the C ABI." + )); + w.line(format!("internal static unsafe class {class}")); + w.block("{", "}", |w| { + w.line("[StructLayout(LayoutKind.Sequential)]"); + w.line("private struct Layout"); + w.block("{", "}", |w| { + for m in &cb.methods { + w.line(format!( + "public {} {};", + fn_pointer_type(&m.abi_params, &m.abi_ret), + safe_cs_name(&m.name) + )); + } + w.line("public delegate* unmanaged[Cdecl] free;"); + }); + w.blank(); + w.line("/// The vtable's address, valid for the process lifetime."); + w.line("internal static readonly IntPtr Pointer = Allocate();"); + w.blank(); + w.line("private static IntPtr Allocate()"); + w.block("{", "}", |w| { + w.line("var vtable = (Layout*)NativeMemory.Alloc((nuint)sizeof(Layout));"); + for m in &cb.methods { + w.line(format!( + "vtable->{} = &{}Trampoline;", + safe_cs_name(&m.name), + method_cs(m) + )); + } + w.line("vtable->free = &FreeTrampoline;"); + w.line("return (IntPtr)vtable;"); + }); + w.blank(); + for m in &cb.methods { + render_trampoline(w, cx, &iface, m); + } + w.line(UNMANAGED_CALLERS_ONLY); + w.line("private static void FreeTrampoline(IntPtr ctx)"); + w.block("{", "}", |w| { + w.line("Ffi.Unregister(ctx);"); + }); + }); + w.blank(); +} + +/// One trampoline: receive the slots, call the implementation, and return +/// the direct result; any exception is reported through `out_err`. +fn render_trampoline(w: &mut CodeWriter, cx: Cx<'_>, iface: &str, m: &CallbackMethodBinding) { + let sig: Vec = m + .abi_params + .iter() + .map(|s| format!("{} {}", cs_ctype(&s.ty), safe_cs_name(&s.name))) + .collect(); + let ret = cs_ctype(&m.abi_ret); + w.line(UNMANAGED_CALLERS_ONLY); + w.line(format!( + "private static {ret} {}Trampoline({})", + method_cs(m), + sig.join(", ") + )); + w.block("{", "}", |w| { + w.line("try"); + w.block("{", "}", |w| { + let mut args = Vec::new(); + for (idx, p) in m.params.iter().enumerate() { + let ptr = safe_cs_name(&p.abi[0].name); + let len = p + .abi + .get(1) + .map(|s| safe_cs_name(&s.name)) + .unwrap_or_default(); + args.push(receive( + w, + cx, + &p.ty, + &ptr, + &len, + Own::Borrow, + &format!("arg{idx}"), + )); + } + let call = format!( + "Ffi.Target<{iface}>(ctx).{}({})", + method_cs(m), + args.join(", ") + ); + match &m.ret { + None => w.line(format!("{call};")), + Some(ty) => w.line(format!("return {};", direct_to_slot(ty, &call))), + }; + }); + w.line("catch (Exception e)"); + w.block("{", "}", |w| { + w.line("Ffi.SetForeignError(out_err, e);"); + if m.ret.is_some() { + w.line("return default;"); + } + }); + }); + w.blank(); +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/calls.rs b/crates/weaveffi-gen/src/targets/dotnet/calls.rs new file mode 100644 index 00000000..b5a668b3 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/calls.rs @@ -0,0 +1,748 @@ +//! Call-path renderers: the wrapper methods for every call shape (sync, +//! async, iterator), argument marshalling driven by [`ArgPass`], result +//! receiving driven by [`Family`], and the per-module static classes. +//! +//! Every wrapper follows one frame: encode arguments (UTF-8 strings, value +//! buffers, callback registrations), pin them with one `fixed` statement, +//! call the import with a stack `FfiError`, check it, and receive the result. + +use crate::codegen::CodeWriter; +use crate::utils::{local_type_name, wrapper_name}; +use heck::ToUpperCamelCase; +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::model::{ + AsyncBinding, CallShape, ErrorBinding, Family, FnBinding, IteratorBinding, ModuleBinding, + ParamBinding, Ty, +}; +use weaveffi_model::plan::{ArgPass, ErrorStrategy}; + +use crate::targets::dotnet::codec::{emit_decode, emit_write}; +use crate::targets::dotnet::docs::{write_doc, write_fn_doc}; +use crate::targets::dotnet::runtime::dotnet_exception_name; +use crate::targets::dotnet::types::{ + camel_fn, cs_ctype, cs_type, safe_cs_name, vtable_class_cs, Cx, +}; + +/// The `[UnmanagedCallersOnly]` attribute every native entry point carries. +pub(crate) const UNMANAGED_CALLERS_ONLY: &str = + "[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]"; + +/// How a wrapper maps a non-zero error slot to an exception, rendering +/// [`ErrorStrategy`]: a throwing function with a domain in scope uses the +/// domain's typed `FromError`; everything else (producer traps) uses the +/// base exception's. +#[derive(Clone)] +pub(crate) struct ErrCtx<'a> { + cx: Cx<'a>, + map: String, + domain: Option<(String, String)>, +} + +impl<'a> ErrCtx<'a> { + /// The error context for one function. + pub(crate) fn for_fn(f: &FnBinding, error: Option<&ErrorBinding>, cx: Cx<'a>) -> Self { + match (f.error_strategy(), error) { + (ErrorStrategy::Throws, Some(eb)) => { + let exc = dotnet_exception_name(eb); + ErrCtx { + cx, + map: format!("{}.FromError", cx.ty(&exc)), + domain: Some((exc, eb.type_name.clone())), + } + } + _ => ErrCtx { + cx, + map: format!("{}.FromError", cx.ty(cx.base)), + domain: None, + }, + } + } + + /// The statement throwing when the local `FfiError` named `var` holds a + /// failure. + fn check(&self, var: &str) -> String { + format!( + "if ({var}.Code != 0) throw Ffi.TakeError(&{var}, {});", + self.map + ) + } + + /// Emit the `` doc line of a throwing wrapper. + fn write_doc(&self, w: &mut CodeWriter) { + if let Some((exc, ty)) = &self.domain { + w.line(format!( + "/// The call reported a {ty} code." + )); + } + } +} + +/// The receiver of a wrapper: what the method is a member of. +#[derive(Clone, Copy)] +pub(crate) enum Receiver<'a> { + /// A free function or interface static: a `static` method. + Static, + /// An interface instance method: passes `Handle` as `self`. + Instance, + /// The interface's `new` constructor: a real C# constructor of the + /// named class. + Constructor(&'a str), +} + +/// The `[Obsolete]` attribute line for a deprecated item. +pub(crate) fn write_obsolete(w: &mut CodeWriter, deprecated: &Option) { + if let Some(msg) = deprecated { + w.line(format!("[Obsolete(\"{}\")]", msg.replace('"', "\\\""))); + } +} + +/// The argument marshalling for one parameter list: statements run before +/// the call, the buffers pinned for it, one expression per ABI slot, and the +/// callback registrations to release if the call never reaches the producer. +struct Marshal { + setup: String, + pins: Vec, + args: Vec, + callbacks: Vec, +} + +impl Marshal { + /// Plan the marshalling of `params` (already camelCased). + fn new(cx: Cx<'_>, params: &[ParamBinding]) -> Self { + let mut pins = Vec::new(); + let mut args = Vec::new(); + let mut callbacks = Vec::new(); + let mut setup = CodeWriter::four_space(); + for p in params { + let n = safe_cs_name(&p.name); + let local = p.name.as_str(); + match p.arg_pass() { + ArgPass::Direct { .. } => args.push(direct_to_slot(&p.ty, &n)), + ArgPass::String { .. } => { + setup.line(format!("var {local}Bytes = Ffi.Utf8({n});")); + pins.push(format!("{local}Ptr = {local}Bytes")); + args.push(format!("{local}Ptr")); + args.push(format!("(nuint){local}Bytes.Length")); + } + ArgPass::Bytes { .. } => { + pins.push(format!("{local}Ptr = {n}")); + args.push(format!("{local}Ptr")); + args.push(format!("(nuint){n}.Length")); + } + ArgPass::Buffer { .. } => { + let writer = format!("{local}Writer"); + setup.line(format!("var {writer} = new FfiBufferWriter();")); + emit_write(&mut setup, &p.ty, &n, &writer, 0); + pins.push(format!("{local}Ptr = {writer}.Written")); + args.push(format!("{local}Ptr")); + args.push(format!("(nuint){writer}.Length")); + } + ArgPass::Object { nullable, .. } => { + if nullable { + let iface = + p.ty.interface_name() + .expect("object types name an interface"); + args.push(format!("{n}?.Handle ?? {}.NativeHandle.Null", cx.ty(iface))); + } else { + args.push(format!("{n}.Handle")); + } + } + // The producer owns the registration once the call reaches + // it: its vtable `free(ctx)` releases it. + ArgPass::Callback { vtable, .. } => { + args.push(format!("{local}Ctx")); + args.push(format!("{}.Pointer", vtable_class_for_slot(&vtable.ty))); + callbacks.push((format!("{local}Ctx"), n)); + } + } + } + // Register last, so a failing encode above can't strand a + // registration. + for (ctx, n) in &callbacks { + setup.line(format!("var {ctx} = Ffi.Register({n});")); + } + let callbacks = callbacks.into_iter().map(|(ctx, _)| ctx).collect(); + Marshal { + setup: setup.finish(), + pins, + args, + callbacks, + } + } + + /// Emit the setup statements. + fn write_setup(&self, w: &mut CodeWriter) { + w.block_raw(&self.setup); + } + + /// Emit `body` inside one `fixed` statement pinning every buffer, or bare + /// when nothing needs pinning. + fn pinned(&self, w: &mut CodeWriter, body: impl FnOnce(&mut CodeWriter)) { + if self.pins.is_empty() { + body(w); + return; + } + w.line(format!("fixed (byte* {})", self.pins.join(", "))); + w.line("{"); + w.scope(body); + w.line("}"); + } + + /// The statements releasing every callback registration. + fn unregister(&self, w: &mut CodeWriter) { + for ctx in &self.callbacks { + w.line(format!("Ffi.Unregister({ctx});")); + } + } +} + +/// The C# class hosting the static vtable a callback slot points at, named +/// from the slot's vtable C type. +fn vtable_class_for_slot(vtable: &CType) -> String { + let CType::Ptr { pointee, .. } = vtable else { + unreachable!("a callback vtable slot is a pointer") + }; + let CType::VtableTag { module, name } = pointee.as_ref() else { + unreachable!("a callback vtable slot points at a vtable tag") + }; + vtable_class_cs(module, name) +} + +/// The expression converting a surface value into its by-value slot: `bool` +/// as a byte, a C-style enum as its `int`, every other scalar as is. +pub(crate) fn direct_to_slot(ty: &Ty, expr: &str) -> String { + match ty { + Ty::Bool => format!("(byte)({expr} ? 1 : 0)"), + Ty::Enum(_) => format!("(int){expr}"), + _ => expr.to_string(), + } +} + +/// The expression converting a by-value slot into its surface type. +pub(crate) fn direct_from_slot(ty: &Ty, slot: &str) -> String { + match ty { + Ty::Bool => format!("{slot} != 0"), + Ty::Enum(name) => format!("({}){slot}", local_type_name(name)), + _ => slot.to_string(), + } +} + +/// Who owns a received `(ptr, len)` run. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Own { + /// The consumer: copy or decode it, then release it with `free_bytes` + /// (returns, async results, iterator items). + Take, + /// The producer, for the duration of a callback: copy or decode only. + Borrow, +} + +/// Emit any statements receiving a value of `ty` from its slots (`ptr`, plus +/// `len` for the `(ptr, len)` families) and return the expression holding +/// the received value. `var` names the decoded local for buffers. Objects +/// always transfer one strong reference, which the new wrapper adopts. +pub(crate) fn receive( + w: &mut CodeWriter, + cx: Cx<'_>, + ty: &Ty, + ptr: &str, + len: &str, + own: Own, + var: &str, +) -> String { + let verb = match own { + Own::Take => "Take", + Own::Borrow => "Read", + }; + match ty.family() { + Family::Direct => direct_from_slot(ty, ptr), + Family::String => format!("Ffi.{verb}String({ptr}, {len})"), + Family::Bytes => format!("Ffi.{verb}Bytes({ptr}, {len})"), + Family::Buffer => { + let reader = match own { + Own::Take => format!("Ffi.TakeBuffer({ptr}, {len})"), + Own::Borrow => format!("new FfiBufferReader(Ffi.ReadBytes({ptr}, {len}))"), + }; + emit_decode(w, cx, ty, var, &reader); + var.to_string() + } + Family::Object { nullable } => { + let class = cx.ty(ty.interface_name().expect("object types name an interface")); + if nullable { + format!("{ptr} == IntPtr.Zero ? null : {class}.Adopt({ptr})") + } else { + format!("{class}.Adopt({ptr})") + } + } + Family::Callback | Family::Iterator => unreachable!("{ty} is never received"), + } +} + +/// The public signature's parameter list. With `span`, bytes parameters are +/// `ReadOnlySpan`. +fn params_sig(params: &[ParamBinding], span: bool) -> Vec { + params + .iter() + .map(|p| { + let ty = if span && p.ty == Ty::Bytes { + "ReadOnlySpan".to_string() + } else { + cs_type(&p.ty) + }; + format!("{ty} {}", safe_cs_name(&p.name)) + }) + .collect() +} + +/// The argument list forwarding a `byte[]` overload to its span overload. +fn forward_args(params: &[ParamBinding]) -> String { + params + .iter() + .map(|p| { + let n = safe_cs_name(&p.name); + if p.ty == Ty::Bytes { + format!("(ReadOnlySpan){n}") + } else { + n + } + }) + .collect::>() + .join(", ") +} + +/// The slot arguments of a call: the receiver (when any), every marshalled +/// parameter slot, then `extra`. +fn call_args(receiver: Receiver<'_>, m: &Marshal, extra: &[String]) -> String { + let mut args = Vec::new(); + if matches!(receiver, Receiver::Instance) { + args.push("Handle".to_string()); + } + args.extend(m.args.iter().cloned()); + args.extend(extra.iter().cloned()); + args.join(", ") +} + +/// The signature line opener for a wrapper: modifiers, return type, and +/// name (or the bare class name for a constructor). +fn opener(receiver: Receiver<'_>, ret: &str, name: &str) -> String { + match receiver { + Receiver::Static => format!("public static {ret} {name}"), + Receiver::Instance => format!("public {ret} {name}"), + Receiver::Constructor(class) => format!("public {class}"), + } +} + +/// Render one wrapper (any shape) named `name`. +pub(crate) fn render_callable( + w: &mut CodeWriter, + f: &FnBinding, + name: &str, + receiver: Receiver<'_>, + err: &ErrCtx<'_>, +) { + let f = camel_fn(f); + match &f.shape { + CallShape::Sync(abi) => { + let span = f.params.iter().any(|p| p.ty == Ty::Bytes); + render_sync(w, &f, &abi.params, &abi.ret, name, receiver, err, span); + if span { + render_array_overload(w, &f, name, receiver, err); + } + } + CallShape::Async(a) => render_async(w, &f, a, name, receiver, err), + CallShape::Iterator(it) => render_iterator(w, &f, it, name, receiver, err), + } +} + +/// The documentation, ``, and `[Obsolete]` lines of a wrapper. +fn write_header(w: &mut CodeWriter, f: &FnBinding, err: &ErrCtx<'_>) { + write_fn_doc(w, &f.doc, &f.params); + err.write_doc(w); + write_obsolete(w, &f.deprecated); +} + +/// A synchronous wrapper: one call, one check, one received result. A +/// constructor stores the adopted pointer as its handle instead of +/// returning it. +#[allow(clippy::too_many_arguments)] +fn render_sync( + w: &mut CodeWriter, + f: &FnBinding, + slots: &[AbiParam], + ret: &CType, + name: &str, + receiver: Receiver<'_>, + err: &ErrCtx<'_>, + span: bool, +) { + let ret_cs = match receiver { + Receiver::Constructor(_) => String::new(), + _ => f.ret.as_ref().map(cs_type).unwrap_or_else(|| "void".into()), + }; + let cx = err.cx; + let m = Marshal::new(cx, &f.params); + let has_out_len = slots.iter().any(|s| s.name == "out_len"); + let mut extra = Vec::new(); + if has_out_len { + extra.push("&ffiOutLen".to_string()); + } + extra.push("&ffiErr".to_string()); + let call = format!( + "NativeMethods.{}({})", + f.c_base, + call_args(receiver, &m, &extra) + ); + + write_header(w, f, err); + w.line(format!( + "{}({})", + opener(receiver, &ret_cs, name), + params_sig(&f.params, span).join(", ") + )); + w.block("{", "}", |w| { + m.write_setup(w); + w.line("var ffiErr = default(FfiError);"); + if has_out_len { + w.line("nuint ffiOutLen = 0;"); + } + m.pinned(w, |w| { + let returns = f.ret.is_some(); + if m.callbacks.is_empty() { + if returns { + w.line(format!("var ffiResult = {call};")); + } else { + w.line(format!("{call};")); + } + } else { + if returns { + w.line(format!("{} ffiResult;", cs_ctype(ret))); + } + w.line("try"); + w.block("{", "}", |w| { + if returns { + w.line(format!("ffiResult = {call};")); + } else { + w.line(format!("{call};")); + } + }); + w.line("catch"); + w.block("{", "}", |w| { + m.unregister(w); + w.line("throw;"); + }); + } + w.line(err.check("ffiErr")); + match (&f.ret, receiver) { + (Some(_), Receiver::Constructor(_)) => { + w.line("Handle = new NativeHandle(ffiResult);"); + } + (Some(ty), _) => { + let expr = + receive(w, cx, ty, "ffiResult", "ffiOutLen", Own::Take, "ffiDecoded"); + w.line(format!("return {expr};")); + } + (None, _) => {} + } + }); + }); + w.blank(); +} + +/// The `byte[]` overload of a wrapper whose bytes parameters are +/// `ReadOnlySpan`, forwarding to it. +fn render_array_overload( + w: &mut CodeWriter, + f: &FnBinding, + name: &str, + receiver: Receiver<'_>, + err: &ErrCtx<'_>, +) { + let ret_cs = f.ret.as_ref().map(cs_type).unwrap_or_else(|| "void".into()); + let args = forward_args(&f.params); + write_header(w, f, err); + let sig = format!( + "{}({})", + opener(receiver, &ret_cs, name), + params_sig(&f.params, false).join(", ") + ); + match receiver { + Receiver::Constructor(_) => { + w.line(format!("{sig} : this({args})")); + w.line("{"); + w.line("}"); + } + _ => { + w.line(format!("{sig} => {name}({args});")); + } + } + w.blank(); +} + +/// An async wrapper: a `Task`-returning method launching the call with a +/// pending `FfiCall` as `context`, plus the `[UnmanagedCallersOnly]` +/// completion that resolves it. A cancellable function takes a +/// `CancellationToken` linked to the native cancel token. +fn render_async( + w: &mut CodeWriter, + f: &FnBinding, + a: &AsyncBinding, + name: &str, + receiver: Receiver<'_>, + err: &ErrCtx<'_>, +) { + let result_cs = f.ret.as_ref().map(cs_type); + let task = match &result_cs { + Some(t) => format!("Task<{t}>"), + None => "Task".into(), + }; + let call_ty = result_cs.clone().unwrap_or_else(|| "bool".into()); + let complete = format!("Complete{name}"); + let cx = err.cx; + let m = Marshal::new(cx, &f.params); + let mut extra = Vec::new(); + if f.cancellable { + extra.push("ffiCall.CancelToken()".to_string()); + } + extra.push(format!("&{complete}")); + extra.push("ffiCall.Context".to_string()); + let call = format!( + "NativeMethods.{}({});", + a.launch.symbol, + call_args(receiver, &m, &extra) + ); + + write_header(w, f, err); + if f.cancellable { + w.line("/// Cancels the native call; the task"); + w.line("/// then completes as canceled."); + } + let mut sig = params_sig(&f.params, false); + if f.cancellable { + sig.push("CancellationToken cancellationToken = default".into()); + } + let opener = match receiver { + Receiver::Instance => format!("public {task} {name}"), + _ => format!("public static {task} {name}"), + }; + w.line(format!("{opener}({})", sig.join(", "))); + w.block("{", "}", |w| { + if f.cancellable { + w.line("if (cancellationToken.IsCancellationRequested)"); + w.block("{", "}", |w| { + let from = match &result_cs { + Some(t) => format!("Task.FromCanceled<{t}>(cancellationToken)"), + None => "Task.FromCanceled(cancellationToken)".into(), + }; + w.line(format!("return {from};")); + }); + } + m.write_setup(w); + let token = if f.cancellable { + "cancellationToken" + } else { + "" + }; + w.line(format!("var ffiCall = new FfiCall<{call_ty}>({token});")); + w.line("try"); + w.block("{", "}", |w| { + m.pinned(w, |w| { + w.line(&call); + }); + }); + w.line("catch"); + w.block("{", "}", |w| { + w.line("ffiCall.Abandon();"); + m.unregister(w); + w.line("throw;"); + }); + w.line("return ffiCall.Task;"); + }); + w.blank(); + + // The completion: fires exactly once, on a producer thread. + let slots: Vec = a + .callback_params + .iter() + .map(|s| format!("{} {}", cs_ctype(&s.ty), safe_cs_name(&s.name))) + .collect(); + w.line(UNMANAGED_CALLERS_ONLY); + w.line(format!( + "private static void {complete}({})", + slots.join(", ") + )); + w.block("{", "}", |w| { + w.line(format!( + "var ffiCall = FfiCall<{call_ty}>.Complete(context);" + )); + w.line("try"); + w.block("{", "}", |w| { + w.line("if (err != null)"); + w.block("{", "}", |w| { + w.line(format!("ffiCall.SetError(err, {});", err.map)); + w.line("return;"); + }); + match &f.ret { + None => { + w.line("ffiCall.SetResult(true);"); + } + Some(ty) => { + let pair = a.callback_params.iter().any(|s| s.name == "result_len"); + let (ptr, len) = if pair { + ("result_ptr", "result_len") + } else { + ("result", "") + }; + let expr = receive(w, cx, ty, ptr, len, Own::Take, "ffiDecoded"); + w.line(format!("ffiCall.SetResult({expr});")); + } + } + }); + w.line("catch (Exception e)"); + w.block("{", "}", |w| { + w.line("ffiCall.SetException(e);"); + }); + }); + w.blank(); +} + +/// An iterator wrapper: the launcher runs eagerly (so launch errors throw +/// at the call), and the returned single-use sequence pulls one item per +/// `MoveNext` through a static `Next{Name}` helper. The native iterator is a +/// `SafeHandle`, destroyed exactly once on exhaustion, disposal, or +/// finalization. +fn render_iterator( + w: &mut CodeWriter, + f: &FnBinding, + it: &IteratorBinding, + name: &str, + receiver: Receiver<'_>, + err: &ErrCtx<'_>, +) { + let elem = cs_type(&it.elem); + let next = format!("Next{name}"); + let cx = err.cx; + let m = Marshal::new(cx, &f.params); + let call = format!( + "NativeMethods.{}({})", + it.launch.symbol, + call_args(receiver, &m, &["&ffiErr".to_string()]) + ); + + write_header(w, f, err); + w.line("/// Streams lazily, one native call per item. The sequence can be"); + w.line("/// enumerated once; disposing its enumerator (as foreach does)"); + w.line("/// releases the native iterator."); + let ret = format!("IEnumerable<{elem}>"); + w.line(format!( + "{}({})", + opener(receiver, &ret, name), + params_sig(&f.params, false).join(", ") + )); + w.block("{", "}", |w| { + m.write_setup(w); + w.line("var ffiErr = default(FfiError);"); + m.pinned(w, |w| { + if m.callbacks.is_empty() { + w.line(format!("var ffiResult = {call};")); + } else { + w.line("IntPtr ffiResult;"); + w.line("try"); + w.block("{", "}", |w| { + w.line(format!("ffiResult = {call};")); + }); + w.line("catch"); + w.block("{", "}", |w| { + m.unregister(w); + w.line("throw;"); + }); + } + w.line(err.check("ffiErr")); + w.line(format!( + "return new FfiSequence<{elem}>(new FfiIteratorHandle(ffiResult, &NativeMethods.{}), &{next});", + it.destroy_symbol + )); + }); + }); + w.blank(); + + // `_next` out-slots after the iterator handle, excluding `out_err`. + let outs: Vec<&AbiParam> = it + .next + .params + .iter() + .skip(1) + .filter(|s| s.name != "out_err") + .collect(); + w.line(format!( + "private static bool {next}(FfiIteratorHandle iter, out {elem} item)" + )); + w.block("{", "}", |w| { + let mut args = vec!["iter".to_string()]; + for s in &outs { + let CType::Ptr { pointee, .. } = &s.ty else { + unreachable!("iterator out-slots are pointers") + }; + let local = if s.name == "out_item" { + "ffiItem" + } else { + "ffiLen" + }; + w.line(format!("{} {local} = default;", cs_ctype(pointee))); + args.push(format!("&{local}")); + } + args.push("&ffiErr".to_string()); + w.line("var ffiErr = default(FfiError);"); + w.line(format!( + "var ffiMore = NativeMethods.{}({});", + it.next.symbol, + args.join(", ") + )); + w.line(err.check("ffiErr")); + w.line("if (ffiMore == 0)"); + w.block("{", "}", |w| { + w.line("item = default!;"); + w.line("return false;"); + }); + let expr = receive( + w, + cx, + &it.elem, + "ffiItem", + "ffiLen", + Own::Take, + "ffiDecoded", + ); + w.line(format!("item = {expr};")); + w.line("return true;"); + }); + w.blank(); +} + +/// Render one module's static class. Submodules become sibling classes named +/// by their full path (`KvStats`), so a module class never shadows a type. +pub(crate) fn render_module_class( + w: &mut CodeWriter, + m: &ModuleBinding, + strip_module_prefix: bool, + cx: Cx<'_>, +) { + if m.functions.is_empty() { + return; + } + let class: String = m.segments.iter().map(|s| s.to_upper_camel_case()).collect(); + // The model falls back to the first function's doc when the module has + // none; that would mislabel the class, so only a module's own doc is + // used. + if !m.functions.iter().any(|f| f.doc == m.doc) { + write_doc(w, &m.doc); + } + w.line(format!("public static unsafe class {class}")); + w.block("{", "}", |w| { + for f in &m.functions { + let name = wrapper_name(&m.path, &f.name, strip_module_prefix).to_upper_camel_case(); + let err = ErrCtx::for_fn(f, m.error.as_ref(), cx); + render_callable(w, f, &name, Receiver::Static, &err); + } + }); + w.blank(); +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/codec.rs b/crates/weaveffi-gen/src/targets/dotnet/codec.rs new file mode 100644 index 00000000..dce630e3 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/codec.rs @@ -0,0 +1,146 @@ +//! Value-buffer codec emitters: the statements encoding a C# value into an +//! `FfiBufferWriter` and decoding one back from an `FfiBufferReader`. +//! +//! Every dispatch goes through [`Ty::wire`], so the wire shapes are never +//! re-derived here. Primitives map onto `Write{Pascal}`/`Read{Pascal}` by +//! their [`Prim`](weaveffi_model::model::Prim) name. An object token carries +//! one strong reference: writing one clones the wrapper's handle through the +//! interface's `_clone` symbol, and reading one adopts the pointer into a new +//! wrapper. + +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::model::{Ty, WireType}; + +use crate::targets::dotnet::types::{cs_type, is_cs_value_type, Cx}; + +/// Emit statements serializing `expr` (a C# expression of the surface type +/// mapped from `ty`) into the writer named `writer`. `depth` keeps nested +/// loop locals unique. +pub(crate) fn emit_write(w: &mut CodeWriter, ty: &Ty, expr: &str, writer: &str, depth: usize) { + match ty.wire() { + WireType::Prim(p) => { + w.line(format!("{writer}.Write{}({expr});", p.pascal())); + } + WireType::Enum(_) => { + w.line(format!("{writer}.WriteI32((int){expr});")); + } + // The token must carry its own strong reference, so clone the + // wrapper's handle rather than writing the pointer it still owns. + WireType::Object(_) => { + w.line(format!("{writer}.WriteObject({expr}.CloneHandle());")); + } + WireType::User(_) => { + w.line(format!("{expr}.WriteTo({writer});")); + } + WireType::Optional(inner) => { + let value = if is_cs_value_type(inner) { + format!("{expr}.Value") + } else { + format!("{expr}!") + }; + w.line(format!("{writer}.WriteBool({expr} != null);")); + w.line(format!("if ({expr} != null)")); + w.block("{", "}", |w| { + emit_write(w, inner, &value, writer, depth); + }); + } + WireType::List(inner) => { + let item = format!("item{depth}"); + w.line(format!("{writer}.WriteLen({expr}.Length);")); + w.line(format!("foreach (var {item} in {expr})")); + w.block("{", "}", |w| { + emit_write(w, inner, &item, writer, depth + 1); + }); + } + WireType::Map(k, v) => { + let entry = format!("entry{depth}"); + w.line(format!("{writer}.WriteLen({expr}.Count);")); + w.line(format!("foreach (var {entry} in {expr})")); + w.block("{", "}", |w| { + emit_write(w, k, &format!("{entry}.Key"), writer, depth + 1); + emit_write(w, v, &format!("{entry}.Value"), writer, depth + 1); + }); + } + } +} + +/// Emit statements declaring a local named `var` and decoding a value of `ty` +/// into it from the reader named `reader`, the inverse of [`emit_write`]. +pub(crate) fn emit_read( + w: &mut CodeWriter, + cx: Cx<'_>, + ty: &Ty, + var: &str, + reader: &str, + depth: usize, +) { + match ty.wire() { + WireType::Prim(p) => { + w.line(format!("var {var} = {reader}.Read{}();", p.pascal())); + } + WireType::Enum(name) => { + let cn = local_type_name(name); + w.line(format!("var {var} = ({cn}){reader}.ReadI32();")); + } + // Adopt the token's strong reference into a fresh wrapper. + WireType::Object(name) => { + let cn = cx.ty(name); + w.line(format!("var {var} = {cn}.Adopt({reader}.ReadObject());")); + } + WireType::User(name) => { + let cn = cx.ty(name); + w.line(format!("var {var} = {cn}.ReadFrom({reader});")); + } + WireType::Optional(inner) => { + w.line(format!("{} {var} = null;", cs_type(ty))); + w.line(format!("if ({reader}.ReadBool())")); + w.block("{", "}", |w| { + emit_read(w, cx, inner, &format!("{var}Value"), reader, depth); + w.line(format!("{var} = {var}Value;")); + }); + } + // Elements can encode to zero bytes, so a count isn't bounded by the + // remaining input; the list grows from a capped capacity instead of + // preallocating whatever the count claims. + WireType::List(inner) => { + let i = format!("i{depth}"); + w.line(format!("var {var}Count = {reader}.ReadLen();")); + w.line(format!( + "var {var}List = new List<{}>({reader}.Capacity({var}Count));", + cs_type(inner) + )); + w.line(format!("for (var {i} = 0; {i} < {var}Count; {i}++)")); + w.block("{", "}", |w| { + emit_read(w, cx, inner, &format!("{var}Item"), reader, depth + 1); + w.line(format!("{var}List.Add({var}Item);")); + }); + w.line(format!("var {var} = {var}List.ToArray();")); + } + WireType::Map(k, v) => { + let i = format!("i{depth}"); + w.line(format!("var {var}Count = {reader}.ReadLen();")); + w.line(format!( + "var {var} = new Dictionary<{}, {}>({reader}.Capacity({var}Count));", + cs_type(k), + cs_type(v) + )); + w.line(format!("for (var {i} = 0; {i} < {var}Count; {i}++)")); + w.block("{", "}", |w| { + emit_read(w, cx, k, &format!("{var}Key"), reader, depth + 1); + emit_read(w, cx, v, &format!("{var}Val"), reader, depth + 1); + w.line(format!("{var}[{var}Key] = {var}Val;")); + }); + } + } +} + +/// Emit statements decoding a whole value buffer held by the reader +/// expression `reader_expr` into a local named `var`, then checking that the +/// buffer was fully consumed. +pub(crate) fn emit_decode(w: &mut CodeWriter, cx: Cx<'_>, ty: &Ty, var: &str, reader_expr: &str) { + let reader = format!("{var}Reader"); + w.line(format!("var {reader} = {reader_expr};")); + emit_read(w, cx, ty, var, &reader, 0); + w.line(format!("{reader}.ExpectEnd();")); +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/docs.rs b/crates/weaveffi-gen/src/targets/dotnet/docs.rs new file mode 100644 index 00000000..fda17f9e --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/docs.rs @@ -0,0 +1,47 @@ +//! XML doc-comment emission: `` blocks for items and the +//! `` plus `` set for callables. + +use crate::codegen::CodeWriter; +use weaveffi_model::model::ParamBinding; + +use crate::targets::dotnet::types::xml_text; + +/// The trimmed, non-empty text of an optional doc comment. +fn text(doc: &Option) -> Option<&str> { + doc.as_deref().map(str::trim).filter(|d| !d.is_empty()) +} + +/// Emit one XML element (``, ``) holding `body`: +/// on one line when the body is a single line, else one `///` line each. +fn element(w: &mut CodeWriter, open: &str, close: &str, body: &str) { + let body = xml_text(body); + if body.contains('\n') { + w.line(format!("/// {open}")); + for line in body.lines() { + w.line(format!("/// {line}").trim_end()); + } + w.line(format!("/// {close}")); + } else { + w.line(format!("/// {open}{body}{close}")); + } +} + +/// Emit a `` for an item's doc comment, if it has one. +pub(crate) fn write_doc(w: &mut CodeWriter, doc: &Option) { + if let Some(d) = text(doc) { + element(w, "", "", d); + } +} + +/// Emit a callable's `` plus one `` per documented +/// parameter. Parameter names must already be the C# spelling (without +/// the `@` escape, which XML doc references omit). +pub(crate) fn write_fn_doc(w: &mut CodeWriter, doc: &Option, params: &[ParamBinding]) { + write_doc(w, doc); + for p in params { + if let Some(d) = text(&p.doc) { + let open = format!("", p.name); + element(w, &open, "", d); + } + } +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/entities.rs b/crates/weaveffi-gen/src/targets/dotnet/entities.rs new file mode 100644 index 00000000..f5d270de --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/entities.rs @@ -0,0 +1,298 @@ +//! Entity renderers: C-style enums, records, rich (algebraic) enums, and the +//! interface wrapper classes. + +use crate::codegen::CodeWriter; +use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use weaveffi_model::model::{ + CallShape, EnumBinding, EnumVariantBinding, ErrorBinding, FieldBinding, InterfaceBinding, + StructBinding, +}; + +use crate::targets::dotnet::calls::{render_callable, write_obsolete, ErrCtx, Receiver}; +use crate::targets::dotnet::codec::{emit_read, emit_write}; +use crate::targets::dotnet::docs::write_doc; +use crate::targets::dotnet::types::{cs_type, safe_cs_name, Cx}; + +/// Render a C-style enum as a C# `enum` with its ABI discriminants. +pub(crate) fn render_enum(w: &mut CodeWriter, e: &EnumBinding) { + write_doc(w, &e.doc); + write_obsolete(w, &e.deprecated); + w.line(format!("public enum {}", e.name)); + w.block("{", "}", |w| { + for v in &e.variants { + write_doc(w, &v.doc); + w.line(format!("{} = {},", v.name, v.value)); + } + }); + w.blank(); +} + +/// The get-only properties and positional constructor shared by records and +/// rich-enum variants. +fn render_value_members(w: &mut CodeWriter, class: &str, fields: &[FieldBinding]) { + for f in fields { + write_doc(w, &f.doc); + w.line(format!( + "public {} {} {{ get; }}", + cs_type(&f.ty), + f.name.to_upper_camel_case() + )); + w.blank(); + } + let params: Vec = fields + .iter() + .map(|f| { + format!( + "{} {}", + cs_type(&f.ty), + safe_cs_name(&f.name.to_lower_camel_case()) + ) + }) + .collect(); + w.line(format!( + "/// Creates a from every field." + )); + w.line(format!("public {class}({})", params.join(", "))); + w.block("{", "}", |w| { + for f in fields { + w.line(format!( + "{} = {};", + f.name.to_upper_camel_case(), + safe_cs_name(&f.name.to_lower_camel_case()) + )); + } + }); +} + +/// Emit the reads of every field into `f{Field}` locals, then return the +/// constructed `class`. +fn render_field_reads(w: &mut CodeWriter, cx: Cx<'_>, class: &str, fields: &[FieldBinding]) { + let mut args = Vec::new(); + for f in fields { + let var = format!("f{}", f.name.to_upper_camel_case()); + emit_read(w, cx, &f.ty, &var, "reader", 0); + args.push(var); + } + w.line(format!("return new {class}({});", args.join(", "))); +} + +/// Render a record as a sealed data class: get-only properties, a +/// positional constructor, and the internal `WriteTo`/`ReadFrom` pair +/// implementing its value-buffer encoding (fields in declaration order). +pub(crate) fn render_record(w: &mut CodeWriter, cx: Cx<'_>, s: &StructBinding) { + write_doc(w, &s.doc); + write_obsolete(w, &s.deprecated); + w.line(format!("public sealed class {}", s.name)); + w.block("{", "}", |w| { + render_value_members(w, &s.name, &s.fields); + w.blank(); + w.line("internal void WriteTo(FfiBufferWriter writer)"); + w.block("{", "}", |w| { + for f in &s.fields { + emit_write(w, &f.ty, &f.name.to_upper_camel_case(), "writer", 0); + } + }); + w.blank(); + w.line(format!( + "internal static {} ReadFrom(FfiBufferReader reader)", + s.name + )); + w.block("{", "}", |w| { + render_field_reads(w, cx, &s.name, &s.fields); + }); + }); + w.blank(); +} + +/// Render a rich enum as a closed class hierarchy: an abstract base with a +/// private constructor and one nested sealed class per variant +/// (`Shape.Circle`). The base hosts the codec: an `i32` tag, then the active +/// variant's fields. +pub(crate) fn render_rich_enum(w: &mut CodeWriter, cx: Cx<'_>, e: &EnumBinding) { + let name = &e.name; + write_doc(w, &e.doc); + write_obsolete(w, &e.deprecated); + w.line(format!("public abstract class {name}")); + w.block("{", "}", |w| { + w.line(format!("private {name}()")); + w.line("{"); + w.line("}"); + w.blank(); + for v in &e.variants { + render_variant(w, name, v); + } + w.line("internal void WriteTo(FfiBufferWriter writer)"); + w.block("{", "}", |w| { + w.line("switch (this)"); + w.block("{", "}", |w| { + for v in &e.variants { + let binding = if v.fields.is_empty() { "_" } else { "v" }; + w.line(format!("case {} {binding}:", v.name)); + w.scope(|w| { + w.line(format!("writer.WriteI32({});", v.value)); + for f in &v.fields { + let expr = format!("v.{}", f.name.to_upper_camel_case()); + emit_write(w, &f.ty, &expr, "writer", 0); + } + w.line("break;"); + }); + } + w.line("default:"); + w.scope(|w| { + w.line(format!( + "throw new InvalidOperationException(\"unknown {name} variant\");" + )); + }); + }); + }); + w.blank(); + w.line(format!( + "internal static {name} ReadFrom(FfiBufferReader reader)" + )); + w.block("{", "}", |w| { + w.line("var tag = reader.ReadI32();"); + w.line("switch (tag)"); + w.block("{", "}", |w| { + for v in &e.variants { + w.line(format!("case {}:", v.value)); + w.block("{", "}", |w| { + render_field_reads(w, cx, &v.name, &v.fields); + }); + } + w.line("default:"); + w.scope(|w| { + w.line(format!( + "throw new InvalidOperationException(\"malformed value buffer: unknown {name} tag \" + tag);" + )); + }); + }); + }); + }); + w.blank(); +} + +/// One nested sealed variant class of a rich enum. +fn render_variant(w: &mut CodeWriter, enum_name: &str, v: &EnumVariantBinding) { + write_doc(w, &v.doc); + w.line(format!("public sealed class {} : {enum_name}", v.name)); + w.block("{", "}", |w| { + if !v.fields.is_empty() { + render_value_members(w, &v.name, &v.fields); + } + }); + w.blank(); +} + +/// Render one interface as a sealed wrapper over a `SafeHandle` subclass +/// holding one strong reference. The handle's `ReleaseHandle` calls the +/// interface's `_destroy`; every call passes the handle itself, so the +/// interop stub keeps the object alive for the call even if the wrapper is +/// disposed or collected meanwhile. Two wrappers are equal when they +/// reference the same native object. +pub(crate) fn render_interface( + w: &mut CodeWriter, + i: &InterfaceBinding, + error: Option<&ErrorBinding>, + cx: Cx<'_>, +) { + let name = &i.name; + write_doc(w, &i.doc); + write_obsolete(w, &i.deprecated); + w.line(format!( + "public sealed unsafe class {name} : IDisposable, IEquatable<{name}>" + )); + w.block("{", "}", |w| { + w.line("/// The native object; borrowed by every call."); + w.line("internal NativeHandle Handle { get; }"); + w.blank(); + w.line(format!("private {name}(NativeHandle handle)")); + w.block("{", "}", |w| { + w.line("Handle = handle;"); + }); + w.blank(); + w.line("/// Wraps one strong reference the caller transfers."); + w.line(format!("internal static {name} Adopt(IntPtr ptr)")); + w.block("{", "}", |w| { + w.line(format!("return new {name}(new NativeHandle(ptr));")); + }); + w.blank(); + w.line("/// Mints a second strong reference the caller owns, as an"); + w.line("/// object token in a value buffer requires."); + w.line("internal IntPtr CloneHandle()"); + w.block("{", "}", |w| { + w.line(format!("return NativeMethods.{}(Handle);", i.clone_symbol)); + }); + w.blank(); + + for c in &i.constructors { + let err = ErrCtx::for_fn(c, error, cx); + let method = c.name.to_upper_camel_case(); + let receiver = if c.name == "new" && matches!(c.shape, CallShape::Sync(_)) { + Receiver::Constructor(name) + } else { + Receiver::Static + }; + render_callable(w, c, &method, receiver, &err); + } + for m in &i.methods { + let err = ErrCtx::for_fn(m, error, cx); + render_callable(w, m, &m.name.to_upper_camel_case(), Receiver::Instance, &err); + } + for s in &i.statics { + let err = ErrCtx::for_fn(s, error, cx); + render_callable(w, s, &s.name.to_upper_camel_case(), Receiver::Static, &err); + } + + w.line("/// Releases this wrapper's reference. The native object is"); + w.line("/// dropped once its last reference is released, and an in-flight"); + w.line("/// call finishes first."); + w.line("public void Dispose()"); + w.block("{", "}", |w| { + w.line("Handle.Dispose();"); + }); + w.blank(); + w.line("/// True when both wrappers reference the same native object."); + w.line(format!("public bool Equals({name}? other)")); + w.block("{", "}", |w| { + w.line("return other is not null && other.Handle.DangerousGetHandle() == Handle.DangerousGetHandle();"); + }); + w.blank(); + w.line("/// "); + w.line("public override bool Equals(object? obj)"); + w.block("{", "}", |w| { + w.line(format!("return obj is {name} other && Equals(other);")); + }); + w.blank(); + w.line("/// "); + w.line("public override int GetHashCode()"); + w.block("{", "}", |w| { + w.line("return Handle.DangerousGetHandle().GetHashCode();"); + }); + w.blank(); + w.line("/// One strong reference, released through"); + w.line(format!("/// {}.", i.destroy_symbol)); + w.line("internal sealed class NativeHandle : SafeHandle"); + w.block("{", "}", |w| { + w.line("/// A null handle, passed for an absent optional object."); + w.line("internal static readonly NativeHandle Null = new NativeHandle();"); + w.blank(); + w.line("public NativeHandle() : base(IntPtr.Zero, true)"); + w.line("{"); + w.line("}"); + w.blank(); + w.line("internal NativeHandle(IntPtr ptr) : base(IntPtr.Zero, true)"); + w.block("{", "}", |w| { + w.line("SetHandle(ptr);"); + }); + w.blank(); + w.line("public override bool IsInvalid => handle == IntPtr.Zero;"); + w.blank(); + w.line("protected override bool ReleaseHandle()"); + w.block("{", "}", |w| { + w.line(format!("NativeMethods.{}(handle);", i.destroy_symbol)); + w.line("return true;"); + }); + }); + }); + w.blank(); +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/mod.rs b/crates/weaveffi-gen/src/targets/dotnet/mod.rs new file mode 100644 index 00000000..b6aee571 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/mod.rs @@ -0,0 +1,291 @@ +//! .NET binding generator. +//! +//! Emits a standalone C# project over the C ABI, revision 3: a `.csproj`, +//! the generated API (`{Namespace}.cs`), the fixed runtime (`Runtime.cs`, +//! from `runtime/Runtime.cs`), and a README. Implements [`LanguageBackend`]. +//! +//! * Native calls use source-generated `[LibraryImport]` declarations over +//! blittable slots, so the bindings need no runtime marshalling and are +//! trim- and AOT-friendly. Strings cross as UTF-8 `(ptr, len)` pairs. +//! * The first native call runs `NativeMethods`' static constructor, which +//! installs a resolver honoring `{PREFIX}_LIBRARY`, then checks the ABI +//! revision and every top-level module's contract checksum. +//! * Records, rich enums, optionals, lists, and maps are value types encoded +//! in the value-buffer format by per-type `WriteTo`/`ReadFrom` pairs. +//! * Interfaces are sealed `IDisposable` wrappers over a `SafeHandle` +//! subclass whose `ReleaseHandle` calls `_destroy`. Every import takes the +//! handle itself, so the interop stub keeps the object alive for the call. +//! * Callback interfaces are C# `interface`s; a passed implementation is +//! pinned in a `GCHandle` and paired with one static vtable of +//! `[UnmanagedCallersOnly]` trampolines. +//! * Async functions return `Task`/`Task` completed by an +//! `[UnmanagedCallersOnly]` completion; cancellable ones take a +//! `CancellationToken` linked to the native cancel token. `iter` +//! returns are single-use lazily streamed `IEnumerable`s. + +mod callbacks; +mod calls; +mod codec; +mod docs; +mod entities; +mod package; +mod pinvoke; +mod runtime; +mod types; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::codegen::CodeWriter; +use crate::package::{PackageContext, PackagedFile}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::resolved::ResolvedApi; + +use crate::targets::dotnet::callbacks::render_callback_interface; +use crate::targets::dotnet::calls::render_module_class; +use crate::targets::dotnet::entities::{ + render_enum, render_interface, render_record, render_rich_enum, +}; +use crate::targets::dotnet::package::{render_csproj, render_readme, Project, NATIVE_ASSETS}; +use crate::targets::dotnet::pinvoke::render_native_methods; +use crate::targets::dotnet::runtime::{ + base_exception_name, render_domain_exception, render_runtime, RuntimeNames, +}; +use crate::targets::dotnet::types::Cx; + +/// Per-target configuration for [`DotnetGenerator`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct DotnetConfig { + /// C# namespace, assembly name, and NuGet id. Defaults to the package + /// name in PascalCase (`my-kv` becomes `MyKv`). + pub namespace: Option, + /// When `true` (the default), strip the module name from wrapper method + /// names; the per-module static class already namespaces them. + pub strip_module_prefix: bool, + /// Basename of the IDL the CLI was invoked with. Set by the CLI. + #[serde(skip)] + pub input_basename: Option, +} + +impl Default for DotnetConfig { + fn default() -> Self { + Self { + namespace: None, + strip_module_prefix: true, + input_basename: None, + } + } +} + +impl DotnetConfig { + /// The C# namespace: the configured one, else `PascalCase(name)`. + pub fn namespace(&self, api: &ResolvedApi) -> String { + self.namespace + .clone() + .unwrap_or_else(|| api.identity().pascal_name()) + } + + /// The IDL basename for generated-file preludes. + pub fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } +} + +/// .NET backend: a C# project binding the C ABI through `[LibraryImport]`. +pub struct DotnetGenerator; + +/// The text files of a generated project, at `dir`, loading `library`. +fn project_files( + api: &ResolvedApi, + model: &BindingModel, + dir: &Utf8Path, + config: &DotnetConfig, + library: &str, + ctx: Option<&PackageContext>, +) -> Vec<(camino::Utf8PathBuf, String)> { + let namespace = config.namespace(api); + let identity = api.identity(); + let input = config.input_basename(); + let project = Project { + identity, + namespace: &namespace, + library, + input_basename: input, + }; + let base = base_exception_name(model, &namespace); + let names = RuntimeNames { + namespace: &namespace, + exception: &base, + prefix: &model.prefix, + library, + library_env: &identity.library_env_var(), + }; + let cs = format!("{namespace}.cs"); + let csproj = format!("{namespace}.csproj"); + let assets = if ctx.is_some() { NATIVE_ASSETS } else { "" }; + vec![ + ( + dir.join(&cs), + render_csharp( + model, + &namespace, + &base, + config.strip_module_prefix, + input, + &cs, + ), + ), + ( + dir.join("Runtime.cs"), + render_runtime(&names, input, "Runtime.cs"), + ), + (dir.join(&csproj), render_csproj(&project, &csproj, assets)), + (dir.join("README.md"), render_readme(&project, ctx)), + ] +} + +impl LanguageBackend for DotnetGenerator { + type Config = DotnetConfig; + + fn name(&self) -> &'static str { + "dotnet" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + let library = &api.identity().library; + project_files(api, model, &out_dir.join("dotnet"), config, library, None) + .into_iter() + .map(|(path, text)| OutputFile::new(path, text)) + .collect() + } + + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let dir = out_dir.join("dotnet"); + let library = &ctx.binaries.lib_name; + let mut files: Vec = + project_files(api, model, &dir, config, library, Some(ctx)) + .into_iter() + .map(|(path, text)| PackagedFile::text(path, text)) + .collect(); + // Bundle each prebuilt library under the `runtimes//native/` + // layout NuGet resolves at restore time. Platforms without a RID + // (Android, wasm32) have no slot in the package. + for nb in &ctx.binaries.binaries { + let Some(rid) = nb.platform.nuget_rid() else { + continue; + }; + let dest = dir + .join("runtimes") + .join(rid) + .join("native") + .join(ctx.binaries.bundled_filename(nb.platform)); + files.push(PackagedFile::copy(dest, nb.source.clone())); + } + Some(files) + } +} + +/// Render the generated API file: typed exceptions, every module's entities, +/// the generated half of `NativeMethods`, and the per-module static classes. +pub(crate) fn render_csharp( + model: &BindingModel, + namespace: &str, + base: &str, + strip_module_prefix: bool, + input_basename: &str, + filename: &str, +) -> String { + let cx = Cx { + ns: namespace, + base, + }; + let mut w = CodeWriter::four_space(); + w.line("#nullable enable"); + w.blank(); + for u in [ + "System", + "System.Collections.Generic", + "System.Runtime.CompilerServices", + "System.Runtime.InteropServices", + "System.Threading", + "System.Threading.Tasks", + ] { + w.line(format!("using {u};")); + } + w.blank(); + w.line(format!("namespace {namespace};")); + w.blank(); + + for m in model.modules.iter() { + if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { + render_domain_exception(&mut w, eb, cx); + } + } + for m in &model.modules { + for e in &m.enums { + if e.is_rich() { + render_rich_enum(&mut w, cx, e); + } else { + render_enum(&mut w, e); + } + } + for s in &m.structs { + render_record(&mut w, cx, s); + } + for cb in &m.callback_interfaces { + render_callback_interface(&mut w, m, cb, cx); + } + for i in &m.interfaces { + render_interface(&mut w, i, m.error.as_ref(), cx); + } + } + for m in &model.modules { + render_module_class(&mut w, m, strip_module_prefix, cx); + } + render_native_methods(&mut w, model); + + let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); + out.push_str(&tidy(&w.finish())); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); + out +} + +/// Drop blank lines directly before a closing brace. +fn tidy(text: &str) -> String { + let lines: Vec<&str> = text.lines().collect(); + let mut out = String::with_capacity(text.len()); + for (i, line) in lines.iter().enumerate() { + let next_closes = lines + .get(i + 1) + .is_some_and(|n| n.trim_start().starts_with('}')); + if line.is_empty() && next_closes { + continue; + } + out.push_str(line); + out.push('\n'); + } + out +} + +#[cfg(test)] +mod tests; diff --git a/crates/weaveffi-gen/src/targets/dotnet/package.rs b/crates/weaveffi-gen/src/targets/dotnet/package.rs new file mode 100644 index 00000000..e34dcbef --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/package.rs @@ -0,0 +1,139 @@ +//! Package-surface renderers: the `.csproj` (which carries the NuGet +//! metadata `dotnet pack` needs) and the README. +//! +//! Every interpolated user string routes through the shared +//! [`xml_escape`](crate::manifest::xml_escape), so markup-sensitive +//! characters can't corrupt the XML. + +use crate::manifest::xml_escape; +use crate::package::PackageContext; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::pkg::Identity; + +/// The names a generated .NET project is built from. +pub(crate) struct Project<'a> { + /// The library identity. + pub identity: &'a Identity, + /// The C# namespace, which is also the assembly name and NuGet id. + pub namespace: &'a str, + /// The native library base name the bindings load. + pub library: &'a str, + /// The IDL basename for generated-file preludes. + pub input_basename: &'a str, +} + +/// Render the `.csproj`. `extra` is spliced in after the main property group +/// (the native-asset item group of a packaged build). +pub(crate) fn render_csproj(p: &Project<'_>, filename: &str, extra: &str) -> String { + let id = p.identity; + let ns = xml_escape(p.namespace); + let mut meta = format!( + " {}\n {}\n", + xml_escape(&id.version), + xml_escape(&id.description_or_default()) + ); + if !id.authors.is_empty() { + meta.push_str(&format!( + " {}\n", + xml_escape(&id.authors.join(", ")) + )); + } + if let Some(license) = &id.license { + meta.push_str(&format!( + " {}\n", + xml_escape(license) + )); + } + if let Some(url) = id.homepage.as_ref().or(id.repository.as_ref()) { + meta.push_str(&format!( + " {}\n", + xml_escape(url) + )); + } + if let Some(url) = &id.repository { + meta.push_str(&format!( + " {}\n", + xml_escape(url) + )); + } + format!( + r#"{prelude} + + + net8.0 + {ns} + {ns} + {ns} +{meta} enable + true + true + + $(NoWarn);CS0618 + +{extra} + + +{trailer}"#, + prelude = render_prelude(CommentStyle::Xml, p.input_basename), + trailer = render_trailer(CommentStyle::Xml, filename), + ) +} + +/// The `` shipping every bundled native library under +/// `runtimes//native/`, where NuGet resolves it at restore time. +pub(crate) const NATIVE_ASSETS: &str = " + + PreserveNewest + + +"; + +/// Render the README. A packaged build lists its bundled runtime +/// identifiers. +pub(crate) fn render_readme(p: &Project<'_>, ctx: Option<&PackageContext>) -> String { + let ns = p.namespace; + let env = p.identity.library_env_var(); + let (mac, linux, win) = p.identity.library_files(); + let mut out = render_prelude(CommentStyle::Xml, p.input_basename); + out.push_str(&format!( + "# {ns} (.NET)\n\n\ + {desc}\n\n\ + The `{ns}` namespace binds the `{lib}` native library through\n\ + source-generated P/Invoke (`[LibraryImport]`), so it's trim- and\n\ + AOT-friendly.\n\n", + desc = p.identity.description_or_default(), + lib = p.library, + )); + match ctx { + Some(ctx) => { + let rids: Vec = ctx + .binaries + .platforms() + .filter_map(|pl| pl.nuget_rid()) + .map(|rid| format!("- `{rid}`")) + .collect(); + out.push_str(&format!( + "## Install\n\n```bash\ndotnet add package {ns}\n```\n\n\ + The package bundles the native library for these runtimes:\n\n{}\n\n", + rids.join("\n") + )); + } + None => { + out.push_str(&format!( + "## Build\n\n```bash\ndotnet build\n```\n\n\ + Reference this project from yours (`dotnet add reference {ns}.csproj`)\n\ + or pack it (`dotnet pack -c Release`).\n\n" + )); + } + } + out.push_str(&format!( + "## Loading the native library\n\n\ + The bindings load `{lib}` with the platform's normal rules\n\ + (`{mac}`, `{linux}`, or `{win}` next to the app or on the\n\ + library search path). Set `{env}` to a full path to load a\n\ + specific file instead.\n\n", + lib = p.library, + )); + out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); + out +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/pinvoke.rs b/crates/weaveffi-gen/src/targets/dotnet/pinvoke.rs new file mode 100644 index 00000000..4a346e50 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/pinvoke.rs @@ -0,0 +1,156 @@ +//! The generated half of `NativeMethods`: the contract checksum check and one +//! `[LibraryImport]` per lowered C symbol, matching each callable's shape. +//! +//! Every slot is blittable (`byte*`, `nuint`, `FfiError*`, function +//! pointers) except object slots, which take the interface's `SafeHandle` +//! subclass: the source-generated stub adds a reference to the handle for +//! the duration of the call, so a wrapper that's disposed or collected +//! mid-call can't free the native object under it. + +use std::collections::HashMap; + +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::abi::AbiParam; +use weaveffi_model::model::{ + checksum_symbol, AbiFn, BindingModel, CallShape, FnBinding, InterfaceBinding, +}; +use weaveffi_model::plan::ArgPass; + +use crate::targets::dotnet::types::{cs_ctype, fn_pointer_type, safe_cs_name}; + +/// The C# type of an interface's `SafeHandle` subclass. +pub(crate) fn handle_type(interface: &str) -> String { + format!("{}.NativeHandle", local_type_name(interface)) +} + +/// Slot types that differ from the plain [`cs_ctype`] mapping for one +/// callable: the receiver and every object parameter take a `SafeHandle`. +fn slot_overrides(f: &FnBinding, owner: Option<&str>) -> HashMap { + let mut map = HashMap::new(); + if let (true, Some(owner)) = (f.has_self, owner) { + map.insert("self".to_string(), handle_type(owner)); + } + for p in &f.params { + if let ArgPass::Object { slot, .. } = p.arg_pass() { + let iface = + p.ty.interface_name() + .expect("object parameters name an interface"); + map.insert(slot.name.clone(), handle_type(iface)); + } + } + map +} + +/// One parameter declaration for `slot`, honoring `overrides`. +fn slot_decl( + slot: &AbiParam, + overrides: &HashMap, + callback: Option<&str>, +) -> String { + let ty = match (overrides.get(&slot.name), callback) { + (Some(ty), _) => ty.clone(), + (None, Some(cb)) if slot.name == "callback" => cb.to_string(), + _ => cs_ctype(&slot.ty), + }; + format!("{ty} {}", safe_cs_name(&slot.name)) +} + +/// Emit one `[LibraryImport]` declaration. +fn import(w: &mut CodeWriter, symbol: &str, ret: &str, params: &[String]) { + w.line(format!( + "[LibraryImport(LibName, EntryPoint = \"{symbol}\")]" + )); + w.line("[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]"); + w.line(format!( + "internal static partial {ret} {symbol}({});", + params.join(", ") + )); + w.blank(); +} + +/// Emit the import for one lowered function. +fn import_fn( + w: &mut CodeWriter, + abi: &AbiFn, + overrides: &HashMap, + callback: Option<&str>, +) { + let params: Vec = abi + .params + .iter() + .map(|s| slot_decl(s, overrides, callback)) + .collect(); + import(w, &abi.symbol, &cs_ctype(&abi.ret), ¶ms); +} + +/// Emit every import behind one callable: the sync symbol; the async +/// launcher (its `callback` slot typed as the completion function pointer); +/// or the iterator launcher, `_next` (taking the iterator's `SafeHandle`), +/// and `_destroy`. +fn render_callable(w: &mut CodeWriter, f: &FnBinding, owner: Option<&str>) { + let overrides = slot_overrides(f, owner); + match &f.shape { + CallShape::Sync(abi) => import_fn(w, abi, &overrides, None), + CallShape::Async(a) => { + let cb = fn_pointer_type(&a.callback_params, &weaveffi_model::abi::CType::Void); + import_fn(w, &a.launch, &overrides, Some(&cb)); + } + CallShape::Iterator(it) => { + import_fn(w, &it.launch, &overrides, None); + let next_overrides = + HashMap::from([("iter".to_string(), "FfiIteratorHandle".to_string())]); + import_fn(w, &it.next, &next_overrides, None); + import(w, &it.destroy_symbol, "void", &["IntPtr iter".to_string()]); + } + } +} + +/// The imports behind one interface: `_clone`, `_destroy`, and every member. +fn render_interface(w: &mut CodeWriter, i: &InterfaceBinding) { + import( + w, + &i.clone_symbol, + "IntPtr", + &[format!("{} self", handle_type(&i.name))], + ); + import(w, &i.destroy_symbol, "void", &["IntPtr self".to_string()]); + for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) { + render_callable(w, f, Some(&i.name)); + } +} + +/// Render the generated half of `NativeMethods`: the checksum check run at +/// load, each root's checksum import, and every interface and function +/// import in declaration order. +pub(crate) fn render_native_methods(w: &mut CodeWriter, model: &BindingModel) { + w.line("internal static unsafe partial class NativeMethods"); + w.line("{"); + w.indent(); + w.line("static partial void VerifyChecksums()"); + w.block("{", "}", |w| { + for root in model.roots() { + let checksum = root.checksum.expect("top-level modules carry a checksum"); + let symbol = checksum_symbol(&model.prefix, &root.name); + w.line(format!( + "VerifyChecksum(\"{}\", &{symbol}, 0x{checksum:016x}UL);", + root.name + )); + } + }); + w.blank(); + for root in model.roots() { + import(w, &checksum_symbol(&model.prefix, &root.name), "ulong", &[]); + } + for m in &model.modules { + for i in &m.interfaces { + render_interface(w, i); + } + for f in &m.functions { + render_callable(w, f, None); + } + } + w.dedent(); + w.line("}"); + w.blank(); +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/runtime.rs b/crates/weaveffi-gen/src/targets/dotnet/runtime.rs new file mode 100644 index 00000000..79eebdd1 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/runtime.rs @@ -0,0 +1,156 @@ +//! The fixed runtime (`runtime/Runtime.cs`, spliced with the library's +//! names) and the per-domain typed exceptions. + +use crate::codegen::CodeWriter; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use heck::ToUpperCamelCase; +use weaveffi_model::errors; +use weaveffi_model::model::{BindingModel, ErrorBinding, ABI_VERSION}; + +use crate::targets::dotnet::codec::emit_read; +use crate::targets::dotnet::docs::write_doc; +use crate::targets::dotnet::types::{cs_str, xml_text, Cx}; + +/// The runtime source shared by every generated library. +const RUNTIME_CS: &str = include_str!("runtime/Runtime.cs"); + +/// The names a generated library splices into the fixed runtime. +pub(crate) struct RuntimeNames<'a> { + /// The C# namespace. + pub namespace: &'a str, + /// The base exception class (see [`base_exception_name`]). + pub exception: &'a str, + /// The C symbol prefix. + pub prefix: &'a str, + /// The native library base name P/Invoke loads. + pub library: &'a str, + /// The `{PREFIX}_LIBRARY` override variable. + pub library_env: &'a str, +} + +/// Render `Runtime.cs`: the fixed runtime with every placeholder replaced. +pub(crate) fn render_runtime( + names: &RuntimeNames<'_>, + input_basename: &str, + filename: &str, +) -> String { + let body = RUNTIME_CS + .replace("{{NAMESPACE}}", names.namespace) + .replace("{{EXCEPTION}}", names.exception) + .replace("{{PREFIX}}", names.prefix) + .replace("{{LIBRARY}}", names.library) + .replace("{{LIBRARY_ENV}}", names.library_env) + .replace("{{ABI_VERSION}}", &ABI_VERSION.to_string()); + format!( + "{}{body}\n{}", + render_prelude(CommentStyle::DoubleSlash, input_basename), + render_trailer(CommentStyle::DoubleSlash, filename) + ) +} + +/// The base exception every failure derives from: `NativeException`, unless +/// the API declares a type of that name, in which case the namespace +/// qualifies it (`KvstoreNativeException`). +pub(crate) fn base_exception_name(model: &BindingModel, namespace: &str) -> String { + const NAME: &str = "NativeException"; + let taken = model.modules.iter().any(|m| { + m.structs.iter().any(|s| s.name == NAME) + || m.enums.iter().any(|e| e.name == NAME) + || m.interfaces.iter().any(|i| i.name == NAME) + || m.error + .as_ref() + .is_some_and(|e| dotnet_exception_name(e) == NAME) + }); + if taken { + format!("{namespace}{NAME}") + } else { + NAME.to_string() + } +} + +/// The C# exception class for one error domain: the domain stem with one +/// `Exception` suffix, so `KvError` becomes `KvException`. +pub(crate) fn dotnet_exception_name(eb: &ErrorBinding) -> String { + errors::exception_type_name(&eb.type_name) +} + +/// One typed exception class per declared error domain, deriving from the +/// base exception. Each code is a `public const int`, and `FromError` maps a +/// raw error to the typed exception, decoding any payload fields into +/// `Data` (keyed by the IDL field name). Undeclared codes (the negative +/// runtime codes) fall back to the base exception's mapping. +pub(crate) fn render_domain_exception(w: &mut CodeWriter, eb: &ErrorBinding, cx: Cx<'_>) { + let exc = dotnet_exception_name(eb); + let base = cx.base; + w.line(format!( + "/// Typed exception for the {} error domain (module {}).", + eb.type_name, + eb.owner_path.replace('_', ".") + )); + w.line(format!("public class {exc} : {base}")); + w.block("{", "}", |w| { + for c in &eb.codes { + if c.doc.is_some() { + write_doc(w, &c.doc); + } else { + w.line(format!("/// {}", xml_text(&c.message))); + } + w.line(format!( + "public const int {} = {};", + errors::pascal(&c.name), + c.value + )); + w.blank(); + } + w.line("/// Creates an exception carrying a domain error code."); + w.line(format!( + "public {exc}(int code, string message) : base(code, message)" + )); + w.line("{"); + w.line("}"); + w.blank(); + w.line( + "internal static new Exception FromError(int code, string message, byte[]? payload)", + ); + w.block("{", "}", |w| { + w.line("switch (code)"); + w.block("{", "}", |w| { + for c in &eb.codes { + let default = cs_str(&c.message); + let ctor = format!( + "new {exc}(code, string.IsNullOrEmpty(message) ? \"{default}\" : message)" + ); + w.line(format!("case {}:", errors::pascal(&c.name))); + if c.fields.is_empty() { + w.indent(); + w.line(format!("return {ctor};")); + w.dedent(); + continue; + } + w.block("{", "}", |w| { + w.line(format!("var e = {ctor};")); + w.line("if (payload != null)"); + w.block("{", "}", |w| { + w.line("var reader = new FfiBufferReader(payload);"); + for f in &c.fields { + let var = format!("f{}", f.name.to_upper_camel_case()); + emit_read(w, cx, &f.ty, &var, "reader", 0); + w.line(format!("e.Data[\"{}\"] = {var};", cs_str(&f.name))); + } + w.line("reader.ExpectEnd();"); + }); + w.line("return e;"); + }); + } + w.line("default:"); + w.indent(); + w.line(format!( + "return {}.FromError(code, message, payload);", + cx.ty(base) + )); + w.dedent(); + }); + }); + }); + w.blank(); +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/runtime/Runtime.cs b/crates/weaveffi-gen/src/targets/dotnet/runtime/Runtime.cs new file mode 100644 index 00000000..faf30ec4 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/runtime/Runtime.cs @@ -0,0 +1,689 @@ +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace {{NAMESPACE}}; + +/// Raised for a failure the native library reports that isn't a typed +/// domain error: producer bugs, panics, marshalling failures, and exceptions +/// thrown by a callback-interface implementation. Every typed domain +/// exception derives from it. +public class {{EXCEPTION}} : Exception +{ + /// An untyped producer error. + public const int GenericErrorCode = -1; + /// The producer panicked; the message carries the panic text. + public const int PanicErrorCode = -2; + /// An argument couldn't be lifted by the producer. + public const int MarshalErrorCode = -3; + /// A callback-interface implementation threw; the message carries + /// the original exception's message. + public const int ForeignErrorCode = -4; + /// The call was cancelled. Surfaces as + /// , never as this type. + public const int CancelledErrorCode = -5; + + /// The error code the native library reported. + public int Code { get; } + + /// Creates an exception carrying a native error code. + public {{EXCEPTION}}(int code, string message) : base(message) + { + Code = code; + } + + /// Maps a runtime error code to its exception: cancellation to + /// , anything else to this + /// type. + internal static Exception FromError(int code, string message, byte[]? payload) + { + if (code == CancelledErrorCode) + { + return new OperationCanceledException(message); + } + return new {{EXCEPTION}}(code, message); + } +} + +/// Maps a native error code, message, and serialized payload to the +/// exception a call throws. +internal delegate Exception FfiErrorMap(int code, string message, byte[]? payload); + +/// The native {{PREFIX}}_error slot. +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct FfiError +{ + public int Code; + public byte* Message; + public byte* PayloadPtr; + public nuint PayloadLen; +} + +/// Marshalling helpers shared by every generated call. +internal static unsafe class Ffi +{ + /// The UTF-8 bytes of a string argument. The native side borrows + /// them as a (pointer, length) pair, so interior NULs survive. + internal static byte[] Utf8(string value) + { + return Encoding.UTF8.GetBytes(value); + } + + /// Decodes a borrowed UTF-8 run; the native side keeps + /// ownership. + internal static string ReadString(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? "" : Encoding.UTF8.GetString(ptr, checked((int)len)); + } + + /// Copies a borrowed byte run; the native side keeps + /// ownership. + internal static byte[] ReadBytes(byte* ptr, nuint len) + { + return ptr == null || len == 0 ? Array.Empty() : new ReadOnlySpan(ptr, checked((int)len)).ToArray(); + } + + /// Decodes a returned UTF-8 string and releases it. + internal static string TakeString(byte* ptr, nuint len) + { + try + { + return ReadString(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies returned bytes and releases them. + internal static byte[] TakeBytes(byte* ptr, nuint len) + { + try + { + return ReadBytes(ptr, len); + } + finally + { + NativeMethods.FreeBytes(ptr, len); + } + } + + /// Copies a returned value buffer, releases it, and returns a + /// reader over the copy. + internal static FfiBufferReader TakeBuffer(byte* ptr, nuint len) + { + return new FfiBufferReader(TakeBytes(ptr, len)); + } + + /// Releases a caller-owned error slot and returns the exception + /// it described. + internal static Exception TakeError(FfiError* err, FfiErrorMap map) + { + var code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorClear(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + /// Releases a heap-boxed async error and returns the exception it + /// described. + internal static Exception TakeBoxedError(FfiError* err, FfiErrorMap map, out int code) + { + code = err->Code; + var message = ErrorMessage(err); + var payload = ReadBytes(err->PayloadPtr, err->PayloadLen); + NativeMethods.ErrorFree(err); + return map(code, message, payload.Length == 0 ? null : payload); + } + + private static string ErrorMessage(FfiError* err) + { + return Marshal.PtrToStringUTF8((IntPtr)err->Message) ?? ""; + } + + /// Reports a callback-interface failure to the producer through + /// out_err. The producer copies the message. + internal static void SetForeignError(FfiError* err, Exception e) + { + var bytes = Encoding.UTF8.GetBytes(e.Message + "\0"); + fixed (byte* message = bytes) + { + NativeMethods.ErrorSet(err, {{EXCEPTION}}.ForeignErrorCode, message); + } + } + + /// Pins a callback-interface implementation for the producer and + /// returns the ctx it hands back to every trampoline. The + /// producer releases it through the vtable's free. + internal static IntPtr Register(object implementation) + { + return GCHandle.ToIntPtr(GCHandle.Alloc(implementation)); + } + + /// Releases a ctx the producer never received (the call + /// failed before reaching it) or no longer holds. + internal static void Unregister(IntPtr ctx) + { + GCHandle.FromIntPtr(ctx).Free(); + } + + /// The implementation behind a trampoline's ctx. + internal static T Target(IntPtr ctx) where T : class + { + return (T)GCHandle.FromIntPtr(ctx).Target!; + } +} + +/// Imports of the native runtime surface plus the load-time contract +/// check. The generated API declares its own imports in the other half of +/// this partial class. +internal static unsafe partial class NativeMethods +{ + /// The native library's base name. + internal const string LibName = "{{LIBRARY}}"; + + /// The environment variable naming an explicit library + /// path. + internal const string LibraryEnvVar = "{{LIBRARY_ENV}}"; + + /// The C ABI revision these bindings were generated + /// against. + internal const uint AbiVersion = {{ABI_VERSION}}; + + // Runs before the first native call through this class, so a library + // built for another ABI revision or another contract fails here instead + // of misreading memory later. + static NativeMethods() + { + try + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, Resolve); + } + catch (InvalidOperationException) + { + // The assembly already has a resolver; default probing still applies. + } + uint found; + try + { + found = AbiVersionOf(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't export {{PREFIX}}_abi_version; these bindings expect ABI revision {AbiVersion}", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"ABI mismatch: these bindings expect revision {AbiVersion} but the loaded {LibName} library reports revision {found}"); + } + VerifyChecksums(); + } + + static partial void VerifyChecksums(); + + private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? searchPath) + { + if (name != LibName) + { + return IntPtr.Zero; + } + var path = Environment.GetEnvironmentVariable(LibraryEnvVar); + return string.IsNullOrEmpty(path) ? IntPtr.Zero : NativeLibrary.Load(path); + } + + /// Fails the load when module was + /// generated from a different contract than the loaded library + /// implements. + private static void VerifyChecksum(string module, delegate* checksum, ulong expected) + { + ulong found; + try + { + found = checksum(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded {LibName} library doesn't implement module '{module}'", e); + } + if (found != expected) + { + throw new InvalidOperationException( + $"contract mismatch for module '{module}': these bindings expect checksum 0x{expected:x16} but the loaded {LibName} library reports 0x{found:x16}; regenerate the bindings for this library"); + } + } + + [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_abi_version")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial uint AbiVersionOf(); + + [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_error_set")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorSet(FfiError* err, int code, byte* message); + + [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_error_clear")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorClear(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_error_free")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void ErrorFree(FfiError* err); + + [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_free_bytes")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void FreeBytes(byte* ptr, nuint len); + + [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_cancel_token_create")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial IntPtr CancelTokenCreate(); + + [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_cancel_token_cancel")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenCancel(IntPtr token); + + [LibraryImport(LibName, EntryPoint = "{{PREFIX}}_cancel_token_destroy")] + [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] + internal static partial void CancelTokenDestroy(IntPtr token); +} + +/// One pending async call: the task it completes, the native +/// context that finds it again, and, for a cancellable function, the +/// native cancel token linked to the caller's +/// . +internal sealed class FfiCall +{ + private readonly TaskCompletionSource _tcs = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationToken _cancellation; + private readonly object _gate = new object(); + private GCHandle _self; + private IntPtr _token; + private CancellationTokenRegistration _registration; + private bool _completed; + private bool _cancelling; + + internal FfiCall(CancellationToken cancellation = default) + { + _cancellation = cancellation; + _self = GCHandle.Alloc(this); + } + + /// The task the completion resolves. + internal Task Task => _tcs.Task; + + /// The context argument for the launcher. + internal IntPtr Context => GCHandle.ToIntPtr(_self); + + /// Creates the native cancel token for the launcher and cancels + /// it when the caller's fires. + internal IntPtr CancelToken() + { + _token = NativeMethods.CancelTokenCreate(); + if (_cancellation.CanBeCanceled) + { + _registration = _cancellation.Register(static state => ((FfiCall)state!).Cancel(), this); + } + return _token; + } + + /// Recovers the call behind a completion's context. Runs + /// exactly once per launch, on a producer thread. + internal static FfiCall Complete(IntPtr context) + { + var handle = GCHandle.FromIntPtr(context); + var call = (FfiCall)handle.Target!; + handle.Free(); + call.Release(); + return call; + } + + /// Releases everything when the launcher threw before the + /// producer took the call, so no completion will arrive. + internal void Abandon() + { + _self.Free(); + Release(); + } + + internal void SetResult(T value) + { + _tcs.TrySetResult(value); + } + + internal void SetException(Exception e) + { + _tcs.TrySetException(e); + } + + /// Faults (or cancels, for the cancelled code) the task from a + /// heap-boxed error, releasing the box. + internal unsafe void SetError(FfiError* err, FfiErrorMap map) + { + var e = Ffi.TakeBoxedError(err, map, out var code); + if (code == {{EXCEPTION}}.CancelledErrorCode) + { + _tcs.TrySetCanceled(_cancellation); + } + else + { + _tcs.TrySetException(e); + } + } + + // The registration callback and the completion race: whichever runs + // last destroys the consumer's token reference, so a cancel never + // touches a destroyed token. + private void Cancel() + { + lock (_gate) + { + if (_completed) + { + return; + } + _cancelling = true; + } + NativeMethods.CancelTokenCancel(_token); + bool destroy; + lock (_gate) + { + _cancelling = false; + destroy = _completed; + } + if (destroy) + { + NativeMethods.CancelTokenDestroy(_token); + } + } + + private void Release() + { + if (_token == IntPtr.Zero) + { + return; + } + bool destroy; + lock (_gate) + { + _completed = true; + destroy = !_cancelling; + } + if (destroy) + { + _registration.Dispose(); + NativeMethods.CancelTokenDestroy(_token); + } + } +} + +/// Owns one native iterator and destroys it exactly once, when +/// enumeration ends, the enumerator is disposed, or the garbage collector +/// finalizes an abandoned one. +internal sealed unsafe class FfiIteratorHandle : SafeHandle +{ + private readonly delegate* _destroy; + + internal FfiIteratorHandle(IntPtr iterator, delegate* destroy) + : base(IntPtr.Zero, true) + { + _destroy = destroy; + SetHandle(iterator); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + _destroy(handle); + return true; + } +} + +/// A lazily streamed sequence backed by a native iterator. It can be +/// enumerated once; each MoveNext pulls one item. +internal sealed unsafe class FfiSequence : IEnumerable, IEnumerator +{ + private readonly FfiIteratorHandle _iterator; + private readonly delegate* _next; + private int _started; + private T _current = default!; + + internal FfiSequence(FfiIteratorHandle iterator, delegate* next) + { + _iterator = iterator; + _next = next; + } + + public T Current => _current; + + object? IEnumerator.Current => _current; + + public IEnumerator GetEnumerator() + { + if (Interlocked.Exchange(ref _started, 1) != 0) + { + throw new InvalidOperationException("this sequence can be enumerated only once"); + } + return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool MoveNext() + { + if (_iterator.IsClosed) + { + return false; + } + if (_next(_iterator, out _current)) + { + return true; + } + _iterator.Dispose(); + return false; + } + + public void Reset() + { + throw new NotSupportedException("a native iterator can't be reset"); + } + + public void Dispose() + { + _iterator.Dispose(); + } +} + +/// Serializes values into the value-buffer wire format +/// (little-endian, packed). +internal sealed class FfiBufferWriter +{ + private byte[] _buf = new byte[64]; + private int _len; + + /// The bytes written so far. + internal ReadOnlySpan Written => _buf.AsSpan(0, _len); + + /// The number of bytes written so far. + internal int Length => _len; + + private Span Grow(int extra) + { + if (_len + extra > _buf.Length) + { + Array.Resize(ref _buf, Math.Max(_buf.Length * 2, _len + extra)); + } + var span = _buf.AsSpan(_len, extra); + _len += extra; + return span; + } + + internal void WriteBool(bool v) => WriteU8(v ? (byte)1 : (byte)0); + + internal void WriteI8(sbyte v) => WriteU8((byte)v); + + internal void WriteU8(byte v) => Grow(1)[0] = v; + + internal void WriteI16(short v) => System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(Grow(2), v); + + internal void WriteU16(ushort v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), v); + + internal void WriteI32(int v) => System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(Grow(4), v); + + internal void WriteU32(uint v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), v); + + internal void WriteI64(long v) => System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(Grow(8), v); + + internal void WriteU64(ulong v) => System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), v); + + internal void WriteF32(float v) => System.Buffers.Binary.BinaryPrimitives.WriteSingleLittleEndian(Grow(4), v); + + internal void WriteF64(double v) => System.Buffers.Binary.BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), v); + + internal void WriteLen(int len) => WriteU32((uint)len); + + internal void WriteString(string v) + { + var count = Encoding.UTF8.GetByteCount(v); + WriteLen(count); + Encoding.UTF8.GetBytes(v, Grow(count)); + } + + internal void WriteBytes(byte[] v) + { + WriteLen(v.Length); + v.CopyTo(Grow(v.Length)); + } + + /// Writes an object token. The token must carry its own strong + /// reference (a fresh clone), which the reader adopts. + internal void WriteObject(IntPtr token) => WriteU64((ulong)(nuint)token); +} + +/// Decodes values from the value-buffer wire format. A malformed +/// buffer is a producer or consumer bug and throws +/// . +internal sealed class FfiBufferReader +{ + private static readonly Encoding Utf8Strict = new UTF8Encoding(false, true); + + private readonly byte[] _data; + private int _pos; + + internal FfiBufferReader(byte[] data) + { + _data = data; + } + + private static InvalidOperationException Malformed(string what) + { + return new InvalidOperationException("malformed value buffer: " + what); + } + + private ReadOnlySpan Take(int n) + { + if (_data.Length - _pos < n) + { + throw Malformed("buffer exhausted"); + } + var span = _data.AsSpan(_pos, n); + _pos += n; + return span; + } + + internal bool ReadBool() + { + var b = ReadU8(); + if (b > 1) + { + throw Malformed("invalid bool byte"); + } + return b == 1; + } + + internal sbyte ReadI8() => (sbyte)ReadU8(); + + internal byte ReadU8() => Take(1)[0]; + + internal short ReadI16() => System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(Take(2)); + + internal ushort ReadU16() => System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); + + internal int ReadI32() => System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(Take(4)); + + internal uint ReadU32() => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); + + internal long ReadI64() => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(Take(8)); + + internal ulong ReadU64() => System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(Take(8)); + + internal float ReadF32() => System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(Take(4)); + + internal double ReadF64() => System.Buffers.Binary.BinaryPrimitives.ReadDoubleLittleEndian(Take(8)); + + /// A collection count. Elements can be zero-sized, so the count + /// isn't checked against the remaining bytes; callers cap their + /// preallocation with . + internal int ReadLen() + { + var len = ReadU32(); + if (len > int.MaxValue) + { + throw Malformed("length prefix out of range"); + } + return (int)len; + } + + /// A preallocation size for elements + /// that a malformed count can't inflate. + internal int Capacity(int count) => Math.Min(count, _data.Length - _pos); + + internal string ReadString() + { + var bytes = Take(ReadLen()); + try + { + return Utf8Strict.GetString(bytes); + } + catch (ArgumentException) + { + throw Malformed("string is not valid UTF-8"); + } + } + + internal byte[] ReadBytes() => Take(ReadLen()).ToArray(); + + /// Reads an object token carrying one strong reference. + internal IntPtr ReadObject() + { + var token = ReadU64(); + if (token == 0) + { + throw Malformed("null object token"); + } + return (IntPtr)(nint)token; + } + + internal void ExpectEnd() + { + if (_pos != _data.Length) + { + throw Malformed("trailing bytes"); + } + } +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/tests.rs b/crates/weaveffi-gen/src/targets/dotnet/tests.rs new file mode 100644 index 00000000..2b666f8e --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/tests.rs @@ -0,0 +1,506 @@ +//! Rendering tests over a small hand-built API exercising every ABI +//! revision 3 shape: objects, nullable objects, objects in records and +//! lists, iterators of objects, a callback interface, async and cancellable +//! functions, and bytes parameters. + +use crate::backend::LanguageBackend; +use crate::package::PackageContext; +use crate::platform::{BinarySet, Platform}; +use camino::Utf8Path; +use weaveffi_model::ir::{ + Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, + TypeRef, +}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; +use weaveffi_model::validate::validate_api; + +use crate::targets::dotnet::{DotnetConfig, DotnetGenerator}; + +fn param(name: &str, ty: TypeRef) -> Param { + Param { + name: name.into(), + ty, + doc: None, + } +} + +fn func(name: &str, params: Vec, returns: Option) -> Function { + Function { + name: name.into(), + params, + returns, + doc: None, + throws: false, + r#async: false, + cancellable: false, + deprecated: None, + } +} + +fn field(name: &str, ty: TypeRef) -> StructField { + StructField { + name: name.into(), + ty, + doc: None, + } +} + +fn named(name: &str) -> TypeRef { + TypeRef::Named(name.into()) +} + +/// A `bus` module with a `Ticker` interface, a `Bundle` record holding +/// objects, a `Subscriber` callback interface, and free functions covering +/// nullable objects, an iterator of objects, async and cancellable calls, a +/// callback-interface parameter, and bytes. +fn fixture() -> ResolvedApi { + let ticker = InterfaceDef { + name: "Ticker".into(), + doc: Some("A counter the producer owns.".into()), + deprecated: None, + constructors: vec![func("new", vec![param("start", TypeRef::I32)], None)], + methods: vec![func("value", vec![], Some(TypeRef::I32))], + statics: vec![], + }; + let bundle = StructDef { + name: "Bundle".into(), + doc: None, + deprecated: None, + fields: vec![ + field("primary", named("Ticker")), + field("others", TypeRef::List(Box::new(named("Ticker")))), + field("label", TypeRef::StringUtf8), + ], + }; + let subscriber = CallbackInterfaceDef { + name: "Subscriber".into(), + doc: Some("Receives bus events.".into()), + deprecated: None, + methods: vec![ + func( + "on_message", + vec![ + param("text", TypeRef::StringUtf8), + param("weight", TypeRef::I32), + param("bundle", named("Bundle")), + param("ticker", named("Ticker")), + ], + Some(TypeRef::Bool), + ), + func("on_close", vec![param("code", TypeRef::I64)], None), + ], + }; + let module = Module { + name: "bus".into(), + doc: None, + functions: vec![ + func( + "maybe_ticker", + vec![param("input", TypeRef::Optional(Box::new(named("Ticker"))))], + Some(TypeRef::Optional(Box::new(named("Ticker")))), + ), + func( + "tickers", + vec![], + Some(TypeRef::Iterator(Box::new(named("Ticker")))), + ), + func( + "subscribe", + vec![param("listener", named("Subscriber"))], + None, + ), + Function { + r#async: true, + ..func("spawn", vec![], Some(named("Ticker"))) + }, + Function { + r#async: true, + cancellable: true, + ..func("wait", vec![param("ms", TypeRef::I64)], Some(TypeRef::I64)) + }, + func( + "roundtrip_bundle", + vec![param("b", named("Bundle"))], + Some(named("Bundle")), + ), + // A method named like the type it returns. + func("bundle", vec![], Some(named("Bundle"))), + func( + "digest", + vec![ + param("data", TypeRef::Bytes), + param("label", TypeRef::StringUtf8), + ], + Some(TypeRef::StringUtf8), + ), + ], + interfaces: vec![ticker], + callback_interfaces: vec![subscriber], + structs: vec![bundle], + enums: vec![], + errors: None, + modules: vec![], + }; + validate_api( + Api { + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![module], + }, + None, + ) + .expect("fixture validates") + .with_identity(Identity::named("bus-lib")) +} + +/// Every generated file's text, keyed by file name. +fn render_files(config: &DotnetConfig) -> Vec<(String, String)> { + let api = fixture(); + let model = BindingModel::build(&api); + DotnetGenerator + .files(&api, &model, Utf8Path::new("out"), config) + .into_iter() + .map(|f| (f.path.file_name().unwrap().to_string(), f.contents)) + .collect() +} + +fn file(files: &[(String, String)], name: &str) -> String { + files + .iter() + .find(|(n, _)| n == name) + .unwrap_or_else(|| { + panic!( + "no {name} in {:?}", + files.iter().map(|f| &f.0).collect::>() + ) + }) + .1 + .clone() +} + +fn render() -> String { + file(&render_files(&DotnetConfig::default()), "BusLib.cs") +} + +fn assert_has(out: &str, needle: &str) { + assert!(out.contains(needle), "missing `{needle}` in:\n{out}"); +} + +#[test] +fn names_come_from_the_identity() { + let files = render_files(&DotnetConfig::default()); + let names: Vec<&str> = files.iter().map(|f| f.0.as_str()).collect(); + assert_eq!( + names, + ["BusLib.cs", "Runtime.cs", "BusLib.csproj", "README.md"] + ); + let runtime = file(&files, "Runtime.cs"); + assert_has(&runtime, "namespace BusLib;"); + assert_has(&runtime, "internal const string LibName = \"bus_lib\";"); + assert_has( + &runtime, + "internal const string LibraryEnvVar = \"BUS_LIB_LIBRARY\";", + ); + assert_has(&runtime, "EntryPoint = \"bus_lib_abi_version\""); + assert_has(&runtime, "internal const uint AbiVersion = 3;"); + assert_has(&runtime, "public class NativeException : Exception"); + assert_has(&runtime, "public const int CancelledErrorCode = -5;"); + assert!( + !runtime.contains("{{"), + "unreplaced placeholder:\n{runtime}" + ); + let csproj = file(&files, "BusLib.csproj"); + assert_has(&csproj, "BusLib"); + assert_has(&csproj, "BusLib"); + for (name, text) in &files { + let body: String = text + .lines() + .filter(|l| !l.contains("Generated by WeaveFFI") && !l.contains("weaveffi generate")) + .collect(); + assert!( + !body.to_lowercase().contains("weaveffi"), + "{name} is branded:\n{text}" + ); + } +} + +#[test] +fn configured_namespace_wins() { + let config = DotnetConfig { + namespace: Some("Acme.Bus".into()), + ..DotnetConfig::default() + }; + let files = render_files(&config); + assert_has(&file(&files, "Acme.Bus.cs"), "namespace Acme.Bus;"); + assert_has( + &file(&files, "Acme.Bus.csproj"), + "Acme.Bus", + ); +} + +#[test] +fn load_time_check_covers_every_root_checksum() { + let out = render(); + assert_has(&out, "static partial void VerifyChecksums()"); + assert_has(&out, "VerifyChecksum(\"bus\", &bus_lib_bus_checksum, 0x"); + assert_has( + &out, + "internal static partial ulong bus_lib_bus_checksum();", + ); +} + +#[test] +fn interfaces_wrap_a_safe_handle() { + let out = render(); + assert_has( + &out, + "public sealed unsafe class Ticker : IDisposable, IEquatable", + ); + assert_has(&out, "internal sealed class NativeHandle : SafeHandle"); + assert_has(&out, "NativeMethods.bus_lib_bus_Ticker_destroy(handle);"); + assert!( + !out.contains("~Ticker()"), + "the SafeHandle is the finalizer" + ); + // Every import borrows through the handle, never a raw pointer. + assert_has( + &out, + "internal static partial int bus_lib_bus_Ticker_value(Ticker.NativeHandle self, FfiError* out_err);", + ); + assert_has( + &out, + "internal static partial IntPtr bus_lib_bus_Ticker_clone(Ticker.NativeHandle self);", + ); + assert_has( + &out, + "var ffiResult = NativeMethods.bus_lib_bus_Ticker_value(Handle, &ffiErr);", + ); + assert_has(&out, "public Ticker(int start)"); + assert_has(&out, "Handle = new NativeHandle(ffiResult);"); + assert_has( + &out, + "return NativeMethods.bus_lib_bus_Ticker_clone(Handle);", + ); +} + +#[test] +fn nullable_objects_pass_the_null_handle() { + let out = render(); + assert_has(&out, "public static Ticker? MaybeTicker(Ticker? input)"); + assert_has( + &out, + "NativeMethods.bus_lib_bus_maybe_ticker(input?.Handle ?? global::BusLib.Ticker.NativeHandle.Null, &ffiErr);", + ); + assert_has( + &out, + "return ffiResult == IntPtr.Zero ? null : global::BusLib.Ticker.Adopt(ffiResult);", + ); +} + +#[test] +fn objects_in_records_use_cloned_tokens_and_adopt_on_read() { + let out = render(); + assert_has(&out, "public sealed class Bundle"); + assert_has(&out, "writer.WriteObject(Primary.CloneHandle());"); + assert_has(&out, "writer.WriteObject(item0.CloneHandle());"); + assert_has( + &out, + "var fPrimary = global::BusLib.Ticker.Adopt(reader.ReadObject());", + ); + assert_has( + &out, + "var fOthersItem = global::BusLib.Ticker.Adopt(reader.ReadObject());", + ); +} + +#[test] +fn type_references_survive_a_member_of_the_same_name() { + let out = render(); + // Inside `Bus`, the method `Bundle()` would shadow the type `Bundle` in + // an expression, so static accesses go through the global namespace. + assert_has(&out, "public static Bundle Bundle()"); + assert_has( + &out, + "var ffiDecoded = global::BusLib.Bundle.ReadFrom(ffiDecodedReader);", + ); +} + +#[test] +fn strings_and_bytes_cross_as_pointer_and_length() { + let out = render(); + assert_has( + &out, + "internal static partial byte* bus_lib_bus_digest(byte* data_ptr, nuint data_len, byte* label_ptr, nuint label_len, nuint* out_len, FfiError* out_err);", + ); + assert_has( + &out, + "public static string Digest(ReadOnlySpan data, string label)", + ); + assert_has(&out, "var labelBytes = Ffi.Utf8(label);"); + assert_has(&out, "fixed (byte* dataPtr = data, labelPtr = labelBytes)"); + assert_has(&out, "return Ffi.TakeString(ffiResult, ffiOutLen);"); + // The byte[] overload forwards to the span one. + assert_has( + &out, + "public static string Digest(byte[] data, string label) => Digest((ReadOnlySpan)data, label);", + ); +} + +#[test] +fn iterators_stream_through_a_safe_handle() { + let out = render(); + assert_has(&out, "public static IEnumerable Tickers()"); + assert_has( + &out, + "return new FfiSequence(new FfiIteratorHandle(ffiResult, &NativeMethods.bus_lib_bus_TickersIterator_destroy), &NextTickers);", + ); + assert_has( + &out, + "private static bool NextTickers(FfiIteratorHandle iter, out Ticker item)", + ); + assert_has( + &out, + "internal static partial int bus_lib_bus_TickersIterator_next(FfiIteratorHandle iter, IntPtr* out_item, FfiError* out_err);", + ); + assert_has(&out, "item = global::BusLib.Ticker.Adopt(ffiItem);"); +} + +#[test] +fn async_calls_complete_through_an_unmanaged_callback() { + let out = render(); + assert_has(&out, "public static Task Spawn()"); + assert_has(&out, "var ffiCall = new FfiCall();"); + assert_has( + &out, + "NativeMethods.bus_lib_bus_spawn(&CompleteSpawn, ffiCall.Context);", + ); + assert_has( + &out, + "internal static partial void bus_lib_bus_spawn(delegate* unmanaged[Cdecl] callback, IntPtr context);", + ); + assert_has( + &out, + "private static void CompleteSpawn(IntPtr context, FfiError* err, IntPtr result)", + ); + assert_has( + &out, + "ffiCall.SetResult(global::BusLib.Ticker.Adopt(result));", + ); + assert_has(&out, "ffiCall.Abandon();"); +} + +#[test] +fn cancellable_calls_take_a_cancellation_token() { + let out = render(); + assert_has( + &out, + "public static Task Wait(long ms, CancellationToken cancellationToken = default)", + ); + assert_has(&out, "return Task.FromCanceled(cancellationToken);"); + assert_has(&out, "var ffiCall = new FfiCall(cancellationToken);"); + assert_has( + &out, + "NativeMethods.bus_lib_bus_wait(ms, ffiCall.CancelToken(), &CompleteWait, ffiCall.Context);", + ); + let runtime = file(&render_files(&DotnetConfig::default()), "Runtime.cs"); + assert_has(&runtime, "_tcs.TrySetCanceled(_cancellation);"); + assert_has(&runtime, "NativeMethods.CancelTokenCancel(_token);"); +} + +#[test] +fn callback_interfaces_render_a_static_vtable_of_trampolines() { + let out = render(); + assert_has(&out, "public interface ISubscriber"); + assert_has( + &out, + "bool OnMessage(string text, int weight, Bundle bundle, Ticker ticker);", + ); + assert_has( + &out, + "internal static unsafe class FfiVtable_bus_Subscriber", + ); + assert_has( + &out, + "public delegate* unmanaged[Cdecl] on_message;", + ); + assert_has(&out, "vtable->on_message = &OnMessageTrampoline;"); + assert_has(&out, "vtable->free = &FreeTrampoline;"); + assert_has( + &out, + "private static byte OnMessageTrampoline(IntPtr ctx, byte* text_ptr, nuint text_len, int weight, byte* bundle_ptr, nuint bundle_len, IntPtr ticker, FfiError* out_err)", + ); + assert_has( + &out, + "return (byte)(Ffi.Target(ctx).OnMessage(Ffi.ReadString(text_ptr, text_len), weight, arg2, global::BusLib.Ticker.Adopt(ticker)) ? 1 : 0);", + ); + assert_has(&out, "Ffi.SetForeignError(out_err, e);"); + // Arguments are borrowed: nothing is freed inside a trampoline. + let start = out.find("OnMessageTrampoline(IntPtr").unwrap(); + let end = out[start..].find("FreeTrampoline(IntPtr").unwrap() + start; + assert!(!out[start..end].contains("Take"), "{}", &out[start..end]); +} + +#[test] +fn passing_a_callback_interface_registers_it_after_encoding() { + let out = render(); + assert_has(&out, "public static void Subscribe(ISubscriber listener)"); + assert_has(&out, "var listenerCtx = Ffi.Register(listener);"); + assert_has( + &out, + "NativeMethods.bus_lib_bus_subscribe(listenerCtx, FfiVtable_bus_Subscriber.Pointer, &ffiErr);", + ); + // A call that never reaches the producer releases the registration. + assert_has(&out, "Ffi.Unregister(listenerCtx);"); +} + +#[test] +fn output_is_deterministic() { + assert_eq!( + render_files(&DotnetConfig::default()), + render_files(&DotnetConfig::default()) + ); +} + +#[test] +fn package_bundles_desktop_runtimes_only() { + let api = fixture(); + let model = BindingModel::build(&api); + let mut binaries = BinarySet::new("bus_lib"); + for p in Platform::ALL { + binaries.insert(p, format!("/tmp/{}/lib", p.id())); + } + let ctx = PackageContext { + binaries: &binaries, + input_basename: Some("bus"), + }; + let files = DotnetGenerator + .package( + &api, + &model, + &ctx, + Utf8Path::new("out"), + &DotnetConfig::default(), + ) + .expect("dotnet packages"); + let paths: Vec = files.iter().map(|f| f.path.to_string()).collect(); + let native: Vec<&String> = paths.iter().filter(|p| p.contains("/runtimes/")).collect(); + assert_eq!(native.len(), Platform::DESKTOP.len(), "{paths:?}"); + for rid in [ + "osx-arm64", + "osx-x64", + "linux-x64", + "linux-arm64", + "win-x64", + ] { + assert!( + paths + .iter() + .any(|p| p.contains(&format!("/runtimes/{rid}/native/"))), + "{paths:?}" + ); + } + assert!(!paths + .iter() + .any(|p| p.contains("android") || p.contains("wasm"))); +} diff --git a/crates/weaveffi-gen/src/targets/dotnet/types.rs b/crates/weaveffi-gen/src/targets/dotnet/types.rs new file mode 100644 index 00000000..5f9fc02b --- /dev/null +++ b/crates/weaveffi-gen/src/targets/dotnet/types.rs @@ -0,0 +1,173 @@ +//! C# type mapping: the idiomatic surface type of every IR type, the +//! blittable spelling of every C ABI type, identifier escaping, and +//! string-literal escaping. + +use crate::lang; +use crate::utils::local_type_name; +use heck::ToLowerCamelCase; +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::model::{FnBinding, Ty}; + +/// The names every renderer shares. +#[derive(Clone, Copy)] +pub(crate) struct Cx<'a> { + /// The C# namespace. + pub ns: &'a str, + /// The base exception class. + pub base: &'a str, +} + +impl Cx<'_> { + /// A user type qualified from the global namespace, for expression + /// contexts (`Card.ReadFrom(...)`) where a member of the enclosing class + /// with the same name (a `Card()` method) would otherwise shadow it. + pub(crate) fn ty(&self, name: &str) -> String { + format!("global::{}.{}", self.ns, local_type_name(name)) + } +} + +/// The C# interface a consumer implements for one callback interface: the +/// bare local name with the conventional `I` prefix (`ISubscriber` for +/// `Subscriber` or `bus.Subscriber`). +pub(crate) fn callback_interface_cs(name: &str) -> String { + format!("I{}", local_type_name(name)) +} + +/// The internal static class hosting one callback interface's vtable and +/// trampolines, named from the module path and the bare name so two modules +/// declaring the same callback interface name never collide. +pub(crate) fn vtable_class_cs(module_path: &str, name: &str) -> String { + format!("FfiVtable_{module_path}_{}", local_type_name(name)) +} + +/// The idiomatic C# surface type for one IR type, as it appears in wrapper +/// signatures, properties, and locals. +pub(crate) fn cs_type(ty: &Ty) -> String { + match ty { + 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 => "string".into(), + Ty::Bytes => "byte[]".into(), + // Records, rich enums, C-style enums, and interfaces surface under + // their bare local name; a cross-module reference (`kv.Store`) uses + // the same name because every module shares one namespace. + Ty::Record(name) | Ty::RichEnum(name) | Ty::Enum(name) | Ty::Interface(name) => { + local_type_name(name).into() + } + Ty::Optional(inner) => format!("{}?", cs_type(inner)), + Ty::List(inner) => format!("{}[]", cs_type(inner)), + Ty::Iterator(inner) => format!("IEnumerable<{}>", cs_type(inner)), + Ty::Map(k, v) => format!("Dictionary<{}, {}>", cs_type(k), cs_type(v)), + Ty::CallbackInterface(name) => callback_interface_cs(name), + } +} + +/// True when `ty` surfaces as a C# value type, so its optional wrapper is +/// `Nullable` and a present value is read through `.Value`. +pub(crate) fn is_cs_value_type(ty: &Ty) -> bool { + matches!( + ty, + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::I64 + | Ty::U64 + | Ty::F32 + | Ty::F64 + | Ty::Bool + | Ty::Enum(_) + ) +} + +/// True for C types that only ever appear behind a pointer and are opaque +/// to C#: interface objects, iterators, vtables, cancel tokens, and `void`. +fn is_opaque(ty: &CType) -> bool { + matches!( + ty, + CType::StructTag { .. } + | CType::VtableTag { .. } + | CType::CancelToken + | CType::Named(_) + | CType::Void + ) +} + +/// The blittable C# spelling of one C ABI type. Every pointer to an opaque +/// type is an `IntPtr`; every other pointer is a real C# pointer +/// (`byte*`, `FfiError*`, `byte**`), so no runtime marshalling is involved. +/// `bool` crosses as its one-byte representation. +pub(crate) fn cs_ctype(ty: &CType) -> String { + match ty { + CType::Int8 => "sbyte".into(), + CType::Int16 => "short".into(), + CType::Int32 | CType::Enum { .. } => "int".into(), + CType::Int64 => "long".into(), + CType::Uint8 | CType::Bool | CType::Char => "byte".into(), + CType::Uint16 => "ushort".into(), + CType::Uint32 => "uint".into(), + CType::Uint64 => "ulong".into(), + CType::Float => "float".into(), + CType::Double => "double".into(), + CType::Size => "nuint".into(), + CType::Void => "void".into(), + CType::Error => "FfiError".into(), + CType::Ptr { pointee, .. } if is_opaque(pointee) => "IntPtr".into(), + CType::Ptr { pointee, .. } => format!("{}*", cs_ctype(pointee)), + CType::StructTag { .. } + | CType::VtableTag { .. } + | CType::CancelToken + | CType::Named(_) => "IntPtr".into(), + } +} + +/// The `delegate* unmanaged[Cdecl]<...>` function-pointer type for a C +/// function taking `params` and returning `ret`. +pub(crate) fn fn_pointer_type(params: &[AbiParam], ret: &CType) -> String { + let mut parts: Vec = params.iter().map(|p| cs_ctype(&p.ty)).collect(); + parts.push(cs_ctype(ret)); + format!("delegate* unmanaged[Cdecl]<{}>", parts.join(", ")) +} + +/// Escapes text for an XML doc comment. +pub(crate) fn xml_text(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + +/// Escapes a string for embedding in a C# string literal. +pub(crate) fn cs_str(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") +} + +/// Escape `name` with the `@` verbatim prefix when C# reserves it. +pub(crate) fn safe_cs_name(name: &str) -> String { + if lang::is_reserved(name, lang::CSHARP_KEYWORDS) { + format!("@{name}") + } else { + name.to_string() + } +} + +/// A copy of `f` whose parameter names are lowerCamelCase, the C# parameter +/// convention. Only wrapper signatures and their locals derive from these +/// names; ABI slot names keep the IDL spelling. +pub(crate) fn camel_fn(f: &FnBinding) -> FnBinding { + let mut f = f.clone(); + for p in &mut f.params { + p.name = p.name.to_lower_camel_case(); + } + f +} diff --git a/crates/weaveffi-gen/src/targets/go/calls.rs b/crates/weaveffi-gen/src/targets/go/calls.rs new file mode 100644 index 00000000..1771bd52 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/go/calls.rs @@ -0,0 +1,967 @@ +//! Call rendering: sync, async, and iterator wrappers, callback-interface +//! types and trampolines, and the argument/return marshalling they share. +//! +//! 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 `Ty`. + +use crate::codegen::CodeWriter; +use crate::lang; +use heck::ToUpperCamelCase; +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::model::Ty; +use weaveffi_model::model::{ + AsyncBinding, BindingModel, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, + FnBinding, IteratorBinding, ParamBinding, +}; +use weaveffi_model::plan::{self, ArgPass, ErrorStrategy, RetPass}; + +use crate::targets::go::codec::{emit_buffer_read, emit_buffer_write}; +use crate::targets::go::docs::{emit_doc, emit_fn_doc}; +use crate::targets::go::types::{ + cgo_type, from_c_direct, go_adopt_expr, go_param_ident, go_type, go_zero, strip_const, + to_c_direct, vtable_accessor, vtable_var, +}; + +/// Package names the generated bindings use inside function bodies. A slot +/// or parameter spelled like one of them is escaped so it can't shadow the +/// package. +const SHADOWED_PACKAGES: &[&str] = &["cgo", "context", "fmt", "iter", "runtime", "unsafe"]; + +// ── Errors ── + +/// How a wrapper body reports a non-zero error slot. +/// +/// A callable with `throws == true` returns `(T, error)` and maps codes +/// through the declaring module's typed helper (`wvMapKv`), falling back to +/// the generic `Error` when no domain is in scope. A callable with +/// `throws == false` has a plain signature and panics via `wvTrap` instead, +/// since a reported error can only be a producer panic, an +/// argument-marshalling failure, or a callback implementation that panicked. +#[derive(Clone, Copy)] +pub(crate) struct ErrCtx<'a> { + /// `true` when the wrapper returns `(T, error)` and surfaces typed errors. + pub(crate) throws: bool, + /// PascalCase stem of the domain in effect (`Kv` names `wvMapKv`); `None` + /// falls back to the generic error. + stem: Option<&'a str>, +} + +impl<'a> ErrCtx<'a> { + /// Build the wrapper error context for `f` from the shared plan's + /// [`ErrorStrategy`]. + pub(crate) fn of(f: &FnBinding, stem: Option<&'a str>) -> Self { + Self { + throws: matches!(f.error_strategy(), ErrorStrategy::Throws), + stem, + } + } + + /// The Go expression converting the `wvFailure` value `failure` into an + /// `error`. + fn map_call(&self, failure: &str) -> String { + match self.stem { + Some(stem) => format!("wvMap{stem}({failure})"), + None => format!("{failure}.err()"), + } + } + + /// Emit the statement(s) checking the error slot named `slot`. A throwing + /// wrapper returns `zero` (when the function has a result) plus the + /// mapped error; a plain wrapper traps. + fn emit_check(&self, w: &mut CodeWriter, slot: &str, zero: Option<&str>) { + if self.throws { + let map = self.map_call(&format!("wvTakeError(&{slot})")); + w.block(format!("if {slot}.code != 0 {{"), "}", |w| { + match zero { + Some(z) => w.line(format!("return {z}, {map}")), + None => w.line(format!("return {map}")), + }; + }); + } else { + w.line(format!("wvTrap(&{slot})")); + } + } + + /// The Go return-type suffix (including the leading space) of a sync + /// wrapper returning `ret`. + fn ret_sig(&self, ret: Option<&Ty>) -> String { + match (ret, self.throws) { + (Some(r), true) => format!(" ({}, error)", go_type(r)), + (Some(r), false) => format!(" {}", go_type(r)), + (None, true) => " error".into(), + (None, false) => String::new(), + } + } + + /// The suffix of every successful `return`: `, nil` when the wrapper also + /// returns an error. + fn ok_tail(&self) -> &'static str { + if self.throws { + ", nil" + } else { + "" + } + } +} + +// ── Trampolines and the cgo preamble ── + +/// The Go spelling of one C ABI slot name inside an exported trampoline's +/// formal list: a slot named after a Go keyword or a package the body uses +/// gains a trailing underscore. The preamble `extern` uses the same spelling +/// so the two prototypes agree. +fn go_slot_ident(name: &str) -> String { + let ident = lang::escape_ident(name, lang::GO_KEYWORDS); + if SHADOWED_PACKAGES.contains(&ident.as_str()) { + format!("{ident}_") + } else { + ident + } +} + +/// The Go spelling of a wrapper parameter, escaped against keywords, the +/// packages wrapper bodies use, and (in an async wrapper) the leading `ctx`. +fn param_ident(p: &ParamBinding, is_async: bool) -> String { + let ident = go_param_ident(&p.name, is_async); + if SHADOWED_PACKAGES.contains(&ident.as_str()) { + format!("{ident}_") + } else { + ident + } +} + +/// The C name of the exported Go trampoline for an async completion typedef. +fn trampoline_name(c_type_name: &str) -> String { + format!("goWv_{c_type_name}") +} + +/// The C name of the exported Go trampoline behind one callback-interface +/// vtable entry (`goWv_{c_tag}_{method}`); `free` names the trailing entry. +fn cb_trampoline_name(c_tag: &str, method: &str) -> String { + format!("goWv_{c_tag}_{method}") +} + +/// The preamble `extern` declaration for one exported trampoline. Pointer +/// types are rendered const-free to match the prototypes cgo writes into +/// `_cgo_export.h` from the Go signature. +fn extern_decl(name: &str, ret: &CType, params: &[AbiParam], prefix: &str) -> String { + let args: Vec = params + .iter() + .map(|p| { + format!( + "{} {}", + strip_const(&p.ty).render_c(prefix), + go_slot_ident(&p.name) + ) + }) + .collect(); + format!( + "extern {} {name}({});", + ret.render_c(prefix), + args.join(", ") + ) +} + +/// The preamble definition of the one process-wide static vtable for `cb`: +/// one trampoline per method in declaration order, then `free`. An entry +/// whose C signature carries `const` pointers is cast back to the vtable's +/// exact field type, since the exported Go function is declared const-free. +fn vtable_def(cb: &CallbackInterfaceBinding, prefix: &str) -> String { + let mut s = format!( + "static const {} {} = {{\n", + cb.vtable_tag, + vtable_var(&cb.vtable_tag) + ); + for m in &cb.methods { + let tramp = cb_trampoline_name(&cb.c_tag, &m.name); + let needs_cast = m.abi_params.iter().any(|p| strip_const(&p.ty) != p.ty); + if needs_cast { + let types: Vec = m.abi_params.iter().map(|p| p.ty.render_c(prefix)).collect(); + s.push_str(&format!( + " ({} (*)({})){tramp},\n", + m.abi_ret.render_c(prefix), + types.join(", ") + )); + } else { + s.push_str(&format!(" {tramp},\n")); + } + } + s.push_str(&format!( + " {},\n}};\n", + cb_trampoline_name(&cb.c_tag, "free") + )); + // cgo reaches a C variable through `//go:cgo_import_static`, which needs + // external linkage, so a `static const` table can't be named as + // `&C.wvVtable_...` from Go. A static accessor function is callable + // through the ordinary cgo stub in the same translation unit. + s.push_str(&format!( + "__attribute__((unused)) static const {}* {}(void) {{ return &{}; }}", + cb.vtable_tag, + vtable_accessor(&cb.vtable_tag), + vtable_var(&cb.vtable_tag) + )); + s +} + +/// Every declaration the cgo preamble needs beyond the header include: for +/// each callback interface, the `extern` prototypes of its method and `free` +/// trampolines followed by its static vtable and accessor; then one `extern` +/// per async completion callback, including async interface members. +/// +/// A file that uses `//export` may only put declarations in its preamble +/// (the preamble is compiled into two C translation units); the vtable is a +/// `static const` so each unit gets a private copy and no symbol is +/// duplicated. Go takes the address of the copy in its own unit through the +/// static accessor, so the producer always sees one vtable whose entries +/// live for the process. +pub(crate) fn collect_preamble_decls(model: &BindingModel) -> Vec { + let prefix = model.prefix.as_str(); + let mut decls = Vec::new(); + for m in &model.modules { + for cb in &m.callback_interfaces { + for meth in &cb.methods { + decls.push(extern_decl( + &cb_trampoline_name(&cb.c_tag, &meth.name), + &meth.abi_ret, + &meth.abi_params, + prefix, + )); + } + decls.push(extern_decl( + &cb_trampoline_name(&cb.c_tag, "free"), + &CType::Void, + &[AbiParam::new("ctx", CType::ptr(CType::Void))], + prefix, + )); + decls.push(vtable_def(cb, prefix)); + } + for f in m.callables() { + if let CallShape::Async(ab) = &f.shape { + decls.push(extern_decl( + &trampoline_name(&ab.callback_type), + &CType::Void, + &ab.callback_params, + prefix, + )); + } + } + } + decls +} + +// ── Callback interfaces ── + +/// The Go method signature of one callback-interface method, as it appears +/// in the consumer-implemented interface type: `OnMessage(text string, +/// weight int32) int64`. +fn cb_method_sig(m: &CallbackMethodBinding) -> String { + let params: Vec = m + .params + .iter() + .map(|p| format!("{} {}", param_ident(p, false), go_type(&p.ty))) + .collect(); + let ret = match &m.ret { + Some(ty) => format!(" {}", go_type(ty)), + None => String::new(), + }; + format!( + "{}({}){ret}", + m.name.to_upper_camel_case(), + params.join(", ") + ) +} + +/// Emit statements converting one callback-method parameter's C slots into +/// a Go value bound to `arg{idx}`, returning that local's name. +/// +/// Strings, bytes, and buffers arriving in a trampoline are borrowed for the +/// dispatch: they're copied or decoded and nothing is freed. An object +/// argument transfers one strong reference, which is adopted into a wrapper +/// (a null `Interface?` adopts to nil). +fn emit_cb_param_arg(w: &mut CodeWriter, idx: usize, p: &ParamBinding) -> String { + let arg = format!("arg{idx}"); + match p.arg_pass() { + ArgPass::Buffer { ptr, len } => { + let r = format!("rArg{idx}"); + w.line(format!( + "{r} := &wvReader{{buf: wvBorrowBytes({}, {})}}", + go_slot_ident(&ptr.name), + go_slot_ident(&len.name) + )); + w.line(format!("var {arg} {}", go_type(&p.ty))); + emit_buffer_read(w, &r, &arg, &p.ty, &format!("Arg{idx}"), 0); + w.line(format!("{r}.expectEnd()")); + } + ArgPass::String { ptr, len } => { + w.line(format!( + "{arg} := wvBorrowString({}, {})", + go_slot_ident(&ptr.name), + go_slot_ident(&len.name) + )); + } + ArgPass::Bytes { ptr, len } => { + w.line(format!( + "{arg} := wvBorrowBytes({}, {})", + go_slot_ident(&ptr.name), + go_slot_ident(&len.name) + )); + } + ArgPass::Object { slot, .. } => { + w.line(format!( + "{arg} := {}", + go_adopt_expr(&p.ty, &go_slot_ident(&slot.name)) + )); + } + ArgPass::Direct { slot } => { + w.line(format!( + "{arg} := {}", + from_c_direct(&go_slot_ident(&slot.name), &p.ty) + )); + } + ArgPass::Callback { .. } => { + unreachable!("validation rejects callback interfaces as callback-method parameters") + } + } + arg +} + +/// Emit the exported trampoline behind one vtable entry. It recovers the +/// implementation from the `cgo.Handle` passed as `ctx`, converts the +/// borrowed arguments, calls the Go method, and writes a direct-family +/// result into the C return. A panic in the implementation is recovered and +/// reported through `{prefix}_error_set(out_err, -4, message)`, and the zero +/// value is returned; nothing ever unwinds through the C frame. +fn render_cb_trampoline( + w: &mut CodeWriter, + prefix: &str, + cb: &CallbackInterfaceBinding, + m: &CallbackMethodBinding, + iface_name: &str, +) { + let tramp = cb_trampoline_name(&cb.c_tag, &m.name); + let formals: Vec = m + .abi_params + .iter() + .map(|s| format!("{} {}", go_slot_ident(&s.name), cgo_type(&s.ty, prefix))) + .collect(); + let ctx = go_slot_ident(&m.abi_params[0].name); + let out_err = go_slot_ident(&m.abi_params[m.abi_params.len() - 1].name); + let ret_sig = match &m.ret { + Some(_) => format!(" (ret {})", cgo_type(&m.abi_ret, prefix)), + None => String::new(), + }; + w.line(format!("//export {tramp}")); + w.block( + format!("func {tramp}({}){ret_sig} {{", formals.join(", ")), + "}", + |w| { + w.block("defer func() {", "}()", |w| { + w.block("if r := recover(); r != nil {", "}", |w| { + w.line(format!("wvForeignError({out_err}, r)")); + }); + }); + w.line(format!( + "impl := cgo.Handle(uintptr({ctx})).Value().({iface_name})" + )); + let args: Vec = m + .params + .iter() + .enumerate() + .map(|(idx, p)| emit_cb_param_arg(w, idx, p)) + .collect(); + let call = format!("impl.{}({})", m.name.to_upper_camel_case(), args.join(", ")); + if m.ret.is_some() { + w.line(format!("ret = {}", to_c_direct(&call, &m.abi_ret, prefix))); + w.line("return"); + } else { + w.line(call); + } + }, + ); + w.blank(); +} + +/// Render one callback interface: the Go `interface` type the consumer +/// implements (one method per IDL method, PascalCase, direct-family or void +/// returns), the exported trampoline behind each vtable entry, and the +/// `free` trampoline that deletes the `cgo.Handle` once the producer drops +/// its last reference to the callback. +/// +/// Passing an implementation to a producer function stores it in a +/// `cgo.Handle` (the `void* ctx` slot) and passes the address of the +/// interface's static vtable from the cgo preamble (see +/// [`collect_preamble_decls`]). The producer may invoke any trampoline from +/// any thread; cgo attaches the calling thread to the Go runtime. +pub(crate) fn render_callback_interface( + out: &mut String, + prefix: &str, + cb: &CallbackInterfaceBinding, +) { + let name = cb.name.to_upper_camel_case(); + let mut w = CodeWriter::tabs(); + let mut d = String::new(); + emit_doc(&mut d, &cb.doc, "", Some(&name)); + if d.is_empty() { + w.line(format!( + "// {name} is a callback interface: implement it in Go and pass the value to" + )); + w.line("// native functions that accept it."); + } else { + w.raw(d); + w.line("//"); + w.line("// Implement this interface in Go and pass the value to native functions"); + w.line("// that accept it."); + } + w.line("//"); + w.line("// The native library may call any method from any thread until it releases"); + w.line("// the implementation. A panic in a method is reported to the native caller"); + w.line("// as a foreign error (code -4) instead of crashing the process."); + if let Some(msg) = &cb.deprecated { + w.line("//"); + w.line(format!("// Deprecated: {msg}")); + } + w.block(format!("type {name} interface {{"), "}", |w| { + for m in &cb.methods { + let mut md = String::new(); + emit_fn_doc( + &mut md, + &m.doc, + &m.params, + "\t", + &m.name.to_upper_camel_case(), + ); + let has_doc = !md.is_empty(); + w.raw(md); + emit_deprecated(w, has_doc, &m.deprecated); + w.line(cb_method_sig(m)); + } + }); + w.blank(); + + for m in &cb.methods { + render_cb_trampoline(&mut w, prefix, cb, m, &name); + } + + let free = cb_trampoline_name(&cb.c_tag, "free"); + w.line(format!("//export {free}")); + w.block(format!("func {free}(ctx unsafe.Pointer) {{"), "}", |w| { + w.line("cgo.Handle(uintptr(ctx)).Delete()"); + }); + w.blank(); + out.push_str(&w.finish()); +} + +// ── Shared wrapper pieces ── + +/// The wrapper signature line: `func (s *Store) Name(params) ret {` or +/// `func Name(params) ret {`. +fn header(receiver: Option<&str>, go_name: &str, params: &[String], ret_sig: &str) -> String { + match receiver { + Some(ty) => format!( + "func (s *{ty}) {go_name}({}){ret_sig} {{", + params.join(", ") + ), + None => format!("func {go_name}({}){ret_sig} {{", params.join(", ")), + } +} + +/// The Go formal parameter list of a wrapper. +fn go_params(f: &FnBinding) -> Vec { + let mut params: Vec = Vec::new(); + if f.is_async { + params.push("ctx context.Context".into()); + } + params.extend( + f.params + .iter() + .map(|p| format!("{} {}", param_ident(p, f.is_async), go_type(&p.ty))), + ); + params +} + +/// Emit the receiver borrow every method opens with: `s.native()` panics +/// when the wrapper was already closed, and the deferred release keeps the +/// native object alive (even across a concurrent `Close`) until the call +/// returns. +fn emit_receiver(w: &mut CodeWriter, args: &mut Vec) { + w.line("cSelf := s.native()"); + w.line("defer s.ref.release()"); + args.push("cSelf".into()); +} + +/// Emit the staging statements and C argument expressions for one Go +/// parameter, dispatching on the shared [`ArgPass`] contract. +/// +/// Strings and bytes pass a borrowed view of the Go memory (cgo pins it for +/// the call); a buffered parameter is packed into a `wvWriter` first. An +/// object parameter borrows the wrapper's pointer for the call, exactly like +/// a receiver; a nil `Interface?` passes NULL. A callback interface stores +/// the implementation in a `cgo.Handle` passed as `ctx` alongside the +/// address of the interface's static vtable. +fn emit_param( + w: &mut CodeWriter, + args: &mut Vec, + p: &ParamBinding, + prefix: &str, + is_async: bool, +) { + let name = param_ident(p, is_async); + let pascal = name.to_upper_camel_case(); + match p.arg_pass() { + ArgPass::Buffer { .. } => { + let wv = format!("w{pascal}"); + w.line(format!("{wv} := &wvWriter{{}}")); + emit_buffer_write(w, &wv, &name, &p.ty, &pascal, 0); + w.line(format!("c{pascal}Ptr, c{pascal}Len := wvBytes({wv}.buf)")); + args.push(format!("c{pascal}Ptr")); + args.push(format!("c{pascal}Len")); + } + ArgPass::String { .. } => { + w.line(format!("c{pascal}Ptr, c{pascal}Len := wvStr({name})")); + args.push(format!("c{pascal}Ptr")); + args.push(format!("c{pascal}Len")); + } + ArgPass::Bytes { .. } => { + w.line(format!("c{pascal}Ptr, c{pascal}Len := wvBytes({name})")); + args.push(format!("c{pascal}Ptr")); + args.push(format!("c{pascal}Len")); + } + ArgPass::Object { slot, nullable } => { + let cv = format!("c{pascal}"); + if nullable { + w.line(format!( + "var {cv} {}", + cgo_type(&strip_const(&slot.ty), prefix) + )); + w.block(format!("if {name} != nil {{"), "}", |w| { + w.line(format!("{cv} = {name}.native()")); + w.line(format!("defer {name}.ref.release()")); + }); + } else { + w.line(format!("{cv} := {name}.native()")); + w.line(format!("defer {name}.ref.release()")); + } + args.push(cv); + } + ArgPass::Callback { vtable, .. } => { + let CType::Ptr { pointee, .. } = &vtable.ty else { + unreachable!("a callback vtable slot is a pointer") + }; + let hv = format!("h{pascal}"); + w.line(format!("{hv} := cgo.NewHandle({name})")); + args.push(format!("C.wvHandlePtr(C.uintptr_t({hv}))")); + args.push(format!( + "C.{}()", + vtable_accessor(&pointee.render_c(prefix)) + )); + } + ArgPass::Direct { slot } => args.push(to_c_direct(&name, &slot.ty, prefix)), + } +} + +/// Emit the statements receiving one producer-owned value per its +/// [`RetPass`] contract, binding it to the Go local `dst` (declared here). +/// `ptr` and `len` name the C slots holding it (`len` only for strings, +/// bytes, and buffers). +/// +/// Strings and bytes are copied and released with `{prefix}_free_bytes`; a +/// buffer is copied, released, decoded, and checked for trailing bytes; an +/// object transfers one strong reference that the wrapper adopts (a null +/// `Interface?` adopts to nil). +fn emit_receive(w: &mut CodeWriter, ty: &Ty, pass: &RetPass, dst: &str, ptr: &str, len: &str) { + match receive_expr(ty, pass, ptr, len) { + Some(expr) => { + w.line(format!("{dst} := {expr}")); + } + None => { + let r = format!("r{}", dst.to_upper_camel_case()); + w.line(format!( + "{r} := &wvReader{{buf: wvTakeBytes({ptr}, {len})}}" + )); + w.line(format!("var {dst} {}", go_type(ty))); + emit_buffer_read(w, &r, dst, ty, &dst.to_upper_camel_case(), 0); + w.line(format!("{r}.expectEnd()")); + } + } +} + +/// The single Go expression receiving a value per `pass`, or `None` for a +/// buffer, which needs decoding statements (see [`emit_receive`]). +fn receive_expr(ty: &Ty, pass: &RetPass, ptr: &str, len: &str) -> Option { + match pass { + RetPass::Void => unreachable!("a present value is never void"), + RetPass::Direct => Some(from_c_direct(ptr, ty)), + RetPass::String => Some(format!("wvTakeString({ptr}, {len})")), + RetPass::Bytes => Some(format!("wvTakeBytes({ptr}, {len})")), + RetPass::Object { .. } => Some(go_adopt_expr(ty, ptr)), + RetPass::Buffer => None, + } +} + +/// Emit the statements returning a received value: `return `, +/// or the decoding statements followed by `return ret` for a buffer. +fn emit_return(w: &mut CodeWriter, ty: &Ty, pass: &RetPass, ptr: &str, len: &str, tail: &str) { + match receive_expr(ty, pass, ptr, len) { + Some(expr) => { + w.line(format!("return {expr}{tail}")); + } + None => { + emit_receive(w, ty, pass, "ret", ptr, len); + w.line(format!("return ret{tail}")); + } + } +} + +/// Emit a `Deprecated:` paragraph after a doc comment that may be empty. +fn emit_deprecated(w: &mut CodeWriter, has_doc: bool, deprecated: &Option) { + if let Some(msg) = deprecated { + if has_doc { + w.line("//"); + } + w.line(format!("// Deprecated: {msg}")); + } +} + +/// `true` when a value received per `pass` arrives as a `(ptr, len)` pair. +fn has_len(pass: &RetPass) -> bool { + matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer) +} + +// ── Async ── + +/// An async callable: a Go wrapper taking a leading `context.Context` that +/// launches the C call with a completion trampoline and blocks on a buffered +/// channel, plus the exported trampoline itself. The channel travels to the +/// producer as a `cgo.Handle` in the `context` slot; `wvComplete` resolves +/// and deletes it, so the completion is delivered exactly once. +/// +/// Every async wrapper returns an `error` (the context's, at least). A +/// cancellable function creates a native cancel token, cancels it when `ctx` +/// is done, and returns `ctx.Err()` for the cancelled (`-5`) completion; any +/// other function returns `ctx.Err()` as soon as `ctx` is done and abandons +/// the result. A producer error is mapped through the domain when the +/// function throws, and panics with the generic `Error` otherwise. +pub(crate) fn render_async_function( + out: &mut String, + prefix: &str, + f: &FnBinding, + ab: &AsyncBinding, + go_name: &str, + receiver: Option<&str>, + err: ErrCtx, +) { + let proto = ab.protocol(f, prefix); + let tramp = trampoline_name(&ab.callback_type); + let val_ty = f + .ret + .as_ref() + .map_or_else(|| "struct{}".to_string(), go_type); + + let mut w = CodeWriter::tabs(); + + // The exported completion trampoline. + let formals: Vec = ab + .callback_params + .iter() + .map(|s| format!("{} {}", go_slot_ident(&s.name), cgo_type(&s.ty, prefix))) + .collect(); + let handle = go_slot_ident(&ab.callback_params[0].name); + let err_slot = go_slot_ident(&ab.callback_params[1].name); + let results: Vec = ab.callback_params[2..] + .iter() + .map(|s| go_slot_ident(&s.name)) + .collect(); + w.line(format!("//export {tramp}")); + w.block( + format!("func {tramp}({}) {{", formals.join(", ")), + "}", + |w| { + w.block( + format!("wvComplete({handle}, {err_slot}, func() {val_ty} {{"), + "})", + |w| match &f.ret { + None => { + w.line("return struct{}{}"); + } + Some(ty) => { + let len = results.get(1).map_or("", String::as_str); + emit_return(w, ty, &proto.result, &results[0], len, ""); + } + }, + ); + }, + ); + w.blank(); + + // The blocking wrapper. + let ret_sig = match &f.ret { + Some(r) => format!(" ({}, error)", go_type(r)), + None => " error".into(), + }; + let zero_ret = |z: &str| match &f.ret { + Some(_) => format!("return {z}, "), + None => "return ".to_string(), + }; + let zero = f.ret.as_ref().map(go_zero).unwrap_or_default(); + let mut doc = String::new(); + emit_fn_doc(&mut doc, &f.doc, &f.params, "", go_name); + if doc.is_empty() { + doc = format!("// {go_name} calls the native async function.\n"); + } + w.raw(doc); + w.line("//"); + if f.cancellable { + w.line("// It blocks until the call completes. Cancelling ctx cancels the native"); + w.line("// call, which then returns ctx.Err()."); + } else { + w.line("// It blocks until the call completes or ctx is done; in the latter case it"); + w.line("// returns ctx.Err() at once and the native result is discarded."); + } + emit_deprecated(&mut w, true, &f.deprecated); + + w.block( + header(receiver, go_name, &go_params(f), &ret_sig), + "}", + |w| { + w.block("if err := ctx.Err(); err != nil {", "}", |w| { + w.line(format!("{}err", zero_ret(&zero))); + }); + let mut args: Vec = Vec::new(); + if receiver.is_some() { + emit_receiver(w, &mut args); + } + for p in &f.params { + emit_param(w, &mut args, p, prefix, true); + } + let token = if proto.cancellable { + w.line(format!("wvToken := C.{prefix}_cancel_token_create()")); + w.line(format!("defer C.{prefix}_cancel_token_destroy(wvToken)")); + args.push("wvToken".into()); + "wvToken" + } else { + "nil" + }; + w.line(format!("wvDone := make(chan wvOutcome[{val_ty}], 1)")); + w.line("wvHandle := cgo.NewHandle(wvDone)"); + args.push(format!("C.{}(unsafe.Pointer(C.{tramp}))", ab.callback_type)); + args.push("C.wvHandlePtr(C.uintptr_t(wvHandle))".into()); + w.line(format!("C.{}({})", ab.launch.symbol, args.join(", "))); + let res = if f.ret.is_some() { "res" } else { "_" }; + w.line(format!("{res}, fail, err := wvAwait(ctx, wvDone, {token})")); + w.block("if err != nil {", "}", |w| { + w.line(format!("{}err", zero_ret(&zero))); + }); + w.block("if fail != nil {", "}", |w| { + if err.throws { + w.line(format!("{}{}", zero_ret(&zero), err.map_call("*fail"))); + } else { + w.line("panic(fail.err())"); + } + }); + match &f.ret { + Some(_) => w.line("return res, nil"), + None => w.line("return nil"), + }; + }, + ); + w.blank(); + out.push_str(&w.finish()); +} + +// ── Functions ── + +/// A sync or iterator callable: the Go wrapper marshalling parameters in, +/// invoking the C symbol, checking the error slot per `err` (typed +/// `(T, error)` when throwing, `wvTrap` panic when plain), and converting the +/// result out. An iterator-returning callable renders through +/// [`render_iterator_fn`] as a lazy sequence instead. With `receiver` set, +/// the wrapper is a method on that wrapper type passing its native pointer +/// as the leading C argument. +pub(crate) fn render_function( + out: &mut String, + prefix: &str, + f: &FnBinding, + go_name: &str, + receiver: Option<&str>, + err: ErrCtx, +) { + if let CallShape::Iterator(ib) = &f.shape { + render_iterator_fn(out, prefix, f, ib, go_name, receiver, err); + return; + } + + let mut w = CodeWriter::tabs(); + let mut doc = String::new(); + emit_fn_doc(&mut doc, &f.doc, &f.params, "", go_name); + let has_doc = !doc.is_empty(); + w.raw(doc); + emit_deprecated(&mut w, has_doc, &f.deprecated); + + let ret_sig = err.ret_sig(f.ret.as_ref()); + let pass = plan::ret_pass(f.ret.as_ref(), prefix); + w.block( + header(receiver, go_name, &go_params(f), &ret_sig), + "}", + |w| { + let mut args: Vec = Vec::new(); + if receiver.is_some() { + emit_receiver(w, &mut args); + } + for p in &f.params { + emit_param(w, &mut args, p, prefix, false); + } + if has_len(&pass) { + w.line("var cRetLen C.size_t"); + args.push("&cRetLen".into()); + } + w.line(format!("var cErr C.{prefix}_error")); + args.push("&cErr".into()); + let call = format!("C.{}({})", f.c_base, args.join(", ")); + match &f.ret { + Some(_) => w.line(format!("cRet := {call}")), + None => w.line(call), + }; + err.emit_check(w, "cErr", f.ret.as_ref().map(go_zero).as_deref()); + match &f.ret { + Some(ty) => { + emit_return(w, ty, &pass, "cRet", "cRetLen", err.ok_tail()); + } + None if err.throws => { + w.line("return nil"); + } + None => {} + } + }, + ); + w.blank(); + out.push_str(&w.finish()); +} + +/// An `iter`-returning callable, rendered per the shared +/// [`weaveffi_model::plan::IteratorProtocol`] pull contract as Go's standard +/// lazy iteration idiom (the `iter` package): +/// +/// - A non-throwing function returns `iter.Seq[T]`. A launch or per-`next` +/// error can only be a producer bug, so it panics via `wvTrap`. +/// - A throwing function returns `iter.Seq2[T, error]`. A launch or +/// per-`next` domain error is yielded as the final `(zero, err)` pair and +/// iteration stops. +/// +/// The producer iterator is launched lazily inside the returned closure, one +/// C `next` call runs per consumer step, and the destroy runs exactly once +/// through a `defer`, whether the sequence is exhausted, stops on an error, +/// or is abandoned by an early `break`. A receiver stays borrowed until then. +fn render_iterator_fn( + out: &mut String, + prefix: &str, + f: &FnBinding, + ib: &IteratorBinding, + go_name: &str, + receiver: Option<&str>, + err: ErrCtx, +) { + let proto = ib.protocol(f, prefix); + let throws = matches!(proto.error, ErrorStrategy::Throws); + let elem = &ib.elem; + let elem_go = go_type(elem); + let CType::Ptr { pointee, .. } = &ib.next.params[1].ty else { + unreachable!("an iterator's out_item slot is always a pointer") + }; + let item_ty = cgo_type(&strip_const(pointee), prefix); + let with_len = has_len(&proto.elem); + let zero = go_zero(elem); + + let (seq_ty, yield_ty) = if throws { + ( + format!("iter.Seq2[{elem_go}, error]"), + format!("func({elem_go}, error) bool"), + ) + } else { + ( + format!("iter.Seq[{elem_go}]"), + format!("func({elem_go}) bool"), + ) + }; + + let mut w = CodeWriter::tabs(); + let mut doc = String::new(); + emit_fn_doc(&mut doc, &f.doc, &f.params, "", go_name); + if doc.is_empty() { + doc = format!("// {go_name} returns a lazy sequence over the native iterator.\n"); + } + w.raw(doc); + w.line("//"); + w.line("// The sequence launches the native iterator on each range and calls next once"); + w.line("// per element; the iterator is destroyed when the range ends, early or not."); + if throws { + w.line("// A failure is yielded as a final (zero value, error) pair."); + } + emit_deprecated(&mut w, true, &f.deprecated); + + // Statements surfacing a non-zero error slot: yield the mapped domain + // error and stop when throwing, trap when plain. + let emit_err_check = |w: &mut CodeWriter, slot: &str| { + if throws { + let map = err.map_call(&format!("wvTakeError(&{slot})")); + w.block(format!("if {slot}.code != 0 {{"), "}", |w| { + w.line(format!("yield({zero}, {map})")); + w.line("return"); + }); + } else { + w.line(format!("wvTrap(&{slot})")); + } + }; + + w.block( + header(receiver, go_name, &go_params(f), &format!(" {seq_ty}")), + "}", + |w| { + w.block(format!("return func(yield {yield_ty}) {{"), "}", |w| { + let mut args: Vec = Vec::new(); + if receiver.is_some() { + emit_receiver(w, &mut args); + } + for p in &f.params { + emit_param(w, &mut args, p, prefix, false); + } + w.line(format!("var cErr C.{prefix}_error")); + args.push("&cErr".into()); + w.line(format!( + "cIter := C.{}({})", + ib.launch.symbol, + args.join(", ") + )); + emit_err_check(w, "cErr"); + w.line(format!("defer C.{}(cIter)", ib.destroy_symbol)); + w.block("for {", "}", |w| { + w.line(format!("var cItem {item_ty}")); + let next_args = if with_len { + w.line("var cItemLen C.size_t"); + "cIter, &cItem, &cItemLen, &cIterErr" + } else { + "cIter, &cItem, &cIterErr" + }; + w.line(format!("var cIterErr C.{prefix}_error")); + w.line(format!("more := C.{}({next_args}) != 0", ib.next.symbol)); + emit_err_check(w, "cIterErr"); + w.block("if !more {", "}", |w| { + w.line("return"); + }); + emit_receive(w, elem, &proto.elem, "item", "cItem", "cItemLen"); + let yield_call = if throws { + "if !yield(item, nil) {" + } else { + "if !yield(item) {" + }; + w.block(yield_call, "}", |w| { + w.line("return"); + }); + }); + }); + }, + ); + w.blank(); + out.push_str(&w.finish()); +} diff --git a/crates/weaveffi-gen-go/src/codec.rs b/crates/weaveffi-gen/src/targets/go/codec.rs similarity index 55% rename from crates/weaveffi-gen-go/src/codec.rs rename to crates/weaveffi-gen/src/targets/go/codec.rs index 85cf8103..7d6592e0 100644 --- a/crates/weaveffi-gen-go/src/codec.rs +++ b/crates/weaveffi-gen/src/targets/go/codec.rs @@ -1,20 +1,21 @@ //! Value-buffer codec emitters: the Go statements serializing and decoding -//! one value in the wire format. +//! one value in the wire format, against the `wvWriter`/`wvReader` pair in +//! `runtime/codec.go`. //! //! Both emitters dispatch on the shared [`Ty::wire`] classification, so the //! non-obvious folds (objects as `u64` tokens carrying one strong reference, //! records and rich enums through one user codec) are decided centrally -//! rather than re-derived from `Ty` here. +//! rather than re-derived from `Ty` here. Every primitive maps onto the +//! runtime method named after it (`writeI32`, `readString`). -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{Prim, WireType}; +use crate::codegen::CodeWriter; +use weaveffi_model::model::{Ty, WireType}; -use crate::types::{go_local, go_type, optional_derefs, token_fn, untoken_fn}; +use crate::targets::go::types::{go_local, go_type, optional_derefs, token_fn, untoken_fn}; /// Emit statements appending `expr` (a Go value of type `ty`) to the -/// `wvWriter` named `writer`, following the wire format. `site` and `depth` -/// uniquify the loop locals generated for nested lists and maps. +/// `wvWriter` named `writer`. `site` and `depth` uniquify the loop locals +/// generated for nested lists and maps. /// /// An object is written as a token carrying a *fresh* strong reference: the /// per-interface `wvToken{Name}` helper calls the interface's `_clone` symbol @@ -28,48 +29,12 @@ pub(crate) fn emit_buffer_write( depth: usize, ) { match ty.wire() { - WireType::Prim(Prim::Bool) => { - w.line(format!("{writer}.writeBool({expr})")); - } - WireType::Prim(Prim::I8) => { - w.line(format!("{writer}.writeI8({expr})")); - } - WireType::Prim(Prim::I16) => { - w.line(format!("{writer}.writeI16({expr})")); - } - WireType::Prim(Prim::I32) => { - w.line(format!("{writer}.writeI32({expr})")); - } - WireType::Prim(Prim::I64) => { - w.line(format!("{writer}.writeI64({expr})")); - } - WireType::Prim(Prim::U8) => { - w.line(format!("{writer}.writeU8({expr})")); - } - WireType::Prim(Prim::U16) => { - w.line(format!("{writer}.writeU16({expr})")); - } - WireType::Prim(Prim::U32) => { - w.line(format!("{writer}.writeU32({expr})")); - } - WireType::Prim(Prim::U64) => { - w.line(format!("{writer}.writeU64({expr})")); - } - WireType::Prim(Prim::F32) => { - w.line(format!("{writer}.writeF32({expr})")); - } - WireType::Prim(Prim::F64) => { - w.line(format!("{writer}.writeF64({expr})")); + WireType::Prim(p) => { + w.line(format!("{writer}.write{}({expr})", p.pascal())); } WireType::Object(n) => { w.line(format!("{writer}.writeU64({}({expr}))", token_fn(n))); } - WireType::Prim(Prim::String) => { - w.line(format!("{writer}.writeString({expr})")); - } - WireType::Prim(Prim::Bytes) => { - w.line(format!("{writer}.writeBytes({expr})")); - } WireType::Enum(_) => { w.line(format!("{writer}.writeI32(int32({expr}))")); } @@ -85,7 +50,7 @@ pub(crate) fn emit_buffer_write( w.indent(); w.line(format!("{writer}.writeOptionFlag(true)")); let inner_expr = if optional_derefs(inner) { - format!("(*{expr})") + format!("*{expr}") } else { expr.to_string() }; @@ -118,7 +83,9 @@ pub(crate) fn emit_buffer_write( /// /// An object token is adopted into a new wrapper by the per-interface /// `wvUntoken{Name}` helper; the wrapper's `Close` (or finalizer) releases -/// the reference the token carried. +/// the reference the token carried. Collections cap their preallocation by +/// the bytes left in the buffer, since a zero-sized element encoding makes +/// any count valid. pub(crate) fn emit_buffer_read( w: &mut CodeWriter, reader: &str, @@ -128,48 +95,12 @@ pub(crate) fn emit_buffer_read( depth: usize, ) { match ty.wire() { - WireType::Prim(Prim::Bool) => { - w.line(format!("{dst} = {reader}.readBool()")); - } - WireType::Prim(Prim::I8) => { - w.line(format!("{dst} = {reader}.readI8()")); - } - WireType::Prim(Prim::I16) => { - w.line(format!("{dst} = {reader}.readI16()")); - } - WireType::Prim(Prim::I32) => { - w.line(format!("{dst} = {reader}.readI32()")); - } - WireType::Prim(Prim::I64) => { - w.line(format!("{dst} = {reader}.readI64()")); - } - WireType::Prim(Prim::U8) => { - w.line(format!("{dst} = {reader}.readU8()")); - } - WireType::Prim(Prim::U16) => { - w.line(format!("{dst} = {reader}.readU16()")); - } - WireType::Prim(Prim::U32) => { - w.line(format!("{dst} = {reader}.readU32()")); - } - WireType::Prim(Prim::U64) => { - w.line(format!("{dst} = {reader}.readU64()")); - } - WireType::Prim(Prim::F32) => { - w.line(format!("{dst} = {reader}.readF32()")); - } - WireType::Prim(Prim::F64) => { - w.line(format!("{dst} = {reader}.readF64()")); + WireType::Prim(p) => { + w.line(format!("{dst} = {reader}.read{}()", p.pascal())); } WireType::Object(n) => { w.line(format!("{dst} = {}({reader}.readU64())", untoken_fn(n))); } - WireType::Prim(Prim::String) => { - w.line(format!("{dst} = {reader}.readString()")); - } - WireType::Prim(Prim::Bytes) => { - w.line(format!("{dst} = {reader}.readBytes()")); - } WireType::Enum(n) => { w.line(format!("{dst} = {}({reader}.readI32())", go_local(n))); } @@ -190,23 +121,27 @@ pub(crate) fn emit_buffer_read( } WireType::List(inner) => { let n = format!("n{site}{depth}"); - let i = format!("i{site}{depth}"); + let e = format!("e{site}{depth}"); + let gt = go_type(inner); w.line(format!("{n} := {reader}.readLen()")); - w.line(format!("{dst} = make([]{}, {n})", go_type(inner))); - w.block(format!("for {i} := range {dst} {{"), "}", |w| { - emit_buffer_read(w, reader, &format!("{dst}[{i}]"), inner, site, depth + 1); + w.line(format!("{dst} = make([]{gt}, 0, {reader}.capHint({n}))")); + w.block(format!("for range {n} {{"), "}", |w| { + w.line(format!("var {e} {gt}")); + emit_buffer_read(w, reader, &e, inner, site, depth + 1); + w.line(format!("{dst} = append({dst}, {e})")); }); } WireType::Map(k, v) => { let n = format!("n{site}{depth}"); - let i = format!("i{site}{depth}"); let kv = format!("k{site}{depth}"); let vv = format!("v{site}{depth}"); let gk = go_type(k); let gv = go_type(v); w.line(format!("{n} := {reader}.readLen()")); - w.line(format!("{dst} = make(map[{gk}]{gv}, {n})")); - w.block(format!("for {i} := 0; {i} < {n}; {i}++ {{"), "}", |w| { + w.line(format!( + "{dst} = make(map[{gk}]{gv}, {reader}.capHint({n}))" + )); + w.block(format!("for range {n} {{"), "}", |w| { w.line(format!("var {kv} {gk}")); emit_buffer_read(w, reader, &kv, k, site, depth + 1); w.line(format!("var {vv} {gv}")); diff --git a/crates/weaveffi-gen-go/src/docs.rs b/crates/weaveffi-gen/src/targets/go/docs.rs similarity index 94% rename from crates/weaveffi-gen-go/src/docs.rs rename to crates/weaveffi-gen/src/targets/go/docs.rs index 97451aff..00fb96cc 100644 --- a/crates/weaveffi-gen-go/src/docs.rs +++ b/crates/weaveffi-gen/src/targets/go/docs.rs @@ -1,15 +1,15 @@ //! Godoc comment emission: the symbol-prefixed doc convention and the //! parameter continuation lines. -use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; -use weaveffi_core::model::ParamBinding; +use crate::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; +use weaveffi_model::model::ParamBinding; /// Emits a Go `// ...` doc comment at `indent`. If `symbol` is provided, the /// first non-empty line is prefixed with the symbol name to follow Go's doc /// convention. Subsequent lines are emitted verbatim with `// `. /// /// Without a symbol, this delegates to the shared -/// [`weaveffi_core::codegen::common::emit_doc`] helper using +/// [`crate::codegen::common::emit_doc`] helper using /// [`DocCommentStyle::DoubleSlash`]. The symbol-prefix flavour stays /// generator-local because godoc's first-line convention is unique to Go. pub(crate) fn emit_doc(out: &mut String, doc: &Option, indent: &str, symbol: Option<&str>) { diff --git a/crates/weaveffi-gen-go/src/entities.rs b/crates/weaveffi-gen/src/targets/go/entities.rs similarity index 80% rename from crates/weaveffi-gen-go/src/entities.rs rename to crates/weaveffi-gen/src/targets/go/entities.rs index e5af71c8..5ab9133e 100644 --- a/crates/weaveffi-gen-go/src/entities.rs +++ b/crates/weaveffi-gen/src/targets/go/entities.rs @@ -1,18 +1,17 @@ //! Entity rendering: plain and rich enums, records, typed error domains, and //! interface (object) wrapper types. +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; use heck::ToUpperCamelCase; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::errors::ERROR_BRAND; -use weaveffi_core::model::{ +use weaveffi_model::model::{ CallShape, EnumBinding, ErrorBinding, InterfaceBinding, ModuleBinding, StructBinding, }; -use weaveffi_core::utils::local_type_name; -use crate::calls::{render_async_function, render_function, ErrCtx}; -use crate::codec::{emit_buffer_read, emit_buffer_write}; -use crate::docs::emit_doc; -use crate::types::{adopt_fn, go_str, go_type, token_fn, untoken_fn}; +use crate::targets::go::calls::{render_async_function, render_function, ErrCtx}; +use crate::targets::go::codec::{emit_buffer_read, emit_buffer_write}; +use crate::targets::go::docs::emit_doc; +use crate::targets::go::types::{adopt_fn, go_str, go_type, token_fn, untoken_fn}; /// One `name rest` line inside a struct or const block, with its optional /// doc comment, for [`emit_aligned`]. @@ -71,12 +70,12 @@ pub(crate) fn domain_stem(module: &ModuleBinding) -> Option { /// fields, and the `wvMap{Stem}` helper converting a non-zero slot's /// `(code, message, payload)` into the typed error (default message when the /// slot carried none, decoded payload attached when the code declares fields, -/// generic [`ERROR_BRAND`] fallback for unknown codes). +/// generic `Error` fallback for unknown codes). /// /// Domain codes are validated positive-only, so the runtime's reserved /// negative codes (generic error, producer panic, marshalling failure) can /// never match a `case` arm here: they fall through to the generic -/// [`ERROR_BRAND`] fallback rather than a typed domain case. +/// `Error` fallback rather than a typed domain case. pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorBinding) { let stem = eb.owner_path.to_upper_camel_case(); let ty = &eb.type_name; @@ -152,27 +151,27 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB "// wvMap{stem} converts a non-zero code from the `{dotted}` domain into a" )); w.line(format!( - "// *{ty}, falling back to the generic *{ERROR_BRAND} for unknown codes." + "// *{ty}, falling back to the generic *Error for unknown codes." )); w.block( - format!("func wvMap{stem}(code int32, message string, payload []byte) error {{"), + format!("func wvMap{stem}(f wvFailure) error {{"), "}", |w| { - w.line("switch code {"); + w.line("switch f.code {"); for c in &eb.codes { let cname = format!("{ty}{}", c.name.to_upper_camel_case()); w.line(format!("case {cname}:")); w.indent(); - w.block("if message == \"\" {", "}", |w| { - w.line(format!("message = {}", go_str(&c.message))); + w.block("if f.message == \"\" {", "}", |w| { + w.line(format!("f.message = {}", go_str(&c.message))); }); if c.fields.is_empty() { - w.line(format!("return &{ty}{{Code: code, Message: message}}")); + w.line(format!("return &{ty}{{Code: f.code, Message: f.message}}")); } else { let pname = format!("{cname}Payload"); - w.line(format!("e := &{ty}{{Code: code, Message: message}}")); - w.block("if payload != nil {", "}", |w| { - w.line("r := &wvReader{buf: payload}"); + w.line(format!("e := &{ty}{{Code: f.code, Message: f.message}}")); + w.block("if f.payload != nil {", "}", |w| { + w.line("r := &wvReader{buf: f.payload}"); w.line(format!("p := &{pname}{{}}")); for f in &c.fields { let fname = f.name.to_upper_camel_case(); @@ -187,7 +186,7 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB } w.line("default:"); w.indent(); - w.line("return wvBrandError(code, message, payload)"); + w.line("return f.err()"); w.dedent(); w.line("}"); }, @@ -234,7 +233,7 @@ pub(crate) fn render_enum(out: &mut String, e: &EnumBinding) { /// values only cross the ABI inside value buffers. /// /// A plain C-style enum is skipped here (it is handled by [`render_enum`]). -pub(crate) fn render_rich_enum(out: &mut String, e: &EnumBinding) { +pub(crate) fn render_rich_enum(out: &mut String, package: &str, e: &EnumBinding) { if !e.is_rich() { return; } @@ -309,7 +308,7 @@ pub(crate) fn render_rich_enum(out: &mut String, e: &EnumBinding) { w.line("default:"); w.indent(); w.line(format!( - "panic(\"weaveffi: {name} value is not one of its variants\")" + "panic(\"{package}: {name} value is not one of its variants\")" )); w.dedent(); w.line("}"); @@ -343,7 +342,7 @@ pub(crate) fn render_rich_enum(out: &mut String, e: &EnumBinding) { w.line("default:"); w.indent(); w.line(format!( - "panic(\"weaveffi: malformed value buffer: {name} tag out of range\")" + "panic(\"{package}: malformed value buffer: {name} tag out of range\")" )); w.dedent(); w.line("}"); @@ -358,7 +357,7 @@ pub(crate) fn render_rich_enum(out: &mut String, e: &EnumBinding) { /// order. Records have no C symbols: no create, no destroy, no getters, no /// builders; instances only cross the ABI inside value buffers. An interface /// field holds a wrapper pointer and crosses as an object token (see -/// [`crate::codec`]). +/// [`crate::targets::go::codec`]). pub(crate) fn render_struct(out: &mut String, s: &StructBinding) { let name = s.name.to_upper_camel_case(); @@ -413,27 +412,28 @@ pub(crate) fn render_struct(out: &mut String, s: &StructBinding) { } /// Render one interface as a reference-counted object wrapper: a struct -/// holding one strong reference (`*C.{c_tag}`) released by an explicit -/// `Close` (idempotent, nils the pointer) or, as a backstop, by a -/// `runtime.SetFinalizer` hook, so `destroy` runs exactly once per wrapper. +/// whose `wvRef` owns one strong reference, released by an explicit `Close` +/// (idempotent and safe to race with in-flight calls) or, as a backstop, by +/// a finalizer, so `destroy` runs exactly once per wrapper. /// -/// Three private helpers accompany the type: `wvAdopt{Name}` wraps an owned -/// pointer coming back from the producer (a return, an async result, an -/// iterator element, a callback-method argument) and returns nil for a null -/// pointer, so `Interface?` needs no separate path; `wvToken{Name}` clones -/// the wrapper's reference into a value-buffer object token; and +/// Private helpers accompany the type: `native` borrows the pointer for one +/// call (panicking after `Close`); `wvAdopt{Name}` wraps an owned pointer +/// coming back from the producer (a return, an async result, an iterator +/// element, a callback-method argument) and returns nil for a null pointer, +/// so `Interface?` needs no separate path; `wvToken{Name}` clones the +/// wrapper's reference into a value-buffer object token; and /// `wvUntoken{Name}` adopts the reference a token carries. /// /// Constructors become package-level factory functions named /// `{PascalCtor}{Type}` (`new` gives `NewStore`, `open` gives `OpenStore`); -/// methods are methods on the wrapper passing `s.ptr` as the leading C +/// methods are methods on the wrapper passing its pointer as the leading C /// argument; statics are package-level functions namespaced by the type /// (`StoreDefaultCapacity`). Members reuse the free-function marshalling /// paths, including the sync/async/iterator shapes and the throws split. pub(crate) fn render_interface( out: &mut String, prefix: &str, - m: &ModuleBinding, + package: &str, iface: &InterfaceBinding, stem: Option<&str>, ) { @@ -442,6 +442,7 @@ pub(crate) fn render_interface( let adopt = adopt_fn(&iface.name); let token = token_fn(&iface.name); let untoken = untoken_fn(&iface.name); + let destroy = format!("wvDestroy{name}"); let mut w = CodeWriter::tabs(); let mut d = String::new(); @@ -461,7 +462,12 @@ pub(crate) fn render_interface( w.line(format!("// Deprecated: {msg}")); } w.block(format!("type {name} struct {{"), "}", |w| { - w.line(format!("ptr *C.{c_tag}")); + w.line("ref wvRef"); + }); + w.blank(); + + w.block(format!("func {destroy}(ptr unsafe.Pointer) {{"), "}", |w| { + w.line(format!("C.{}((*C.{c_tag})(ptr))", iface.destroy_symbol)); }); w.blank(); @@ -476,25 +482,37 @@ pub(crate) fn render_interface( w.block("if ptr == nil {", "}", |w| { w.line("return nil"); }); - w.line(format!("s := &{name}{{ptr: ptr}}")); + w.line(format!("s := &{name}{{}}")); + w.line(format!("s.ref.init(unsafe.Pointer(ptr), {destroy})")); w.line(format!("runtime.SetFinalizer(s, (*{name}).Close)")); w.line("return s"); }, ); w.blank(); + w.line("// native borrows the wrapper's pointer for one call; pair it with a"); + w.line("// deferred s.ref.release()."); + w.block( + format!("func (s *{name}) native() *C.{c_tag} {{"), + "}", + |w| { + w.block("if s == nil {", "}", |w| { + w.line(format!("panic(\"{package}: nil *{name}\")")); + }); + w.line(format!("return (*C.{c_tag})(s.ref.acquire(\"{name}\"))")); + }, + ); + w.blank(); + w.line(format!( "// {token} clones o's reference into a value-buffer object token. The" )); w.line("// wrapper keeps its own reference; the token carries the new one."); w.block(format!("func {token}(o *{name}) uint64 {{"), "}", |w| { - w.block("if o == nil || o.ptr == nil {", "}", |w| { - w.line(format!( - "panic(\"weaveffi: nil or closed {name} cannot be encoded in a non-optional position\")" - )); - }); + w.line("ptr := o.native()"); + w.line("defer o.ref.release()"); w.line(format!( - "return uint64(uintptr(unsafe.Pointer(C.{}(o.ptr))))", + "return uint64(uintptr(unsafe.Pointer(C.{}(ptr))))", iface.clone_symbol )); }); @@ -509,7 +527,9 @@ pub(crate) fn render_interface( "}", |w| { w.block("if token == 0 {", "}", |w| { - w.line("panic(\"weaveffi: malformed value buffer: null object token\")"); + w.line(format!( + "panic(\"{package}: malformed value buffer: null object token\")" + )); }); w.line(format!( "return {adopt}((*C.{c_tag})(C.wvHandlePtr(C.uintptr_t(token))))" @@ -517,21 +537,32 @@ pub(crate) fn render_interface( }, ); w.blank(); + + w.line("// Close releases the wrapper's strong reference and always returns nil."); + w.line("// It's idempotent and safe to call from any goroutine, even while a call"); + w.line("// on the wrapper is in flight: the reference is then released when that"); + w.line("// call returns. The object itself is dropped once its last reference, from"); + w.line("// any wrapper, record, or the native side, is gone."); + w.block(format!("func (s *{name}) Close() error {{"), "}", |w| { + w.line("runtime.SetFinalizer(s, nil)"); + w.line("s.ref.close()"); + w.line("return nil"); + }); + w.blank(); out.push_str(&w.finish()); for c in &iface.constructors { let go_name = format!("{}{name}", c.name.to_upper_camel_case()); - let err = ErrCtx::of(c, stem); - render_function(out, prefix, &m.path, c, &go_name, None, err); + render_function(out, prefix, c, &go_name, None, ErrCtx::of(c, stem)); } for f in &iface.methods { let go_name = f.name.to_upper_camel_case(); let err = ErrCtx::of(f, stem); if let CallShape::Async(ab) = &f.shape { - render_async_function(out, prefix, &m.path, f, ab, &go_name, Some(&name), err); + render_async_function(out, prefix, f, ab, &go_name, Some(&name), err); } else { - render_function(out, prefix, &m.path, f, &go_name, Some(&name), err); + render_function(out, prefix, f, &go_name, Some(&name), err); } } @@ -539,23 +570,9 @@ pub(crate) fn render_interface( let go_name = format!("{name}{}", f.name.to_upper_camel_case()); let err = ErrCtx::of(f, stem); if let CallShape::Async(ab) = &f.shape { - render_async_function(out, prefix, &m.path, f, ab, &go_name, None, err); + render_async_function(out, prefix, f, ab, &go_name, None, err); } else { - render_function(out, prefix, &m.path, f, &go_name, None, err); + render_function(out, prefix, f, &go_name, None, err); } } - - let mut w = CodeWriter::tabs(); - w.line("// Close releases the wrapper's strong reference. Calling it more than once"); - w.line("// is harmless; the object itself is dropped by the native library when its"); - w.line("// last reference (from any wrapper, record, or producer) is released."); - w.block(format!("func (s *{name}) Close() {{"), "}", |w| { - w.block("if s.ptr != nil {", "}", |w| { - w.line(format!("C.{}(s.ptr)", iface.destroy_symbol)); - w.line("s.ptr = nil"); - w.line("runtime.SetFinalizer(s, nil)"); - }); - }); - w.blank(); - out.push_str(&w.finish()); } diff --git a/crates/weaveffi-gen/src/targets/go/mod.rs b/crates/weaveffi-gen/src/targets/go/mod.rs new file mode 100644 index 00000000..0a503500 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/go/mod.rs @@ -0,0 +1,884 @@ +//! Go (cgo) binding generator for WeaveFFI. +//! +//! Emits a self-contained Go module under `go/`: `go.mod`, a copy of the C +//! header, `bindings.go` (the API), `runtime.go` (error, string, object, and +//! async plumbing), and, when the API has buffered values, `codec.go` (the +//! value-buffer writer and reader). The module path defaults to the package +//! name and the Go package name is the C prefix; the bindings link +//! `-l{library}`. Implements [`LanguageBackend`]; the shared driver bridges +//! it into the generator pipeline. +//! +//! Records, rich enums, optionals, lists, and maps are value types that +//! cross the C ABI serialized in the value-buffer format (one +//! `const uint8_t*` + `size_t` pair), with one pack and one unpack function +//! per record and rich enum. Strings and bytes cross as borrowed +//! `(ptr, len)` views of Go memory and come back as producer allocations the +//! bindings copy and release with `{prefix}_free_bytes`. +//! +//! Interfaces are reference-counted objects: each Go wrapper holds one strong +//! reference released by `Close` or, as a backstop, by a finalizer, and an +//! object inside a value buffer crosses as a cloned-reference token. +//! Callback interfaces are Go `interface` types the consumer implements; an +//! implementation crosses as a `cgo.Handle` plus the address of one static +//! vtable per interface, filled with exported Go trampolines that recover +//! panics into the producer's error slot. Async functions take a +//! `context.Context`, and a cancellable one cancels its native token when +//! the context is done. + +mod calls; +mod codec; +mod docs; +mod entities; +mod package; +mod runtime; +mod types; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::lang; +use crate::package::{PackageContext, PackagedFile}; +use crate::targets::c::render_c_header_from_model; +use crate::utils::{render_prelude, render_trailer, wrapper_name, CommentStyle}; +use camino::Utf8Path; +use heck::ToUpperCamelCase; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use weaveffi_model::model::{ + checksum_symbol, BindingModel, CallShape, CallbackInterfaceBinding, EnumBinding, ErrorBinding, + FnBinding, InterfaceBinding, ModuleBinding, StructBinding, +}; +use weaveffi_model::resolved::ResolvedApi; + +use crate::targets::go::calls::{ + collect_preamble_decls, render_async_function, render_callback_interface, render_function, + ErrCtx, +}; +use crate::targets::go::entities::{ + domain_stem, render_enum, render_error, render_interface, render_rich_enum, render_struct, +}; +use crate::targets::go::package::{package_files, render_go_mod, render_readme}; +use crate::targets::go::runtime::{render_codec, render_runtime, RuntimeNames}; + +/// Per-target configuration for [`GoGenerator`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct GoConfig { + /// Go module path written to `go.mod`. Defaults to the package name. + pub module_path: Option, + /// When `true` (the default), strip the IR module path from emitted + /// package-level function names, so module `kv`'s `delete` surfaces as + /// `Delete` rather than `KvDelete`. Set to `false` to restore the + /// module-prefixed spelling. Interface members are namespaced by their + /// wrapper type and never carry the module prefix. + pub strip_module_prefix: bool, + /// Basename of the IDL the CLI was invoked with. + #[serde(skip)] + pub input_basename: Option, + /// The identity-derived names the entity hooks render with, filled in by + /// [`render_bindings`] for the duration of one render. + #[serde(skip)] + render: RenderNames, +} + +/// What the [`LanguageBackend`] entity hooks need beyond the user's +/// configuration, carried on a per-render copy of [`GoConfig`]. +#[derive(Debug, Clone, Default)] +struct RenderNames { + /// The C symbol prefix. + prefix: String, + /// The Go package name. + package: String, + /// Base C symbols of the free functions that keep their module prefix + /// even when `strip_module_prefix` is on (see [`colliding_functions`]). + prefixed: BTreeSet, +} + +impl Default for GoConfig { + fn default() -> Self { + Self { + module_path: None, + strip_module_prefix: true, + input_basename: None, + render: RenderNames::default(), + } + } +} + +impl GoConfig { + /// The input IDL basename embedded in generated file headers, falling + /// back to `"api.yml"`. + pub fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } +} + +/// Every identity-derived name the generated module uses. +pub(crate) struct Names { + /// The Go module path (`go.mod`): the configured `module_path`, else the + /// package name. + pub(crate) module_path: String, + /// The Go package name: the C prefix, escaped if it's a Go keyword. + pub(crate) package: String, + /// The native library base name the bindings link (`-l{library}`). + pub(crate) library: String, + /// The bundled C header's file name, `{library}.h`. + pub(crate) header: String, +} + +impl Names { + pub(crate) fn new(api: &ResolvedApi, config: &GoConfig) -> Self { + let id = api.identity(); + Self { + module_path: config + .module_path + .as_deref() + .map(str::trim) + .filter(|p| !p.is_empty()) + .unwrap_or(&id.name) + .to_string(), + package: lang::escape_ident(&id.prefix, lang::GO_KEYWORDS), + library: id.library.clone(), + header: crate::targets::c::header_name(api), + } + } +} + +/// Go backend: emits a cgo module binding the C ABI exposed by the +/// underlying cdylib. +pub struct GoGenerator; + +impl LanguageBackend for GoGenerator { + type Config = GoConfig; + + fn name(&self) -> &'static str { + "go" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn render_enum(&self, out: &mut String, e: &EnumBinding, config: &Self::Config) { + // A plain C-style enum becomes an `int32` + constants; a rich + // (algebraic) enum becomes a sealed sum type. Each renderer skips + // the other kind. + render_enum(out, e); + render_rich_enum(out, &config.render.package, e); + } + + fn render_struct( + &self, + out: &mut String, + _module: &ModuleBinding, + s: &StructBinding, + _config: &Self::Config, + ) { + render_struct(out, s); + } + + fn render_error( + &self, + out: &mut String, + module: &ModuleBinding, + e: &ErrorBinding, + _config: &Self::Config, + ) { + // Emitted once, in the declaring module; inheriting submodules + // reference the ancestor's type through `wvMap{Stem}`. + render_error(out, module, e); + } + + fn render_callback_interface( + &self, + out: &mut String, + _module: &ModuleBinding, + cb: &CallbackInterfaceBinding, + config: &Self::Config, + ) { + render_callback_interface(out, &config.render.prefix, cb); + } + + fn render_interface( + &self, + out: &mut String, + module: &ModuleBinding, + i: &InterfaceBinding, + config: &Self::Config, + ) { + let stem = domain_stem(module); + let r = &config.render; + render_interface(out, &r.prefix, &r.package, i, stem.as_deref()); + } + + fn render_function( + &self, + out: &mut String, + module: &ModuleBinding, + f: &FnBinding, + config: &Self::Config, + ) { + let strip = config.strip_module_prefix && !config.render.prefixed.contains(&f.c_base); + let go_name = wrapper_name(&module.path, &f.name, strip).to_upper_camel_case(); + let stem = domain_stem(module); + let err = ErrCtx::of(f, stem.as_deref()); + let prefix = &config.render.prefix; + if let CallShape::Async(ab) = &f.shape { + render_async_function(out, prefix, f, ab, &go_name, None, err); + } else { + render_function(out, prefix, f, &go_name, None, err); + } + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + let dir = out_dir.join("go"); + render_files(api, model, config) + .into_iter() + .map(|(name, contents)| OutputFile::new(dir.join(name), contents)) + .collect() + } + + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + Some(package_files(api, model, ctx, out_dir, config)) + } +} + +/// Every file of the generated module, as `(file name, contents)` pairs. +pub(crate) fn render_files( + api: &ResolvedApi, + model: &BindingModel, + config: &GoConfig, +) -> Vec<(String, String)> { + let input_basename = config.input_basename(); + let names = Names::new(api, config); + let runtime_names = RuntimeNames { + package: &names.package, + prefix: &model.prefix, + header: &names.header, + }; + let mut files = vec![ + ( + "go.mod".to_string(), + render_go_mod(&names.module_path, input_basename), + ), + ( + "README.md".to_string(), + render_readme(&names, input_basename), + ), + ( + names.header.clone(), + render_c_header_from_model(model, input_basename, &names.header), + ), + ( + "bindings.go".to_string(), + render_bindings(model, config, &names), + ), + ( + "runtime.go".to_string(), + render_runtime(&runtime_names, input_basename), + ), + ]; + if model.has_buffers() { + files.push(( + "codec.go".to_string(), + render_codec(&runtime_names, input_basename), + )); + } + files +} + +// ── Name collisions ── + +/// The free functions whose module-stripped Go name would clash in the one +/// flat package the module tree renders into: with a declared type, +/// constant, or interface factory or static, with the runtime's exported +/// names, or with another module's function of the same name. Those keep +/// their module prefix (`directory.card` beside a `Card` record surfaces as +/// `DirectoryCard`). +fn colliding_functions(model: &BindingModel) -> BTreeSet { + let mut taken: BTreeSet = ["Error", "DebugLive"].map(String::from).into(); + for m in &model.modules { + if let Some(e) = m.error.as_ref().filter(|e| e.declared_here) { + taken.insert(e.type_name.clone()); + for c in &e.codes { + let code = format!("{}{}", e.type_name, c.name.to_upper_camel_case()); + taken.insert(format!("{code}Payload")); + taken.insert(code); + } + } + for e in &m.enums { + let name = e.name.to_upper_camel_case(); + for v in &e.variants { + taken.insert(format!("{name}{}", v.name.to_upper_camel_case())); + } + taken.insert(name); + } + let types = m + .structs + .iter() + .map(|s| &s.name) + .chain(m.callback_interfaces.iter().map(|c| &c.name)); + taken.extend(types.map(|n| n.to_upper_camel_case())); + for i in &m.interfaces { + let name = i.name.to_upper_camel_case(); + for c in &i.constructors { + taken.insert(format!("{}{name}", c.name.to_upper_camel_case())); + } + for f in &i.statics { + taken.insert(format!("{name}{}", f.name.to_upper_camel_case())); + } + taken.insert(name); + } + } + let mut seen: BTreeMap = BTreeMap::new(); + for (_, f) in model.functions() { + *seen.entry(f.name.to_upper_camel_case()).or_default() += 1; + } + model + .functions() + .filter(|(_, f)| { + let name = f.name.to_upper_camel_case(); + taken.contains(&name) || seen[&name] > 1 + }) + .map(|(_, f)| f.c_base.clone()) + .collect() +} + +// ── Import scanning ── + +/// The standard-library packages `bindings.go` imports, computed by one pass +/// over the lowered model. Everything else lives in `runtime.go` and +/// `codec.go`, whose imports are fixed. +#[derive(Default, Clone, Copy)] +struct Imports { + /// `context` (async wrappers take a `context.Context`). + context: bool, + /// `fmt` (typed error domains format their message). + fmt: bool, + /// `iter` (lazy sequences returned by `iter` functions). + iter: bool, + /// `runtime` (`SetFinalizer` on object wrappers). + runtime: bool, + /// `runtime/cgo` (`cgo.Handle` for callback-interface implementations + /// and async completion channels). + cgo: bool, + /// `unsafe` (object pointers, callback contexts, completion trampolines). + unsafe_ptr: bool, + /// The `wvHandlePtr` preamble helper that widens a `cgo.Handle` or an + /// object token into the `void*` the ABI carries. Doing the integer to + /// pointer conversion in C keeps `go vet` from flagging the generated + /// file for a "possible misuse of unsafe.Pointer". + handle_ptr: bool, +} + +fn scan_imports(model: &BindingModel) -> Imports { + let has_async = model.has_async(); + let has_interfaces = model.has_interfaces(); + let has_callbacks = model.has_callback_interfaces(); + Imports { + context: has_async, + fmt: model.modules.iter().any(ModuleBinding::declares_error), + iter: model.has_iterators(), + runtime: has_interfaces, + cgo: has_callbacks || has_async, + unsafe_ptr: has_interfaces || has_callbacks || has_async, + handle_ptr: has_interfaces || has_callbacks || has_async, + } +} + +// ── Top-level rendering ── + +/// Render `bindings.go`: the cgo preamble, imports, the load-time ABI and +/// checksum checks, and every module's entities and wrappers in the canonical +/// member order (error domain, enums, structs, callback interfaces, +/// interfaces, functions). +pub(crate) fn render_bindings(model: &BindingModel, config: &GoConfig, names: &Names) -> String { + let prefix = model.prefix.as_str(); + let imports = scan_imports(model); + let mut out = render_prelude(CommentStyle::DoubleSlash, config.input_basename()); + + out.push_str(&format!("package {}\n\n", names.package)); + out.push_str("/*\n"); + out.push_str(&format!("#cgo LDFLAGS: -l{}\n", names.library)); + if model.callables().any(|(_, f)| f.deprecated.is_some()) { + // The bindings call deprecated functions on the user's behalf; only + // the user's own calls should warn, through Go's `Deprecated:` docs. + out.push_str("#cgo CFLAGS: -Wno-deprecated-declarations\n"); + } + out.push_str(&format!("#include \"{}\"\n", names.header)); + if imports.handle_ptr { + // Each preamble helper is compiled into every cgo translation unit, + // some of which never call it. + out.push_str( + "__attribute__((unused)) static void* wvHandlePtr(uintptr_t h) { return (void*)h; }\n", + ); + } + // Forward declarations for the //export trampolines below (mirroring the + // const-free prototypes cgo emits into _cgo_export.h) and the static + // vtable of each callback interface. + for decl in collect_preamble_decls(model) { + out.push_str(&decl); + out.push('\n'); + } + out.push_str("*/\n"); + out.push_str("import \"C\"\n"); + + let packages = [ + (imports.context, "context"), + (imports.fmt, "fmt"), + (imports.iter, "iter"), + (imports.runtime, "runtime"), + (imports.cgo, "runtime/cgo"), + (imports.unsafe_ptr, "unsafe"), + ]; + let used: Vec<&str> = packages + .iter() + .filter(|(on, _)| *on) + .map(|(_, p)| *p) + .collect(); + if !used.is_empty() { + out.push_str("\nimport (\n"); + for p in used { + out.push_str(&format!("\t\"{p}\"\n")); + } + out.push_str(")\n"); + } + out.push('\n'); + + // Load-time checks: the ABI revision, then each top-level module's + // contract checksum. + out.push_str("func init() {\n\twvCheckABI()\n"); + for root in model.roots() { + let checksum = root.checksum.expect("top-level modules carry a checksum"); + out.push_str(&format!( + "\twvCheckModule(\"{}\", {checksum:#018x}, uint64(C.{}()))\n", + root.name, + checksum_symbol(prefix, &root.name) + )); + } + out.push_str("}\n\n"); + + let mut config = config.clone(); + config.render = RenderNames { + prefix: prefix.to_string(), + package: names.package.clone(), + prefixed: colliding_functions(model), + }; + for m in &model.modules { + GoGenerator.emit_members(&mut out, m, &config); + } + + // Exactly one blank line before the trailer keeps the file gofmt-clean. + out.truncate(out.trim_end().len()); + out.push_str("\n\n"); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, "bindings.go")); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use weaveffi_model::ir::{ + Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, + TypeRef, + }; + use weaveffi_model::pkg::Identity; + + fn param(name: &str, ty: TypeRef) -> Param { + Param { + name: name.into(), + ty, + doc: None, + } + } + + fn func(name: &str, params: Vec, returns: Option) -> Function { + Function { + name: name.into(), + params, + returns, + doc: None, + throws: false, + r#async: false, + cancellable: false, + deprecated: None, + } + } + + fn field(name: &str, ty: TypeRef) -> StructField { + StructField { + name: name.into(), + ty, + doc: None, + } + } + + fn named(n: &str) -> TypeRef { + TypeRef::Named(n.into()) + } + + fn optional(t: TypeRef) -> TypeRef { + TypeRef::Optional(Box::new(t)) + } + + /// One module exercising every shape: an interface with a constructor + /// and a method, `Interface?` in and out, a record with `Interface` and + /// `[Interface]` fields, an iterator of interfaces, a callback interface + /// taking a string, an i32, a record, and an object (one method returning + /// `bool`, one returning void), a function taking that callback + /// interface, and plain and cancellable async functions. + fn fixture() -> ResolvedApi { + let module = Module { + name: "bus".into(), + doc: None, + functions: vec![ + func( + "pick", + vec![param("preferred", optional(named("Ticker")))], + Some(optional(named("Ticker"))), + ), + func( + "all_tickers", + vec![], + Some(TypeRef::Iterator(Box::new(named("Ticker")))), + ), + func( + "subscribe", + vec![param("subscriber", named("Subscriber"))], + None, + ), + func( + "echo", + vec![param("text", TypeRef::StringUtf8)], + Some(TypeRef::StringUtf8), + ), + Function { + r#async: true, + ..func( + "fetch", + vec![param("id", TypeRef::I32)], + Some(named("Ticker")), + ) + }, + Function { + r#async: true, + cancellable: true, + ..func("wait", vec![param("ctx", TypeRef::I64)], None) + }, + ], + interfaces: vec![InterfaceDef { + name: "Ticker".into(), + doc: Some("A ticking counter.".into()), + deprecated: None, + constructors: vec![func("new", vec![param("start", TypeRef::I64)], None)], + methods: vec![func("value", vec![], Some(TypeRef::I64))], + statics: vec![], + }], + callback_interfaces: vec![CallbackInterfaceDef { + name: "Subscriber".into(), + doc: Some("Receives bus events.".into()), + deprecated: None, + methods: vec![ + func( + "on_message", + vec![ + param("text", TypeRef::StringUtf8), + param("weight", TypeRef::I32), + param("envelope", named("Envelope")), + ], + None, + ), + func( + "on_ticker", + vec![param("ticker", named("Ticker"))], + Some(TypeRef::Bool), + ), + ], + }], + structs: vec![StructDef { + name: "Envelope".into(), + doc: None, + deprecated: None, + fields: vec![ + field("topic", TypeRef::StringUtf8), + field("primary", named("Ticker")), + field("others", TypeRef::List(Box::new(named("Ticker")))), + ], + }], + enums: vec![], + errors: None, + modules: vec![], + }; + ResolvedApi::assume_valid(Api { + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![module], + }) + .with_identity(Identity::named("acme-bus")) + } + + fn render_all(config: &GoConfig) -> Vec<(String, String)> { + let api = fixture(); + let model = BindingModel::build(&api); + render_files(&api, &model, config) + } + + fn render(config: &GoConfig) -> String { + render_all(config) + .into_iter() + .find(|(n, _)| n == "bindings.go") + .map(|(_, c)| c) + .expect("bindings.go") + } + + fn assert_has(src: &str, needle: &str) { + assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); + } + + #[test] + fn identity_drives_every_name() { + let files = render_all(&GoConfig::default()); + let names: Vec<&str> = files.iter().map(|(n, _)| n.as_str()).collect(); + assert_eq!( + names, + [ + "go.mod", + "README.md", + "acme_bus.h", + "bindings.go", + "runtime.go", + "codec.go" + ] + ); + let get = |n: &str| &files.iter().find(|(f, _)| f == n).unwrap().1; + assert_has(get("go.mod"), "module acme-bus\n"); + let src = get("bindings.go"); + assert_has(src, "package acme_bus\n"); + assert_has(src, "#cgo LDFLAGS: -lacme_bus\n#include \"acme_bus.h\"\n"); + assert_has(src, "\twvCheckABI()\n\twvCheckModule(\"bus\", 0x"); + assert_has(src, "uint64(C.acme_bus_bus_checksum()))"); + let rt = get("runtime.go"); + assert_has(rt, "package acme_bus\n"); + assert_has(rt, "C.acme_bus_free_bytes(ptr, n)"); + assert_has(rt, "const wvABIVersion uint32 = 3\n"); + assert!(!rt.contains("{{"), "unsubstituted placeholder"); + for (name, contents) in &files { + if name.ends_with(".go") || name == "go.mod" { + let body: String = contents.lines().skip(3).collect::>().join("\n"); + assert!( + !body.to_lowercase().contains("weaveffi"), + "{name} is branded:\n{body}" + ); + } + } + + let custom = render_all(&GoConfig { + module_path: Some("example.com/bus".into()), + ..GoConfig::default() + }); + assert_has(&custom[0].1, "module example.com/bus\n"); + } + + #[test] + fn preamble_declares_trampolines_and_one_static_vtable() { + let src = render(&GoConfig::default()); + assert_has( + &src, + "extern void goWv_acme_bus_bus_Subscriber_on_message(void* ctx, uint8_t* text_ptr, size_t text_len, int32_t weight, uint8_t* envelope_ptr, size_t envelope_len, acme_bus_error* out_err);", + ); + assert_has( + &src, + "extern bool goWv_acme_bus_bus_Subscriber_on_ticker(void* ctx, acme_bus_bus_Ticker* ticker, acme_bus_error* out_err);", + ); + assert_has( + &src, + "static const acme_bus_bus_Subscriber_vtable wvVtable_acme_bus_bus_Subscriber_vtable = {", + ); + assert_has( + &src, + "(void (*)(void*, const uint8_t*, size_t, int32_t, const uint8_t*, size_t, acme_bus_error*))goWv_acme_bus_bus_Subscriber_on_message,", + ); + assert_has(&src, " goWv_acme_bus_bus_Subscriber_on_ticker,"); + assert_has(&src, " goWv_acme_bus_bus_Subscriber_free,\n};"); + assert_has( + &src, + "extern void goWv_acme_bus_bus_fetch_callback(void* context_, acme_bus_error* err, acme_bus_bus_Ticker* result);", + ); + } + + #[test] + fn strings_cross_as_borrowed_views_and_owned_runs() { + let src = render(&GoConfig::default()); + assert_has(&src, "cTextPtr, cTextLen := wvStr(text)"); + assert_has(&src, "var cRetLen C.size_t"); + assert_has( + &src, + "cRet := C.acme_bus_bus_echo(cTextPtr, cTextLen, &cRetLen, &cErr)", + ); + assert_has(&src, "return wvTakeString(cRet, cRetLen)"); + assert!(!src.contains("CString"), "no NUL-terminated strings"); + assert_has(&src, "arg0 := wvBorrowString(text_ptr, text_len)"); + } + + #[test] + fn callback_interface_renders_go_interface_and_trampolines() { + let src = render(&GoConfig::default()); + assert_has(&src, "type Subscriber interface {"); + assert_has( + &src, + "\tOnMessage(text string, weight int32, envelope Envelope)\n", + ); + assert_has(&src, "\tOnTicker(ticker *Ticker) bool\n"); + assert_has(&src, "wvForeignError(out_err, r)"); + assert_has(&src, "arg1 := int32(weight)"); + assert_has( + &src, + "rArg2 := &wvReader{buf: wvBorrowBytes(envelope_ptr, envelope_len)}", + ); + assert_has(&src, "arg0 := wvAdoptTicker(ticker)"); + assert_has(&src, "ret = C._Bool(impl.OnTicker(arg0))"); + assert_has(&src, "hSubscriber := cgo.NewHandle(subscriber)"); + assert_has( + &src, + "C.acme_bus_bus_subscribe(C.wvHandlePtr(C.uintptr_t(hSubscriber)), C.wvVtablePtr_acme_bus_bus_Subscriber_vtable(), &cErr)", + ); + } + + #[test] + fn interface_wrapper_guards_its_reference() { + let src = render(&GoConfig::default()); + assert_has(&src, "type Ticker struct {\n\tref wvRef\n}"); + assert_has(&src, "s.ref.init(unsafe.Pointer(ptr), wvDestroyTicker)"); + assert_has( + &src, + "func (s *Ticker) Close() error {\n\truntime.SetFinalizer(s, nil)\n\ts.ref.close()\n\treturn nil\n}", + ); + assert_has( + &src, + "func (s *Ticker) Value() int64 {\n\tcSelf := s.native()\n\tdefer s.ref.release()\n", + ); + assert_has(&src, "panic(\"acme_bus: nil *Ticker\")"); + assert_has( + &src, + "var cPreferred *C.acme_bus_bus_Ticker\n\tif preferred != nil {\n\t\tcPreferred = preferred.native()\n\t\tdefer preferred.ref.release()\n\t}", + ); + assert_has(&src, "v.Primary = wvUntokenTicker(r.readU64())"); + } + + #[test] + fn async_functions_take_a_context_and_cancel() { + let src = render(&GoConfig::default()); + assert_has( + &src, + "func Fetch(ctx context.Context, id int32) (*Ticker, error) {", + ); + assert_has(&src, "res, fail, err := wvAwait(ctx, wvDone, nil)"); + assert_has( + &src, + "wvComplete(context_, err, func() *Ticker {\n\t\treturn wvAdoptTicker(result)", + ); + assert_has(&src, "func Wait(ctx context.Context, ctx_ int64) error {"); + assert_has(&src, "wvToken := C.acme_bus_cancel_token_create()"); + assert_has(&src, "defer C.acme_bus_cancel_token_destroy(wvToken)"); + assert_has( + &src, + "C.acme_bus_bus_wait(C.int64_t(ctx_), wvToken, C.acme_bus_bus_wait_callback(unsafe.Pointer(C.goWv_acme_bus_bus_wait_callback)), C.wvHandlePtr(C.uintptr_t(wvHandle)))", + ); + assert_has(&src, "_, fail, err := wvAwait(ctx, wvDone, wvToken)"); + } + + #[test] + fn iterators_are_lazy_sequences() { + let src = render(&GoConfig::default()); + assert_has(&src, "func AllTickers() iter.Seq[*Ticker] {"); + assert_has(&src, "var cItem *C.acme_bus_bus_Ticker"); + assert_has( + &src, + "more := C.acme_bus_bus_AllTickersIterator_next(cIter, &cItem, &cIterErr) != 0", + ); + assert_has(&src, "item := wvAdoptTicker(cItem)"); + assert_has( + &src, + "defer C.acme_bus_bus_AllTickersIterator_destroy(cIter)", + ); + } + + #[test] + fn undocumented_enum_members_are_gofmt_aligned() { + use weaveffi_model::ir::{EnumDef, EnumVariant}; + let variant = |name: &str, value: i32, fields: Vec| EnumVariant { + name: name.into(), + value, + doc: None, + fields, + }; + let module = Module { + name: "paint".into(), + doc: None, + functions: vec![], + interfaces: vec![], + callback_interfaces: vec![], + structs: vec![], + enums: vec![ + EnumDef { + name: "Channel".into(), + doc: None, + deprecated: None, + variants: vec![ + variant("red", 0, vec![]), + variant("green", 1, vec![]), + variant("blue", 2, vec![]), + ], + }, + EnumDef { + name: "Shape".into(), + doc: None, + deprecated: None, + variants: vec![variant( + "rectangle", + 0, + vec![field("width", TypeRef::F32), field("height", TypeRef::F32)], + )], + }, + ], + errors: None, + modules: vec![], + }; + let api = ResolvedApi::assume_valid(Api { + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![module], + }); + let config = GoConfig::default(); + let model = BindingModel::build(&api); + let src = render_bindings(&model, &config, &Names::new(&api, &config)); + // gofmt pads a run of undocumented const and field names to one + // column; emitting it pre-aligned keeps the file gofmt-clean. + assert_has( + &src, + "const (\n\tChannelRed Channel = 0\n\tChannelGreen Channel = 1\n\tChannelBlue Channel = 2\n)", + ); + assert_has( + &src, + "type ShapeRectangle struct {\n\tWidth float32\n\tHeight float32\n}", + ); + } + + #[test] + fn output_is_deterministic() { + assert_eq!( + render_all(&GoConfig::default()), + render_all(&GoConfig::default()) + ); + } +} diff --git a/crates/weaveffi-gen/src/targets/go/package.rs b/crates/weaveffi-gen/src/targets/go/package.rs new file mode 100644 index 00000000..07f056c1 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/go/package.rs @@ -0,0 +1,188 @@ +//! Module scaffold: `go.mod`, the README flavors, and the `package` hook +//! bundling per-platform shared libraries with a rewritten cgo preamble. + +use crate::package::{PackageContext, PackagedFile}; +use crate::platform::{NativeBinary, Platform}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use camino::Utf8Path; +use weaveffi_model::model::BindingModel; +use weaveffi_model::resolved::ResolvedApi; + +use crate::targets::go::{render_files, GoConfig, Names}; + +/// The `(GOOS, GOARCH)` build-constraint tokens for a [`Platform`], used on +/// `#cgo` directive lines, or `None` for a platform the cgo package has no +/// slot for (Android and wasm32 binaries are skipped when packaging). +fn go_build_tags(p: Platform) -> Option<(&'static str, &'static str)> { + match p { + Platform::MacosArm64 => Some(("darwin", "arm64")), + Platform::MacosX64 => Some(("darwin", "amd64")), + Platform::LinuxX64 => Some(("linux", "amd64")), + Platform::LinuxArm64 => Some(("linux", "arm64")), + Platform::WindowsX64 => Some(("windows", "amd64")), + Platform::AndroidArm64 + | Platform::AndroidX64 + | Platform::Wasm32 + | Platform::IosArm64 + | Platform::IosSimArm64 + | Platform::IosSimX64 => None, + } +} + +/// The bundled binaries this package has a `#cgo` slot for, in the binary +/// set's order. +fn bundled<'a>( + ctx: &'a PackageContext<'a>, +) -> impl Iterator { + ctx.binaries + .binaries + .iter() + .filter_map(|nb| go_build_tags(nb.platform).map(|(goos, goarch)| (nb, goos, goarch))) +} + +/// The generated `go.mod` for the emitted module. +pub(crate) fn render_go_mod(module_path: &str, input_basename: &str) -> String { + let prelude = render_prelude(CommentStyle::DoubleSlash, input_basename); + let trailer = render_trailer(CommentStyle::DoubleSlash, "go.mod"); + // Go 1.23 is required for the standard `iter` package the lazy + // `iter` wrappers return. + format!("{prelude}module {module_path}\n\ngo 1.23\n\n{trailer}") +} + +/// README for the generate-mode output. +pub(crate) fn render_readme(names: &Names, input_basename: &str) -> String { + let prelude = render_prelude(CommentStyle::Xml, input_basename); + let trailer = render_trailer(CommentStyle::Xml, "README.md"); + let Names { + module_path, + package, + library, + header, + .. + } = names; + format!( + r#"{prelude}# {package} (Go) + +cgo bindings for the `{library}` native library. + +## Build + +The module ships its own copy of the C header (`{header}`) and links with +`-l{library}`. Point the linker at the directory holding `lib{library}.so`, +`lib{library}.dylib`, or `{library}.dll`, and make the library findable at +run time: + +```sh +export CGO_LDFLAGS="-L/path/to/lib" +export LD_LIBRARY_PATH="/path/to/lib" # DYLD_LIBRARY_PATH on macOS +go build ./... +``` + +Depend on the module from your own project with a `require` (and, for a +local checkout, a `replace`) of `{module_path}`, then import it: + +```go +import "{module_path}" +``` + +Go 1.23 or newer is required. + +## Usage notes + +- Records are plain structs and rich enums are sealed interfaces; both cross + the boundary by value. +- Interfaces are reference-counted objects. Call `Close` when you're done + with one (a finalizer releases it otherwise); it's safe to call twice or + while a call is in flight. +- Async functions take a `context.Context` first. Cancellable ones cancel + the native call when the context is done. +- Callback interfaces are Go interfaces you implement. The library may call + them from any thread, and a panic is reported to the native caller as a + foreign error instead of crashing. +- Loading checks the library's ABI revision and contract checksums and + panics with the stale module's name on a mismatch. + +{trailer}"# + ) +} + +/// README for a packaged Go module that bundles per-platform libraries. +fn render_packaged_readme(ctx: &PackageContext, names: &Names, input_basename: &str) -> String { + let prelude = render_prelude(CommentStyle::Xml, input_basename); + let trailer = render_trailer(CommentStyle::Xml, "README.md"); + let platforms: Vec = bundled(ctx) + .map(|(nb, _, _)| format!("- `lib/{}/`", nb.platform.id())) + .collect(); + let platform_list = platforms.join("\n"); + let package = &names.package; + format!( + r#"{prelude}# {package} (Go) + +cgo bindings with a prebuilt shared library bundled for each platform under +`lib//`. The cgo preamble adds the matching `${{SRCDIR}}`-relative +library search path and rpath per GOOS/GOARCH, so `go build` links the right +library with no manual `CGO_LDFLAGS`. + +## Bundled platforms + +{platform_list} + +{trailer}"#, + ) +} + +/// The full packaged file set: the generated module with a self-contained +/// cgo preamble plus one bundled shared library per desktop platform. +/// Binaries for platforms without a `GOOS,GOARCH` cgo slot (Android, wasm32) +/// are skipped. +pub(crate) fn package_files( + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &GoConfig, +) -> Vec { + let dir = out_dir.join("go"); + let input_basename = config.input_basename(); + let names = Names::new(api, config); + let library = &names.library; + + // Expand the single generate-mode `#cgo LDFLAGS` line into per + // GOOS/GOARCH library search and rpath directives (all `${SRCDIR}` + // relative). cgo selects the matching line at build time. + let original = format!("#cgo LDFLAGS: -l{library}\n"); + let mut cgo = String::new(); + for (nb, goos, goarch) in bundled(ctx) { + let id = nb.platform.id(); + if nb.platform == Platform::WindowsX64 { + cgo.push_str(&format!( + "#cgo {goos},{goarch} LDFLAGS: -L${{SRCDIR}}/lib/{id}\n" + )); + } else { + cgo.push_str(&format!( + "#cgo {goos},{goarch} LDFLAGS: -L${{SRCDIR}}/lib/{id} -Wl,-rpath,${{SRCDIR}}/lib/{id}\n" + )); + } + } + cgo.push_str(&original); + + let mut files: Vec = render_files(api, model, config) + .into_iter() + .map(|(name, contents)| { + let contents = match name.as_str() { + "bindings.go" => contents.replace(&original, &cgo), + "README.md" => render_packaged_readme(ctx, &names, input_basename), + _ => contents, + }; + PackagedFile::text(dir.join(name), contents) + }) + .collect(); + for (nb, _, _) in bundled(ctx) { + let dest = dir + .join("lib") + .join(nb.platform.id()) + .join(ctx.binaries.bundled_filename(nb.platform)); + files.push(PackagedFile::copy(dest, nb.source.clone())); + } + files +} diff --git a/crates/weaveffi-gen/src/targets/go/runtime.rs b/crates/weaveffi-gen/src/targets/go/runtime.rs new file mode 100644 index 00000000..c95037ca --- /dev/null +++ b/crates/weaveffi-gen/src/targets/go/runtime.rs @@ -0,0 +1,49 @@ +//! The fixed Go runtime shipped in every generated module. +//! +//! The sources live in `runtime/` as ordinary Go files and are spliced into +//! the output with `{{PLACEHOLDER}}` substitution: +//! +//! * `runtime.go`: the load-time ABI and checksum checks, the generic `Error` +//! type, error-slot and string/bytes helpers, the object reference guard +//! (`wvRef`), the async completion bridge, and `DebugLive`. +//! * `codec.go`: the value-buffer writer and reader, emitted only when the +//! API has buffered values. + +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::model::ABI_VERSION; + +const RUNTIME_GO: &str = include_str!("runtime/runtime.go"); +const CODEC_GO: &str = include_str!("runtime/codec.go"); + +/// The names a runtime template is specialized with. +pub(crate) struct RuntimeNames<'a> { + /// The Go package name. + pub(crate) package: &'a str, + /// The C symbol prefix. + pub(crate) prefix: &'a str, + /// The bundled C header's file name. + pub(crate) header: &'a str, +} + +fn splice(template: &str, names: &RuntimeNames, input_basename: &str, file: &str) -> String { + let body = template + .replace("{{PACKAGE}}", names.package) + .replace("{{PREFIX}}", names.prefix) + .replace("{{HEADER}}", names.header) + .replace("{{ABI_VERSION}}", &ABI_VERSION.to_string()); + format!( + "{}{body}\n{}", + render_prelude(CommentStyle::DoubleSlash, input_basename), + render_trailer(CommentStyle::DoubleSlash, file) + ) +} + +/// Render `runtime.go`. +pub(crate) fn render_runtime(names: &RuntimeNames, input_basename: &str) -> String { + splice(RUNTIME_GO, names, input_basename, "runtime.go") +} + +/// Render `codec.go`. +pub(crate) fn render_codec(names: &RuntimeNames, input_basename: &str) -> String { + splice(CODEC_GO, names, input_basename, "codec.go") +} diff --git a/crates/weaveffi-gen/src/targets/go/runtime/codec.go b/crates/weaveffi-gen/src/targets/go/runtime/codec.go new file mode 100644 index 00000000..d61154d0 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/go/runtime/codec.go @@ -0,0 +1,196 @@ +package {{PACKAGE}} + +import ( + "encoding/binary" + "math" + "unicode/utf8" +) + +// wvWriter serializes values into the value-buffer format: little-endian, +// packed, u32 length prefixes. +type wvWriter struct { + buf []byte +} + +func (w *wvWriter) writeBool(v bool) { + if v { + w.buf = append(w.buf, 1) + } else { + w.buf = append(w.buf, 0) + } +} + +func (w *wvWriter) writeI8(v int8) { + w.buf = append(w.buf, byte(v)) +} + +func (w *wvWriter) writeU8(v uint8) { + w.buf = append(w.buf, v) +} + +func (w *wvWriter) writeI16(v int16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, uint16(v)) +} + +func (w *wvWriter) writeU16(v uint16) { + w.buf = binary.LittleEndian.AppendUint16(w.buf, v) +} + +func (w *wvWriter) writeI32(v int32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, uint32(v)) +} + +func (w *wvWriter) writeU32(v uint32) { + w.buf = binary.LittleEndian.AppendUint32(w.buf, v) +} + +func (w *wvWriter) writeI64(v int64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, uint64(v)) +} + +func (w *wvWriter) writeU64(v uint64) { + w.buf = binary.LittleEndian.AppendUint64(w.buf, v) +} + +func (w *wvWriter) writeF32(v float32) { + w.writeU32(math.Float32bits(v)) +} + +func (w *wvWriter) writeF64(v float64) { + w.writeU64(math.Float64bits(v)) +} + +func (w *wvWriter) writeLen(n int) { + if n < 0 || uint64(n) > math.MaxUint32 { + panic("{{PACKAGE}}: 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 value-buffer format. A malformed buffer +// is a producer/consumer contract violation, so every read panics instead of +// returning a typed domain error. +type wvReader struct { + buf []byte + pos int +} + +func wvMalformed(context string) { + panic("{{PACKAGE}}: malformed value buffer: " + context) +} + +func (r *wvReader) take(n int, context string) []byte { + if n < 0 || len(r.buf)-r.pos < n { + wvMalformed(context) + } + b := r.buf[r.pos : r.pos+n] + r.pos += n + return b +} + +func (r *wvReader) readBool() bool { + switch r.take(1, "bool")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("bool byte out of range") + return false +} + +func (r *wvReader) readI8() int8 { + return int8(r.take(1, "i8")[0]) +} + +func (r *wvReader) readU8() uint8 { + return r.take(1, "u8")[0] +} + +func (r *wvReader) readI16() int16 { + return int16(binary.LittleEndian.Uint16(r.take(2, "i16"))) +} + +func (r *wvReader) readU16() uint16 { + return binary.LittleEndian.Uint16(r.take(2, "u16")) +} + +func (r *wvReader) readI32() int32 { + return int32(binary.LittleEndian.Uint32(r.take(4, "i32"))) +} + +func (r *wvReader) readU32() uint32 { + return binary.LittleEndian.Uint32(r.take(4, "u32")) +} + +func (r *wvReader) readI64() int64 { + return int64(binary.LittleEndian.Uint64(r.take(8, "i64"))) +} + +func (r *wvReader) readU64() uint64 { + return binary.LittleEndian.Uint64(r.take(8, "u64")) +} + +func (r *wvReader) readF32() float32 { + return math.Float32frombits(r.readU32()) +} + +func (r *wvReader) readF64() float64 { + return math.Float64frombits(r.readU64()) +} + +// readLen reads a u32 element count. Elements may encode to zero bytes, so +// the count isn't checked against the remaining buffer; callers cap their +// preallocation with capHint instead. +func (r *wvReader) readLen() int { + return int(r.readU32()) +} + +// capHint bounds a preallocation for n decoded elements by the bytes left in +// the buffer, so a corrupt count can't force a huge allocation. +func (r *wvReader) capHint(n int) int { + return min(n, len(r.buf)-r.pos) +} + +func (r *wvReader) readString() string { + b := r.take(r.readLen(), "string bytes") + if !utf8.Valid(b) { + wvMalformed("string is not valid UTF-8") + } + return string(b) +} + +func (r *wvReader) readBytes() []byte { + return append([]byte{}, r.take(r.readLen(), "byte buffer")...) +} + +func (r *wvReader) readOptionFlag() bool { + switch r.take(1, "option flag")[0] { + case 0: + return false + case 1: + return true + } + wvMalformed("option flag byte out of range") + return false +} + +func (r *wvReader) expectEnd() { + if r.pos != len(r.buf) { + wvMalformed("trailing bytes after value") + } +} diff --git a/crates/weaveffi-gen/src/targets/go/runtime/runtime.go b/crates/weaveffi-gen/src/targets/go/runtime/runtime.go new file mode 100644 index 00000000..7795f40c --- /dev/null +++ b/crates/weaveffi-gen/src/targets/go/runtime/runtime.go @@ -0,0 +1,296 @@ +package {{PACKAGE}} + +/* +#include +#include "{{HEADER}}" +*/ +import "C" + +import ( + "context" + "fmt" + "runtime/cgo" + "sync/atomic" + "unsafe" +) + +// wvABIVersion is the C ABI revision these bindings were generated against. +const wvABIVersion uint32 = {{ABI_VERSION}} + +// The runtime error codes this package produces or interprets itself. +const ( + wvCodeMarshal int32 = -3 + wvCodeForeign int32 = -4 + wvCodeCancelled int32 = -5 +) + +// Error reports a failure that no typed error domain claims: an unknown +// code, a generic failure (code -1), a producer panic (-2), a marshalling +// failure (-3), or a Go callback-interface implementation that panicked +// (-4). The code is preserved so callers can tell them apart. +type Error struct { + // Code is the numeric ABI error code. + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *Error) Error() string { + return fmt.Sprintf("{{PACKAGE}}: %s (code %d)", e.Message, e.Code) +} + +// DebugLive reports how many resources of one kind the native library holds: +// 0 objects, 1 callbacks, 2 iterators, 3 cancel tokens, 4 returned +// allocations. It's always 0 unless the library was built with leak checks +// enabled; tests use it to assert that nothing leaked. +func DebugLive(kind int32) uint64 { + return uint64(C.{{PREFIX}}_debug_live(C.int32_t(kind))) +} + +// wvCheckABI panics when the linked library implements a different C ABI +// revision than these bindings were generated for. +func wvCheckABI() { + if found := uint32(C.{{PREFIX}}_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("{{PACKAGE}}: ABI mismatch: these bindings expect revision %d but the linked library reports revision %d", wvABIVersion, found)) + } +} + +// wvCheckModule panics when the linked library's contract checksum for a +// top-level module differs from the one these bindings were generated +// against, which means the bindings are stale. +func wvCheckModule(module string, want, found uint64) { + if found != want { + panic(fmt.Sprintf("{{PACKAGE}}: module %q doesn't match the linked library (bindings checksum %#016x, library checksum %#016x); regenerate the bindings", module, want, found)) + } +} + +// ── Errors ── + +// wvFailure is a non-zero error slot copied into Go memory. +type wvFailure struct { + code int32 + message string + payload []byte +} + +// err converts the failure into the generic *Error. +func (f wvFailure) err() error { + return &Error{Code: f.code, Message: f.message} +} + +func wvCopyFailure(cErr *C.{{PREFIX}}_error) wvFailure { + f := wvFailure{code: int32(cErr.code)} + if cErr.message != nil { + f.message = C.GoString(cErr.message) + } + if cErr.payload_ptr != nil { + f.payload = wvBorrowBytes(cErr.payload_ptr, cErr.payload_len) + } + return f +} + +// wvTakeError copies a non-zero error slot and clears it. +func wvTakeError(cErr *C.{{PREFIX}}_error) wvFailure { + f := wvCopyFailure(cErr) + C.{{PREFIX}}_error_clear(cErr) + return f +} + +// wvTakeBoxedError copies the heap-boxed error an async completion receives +// and frees the box. +func wvTakeBoxedError(cErr *C.{{PREFIX}}_error) wvFailure { + f := wvCopyFailure(cErr) + C.{{PREFIX}}_error_free(cErr) + return f +} + +// wvTrap panics with the generic *Error when the slot reports a failure. +// Wrappers of functions that declare no error domain check their slot with +// it: a failure there can only be a producer bug, a marshalling failure, or +// a callback-interface implementation that panicked. +func wvTrap(cErr *C.{{PREFIX}}_error) { + if cErr.code != 0 { + panic(wvTakeError(cErr).err()) + } +} + +// wvForeignError reports a panic recovered inside a callback-interface +// trampoline through the producer's error slot (code -4). The producer +// copies the message, so it's only borrowed for the call. +func wvForeignError(outErr *C.{{PREFIX}}_error, recovered any) { + msg := C.CString(fmt.Sprint(recovered)) + defer C.free(unsafe.Pointer(msg)) + C.{{PREFIX}}_error_set(outErr, C.int32_t(wvCodeForeign), msg) +} + +// ── Strings and bytes ── + +// wvStr returns a borrowed (pointer, length) view of s's UTF-8 bytes for the +// duration of one call. Strings may contain NUL bytes. +func wvStr(s string) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(s))), C.size_t(len(s)) +} + +// wvBytes returns a borrowed (pointer, length) view of b for the duration of +// one call. +func wvBytes(b []byte) (*C.uint8_t, C.size_t) { + return (*C.uint8_t)(unsafe.Pointer(unsafe.SliceData(b))), C.size_t(len(b)) +} + +// wvBorrowString copies a borrowed UTF-8 run into a Go string. +func wvBorrowString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + return string(unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)) +} + +// wvTakeString copies a returned UTF-8 run into a Go string and releases the +// producer's allocation. +func wvTakeString(ptr *C.uint8_t, n C.size_t) string { + if ptr == nil { + return "" + } + s := wvBorrowString(ptr, n) + C.{{PREFIX}}_free_bytes(ptr, n) + return s +} + +// wvBorrowBytes copies a borrowed byte run into Go memory. A null pointer +// copies to nil. +func wvBorrowBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + return append([]byte{}, unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n)...) +} + +// wvTakeBytes copies a returned byte run (raw bytes or a value buffer) into +// Go memory and releases the producer's allocation. +func wvTakeBytes(ptr *C.uint8_t, n C.size_t) []byte { + if ptr == nil { + return nil + } + b := wvBorrowBytes(ptr, n) + C.{{PREFIX}}_free_bytes(ptr, n) + return b +} + +// ── Objects ── + +// wvRef owns one strong reference to a native object on behalf of a wrapper. +// Calls borrow the pointer between acquire and release. Close may race them: +// the reference is destroyed exactly once, by close when no call is in +// flight, or else by the last in-flight call to finish. +type wvRef struct { + ptr unsafe.Pointer + destroy func(unsafe.Pointer) + // state counts in-flight calls in its upper bits; the low bit is set + // once close has run. + state atomic.Uint64 +} + +func (r *wvRef) init(ptr unsafe.Pointer, destroy func(unsafe.Pointer)) { + r.ptr = ptr + r.destroy = destroy +} + +// acquire borrows the pointer for one call, panicking when the wrapper was +// already closed. +func (r *wvRef) acquire(typeName string) unsafe.Pointer { + for { + s := r.state.Load() + if s&1 != 0 { + panic("{{PACKAGE}}: " + typeName + " used after Close") + } + if r.state.CompareAndSwap(s, s+2) { + return r.ptr + } + } +} + +// release ends one call's borrow. +func (r *wvRef) release() { + if r.state.Add(^uint64(1)) == 1 { + r.destroy(r.ptr) + } +} + +// close releases the wrapper's reference once no call is in flight. Calling +// it again is a no-op. +func (r *wvRef) close() { + for { + s := r.state.Load() + if s&1 != 0 { + return + } + if r.state.CompareAndSwap(s, s|1) { + if s == 0 { + r.destroy(r.ptr) + } + return + } + } +} + +// ── Async ── + +// wvOutcome is one async completion: a converted result, or the failure the +// producer reported. +type wvOutcome[T any] struct { + val T + fail *wvFailure +} + +// wvComplete delivers one async completion to the waiting wrapper. It runs on +// a producer thread inside an exported trampoline, so it never panics: a +// result that fails to decode is delivered as a marshalling failure. +func wvComplete[T any](handle unsafe.Pointer, cErr *C.{{PREFIX}}_error, convert func() T) { + h := cgo.Handle(uintptr(handle)) + done := h.Value().(chan wvOutcome[T]) + h.Delete() + done <- wvSettle(cErr, convert) +} + +func wvSettle[T any](cErr *C.{{PREFIX}}_error, convert func() T) (o wvOutcome[T]) { + if cErr != nil { + if f := wvTakeBoxedError(cErr); f.code != 0 { + o.fail = &f + return o + } + } + defer func() { + if r := recover(); r != nil { + o = wvOutcome[T]{fail: &wvFailure{code: wvCodeMarshal, message: fmt.Sprint(r)}} + } + }() + o.val = convert() + return o +} + +// wvAwait blocks until an async call completes or ctx is done. With a cancel +// token, ctx's cancellation cancels the native call and waits for its +// completion, which the producer delivers promptly; without one, the call is +// abandoned and ctx.Err() returned at once. A cancelled completion (code -5) +// returns ctx.Err(), or context.Canceled when ctx itself isn't done. +func wvAwait[T any](ctx context.Context, done chan wvOutcome[T], token *C.{{PREFIX}}_cancel_token) (T, *wvFailure, error) { + var o wvOutcome[T] + select { + case o = <-done: + case <-ctx.Done(): + if token == nil { + var zero T + return zero, nil, ctx.Err() + } + C.{{PREFIX}}_cancel_token_cancel(token) + o = <-done + } + if o.fail != nil && o.fail.code == wvCodeCancelled { + var zero T + if err := ctx.Err(); err != nil { + return zero, nil, err + } + return zero, nil, context.Canceled + } + return o.val, o.fail, nil +} diff --git a/crates/weaveffi-gen-go/src/types.rs b/crates/weaveffi-gen/src/targets/go/types.rs similarity index 59% rename from crates/weaveffi-gen-go/src/types.rs rename to crates/weaveffi-gen/src/targets/go/types.rs index c40fdd32..f7480c03 100644 --- a/crates/weaveffi-gen-go/src/types.rs +++ b/crates/weaveffi-gen/src/targets/go/types.rs @@ -2,26 +2,33 @@ //! conversions, and user-chosen identifiers are spelled in the generated //! package. +use crate::lang; +use crate::utils::local_type_name; 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_model::abi::{CType, ConstPos}; +use weaveffi_model::model::Ty; /// The local Go type name (PascalCase) of a user-defined type reference, -/// stripping any qualifying module path. +/// stripping its module path. pub(crate) fn go_local(n: &str) -> String { local_type_name(n).to_upper_camel_case() } /// The Go spelling of a user-chosen parameter name: lowerCamelCase, with a /// trailing `_` appended when the conversion lands on a Go keyword (a param -/// named `type` surfaces as `type_`). +/// named `type` surfaces as `type_`). In an async wrapper, a parameter named +/// `ctx` is escaped the same way so it can't shadow the leading +/// `context.Context`. /// /// Only parameter positions need escaping: every other user-chosen name is /// emitted in PascalCase, and Go keywords are all lowercase. -pub(crate) fn go_param_ident(name: &str) -> String { - lang::escape_ident(&name.to_lower_camel_case(), lang::GO_KEYWORDS) +pub(crate) fn go_param_ident(name: &str, is_async: bool) -> String { + let ident = lang::escape_ident(&name.to_lower_camel_case(), lang::GO_KEYWORDS); + if is_async && ident == "ctx" { + "ctx_".into() + } else { + ident + } } /// The Go type spelling of an IR type reference. @@ -41,16 +48,11 @@ pub(crate) fn go_type(ty: &Ty) -> String { Ty::StringUtf8 => "string".into(), Ty::Bytes => "[]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`. - Ty::Record(n) | Ty::RichEnum(n) => go_local(n), + // (nil-able), so neither takes a pointer at the type site. + Ty::Record(n) | Ty::RichEnum(n) | Ty::Enum(n) | Ty::CallbackInterface(n) => go_local(n), // An object wrapper is always handled through a pointer so one Go - // value owns the strong reference; a callback interface is the Go - // interface type the consumer implements. + // value owns the strong reference. Ty::Interface(n) => format!("*{}", go_local(n)), - Ty::CallbackInterface(n) => go_local(n), - Ty::Enum(n) => go_local(n), Ty::Optional(inner) => { if optional_derefs(inner) { format!("*{}", go_type(inner)) @@ -79,8 +81,7 @@ pub(crate) fn optional_derefs(inner: &Ty) -> bool { ) } -/// The Go zero-value expression of a type, returned on the error path of a -/// throwing wrapper. +/// The Go zero-value expression of a type, returned on an error path. pub(crate) fn go_zero(ty: &Ty) -> String { match ty { Ty::I8 @@ -92,75 +93,31 @@ pub(crate) fn go_zero(ty: &Ty) -> String { | Ty::U32 | Ty::U64 | Ty::F32 - | Ty::F64 => "0".into(), + | Ty::F64 + | Ty::Enum(_) => "0".into(), Ty::Bool => "false".into(), Ty::StringUtf8 => "\"\"".into(), - Ty::Enum(_) => "0".into(), // A record is a value struct: its zero is the empty literal. Ty::Record(n) => format!("{}{{}}", go_local(n)), _ => "nil".into(), } } -/// 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: &Ty, prefix: &str, module: &str) -> Option { - match ty { - 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 => 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: &Ty, prefix: &str, module: &str) -> String { - match ty { - Ty::Bool => format!("boolToC({expr})"), - _ => { - if let Some(ct) = c_scalar_type(ty, prefix, module) { - format!("{ct}({expr})") - } else { - expr.to_string() - } - } - } +/// The Go expression converting the Go value `expr` into the by-value C slot +/// of type `slot` (a scalar, `bool`, or C-style enum). +pub(crate) fn to_c_direct(expr: &str, slot: &CType, prefix: &str) -> String { + format!("{}({expr})", cgo_type(slot, prefix)) } -/// The Go expression converting a C scalar `expr` back into its Go value. -/// Non-scalar types pass through unchanged. -pub(crate) fn go_scalar_conv(expr: &str, ty: &Ty) -> String { - match ty { - 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 => 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 converting a by-value C slot `expr` back into the Go +/// value of type `ty`. +pub(crate) fn from_c_direct(expr: &str, ty: &Ty) -> String { + format!("{}({expr})", go_type(ty)) } /// The name of the per-interface adopt helper (`wvAdoptStore`) that wraps one /// owned strong reference in a new wrapper (or returns nil for a null -/// pointer). `n` may be dot-qualified. +/// pointer). pub(crate) fn adopt_fn(n: &str) -> String { format!("wvAdopt{}", go_local(n)) } @@ -189,16 +146,16 @@ pub(crate) fn go_adopt_expr(ty: &Ty, ptr_expr: &str) -> String { } /// The C identifier of the process-wide static vtable emitted in the cgo -/// preamble for the callback interface whose C tag is `c_tag`. -pub(crate) fn vtable_var(c_tag: &str) -> String { - format!("wvVtable_{c_tag}") +/// preamble for the vtable struct `vtable_tag`. +pub(crate) fn vtable_var(vtable_tag: &str) -> String { + format!("wvVtable_{vtable_tag}") } /// The C identifier of the static preamble function returning the address /// of [`vtable_var`]'s table; Go can't take a `static` variable's address /// through cgo directly, so wrappers call this instead. -pub(crate) fn vtable_accessor(c_tag: &str) -> String { - format!("wvVtablePtr_{c_tag}") +pub(crate) fn vtable_accessor(vtable_tag: &str) -> String { + format!("wvVtablePtr_{vtable_tag}") } /// Quote `s` as a Go string literal, escaping backslashes, quotes, and @@ -219,8 +176,9 @@ pub(crate) fn go_str(s: &str) -> String { out } -/// Go formal type for one C ABI slot in a trampoline signature. -pub(crate) fn cgo_slot_type(ct: &CType, prefix: &str) -> String { +/// The cgo spelling of one C ABI type: `C.int32_t`, `*C.uint8_t`, +/// `*C.kv_kv_Store`, and `unsafe.Pointer` for `void*`. +pub(crate) fn cgo_type(ct: &CType, prefix: &str) -> String { match ct { CType::Int8 => "C.int8_t".into(), CType::Int16 => "C.int16_t".into(), @@ -235,21 +193,10 @@ pub(crate) fn cgo_slot_type(ct: &CType, prefix: &str) -> String { CType::Bool => "C._Bool".into(), CType::Size => "C.size_t".into(), CType::Char => "C.char".into(), - CType::CancelToken => format!("C.{prefix}_cancel_token"), - CType::Error => format!("C.{prefix}_error"), - CType::Enum { module, name } | CType::StructTag { module, name } => { - format!("C.{prefix}_{module}_{name}") - } - CType::VtableTag { module, name } => format!("C.{prefix}_{module}_{name}_vtable"), - CType::Named(core) => format!("C.{prefix}_{core}"), - CType::Ptr { pointee, .. } => { - if **pointee == CType::Void { - "unsafe.Pointer".into() - } else { - format!("*{}", cgo_slot_type(pointee, prefix)) - } - } + CType::Ptr { pointee, .. } if **pointee == CType::Void => "unsafe.Pointer".into(), + CType::Ptr { pointee, .. } => format!("*{}", cgo_type(pointee, prefix)), CType::Void => unreachable!("void only appears behind a pointer"), + named => format!("C.{}", named.render_c(prefix)), } } diff --git a/crates/weaveffi-gen/src/targets/js/api.rs b/crates/weaveffi-gen/src/targets/js/api.rs new file mode 100644 index 00000000..1e05efbf --- /dev/null +++ b/crates/weaveffi-gen/src/targets/js/api.rs @@ -0,0 +1,588 @@ +//! The idiomatic JavaScript API, shared by both transports. +//! +//! Everything here calls the native side through `$raw`, an object with one +//! function per C symbol (keyed by the symbol name) that the transport +//! provides: the N-API addon on Node.js, linear-memory glue on WebAssembly. +//! The raw calling convention, which both transports implement, is: +//! +//! * **Arguments**, in ABI order (an instance method's handle first): a +//! direct value as a `number`, `bigint`, or `boolean`; a string as a +//! `string`; bytes and value buffers as a `Uint8Array`; an object as its +//! handle (`null` for an absent `Interface?`); a callback interface as an +//! adapter object whose methods, named as in the IDL, take and return raw +//! values; and, last, a cancellable call's cancel token handle (or +//! `null`). +//! * **Results** come back the same way: a direct value, a `string`, a +//! `Uint8Array`, or an object handle (one strong reference). An async +//! launcher returns a `Promise` of its result; an iterator launcher +//! returns a handle, and the iterator's `next` returns an element or +//! `undefined` when the iterator is exhausted. +//! * **Failures** throw (or reject with) a runtime `$Fault`. +//! +//! The transport also defines `$token`, which turns an object token read +//! from a value buffer into a handle. + +use weaveffi_model::model::{ + BindingModel, CallShape, CallbackInterfaceBinding, ErrorBinding, FnBinding, InterfaceBinding, + ModuleBinding, ParamBinding, Ty, +}; +use weaveffi_model::plan::{self, ArgPass, RetPass}; + +use crate::codegen::CodeWriter; +use crate::targets::js::codec::{emit_codecs, read_expr, reader_fn, writer_fn}; +use crate::targets::js::names::{ + callback_method_name, code_class, decl, direct_prim, error_mapper, fn_name, helper, js_string, + member_name, module_name, param_name, type_decl, +}; + +/// Render the shared API section of `index.js`: everything after the +/// transport has defined `$raw` and `$token`. +pub(crate) fn render_api(model: &BindingModel) -> String { + let mut w = CodeWriter::two_space(); + let prefix = &model.prefix; + + if model.callables().any(|(_, f)| f.cancellable) { + w.line("// The native cancel tokens behind `AbortSignal`s."); + w.block("const $tokens = {", "};", |w| { + w.line(format!( + "create: () => $raw.{prefix}_cancel_token_create()," + )); + w.line(format!( + "cancel: (t) => $raw.{prefix}_cancel_token_cancel(t)," + )); + w.line(format!( + "destroy: (t) => $raw.{prefix}_cancel_token_destroy(t)," + )); + }); + w.blank(); + } + + w.line("/**"); + w.line(" * The native library's live-allocation counters (0 objects, 1 callbacks,"); + w.line(" * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests. Always"); + w.line(" * zero unless the library enables its `leak-check` feature."); + w.line(" */"); + w.block("export function __debugLive(kind) {", "}", |w| { + w.line(format!("return $raw.{prefix}_debug_live(kind);")); + }); + w.blank(); + + for m in &model.modules { + if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { + emit_error_domain(&mut w, m, eb); + } + for e in m.enums.iter().filter(|e| !e.rich) { + w.block( + format!("const {} = Object.freeze({{", decl(&m.segments, &e.name)), + "});", + |w| { + for v in &e.variants { + w.line(format!("{}: {},", v.name, v.value)); + } + for v in &e.variants { + w.line(format!("{}: {},", v.value, js_string(&v.name))); + } + }, + ); + w.blank(); + } + } + + if model.has_buffers() { + let mut codecs = CodeWriter::two_space(); + emit_codecs(&mut codecs, model); + if !codecs.is_empty() { + w.raw(codecs.finish()); + w.blank(); + } + } + + for m in &model.modules { + for cb in &m.callback_interfaces { + emit_adapter(&mut w, m, cb, prefix); + } + for i in &m.interfaces { + emit_class(&mut w, model, m, i); + } + for f in &m.functions { + emit_iterator_spec(&mut w, model, m, f, &decl(&m.segments, &f.name)); + let is_async = if f.is_async { "async " } else { "" }; + let mut params = js_params(f); + if f.cancellable { + params.push("$options".into()); + } + let name = fn_name(&f.name); + w.block( + format!( + "const {} = {is_async}function {name}({}) {{", + decl(&m.segments, &name), + params.join(", ") + ), + "};", + |w| { + emit_body( + w, + model, + m, + f, + None, + &decl(&m.segments, &f.name), + Finish::Return, + ) + }, + ); + w.blank(); + } + } + + for m in model.roots() { + emit_namespace( + &mut w, + model, + m, + &format!("export const {} = ", module_name(m)), + ); + w.blank(); + } + w.finish() +} + +/// The JavaScript parameter names of a callable. +fn js_params(f: &FnBinding) -> Vec { + f.params.iter().map(|p| param_name(&p.name)).collect() +} + +/// Emit one error domain: the domain class, one class per code, and the +/// mapper (`$from$kv$KvError`) its throwing callables route faults through. +fn emit_error_domain(w: &mut CodeWriter, m: &ModuleBinding, eb: &ErrorBinding) { + let domain = decl(&m.segments, &eb.type_name); + let key = decl(&m.segments, &eb.name); + w.line(format!( + "const {domain} = class {} extends $Error {{}};", + eb.type_name + )); + for c in &eb.codes { + let class = code_class(&c.name); + w.block( + format!( + "const {} = class {class} extends {domain} {{", + decl(&m.segments, &class) + ), + "};", + |w| { + w.line(format!("static CODE = {};", c.value)); + w.block( + format!("constructor(message = {}) {{", js_string(&c.message)), + "}", + |w| { + w.line(format!("super({}, message);", c.value)); + }, + ); + }, + ); + } + let codes: Vec = eb + .codes + .iter() + .map(|c| format!("[{}, {}]", c.value, decl(&m.segments, &code_class(&c.name)))) + .collect(); + w.line(format!( + "const $codes${key} = new Map([{}]);", + codes.join(", ") + )); + let payloads: Vec = eb + .codes + .iter() + .filter(|c| !c.fields.is_empty()) + .map(|c| { + let fields: Vec = c + .fields + .iter() + .map(|f| format!("{}: {}", f.name, read_expr(&f.ty))) + .collect(); + format!("[{}, (r) => ({{ {} }})]", c.value, fields.join(", ")) + }) + .collect(); + if payloads.is_empty() { + w.block(format!("function $from${key}(e) {{"), "}", |w| { + w.line(format!("return $domain(e, $codes${key});")); + }); + } else { + w.line(format!( + "const $payloads${key} = new Map([{}]);", + payloads.join(", ") + )); + w.block(format!("function $from${key}(e) {{"), "}", |w| { + w.line(format!("return $domain(e, $codes${key}, $payloads${key});")); + }); + } + w.blank(); +} + +/// Emit the adapter for one callback interface: it checks the consumer's +/// implementation and returns the raw object the transport calls, whose +/// methods (named as in the IDL) convert raw arguments into idiomatic values +/// (decoding buffers, adopting objects), call the implementation, and check +/// the return value. A thrown exception reaches the transport, which +/// reports it to the producer as a code -4 failure. +fn emit_adapter( + w: &mut CodeWriter, + m: &ModuleBinding, + cb: &CallbackInterfaceBinding, + prefix: &str, +) { + let protocol = cb.protocol(prefix); + let dotted = format!("{}.{}", m.dot_path, cb.name); + w.block( + format!("function {}(impl) {{", helper("adapt", &dotted)), + "}", + |w| { + w.line(format!("$impl(impl, {});", js_string(&cb.name))); + w.block("return {", "};", |w| { + for (method, passes) in cb.methods.iter().zip(&protocol.method_args) { + let raw: Vec = + (0..method.params.len()).map(|i| format!("$a{i}")).collect(); + let args: Vec = method + .params + .iter() + .zip(passes) + .zip(&raw) + .map(|((p, pass), a)| receive(&p.ty, pass, a)) + .collect(); + let call = format!( + "impl.{}({})", + callback_method_name(&method.name), + args.join(", ") + ); + w.block( + format!("{}({}) {{", method.name, raw.join(", ")), + "},", + |w| match &method.ret { + None => { + w.line(format!("{call};")); + } + Some(ty) => { + let where_ = js_string(&format!( + "{}.{}", + cb.name, + callback_method_name(&method.name) + )); + w.line(format!( + "return $ret.{}({call}, {where_});", + direct_prim(ty).pascal() + )); + } + }, + ); + } + }); + }, + ); + w.blank(); +} + +/// The idiomatic value of a raw value `raw` of type `ty` received from the +/// native side per `pass`: decoded when buffered, adopted when an object, +/// unchanged otherwise. +fn receive(ty: &Ty, pass: &RetPass, raw: &str) -> String { + match pass { + RetPass::Buffer => format!("$decode({raw}, {})", reader_fn(ty)), + RetPass::Object { nullable, .. } => { + let cls = type_decl(ty.interface_name().expect("object type names an interface")); + if *nullable { + format!("$adoptOpt({cls}, {raw})") + } else { + format!("$adopt({cls}, {raw})") + } + } + RetPass::Void | RetPass::Direct | RetPass::String | RetPass::Bytes => raw.to_string(), + } +} + +/// Emit the iterator spec of an iterator-returning callable (`key` names +/// the callable), the shared `next`/`destroy`/error mapping/element +/// conversion its `$Iterator`s use. +fn emit_iterator_spec( + w: &mut CodeWriter, + model: &BindingModel, + m: &ModuleBinding, + f: &FnBinding, + key: &str, +) { + let CallShape::Iterator(it) = &f.shape else { + return; + }; + let protocol = it.protocol(f, &model.prefix); + let convert = match receive(&it.elem, &protocol.elem, "v") { + v if v == "v" => "null".to_string(), + other => format!("(v) => {other}"), + }; + w.block(format!("const $it${key} = {{"), "};", |w| { + w.line(format!("next: (h) => $raw.{}(h),", it.next.symbol)); + w.line(format!("destroy: (h) => $raw.{}(h),", it.destroy_symbol)); + w.line(format!( + "map: {},", + error_mapper(&model.modules, f, m.error.as_ref()) + )); + w.line(format!("convert: {convert},")); + }); +} + +/// Emit one interface's class. It extends the runtime's `$Object` (which +/// provides `close()` and `[Symbol.dispose]()`), exposes the canonical +/// synchronous `new` constructor as the JavaScript constructor and every +/// other constructor as a static factory, and gives the runtime its native +/// `$destroy` and `$clone`. +fn emit_class(w: &mut CodeWriter, model: &BindingModel, m: &ModuleBinding, i: &InterfaceBinding) { + let cls = decl(&m.segments, &i.name); + let canonical = i + .constructors + .iter() + .find(|c| c.name == "new" && !c.is_async); + for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) { + emit_iterator_spec(w, model, m, f, &format!("{cls}${}", f.name)); + } + w.block( + format!("const {cls} = class {} extends $Object {{", i.name), + "};", + |w| { + match canonical { + Some(c) => { + w.block( + format!("constructor({}) {{", js_params(c).join(", ")), + "}", + |w| { + w.line("super();"); + emit_body(w, model, m, c, None, &cls, Finish::Own); + }, + ); + } + None => { + w.block("constructor() {", "}", |w| { + w.line("super();"); + w.line(format!( + "throw new TypeError({});", + js_string(&format!( + "{} has no public constructor; use its factory functions", + i.name + )) + )); + }); + } + } + w.block("static $destroy(h) {", "}", |w| { + w.line(format!("$raw.{}(h);", i.destroy_symbol)); + }); + w.block("static $clone(h) {", "}", |w| { + w.line(format!("return $raw.{}(h);", i.clone_symbol)); + }); + let members = i + .constructors + .iter() + .filter(|c| canonical.is_none_or(|k| !std::ptr::eq(k, *c))) + .map(|f| (f, true)) + .chain(i.methods.iter().map(|f| (f, false))) + .chain(i.statics.iter().map(|f| (f, true))); + for (f, is_static) in members { + let mut params = js_params(f); + if f.cancellable { + params.push("$options".into()); + } + let head = format!( + "{}{}{}({}) {{", + if is_static { "static " } else { "" }, + if f.is_async { "async " } else { "" }, + member_name(&f.name, is_static), + params.join(", ") + ); + let recv = (!is_static).then_some(cls.as_str()); + let key = format!("{cls}${}", f.name); + w.block(head, "}", |w| { + emit_body(w, model, m, f, recv, &key, Finish::Return); + }); + } + }, + ); + w.blank(); +} + +/// What a callable's body does with the converted result. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Finish { + /// Return it (or nothing, for a void callable). + Return, + /// Bind the returned handle to `this` (the canonical constructor). + Own, +} + +/// One object lent to a call: the wrapper expression, its class, and +/// whether `null` is allowed. +struct Loan { + value: String, + local: String, + cls: String, + nullable: bool, +} + +/// Emit the body of one callable. `recv` is the class of an instance +/// method's receiver; `key` names the callable's iterator spec. +fn emit_body( + w: &mut CodeWriter, + model: &BindingModel, + m: &ModuleBinding, + f: &FnBinding, + recv: Option<&str>, + key: &str, + finish: Finish, +) { + let mut loans: Vec = Vec::new(); + let mut args: Vec = Vec::new(); + if let Some(cls) = recv { + loans.push(Loan { + value: "this".into(), + local: "$self".into(), + cls: cls.to_string(), + nullable: false, + }); + args.push("$self".into()); + } + for p in &f.params { + args.push(arg_expr(p, &mut loans)); + } + let symbol = match &f.shape { + CallShape::Sync(abi) => &abi.symbol, + CallShape::Async(a) => &a.launch.symbol, + CallShape::Iterator(it) => &it.launch.symbol, + }; + let call = if f.cancellable { + args.push("$t".into()); + format!( + "await $cancellable($options?.signal, $tokens, ($t) => $raw.{symbol}({}))", + args.join(", ") + ) + } else if f.is_async { + format!("await $raw.{symbol}({})", args.join(", ")) + } else { + format!("$raw.{symbol}({})", args.join(", ")) + }; + let stmt = match (&f.shape, finish) { + (_, Finish::Own) => format!("$own(this, {call});"), + (CallShape::Iterator(_), _) => format!("return new $Iterator({call}, $it${key});"), + _ => match plan::ret_pass(f.ret.as_ref(), &model.prefix) { + RetPass::Void => format!("{call};"), + pass => format!( + "return {};", + receive(f.ret.as_ref().expect("non-void"), &pass, &call) + ), + }, + }; + let map = error_mapper(&model.modules, f, m.error.as_ref()); + emit_loans(w, &loans, &stmt, &map); +} + +/// Emit the loans (each `$lend` paired with an `$unlend` in a `finally`) +/// around the call statement, whose faults are mapped by `map`. +fn emit_loans(w: &mut CodeWriter, loans: &[Loan], stmt: &str, map: &str) { + let Some((first, rest)) = loans.split_first() else { + w.line("try {"); + w.scope(|w| { + w.line(stmt); + }); + emit_catch(w, map); + w.line("}"); + return; + }; + let lend = if first.nullable { "$lendOpt" } else { "$lend" }; + w.line(format!( + "const {} = {lend}({}, {});", + first.local, first.value, first.cls + )); + w.line("try {"); + w.scope(|w| { + if rest.is_empty() { + w.line(stmt); + } else { + emit_loans(w, rest, stmt, map); + } + }); + if rest.is_empty() { + emit_catch(w, map); + } + w.line("} finally {"); + w.scope(|w| { + w.line(format!("$unlend({});", first.value)); + }); + w.line("}"); +} + +/// Emit the `catch` clause mapping a native fault through `map`. +fn emit_catch(w: &mut CodeWriter, map: &str) { + w.line("} catch ($e) {"); + w.scope(|w| { + w.line(format!("throw {map}($e);")); + }); +} + +/// The raw argument expression of one parameter, recording any object it +/// lends. +fn arg_expr(p: &ParamBinding, loans: &mut Vec) -> String { + let name = param_name(&p.name); + match p.arg_pass() { + ArgPass::Direct { .. } | ArgPass::String { .. } | ArgPass::Bytes { .. } => name, + ArgPass::Buffer { .. } => format!("$encode({name}, {})", writer_fn(&p.ty)), + ArgPass::Object { nullable, .. } => { + let cls = type_decl( + p.ty.interface_name() + .expect("object parameter names an interface"), + ); + let local = format!("$o{}", loans.len()); + loans.push(Loan { + value: name, + local: local.clone(), + cls, + nullable, + }); + local + } + ArgPass::Callback { .. } => { + let cb = + p.ty.callback_interface_name() + .expect("callback parameter names a callback interface"); + format!("{}({name})", helper("adapt", cb)) + } + } +} + +/// Emit the frozen namespace object of `m` and, nested, its submodules. +/// `lead` precedes the opening line (`export const kv = ` for a root, +/// `stats: ` for a submodule). +fn emit_namespace(w: &mut CodeWriter, model: &BindingModel, m: &ModuleBinding, lead: &str) { + w.line(format!("{lead}Object.freeze({{")); + w.scope(|w| { + if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { + w.line(format!( + "{}: {},", + eb.type_name, + decl(&m.segments, &eb.type_name) + )); + for c in &eb.codes { + let class = code_class(&c.name); + w.line(format!("{class}: {},", decl(&m.segments, &class))); + } + } + for e in m.enums.iter().filter(|e| !e.rich) { + w.line(format!("{}: {},", e.name, decl(&m.segments, &e.name))); + } + for i in &m.interfaces { + w.line(format!("{}: {},", i.name, decl(&m.segments, &i.name))); + } + for f in &m.functions { + let name = fn_name(&f.name); + w.line(format!("{name}: {},", decl(&m.segments, &name))); + } + for child in model.children(m) { + emit_namespace(w, model, child, &format!("{}: ", module_name(child))); + } + }); + w.line(if m.segments.len() == 1 { "});" } else { "})," }); +} diff --git a/crates/weaveffi-gen/src/targets/js/codec.rs b/crates/weaveffi-gen/src/targets/js/codec.rs new file mode 100644 index 00000000..f4e35ac9 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/js/codec.rs @@ -0,0 +1,200 @@ +//! Value-buffer codec emitters. +//! +//! Records, rich enums, optionals, lists, maps, and error payloads cross the +//! ABI as value buffers. The runtime's `$Writer` and `$Reader` implement the +//! wire primitives (`writeI32`, `readString`, ...) and the optional, list, +//! and map combinators; this module emits the expressions that compose them +//! for a given type, plus one writer and one reader function per record and +//! rich enum (`$w$kv$Entry`, `$r$kv$Entry`). Every dispatch goes through +//! [`Ty::wire`], and primitives collapse onto one arm through +//! [`Prim::pascal`]. +//! +//! Object tokens carry a strong reference: writing one clones the wrapper's +//! native object (`$clone`), and reading one adopts the reference into a new +//! wrapper. The transport defines `$token`, which turns the `u64` token into +//! its handle representation. + +use weaveffi_model::model::{BindingModel, Prim, Ty, WireType}; + +use crate::codegen::CodeWriter; +use crate::targets::js::names::{helper, js_string, type_decl}; + +/// A function expression `(w, v) => ...` writing one value of `ty`. +pub(crate) fn writer_fn(ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(p) => format!("$W.{}", p.pascal()), + WireType::Enum(_) => "$W.I32".into(), + WireType::User(n) => helper("w", n), + _ => format!("(w, v) => {}", write_expr(ty, "v")), + } +} + +/// A function expression `(r) => value` reading one value of `ty`. +pub(crate) fn reader_fn(ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(p) => format!("$R.{}", p.pascal()), + WireType::Enum(_) => "$R.I32".into(), + WireType::User(n) => helper("r", n), + _ => format!("(r) => {}", read_expr(ty)), + } +} + +/// The expression writing `val` of type `ty` to the writer `w`. +pub(crate) fn write_expr(ty: &Ty, val: &str) -> String { + match ty.wire() { + WireType::Prim(p) => format!("w.write{}({val})", p.pascal()), + WireType::Enum(_) => format!("w.writeI32({val})"), + WireType::Object(n) => format!("w.writeU64(BigInt($clone({val}, {})))", type_decl(n)), + WireType::User(n) => format!("{}(w, {val})", helper("w", n)), + WireType::Optional(inner) => format!("w.writeOpt({val}, {})", writer_fn(inner)), + WireType::List(inner) => format!("w.writeList({val}, {})", writer_fn(inner)), + WireType::Map(k, v) => format!("w.writeMap({val}, {}, {})", key_writer_fn(k), writer_fn(v)), + } +} + +/// The expression reading one value of type `ty` from the reader `r`. +pub(crate) fn read_expr(ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(p) => format!("r.read{}()", p.pascal()), + WireType::Enum(_) => "r.readI32()".into(), + WireType::Object(n) => format!("$adopt({}, $token(r.readU64()))", type_decl(n)), + WireType::User(n) => format!("{}(r)", helper("r", n)), + WireType::Optional(inner) => format!("r.readOpt({})", reader_fn(inner)), + WireType::List(inner) => format!("r.readList({})", reader_fn(inner)), + WireType::Map(k, v) => format!("r.readMap({}, {})", reader_fn(k), reader_fn(v)), + } +} + +/// A writer for one map key. Plain-object keys are always strings, so +/// numeric, `bigint`, `bool`, and enum keys convert first (a `Map`'s typed +/// keys pass through the same conversions unchanged). +fn key_writer_fn(ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(Prim::String) => "$W.String".into(), + WireType::Prim(Prim::Bool) => "(w, k) => w.writeBool(k === true || k === 'true')".into(), + WireType::Prim(p @ (Prim::I64 | Prim::U64)) => { + format!("(w, k) => w.write{}(BigInt(k))", p.pascal()) + } + WireType::Prim(p) => format!("(w, k) => w.write{}(Number(k))", p.pascal()), + WireType::Enum(_) => "(w, k) => w.writeI32(Number(k))".into(), + other => unreachable!("validation admits no {other:?} map keys"), + } +} + +/// Emit the writer and reader of every record and rich enum in the model. +pub(crate) fn emit_codecs(w: &mut CodeWriter, model: &BindingModel) { + for m in &model.modules { + for s in &m.structs { + let dotted = format!("{}.{}", m.dot_path, s.name); + w.block( + format!("function {}(w, v) {{", helper("w", &dotted)), + "}", + |w| { + for f in &s.fields { + w.line(format!("{};", write_expr(&f.ty, &format!("v.{}", f.name)))); + } + }, + ); + w.block( + format!("function {}(r) {{", helper("r", &dotted)), + "}", + |w| { + if s.fields.is_empty() { + w.line("return {};"); + } else { + w.block("return {", "};", |w| { + for f in &s.fields { + w.line(format!("{}: {},", f.name, read_expr(&f.ty))); + } + }); + } + }, + ); + } + for e in m.enums.iter().filter(|e| e.rich) { + let dotted = format!("{}.{}", m.dot_path, e.name); + let what = js_string(&format!("unknown {} tag: ", e.name)); + w.block( + format!("function {}(w, v) {{", helper("w", &dotted)), + "}", + |w| { + w.block("switch (v.tag) {", "}", |w| { + for v in &e.variants { + w.line(format!("case {}:", js_string(&v.name))); + w.scope(|w| { + w.line(format!("w.writeI32({});", v.value)); + for f in &v.fields { + w.line(format!( + "{};", + write_expr(&f.ty, &format!("v.{}", f.name)) + )); + } + w.line("return;"); + }); + } + w.line("default:"); + w.line(format!(" throw new TypeError({what} + (v && v.tag));")); + }); + }, + ); + w.block( + format!("function {}(r) {{", helper("r", &dotted)), + "}", + |w| { + w.line("const tag = r.readI32();"); + w.block("switch (tag) {", "}", |w| { + for v in &e.variants { + let fields: String = v + .fields + .iter() + .map(|f| format!(", {}: {}", f.name, read_expr(&f.ty))) + .collect(); + w.line(format!( + "case {}: return {{ tag: {}{fields} }};", + v.value, + js_string(&v.name) + )); + } + w.line("default:"); + w.line(format!(" throw new $Error(-3, {what} + tag);")); + }); + }, + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn primitives_collapse_onto_pascal_names() { + assert_eq!(write_expr(&Ty::I64, "v.n"), "w.writeI64(v.n)"); + assert_eq!(read_expr(&Ty::StringUtf8), "r.readString()"); + assert_eq!(writer_fn(&Ty::Bytes), "$W.Bytes"); + assert_eq!(reader_fn(&Ty::Enum("kv.Kind".into())), "$R.I32"); + } + + #[test] + fn composites_use_the_runtime_combinators() { + let ty = Ty::Optional(Box::new(Ty::List(Box::new(Ty::Record("kv.Entry".into()))))); + assert_eq!( + write_expr(&ty, "v.x"), + "w.writeOpt(v.x, (w, v) => w.writeList(v, $w$kv$Entry))" + ); + assert_eq!(read_expr(&ty), "r.readOpt((r) => r.readList($r$kv$Entry))"); + let map = Ty::Map( + Box::new(Ty::U64), + Box::new(Ty::Interface("kv.Store".into())), + ); + assert_eq!( + write_expr(&map, "v.m"), + "w.writeMap(v.m, (w, k) => w.writeU64(BigInt(k)), (w, v) => w.writeU64(BigInt($clone(v, kv$Store))))" + ); + assert_eq!( + read_expr(&map), + "r.readMap($R.U64, (r) => $adopt(kv$Store, $token(r.readU64())))" + ); + } +} diff --git a/crates/weaveffi-gen/src/targets/js/dts.rs b/crates/weaveffi-gen/src/targets/js/dts.rs new file mode 100644 index 00000000..bd2ada70 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/js/dts.rs @@ -0,0 +1,342 @@ +//! TypeScript declarations (`index.d.ts`), shared by both transports. +//! +//! Each top-level IDL module is an exported namespace (`kv`), nested +//! modules are nested namespaces, and user types are referenced by their +//! full path from the root (`kv.Entry`), so equal names in different modules +//! never collide. + +use weaveffi_model::model::{ + BindingModel, CallShape, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, + InterfaceBinding, ModuleBinding, ParamBinding, StructBinding, Ty, +}; + +use crate::codegen::CodeWriter; +use crate::targets::js::names::{ + callback_method_name, code_class, error_owner, fn_name, js_string, member_name, module_name, + param_name, ts_path, ts_type, +}; + +/// Render the declarations body: the root error classes, `extra` (the +/// transport's own exports, such as WebAssembly's `init`), the leak-counter +/// hook, and one namespace per top-level module. `root_error` is the root +/// error class name. +pub(crate) fn render_declarations(model: &BindingModel, root_error: &str, extra: &str) -> String { + let mut w = CodeWriter::two_space(); + w.line("/// "); + w.blank(); + w.line("/**"); + w.line(" * The root of every error these bindings throw. `code` is the ABI error"); + w.line(" * code: positive for a declared domain error, negative for a runtime trap"); + w.line(" * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback"); + w.line(" * implementation failed, -5 cancelled)."); + w.line(" */"); + w.block( + format!("export declare class {root_error} extends Error {{"), + "}", + |w| { + w.line("readonly code: number;"); + w.line("constructor(code: number, message?: string);"); + }, + ); + w.line("/** The error a cancelled async call rejects with (code -5). */"); + w.block( + format!("export declare class CancelledError extends {root_error} {{"), + "}", + |w| { + w.line("constructor(message?: string);"); + }, + ); + w.line("/** The root error class, under a name no namespace member can shadow. */"); + w.line(format!("declare const $Error: typeof {root_error};")); + w.raw(extra); + w.line("/**"); + w.line(" * The native library's live-allocation counters (0 objects, 1 callbacks,"); + w.line(" * 2 iterators, 3 cancel tokens, 4 returned buffers), for leak tests."); + w.line(" * @internal"); + w.line(" */"); + w.line("export declare function __debugLive(kind: number): bigint;"); + for m in model.roots() { + w.blank(); + doc(&mut w, m.doc.as_deref(), &[]); + w.block( + format!("export declare namespace {} {{", module_name(m)), + "}", + |w| namespace_body(w, model, m), + ); + } + w.finish() +} + +fn namespace_body(w: &mut CodeWriter, model: &BindingModel, m: &ModuleBinding) { + if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { + error_domain(w, m, eb); + } + for e in &m.enums { + enumeration(w, e); + } + for s in &m.structs { + record(w, s); + } + for cb in &m.callback_interfaces { + callback_interface(w, cb); + } + for i in &m.interfaces { + class(w, model, m, i); + } + for f in &m.functions { + doc(w, f.doc.as_deref(), &fn_tags(model, m, f)); + w.line(format!( + "export function {}({}): {};", + fn_name(&f.name), + params(f), + ret(f) + )); + } + for child in model.children(m) { + doc(w, child.doc.as_deref(), &[]); + w.block( + format!("export namespace {} {{", module_name(child)), + "}", + |w| namespace_body(w, model, child), + ); + } +} + +/// Emit a JSDoc block: the doc text, then `tags` (each a full tag line such +/// as `@deprecated use x`). Nothing when both are empty. +fn doc(w: &mut CodeWriter, text: Option<&str>, tags: &[String]) { + let text = text.map(str::trim).filter(|t| !t.is_empty()); + let mut lines: Vec = text + .map(|t| t.lines().map(str::to_string).collect()) + .unwrap_or_default(); + lines.extend(tags.iter().cloned()); + match lines.as_slice() { + [] => {} + [one] => { + w.line(format!("/** {one} */")); + } + _ => { + w.line("/**"); + for l in &lines { + if l.is_empty() { + w.line(" *"); + } else { + w.line(format!(" * {l}")); + } + } + w.line(" */"); + } + } +} + +fn deprecated(msg: Option<&String>) -> Vec { + msg.map(|m| vec![format!("@deprecated {m}")]) + .unwrap_or_default() +} + +/// The JSDoc tags of a callable: documented parameters, the domain it +/// throws, and any deprecation. +fn fn_tags(model: &BindingModel, m: &ModuleBinding, f: &FnBinding) -> Vec { + let mut tags = param_tags(&f.params); + if let (true, Some(eb)) = (f.throws, m.error.as_ref()) { + let owner = error_owner(&model.modules, eb); + tags.push(format!( + "@throws {{{}}}", + ts_path(&format!("{}.{}", owner.dot_path, eb.type_name)) + )); + } + tags.extend(deprecated(f.deprecated.as_ref())); + tags +} + +fn param_tags(params: &[ParamBinding]) -> Vec { + params + .iter() + .filter_map(|p| { + let d = p.doc.as_deref()?.trim(); + (!d.is_empty()) + .then(|| format!("@param {} {}", param_name(&p.name), d.replace('\n', " "))) + }) + .collect() +} + +fn params(f: &FnBinding) -> String { + let mut out: Vec = f + .params + .iter() + .map(|p| format!("{}: {}", param_name(&p.name), ts_type(&p.ty))) + .collect(); + if f.cancellable { + out.push("options?: { signal?: AbortSignal }".into()); + } + out.join(", ") +} + +fn ret(f: &FnBinding) -> String { + let base = match (&f.shape, &f.ret) { + (CallShape::Iterator(it), _) => format!("IterableIterator<{}>", ts_type(&it.elem)), + (_, Some(ty)) => ts_type(ty), + (_, None) => "void".into(), + }; + if f.is_async { + format!("Promise<{base}>") + } else { + base + } +} + +fn error_domain(w: &mut CodeWriter, m: &ModuleBinding, eb: &ErrorBinding) { + let domain_ts = ts_path(&format!("{}.{}", m.dot_path, eb.type_name)); + w.line(format!( + "/** The errors of the `{}` error domain. */", + m.dot_path + )); + w.block( + format!("export class {} extends $Error {{", eb.type_name), + "}", + |w| { + w.line("constructor(code: number, message?: string);"); + }, + ); + for c in &eb.codes { + doc(w, Some(c.doc.as_deref().unwrap_or(&c.message)), &[]); + w.block( + format!( + "export class {} extends {domain_ts} {{", + code_class(&c.name) + ), + "}", + |w| { + w.line(format!("static readonly CODE: {};", c.value)); + for f in &c.fields { + doc(w, f.doc.as_deref(), &[]); + w.line(format!("readonly {}: {};", f.name, ts_type(&f.ty))); + } + w.line("constructor(message?: string);"); + }, + ); + } +} + +fn enumeration(w: &mut CodeWriter, e: &EnumBinding) { + doc(w, e.doc.as_deref(), &deprecated(e.deprecated.as_ref())); + if e.rich { + w.line(format!("export type {} =", e.name)); + w.scope(|w| { + let last = e.variants.len().saturating_sub(1); + for (i, v) in e.variants.iter().enumerate() { + doc(w, v.doc.as_deref(), &[]); + let fields: String = v + .fields + .iter() + .map(|f| format!("; {}: {}", f.name, ts_type(&f.ty))) + .collect(); + let end = if i == last { ";" } else { "" }; + w.line(format!("| {{ tag: {}{fields} }}{end}", js_string(&v.name))); + } + }); + return; + } + w.block(format!("export enum {} {{", e.name), "}", |w| { + for v in &e.variants { + doc(w, v.doc.as_deref(), &[]); + w.line(format!("{} = {},", v.name, v.value)); + } + }); +} + +fn record(w: &mut CodeWriter, s: &StructBinding) { + doc(w, s.doc.as_deref(), &deprecated(s.deprecated.as_ref())); + w.block(format!("export interface {} {{", s.name), "}", |w| { + for f in &s.fields { + doc(w, f.doc.as_deref(), &[]); + w.line(format!("{}: {};", f.name, ts_type(&f.ty))); + } + }); +} + +/// A callback interface: any object with these methods implements it. +pub(crate) fn callback_interface(w: &mut CodeWriter, cb: &CallbackInterfaceBinding) { + doc(w, cb.doc.as_deref(), &deprecated(cb.deprecated.as_ref())); + w.block(format!("export interface {} {{", cb.name), "}", |w| { + for m in &cb.methods { + let mut tags = param_tags(&m.params); + tags.extend(deprecated(m.deprecated.as_ref())); + doc(w, m.doc.as_deref(), &tags); + let ps: Vec = m + .params + .iter() + .map(|p| format!("{}: {}", param_name(&p.name), ts_type(&p.ty))) + .collect(); + let r = m.ret.as_ref().map_or_else(|| "void".to_string(), ts_type); + w.line(format!( + "{}({}): {r};", + callback_method_name(&m.name), + ps.join(", ") + )); + } + }); +} + +fn class(w: &mut CodeWriter, model: &BindingModel, m: &ModuleBinding, i: &InterfaceBinding) { + let self_ty = ts_type(&Ty::Interface(format!("{}.{}", m.dot_path, i.name))); + doc(w, i.doc.as_deref(), &deprecated(i.deprecated.as_ref())); + w.block(format!("export class {} {{", i.name), "}", |w| { + let canonical = i + .constructors + .iter() + .find(|c| c.name == "new" && !c.is_async); + match canonical { + Some(c) => { + doc(w, c.doc.as_deref(), &fn_tags(model, m, c)); + w.line(format!("constructor({});", params(c))); + } + None => { + w.line("private constructor();"); + } + } + for c in i + .constructors + .iter() + .filter(|c| canonical.is_none_or(|k| !std::ptr::eq(k, *c))) + { + doc(w, c.doc.as_deref(), &fn_tags(model, m, c)); + let r = if c.is_async { + format!("Promise<{self_ty}>") + } else { + self_ty.clone() + }; + w.line(format!( + "static {}({}): {r};", + member_name(&c.name, true), + params(c) + )); + } + for f in &i.methods { + doc(w, f.doc.as_deref(), &fn_tags(model, m, f)); + w.line(format!( + "{}({}): {};", + member_name(&f.name, false), + params(f), + ret(f) + )); + } + for f in &i.statics { + doc(w, f.doc.as_deref(), &fn_tags(model, m, f)); + w.line(format!( + "static {}({}): {};", + member_name(&f.name, true), + params(f), + ret(f) + )); + } + w.line("/**"); + w.line(" * Release this wrapper's reference to the native object. Safe to call"); + w.line(" * more than once; a wrapper that is never closed is released when it is"); + w.line(" * garbage collected. Using the wrapper after `close()` throws."); + w.line(" */"); + w.line("close(): void;"); + w.line("/** Alias of `close()` for `using` declarations. */"); + w.line("[Symbol.dispose](): void;"); + }); +} diff --git a/crates/weaveffi-gen/src/targets/js/mod.rs b/crates/weaveffi-gen/src/targets/js/mod.rs new file mode 100644 index 00000000..d65678ce --- /dev/null +++ b/crates/weaveffi-gen/src/targets/js/mod.rs @@ -0,0 +1,372 @@ +//! The JavaScript layer shared by the [`node`](crate::targets::node) and +//! [`wasm`](crate::targets::wasm) targets. +//! +//! Both targets generate the same ES module: one exported namespace object +//! per top-level IDL module (`export const kv = { Store, get, ... }`, with +//! nested modules as nested namespaces), the same error classes (a root +//! `{Package}Error`, one `{Domain}Error` per error domain, one `{Code}Error` +//! per code, and `CancelledError`), records as plain objects, rich enums as +//! tagged unions, interfaces as classes with `close()`, `[Symbol.dispose]`, +//! and a `FinalizationRegistry` backstop, lazy iterators, `Promise`-returning +//! async functions with `AbortSignal` cancellation, and the same `.d.ts`. +//! Only the transport differs: Node.js calls an N-API addon, WebAssembly +//! stages values in linear memory. Both expose the raw calling convention +//! documented in [`api`] as an object named `$raw`. +//! +//! The fixed runtime (codec, error classes, object and iterator wrappers, +//! cancellation, the load-time check) is a real JavaScript file, +//! `runtime/runtime.js`, emitted next to `index.js`. + +mod api; +mod codec; +mod dts; +pub(crate) mod names; + +use weaveffi_model::model::{BindingModel, ABI_VERSION}; +use weaveffi_model::pkg::Identity; + +use crate::codegen::CodeWriter; +use crate::manifest::{JsonObject, JsonValue}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; + +pub(crate) use api::render_api; +pub(crate) use dts::callback_interface as render_callback_interface_dts; + +/// The shared runtime, with `{{ERROR_CLASS}}` naming the root error class. +const RUNTIME_JS: &str = include_str!("runtime/runtime.js"); + +/// The runtime helpers the generated API may reference. +const RUNTIME_IMPORTS: &[&str] = &[ + "$Fault", + "$fault", + "$domain", + "$W", + "$R", + "$encode", + "$decode", + "$Object", + "$own", + "$adopt", + "$adoptOpt", + "$lend", + "$lendOpt", + "$unlend", + "$clone", + "$Iterator", + "$cancellable", + "$impl", + "$ret", + "$verify", +]; + +/// The root error class of a package: `{PascalName}Error` (`KvstoreError`). +pub(crate) fn root_error_class(identity: &Identity) -> String { + weaveffi_model::errors::type_name(&identity.pascal_name(), "Error") +} + +/// Render `runtime.js`. +pub(crate) fn render_runtime(identity: &Identity, input_basename: &str) -> String { + let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); + out.push_str(&RUNTIME_JS.replace("{{ERROR_CLASS}}", &root_error_class(identity))); + out.push('\n'); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, "runtime.js")); + out +} + +/// The import and re-export lines every `index.js` starts with. +pub(crate) fn render_imports(identity: &Identity) -> String { + let root = root_error_class(identity); + let mut w = CodeWriter::two_space(); + w.line("import {"); + w.scope(|w| { + for name in RUNTIME_IMPORTS { + w.line(format!("{name},")); + } + w.line(format!("{root} as $Error,")); + }); + w.line("} from './runtime.js';"); + w.line(format!( + "export {{ {root}, CancelledError }} from './runtime.js';" + )); + w.finish() +} + +/// The load-time contract check: the ABI revision and every top-level +/// module's checksum. +pub(crate) fn render_verify(model: &BindingModel, identity: &Identity) -> String { + let sums: Vec = model + .roots() + .map(|m| { + format!( + "['{}', 0x{:016x}n]", + m.name, + m.checksum.expect("a top-level module has a checksum") + ) + }) + .collect(); + format!( + "$verify($raw, {}, '{}', {ABI_VERSION}, [{}]);\n", + names::js_string(&identity.name), + model.prefix, + sums.join(", ") + ) +} + +/// Render `index.d.ts`. `extra` carries the transport's own declarations. +pub(crate) fn render_dts( + model: &BindingModel, + identity: &Identity, + input_basename: &str, + extra: &str, +) -> String { + let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); + out.push_str(&dts::render_declarations( + model, + &root_error_class(identity), + extra, + )); + out.push('\n'); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, "index.d.ts")); + out +} + +/// The npm metadata both `package.json` files share: the generated-file +/// notice, then name, version, description, and the optional fields. +pub(crate) fn npm_metadata(identity: &Identity, name: &str, input_basename: &str) -> JsonObject { + let version = env!("CARGO_PKG_VERSION"); + let mut obj = JsonObject::new() + .str_entry( + "//", + format!("Generated by WeaveFFI {version} from {input_basename}"), + ) + .str_entry( + "//warning", + "DO NOT EDIT. Your changes will be overwritten.", + ) + .str_entry( + "//regenerate", + format!("To regenerate: weaveffi generate {input_basename} -o "), + ) + .str_entry("name", name) + .str_entry("version", &identity.version) + .str_entry("description", identity.description_or_default()) + .opt_str_entry("license", identity.license.as_deref()); + if let Some(author) = identity.authors.first() { + obj = obj.str_entry("author", author); + } + obj = obj.opt_str_entry("homepage", identity.homepage.as_deref()); + if let Some(repository) = &identity.repository { + obj = obj.entry( + "repository", + JsonValue::Object( + JsonObject::new() + .str_entry("type", "git") + .str_entry("url", repository), + ), + ); + } + obj.str_entry("type", "module") + .str_entry("main", "index.js") + .str_entry("types", "index.d.ts") + .entry( + "exports", + JsonValue::Object( + JsonObject::new().entry( + ".", + JsonValue::Object( + JsonObject::new() + .str_entry("types", "./index.d.ts") + .str_entry("default", "./index.js"), + ), + ), + ), + ) +} + +/// A small API exercising every shape both JavaScript targets render, for +/// their unit tests: an error domain with a payload, a C-style and a rich +/// enum, a record holding objects, a callback interface, an interface with +/// a canonical constructor, a method named `close`, a throwing method, an +/// iterator of objects, a nullable async result, and a cancellable async +/// method, free functions (one named after a keyword), and a nested module. +#[cfg(test)] +pub(crate) fn test_api(name: &str) -> weaveffi_model::resolved::ResolvedApi { + const YAML: &str = r#" +version: "0.10.0" +modules: + - name: kv + doc: A key-value store. + errors: + name: KvError + codes: + - { name: KEY_NOT_FOUND, code: 1, message: "key not found", fields: [{ name: key, type: string }] } + - { name: FULL, code: 2, message: "it's full" } + enums: + - name: Mode + variants: + - { name: Fast, value: 0 } + - { name: Safe, value: 7 } + - name: Shape + variants: + - { name: Empty, value: 0 } + - { name: Circle, value: 1, fields: [{ name: radius, type: f64 }] } + structs: + - name: Bundle + fields: + - { name: primary, type: Store } + - { name: extras, type: "[Store]" } + - { name: stamp, type: i64 } + - { name: by_mode, type: "{Mode:bool}" } + callback_interfaces: + - name: Listener + methods: + - name: on_message + params: [{ name: text, type: string }, { name: weight, type: u64 }] + return: bool + - name: on_bundle + params: [{ name: bundle, type: Bundle }, { name: store, type: Store }, { name: alt, type: "Store?" }] + interfaces: + - name: Store + doc: A store. + constructors: + - { name: new, params: [{ name: path, type: string }] } + methods: + - { name: get, params: [{ name: key, type: string }], return: bytes, throws: true } + - { name: close, params: [] } + - { name: scan, params: [], return: "iter" } + - { name: fetch, params: [{ name: path, type: string }], return: "Store?", async: true } + - { name: wait, params: [{ name: ms, type: i64 }], return: i64, async: true, cancellable: true } + functions: + - { name: subscribe, params: [{ name: listener, type: Listener }, { name: mode, type: Mode }] } + - { name: widen, params: [{ name: n, type: i64 }, { name: m, type: u64 }], return: u64 } + - { name: delete, params: [{ name: class, type: "Store?" }], return: bool } + - { name: shapes, params: [], return: "iter" } + modules: + - name: stats + functions: + - { name: count, params: [{ name: store, type: Store }], return: i64, throws: true } +"#; + let api = weaveffi_model::parse::parse_api_str(YAML, "yaml").expect("test fixture parses"); + weaveffi_model::validate::validate_api(api, None) + .expect("test fixture validates") + .with_identity(Identity::named(name)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn runtime_names_the_package_root_error() { + let rt = render_runtime(&Identity::named("my-kv"), "kv.yml"); + assert!( + rt.contains("export class MyKvError extends Error {"), + "{rt}" + ); + assert!(rt.contains("export class CancelledError extends MyKvError {")); + assert!(!rt.contains("{{")); + assert_eq!(root_error_class(&Identity::named("io_error")), "IoError"); + } + + fn api_js() -> String { + render_api(&BindingModel::build(&test_api("acme-kv"))) + } + + fn has(src: &str, needle: &str) { + assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); + } + + #[test] + fn modules_are_frozen_namespaces_with_escaped_members() { + let js = api_js(); + has(&js, "export const kv = Object.freeze({"); + has(&js, " Store: kv$Store,\n"); + has(&js, " delete_: kv$delete_,\n"); + has( + &js, + " stats: Object.freeze({\n count: kv$stats$count,\n }),\n});", + ); + has(&js, "const kv$delete_ = function delete_(class_) {"); + has(&js, " close_() {"); + } + + #[test] + fn calls_lend_objects_and_map_faults_by_strategy() { + let js = api_js(); + has( + &js, + " get(key) {\n const $self = $lend(this, kv$Store);\n try {\n return $raw.acme_kv_kv_Store_get($self, key);\n } catch ($e) {\n throw $from$kv$KvError($e);\n } finally {\n $unlend(this);\n }\n }", + ); + has(&js, "const $o0 = $lendOpt(class_, kv$Store);"); + has(&js, "throw $fault($e);"); + has( + &js, + "return $domain(e, $codes$kv$KvError, $payloads$kv$KvError);", + ); + has(&js, "[1, (r) => ({ key: r.readString() })]"); + has(&js, "constructor(message = 'it\\'s full') {"); + } + + #[test] + fn async_iterators_and_callbacks_follow_the_raw_convention() { + let js = api_js(); + has( + &js, + "return await $cancellable($options?.signal, $tokens, ($t) => $raw.acme_kv_kv_Store_wait($self, ms, $t));", + ); + has( + &js, + "return $adoptOpt(kv$Store, await $raw.acme_kv_kv_Store_fetch($self, path));", + ); + has( + &js, + "return new $Iterator($raw.acme_kv_kv_Store_scan($self), $it$kv$Store$scan);", + ); + has(&js, "convert: (v) => $adopt(kv$Store, v),"); + has(&js, "convert: (v) => $decode(v, $r$kv$Shape),"); + has( + &js, + "return $ret.Bool(impl.onMessage($a0, $a1), 'Listener.onMessage');", + ); + has(&js, "impl.onBundle($decode($a0, $r$kv$Bundle), $adopt(kv$Store, $a1), $adoptOpt(kv$Store, $a2));"); + has( + &js, + "$raw.acme_kv_kv_subscribe($adapt$kv$Listener(listener), mode);", + ); + } + + #[test] + fn codecs_clone_object_tokens_and_convert_map_keys() { + let js = api_js(); + has(&js, "w.writeU64(BigInt($clone(v.primary, kv$Store)));"); + has(&js, "primary: $adopt(kv$Store, $token(r.readU64())),"); + has( + &js, + "w.writeMap(v.by_mode, (w, k) => w.writeI32(Number(k)), $W.Bool);", + ); + } + + #[test] + fn declarations_qualify_types_and_alias_the_root_error() { + let model = BindingModel::build(&test_api("kv")); + let dts = render_dts(&model, &Identity::named("kv"), "kv.yml", ""); + has(&dts, "export declare class KvError extends Error {"); + has(&dts, "declare const $Error: typeof KvError;"); + has(&dts, "export declare namespace kv {"); + has(&dts, " export class KvError extends $Error {"); + has(&dts, " export class KeyNotFoundError extends kv.KvError {"); + has(&dts, " readonly key: string;"); + has( + &dts, + " wait(ms: bigint, options?: { signal?: AbortSignal }): Promise;", + ); + has(&dts, " scan(): IterableIterator;"); + has(&dts, " close_(): void;"); + has(&dts, " by_mode: Partial>;"); + has( + &dts, + " export function delete_(class_: kv.Store | null): boolean;", + ); + has(&dts, " export function count(store: kv.Store): bigint;"); + } +} diff --git a/crates/weaveffi-gen/src/targets/js/names.rs b/crates/weaveffi-gen/src/targets/js/names.rs new file mode 100644 index 00000000..79beef4b --- /dev/null +++ b/crates/weaveffi-gen/src/targets/js/names.rs @@ -0,0 +1,262 @@ +//! JavaScript and TypeScript spellings of IDL names and types. +//! +//! Generated code keeps three kinds of names apart so none can collide: +//! +//! * **Public names** are what consumers write: `kv.Store`, `kv.openStore`. +//! Identifiers that are reserved words gain a trailing underscore. +//! * **Declaration names** are the module-scope bindings of user +//! declarations, the dotted path joined with `$` (`kv$Store`, +//! `kv$stats$getStats`). IDL identifiers never contain `$`, so two modules +//! declaring the same name never clash, and no declaration can shadow a +//! JavaScript global. +//! * **Helper names** start with `$` (`$lend`, `$w$kv$Entry`): runtime +//! imports and generated support code, which no IDL name can reach. + +use heck::ToLowerCamelCase; +use weaveffi_model::model::{ErrorBinding, FnBinding, ModuleBinding, Prim, Ty, WireType}; + +use crate::lang; +use crate::utils::local_type_name; + +/// Identifiers that are not JS keywords but can't name a binding or +/// parameter in strict-mode (module) code. +const STRICT_RESERVED: &[&str] = &["arguments", "eval"]; + +/// Escape `name` with a trailing underscore when it can't be a JavaScript +/// binding (a keyword or a strict-mode reserved identifier). +pub(crate) fn js_ident(name: &str) -> String { + if lang::is_reserved(name, lang::JS_KEYWORDS) || STRICT_RESERVED.contains(&name) { + format!("{name}_") + } else { + name.to_string() + } +} + +/// The public name of a free function (`open_store` becomes `openStore`). +pub(crate) fn fn_name(name: &str) -> String { + js_ident(&name.to_lower_camel_case()) +} + +/// The name of an interface member. Instance members can't be named +/// `constructor` or `close` (the wrapper's own release method), and statics +/// can't replace the class's `name`, `length`, or `prototype`; those gain a +/// trailing underscore. +pub(crate) fn member_name(name: &str, is_static: bool) -> String { + let camel = name.to_lower_camel_case(); + let clash = if is_static { + matches!(camel.as_str(), "name" | "length" | "prototype" | "caller") + } else { + matches!(camel.as_str(), "constructor" | "close") + }; + if clash { + format!("{camel}_") + } else { + camel + } +} + +/// The method a callback-interface implementation provides (`on_evict` +/// becomes `onEvict`). +pub(crate) fn callback_method_name(name: &str) -> String { + name.to_lower_camel_case() +} + +/// The parameter name of `name` (`ttl_seconds` becomes `ttlSeconds`). +pub(crate) fn param_name(name: &str) -> String { + js_ident(&name.to_lower_camel_case()) +} + +/// The declaration name of `name` declared in module `segments`. +pub(crate) fn decl(segments: &[String], name: &str) -> String { + format!("{}${name}", segments.join("$")) +} + +/// The declaration name of a user type from its absolute dotted path +/// (`kv.Store` becomes `kv$Store`). +pub(crate) fn type_decl(dotted: &str) -> String { + dotted.replace('.', "$") +} + +/// A helper name derived from a user type (`$w` and `kv.Entry` give +/// `$w$kv$Entry`). +pub(crate) fn helper(kind: &str, dotted: &str) -> String { + format!("${kind}${}", type_decl(dotted)) +} + +/// The public name of a module (its namespace object). +pub(crate) fn module_name(m: &ModuleBinding) -> String { + js_ident(&m.name) +} + +/// The TypeScript path of a user type from its absolute dotted path, each +/// module segment escaped like the namespace it names (`kv.Store`). +pub(crate) fn ts_path(dotted: &str) -> String { + let local = local_type_name(dotted); + match dotted.rsplit_once('.') { + Some((modules, _)) => { + let mut out: Vec = modules.split('.').map(js_ident).collect(); + out.push(local.to_string()); + out.join(".") + } + None => local.to_string(), + } +} + +/// The error class name of an error code (`KEY_NOT_FOUND` becomes +/// `KeyNotFoundError`). +pub(crate) fn code_class(code: &str) -> String { + weaveffi_model::errors::type_name(code, "Error") +} + +/// The module that declares error domain `eb`. +pub(crate) fn error_owner<'a>( + modules: &'a [ModuleBinding], + eb: &ErrorBinding, +) -> &'a ModuleBinding { + modules + .iter() + .find(|m| m.path == eb.owner_path && m.declares_error()) + .expect("an error binding's owner module declares the domain") +} + +/// The function that maps a native failure of callable `f` (declared in a +/// module whose domain is `error`) onto the error classes: the domain's +/// mapper for a throwing callable, the runtime's `$fault` otherwise. +pub(crate) fn error_mapper( + modules: &[ModuleBinding], + f: &FnBinding, + error: Option<&ErrorBinding>, +) -> String { + match error { + Some(eb) if f.throws => { + let owner = error_owner(modules, eb); + format!("$from${}", decl(&owner.segments, &eb.name)) + } + _ => "$fault".to_string(), + } +} + +/// The TypeScript type of `ty`. +pub(crate) fn ts_type(ty: &Ty) -> String { + match ty { + Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::F32 | Ty::F64 => { + "number".into() + } + Ty::I64 | Ty::U64 => "bigint".into(), + Ty::Bool => "boolean".into(), + Ty::StringUtf8 => "string".into(), + Ty::Bytes => "Uint8Array".into(), + Ty::Record(n) + | Ty::RichEnum(n) + | Ty::Enum(n) + | Ty::Interface(n) + | Ty::CallbackInterface(n) => ts_path(n), + Ty::Optional(inner) => format!("{} | null", ts_type(inner)), + Ty::List(inner) => { + let t = ts_type(inner); + if t.contains(' ') { + format!("({t})[]") + } else { + format!("{t}[]") + } + } + Ty::Map(k, v) => { + let value = ts_type(v); + match k.as_ref() { + Ty::Enum(e) => format!("Partial>", ts_path(e)), + Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 => { + format!("Record") + } + _ => format!("Record"), + } + } + Ty::Iterator(inner) => format!("IterableIterator<{}>", ts_type(inner)), + } +} + +/// The wire primitive a direct-family type is read and written as (C-style +/// enums travel as their `i32` discriminant). +/// +/// # Panics +/// +/// Panics on a type outside the direct family. +pub(crate) fn direct_prim(ty: &Ty) -> Prim { + match ty.wire() { + WireType::Prim(p) => p, + WireType::Enum(_) => Prim::I32, + other => unreachable!("not a direct type: {other:?}"), + } +} + +/// A single-quoted JavaScript string literal of `s`. +pub(crate) fn js_string(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + out.push('\''); + for c in s.chars() { + match c { + '\\' => out.push_str("\\\\"), + '\'' => out.push_str("\\'"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + '\u{2028}' => out.push_str("\\u2028"), + '\u{2029}' => out.push_str("\\u2029"), + c if (c as u32) < 0x20 => out.push_str(&format!("\\x{:02x}", c as u32)), + c => out.push(c), + } + } + out.push('\''); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reserved_words_gain_an_underscore() { + assert_eq!(js_ident("class"), "class_"); + assert_eq!(js_ident("eval"), "eval_"); + assert_eq!(js_ident("store"), "store"); + assert_eq!(fn_name("delete"), "delete_"); + assert_eq!(fn_name("open_store"), "openStore"); + assert_eq!(param_name("import"), "import_"); + assert_eq!(member_name("close", false), "close_"); + assert_eq!(member_name("name", true), "name_"); + assert_eq!(member_name("name", false), "name"); + } + + #[test] + fn declaration_names_join_the_path_with_dollars() { + let segs = vec!["a".to_string(), "b".to_string()]; + assert_eq!(decl(&segs, "C"), "a$b$C"); + assert_eq!(type_decl("a.b.C"), "a$b$C"); + assert_eq!(helper("w", "kv.Entry"), "$w$kv$Entry"); + assert_eq!(ts_path("class.inner.Widget"), "class_.inner.Widget"); + } + + #[test] + fn ts_types_qualify_user_types_from_the_root() { + assert_eq!(ts_type(&Ty::Record("kv.Entry".into())), "kv.Entry"); + assert_eq!( + ts_type(&Ty::List(Box::new(Ty::Optional(Box::new(Ty::I64))))), + "(bigint | null)[]" + ); + assert_eq!( + ts_type(&Ty::Map( + Box::new(Ty::Enum("kv.Kind".into())), + Box::new(Ty::Bool) + )), + "Partial>" + ); + assert_eq!( + ts_type(&Ty::Map(Box::new(Ty::U64), Box::new(Ty::Bytes))), + "Record" + ); + } + + #[test] + fn string_literals_escape_quotes_and_controls() { + assert_eq!(js_string("it's\n\u{1}"), "'it\\'s\\n\\x01'"); + } +} diff --git a/crates/weaveffi-gen/src/targets/js/runtime/runtime.js b/crates/weaveffi-gen/src/targets/js/runtime/runtime.js new file mode 100644 index 00000000..48ac9191 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/js/runtime/runtime.js @@ -0,0 +1,569 @@ +// The runtime shared by every binding in this package: the error classes, +// the value-buffer codec, the object and iterator wrappers, cancellation, and +// the load-time contract check. The generated `index.js` imports it; the +// native transport (an N-API addon or a WebAssembly instance) never sees it, +// except for `$Fault`, the shape every native failure arrives in. + +/** + * The root of every error these bindings throw. `code` is the ABI error + * code: positive for a declared domain error, negative for a runtime trap + * (-1 generic, -2 producer panic, -3 marshalling failure, -4 a callback + * implementation failed, -5 cancelled). + */ +export class {{ERROR_CLASS}} extends Error { + constructor(code, message) { + super(message === undefined ? '' : message); + this.name = new.target.name; + this.code = code; + } +} + +/** The error a cancelled async call rejects with (code -5). */ +export class CancelledError extends {{ERROR_CLASS}} { + constructor(message = 'cancelled') { + super(-5, message); + } +} + +/** + * A failure as the native transport reports it, before the generated + * wrappers map it onto an error class. `payload` holds the matched error + * code's fields as a value buffer, or `null`. + */ +export class $Fault { + constructor(code, message, payload) { + this.code = code; + this.message = message; + this.payload = payload === undefined ? null : payload; + } +} + +/** + * Map an exception raised by a native call onto the public error classes: + * a cancelled call becomes `CancelledError`, any other fault the root error, + * and a WebAssembly trap (a producer panic on a `panic = "abort"` build) a + * code -2 error. Anything else (a `TypeError` from argument checking, say) + * passes through unchanged. + */ +export function $fault(e) { + if (e instanceof $Fault) { + return e.code === -5 + ? new CancelledError(e.message || undefined) + : new {{ERROR_CLASS}}(e.code, e.message); + } + if (typeof WebAssembly === 'object' && e instanceof WebAssembly.RuntimeError) { + return new {{ERROR_CLASS}}(-2, 'the native library trapped: ' + e.message); + } + return e; +} + +/** + * Map a fault onto an error domain: `codes` maps each declared code to its + * class, and `payloads` (optional) maps codes that carry fields to the + * reader that decodes them onto the error. Codes outside the domain fall + * back to `$fault`. + */ +export function $domain(e, codes, payloads) { + if (e instanceof $Fault) { + const cls = codes.get(e.code); + if (cls !== undefined) { + const err = new cls(e.message || undefined); + const read = payloads === undefined ? undefined : payloads.get(e.code); + if (read !== undefined && e.payload !== null) { + Object.assign(err, $decode(e.payload, read)); + } + return err; + } + } + return $fault(e); +} + +function $malformed(what) { + return new {{ERROR_CLASS}}(-3, 'malformed value buffer: ' + what); +} + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw $expected('a bigint', v); +} + +const $utf8 = new TextEncoder(); +const $utf8Strict = new TextDecoder('utf-8', { fatal: true }); + +/** + * Writes the value-buffer wire format: little-endian, packed, no alignment. + * Strings and bytes are a u32 length then the bytes, optionals a presence + * byte then the value, lists a u32 count then the elements, and maps a u32 + * count then alternating keys and values. + */ +export class $Writer { + constructor() { + this.buf = new Uint8Array(64); + this.view = new DataView(this.buf.buffer); + this.len = 0; + } + + reserve(n) { + if (this.len + n <= this.buf.length) return; + let cap = this.buf.length * 2; + while (cap < this.len + n) cap *= 2; + const grown = new Uint8Array(cap); + grown.set(this.buf.subarray(0, this.len)); + this.buf = grown; + this.view = new DataView(grown.buffer); + } + + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + big(v, bits) { + const b = $bigint(v); + if (bits(64, b) !== b) throw new RangeError(`${b} does not fit in a 64-bit integer`); + return b; + } + + writeBool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + this.reserve(1); + this.buf[this.len++] = v ? 1 : 0; + } + + writeI8(v) { this.reserve(1); this.view.setInt8(this.len, this.num(v)); this.len += 1; } + writeU8(v) { this.reserve(1); this.view.setUint8(this.len, this.num(v)); this.len += 1; } + writeI16(v) { this.reserve(2); this.view.setInt16(this.len, this.num(v), true); this.len += 2; } + writeU16(v) { this.reserve(2); this.view.setUint16(this.len, this.num(v), true); this.len += 2; } + writeI32(v) { this.reserve(4); this.view.setInt32(this.len, this.num(v), true); this.len += 4; } + writeU32(v) { this.reserve(4); this.view.setUint32(this.len, this.num(v), true); this.len += 4; } + writeI64(v) { this.reserve(8); this.view.setBigInt64(this.len, this.big(v, BigInt.asIntN), true); this.len += 8; } + writeU64(v) { this.reserve(8); this.view.setBigUint64(this.len, this.big(v, BigInt.asUintN), true); this.len += 8; } + writeF32(v) { this.reserve(4); this.view.setFloat32(this.len, this.num(v), true); this.len += 4; } + writeF64(v) { this.reserve(8); this.view.setFloat64(this.len, this.num(v), true); this.len += 8; } + + writeString(v) { + if (typeof v !== 'string') throw $expected('a string', v); + // UTF-8 never needs more than three bytes per UTF-16 code unit. + this.reserve(4 + v.length * 3); + const { written } = $utf8.encodeInto(v, this.buf.subarray(this.len + 4)); + this.view.setUint32(this.len, written, true); + this.len += 4 + written; + } + + writeBytes(v) { + if (!(v instanceof Uint8Array)) throw $expected('a Uint8Array', v); + this.reserve(4 + v.length); + this.view.setUint32(this.len, v.length, true); + this.buf.set(v, this.len + 4); + this.len += 4 + v.length; + } + + writeOpt(v, write) { + if (v === null || v === undefined) { + this.writeBool(false); + } else { + this.writeBool(true); + write(this, v); + } + } + + writeList(v, write) { + if (!Array.isArray(v)) throw $expected('an array', v); + this.writeU32(v.length); + for (const e of v) write(this, e); + } + + writeMap(v, writeKey, writeValue) { + if (v === null || typeof v !== 'object') throw $expected('an object or a Map', v); + const entries = v instanceof Map ? [...v.entries()] : Object.entries(v); + this.writeU32(entries.length); + for (const [k, e] of entries) { + writeKey(this, k); + writeValue(this, e); + } + } + + finish() { + return this.buf.subarray(0, this.len); + } +} + +/** + * Reads the value-buffer wire format. Every read is bounds-checked; a + * truncated buffer, an invalid bool or presence byte, invalid UTF-8, or + * trailing bytes throw a code -3 error. + */ +export class $Reader { + constructor(bytes) { + this.buf = bytes; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this.pos = 0; + } + + take(n) { + if (n > this.buf.length - this.pos) throw $malformed('truncated'); + const at = this.pos; + this.pos += n; + return at; + } + + readBool() { + const b = this.buf[this.take(1)]; + if (b > 1) throw $malformed('invalid bool byte ' + b); + return b === 1; + } + + readI8() { return this.view.getInt8(this.take(1)); } + readU8() { return this.view.getUint8(this.take(1)); } + readI16() { return this.view.getInt16(this.take(2), true); } + readU16() { return this.view.getUint16(this.take(2), true); } + readI32() { return this.view.getInt32(this.take(4), true); } + readU32() { return this.view.getUint32(this.take(4), true); } + readI64() { return this.view.getBigInt64(this.take(8), true); } + readU64() { return this.view.getBigUint64(this.take(8), true); } + readF32() { return this.view.getFloat32(this.take(4), true); } + readF64() { return this.view.getFloat64(this.take(8), true); } + + readString() { + const n = this.readU32(); + const at = this.take(n); + try { + return $utf8Strict.decode(this.buf.subarray(at, at + n)); + } catch { + throw $malformed('invalid UTF-8'); + } + } + + readBytes() { + const n = this.readU32(); + const at = this.take(n); + return this.buf.slice(at, at + n); + } + + readOpt(read) { + return this.readBool() ? read(this) : null; + } + + // A count may exceed the remaining bytes when elements encode to zero + // bytes (a record without fields), so it is not checked up front; a + // truncated element still fails on its first read. + readList(read) { + const n = this.readU32(); + const out = []; + for (let i = 0; i < n; i++) out.push(read(this)); + return out; + } + + readMap(readKey, readValue) { + const n = this.readU32(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = readKey(this); + out[k] = readValue(this); + } + return out; + } + + end() { + if (this.pos !== this.buf.length) throw $malformed('trailing bytes'); + } +} + +/** Encode `v` with `write` into a fresh value buffer. */ +export function $encode(v, write) { + const w = new $Writer(); + write(w, v); + return w.finish(); +} + +/** Decode a whole value buffer with `read`, rejecting trailing bytes. */ +export function $decode(bytes, read) { + const r = new $Reader(bytes); + const v = read(r); + r.end(); + return v; +} + +/** Element writers for the fixed-width primitives, strings, and bytes. */ +export const $W = Object.freeze({ + Bool: (w, v) => w.writeBool(v), + I8: (w, v) => w.writeI8(v), + I16: (w, v) => w.writeI16(v), + I32: (w, v) => w.writeI32(v), + I64: (w, v) => w.writeI64(v), + U8: (w, v) => w.writeU8(v), + U16: (w, v) => w.writeU16(v), + U32: (w, v) => w.writeU32(v), + U64: (w, v) => w.writeU64(v), + F32: (w, v) => w.writeF32(v), + F64: (w, v) => w.writeF64(v), + String: (w, v) => w.writeString(v), + Bytes: (w, v) => w.writeBytes(v), +}); + +/** Element readers for the fixed-width primitives, strings, and bytes. */ +export const $R = Object.freeze({ + Bool: (r) => r.readBool(), + I8: (r) => r.readI8(), + I16: (r) => r.readI16(), + I32: (r) => r.readI32(), + I64: (r) => r.readI64(), + U8: (r) => r.readU8(), + U16: (r) => r.readU16(), + U32: (r) => r.readU32(), + U64: (r) => r.readU64(), + F32: (r) => r.readF32(), + F64: (r) => r.readF64(), + String: (r) => r.readString(), + Bytes: (r) => r.readBytes(), +}); + +/** The key `using` declarations call, or a stand-in where it is missing. */ +export const $dispose = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose'); + +// Releases the native reference of a wrapper collected without close(). +// The held value never references the wrapper, so it can't keep it alive. +const $objects = new FinalizationRegistry((held) => held.destroy(held.handle)); + +function $release(o) { + const handle = o.$h; + o.$h = null; + o.constructor.$destroy(handle); +} + +/** + * The base of every interface class. An instance owns one strong reference + * to a native object. `close()` (or a `using` declaration) releases it; + * a wrapper that is garbage collected unclosed is released by a + * `FinalizationRegistry`. A `close()` that races an in-flight call (from a + * callback, or while an async method is pending) is deferred until that + * call returns, so the native object is never freed mid-call. + */ +export class $Object { + close() { + if (this.$closed !== false) return; + this.$closed = true; + $objects.unregister(this); + if (this.$calls === 0) $release(this); + } + + [$dispose]() { + this.close(); + } +} + +/** Bind a native handle (one strong reference) to wrapper `o`. */ +export function $own(o, handle) { + o.$h = handle; + o.$calls = 0; + o.$closed = false; + $objects.register(o, { handle, destroy: o.constructor.$destroy }, o); + return o; +} + +/** Wrap a handle the native side handed over in a new `cls` instance. */ +export function $adopt(cls, handle) { + return $own(Object.create(cls.prototype), handle); +} + +/** `$adopt`, with `null` for an absent object. */ +export function $adoptOpt(cls, handle) { + return handle === null ? null : $adopt(cls, handle); +} + +/** + * Lend wrapper `o` (an instance of `cls`) to one native call and return its + * handle. Every `$lend` is paired with an `$unlend` when the call returns. + */ +export function $lend(o, cls) { + if (!(o instanceof cls)) throw $expected(`a ${cls.name}`, o); + if (o.$closed !== false) throw new {{ERROR_CLASS}}(-3, `${cls.name} used after close()`); + o.$calls++; + return o.$h; +} + +/** `$lend`, with `null` for an absent object. */ +export function $lendOpt(o, cls) { + return o === null || o === undefined ? null : $lend(o, cls); +} + +/** End one loan of `o`, completing a `close()` deferred by the loan. */ +export function $unlend(o) { + if (o === null || o === undefined) return; + if (--o.$calls === 0 && o.$closed) $release(o); +} + +/** + * A second strong reference to `o`'s native object, for an object token + * written into a value buffer (the native side adopts it). + */ +export function $clone(o, cls) { + const handle = $lend(o, cls); + try { + return cls.$clone(handle); + } finally { + $unlend(o); + } +} + +const $iterators = new FinalizationRegistry((held) => held.destroy(held.handle)); + +/** + * A lazy iterator over a native `iter`: each `next()` makes exactly one + * native call. The native iterator is destroyed exactly once: when it is + * exhausted or fails, by `return()` (which `for...of` calls on `break`), by + * `close()` or a `using` declaration, or when the iterator is collected. + * `spec` supplies `next`, `destroy`, `map` (the error mapping), and + * `convert` (or `null` when elements need no conversion). + */ +export class $Iterator { + constructor(handle, spec) { + this.$h = handle; + this.$spec = spec; + $iterators.register(this, { handle, destroy: spec.destroy }, this); + } + + next() { + if (this.$h === null) return { done: true, value: undefined }; + const spec = this.$spec; + let v; + try { + v = spec.next(this.$h); + } catch (e) { + this.close(); + throw spec.map(e); + } + if (v === undefined) { + this.close(); + return { done: true, value: undefined }; + } + return { done: false, value: spec.convert === null ? v : spec.convert(v) }; + } + + return(value) { + this.close(); + return { done: true, value }; + } + + close() { + const handle = this.$h; + if (handle === null) return; + this.$h = null; + $iterators.unregister(this); + this.$spec.destroy(handle); + } + + [Symbol.iterator]() { + return this; + } + + [$dispose]() { + this.close(); + } +} + +/** + * Run a cancellable launch. Without a signal, `launch(null)` runs as is. + * With one, a native cancel token is created and passed to `launch`, + * aborting the signal cancels it (an already-aborted signal cancels it + * before the launch, so the call completes with `CancelledError` at once), + * and the token is destroyed once the call settles. `tokens` supplies the + * native `create`, `cancel`, and `destroy`. + */ +export function $cancellable(signal, tokens, launch) { + if (signal === undefined || signal === null) return launch(null); + if (typeof signal.addEventListener !== 'function') throw $expected('an AbortSignal', signal); + const token = tokens.create(); + const abort = () => tokens.cancel(token); + const done = () => { + signal.removeEventListener('abort', abort); + tokens.destroy(token); + }; + if (signal.aborted) { + abort(); + } else { + signal.addEventListener('abort', abort, { once: true }); + } + let pending; + try { + pending = launch(token); + } catch (e) { + done(); + throw e; + } + return pending.finally(done); +} + +/** Check a callback interface implementation. */ +export function $impl(impl, what) { + if (impl === null || (typeof impl !== 'object' && typeof impl !== 'function')) { + throw $expected(`a ${what} implementation`, impl); + } + return impl; +} + +function $num(v, where) { + if (typeof v !== 'number') throw new TypeError(`${where} must return a number`); + return v; +} + +function $retBigint(v, where) { + if (typeof v === 'bigint') return v; + if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v); + throw new TypeError(`${where} must return a bigint`); +} + +/** + * Checks for a callback method's return value, by wire primitive. A value + * of the wrong type throws, which reports the call as a code -4 failure. + */ +export const $ret = Object.freeze({ + Bool(v, where) { + if (typeof v !== 'boolean') throw new TypeError(`${where} must return a boolean`); + return v; + }, + I8: $num, + I16: $num, + I32: $num, + U8: $num, + U16: $num, + U32: $num, + F32: $num, + F64: $num, + I64: $retBigint, + U64: $retBigint, +}); + +/** + * The load-time contract check: the native library must implement ABI + * revision `abi`, and every top-level module's contract checksum must + * match the one these bindings were generated from. + */ +export function $verify(raw, library, prefix, abi, checksums) { + const version = raw[`${prefix}_abi_version`]; + if (typeof version !== 'function') { + throw new Error(`${library}: the native library does not export ${prefix}_abi_version`); + } + const found = Number(version()); + if (found !== abi) { + throw new Error( + `${library}: the native library implements ABI revision ${found}, but these bindings require revision ${abi}`, + ); + } + for (const [module, expected] of checksums) { + const checksum = raw[`${prefix}_${module}_checksum`]; + const got = typeof checksum === 'function' ? BigInt.asUintN(64, BigInt(checksum())) : null; + if (got !== expected) { + const what = got === null ? 'no contract checksum' : `contract checksum 0x${got.toString(16)}`; + throw new Error( + `${library}: module '${module}' of the native library does not match these bindings ` + + `(${what}, expected 0x${expected.toString(16)}); regenerate the bindings for this build`, + ); + } + } +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/bridge.rs b/crates/weaveffi-gen/src/targets/kotlin/bridge.rs new file mode 100644 index 00000000..1b22fcf5 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/bridge.rs @@ -0,0 +1,206 @@ +//! `JniBridge.kt`: the internal object declaring one `external fun` per C +//! symbol (named after the symbol, so two modules' functions never +//! collide), the error factory the shim throws through, and the static +//! dispatch shims callback trampolines call. + +use crate::codegen::CodeWriter; +use weaveffi_model::model::{ + BindingModel, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, FnBinding, +}; + +use crate::targets::kotlin::calls::{item_slot, lift, lower}; +use crate::targets::kotlin::names::{kt_member, kt_param, Names}; + +/// The `JniBridge` member name of a callback method's dispatch shim. +pub(crate) fn shim_name( + n: &Names, + cb: &CallbackInterfaceBinding, + m: &CallbackMethodBinding, +) -> String { + n.native(&format!("{}_{}", cb.c_tag, m.name)) +} + +/// The JVM descriptor of a callback method's dispatch shim. +pub(crate) fn shim_descriptor( + n: &Names, + cb: &CallbackInterfaceBinding, + m: &CallbackMethodBinding, +) -> String { + let mut sig = format!("(L{}/{};", n.package_path, n.ty(&cb.name)); + for p in &m.params { + sig.push_str(&n.jni_descriptor(Some(&p.ty))); + } + sig.push(')'); + sig.push_str(&n.jni_descriptor(m.ret.as_ref())); + sig +} + +/// The `external fun` declarations backing callable `f`. +fn callable_natives(w: &mut CodeWriter, n: &Names, f: &FnBinding) { + let mut params: Vec = Vec::new(); + if f.has_self { + params.push("_self: Long".to_string()); + } + params.extend( + f.params + .iter() + .map(|p| format!("{}: {}", kt_param(&p.name), n.jni_type(&p.ty))), + ); + match &f.shape { + CallShape::Sync(abi) => { + let ret = f + .ret + .as_ref() + .map(|t| format!(": {}", n.jni_type(t))) + .unwrap_or_default(); + w.line(format!( + "@JvmStatic external fun {}({}){ret}", + n.native(&abi.symbol), + params.join(", ") + )); + } + CallShape::Async(ab) => { + if f.cancellable { + params.push("_token: Long".to_string()); + } + params.push("_done: NativeCompletion<*>".to_string()); + w.line(format!( + "@JvmStatic external fun {}({})", + n.native(&ab.launch.symbol), + params.join(", ") + )); + } + CallShape::Iterator(it) => { + w.line(format!( + "@JvmStatic external fun {}({}): Long", + n.native(&it.launch.symbol), + params.join(", ") + )); + match item_slot(&it.elem) { + None => w.line(format!( + "@JvmStatic external fun {}(_iter: Long): ByteArray?", + n.native(&it.next.symbol) + )), + Some(slot) => w.line(format!( + "@JvmStatic external fun {}(_iter: Long, _out: {}): Boolean", + n.native(&it.next.symbol), + slot.kotlin_array + )), + }; + w.line(format!( + "@JvmStatic external fun {}(_iter: Long)", + n.native(&it.destroy_symbol) + )); + } + } +} + +/// The static shim a trampoline calls for one callback method: lifts each +/// argument from its JNI form, calls the implementation, and lowers the +/// result. +fn callback_shim( + w: &mut CodeWriter, + n: &Names, + cb: &CallbackInterfaceBinding, + m: &CallbackMethodBinding, +) { + let mut params = vec![format!("_impl: {}", n.ty(&cb.name))]; + params.extend( + m.params + .iter() + .map(|p| format!("{}: {}", kt_param(&p.name), n.jni_type(&p.ty))), + ); + let args: Vec = m + .params + .iter() + .map(|p| lift(n, &p.ty, &kt_param(&p.name))) + .collect(); + let call = format!("_impl.{}({})", kt_member(&m.name), args.join(", ")); + let name = shim_name(n, cb, m); + w.line("@JvmStatic"); + match &m.ret { + None => w.line(format!("fun {name}({}) {{ {call} }}", params.join(", "))), + Some(t) => w.line(format!( + "fun {name}({}): {} = {}", + params.join(", "), + n.jni_type(t), + lower(n, t, &call) + )), + }; +} + +/// Render the body of `JniBridge.kt` (after the package line). +pub(crate) fn render_bridge(n: &Names, model: &BindingModel) -> String { + let mut w = CodeWriter::four_space(); + w.blank(); + w.line("/**"); + w.line(format!( + " * The JNI entry points of `lib{}_jni`: one native per C symbol, named after", + n.library + )); + w.line(" * it, plus the hooks the shim calls back into. Internal plumbing for the"); + w.line(" * public wrappers."); + w.line(" */"); + w.line("internal object JniBridge {"); + w.scope(|w| { + w.line("init {"); + w.line(" NativeLibrary.ensureLoaded()"); + w.line("}"); + w.blank(); + w.line("/** The exception for a native failure; domain 0 is the generic one. */"); + w.line("@JvmStatic"); + w.line("@Suppress(\"UNUSED_PARAMETER\")"); + w.line("fun error(domain: Int, code: Int, message: ByteArray, payload: ByteArray?): FfiException {"); + w.scope(|w| { + w.line("val text = decodeUtf8(message)"); + w.line("return when (domain) {"); + w.scope(|w| { + for (i, eb) in n.domains(model) { + w.line(format!( + "{i} -> {}.fromCode(code, text, payload)", + n.exception(eb) + )); + } + w.line("else -> FfiException(code, text)"); + }); + w.line("}"); + }); + w.line("}"); + if model.has_callback_interfaces() { + w.blank(); + w.line("/** The message a failed callback reports to the producer (code -4). */"); + w.line("@JvmStatic"); + w.line("fun describe(error: Throwable): ByteArray = encodeUtf8(error.toString())"); + } + w.blank(); + w.line("@JvmStatic external fun debug_live(kind: Int): Long"); + if model.has_async() { + w.line("@JvmStatic external fun cancel_token_create(): Long"); + w.line("@JvmStatic external fun cancel_token_cancel(token: Long)"); + w.line("@JvmStatic external fun cancel_token_destroy(token: Long)"); + } + for m in &model.modules { + for i in &m.interfaces { + w.line(format!( + "@JvmStatic external fun {}(_self: Long): Long", + n.native(&i.clone_symbol) + )); + w.line(format!( + "@JvmStatic external fun {}(_self: Long)", + n.native(&i.destroy_symbol) + )); + } + for f in m.callables() { + callable_natives(w, n, f); + } + } + for (_, cb) in model.callback_interfaces() { + for m in &cb.methods { + w.blank(); + callback_shim(w, n, cb, m); + } + } + }); + w.line("}"); + w.finish() +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/calls.rs b/crates/weaveffi-gen/src/targets/kotlin/calls.rs new file mode 100644 index 00000000..450ebdf0 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/calls.rs @@ -0,0 +1,293 @@ +//! Public Kotlin wrappers for callables: how each argument lowers to its +//! JNI form, how each result lifts back, and the borrow scopes that keep +//! every object (receiver and arguments) alive and unreleased for the whole +//! native call. + +use crate::codegen::CodeWriter; +use weaveffi_model::model::{CallShape, ErrorBinding, FnBinding, ParamBinding, Ty}; + +use crate::targets::kotlin::codec::{decode_expr, encode_expr}; +use crate::targets::kotlin::names::{kt_param, Names}; + +/// One line of a generated expression, with its depth relative to the +/// expression's first line. +type Line = (usize, String); + +/// The Kotlin expression lowering a non-object argument `expr` of `t` to its +/// JNI form. +pub(crate) fn lower(n: &Names, t: &Ty, expr: &str) -> String { + match t { + Ty::Enum(_) => format!("{expr}.value"), + Ty::StringUtf8 => format!("encodeUtf8({expr})"), + _ if t.is_buffered() => encode_expr(n, t, expr), + _ => expr.to_string(), + } +} + +/// The Kotlin expression lifting the JNI form `expr` of `t` back into its +/// public value. Objects are adopted: the wrapper owes the reference's +/// release. +pub(crate) fn lift(n: &Names, t: &Ty, expr: &str) -> String { + match t { + Ty::Enum(name) => format!("{}.fromValue({expr})", n.ty(name)), + Ty::StringUtf8 => format!("decodeUtf8({expr})"), + Ty::Interface(name) => format!("{}.fromHandle({expr})", n.ty(name)), + Ty::Optional(inner) if !t.is_buffered() => { + let name = inner + .interface_name() + .expect("only Interface? is unbuffered"); + format!("{}.fromHandleOrNull({expr})", n.ty(name)) + } + _ if t.is_buffered() => decode_expr(n, t, expr), + _ => expr.to_string(), + } +} + +/// The borrow scopes and JNI arguments for a call to `f`: the receiver +/// (when `has_self`) and every object argument open one `borrow` scope each, +/// and every other argument lowers inline. +fn lower_args(n: &Names, f: &FnBinding, has_self: bool) -> (Vec, Vec) { + let mut opens = Vec::new(); + let mut args = Vec::new(); + if has_self { + opens.push("handle.borrow { _self ->".to_string()); + args.push("_self".to_string()); + } + for (i, p) in f.params.iter().enumerate() { + let name = kt_param(&p.name); + match &p.ty { + Ty::Interface(_) => { + opens.push(format!("{name}.handle.borrow {{ _h{i} ->")); + args.push(format!("_h{i}")); + } + Ty::Optional(_) if !p.ty.is_buffered() => { + opens.push(format!("{name}?.handle.borrowOrNull {{ _h{i} ->")); + args.push(format!("_h{i}")); + } + t => args.push(lower(n, t, &name)), + } + } + (opens, args) +} + +/// Wrap `body` (lines relative to depth 0) in the borrow scopes `opens`. +fn nest(opens: &[String], body: Vec) -> Vec { + let mut out: Vec = opens + .iter() + .enumerate() + .map(|(d, o)| (d, o.clone())) + .collect(); + out.extend(body.into_iter().map(|(d, l)| (d + opens.len(), l))); + for d in (0..opens.len()).rev() { + out.push((d, "}".to_string())); + } + out +} + +/// Emit a function declaration `sig` (everything before `=` or `{`) whose +/// body is the expression `lines`: an expression body when the function +/// returns a value, a block body otherwise. +fn emit_body(w: &mut CodeWriter, sig: &str, returns: bool, lines: Vec) { + let base = w.depth(); + if returns { + let mut iter = lines.into_iter(); + let (_, first) = iter.next().expect("a body has at least one line"); + w.line(format!("{sig} = {first}")); + for (d, l) in iter { + w.raw(format!("{}{l}\n", " ".repeat(base + d))); + } + } else { + w.line(format!("{sig} {{")); + for (d, l) in lines { + w.raw(format!("{}{l}\n", " ".repeat(base + 1 + d))); + } + w.line("}"); + } +} + +/// The Kotlin parameter list of a public wrapper. +pub(crate) fn params_sig(n: &Names, params: &[ParamBinding]) -> String { + params + .iter() + .map(|p| format!("{}: {}", kt_param(&p.name), n.kt_type(&p.ty))) + .collect::>() + .join(", ") +} + +/// The `: Type` return annotation of a public wrapper, or nothing for void. +fn ret_sig(n: &Names, ret: Option<&Ty>) -> String { + ret.map(|t| format!(": {}", n.kt_type(t))) + .unwrap_or_default() +} + +/// The `@Deprecated` annotation for a deprecation message. +pub(crate) fn deprecated_line(msg: &str) -> String { + format!( + "@Deprecated(\"{}\")", + msg.replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('$', "\\$") + ) +} + +/// The step function of a `NativeIterator`: pulls one element through the +/// iterator's `_next` native, returning `NativeIterator.DONE` at the end. +fn iterator_step(n: &Names, f: &FnBinding) -> Vec { + let CallShape::Iterator(it) = &f.shape else { + unreachable!("iterator_step needs an iterator call shape"); + }; + let next = n.native(&it.next.symbol); + match item_slot(&it.elem) { + None => vec![ + (0, format!("val _item = JniBridge.{next}(_it)")), + ( + 0, + format!( + "if (_item == null) NativeIterator.DONE else {}", + lift(n, &it.elem, "_item") + ), + ), + ], + Some(slot) => vec![ + (0, format!("val _slot = {}(1)", slot.kotlin_array)), + ( + 0, + format!( + "if (JniBridge.{next}(_it, _slot)) {} else NativeIterator.DONE", + lift(n, &it.elem, "_slot[0]") + ), + ), + ], + } +} + +/// How an iterator's `_next` native hands back a direct or object element: +/// through a one-element primitive array (strings, bytes, and buffers come +/// back as a nullable `ByteArray` instead). +pub(crate) struct ItemSlot { + /// The Kotlin array type (`IntArray`). + pub kotlin_array: &'static str, + /// The JNI array type (`jintArray`). + pub jni_array: &'static str, + /// The JNI element type (`jint`). + pub jni_elem: &'static str, + /// The JNI array-region setter stem (`Int` in `SetIntArrayRegion`). + pub region: &'static str, +} + +/// The slot an iterator element of `t` comes back through, or `None` for +/// the `ByteArray` families. +pub(crate) fn item_slot(t: &Ty) -> Option { + if matches!(t, Ty::StringUtf8 | Ty::Bytes) || t.is_buffered() { + return None; + } + let (kotlin_array, jni_array, jni_elem, region) = match t { + Ty::Bool => ("BooleanArray", "jbooleanArray", "jboolean", "Boolean"), + Ty::I8 | Ty::U8 => ("ByteArray", "jbyteArray", "jbyte", "Byte"), + Ty::I16 | Ty::U16 => ("ShortArray", "jshortArray", "jshort", "Short"), + Ty::I32 | Ty::Enum(_) => ("IntArray", "jintArray", "jint", "Int"), + Ty::F32 => ("FloatArray", "jfloatArray", "jfloat", "Float"), + Ty::F64 => ("DoubleArray", "jdoubleArray", "jdouble", "Double"), + _ => ("LongArray", "jlongArray", "jlong", "Long"), + }; + Some(ItemSlot { + kotlin_array, + jni_array, + jni_elem, + region, + }) +} + +/// Emit the public wrapper for callable `f`: `decl` is everything before the +/// parameter list (`fun count`, `operator fun invoke`, `suspend fun fetch` +/// is added for async callables), `has_self` borrows the receiver. +pub(crate) fn emit_callable( + w: &mut CodeWriter, + n: &Names, + f: &FnBinding, + decl: &str, + has_self: bool, + error: Option<&ErrorBinding>, +) { + if let Some(msg) = &f.deprecated { + w.line(deprecated_line(msg)); + } + let params = params_sig(n, &f.params); + let ret = ret_sig(n, f.ret.as_ref()); + let (opens, mut args) = lower_args(n, f, has_self); + match &f.shape { + CallShape::Sync(abi) => { + let call = format!("JniBridge.{}({})", n.native(&abi.symbol), args.join(", ")); + let body = match &f.ret { + Some(t) => lift(n, t, &call), + None => call, + }; + let sig = format!("{decl}({params}){ret}"); + if opens.is_empty() && f.ret.is_some() { + w.line(format!("{sig} = {body}")); + } else { + emit_body(w, &sig, f.ret.is_some(), nest(&opens, vec![(0, body)])); + } + } + CallShape::Iterator(it) => { + let launch = format!( + "JniBridge.{}({})", + n.native(&it.launch.symbol), + args.join(", ") + ); + let mut body = vec![( + 0, + format!( + "NativeIterator({launch}, JniBridge::{}) {{ _it ->", + n.native(&it.destroy_symbol) + ), + )]; + body.extend(iterator_step(n, f).into_iter().map(|(d, l)| (d + 1, l))); + body.push((0, "}".to_string())); + emit_body( + w, + &format!("{decl}({params}){ret}"), + true, + nest(&opens, body), + ); + } + CallShape::Async(ab) => { + if f.cancellable { + args.push("_token".to_string()); + } + args.push("_done".to_string()); + let call = format!( + "JniBridge.{}({})", + n.native(&ab.launch.symbol), + args.join(", ") + ); + let convert = match &f.ret { + None => "{ }".to_string(), + // `lift` only ever places its operand alone or in argument + // position, so the bare cast needs no parentheses. + Some(t) => format!("{{ {} }}", lift(n, t, &format!("it as {}", n.jni_type(t)))), + }; + let token = if f.cancellable { "_token" } else { "_" }; + let mut body = vec![( + 0, + format!( + "awaitNative({}, {}, {convert}) {{ {token}, _done ->", + f.cancellable, + n.domain(f, error) + ), + )]; + body.extend( + nest(&opens, vec![(0, call)]) + .into_iter() + .map(|(d, l)| (d + 1, l)), + ); + body.push((0, "}".to_string())); + emit_body( + w, + &format!("suspend {decl}({params}){ret}"), + f.ret.is_some(), + body, + ); + } + } +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/codec.rs b/crates/weaveffi-gen/src/targets/kotlin/codec.rs new file mode 100644 index 00000000..4ceeca63 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/codec.rs @@ -0,0 +1,88 @@ +//! Value-buffer codec expressions: the Kotlin statement writing and the +//! expression reading every wire shape, dispatched on [`Ty::wire`]. The +//! runtime's `BufferWriter`/`BufferReader` name one method per [`Prim`] +//! (`writeI32`, `readString`), so primitives need a single arm. +//! +//! [`Prim`]: weaveffi_model::model::Prim + +use weaveffi_model::model::{Ty, WireType}; +use weaveffi_model::plan::destroy_symbol; + +use crate::targets::kotlin::names::Names; + +/// The Kotlin statement writing `expr` (the public Kotlin value of `t`) into +/// the writer `w`. Nested lambdas name their parameter `_v{depth}` (and +/// `_k{depth}` for map keys), which no user identifier can spell. +/// +/// An object is written as a token carrying a new strong reference +/// (`cloneHandle()`), so the encoding never hands over the reference the +/// wrapper still owns; the writer releases the token again if the encoding +/// fails before the buffer reaches the producer. +pub(crate) fn write_expr(n: &Names, t: &Ty, w: &str, expr: &str, depth: usize) -> String { + match t.wire() { + WireType::Prim(p) => format!("{w}.write{}({expr})", p.pascal()), + WireType::Enum(_) => format!("{w}.writeI32({expr}.value)"), + WireType::Object(name) => format!( + "{w}.writeObject({expr}.cloneHandle(), JniBridge::{})", + n.native(&destroy_symbol(name, &n.prefix)) + ), + WireType::User(name) => format!("pack{}({w}, {expr})", n.ty(name)), + WireType::Optional(inner) => { + let v = format!("_v{depth}"); + format!( + "{w}.writeOptional({expr}) {{ {v} -> {} }}", + write_expr(n, inner, w, &v, depth + 1) + ) + } + WireType::List(inner) => { + let v = format!("_v{depth}"); + format!( + "{w}.writeList({expr}) {{ {v} -> {} }}", + write_expr(n, inner, w, &v, depth + 1) + ) + } + WireType::Map(k, v) => { + let kv = format!("_k{depth}"); + let vv = format!("_v{depth}"); + format!( + "{w}.writeMap({expr}, {{ {kv} -> {} }}, {{ {vv} -> {} }})", + write_expr(n, k, w, &kv, depth + 1), + write_expr(n, v, w, &vv, depth + 1) + ) + } + } +} + +/// The Kotlin expression reading a value of `t` from the reader `r`; the +/// inverse of [`write_expr`]. An object token is adopted into a new wrapper, +/// which owes the reference's release. +pub(crate) fn read_expr(n: &Names, t: &Ty, r: &str) -> String { + match t.wire() { + WireType::Prim(p) => format!("{r}.read{}()", p.pascal()), + WireType::Enum(name) => format!("{}.fromValue({r}.readI32())", n.ty(name)), + WireType::Object(name) => format!("{}.fromHandle({r}.readObject())", n.ty(name)), + WireType::User(name) => format!("unpack{}({r})", n.ty(name)), + WireType::Optional(inner) => format!("{r}.readOptional {{ {} }}", read_expr(n, inner, r)), + WireType::List(inner) => format!("{r}.readList {{ {} }}", read_expr(n, inner, r)), + WireType::Map(k, v) => format!( + "{r}.readMap({{ {} }}, {{ {} }})", + read_expr(n, k, r), + read_expr(n, v, r) + ), + } +} + +/// The Kotlin expression encoding the public value `expr` of `t` into a +/// fresh value buffer. +pub(crate) fn encode_expr(n: &Names, t: &Ty, expr: &str) -> String { + format!( + "encodeBuffer {{ _w -> {} }}", + write_expr(n, t, "_w", expr, 0) + ) +} + +/// The Kotlin expression decoding the value buffer `expr` into the public +/// value of `t`. +pub(crate) fn decode_expr(n: &Names, t: &Ty, expr: &str) -> String { + format!("decodeBuffer({expr}) {{ _r -> {} }}", read_expr(n, t, "_r")) +} diff --git a/crates/weaveffi-gen-kotlin/src/docs.rs b/crates/weaveffi-gen/src/targets/kotlin/docs.rs similarity index 74% rename from crates/weaveffi-gen-kotlin/src/docs.rs rename to crates/weaveffi-gen/src/targets/kotlin/docs.rs index 8dc16f20..19ed7159 100644 --- a/crates/weaveffi-gen-kotlin/src/docs.rs +++ b/crates/weaveffi-gen/src/targets/kotlin/docs.rs @@ -1,9 +1,10 @@ -//! KDoc comment emission and the writer-splicing helpers the renderers use -//! to interleave pre-indented blocks. +//! KDoc comment emission. -use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::ParamBinding; +use crate::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; +use crate::codegen::CodeWriter; +use weaveffi_model::model::ParamBinding; + +use crate::targets::kotlin::names::kt_param; /// Emits a Kotlin KDoc comment at `indent`. Single-line docs collapse to /// `/** text */`; multi-line docs expand to a block with ` * ` prefixed lines. @@ -12,8 +13,8 @@ pub(crate) fn emit_doc(out: &mut String, doc: &Option, indent: &str) { } /// Emits a KDoc block for a function: function doc plus `@param name desc` -/// lines for each documented parameter. Skips entirely when there is nothing -/// to document. +/// lines for each documented parameter; without parameter docs this is +/// [`emit_doc`]. pub(crate) fn emit_fn_doc( out: &mut String, doc: &Option, @@ -21,10 +22,11 @@ pub(crate) fn emit_fn_doc( indent: &str, ) { let has_param_docs = params.iter().any(|p| p.doc.is_some()); - let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty()); - if trimmed_doc.is_none() && !has_param_docs { + if !has_param_docs { + emit_doc(out, doc, indent); return; } + let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty()); out.push_str(indent); out.push_str("/**\n"); if let Some(d) = trimmed_doc { @@ -83,12 +85,14 @@ pub(crate) fn writer_fn_doc(w: &mut CodeWriter, doc: &Option, params: &[ w.raw(tmp); } -/// Run a sub-renderer that writes already-indented text into a scratch buffer, -/// then splice it verbatim into `w`. The interleaved emitters (`write_*`) carry -/// their own absolute indentation, so a [`CodeWriter`]-based caller folds them -/// in with [`CodeWriter::raw`] without disturbing its own depth. -pub(crate) fn splice(w: &mut CodeWriter, render: impl FnOnce(&mut String)) { - let mut tmp = String::new(); - render(&mut tmp); - w.raw(tmp); +/// Clone `params` with their Kotlin spellings so KDoc `@param` tags match +/// the emitted parameter names. +pub(crate) fn camel_params(params: &[ParamBinding]) -> Vec { + params + .iter() + .map(|p| ParamBinding { + name: kt_param(&p.name), + ..p.clone() + }) + .collect() } diff --git a/crates/weaveffi-gen/src/targets/kotlin/entities.rs b/crates/weaveffi-gen/src/targets/kotlin/entities.rs new file mode 100644 index 00000000..d0456cf4 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/entities.rs @@ -0,0 +1,434 @@ +//! Kotlin declarations for one top-level module: enums, records, rich +//! enums, callback interfaces, interface wrapper classes, error domains, +//! and the module object holding the free functions (nested modules become +//! nested objects). + +use crate::codegen::common::pascal_case; +use crate::codegen::CodeWriter; +use weaveffi_model::errors; +use weaveffi_model::model::{ + BindingModel, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FieldBinding, + InterfaceBinding, ModuleBinding, StructBinding, +}; + +use crate::targets::kotlin::calls::{deprecated_line, emit_callable}; +use crate::targets::kotlin::codec::{read_expr, write_expr}; +use crate::targets::kotlin::docs::{camel_params, writer_doc, writer_fn_doc}; +use crate::targets::kotlin::names::{kt_escape, kt_member, kt_param, Names}; + +/// Emit `@Deprecated` for a deprecated declaration. +fn deprecated(w: &mut CodeWriter, msg: &Option) { + if let Some(msg) = msg { + w.line(deprecated_line(msg)); + } +} + +/// The `val name: Type` constructor properties of a record or variant. +fn field_decls(n: &Names, fields: &[FieldBinding]) -> Vec { + fields + .iter() + .map(|f| format!("val {}: {}", kt_escape(&f.name), n.kt_type(&f.ty))) + .collect() +} + +/// Emit a class header with constructor properties: on one line when no +/// field is documented, one property per line (each with its KDoc) +/// otherwise. `head` is everything before `(`, `tail` everything after `)`. +fn emit_properties(w: &mut CodeWriter, n: &Names, head: &str, fields: &[FieldBinding], tail: &str) { + if fields.iter().any(|f| f.doc.is_some()) { + w.line(format!("{head}(")); + w.scope(|w| { + for (f, decl) in fields.iter().zip(field_decls(n, fields)) { + writer_doc(w, &f.doc); + w.line(format!("{decl},")); + } + }); + w.line(format!("){tail}")); + } else { + w.line(format!( + "{head}({}){tail}", + field_decls(n, fields).join(", ") + )); + } +} + +/// A C-style enum: an `enum class` carrying its ABI value. +fn render_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { + let name = n.ty(&e.name); + w.blank(); + writer_doc(w, &e.doc); + deprecated(w, &e.deprecated); + w.line(format!("enum class {name}(val value: Int) {{")); + w.scope(|w| { + for (i, v) in e.variants.iter().enumerate() { + writer_doc(w, &v.doc); + let end = if i + 1 == e.variants.len() { ";" } else { "," }; + w.line(format!("{}({}){end}", kt_escape(&v.name), v.value)); + } + w.blank(); + w.line("companion object {"); + w.scope(|w| { + w.line("/** The variant whose ABI value is [value]. */"); + w.line(format!( + "fun fromValue(value: Int): {name} = entries.firstOrNull {{ it.value == value }}" + )); + w.line(format!( + " ?: throw FfiException(-3, \"unknown {name} value $value\")" + )); + }); + w.line("}"); + }); + w.line("}"); +} + +/// A rich enum: a sealed class with one subclass per variant, plus its +/// buffer codec (an `i32` tag, then the variant's fields). +fn render_rich_enum(w: &mut CodeWriter, n: &Names, e: &EnumBinding) { + let name = n.ty(&e.name); + w.blank(); + writer_doc(w, &e.doc); + deprecated(w, &e.deprecated); + w.line(format!("sealed class {name} {{")); + w.scope(|w| { + for v in &e.variants { + writer_doc(w, &v.doc); + let vn = kt_escape(&pascal_case(&v.name)); + if v.fields.is_empty() { + w.line(format!("object {vn} : {name}()")); + } else { + emit_properties( + w, + n, + &format!("data class {vn}"), + &v.fields, + &format!(" : {name}()"), + ); + } + } + }); + w.line("}"); + w.blank(); + w.line(format!( + "internal fun pack{name}(_w: BufferWriter, _v: {name}) {{" + )); + w.scope(|w| { + w.line("when (_v) {"); + w.scope(|w| { + for v in &e.variants { + let vn = kt_escape(&pascal_case(&v.name)); + if v.fields.is_empty() { + w.line(format!("is {name}.{vn} -> _w.writeI32({})", v.value)); + } else { + w.line(format!("is {name}.{vn} -> {{")); + w.scope(|w| { + w.line(format!("_w.writeI32({})", v.value)); + for f in &v.fields { + let expr = format!("_v.{}", kt_escape(&f.name)); + w.line(write_expr(n, &f.ty, "_w", &expr, 0)); + } + }); + w.line("}"); + } + } + }); + w.line("}"); + }); + w.line("}"); + w.blank(); + w.line(format!( + "internal fun unpack{name}(_r: BufferReader): {name} = when (val _tag = _r.readI32()) {{" + )); + w.scope(|w| { + for v in &e.variants { + let vn = kt_escape(&pascal_case(&v.name)); + if v.fields.is_empty() { + w.line(format!("{} -> {name}.{vn}", v.value)); + } else { + let reads: Vec = + v.fields.iter().map(|f| read_expr(n, &f.ty, "_r")).collect(); + w.line(format!("{} -> {name}.{vn}({})", v.value, reads.join(", "))); + } + } + w.line(format!( + "else -> throw FfiException(-3, \"malformed value buffer: unknown {name} tag $_tag\")" + )); + }); + w.line("}"); +} + +/// A record: a `data class` (a plain `class` when it has no fields), plus +/// its buffer codec (the fields in declaration order). +fn render_struct(w: &mut CodeWriter, n: &Names, s: &StructBinding) { + let name = n.ty(&s.name); + w.blank(); + writer_doc(w, &s.doc); + deprecated(w, &s.deprecated); + if s.fields.is_empty() { + w.line(format!("class {name}")); + w.blank(); + w.line(format!( + "internal fun pack{name}(_w: BufferWriter, _v: {name}) {{}}" + )); + w.blank(); + w.line(format!( + "internal fun unpack{name}(_r: BufferReader): {name} = {name}()" + )); + return; + } + emit_properties(w, n, &format!("data class {name}"), &s.fields, ""); + w.blank(); + w.line(format!( + "internal fun pack{name}(_w: BufferWriter, _v: {name}) {{" + )); + w.scope(|w| { + for f in &s.fields { + let expr = format!("_v.{}", kt_escape(&f.name)); + w.line(write_expr(n, &f.ty, "_w", &expr, 0)); + } + }); + w.line("}"); + w.blank(); + w.line(format!( + "internal fun unpack{name}(_r: BufferReader): {name} = {name}(" + )); + w.scope(|w| { + for f in &s.fields { + w.line(format!("{},", read_expr(n, &f.ty, "_r"))); + } + }); + w.line(")"); +} + +/// A callback interface: the Kotlin `interface` the consumer implements. +/// Its JNI dispatch shims live on `JniBridge`. +fn render_callback_interface(w: &mut CodeWriter, n: &Names, cb: &CallbackInterfaceBinding) { + w.blank(); + writer_doc(w, &cb.doc); + deprecated(w, &cb.deprecated); + w.line(format!("interface {} {{", n.ty(&cb.name))); + w.scope(|w| { + for (i, m) in cb.methods.iter().enumerate() { + if i > 0 { + w.blank(); + } + writer_fn_doc(w, &m.doc, &camel_params(&m.params)); + deprecated(w, &m.deprecated); + let params: Vec = m + .params + .iter() + .map(|p| format!("{}: {}", kt_param(&p.name), n.kt_type(&p.ty))) + .collect(); + let ret = m + .ret + .as_ref() + .map(|t| format!(": {}", n.kt_type(t))) + .unwrap_or_default(); + w.line(format!( + "fun {}({}){ret}", + kt_member(&m.name), + params.join(", ") + )); + } + }); + w.line("}"); +} + +/// An interface: an `AutoCloseable` wrapper owning one strong reference. +/// Every call borrows the reference (so neither `close()` nor the cleaner +/// can release it mid-call); constructors and statics live on the +/// companion, with the `new` constructor as `operator fun invoke`. +fn render_interface( + w: &mut CodeWriter, + n: &Names, + i: &InterfaceBinding, + error: Option<&ErrorBinding>, +) { + let name = n.ty(&i.name); + let destroy = n.native(&i.destroy_symbol); + let clone = n.native(&i.clone_symbol); + w.blank(); + writer_doc(w, &i.doc); + deprecated(w, &i.deprecated); + // The address constructor is private so a `new` constructor taking one + // `Long` (exposed as `invoke`) is never shadowed by it. + w.line(format!( + "class {name} private constructor(address: Long) : AutoCloseable {{" + )); + w.scope(|w| { + w.line(format!( + "internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::{destroy}))" + )); + w.blank(); + w.line("/** A new strong reference to this object, as written into a value buffer. */"); + w.line(format!( + "internal fun cloneHandle(): Long = handle.borrow {{ JniBridge.{clone}(it) }}" + )); + for f in &i.methods { + w.blank(); + writer_fn_doc(w, &f.doc, &camel_params(&f.params)); + emit_callable(w, n, f, &format!("fun {}", kt_member(&f.name)), true, error); + } + w.blank(); + w.line("/** Releases this wrapper's native reference; safe to call more than once. */"); + w.line("override fun close() = handle.close()"); + w.blank(); + w.line("companion object {"); + w.scope(|w| { + w.line("/** Adopts one strong reference (a return value or a buffer token). */"); + w.line(format!("internal fun fromHandle(address: Long): {name} = {name}(address)")); + w.blank(); + w.line("/** [fromHandle], with address 0 meaning no object. */"); + w.line(format!( + "internal fun fromHandleOrNull(address: Long): {name}? = if (address == 0L) null else {name}(address)" + )); + for c in &i.constructors { + w.blank(); + writer_fn_doc(w, &c.doc, &camel_params(&c.params)); + let decl = if c.name == "new" { + "operator fun invoke".to_string() + } else { + format!("fun {}", kt_member(&c.name)) + }; + emit_callable(w, n, c, &decl, false, error); + } + for f in &i.statics { + w.blank(); + writer_fn_doc(w, &f.doc, &camel_params(&f.params)); + emit_callable(w, n, f, &format!("fun {}", kt_member(&f.name)), false, error); + } + }); + w.line("}"); + }); + w.line("}"); +} + +/// An error domain: a sealed exception class with one subclass per code +/// (payload fields as constructor properties) and the `fromCode` factory +/// the bridge maps raw codes through. Unknown codes, including every +/// runtime (negative) code, map to the generic `FfiException`. +fn render_error_domain(w: &mut CodeWriter, n: &Names, eb: &ErrorBinding) { + let exc = n.exception(eb); + w.blank(); + w.line(format!("/** Errors of the `{}` domain. */", eb.name)); + w.line(format!( + "sealed class {exc}(code: Int, message: String) : FfiException(code, message) {{" + )); + w.scope(|w| { + for ec in &eb.codes { + writer_doc(w, &ec.doc); + let class = errors::pascal(&ec.name); + let default = ec.message.replace('\\', "\\\\").replace('"', "\\\"").replace('$', "\\$"); + let mut params = vec![format!("message: String = \"{default}\"")]; + params.extend(field_decls(n, &ec.fields)); + w.line(format!( + "class {class}({}) : {exc}({}, message)", + params.join(", "), + ec.value + )); + } + w.blank(); + w.line("internal companion object {"); + w.scope(|w| { + w.line("/** The exception for a raw code and its serialized payload. */"); + w.line("fun fromCode(code: Int, message: String, payload: ByteArray?): FfiException = when (code) {"); + w.scope(|w| { + for ec in &eb.codes { + let class = errors::pascal(&ec.name); + if ec.fields.is_empty() { + w.line(format!("{} -> {class}(message)", ec.value)); + } else { + let reads: Vec = + ec.fields.iter().map(|f| read_expr(n, &f.ty, "_r")).collect(); + w.line(format!( + "{} -> if (payload == null) FfiException(code, message) else decodeBuffer(payload) {{ _r -> {class}(message, {}) }}", + ec.value, + reads.join(", ") + )); + } + } + w.line("else -> FfiException(code, message)"); + }); + w.line("}"); + }); + w.line("}"); + }); + w.line("}"); +} + +/// Whether `m` or any module below it declares a free function. +fn has_functions(model: &BindingModel, m: &ModuleBinding) -> bool { + !m.functions.is_empty() || model.children(m).any(|c| has_functions(model, c)) +} + +/// The module object for `m`: its free functions, then one nested object +/// per submodule that has functions of its own. +fn render_module_object(w: &mut CodeWriter, n: &Names, model: &BindingModel, m: &ModuleBinding) { + // The model falls back to a function's doc for an undocumented module; + // that reads wrong on the object, so only a module's own doc is kept. + if !m.functions.iter().any(|f| f.doc == m.doc) { + writer_doc(w, &m.doc); + } + w.line(format!("object {} {{", n.object(m))); + w.scope(|w| { + let mut first = true; + for f in &m.functions { + if !first { + w.blank(); + } + first = false; + writer_fn_doc(w, &f.doc, &camel_params(&f.params)); + emit_callable( + w, + n, + f, + &format!("fun {}", n.function(m, f)), + false, + m.error.as_ref(), + ); + } + for child in model.children(m).filter(|c| has_functions(model, c)) { + if !first { + w.blank(); + } + first = false; + render_module_object(w, n, model, child); + } + }); + w.line("}"); +} + +/// Render every declaration of the top-level module `root` and its +/// submodules (the body of `{Root}.kt`, after the package line). +pub(crate) fn render_root(n: &Names, model: &BindingModel, root: &ModuleBinding) -> String { + let mut w = CodeWriter::four_space(); + let tree = model + .modules + .iter() + .filter(|m| m.segments.first() == root.segments.first()); + for m in tree { + if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { + render_error_domain(&mut w, n, eb); + } + for e in &m.enums { + if e.is_rich() { + render_rich_enum(&mut w, n, e); + } else { + render_enum(&mut w, n, e); + } + } + for s in &m.structs { + render_struct(&mut w, n, s); + } + for cb in &m.callback_interfaces { + render_callback_interface(&mut w, n, cb); + } + for i in &m.interfaces { + render_interface(&mut w, n, i, m.error.as_ref()); + } + } + if has_functions(model, root) { + w.blank(); + render_module_object(&mut w, n, model, root); + } + w.finish() +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/jni.rs b/crates/weaveffi-gen/src/targets/kotlin/jni.rs new file mode 100644 index 00000000..3342e9aa --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/jni.rs @@ -0,0 +1,579 @@ +//! The JNI shim (`{library}_jni.c`): the fixed runtime (from `runtime/`), +//! the load-time contract check, callback-interface trampolines and static +//! vtables, and one `Java_..._JniBridge_*` export per `JniBridge` native. +//! +//! Every export receives strings, bytes, and value buffers as a `ByteArray` +//! it borrows for the call as a `(ptr, len)` pair, and returns them as a +//! fresh `ByteArray` after freeing the producer's allocation with +//! `{prefix}_free_bytes`. The C argument list is built by walking the +//! callable's lowered [`AbiFn`] slots, so it matches the header exactly. + +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::model::{ + checksum_symbol, AbiFn, BindingModel, CallShape, CallbackInterfaceBinding, Family, FnBinding, + InterfaceBinding, IteratorBinding, Ty, +}; +use weaveffi_model::plan::{ArgPass, RetPass}; + +use crate::targets::kotlin::bridge::{shim_descriptor, shim_name}; +use crate::targets::kotlin::calls::item_slot; +use crate::targets::kotlin::names::{jni_c_type, jni_kind, Names}; + +/// The C name of a user parameter inside an export (`p_{name}`), which no +/// C keyword and no local of the shim can spell. +fn c_param(name: &str) -> String { + format!("p_{name}") +} + +/// The static vtable the shim passes for callback interface `cb`. +fn vtable_var(n: &Names, cb: &CallbackInterfaceBinding) -> String { + format!("Jni_{}_vtable", n.native(&cb.c_tag)) +} + +/// The cached method ID of one callback method's dispatch shim. +fn shim_mid(n: &Names, cb: &CallbackInterfaceBinding, method: &str) -> String { + format!("Jni_mid_{}_{method}", n.native(&cb.c_tag)) +} + +/// The C function a vtable entry points at for one callback method. +fn trampoline(n: &Names, cb: &CallbackInterfaceBinding, method: &str) -> String { + format!("Jni_{}_{method}", n.native(&cb.c_tag)) +} + +/// The pointee of a pointer slot (an iterator's `out_item`). +fn pointee(t: &CType) -> &CType { + match t { + CType::Ptr { pointee, .. } => pointee, + other => other, + } +} + +/// One export under construction: its JNI parameters, the statements before +/// and after the C call, and the C call's arguments in slot order. +struct Export { + params: Vec, + before: Vec, + after: Vec, + args: Vec, +} + +/// Lower `f`'s receiver and parameters onto the slots of `abi`, then the +/// trailing slots (`out_len`, `out_err`, `cancel_token`, `callback`, +/// `context`) by name. +fn lower_export(n: &Names, model: &BindingModel, f: &FnBinding, abi: &AbiFn, done: &str) -> Export { + let p = &n.prefix; + let mut e = Export { + params: Vec::new(), + before: Vec::new(), + after: Vec::new(), + args: Vec::new(), + }; + let mut slots = abi.params.iter(); + if f.has_self { + let slot = slots.next().expect("a method has a receiver slot"); + e.params.push("jlong self".into()); + e.args + .push(format!("({})(intptr_t)self", slot.ty.render_c(p))); + } + for param in &f.params { + for _ in ¶m.abi { + slots.next(); + } + let name = c_param(¶m.name); + e.params.push(format!("{} {name}", jni_c_type(¶m.ty))); + match param.arg_pass() { + ArgPass::Direct { slot } => e.args.push(if matches!(param.ty, Ty::Bool) { + format!("{name} == JNI_TRUE") + } else { + format!("({}){name}", slot.ty.render_c(p)) + }), + ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + e.before.push(format!( + "Jni_bytes {name}_b = Jni_borrow_bytes(env, {name});" + )); + e.after.push(format!("Jni_release_bytes(env, &{name}_b);")); + e.args.push(format!("{name}_b.ptr")); + e.args.push(format!("{name}_b.len")); + } + ArgPass::Object { slot, .. } => { + e.args + .push(format!("({})(intptr_t){name}", slot.ty.render_c(p))); + } + ArgPass::Callback { .. } => { + let cb_name = param + .ty + .callback_interface_name() + .expect("callback slots name a callback interface"); + let (_, cb) = model + .callback_interfaces() + .find(|(_, c)| c.name == local_type_name(cb_name)) + .expect("callback interfaces resolve"); + // The producer owns this global reference from here on and + // drops it through the vtable's `free`. + e.args + .push(format!("(void*)(*env)->NewGlobalRef(env, {name})")); + e.args.push(format!("&{}", vtable_var(n, cb))); + } + } + } + for slot in slots { + e.args.push(match slot.name.as_str() { + "out_len" => "&out_len".into(), + "out_err" => "&err".into(), + "cancel_token" => { + e.params.push("jlong cancel_token".into()); + format!("({p}_cancel_token*)(intptr_t)cancel_token") + } + "callback" => done.to_string(), + "context" => "context".into(), + other => unreachable!("unexpected trailing slot '{other}'"), + }); + } + e +} + +/// Emit `JNIEXPORT {ret} JNICALL {export}(...) {` with the shared leading +/// parameters. +fn open_export(w: &mut CodeWriter, n: &Names, ret: &str, native: &str, params: &[String]) { + let mut all = vec!["JNIEnv* env".to_string(), "jclass cls".to_string()]; + all.extend(params.iter().cloned()); + w.line(format!( + "JNIEXPORT {ret} JNICALL {}({}) {{", + n.jni_export(native), + all.join(", ") + )); +} + +/// The error check after a sync call: throw through `domain` and return. +fn error_check(w: &mut CodeWriter, domain: u32, fail: &str) { + w.line("if (err.code != 0) {"); + w.line(format!(" Jni_throw(env, &err, {domain});")); + w.line(format!(" return{fail};")); + w.line("}"); +} + +/// The JNI value of a direct C value `v` of `t`. +fn direct_to_jni(t: &Ty, v: &str) -> String { + if matches!(t, Ty::Bool) { + format!("{v} ? JNI_TRUE : JNI_FALSE") + } else { + format!("({}){v}", jni_c_type(t)) + } +} + +/// A synchronous callable (or an iterator launcher). +fn render_sync( + w: &mut CodeWriter, + n: &Names, + model: &BindingModel, + f: &FnBinding, + abi: &AbiFn, + domain: u32, +) { + let p = &n.prefix; + let native = n.native(&abi.symbol); + let e = lower_export(n, model, f, abi, ""); + let is_iter = matches!(f.shape, CallShape::Iterator(_)); + let ret = if is_iter { + RetPass::Direct + } else { + weaveffi_model::plan::ret_pass(f.ret.as_ref(), p) + }; + let (jret, fail) = match (&ret, is_iter) { + (_, true) => ("jlong".to_string(), " 0"), + (RetPass::Void, _) => ("void".to_string(), ""), + (RetPass::String | RetPass::Bytes | RetPass::Buffer, _) => { + ("jbyteArray".to_string(), " NULL") + } + (RetPass::Object { .. }, _) => ("jlong".to_string(), " 0"), + (RetPass::Direct, _) => ( + jni_c_type(f.ret.as_ref().expect("direct returns have a type")).to_string(), + " 0", + ), + }; + open_export(w, n, &jret, &native, &e.params); + w.scope(|w| { + w.line(format!("{p}_error err = {{0, NULL, NULL, 0}};")); + w.line("(void)cls;"); + for s in &e.before { + w.line(s); + } + let call = format!("{}({})", abi.symbol, e.args.join(", ")); + match (&ret, is_iter) { + (RetPass::Void, false) => w.line(format!("{call};")), + (RetPass::String | RetPass::Bytes | RetPass::Buffer, false) => { + w.line("size_t out_len = 0;"); + w.line(format!("{} rv = {call};", abi.ret.render_c(p))) + } + _ => w.line(format!("{} rv = {call};", abi.ret.render_c(p))), + }; + for s in &e.after { + w.line(s); + } + error_check(w, domain, fail); + match (&ret, is_iter) { + (_, true) | (RetPass::Object { .. }, _) => { + w.line("return (jlong)(intptr_t)rv;"); + } + (RetPass::Void, _) => {} + (RetPass::String | RetPass::Bytes | RetPass::Buffer, _) => { + w.line("return Jni_take_bytes(env, rv, out_len);"); + } + (RetPass::Direct, _) => { + let t = f.ret.as_ref().expect("direct returns have a type"); + w.line(format!("return {};", direct_to_jni(t, "rv"))); + } + }; + }); + w.line("}"); + w.blank(); +} + +/// An async callable: its completion trampoline, then the launcher export. +fn render_async(w: &mut CodeWriter, n: &Names, model: &BindingModel, f: &FnBinding) { + let CallShape::Async(ab) = &f.shape else { + unreachable!("render_async needs an async call shape"); + }; + let p = &n.prefix; + let native = n.native(&ab.launch.symbol); + let done = format!("Jni_done_{native}"); + let result_slots: Vec<&AbiParam> = ab.callback_params.iter().skip(2).collect(); + let decls: String = result_slots + .iter() + .map(|s| format!(", {} {}", s.ty.render_c(p), s.name)) + .collect(); + w.line(format!( + "static void {done}(void* context, {p}_error* err{decls}) {{" + )); + w.scope(|w| { + w.line("JNIEnv* env = Jni_complete_begin(context, err);"); + w.line("if (env == NULL) {"); + w.line(" return;"); + w.line("}"); + let deliver = match weaveffi_model::plan::ret_pass(f.ret.as_ref(), p) { + RetPass::Void => "Jni_on_unit".to_string(), + RetPass::String | RetPass::Bytes | RetPass::Buffer => { + "Jni_on_bytes, Jni_take_bytes(env, result_ptr, result_len)".to_string() + } + RetPass::Object { .. } => "Jni_on_long, (jlong)(intptr_t)result".to_string(), + RetPass::Direct => { + let t = f.ret.as_ref().expect("direct results have a type"); + format!( + "Jni_on_{}, {}", + jni_kind(t).to_lowercase(), + direct_to_jni(t, "result") + ) + } + }; + w.line(format!( + "(*env)->CallVoidMethod(env, (jobject)context, {deliver});" + )); + w.line("Jni_complete_end(env, context);"); + }); + w.line("}"); + w.blank(); + + let mut e = lower_export(n, model, f, &ab.launch, &done); + e.params.push("jobject completion".into()); + open_export(w, n, "void", &native, &e.params); + w.scope(|w| { + w.line("void* context = (void*)(*env)->NewGlobalRef(env, completion);"); + w.line("(void)cls;"); + for s in &e.before { + w.line(s); + } + w.line(format!("{}({});", ab.launch.symbol, e.args.join(", "))); + for s in &e.after { + w.line(s); + } + }); + w.line("}"); + w.blank(); +} + +/// An iterator's `_next` and `_destroy` exports. +fn render_iterator_natives(w: &mut CodeWriter, n: &Names, it: &IteratorBinding, domain: u32) { + let p = &n.prefix; + let tag = &it.iter_tag; + let item_ty = pointee(&it.next.params[1].ty).render_c(p); + let has_len = it.next.params.iter().any(|s| s.name == "out_len"); + let next = n.native(&it.next.symbol); + match item_slot(&it.elem) { + None => { + open_export(w, n, "jbyteArray", &next, &["jlong iter".to_string()]); + w.scope(|w| { + w.line(format!("{p}_error err = {{0, NULL, NULL, 0}};")); + w.line(format!("{item_ty} item = NULL;")); + w.line("size_t out_len = 0;"); + w.line("(void)cls;"); + w.line(format!( + "int32_t has = {}(({tag}*)(intptr_t)iter, &item, &out_len, &err);", + it.next.symbol + )); + error_check(w, domain, " NULL"); + w.line("if (has == 0) {"); + w.line(" return NULL;"); + w.line("}"); + w.line("return Jni_take_bytes(env, item, out_len);"); + }); + } + Some(slot) => { + open_export( + w, + n, + "jboolean", + &next, + &["jlong iter".to_string(), format!("{} out", slot.jni_array)], + ); + w.scope(|w| { + w.line(format!("{p}_error err = {{0, NULL, NULL, 0}};")); + w.line(format!("{item_ty} item = ({item_ty})0;")); + w.line("(void)cls;"); + let len = if has_len { "&out_len, " } else { "" }; + w.line(format!( + "int32_t has = {}(({tag}*)(intptr_t)iter, &item, {len}&err);", + it.next.symbol + )); + error_check(w, domain, " JNI_FALSE"); + w.line("if (has == 0) {"); + w.line(" return JNI_FALSE;"); + w.line("}"); + let value = match &it.elem { + Ty::Interface(_) | Ty::Optional(_) => "(jlong)(intptr_t)item".to_string(), + t => direct_to_jni(t, "item"), + }; + w.line(format!("{} value = {value};", slot.jni_elem)); + w.line(format!( + "(*env)->Set{}ArrayRegion(env, out, 0, 1, &value);", + slot.region + )); + w.line("return JNI_TRUE;"); + }); + } + } + w.line("}"); + w.blank(); + open_export( + w, + n, + "void", + &n.native(&it.destroy_symbol), + &["jlong iter".to_string()], + ); + w.scope(|w| { + w.line("(void)env;"); + w.line("(void)cls;"); + w.line(format!("{}(({tag}*)(intptr_t)iter);", it.destroy_symbol)); + }); + w.line("}"); + w.blank(); +} + +/// An interface's `_clone` and `_destroy` exports. +fn render_interface_natives(w: &mut CodeWriter, n: &Names, i: &InterfaceBinding) { + let tag = &i.c_tag; + open_export( + w, + n, + "jlong", + &n.native(&i.clone_symbol), + &["jlong self".to_string()], + ); + w.scope(|w| { + w.line("(void)env;"); + w.line("(void)cls;"); + w.line(format!( + "return (jlong)(intptr_t){}((const {tag}*)(intptr_t)self);", + i.clone_symbol + )); + }); + w.line("}"); + w.blank(); + open_export( + w, + n, + "void", + &n.native(&i.destroy_symbol), + &["jlong self".to_string()], + ); + w.scope(|w| { + w.line("(void)env;"); + w.line("(void)cls;"); + w.line(format!("{}(({tag}*)(intptr_t)self);", i.destroy_symbol)); + }); + w.line("}"); + w.blank(); +} + +/// One callback interface: the cached shim method IDs, one trampoline per +/// method, and the static vtable. +fn render_callback_interface(w: &mut CodeWriter, n: &Names, cb: &CallbackInterfaceBinding) { + let p = &n.prefix; + for m in &cb.methods { + w.line(format!( + "static jmethodID {} = NULL;", + shim_mid(n, cb, &m.name) + )); + } + w.blank(); + for m in &cb.methods { + // The first slot is `ctx` and the last `out_err`; everything between + // belongs to the method's parameters, in order. + let inner = &m.abi_params[1..m.abi_params.len() - 1]; + let mut decls = vec!["void* ctx".to_string()]; + decls.extend( + inner + .iter() + .map(|s| format!("{} {}", s.ty.render_c(p), c_param(&s.name))), + ); + decls.push(format!("{p}_error* out_err")); + let ret_c = m.abi_ret.render_c(p); + let fail = if m.ret.is_some() { + format!(" ({ret_c})0") + } else { + String::new() + }; + w.line(format!( + "static {ret_c} {}({}) {{", + trampoline(n, cb, &m.name), + decls.join(", ") + )); + w.scope(|w| { + w.line("JNIEnv* env = Jni_callback_begin(out_err);"); + w.line("if (env == NULL) {"); + w.line(format!(" return{fail};")); + w.line("}"); + let mut args = vec!["(jobject)ctx".to_string()]; + let mut slots = inner.iter(); + for param in &m.params { + let own: Vec<&AbiParam> = param.abi.iter().filter_map(|_| slots.next()).collect(); + let arg = match param.ty.family() { + Family::String | Family::Bytes | Family::Buffer => format!( + "Jni_new_bytes(env, {}, {})", + c_param(&own[0].name), + c_param(&own[1].name) + ), + Family::Object { .. } => { + format!("(jlong)(intptr_t){}", c_param(&own[0].name)) + } + _ => direct_to_jni(¶m.ty, &c_param(&own[0].name)), + }; + args.push(arg); + } + let kind = m.ret.as_ref().map(jni_kind).unwrap_or("Void"); + let call = format!( + "(*env)->CallStatic{kind}Method(env, Jni_bridge, {}, {})", + shim_mid(n, cb, &m.name), + args.join(", ") + ); + match &m.ret { + None => { + w.line(format!("{call};")); + w.line("Jni_callback_end(env, out_err);"); + } + Some(t) => { + w.line(format!("{} rv = {call};", jni_c_type(t))); + w.line("if (Jni_callback_end(env, out_err)) {"); + w.line(format!(" return{fail};")); + w.line("}"); + if matches!(t, Ty::Bool) { + w.line("return rv == JNI_TRUE;"); + } else { + w.line(format!("return ({ret_c})rv;")); + } + } + } + }); + w.line("}"); + w.blank(); + } + let mut entries: Vec = cb + .methods + .iter() + .map(|m| trampoline(n, cb, &m.name)) + .collect(); + entries.push("Jni_release_callback".to_string()); + w.line(format!( + "static const {} {} = {{{}}};", + cb.vtable_tag, + vtable_var(n, cb), + entries.join(", ") + )); + w.blank(); +} + +/// `Jni_load`: the per-module contract checks and the method IDs the +/// generated code caches. +fn render_load(w: &mut CodeWriter, n: &Names, model: &BindingModel, name: &str) { + w.line("static jint Jni_load(JNIEnv* env) {"); + w.scope(|w| { + w.line("(void)env;"); + for root in model.roots() { + let Some(sum) = root.checksum else { continue }; + w.line(format!( + "if ({}() != UINT64_C(0x{sum:016x})) {{", + checksum_symbol(&n.prefix, &root.name) + )); + w.line(format!( + " return Jni_load_error(env, \"{name}: module '{}' does not match these bindings (its contract checksum differs); regenerate the bindings or rebuild the library\");", + root.name + )); + w.line("}"); + } + if model.has_async() { + w.line("if (Jni_load_async(env) != JNI_OK) {"); + w.line(" return JNI_ERR;"); + w.line("}"); + } + if model.has_callback_interfaces() { + w.line("if (Jni_load_callbacks(env) != JNI_OK) {"); + w.line(" return JNI_ERR;"); + w.line("}"); + } + for (_, cb) in model.callback_interfaces() { + for m in &cb.methods { + let mid = shim_mid(n, cb, &m.name); + w.line(format!( + "{mid} = (*env)->GetStaticMethodID(env, Jni_bridge, \"{}\", \"{}\");", + shim_name(n, cb, m), + shim_descriptor(n, cb, m) + )); + w.line(format!("if ({mid} == NULL) {{")); + w.line(" return JNI_ERR;"); + w.line("}"); + } + } + w.line("return JNI_OK;"); + }); + w.line("}"); + w.blank(); +} + +/// Render the generated part of the shim (after the runtime templates). +pub(crate) fn render_exports(n: &Names, model: &BindingModel, name: &str) -> String { + let mut w = CodeWriter::four_space(); + w.blank(); + for (_, cb) in model.callback_interfaces() { + render_callback_interface(&mut w, n, cb); + } + render_load(&mut w, n, model, name); + for m in &model.modules { + for i in &m.interfaces { + render_interface_natives(&mut w, n, i); + } + for f in m.callables() { + let domain = n.domain(f, m.error.as_ref()); + match &f.shape { + CallShape::Sync(abi) => render_sync(&mut w, n, model, f, abi, domain), + CallShape::Iterator(it) => { + render_sync(&mut w, n, model, f, &it.launch, domain); + render_iterator_natives(&mut w, n, it, domain); + } + CallShape::Async(_) => render_async(&mut w, n, model, f), + } + } + } + w.finish() +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/mod.rs b/crates/weaveffi-gen/src/targets/kotlin/mod.rs new file mode 100644 index 00000000..3dabe792 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/mod.rs @@ -0,0 +1,317 @@ +//! Kotlin binding generator. +//! +//! Emits a Gradle library module (`kotlin/`) in the identity's package +//! (`{prefix}` unless configured): Kotlin sources over a JNI shim +//! (`lib{library}_jni`) that calls the C ABI of the producer library +//! (`lib{library}`). The default flavor is an Android library (the shim builds +//! through the NDK's CMake); the `jvm` flavor is a plain Kotlin/JVM library +//! whose shim is built with CMake directly. +//! +//! The Kotlin surface: records as `data class`es and rich enums as sealed +//! classes (decoded from value buffers), C-style enums as `enum class`es, +//! interfaces as `AutoCloseable` wrappers whose calls borrow the native +//! reference (so neither `close()` nor the phantom-reference cleaner frees it +//! mid-call), `iter` returns as `NativeIterator`, async callables as +//! `suspend fun`s (cancellation cancels the native token), callback +//! interfaces as Kotlin `interface`s, and one `object` per module holding its +//! free functions. `JNI_OnLoad` checks the ABI revision and every top-level +//! module's contract checksum before the first call. + +mod bridge; +mod calls; +mod codec; +mod docs; +mod entities; +mod jni; +mod names; +mod package; +mod runtime; + +use camino::{Utf8Path, Utf8PathBuf}; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::resolved::ResolvedApi; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::package::{PackageContext, PackagedFile}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; + +use crate::targets::kotlin::names::{resolve_package, Names}; + +/// Which Gradle module the Kotlin target emits. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum KotlinFlavor { + /// An Android library module (`com.android.library`) whose JNI shim + /// builds through the NDK's CMake. + #[default] + Android, + /// A plain Kotlin/JVM library; the JNI shim is built with CMake + /// separately and loaded from `java.library.path`. + Jvm, +} + +/// Per-target configuration for [`KotlinGenerator`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct KotlinConfig { + /// The Kotlin package of the generated sources (default: the identity's + /// C prefix, such as `kvstore`). + pub package: Option, + /// The Gradle module flavor (default `android`). + pub flavor: KotlinFlavor, + /// When `true` (the default), free functions drop their module path from + /// their Kotlin name (`Contacts.count()` rather than + /// `Contacts.contactsCount()`). + pub strip_module_prefix: bool, + /// Basename of the IDL the CLI was invoked with. + #[serde(skip)] + pub input_basename: Option, +} + +impl Default for KotlinConfig { + /// Android flavor, module prefixes stripped, package from the identity. + fn default() -> Self { + Self { + package: None, + flavor: KotlinFlavor::Android, + strip_module_prefix: true, + input_basename: None, + } + } +} + +impl KotlinConfig { + /// The configured Kotlin package, if any; the identity's prefix applies + /// otherwise. + pub fn package(&self) -> Option<&str> { + self.package.as_deref() + } + + /// Returns the input IDL basename embedded in generated file headers, + /// falling back to `"api.yml"`. + pub fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } +} + +/// Kotlin backend: a Gradle module with Kotlin sources over a JNI shim. +pub struct KotlinGenerator; + +/// Everything one generation run renders from. +struct Plan<'a> { + api: &'a ResolvedApi, + model: &'a BindingModel, + names: Names, + config: &'a KotlinConfig, + dir: Utf8PathBuf, +} + +impl<'a> Plan<'a> { + fn new( + api: &'a ResolvedApi, + model: &'a BindingModel, + out_dir: &Utf8Path, + config: &'a KotlinConfig, + ) -> Self { + let id = api.identity(); + let package = resolve_package(config.package(), &id.prefix); + let names = Names::new(model, &package, &id.library, config.strip_module_prefix); + Self { + api, + model, + names, + config, + dir: out_dir.join("kotlin"), + } + } + + /// Wrap a Kotlin source body: header, optional deprecation suppression, + /// the package line, the body, and the trailer. + fn kotlin_file(&self, file: &str, body: &str, suppress_deprecation: bool) -> String { + let mut out = render_prelude(CommentStyle::DoubleSlash, self.config.input_basename()); + if suppress_deprecation { + out.push_str("@file:Suppress(\"DEPRECATION\")\n\n"); + } + out.push_str(&format!("package {}\n", self.names.package)); + out.push_str(body); + out.push('\n'); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, file)); + out + } + + /// A fixed runtime source (already carrying its package line). + fn runtime_file(&self, file: &str, source: &str) -> String { + format!( + "{}{source}\n{}", + render_prelude(CommentStyle::DoubleSlash, self.config.input_basename()), + render_trailer(CommentStyle::DoubleSlash, file) + ) + } + + fn files(&self) -> Vec<(Utf8PathBuf, String)> { + let id = self.api.identity(); + let n = &self.names; + let model = self.model; + let input = self.config.input_basename(); + let src = self + .dir + .join("src/main/kotlin") + .join(n.package_path.as_str()); + let cpp = self.dir.join("src/main/cpp"); + let header = format!("{}.h", id.library); + let jni_c = format!("{}.c", runtime::jni_library(&id.library)); + let deprecations = uses_deprecated(model); + + let mut files = vec![ + ( + self.dir.join("settings.gradle.kts"), + package::settings_gradle(id, self.config.flavor, input), + ), + ( + self.dir.join("build.gradle.kts"), + package::build_gradle(id, &n.package, self.config.flavor, model.has_async(), input), + ), + ]; + if self.config.flavor == KotlinFlavor::Android { + files.push(( + self.dir.join("consumer-rules.pro"), + package::consumer_rules(&n.package, input), + )); + } + files.push((cpp.join("CMakeLists.txt"), package::cmake_lists(id, input))); + files.push(( + cpp.join(&header), + crate::targets::c::render_c_header_from_model(model, input, &header), + )); + let c_name = id.name.replace('\\', "\\\\").replace('"', "\\\""); + let jni_source = format!( + "{}{}{}\n{}", + render_prelude(CommentStyle::DoubleSlash, input), + runtime::jni_runtime(n, model, &header, &c_name), + jni::render_exports(n, model, &c_name), + render_trailer(CommentStyle::DoubleSlash, &jni_c) + ); + files.push((cpp.join(&jni_c), jni_source)); + + files.push(( + src.join("Runtime.kt"), + self.runtime_file("Runtime.kt", &runtime::runtime_kt(n, &id.library_env_var())), + )); + if model.has_buffers() { + files.push(( + src.join("Buffers.kt"), + self.runtime_file("Buffers.kt", &runtime::buffers_kt(n)), + )); + } + if model.has_async() { + files.push(( + src.join("Async.kt"), + self.runtime_file("Async.kt", &runtime::async_kt(n)), + )); + } + files.push(( + src.join("JniBridge.kt"), + self.kotlin_file( + "JniBridge.kt", + &bridge::render_bridge(n, model), + deprecations, + ), + )); + for root in model.roots() { + let file = format!("{}.kt", n.object(root)); + let body = entities::render_root(n, model, root); + files.push(( + src.join(&file), + self.kotlin_file(&file, &body, deprecations), + )); + } + files + } +} + +/// Whether generated code references a deprecated declaration (a type or a +/// callback method), so its files suppress the warning. +fn uses_deprecated(model: &BindingModel) -> bool { + model.modules.iter().any(|m| { + m.enums.iter().any(|e| e.deprecated.is_some()) + || m.structs.iter().any(|s| s.deprecated.is_some()) + || m.interfaces.iter().any(|i| i.deprecated.is_some()) + || m.callback_interfaces + .iter() + .any(|c| c.deprecated.is_some() || c.methods.iter().any(|x| x.deprecated.is_some())) + }) +} + +impl LanguageBackend for KotlinGenerator { + type Config = KotlinConfig; + + fn name(&self) -> &'static str { + "kotlin" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + Plan::new(api, model, out_dir, config) + .files() + .into_iter() + .map(|(path, contents)| OutputFile::new(path, contents)) + .collect() + } + + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let plan = Plan::new(api, model, out_dir, config); + let mut files: Vec = plan + .files() + .into_iter() + .map(|(path, contents)| PackagedFile::text(path, contents)) + .collect(); + files.push(PackagedFile::text( + plan.dir.join("README.md"), + package::packaged_readme( + api.identity(), + &plan.names.package, + ctx, + config.input_basename(), + ), + )); + // Android binaries land in `jniLibs//`, where the shim's CMake + // build links them and the AAR packages them; desktop binaries are + // classpath resources the loader extracts at run time. + for nb in &ctx.binaries.binaries { + let filename = ctx.binaries.bundled_filename(nb.platform); + if let Some(abi) = nb.platform.android_abi() { + let dest = plan.dir.join("src/main/jniLibs").join(abi).join(filename); + files.push(PackagedFile::copy(dest, nb.source.clone())); + } else if nb.platform.is_desktop() { + let dest = plan + .dir + .join("src/main/resources/natives") + .join(nb.platform.id()) + .join(filename); + files.push(PackagedFile::copy(dest, nb.source.clone())); + } + } + Some(files) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/weaveffi-gen/src/targets/kotlin/names.rs b/crates/weaveffi-gen/src/targets/kotlin/names.rs new file mode 100644 index 00000000..d31fc220 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/names.rs @@ -0,0 +1,417 @@ +//! Naming policy: the Kotlin package, the Kotlin name of every user type, +//! error domain, and module object, the Kotlin and JNI types each IR type +//! crosses as, and JNI name mangling. +//! +//! User types live at the top level of the package, so a type whose name is +//! a Kotlin keyword, a type Kotlin or Java imports by default (`Unit`, +//! `String`, `Result`, ...), or a name the generated runtime declares gains a +//! trailing underscore (`Unit` becomes `Unit_`). A module object whose name +//! would equal a type's gains a `Module` suffix (`kv.stats` beside a `Stats` +//! record becomes `Kv.StatsModule`). + +use std::collections::{HashMap, HashSet}; +use std::fmt::Write as _; + +use crate::codegen::common::pascal_case; +use crate::lang; +use crate::utils::{local_type_name, wrapper_name}; +use weaveffi_model::errors; +use weaveffi_model::model::{BindingModel, ErrorBinding, FnBinding, ModuleBinding, Ty}; + +/// Names user types must not take: Kotlin and Java types the compiler +/// imports into every file, plus the declarations of the generated runtime. +pub(crate) const RESERVED_TYPE_NAMES: &[&str] = &[ + "Any", + "Array", + "AutoCloseable", + "Boolean", + "BooleanArray", + "BufferReader", + "BufferWriter", + "Byte", + "ByteArray", + "Char", + "CharSequence", + "Charsets", + "Collection", + "Comparable", + "Deprecated", + "Double", + "DoubleArray", + "Enum", + "Error", + "Exception", + "FfiException", + "Float", + "FloatArray", + "Function", + "IllegalArgumentException", + "IllegalStateException", + "Int", + "IntArray", + "Iterable", + "Iterator", + "JniBridge", + "JvmStatic", + "Lazy", + "List", + "Long", + "LongArray", + "Map", + "MutableList", + "MutableMap", + "MutableSet", + "NativeCleaner", + "NativeCompletion", + "NativeHandle", + "NativeIterator", + "NativeLibrary", + "NoSuchElementException", + "Nothing", + "Number", + "Object", + "Pair", + "Result", + "Runnable", + "RuntimeException", + "Sequence", + "Set", + "Short", + "ShortArray", + "String", + "Suppress", + "System", + "Thread", + "Throwable", + "Triple", + "Unit", + "Volatile", +]; + +/// Member names the generated wrapper classes already use (or inherit from +/// `Any`); a method, static, or callback method spelled the same gains a +/// trailing underscore. +const RESERVED_MEMBER_NAMES: &[&str] = &[ + "cloneHandle", + "close", + "equals", + "fromHandle", + "fromHandleOrNull", + "handle", + "hashCode", + "toString", +]; + +/// Escape a user identifier for a Kotlin declaration or expression position: +/// a reserved word gains a trailing underscore. +pub(crate) fn kt_escape(name: &str) -> String { + lang::escape_ident(name, lang::KOTLIN_KEYWORDS) +} + +/// Lower-camelCase an identifier: `rich_variant` becomes `richVariant`. +pub(crate) fn lower_camel(s: &str) -> String { + let pascal = pascal_case(s); + let mut chars = pascal.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_lowercase().chain(chars).collect(), + } +} + +/// The Kotlin spelling of a parameter: lowerCamelCased, then escaped. It +/// never starts with `_`, which keeps every generated local (all spelled +/// `_name`) out of its way. +pub(crate) fn kt_param(name: &str) -> String { + kt_escape(&lower_camel(name)) +} + +/// The Kotlin spelling of a method, static, or callback method: like +/// [`kt_param`], and also escaped away from the members every wrapper +/// declares. +pub(crate) fn kt_member(name: &str) -> String { + let n = kt_param(name); + if RESERVED_MEMBER_NAMES.contains(&n.as_str()) { + format!("{n}_") + } else { + n + } +} + +/// Escape a user type name away from keywords and reserved type names. +fn kt_type_ident(name: &str) -> String { + if lang::is_reserved(name, lang::KOTLIN_KEYWORDS) || RESERVED_TYPE_NAMES.contains(&name) { + format!("{name}_") + } else { + name.to_string() + } +} + +/// JNI exports map a Java identifier to a C symbol by escaping `_` to `_1` +/// (plus `;`, `[`, and non-ASCII characters). +pub(crate) fn jni_mangle(ident: &str) -> String { + let mut out = String::with_capacity(ident.len()); + for c in ident.chars() { + match c { + '_' => out.push_str("_1"), + ';' => out.push_str("_2"), + '[' => out.push_str("_3"), + c if c.is_ascii_alphanumeric() => out.push(c), + c => { + let _ = write!(out, "_0{:04x}", c as u32); + } + } + } + out +} + +/// Every name the generated Kotlin and JNI code needs, resolved once from +/// the identity, the target configuration, and the model. +pub(crate) struct Names { + /// The Kotlin package (`kvstore`). + pub package: String, + /// The package as a JVM internal path (`kvstore`, `com/example/kv`). + pub package_path: String, + /// The C symbol prefix (`kvstore`). + pub prefix: String, + /// The library name the producer is loaded as and the JNI shim derives + /// from (`kvstore`). + pub library: String, + /// Whether free-function names drop their module path. + pub strip_module_prefix: bool, + types: HashMap, + exceptions: HashMap, + domains: HashMap, + objects: HashMap, +} + +impl Names { + /// Resolve every name for `model`, with `package` the Kotlin package + /// (already validated) and `library` the producer library name. + pub(crate) fn new( + model: &BindingModel, + package: &str, + library: &str, + strip_module_prefix: bool, + ) -> Self { + let mut types = HashMap::new(); + let mut exceptions = HashMap::new(); + let mut domains = HashMap::new(); + for m in &model.modules { + let declared = m + .enums + .iter() + .map(|e| &e.name) + .chain(m.structs.iter().map(|s| &s.name)) + .chain(m.interfaces.iter().map(|i| &i.name)) + .chain(m.callback_interfaces.iter().map(|c| &c.name)); + for name in declared { + types.insert(name.clone(), kt_type_ident(name)); + } + if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { + exceptions.insert( + eb.c_tag.clone(), + kt_type_ident(&errors::exception_type_name(&eb.name)), + ); + let next = u32::try_from(domains.len() + 1).unwrap_or(u32::MAX); + domains.insert(eb.c_tag.clone(), next); + } + } + let taken: HashSet<&String> = types.values().chain(exceptions.values()).collect(); + let objects = model + .modules + .iter() + .map(|m| { + let mut name = kt_type_ident(&pascal_case(&m.name)); + if taken.contains(&name) { + name.push_str("Module"); + } + (m.path.clone(), name) + }) + .collect(); + Self { + package: package.to_string(), + package_path: package.replace('.', "/"), + prefix: model.prefix.clone(), + library: library.to_string(), + strip_module_prefix, + types, + exceptions, + domains, + objects, + } + } + + /// The Kotlin class of a user type named `ir_name` (bare or dotted). + pub(crate) fn ty(&self, ir_name: &str) -> String { + let local = local_type_name(ir_name); + self.types + .get(local) + .cloned() + .unwrap_or_else(|| kt_type_ident(local)) + } + + /// The Kotlin exception class of an error domain. + pub(crate) fn exception(&self, eb: &ErrorBinding) -> String { + self.exceptions + .get(&eb.c_tag) + .cloned() + .unwrap_or_else(|| kt_type_ident(&errors::exception_type_name(&eb.name))) + } + + /// The domain index `JniBridge.error` maps a failure of `f` through: + /// the module's domain for a throwing callable, else 0 (generic). + pub(crate) fn domain(&self, f: &FnBinding, error: Option<&ErrorBinding>) -> u32 { + match error { + Some(eb) if f.throws => self.domains.get(&eb.c_tag).copied().unwrap_or(0), + _ => 0, + } + } + + /// Every declared domain with its index, in index order. + pub(crate) fn domains<'a>(&self, model: &'a BindingModel) -> Vec<(u32, &'a ErrorBinding)> { + let mut out: Vec<(u32, &ErrorBinding)> = model + .modules + .iter() + .filter_map(|m| m.error.as_ref().filter(|e| e.declared_here)) + .filter_map(|eb| self.domains.get(&eb.c_tag).map(|i| (*i, eb))) + .collect(); + out.sort_by_key(|(i, _)| *i); + out + } + + /// The simple Kotlin name of a module's object. + pub(crate) fn object(&self, m: &ModuleBinding) -> String { + self.objects + .get(&m.path) + .cloned() + .unwrap_or_else(|| pascal_case(&m.name)) + } + + /// The Kotlin name of a free function. + pub(crate) fn function(&self, m: &ModuleBinding, f: &FnBinding) -> String { + kt_member(&wrapper_name(&m.path, &f.name, self.strip_module_prefix)) + } + + /// The `JniBridge` member name for a C symbol: the symbol without the + /// library prefix, unique because C symbols are. + pub(crate) fn native(&self, symbol: &str) -> String { + symbol + .strip_prefix(&format!("{}_", self.prefix)) + .unwrap_or(symbol) + .to_string() + } + + /// The JNI export name of a `JniBridge` native. + pub(crate) fn jni_export(&self, native: &str) -> String { + format!("Java_{}_{}", self.jni_class(), jni_mangle(native)) + } + + /// The mangled `{package}_JniBridge` part of every JNI export name. + pub(crate) fn jni_class(&self) -> String { + let pkg: Vec = self.package.split('.').map(jni_mangle).collect(); + format!("{}_JniBridge", pkg.join("_")) + } + + /// The public Kotlin type of an IR type. + pub(crate) fn kt_type(&self, t: &Ty) -> String { + match t { + Ty::I8 | Ty::U8 => "Byte".into(), + Ty::I16 | Ty::U16 => "Short".into(), + Ty::I32 => "Int".into(), + Ty::U32 | Ty::I64 | Ty::U64 => "Long".into(), + Ty::F32 => "Float".into(), + Ty::F64 => "Double".into(), + Ty::Bool => "Boolean".into(), + Ty::StringUtf8 => "String".into(), + Ty::Bytes => "ByteArray".into(), + Ty::Record(n) + | Ty::RichEnum(n) + | Ty::Enum(n) + | Ty::Interface(n) + | Ty::CallbackInterface(n) => self.ty(n), + Ty::Optional(inner) => format!("{}?", self.kt_type(inner)), + Ty::List(inner) => format!("List<{}>", self.kt_type(inner)), + Ty::Map(k, v) => format!("Map<{}, {}>", self.kt_type(k), self.kt_type(v)), + Ty::Iterator(inner) => format!("NativeIterator<{}>", self.kt_type(inner)), + } + } + + /// The Kotlin type a value crosses the JNI boundary as: strings, bytes, + /// and value buffers as `ByteArray`, enums as `Int`, objects and + /// iterators as their address (`0L` is none), and a callback interface + /// as the implementing object. + pub(crate) fn jni_type(&self, t: &Ty) -> String { + match t { + Ty::Enum(_) => "Int".into(), + Ty::StringUtf8 | Ty::Bytes => "ByteArray".into(), + Ty::Interface(_) | Ty::Iterator(_) => "Long".into(), + Ty::CallbackInterface(n) => self.ty(n), + _ if t.is_buffered() => "ByteArray".into(), + Ty::Optional(_) => "Long".into(), + other => self.kt_type(other), + } + } + + /// The JVM type descriptor of [`Names::jni_type`], for the method IDs + /// the shim caches. + pub(crate) fn jni_descriptor(&self, t: Option<&Ty>) -> String { + let Some(t) = t else { + return "V".into(); + }; + match self.jni_type(t).as_str() { + "Byte" => "B".into(), + "Short" => "S".into(), + "Int" => "I".into(), + "Long" => "J".into(), + "Float" => "F".into(), + "Double" => "D".into(), + "Boolean" => "Z".into(), + "ByteArray" => "[B".into(), + class => format!("L{}/{class};", self.package_path), + } + } +} + +/// The JNI C type of [`Names::jni_type`]. +pub(crate) fn jni_c_type(t: &Ty) -> &'static str { + match t { + Ty::I8 | Ty::U8 => "jbyte", + Ty::I16 | Ty::U16 => "jshort", + Ty::I32 | Ty::Enum(_) => "jint", + Ty::U32 | Ty::I64 | Ty::U64 | Ty::Interface(_) | Ty::Iterator(_) => "jlong", + Ty::F32 => "jfloat", + Ty::F64 => "jdouble", + Ty::Bool => "jboolean", + Ty::StringUtf8 | Ty::Bytes => "jbyteArray", + Ty::CallbackInterface(_) => "jobject", + _ if t.is_buffered() => "jbyteArray", + Ty::Optional(_) => "jlong", + _ => unreachable!("every type is covered above"), + } +} + +/// The `Call{Kind}Method` / `on{Kind}` stem for a direct-family or object +/// value: `Boolean`, `Byte`, `Short`, `Int`, `Long`, `Float`, or `Double`. +pub(crate) fn jni_kind(t: &Ty) -> &'static str { + match t { + Ty::Bool => "Boolean", + Ty::I8 | Ty::U8 => "Byte", + Ty::I16 | Ty::U16 => "Short", + Ty::I32 | Ty::Enum(_) => "Int", + Ty::F32 => "Float", + Ty::F64 => "Double", + _ => "Long", + } +} + +/// Resolve the Kotlin package: the configured one, else the identity +/// prefix. A segment that is a Kotlin keyword gains a trailing underscore. +pub(crate) fn resolve_package(configured: Option<&str>, prefix: &str) -> String { + let raw = configured + .filter(|p| !p.trim().is_empty()) + .unwrap_or(prefix); + raw.split('.') + .map(|seg| kt_escape(seg.trim())) + .collect::>() + .join(".") +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/package.rs b/crates/weaveffi-gen/src/targets/kotlin/package.rs new file mode 100644 index 00000000..d9cdcfb3 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/package.rs @@ -0,0 +1,305 @@ +//! Build scaffolding: `settings.gradle.kts`, `build.gradle.kts` for the +//! Android library or plain Kotlin/JVM flavor (both with a `maven-publish` +//! publication), the R8 keep rules, the CMake build for the JNI shim, and +//! the packaged layout's README. + +use crate::package::PackageContext; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::pkg::Identity; + +use crate::targets::kotlin::runtime::jni_library; +use crate::targets::kotlin::KotlinFlavor; + +/// The Kotlin Gradle plugin version the build files pin. +const KOTLIN_VERSION: &str = "2.0.21"; +/// The Android Gradle plugin version the Android flavor pins. +const AGP_VERSION: &str = "8.7.3"; +/// The coroutines runtime async callables suspend through. +const COROUTINES: &str = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0"; + +/// Escape `s` for a double-quoted Kotlin string literal. +pub(crate) fn kts_quote(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('$', "\\$") +} + +/// Wrap `body` in the generated-file header and trailer. +fn wrap(style: CommentStyle, input_basename: &str, file: &str, body: &str) -> String { + format!( + "{}{body}\n{}", + render_prelude(style, input_basename), + render_trailer(style, file) + ) +} + +/// `settings.gradle.kts`: plugin and dependency repositories, so the module +/// builds on its own, and the root project named after the package. +pub(crate) fn settings_gradle(id: &Identity, flavor: KotlinFlavor, input_basename: &str) -> String { + let google = match flavor { + KotlinFlavor::Android => " google()\n", + KotlinFlavor::Jvm => "", + }; + let body = format!( + r#"pluginManagement {{ + repositories {{ +{google} mavenCentral() + gradlePluginPortal() + }} +}} + +dependencyResolutionManagement {{ + repositories {{ +{google} mavenCentral() + }} +}} + +rootProject.name = "{}" +"#, + kts_quote(&id.name) + ); + wrap( + CommentStyle::DoubleSlash, + input_basename, + "settings.gradle.kts", + &body, + ) +} + +/// `build.gradle.kts` for the configured flavor. +pub(crate) fn build_gradle( + id: &Identity, + package: &str, + flavor: KotlinFlavor, + has_async: bool, + input_basename: &str, +) -> String { + let group = kts_quote(package); + let version = kts_quote(&id.version); + let artifact = kts_quote(&id.name); + let deps = if has_async { + format!("dependencies {{\n implementation(\"{COROUTINES}\")\n}}\n\n") + } else { + String::new() + }; + let body = match flavor { + KotlinFlavor::Android => format!( + r#"plugins {{ + id("com.android.library") version "{AGP_VERSION}" + id("org.jetbrains.kotlin.android") version "{KOTLIN_VERSION}" + `maven-publish` +}} + +group = "{group}" +version = "{version}" + +android {{ + namespace = "{group}" + compileSdk = 35 + defaultConfig {{ + minSdk = 21 + consumerProguardFiles("consumer-rules.pro") + }} + // Builds lib{jni}.so per ABI, linked against the producer library in + // src/main/jniLibs// (see src/main/cpp/CMakeLists.txt). + externalNativeBuild {{ + cmake {{ + path = file("src/main/cpp/CMakeLists.txt") + }} + }} + compileOptions {{ + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + }} + publishing {{ + singleVariant("release") {{ + withSourcesJar() + }} + }} +}} + +kotlin {{ + compilerOptions {{ + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + }} +}} + +{deps}publishing {{ + publications {{ + register("release") {{ + artifactId = "{artifact}" + afterEvaluate {{ + from(components["release"]) + }} + }} + }} +}} +"#, + jni = jni_library(&id.library), + ), + KotlinFlavor::Jvm => format!( + r#"plugins {{ + kotlin("jvm") version "{KOTLIN_VERSION}" + `java-library` + `maven-publish` +}} + +group = "{group}" +version = "{version}" + +// The JNI shim (lib{jni}) is built separately with CMake from +// src/main/cpp and must be on java.library.path at run time. + +{deps}java {{ + withSourcesJar() +}} + +publishing {{ + publications {{ + create("maven") {{ + artifactId = "{artifact}" + from(components["java"]) + }} + }} +}} +"#, + jni = jni_library(&id.library), + ), + }; + wrap( + CommentStyle::DoubleSlash, + input_basename, + "build.gradle.kts", + &body, + ) +} + +/// `consumer-rules.pro`: the JNI shim finds classes and methods by name, so +/// R8 must keep the package intact in consuming apps. +pub(crate) fn consumer_rules(package: &str, input_basename: &str) -> String { + let body = format!( + "# The JNI shim looks classes and methods up by name.\n-keep class {package}.** {{ *; }}\n" + ); + wrap( + CommentStyle::Hash, + input_basename, + "consumer-rules.pro", + &body, + ) +} + +/// `src/main/cpp/CMakeLists.txt`: builds `lib{library}_jni` from the shim +/// and the bundled header, linked against the producer library found in +/// `{PREFIX}_LIBRARY_DIR` (on Android, `src/main/jniLibs/` by default). +pub(crate) fn cmake_lists(id: &Identity, input_basename: &str) -> String { + let lib = &id.library; + let jni = jni_library(lib); + let upper = id.macro_prefix(); + let dir_var = format!("{upper}_LIBRARY_DIR"); + let body = format!( + r#"cmake_minimum_required(VERSION 3.18) +project({jni} C) + +# The directory holding the producer library (lib{lib}). Android builds +# default to the per-ABI copy under src/main/jniLibs/. +set({dir_var} "" CACHE PATH "Directory holding the {lib} producer library") +if(NOT {dir_var} AND ANDROID) + set({dir_var} "${{CMAKE_CURRENT_SOURCE_DIR}}/../jniLibs/${{ANDROID_ABI}}") +endif() +if({dir_var}) + find_library({upper}_PRODUCER NAMES {lib} PATHS "${{{dir_var}}}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED) +else() + set(CMAKE_FIND_FRAMEWORK NEVER) + find_library({upper}_PRODUCER NAMES {lib} REQUIRED) +endif() + +add_library({jni} SHARED {jni}.c) +target_include_directories({jni} PRIVATE "${{CMAKE_CURRENT_SOURCE_DIR}}") +target_link_libraries({jni} PRIVATE "${{{upper}_PRODUCER}}") +if(NOT ANDROID) + find_package(JNI REQUIRED) + find_package(Threads REQUIRED) + target_include_directories({jni} PRIVATE ${{JNI_INCLUDE_DIRS}}) + target_link_libraries({jni} PRIVATE Threads::Threads) +endif() +"# + ); + wrap(CommentStyle::Hash, input_basename, "CMakeLists.txt", &body) +} + +/// The packaged module's README: the layout, the bundled binaries, and how +/// each runtime loads them. +pub(crate) fn packaged_readme( + id: &Identity, + package: &str, + ctx: &PackageContext, + input_basename: &str, +) -> String { + let jni = jni_library(&id.library); + let mut android = Vec::new(); + let mut desktop = Vec::new(); + for nb in &ctx.binaries.binaries { + let filename = ctx.binaries.bundled_filename(nb.platform); + if let Some(abi) = nb.platform.android_abi() { + android.push(format!("- `src/main/jniLibs/{abi}/{filename}`")); + } else if nb.platform.is_desktop() { + desktop.push(format!( + "- `src/main/resources/natives/{}/{filename}`", + nb.platform.id() + )); + } + } + let list = |v: Vec| { + if v.is_empty() { + "- (none bundled)".to_string() + } else { + v.join("\n") + } + }; + let body = format!( + r#"# {name} (Kotlin) + +Kotlin bindings for `{name}` in the `{package}` package: the Kotlin sources +under `src/main/kotlin/`, the JNI shim and the C header it compiles against +under `src/main/cpp/`, and the prebuilt producer libraries. + +## Android + +The Android Gradle plugin builds `lib{jni}.so` for each ABI against the +bundled `src/main/jniLibs//lib{lib}.so` and packages both into the AAR. +Publish it with `./gradlew publishToMavenLocal`. + +## Desktop JVM + +Build the shim once per platform against the bundled producer library and +place it beside that library: + +```bash +cmake -S src/main/cpp -B build -D{upper}_LIBRARY_DIR=src/main/resources/natives/ +cmake --build build +cp build/lib{jni}.* src/main/resources/natives// +``` + +At run time the bindings load both libraries from `natives/-/` on +the classpath, falling back to `java.library.path`. + +## Bundled Android ABIs + +{android} + +## Bundled desktop platforms + +{desktop} +"#, + name = id.name, + lib = id.library, + upper = id.macro_prefix(), + android = list(android), + desktop = list(desktop), + ); + format!( + "{}{body}\n{}", + render_prelude(CommentStyle::Xml, input_basename), + render_trailer(CommentStyle::Xml, "README.md") + ) +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/runtime.rs b/crates/weaveffi-gen/src/targets/kotlin/runtime.rs new file mode 100644 index 00000000..cc1c5edd --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/runtime.rs @@ -0,0 +1,90 @@ +//! The fixed runtime sources under `runtime/`, spliced into the output with +//! `{{PLACEHOLDER}}` substitution: the Kotlin runtime (`FfiException`, +//! `NativeIterator`, the loader, `NativeHandle`, the phantom-reference +//! cleaner), the value-buffer codec, the coroutine bridge, and the JNI shim's +//! helpers. + +use crate::cabi::ABI_VERSION; +use weaveffi_model::model::BindingModel; + +use crate::targets::kotlin::names::Names; + +const RUNTIME_KT: &str = include_str!("runtime/Runtime.kt"); +const BUFFERS_KT: &str = include_str!("runtime/Buffers.kt"); +const ASYNC_KT: &str = include_str!("runtime/Async.kt"); +const JNI_CORE_C: &str = include_str!("runtime/jni_core.c"); +const JNI_THREADS_C: &str = include_str!("runtime/jni_threads.c"); +const JNI_ASYNC_C: &str = include_str!("runtime/jni_async.c"); +const JNI_CALLBACKS_C: &str = include_str!("runtime/jni_callbacks.c"); + +/// Replace every `{{KEY}}` in `template` with its value. +fn fill(template: &str, vars: &[(&str, &str)]) -> String { + let mut out = template.to_string(); + for (key, value) in vars { + out = out.replace(&format!("{{{{{key}}}}}"), value); + } + debug_assert!( + !out.contains("{{"), + "unfilled placeholder in a runtime template" + ); + out +} + +/// The JNI shim library the Kotlin side loads: `{library}_jni`. +pub(crate) fn jni_library(library: &str) -> String { + format!("{library}_jni") +} + +/// `Runtime.kt`: always emitted. +pub(crate) fn runtime_kt(n: &Names, env_var: &str) -> String { + fill( + RUNTIME_KT, + &[ + ("PACKAGE", &n.package), + ("LIBRARY", &n.library), + ("JNI_LIBRARY", &jni_library(&n.library)), + ("LIBRARY_ENV", env_var), + ], + ) +} + +/// `Buffers.kt`: emitted when any value crosses as a value buffer. +pub(crate) fn buffers_kt(n: &Names) -> String { + fill(BUFFERS_KT, &[("PACKAGE", &n.package)]) +} + +/// `Async.kt`: emitted when any callable is async. +pub(crate) fn async_kt(n: &Names) -> String { + fill(ASYNC_KT, &[("PACKAGE", &n.package)]) +} + +/// The fixed head of the JNI shim: the core helpers and `JNI_OnLoad`, plus +/// the thread, async, and callback support the model needs. +pub(crate) fn jni_runtime(n: &Names, model: &BindingModel, header: &str, name: &str) -> String { + let abi = ABI_VERSION.to_string(); + let jni_class = n.jni_class(); + let macro_prefix = n.prefix.to_ascii_uppercase(); + let vars: [(&str, &str); 8] = [ + ("PREFIX", &n.prefix), + ("MACRO_PREFIX", ¯o_prefix), + ("HEADER", header), + ("ABI_VERSION", &abi), + ("NAME", name), + ("PACKAGE_PATH", &n.package_path), + ("JNI_CLASS", &jni_class), + ("LIBRARY", &n.library), + ]; + let mut out = fill(JNI_CORE_C, &vars); + let has_async = model.has_async(); + let has_callbacks = model.has_callback_interfaces(); + if has_async || has_callbacks { + out.push_str(&fill(JNI_THREADS_C, &vars)); + } + if has_async { + out.push_str(&fill(JNI_ASYNC_C, &vars)); + } + if has_callbacks { + out.push_str(&fill(JNI_CALLBACKS_C, &vars)); + } + out +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/runtime/Async.kt b/crates/weaveffi-gen/src/targets/kotlin/runtime/Async.kt new file mode 100644 index 00000000..a7106072 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/runtime/Async.kt @@ -0,0 +1,115 @@ +package {{PACKAGE}} + +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Bridges one native async call to a suspended coroutine. The JNI shim pins + * this object with a global reference and calls exactly one `on*` method, + * from whichever producer thread completes the call. + */ +internal class NativeCompletion( + private val cont: CancellableContinuation, + private val domain: Int, + private val convert: (Any?) -> T, + cancellable: Boolean, +) { + // The native cancel token, or 0 when the call isn't cancellable or has + // completed. Guarded by `this` so a late cancel never touches a freed + // token; a completion that lands inside `cancel` (on the same thread) + // defers the release until the cancel returns. + private var token: Long = if (cancellable) JniBridge.cancel_token_create() else 0L + private var cancelling = false + private var released = false + + val cancelToken: Long + get() = synchronized(this) { token } + + fun cancel() { + synchronized(this) { + if (token == 0L) return + cancelling = true + try { + JniBridge.cancel_token_cancel(token) + } finally { + cancelling = false + } + if (released) destroyToken() + } + } + + fun releaseToken() { + synchronized(this) { + if (cancelling) released = true else destroyToken() + } + } + + private fun destroyToken() { + if (token != 0L) JniBridge.cancel_token_destroy(token) + token = 0L + } + + fun onUnit() = succeed(Unit) + + fun onBoolean(value: Boolean) = succeed(value) + + fun onByte(value: Byte) = succeed(value) + + fun onShort(value: Short) = succeed(value) + + fun onInt(value: Int) = succeed(value) + + fun onLong(value: Long) = succeed(value) + + fun onFloat(value: Float) = succeed(value) + + fun onDouble(value: Double) = succeed(value) + + fun onBytes(value: ByteArray) = succeed(value) + + fun onError(code: Int, message: ByteArray, payload: ByteArray?) { + releaseToken() + val error = if (code == -5) { + CancellationException(decodeUtf8(message)) + } else { + JniBridge.error(domain, code, message, payload) + } + cont.resumeWithException(error) + } + + private fun succeed(raw: Any?) { + releaseToken() + val result = try { + convert(raw) + } catch (t: Throwable) { + cont.resumeWithException(t) + return + } + cont.resume(result) + } +} + +/** + * Starts a native async call and suspends until it completes. [launch] gets + * the cancel token (0 unless [cancellable]) and the completion to hand to the + * shim. Cancelling the coroutine cancels the native call; a cancelled call + * (code -5) resumes with [CancellationException]. + */ +internal suspend fun awaitNative( + cancellable: Boolean, + domain: Int, + convert: (Any?) -> T, + launch: (Long, NativeCompletion) -> Unit, +): T = suspendCancellableCoroutine { cont -> + val completion = NativeCompletion(cont, domain, convert, cancellable) + cont.invokeOnCancellation { completion.cancel() } + try { + launch(completion.cancelToken, completion) + } catch (t: Throwable) { + completion.releaseToken() + throw t + } +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/runtime/Buffers.kt b/crates/weaveffi-gen/src/targets/kotlin/runtime/Buffers.kt new file mode 100644 index 00000000..bb8f310c --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/runtime/Buffers.kt @@ -0,0 +1,246 @@ +package {{PACKAGE}} + +/** + * Growable little-endian writer for the value-buffer wire format that + * records, rich enums, optionals, lists, and maps cross the ABI in. + */ +internal class BufferWriter { + private var buf = ByteArray(64) + private var len = 0 + private var minted: ArrayList Unit>>? = null + + private fun reserve(extra: Int) { + if (len + extra <= buf.size) return + var cap = buf.size * 2 + while (cap < len + extra) cap *= 2 + buf = buf.copyOf(cap) + } + + fun toByteArray(): ByteArray = buf.copyOf(len) + + fun writeBool(v: Boolean) { + reserve(1) + buf[len++] = if (v) 1 else 0 + } + + fun writeI8(v: Byte) { + reserve(1) + buf[len++] = v + } + + fun writeU8(v: Byte) = writeI8(v) + + fun writeI16(v: Short) { + reserve(2) + val b = v.toInt() + buf[len++] = b.toByte() + buf[len++] = (b shr 8).toByte() + } + + fun writeU16(v: Short) = writeI16(v) + + fun writeI32(v: Int) { + reserve(4) + buf[len++] = v.toByte() + buf[len++] = (v shr 8).toByte() + buf[len++] = (v shr 16).toByte() + buf[len++] = (v shr 24).toByte() + } + + fun writeU32(v: Long) = writeI32(v.toInt()) + + fun writeI64(v: Long) { + writeI32(v.toInt()) + writeI32((v ushr 32).toInt()) + } + + fun writeU64(v: Long) = writeI64(v) + + fun writeF32(v: Float) = writeI32(v.toRawBits()) + + fun writeF64(v: Double) = writeI64(v.toRawBits()) + + fun writeString(v: String) = writeBytes(encodeUtf8(v)) + + fun writeBytes(v: ByteArray) { + writeI32(v.size) + reserve(v.size) + v.copyInto(buf, len) + len += v.size + } + + /** + * An object token: one new strong reference the reader adopts, which + * [abandon] releases with [release] if the encoding fails. + */ + fun writeObject(token: Long, release: (Long) -> Unit) { + val list = minted ?: ArrayList Unit>>().also { minted = it } + list.add(token to release) + writeI64(token) + } + + /** Releases every object token written so far, for an encoding that failed midway. */ + fun abandon() { + minted?.forEach { (token, release) -> release(token) } + minted = null + } + + inline fun writeOptional(v: T?, write: (T) -> Unit) { + if (v == null) { + writeBool(false) + } else { + writeBool(true) + write(v) + } + } + + inline fun writeList(v: List, write: (T) -> Unit) { + writeI32(v.size) + for (e in v) write(e) + } + + inline fun writeMap(v: Map, writeKey: (K) -> Unit, writeValue: (V) -> Unit) { + writeI32(v.size) + for ((k, e) in v) { + writeKey(k) + writeValue(e) + } + } +} + +/** + * Validating little-endian reader for the value-buffer wire format. A + * truncated buffer, an invalid flag byte, a zero object token, an oversized + * length, or trailing bytes raise [FfiException] code -3. + */ +internal class BufferReader(private val buf: ByteArray) { + private var pos = 0 + + private fun malformed(detail: String): Nothing = + throw FfiException(-3, "malformed value buffer: $detail") + + private fun take(n: Int): Int { + if (n > buf.size - pos) malformed("truncated buffer") + val at = pos + pos += n + return at + } + + fun readBool(): Boolean = when (buf[take(1)].toInt()) { + 0 -> false + 1 -> true + else -> malformed("invalid bool byte") + } + + fun readI8(): Byte = buf[take(1)] + + fun readU8(): Byte = readI8() + + fun readI16(): Short { + val at = take(2) + return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() + } + + fun readU16(): Short = readI16() + + 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 readU64(): Long = readI64() + + fun readF32(): Float = Float.fromBits(readI32()) + + fun readF64(): Double = Double.fromBits(readI64()) + + private fun readLen(): Int { + val n = readI32() + if (n < 0 || n > buf.size - pos) malformed("length prefix exceeds remaining bytes") + return n + } + + fun readString(): String { + val n = readLen() + val at = take(n) + return try { + Charsets.UTF_8.newDecoder().decode(java.nio.ByteBuffer.wrap(buf, at, n)).toString() + } catch (e: java.nio.charset.CharacterCodingException) { + malformed("string is not valid UTF-8") + } + } + + fun readBytes(): ByteArray { + val n = readLen() + val at = take(n) + return buf.copyOfRange(at, at + n) + } + + /** An object token: one strong reference the caller adopts. */ + fun readObject(): Long { + val token = readI64() + if (token == 0L) malformed("null object token") + return token + } + + inline fun readOptional(read: () -> T): T? = if (readBool()) read() else null + + inline fun readList(read: () -> T): List { + val n = readCount() + val out = ArrayList(minOf(n, 4096)) + repeat(n) { out.add(read()) } + return out + } + + inline fun readMap(readKey: () -> K, readValue: () -> V): Map { + val n = readCount() + val out = LinkedHashMap(minOf(n, 4096)) + repeat(n) { + val k = readKey() + out[k] = readValue() + } + return out + } + + /** An element count; elements may encode to zero bytes, so only the sign is checked. */ + fun readCount(): Int { + val n = readI32() + if (n < 0) malformed("negative element count") + return n + } + + fun expectEnd() { + if (pos != buf.size) malformed("trailing bytes after value") + } +} + +/** Encodes one value into a fresh value buffer. */ +internal inline fun encodeBuffer(write: (BufferWriter) -> Unit): ByteArray { + val w = BufferWriter() + try { + write(w) + } catch (t: Throwable) { + w.abandon() + throw t + } + return w.toByteArray() +} + +/** Decodes one value from [bytes], rejecting malformed or trailing bytes. */ +internal inline fun decodeBuffer(bytes: ByteArray, read: (BufferReader) -> T): T { + val r = BufferReader(bytes) + val v = read(r) + r.expectEnd() + return v +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/runtime/Runtime.kt b/crates/weaveffi-gen/src/targets/kotlin/runtime/Runtime.kt new file mode 100644 index 00000000..c719a7bb --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/runtime/Runtime.kt @@ -0,0 +1,203 @@ +package {{PACKAGE}} + +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * A failure reported by the native library: a producer panic (code -2), a + * marshalling failure (-3), a callback implementation that threw (-4), or any + * code these bindings don't know. Typed error domains subclass it. + */ +open class FfiException(val code: Int, message: String) : Exception(message) + +/** + * A lazy sequence streamed from the native library, one element per step. + * The native iterator is released when the sequence is exhausted, when + * [close] is called, or once this object becomes unreachable, whichever + * comes first. + */ +class NativeIterator internal constructor( + address: Long, + release: (Long) -> Unit, + private val step: (Long) -> Any?, +) : Iterator, AutoCloseable { + private val handle = NativeCleaner.register(this, NativeHandle(address, release)) + private var lookahead: Any? = EMPTY + + override fun hasNext(): Boolean { + if (lookahead !== EMPTY) return true + if (handle.isClosed) return false + val item = handle.borrow(step) + if (item === DONE) { + close() + return false + } + lookahead = item + return true + } + + override fun next(): T { + if (!hasNext()) throw NoSuchElementException() + val item = lookahead + lookahead = EMPTY + @Suppress("UNCHECKED_CAST") + return item as T + } + + /** Releases the native iterator; safe to call more than once. */ + override fun close() = handle.close() + + internal companion object { + /** What a step function returns once the native sequence is exhausted. */ + val DONE = Any() + private val EMPTY = Any() + } +} + +/** Loads the producer library and then the JNI shim, exactly once. */ +internal object NativeLibrary { + private const val LIBRARY = "{{LIBRARY}}" + private const val JNI_LIBRARY = "{{JNI_LIBRARY}}" + + init { + val explicit = System.getenv("{{LIBRARY_ENV}}") + if (!explicit.isNullOrEmpty()) { + System.load(explicit) + } else if (!loadBundled(LIBRARY)) { + // The shim's own link dependency usually finds the producer when + // it isn't on java.library.path, so a miss here is only fatal if + // loading the shim fails too. + try { + System.loadLibrary(LIBRARY) + } catch (ignored: UnsatisfiedLinkError) { + } + } + if (!loadBundled(JNI_LIBRARY)) System.loadLibrary(JNI_LIBRARY) + } + + fun ensureLoaded() {} + + /** + * Loads `base` from the `natives/-/` classpath resources a + * packaged desktop build bundles; false when it isn't bundled. + */ + private fun loadBundled(base: String): Boolean { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + val osId = when { + os.contains("mac") || os.contains("darwin") -> "darwin" + os.contains("win") -> "windows" + else -> "linux" + } + val archId = if (arch == "aarch64" || arch == "arm64") "arm64" else "x64" + val file = when (osId) { + "darwin" -> "lib$base.dylib" + "windows" -> "$base.dll" + else -> "lib$base.so" + } + val stream = NativeLibrary::class.java.getResourceAsStream("/natives/$osId-$archId/$file") ?: return false + val dir = java.nio.file.Files.createTempDirectory("$base-natives") + val target = dir.resolve(file) + stream.use { java.nio.file.Files.copy(it, target) } + target.toFile().deleteOnExit() + dir.toFile().deleteOnExit() + System.load(target.toAbsolutePath().toString()) + return true + } +} + +/** + * One strong reference to a native object, released exactly once: by + * [close], or by [NativeCleaner] once the owning wrapper is unreachable. + * Every call borrows the address for its duration, and a close that races an + * in-flight call defers the release until the last borrow ends, so the object + * is never freed while native code is using it. + */ +internal class NativeHandle(val address: Long, private val release: (Long) -> Unit) { + // Bit 0 is the closed flag; the remaining bits count in-flight borrows. + private val state = AtomicLong(0L) + + val isClosed: Boolean get() = state.get() and 1L != 0L + + fun acquire(): Long { + while (true) { + val s = state.get() + check(s and 1L == 0L) { "native object used after close()" } + if (state.compareAndSet(s, s + 2L)) return address + } + } + + fun unacquire() { + if (state.addAndGet(-2L) == 1L) release(address) + } + + fun close() { + while (true) { + val s = state.get() + if (s and 1L != 0L) return + if (state.compareAndSet(s, s or 1L)) { + if (s == 0L) release(address) + return + } + } + } +} + +/** Runs [block] with the native address borrowed for its duration. */ +internal inline fun NativeHandle.borrow(block: (Long) -> R): R { + val address = acquire() + try { + return block(address) + } finally { + unacquire() + } +} + +/** [borrow] for an optional object: a missing object lends address 0. */ +internal inline fun NativeHandle?.borrowOrNull(block: (Long) -> R): R = + if (this == null) block(0L) else borrow(block) + +/** + * Releases the native reference of every wrapper that becomes unreachable + * without being closed. A daemon thread drains a phantom-reference queue, so + * this works on every Android API level and every JVM. + */ +internal object NativeCleaner { + private val queue = ReferenceQueue() + private val pending: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) + + private class Entry(owner: Any, val handle: NativeHandle, queue: ReferenceQueue) : + PhantomReference(owner, queue) + + init { + val thread = Thread({ drain() }, "{{PACKAGE}}-cleaner") + thread.isDaemon = true + thread.start() + } + + /** Releases [handle] once [owner] is unreachable; returns [handle]. */ + fun register(owner: Any, handle: NativeHandle): NativeHandle { + pending.add(Entry(owner, handle, queue)) + return handle + } + + private fun drain() { + while (true) { + val entry = queue.remove() as Entry + pending.remove(entry) + try { + entry.handle.close() + } catch (_: Throwable) { + } + } + } +} + +/** UTF-8 bytes of [value], for a string crossing into native code. */ +internal fun encodeUtf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8) + +/** The string a native UTF-8 byte sequence spells. */ +internal fun decodeUtf8(bytes: ByteArray): String = String(bytes, Charsets.UTF_8) diff --git a/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_async.c b/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_async.c new file mode 100644 index 00000000..f1fd9ec0 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_async.c @@ -0,0 +1,97 @@ + +/* Async completions deliver to the pinned Kotlin `NativeCompletion`. */ +static jmethodID Jni_on_unit = NULL; +static jmethodID Jni_on_boolean = NULL; +static jmethodID Jni_on_byte = NULL; +static jmethodID Jni_on_short = NULL; +static jmethodID Jni_on_int = NULL; +static jmethodID Jni_on_long = NULL; +static jmethodID Jni_on_float = NULL; +static jmethodID Jni_on_double = NULL; +static jmethodID Jni_on_bytes = NULL; +static jmethodID Jni_on_error = NULL; + +static jint Jni_load_async(JNIEnv* env) { + jclass cls = (*env)->FindClass(env, "{{PACKAGE_PATH}}/NativeCompletion"); + if (cls == NULL) { + return JNI_ERR; + } + Jni_on_unit = (*env)->GetMethodID(env, cls, "onUnit", "()V"); + Jni_on_boolean = (*env)->GetMethodID(env, cls, "onBoolean", "(Z)V"); + Jni_on_byte = (*env)->GetMethodID(env, cls, "onByte", "(B)V"); + Jni_on_short = (*env)->GetMethodID(env, cls, "onShort", "(S)V"); + Jni_on_int = (*env)->GetMethodID(env, cls, "onInt", "(I)V"); + Jni_on_long = (*env)->GetMethodID(env, cls, "onLong", "(J)V"); + Jni_on_float = (*env)->GetMethodID(env, cls, "onFloat", "(F)V"); + Jni_on_double = (*env)->GetMethodID(env, cls, "onDouble", "(D)V"); + Jni_on_bytes = (*env)->GetMethodID(env, cls, "onBytes", "([B)V"); + Jni_on_error = (*env)->GetMethodID(env, cls, "onError", "(I[B[B)V"); + if (Jni_on_unit == NULL || Jni_on_error == NULL || Jni_on_bytes == NULL) { + return JNI_ERR; + } + return JNI_OK; +} + +/* Starts a completion on the producer thread: returns the JNIEnv with a + local frame pushed, or NULL when the call failed (the failure, or the + loss of the JVM, has then been handled and `context` released). */ +static JNIEnv* Jni_complete_begin(void* context, {{PREFIX}}_error* err) { + JNIEnv* env = Jni_env(); + if (env == NULL) { + {{PREFIX}}_error_free(err); + return NULL; + } + if ((*env)->PushLocalFrame(env, 8) != 0) { + (*env)->ExceptionClear(env); + {{PREFIX}}_error_free(err); + return NULL; + } + if (err == NULL) { + return env; + } + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + {{PREFIX}}_error_free(err); + (*env)->CallVoidMethod(env, (jobject)context, Jni_on_error, code, text, payload); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + (*env)->DeleteGlobalRef(env, (jobject)context); + return NULL; +} + +/* Finishes a successful completion: reports anything the Kotlin side threw + (it has no caller to propagate to) and releases `context`. */ +static void Jni_complete_end(JNIEnv* env, void* context) { + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->PopLocalFrame(env, NULL); + (*env)->DeleteGlobalRef(env, (jobject)context); +} + +JNIEXPORT jlong JNICALL Java_{{JNI_CLASS}}_cancel_1token_1create(JNIEnv* env, jclass cls) { + (void)env; + (void)cls; + return (jlong)(intptr_t){{PREFIX}}_cancel_token_create(); +} + +JNIEXPORT void JNICALL Java_{{JNI_CLASS}}_cancel_1token_1cancel(JNIEnv* env, jclass cls, jlong token) { + (void)env; + (void)cls; + {{PREFIX}}_cancel_token_cancel(({{PREFIX}}_cancel_token*)(intptr_t)token); +} + +JNIEXPORT void JNICALL Java_{{JNI_CLASS}}_cancel_1token_1destroy(JNIEnv* env, jclass cls, jlong token) { + (void)env; + (void)cls; + {{PREFIX}}_cancel_token_destroy(({{PREFIX}}_cancel_token*)(intptr_t)token); +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_callbacks.c b/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_callbacks.c new file mode 100644 index 00000000..b834fe86 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_callbacks.c @@ -0,0 +1,63 @@ + +/* Callback interfaces: the producer holds a global reference to the Kotlin + implementation as `ctx`; trampolines call the matching static dispatch + method on JniBridge from whatever thread the producer uses. */ +static jmethodID Jni_describe = NULL; + +static jint Jni_load_callbacks(JNIEnv* env) { + Jni_describe = (*env)->GetStaticMethodID(env, Jni_bridge, "describe", "(Ljava/lang/Throwable;)[B"); + return Jni_describe != NULL ? JNI_OK : JNI_ERR; +} + +/* Enters a trampoline: the JNIEnv with a local frame pushed, or NULL after + reporting the failure through `out_err`. */ +static JNIEnv* Jni_callback_begin({{PREFIX}}_error* out_err) { + JNIEnv* env = Jni_env(); + if (env == NULL) { + {{PREFIX}}_error_set(out_err, -4, "could not attach the calling thread to the JVM"); + return NULL; + } + if ((*env)->PushLocalFrame(env, 16) != 0) { + (*env)->ExceptionClear(env); + {{PREFIX}}_error_set(out_err, -4, "JNI local frame exhausted"); + return NULL; + } + return env; +} + +/* Leaves a trampoline. When the implementation threw, reports its + `toString()` through `out_err` with code -4 and returns 1; nothing ever + unwinds through the producer's frames. */ +static int Jni_callback_end(JNIEnv* env, {{PREFIX}}_error* out_err) { + int failed = 0; + if ((*env)->ExceptionCheck(env)) { + failed = 1; + jthrowable ex = (*env)->ExceptionOccurred(env); + (*env)->ExceptionClear(env); + jbyteArray text = (jbyteArray)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_describe, ex); + char* message = NULL; + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + } else if (text != NULL) { + jsize len = (*env)->GetArrayLength(env, text); + message = (char*)malloc((size_t)len + 1u); + if (message != NULL) { + (*env)->GetByteArrayRegion(env, text, 0, len, (jbyte*)message); + message[len] = '\0'; + } + } + {{PREFIX}}_error_set(out_err, -4, message != NULL ? message : "callback implementation threw"); + free(message); + } + (*env)->PopLocalFrame(env, NULL); + return failed; +} + +/* The vtable `free` entry: drops the producer's reference to the Kotlin + implementation. */ +static void Jni_release_callback(void* ctx) { + JNIEnv* env = Jni_env(); + if (env != NULL && ctx != NULL) { + (*env)->DeleteGlobalRef(env, (jobject)ctx); + } +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_core.c b/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_core.c new file mode 100644 index 00000000..e7cc4a3e --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_core.c @@ -0,0 +1,128 @@ +#include +#include +#include +#include +#include +#include + +/* The shim forwards deprecated functions too, so it doesn't warn about them. */ +#define {{MACRO_PREFIX}}_DEPRECATED(msg) +#include "{{HEADER}}" + +/* Every static name in this file starts with `Jni_`, which no C ABI symbol + (all lowercase `{{PREFIX}}_...`) and no JNI export (`Java_...`) can + spell. */ + +static JavaVM* Jni_vm = NULL; +static jclass Jni_bridge = NULL; +static jmethodID Jni_error = NULL; + +/* A borrowed view of a Kotlin `ByteArray`: strings, bytes, and value + buffers all cross as one. */ +typedef struct { + jbyteArray array; + jbyte* elems; + const uint8_t* ptr; + size_t len; +} Jni_bytes; + +static inline Jni_bytes Jni_borrow_bytes(JNIEnv* env, jbyteArray array) { + Jni_bytes b = {array, NULL, NULL, 0}; + if (array == NULL) { + return b; + } + b.len = (size_t)(*env)->GetArrayLength(env, array); + b.elems = (*env)->GetByteArrayElements(env, array, NULL); + b.ptr = (const uint8_t*)b.elems; + if (b.elems == NULL) { + b.len = 0; + } + return b; +} + +static inline void Jni_release_bytes(JNIEnv* env, Jni_bytes* b) { + if (b->elems != NULL) { + (*env)->ReleaseByteArrayElements(env, b->array, b->elems, JNI_ABORT); + } +} + +/* Copies `len` bytes into a new Kotlin `ByteArray` (never NULL for valid + input, so NULL + 0 is the empty array). */ +static inline jbyteArray Jni_new_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = (*env)->NewByteArray(env, (jsize)len); + if (out != NULL && len > 0 && ptr != NULL) { + (*env)->SetByteArrayRegion(env, out, 0, (jsize)len, (const jbyte*)ptr); + } + return out; +} + +/* Copies a producer-owned allocation into a `ByteArray`, then frees it. */ +static inline jbyteArray Jni_take_bytes(JNIEnv* env, const uint8_t* ptr, size_t len) { + jbyteArray out = Jni_new_bytes(env, ptr, len); + if (ptr != NULL) { + {{PREFIX}}_free_bytes((uint8_t*)ptr, len); + } + return out; +} + +/* Throws the Kotlin exception for `err` (mapped through domain `domain`, + 0 for the generic one) and clears `err`. */ +static inline void Jni_throw(JNIEnv* env, {{PREFIX}}_error* err, jint domain) { + const char* message = err->message != NULL ? err->message : ""; + jbyteArray text = Jni_new_bytes(env, (const uint8_t*)message, strlen(message)); + jbyteArray payload = NULL; + if (err->payload_ptr != NULL) { + payload = Jni_new_bytes(env, err->payload_ptr, err->payload_len); + } + jint code = (jint)err->code; + {{PREFIX}}_error_clear(err); + if ((*env)->ExceptionCheck(env)) { + return; + } + jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, Jni_bridge, Jni_error, domain, code, text, payload); + if (ex != NULL && !(*env)->ExceptionCheck(env)) { + (*env)->Throw(env, ex); + } +} + +static jint Jni_load_error(JNIEnv* env, const char* message) { + jclass cls = (*env)->FindClass(env, "java/lang/UnsatisfiedLinkError"); + if (cls != NULL) { + (*env)->ThrowNew(env, cls, message); + } + return JNI_ERR; +} + +/* Generated below: the contract checks and the method IDs the shim caches. */ +static jint Jni_load(JNIEnv* env); + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env = NULL; + (void)reserved; + Jni_vm = vm; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + if ({{PREFIX}}_abi_version() != {{ABI_VERSION}}u) { + return Jni_load_error(env, "{{NAME}}: the native library implements a different C ABI revision than these bindings (expected {{ABI_VERSION}})"); + } + jclass bridge = (*env)->FindClass(env, "{{PACKAGE_PATH}}/JniBridge"); + if (bridge == NULL) { + return JNI_ERR; + } + Jni_bridge = (jclass)(*env)->NewGlobalRef(env, bridge); + Jni_error = (*env)->GetStaticMethodID(env, Jni_bridge, "error", "(II[B[B)L{{PACKAGE_PATH}}/FfiException;"); + if (Jni_error == NULL) { + return JNI_ERR; + } + if (Jni_load(env) != JNI_OK) { + return JNI_ERR; + } + return JNI_VERSION_1_6; +} + +JNIEXPORT jlong JNICALL Java_{{JNI_CLASS}}_debug_1live(JNIEnv* env, jclass cls, jint kind) { + (void)env; + (void)cls; + return (jlong){{PREFIX}}_debug_live((int32_t)kind); +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_threads.c b/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_threads.c new file mode 100644 index 00000000..84cd06c0 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/runtime/jni_threads.c @@ -0,0 +1,49 @@ + +/* Producer threads (async completions, callback calls, vtable `free`) are + usually not JVM threads. The first time one calls in it attaches as a + daemon and keeps its JNIEnv; a thread-local destructor detaches it when + the thread exits, so a busy producer thread pays for one attach, not one + per call. */ +#if defined(_WIN32) +static void Jni_remember_attach(void) {} +#else +#include + +static pthread_key_t Jni_env_key; +static pthread_once_t Jni_env_once = PTHREAD_ONCE_INIT; + +static void Jni_detach(void* env) { + (void)env; + if (Jni_vm != NULL) { + (*Jni_vm)->DetachCurrentThread(Jni_vm); + } +} + +static void Jni_make_env_key(void) { + pthread_key_create(&Jni_env_key, Jni_detach); +} + +static void Jni_remember_attach(void) { + pthread_once(&Jni_env_once, Jni_make_env_key); + pthread_setspecific(Jni_env_key, (void*)Jni_vm); +} +#endif + +/* The calling thread's JNIEnv, attaching the thread if needed; NULL when + the JVM is unavailable. */ +static JNIEnv* Jni_env(void) { + JNIEnv* env = NULL; + if (Jni_vm == NULL) { + return NULL; + } + if ((*Jni_vm)->GetEnv(Jni_vm, (void**)&env, JNI_VERSION_1_6) == JNI_OK) { + return env; + } + /* `void*` converts to the `JNIEnv**` Android declares and the `void**` + the JDK declares. */ + if ((*Jni_vm)->AttachCurrentThreadAsDaemon(Jni_vm, (void*)&env, NULL) != JNI_OK) { + return NULL; + } + Jni_remember_attach(); + return env; +} diff --git a/crates/weaveffi-gen/src/targets/kotlin/tests.rs b/crates/weaveffi-gen/src/targets/kotlin/tests.rs new file mode 100644 index 00000000..258a9244 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/kotlin/tests.rs @@ -0,0 +1,384 @@ +//! Unit tests rendering a small IR through the generator: identity-driven +//! names and layout, configuration overrides, name escaping, the ABI 3 JNI +//! surface (ptr+len strings, load-time checks, cancellation), object +//! lifetime, callbacks, iterators, and packaging. + +use camino::{Utf8Path, Utf8PathBuf}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; + +use crate::backend::LanguageBackend; +use crate::package::{FileContent, PackageContext}; +use crate::platform::{BinarySet, NativeBinary, Platform}; +use crate::targets::kotlin::{KotlinConfig, KotlinFlavor, KotlinGenerator}; + +const FIXTURE: &str = r#" +version: "0.10.0" +modules: + - name: bus + enums: + - name: Level + variants: + - { name: Low, value: 0 } + - { name: High, value: 1 } + structs: + - name: Event + fields: + - { name: id, type: i64 } + - { name: source, type: Store } + callback_interfaces: + - name: Subscriber + methods: + - name: on_event + params: + - { name: event, type: Event } + - { name: note, type: string } + return: Level + errors: + name: BusError + codes: + - { name: Closed, code: 1, message: "Bus is closed" } + interfaces: + - name: Store + constructors: + - name: new + params: + - { name: name, type: string } + methods: + - name: get + params: + - { name: key, type: string } + return: "string?" + throws: true + - name: larger + params: + - { name: other, type: "Store?" } + return: "Store?" + - name: keys + params: [] + return: "iter" + - name: counts + params: [] + return: "iter" + - name: subscribe + params: + - { name: sub, type: Subscriber } + - name: drain + params: [] + return: i64 + async: true + cancellable: true + throws: true + functions: + - name: get + params: + - { name: id, type: i64 } + return: Event + modules: + - name: stats + structs: + - name: Stats + fields: + - { name: total, type: i64 } + functions: + - name: get + params: + - { name: store, type: Store } + return: Stats + - name: units + enums: + - name: Unit + variants: + - { name: Px, value: 0 } + functions: + - name: echo + params: + - { name: unit, type: Unit } + return: Unit +"#; + +fn api() -> ResolvedApi { + let api = weaveffi_model::parse::parse_api_str(FIXTURE, "yaml").expect("fixture parses"); + weaveffi_model::validate::validate_api(api, None) + .expect("fixture validates") + .with_identity(Identity::named("event-bus")) +} + +/// Render with `config` and return `(path, contents)` pairs, paths with `/`. +fn render(config: &KotlinConfig) -> Vec<(String, String)> { + let api = api(); + let model = BindingModel::build(&api); + KotlinGenerator + .files(&api, &model, Utf8Path::new("out"), config) + .into_iter() + .map(|f| (f.path.as_str().replace('\\', "/"), f.contents)) + .collect() +} + +fn file<'a>(files: &'a [(String, String)], suffix: &str) -> &'a str { + files + .iter() + .find(|(p, _)| p.ends_with(suffix)) + .map(|(_, c)| c.as_str()) + .unwrap_or_else(|| { + let paths: Vec<&String> = files.iter().map(|f| &f.0).collect(); + panic!("{suffix} missing from {paths:?}") + }) +} + +#[test] +fn layout_follows_the_identity() { + let files = render(&KotlinConfig::default()); + let paths: Vec<&str> = files.iter().map(|(p, _)| p.as_str()).collect(); + assert_eq!( + paths, + [ + "out/kotlin/settings.gradle.kts", + "out/kotlin/build.gradle.kts", + "out/kotlin/consumer-rules.pro", + "out/kotlin/src/main/cpp/CMakeLists.txt", + "out/kotlin/src/main/cpp/event_bus.h", + "out/kotlin/src/main/cpp/event_bus_jni.c", + "out/kotlin/src/main/kotlin/event_bus/Runtime.kt", + "out/kotlin/src/main/kotlin/event_bus/Buffers.kt", + "out/kotlin/src/main/kotlin/event_bus/Async.kt", + "out/kotlin/src/main/kotlin/event_bus/JniBridge.kt", + "out/kotlin/src/main/kotlin/event_bus/Bus.kt", + "out/kotlin/src/main/kotlin/event_bus/Units.kt", + ] + ); + let runtime = file(&files, "Runtime.kt"); + assert!(runtime.contains("package event_bus\n")); + assert!(runtime.contains("private const val LIBRARY = \"event_bus\"")); + assert!(runtime.contains("private const val JNI_LIBRARY = \"event_bus_jni\"")); + assert!(runtime.contains("System.getenv(\"EVENT_BUS_LIBRARY\")")); + let settings = file(&files, "settings.gradle.kts"); + assert!(settings.contains("rootProject.name = \"event-bus\"")); + assert!(settings.contains("google()")); + let gradle = file(&files, "build.gradle.kts"); + assert!(gradle.contains("id(\"com.android.library\")")); + assert!(gradle.contains("namespace = \"event_bus\"")); + assert!(gradle.contains("minSdk = 21")); + assert!(gradle.contains("`maven-publish`")); + assert!(gradle.contains("kotlinx-coroutines-core")); + let cmake = file(&files, "CMakeLists.txt"); + assert!(cmake.contains("add_library(event_bus_jni SHARED event_bus_jni.c)")); + assert!(cmake.contains("find_library(EVENT_BUS_PRODUCER NAMES event_bus")); + assert!(cmake.contains("../jniLibs/${ANDROID_ABI}")); + // The bundled header is the C target's, verbatim. + for (path, contents) in files.iter().filter(|(p, _)| !p.ends_with(".h")) { + let body: String = contents.lines().skip(3).collect::>().join("\n"); + assert!( + !body.contains("WeaveFFI") && !body.contains("weaveffi"), + "{path} carries WeaveFFI branding" + ); + } +} + +#[test] +fn configuration_overrides_package_and_flavor() { + let config = KotlinConfig { + package: Some("com.example.bus".into()), + flavor: KotlinFlavor::Jvm, + ..KotlinConfig::default() + }; + let files = render(&config); + assert!(files + .iter() + .all(|(p, _)| !p.ends_with("consumer-rules.pro"))); + assert!(files + .iter() + .any(|(p, _)| p == "out/kotlin/src/main/kotlin/com/example/bus/JniBridge.kt")); + let gradle = file(&files, "build.gradle.kts"); + assert!(gradle.contains("kotlin(\"jvm\")")); + assert!(gradle.contains("group = \"com.example.bus\"")); + assert!(!gradle.contains("com.android.library")); + let c = file(&files, "event_bus_jni.c"); + assert!(c.contains("Java_com_example_bus_JniBridge_bus_1Store_1get(")); + assert!(c.contains("FindClass(env, \"com/example/bus/JniBridge\")")); +} + +#[test] +fn modules_are_objects_and_shadowing_names_are_escaped() { + let files = render(&KotlinConfig::default()); + let bus = file(&files, "Bus.kt"); + // Two modules each declare `get`; the module objects keep them apart, + // and the `stats` object steps aside for the `Stats` record. + assert!(bus.contains("object Bus {")); + assert!(bus.contains(" fun get(id: Long): Event = ")); + assert!(bus.contains(" object StatsModule {")); + assert!(bus.contains("fun get(store: Store): Stats = store.handle.borrow { _h0 ->")); + // A user type named like a Kotlin builtin gains a trailing underscore. + let units = file(&files, "Units.kt"); + assert!(units.contains("enum class Unit_(val value: Int) {")); + assert!(units.contains( + "fun echo(unit: Unit_): Unit_ = Unit_.fromValue(JniBridge.units_echo(unit.value))" + )); + // JNI natives are named after their (unique) C symbols. + let bridge = file(&files, "JniBridge.kt"); + assert!(bridge.contains("@JvmStatic external fun bus_get(id: Long): ByteArray")); + assert!(bridge.contains("@JvmStatic external fun bus_stats_get(store: Long): ByteArray")); +} + +#[test] +fn strings_cross_as_bytes_with_lengths() { + let files = render(&KotlinConfig::default()); + let bus = file(&files, "Bus.kt"); + assert!(bus.contains( + "fun get(key: String): String? = handle.borrow { _self ->\n decodeBuffer(JniBridge.bus_Store_get(_self, encodeUtf8(key))) { _r -> _r.readOptional { _r.readString() } }" + )); + let c = file(&files, "event_bus_jni.c"); + assert!(c.contains("Jni_bytes p_key_b = Jni_borrow_bytes(env, p_key);")); + assert!(c.contains( + "event_bus_bus_Store_get((const event_bus_bus_Store*)(intptr_t)self, p_key_b.ptr, p_key_b.len, &out_len, &err);" + )); + assert!(c.contains("return Jni_take_bytes(env, rv, out_len);")); + assert!(c.contains("event_bus_free_bytes((uint8_t*)ptr, len);")); + assert!(!c.contains("free_string")); + assert!(!c.contains("GetStringUTFChars")); +} + +#[test] +fn load_checks_the_abi_revision_and_every_root_checksum() { + let api = api(); + let model = BindingModel::build(&api); + let files = render(&KotlinConfig::default()); + let c = file(&files, "event_bus_jni.c"); + assert!(c.contains("if (event_bus_abi_version() != 3u) {")); + for root in model.roots() { + let sum = root.checksum.expect("roots carry a checksum"); + assert!(c.contains(&format!( + "if (event_bus_{}_checksum() != UINT64_C(0x{sum:016x})) {{", + root.name + ))); + assert!(c.contains(&format!( + "module '{}' does not match these bindings", + root.name + ))); + } +} + +#[test] +fn objects_are_borrowed_for_every_call() { + let files = render(&KotlinConfig::default()); + let bus = file(&files, "Bus.kt"); + assert!(bus.contains("class Store private constructor(address: Long) : AutoCloseable {")); + assert!(bus.contains( + "internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::bus_Store_destroy))" + )); + assert!(bus.contains( + "fun larger(other: Store?): Store? = handle.borrow { _self ->\n other?.handle.borrowOrNull { _h0 ->\n Store.fromHandleOrNull(JniBridge.bus_Store_larger(_self, _h0))" + )); + // Objects inside buffers are cloned on the way in and released again if + // the encoding fails. + assert!(bus.contains("_w.writeObject(_v.source.cloneHandle(), JniBridge::bus_Store_destroy)")); + let runtime = file(&files, "Runtime.kt"); + assert!(runtime.contains("PhantomReference")); + assert!(!runtime.contains("java.lang.ref.Cleaner")); +} + +#[test] +fn cancellable_async_wires_the_token() { + let files = render(&KotlinConfig::default()); + let bus = file(&files, "Bus.kt"); + assert!(bus.contains( + "suspend fun drain(): Long = awaitNative(true, 1, { it as Long }) { _token, _done ->" + )); + assert!(bus.contains("JniBridge.bus_Store_drain(_self, _token, _done)")); + let runtime = file(&files, "Async.kt"); + assert!(runtime.contains("cont.invokeOnCancellation { completion.cancel() }")); + assert!(runtime.contains("if (code == -5)")); + let c = file(&files, "event_bus_jni.c"); + assert!(c.contains( + "(event_bus_cancel_token*)(intptr_t)cancel_token, Jni_done_bus_Store_drain, context);" + )); + assert!(c.contains("CallVoidMethod(env, (jobject)context, Jni_on_long, (jlong)result);")); +} + +#[test] +fn callbacks_dispatch_through_cached_static_shims() { + let files = render(&KotlinConfig::default()); + let bridge = file(&files, "JniBridge.kt"); + assert!(bridge.contains( + "fun bus_Subscriber_on_event(_impl: Subscriber, event: ByteArray, note: ByteArray): Int = _impl.onEvent(decodeBuffer(event) { _r -> unpackEvent(_r) }, decodeUtf8(note)).value" + )); + let c = file(&files, "event_bus_jni.c"); + assert!(c.contains( + "GetStaticMethodID(env, Jni_bridge, \"bus_Subscriber_on_event\", \"(Levent_bus/Subscriber;[B[B)I\");" + )); + assert!(c.contains("pthread_key_create(&Jni_env_key, Jni_detach);")); + assert!(c.contains("AttachCurrentThreadAsDaemon")); + assert!(c.contains("event_bus_error_set(out_err, -4,")); + assert!(c.contains( + "static const event_bus_bus_Subscriber_vtable Jni_bus_Subscriber_vtable = {Jni_bus_Subscriber_on_event, Jni_release_callback};" + )); +} + +#[test] +fn iterators_stream_through_native_iterator() { + let files = render(&KotlinConfig::default()); + let bus = file(&files, "Bus.kt"); + assert!(bus.contains( + "fun keys(): NativeIterator = handle.borrow { _self ->\n NativeIterator(JniBridge.bus_Store_keys(_self), JniBridge::bus_Store_KeysIterator_destroy) { _it ->" + )); + assert!(bus.contains( + "if (JniBridge.bus_Store_CountsIterator_next(_it, _slot)) _slot[0] else NativeIterator.DONE" + )); + let c = file(&files, "event_bus_jni.c"); + assert!(c.contains("(*env)->SetIntArrayRegion(env, out, 0, 1, &value);")); +} + +#[test] +fn package_bundles_prebuilt_binaries() { + let api = api(); + let model = BindingModel::build(&api); + let mut binaries = BinarySet::new("event_bus"); + binaries.binaries.push(NativeBinary { + platform: Platform::AndroidArm64, + source: Utf8PathBuf::from("/prebuilt/android-arm64/libevent_bus.so"), + }); + binaries.binaries.push(NativeBinary { + platform: Platform::MacosArm64, + source: Utf8PathBuf::from("/prebuilt/darwin-arm64/libevent_bus.dylib"), + }); + binaries.binaries.push(NativeBinary { + platform: Platform::Wasm32, + source: Utf8PathBuf::from("/prebuilt/wasm32/event_bus.wasm"), + }); + let ctx = PackageContext { + binaries: &binaries, + input_basename: Some("bus.yml"), + }; + let files = KotlinGenerator + .package( + &api, + &model, + &ctx, + Utf8Path::new("out"), + &KotlinConfig::default(), + ) + .expect("kotlin supports packaging"); + let paths: Vec = files + .iter() + .map(|f| f.path.as_str().replace('\\', "/")) + .collect(); + for expected in [ + "out/kotlin/README.md", + "out/kotlin/src/main/jniLibs/arm64-v8a/libevent_bus.so", + "out/kotlin/src/main/resources/natives/darwin-arm64/libevent_bus.dylib", + ] { + assert!(paths.iter().any(|p| p == expected), "{expected} missing"); + } + assert!(!paths.iter().any(|p| p.contains("wasm"))); + let readme = files + .iter() + .find(|f| f.path.as_str().ends_with("README.md")) + .map(|f| match &f.content { + FileContent::Text(t) => t.clone(), + FileContent::Copy(_) => panic!("README is text"), + }) + .expect("README"); + assert!(readme.contains("- `src/main/jniLibs/arm64-v8a/libevent_bus.so`")); + assert!(readme.contains("-DEVENT_BUS_LIBRARY_DIR=")); +} diff --git a/crates/weaveffi-gen/src/targets/mod.rs b/crates/weaveffi-gen/src/targets/mod.rs new file mode 100644 index 00000000..f6f16a1c --- /dev/null +++ b/crates/weaveffi-gen/src/targets/mod.rs @@ -0,0 +1,72 @@ +//! The language generators, one module per target. Each implements +//! [`LanguageBackend`](crate::backend::LanguageBackend) and exposes its +//! generator and configuration types (for example +//! [`swift::SwiftGenerator`] and [`swift::SwiftConfig`]). + +pub mod c; +pub mod cpp; +pub mod dart; +pub mod dotnet; +pub mod go; +pub(crate) mod js; +pub mod kotlin; +pub mod node; +pub mod python; +pub mod ruby; +pub mod swift; +pub mod wasm; + +use crate::codegen::{ConfiguredBackend, Target}; + +/// Every target's name, in canonical order (the `--target` tokens). +pub const NAMES: [&str; 11] = [ + "c", "cpp", "swift", "kotlin", "node", "wasm", "python", "dotnet", "dart", "go", "ruby", +]; + +/// Every target with its default configuration, in [`NAMES`] order. +#[must_use] +pub fn all_default() -> Vec> { + vec![ + Box::new(ConfiguredBackend::new(c::CGenerator, c::CConfig::default())), + Box::new(ConfiguredBackend::new( + cpp::CppGenerator, + cpp::CppConfig::default(), + )), + Box::new(ConfiguredBackend::new( + swift::SwiftGenerator, + swift::SwiftConfig::default(), + )), + Box::new(ConfiguredBackend::new( + kotlin::KotlinGenerator, + kotlin::KotlinConfig::default(), + )), + Box::new(ConfiguredBackend::new( + node::NodeGenerator, + node::NodeConfig::default(), + )), + Box::new(ConfiguredBackend::new( + wasm::WasmGenerator, + wasm::WasmConfig::default(), + )), + Box::new(ConfiguredBackend::new( + python::PythonGenerator, + python::PythonConfig::default(), + )), + Box::new(ConfiguredBackend::new( + dotnet::DotnetGenerator, + dotnet::DotnetConfig::default(), + )), + Box::new(ConfiguredBackend::new( + dart::DartGenerator, + dart::DartConfig::default(), + )), + Box::new(ConfiguredBackend::new( + go::GoGenerator, + go::GoConfig::default(), + )), + Box::new(ConfiguredBackend::new( + ruby::RubyGenerator, + ruby::RubyConfig::default(), + )), + ] +} diff --git a/crates/weaveffi-gen/src/targets/node/addon.rs b/crates/weaveffi-gen/src/targets/node/addon.rs new file mode 100644 index 00000000..36226d47 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/node/addon.rs @@ -0,0 +1,669 @@ +//! The N-API addon (`{library}_node.c`): the Node.js transport. +//! +//! The fixed part (value conversions, error reporting, the async and +//! callback-interface machinery, and the runtime symbols) is +//! `runtime/addon.c`. This module appends one exported entry point per C +//! symbol (callables, async launchers, iterator `next`/`destroy`, interface +//! `clone`/`destroy`, and module checksums), each following the raw calling +//! convention of the shared JavaScript layer, plus a static vtable with +//! thread-hopping trampolines per callback interface. + +use weaveffi_model::model::{ + iterator_item_ctype, BindingModel, CallShape, CallbackInterfaceBinding, FnBinding, Prim, Ty, +}; +use weaveffi_model::plan::{self, ArgPass, RetPass}; + +use crate::codegen::CodeWriter; +use crate::targets::js::names::direct_prim; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; + +/// The fixed addon runtime, with `{{PREFIX}}` (the C prefix), `{{MACRO}}` +/// (its uppercase form), and `{{HEADER}}` (the header file) placeholders. +const ADDON_C: &str = include_str!("runtime/addon.c"); + +/// The `js_arg_*` reader and C type of a direct primitive's temporary. +fn arg_reader(p: Prim) -> (&'static str, &'static str) { + match p { + Prim::Bool => ("js_arg_bool", "bool"), + Prim::I8 => ("js_arg_i8", "int8_t"), + Prim::I16 => ("js_arg_i16", "int16_t"), + Prim::I32 => ("js_arg_i32", "int32_t"), + Prim::I64 => ("js_arg_i64", "int64_t"), + Prim::U8 => ("js_arg_u8", "uint8_t"), + Prim::U16 => ("js_arg_u16", "uint16_t"), + Prim::U32 => ("js_arg_u32", "uint32_t"), + Prim::U64 => ("js_arg_u64", "uint64_t"), + Prim::F32 => ("js_arg_f32", "float"), + Prim::F64 => ("js_arg_f64", "double"), + Prim::String | Prim::Bytes => unreachable!("not a direct primitive"), + } +} + +/// The expression creating a JavaScript value from the direct C value +/// `expr`. +fn new_direct(p: Prim, expr: &str) -> String { + match p { + Prim::Bool => format!("js_new_bool(env, {expr})"), + Prim::I8 | Prim::I16 | Prim::I32 => format!("js_new_i32(env, (int32_t){expr})"), + Prim::U8 | Prim::U16 | Prim::U32 => format!("js_new_u32(env, (uint32_t){expr})"), + Prim::I64 => format!("js_new_i64(env, {expr})"), + Prim::U64 => format!("js_new_u64(env, {expr})"), + Prim::F32 | Prim::F64 => format!("js_new_f64(env, (double){expr})"), + Prim::String | Prim::Bytes => unreachable!("not a direct primitive"), + } +} + +/// The expression converting a value received from the producer: `slots` +/// are the C expressions of its ABI slots (one, or a pointer and a length). +/// `owned` values (returns, async results, iterator elements) are released +/// after conversion; borrowed ones (callback arguments) are copied. +fn receive(ty: &Ty, pass: &RetPass, slots: &[String], owned: bool) -> String { + match pass { + RetPass::Void => "js_undefined(env)".into(), + RetPass::Direct => new_direct(direct_prim(ty), &slots[0]), + RetPass::String => format!( + "{}(env, {}, {})", + if owned { "js_take_str" } else { "js_new_str" }, + slots[0], + slots[1] + ), + RetPass::Bytes | RetPass::Buffer => format!( + "{}(env, {}, {})", + if owned { + "js_take_bytes" + } else { + "js_new_bytes" + }, + slots[0], + slots[1] + ), + RetPass::Object { .. } => format!("js_new_handle(env, {})", slots[0]), + } +} + +/// The name of the dispatcher and vtable of a callback interface by its C +/// tag. +fn cb_names(c_tag: &str) -> (String, String) { + (format!("dispatch_{c_tag}"), format!("vtable_{c_tag}")) +} + +/// Render the complete addon source. +pub(crate) fn render_addon_c( + model: &BindingModel, + header: &str, + input_basename: &str, + file_name: &str, +) -> String { + let prefix = model.prefix.as_str(); + let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); + out.push_str( + &ADDON_C + .replace("{{PREFIX}}", prefix) + .replace("{{MACRO}}", &prefix.to_ascii_uppercase()) + .replace("{{HEADER}}", header), + ); + out.push('\n'); + + let mut w = CodeWriter::two_space(); + let mut exports: Vec = Vec::new(); + + for root in model.roots() { + let symbol = weaveffi_model::model::checksum_symbol(prefix, &root.name); + w.block( + format!("static napi_value nx_{symbol}(napi_env env, napi_callback_info info) {{"), + "}", + |w| { + w.line("(void)info;"); + w.line(format!("return js_new_u64(env, {symbol}());")); + }, + ); + w.blank(); + exports.push(symbol); + } + + for (_, cb) in model.callback_interfaces() { + emit_callback_interface(&mut w, cb, prefix); + } + + for m in &model.modules { + for i in &m.interfaces { + for (symbol, body) in [ + ( + &i.clone_symbol, + format!( + "return js_new_handle(env, {}((const {}*)h));", + i.clone_symbol, i.c_tag + ), + ), + ( + &i.destroy_symbol, + format!( + "{}(({}*)h);\nreturn js_undefined(env);", + i.destroy_symbol, i.c_tag + ), + ), + ] { + emit_handle_fn(&mut w, symbol, &body); + exports.push(symbol.clone()); + } + } + for f in m.callables() { + emit_callable(&mut w, model, f, &mut exports); + } + } + + w.block("NAPI_MODULE_INIT() {", "}", |w| { + w.line("js_env* e = (js_env*)calloc(1, sizeof *e);"); + w.line("napi_set_instance_data(env, e, js_env_free, NULL);"); + w.block("napi_property_descriptor props[] = {", "};", |w| { + w.line("JS_RUNTIME_EXPORTS,"); + for symbol in &exports { + w.line(format!( + "{{\"{symbol}\", NULL, nx_{symbol}, NULL, NULL, NULL, napi_default, NULL}}," + )); + } + }); + w.line("napi_define_properties(env, exports, sizeof props / sizeof props[0], props);"); + w.line("return exports;"); + }); + out.push_str(&w.finish()); + out.push('\n'); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, file_name)); + out +} + +/// An entry point taking one handle `h` and running `body`. +fn emit_handle_fn(w: &mut CodeWriter, symbol: &str, body: &str) { + w.block( + format!("static napi_value nx_{symbol}(napi_env env, napi_callback_info info) {{"), + "}", + |w| { + w.line("JS_ARGS(1);"); + w.line("void* h = NULL;"); + w.line("if (!js_arg_handle(env, argv[0], &h, false)) return NULL;"); + w.block_raw(body); + }, + ); + w.blank(); +} + +/// The marshalled arguments of one entry point. +#[derive(Default)] +struct Args { + /// The number of JavaScript arguments. + argc: usize, + /// Local declarations, all initialized before the first read can fail. + decls: Vec, + /// Argument reads (each jumps to `done` on failure). + reads: Vec, + /// The C argument expressions, in slot order. + call: Vec, + /// Statements run after the call (callback registrations the producer + /// now owns). + transfer: Vec, + /// Statements run on every exit. + cleanup: Vec, +} + +/// Marshal the receiver, parameters, and cancel token of `f` from `argv`. +fn marshal(model: &BindingModel, f: &FnBinding, launch: &[weaveffi_model::abi::AbiParam]) -> Args { + let prefix = &model.prefix; + let mut a = Args::default(); + let mut idx = 0usize; + if f.has_self { + let ty = launch[0].ty.render_c(prefix); + a.decls.push("void* self_h = NULL;".into()); + a.reads.push(format!( + "if (!js_arg_handle(env, argv[{idx}], &self_h, false)) goto done;" + )); + a.call.push(format!("({ty})self_h")); + idx += 1; + } + for (n, p) in f.params.iter().enumerate() { + let v = format!("a{n}"); + match p.arg_pass() { + ArgPass::Direct { slot } => { + let (reader, tmp) = arg_reader(direct_prim(&p.ty)); + a.decls.push(format!("{tmp} {v} = 0;")); + a.reads + .push(format!("if (!{reader}(env, argv[{idx}], &{v})) goto done;")); + if matches!(p.ty, Ty::Enum(_)) { + a.call.push(format!("({}){v}", slot.ty.render_c(prefix))); + } else { + a.call.push(v); + } + } + ArgPass::String { .. } => { + a.decls.push(format!("js_str {v} = JS_STR_INIT;")); + a.reads.push(format!( + "if (!js_arg_str(env, argv[{idx}], &{v})) goto done;" + )); + a.call.push(format!("JS_STR_PTR({v})")); + a.call.push(format!("{v}.len")); + a.cleanup.push(format!("js_str_free(&{v});")); + } + ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + a.decls.push(format!("const uint8_t* {v} = NULL;")); + a.decls.push(format!("size_t {v}_len = 0;")); + a.reads.push(format!( + "if (!js_arg_bytes(env, argv[{idx}], &{v}, &{v}_len)) goto done;" + )); + a.call.push(v.clone()); + a.call.push(format!("{v}_len")); + } + ArgPass::Object { slot, nullable } => { + a.decls.push(format!("void* {v} = NULL;")); + a.reads.push(format!( + "if (!js_arg_handle(env, argv[{idx}], &{v}, {nullable})) goto done;" + )); + a.call.push(format!("({}){v}", slot.ty.render_c(prefix))); + } + ArgPass::Callback { .. } => { + let cb = find_callback( + model, + p.ty.callback_interface_name().expect("callback parameter"), + ); + let (dispatch, vtable) = cb_names(&cb.c_tag); + a.decls.push(format!("js_cb* {v} = NULL;")); + a.reads.push(format!( + "if (!js_arg_cb(env, argv[{idx}], \"{}\", {dispatch}, &{v})) goto done;", + cb.c_tag + )); + a.call.push(format!("(void*){v}")); + a.call.push(format!("&{vtable}")); + a.transfer.push(format!("{v} = NULL;")); + a.cleanup + .push(format!("if ({v} != NULL) js_cb_release(env, {v});")); + } + } + idx += 1; + } + if f.cancellable { + a.decls.push("void* token = NULL;".into()); + a.reads.push(format!( + "if (!js_arg_handle(env, argv[{idx}], &token, true)) goto done;" + )); + a.call.push(format!("({prefix}_cancel_token*)token")); + idx += 1; + } + a.argc = idx; + a +} + +/// The callback interface named by the absolute dotted path `dotted`. +fn find_callback<'a>(model: &'a BindingModel, dotted: &str) -> &'a CallbackInterfaceBinding { + let (module, name) = dotted.rsplit_once('.').expect("absolute type path"); + model + .modules + .iter() + .find(|m| m.dot_path == module) + .and_then(|m| m.callback_interface(name)) + .expect("callback interface exists") +} + +/// The async result kind and the statements storing the completion's +/// result slots into `a`. +fn async_result(f: &FnBinding, prefix: &str) -> (&'static str, Vec) { + let pass = plan::ret_pass(f.ret.as_ref(), prefix); + match pass { + RetPass::Void => ("JS_R_VOID", vec![]), + RetPass::Direct => { + let ty = f.ret.as_ref().expect("direct result"); + let (kind, field) = match direct_prim(ty) { + Prim::Bool => ("JS_R_BOOL", "b"), + Prim::I8 | Prim::I16 | Prim::I32 => ("JS_R_I32", "i"), + Prim::I64 => ("JS_R_I64", "i"), + Prim::U8 | Prim::U16 | Prim::U32 => ("JS_R_U32", "u"), + Prim::U64 => ("JS_R_U64", "u"), + Prim::F32 | Prim::F64 => ("JS_R_F64", "f"), + Prim::String | Prim::Bytes => unreachable!("not direct"), + }; + (kind, vec![format!("a->v.{field} = result;")]) + } + RetPass::String | RetPass::Bytes | RetPass::Buffer => ( + if pass == RetPass::String { + "JS_R_STR" + } else { + "JS_R_BYTES" + }, + vec!["a->v.p = result_ptr;".into(), "a->len = result_len;".into()], + ), + RetPass::Object { .. } => ("JS_R_HANDLE", vec!["a->v.p = result;".into()]), + } +} + +/// Emit one callable's entry point(s). +fn emit_callable( + w: &mut CodeWriter, + model: &BindingModel, + f: &FnBinding, + exports: &mut Vec, +) { + let prefix = model.prefix.as_str(); + match &f.shape { + CallShape::Async(ab) => { + let symbol = &ab.launch.symbol; + let (kind, store) = async_result(f, prefix); + let params: Vec = ab + .callback_params + .iter() + .map(|p| format!("{} {}", p.ty.render_c(prefix), p.name)) + .collect(); + w.block( + format!("static void done_{symbol}({}) {{", params.join(", ")), + "}", + |w| { + w.line("js_async* a = (js_async*)context;"); + for s in &store { + w.line(s); + } + w.line("js_async_done(a, err);"); + }, + ); + w.blank(); + let mut args = marshal(model, f, &ab.launch.params); + args.call.push(format!("done_{symbol}")); + args.call.push("a".into()); + emit_entry(w, symbol, &args, |w| { + w.line(format!( + "js_async* a = js_async_begin(env, {kind}, \"{symbol}\", &ret);" + )); + w.line(format!("{symbol}({});", args.call.join(", "))); + for t in &args.transfer { + w.line(t); + } + }); + exports.push(symbol.clone()); + } + CallShape::Sync(abi) => { + let args = marshal(model, f, &abi.params); + let pass = plan::ret_pass(f.ret.as_ref(), prefix); + let result = f.ret.as_ref().map(|ty| SyncResult { + c_type: abi.ret.render_c(prefix), + out_len: matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer), + value: receive(ty, &pass, &["r".into(), "out_len".into()], true), + }); + emit_sync(w, &abi.symbol, &args, result.as_ref()); + exports.push(abi.symbol.clone()); + } + CallShape::Iterator(it) => { + let args = marshal(model, f, &it.launch.params); + let handle = SyncResult { + c_type: it.launch.ret.render_c(prefix), + out_len: false, + value: "js_new_handle(env, r)".into(), + }; + emit_sync(w, &it.launch.symbol, &args, Some(&handle)); + exports.push(it.launch.symbol.clone()); + + let item = iterator_item_ctype(&it.elem, "").render_c(prefix); + let protocol = it.protocol(f, prefix); + let pair = matches!( + protocol.elem, + RetPass::String | RetPass::Bytes | RetPass::Buffer + ); + let symbol = &it.next.symbol; + w.block( + format!("static napi_value nx_{symbol}(napi_env env, napi_callback_info info) {{"), + "}", + |w| { + w.line("JS_ARGS(1);"); + w.line("void* h = NULL;"); + w.line("if (!js_arg_handle(env, argv[0], &h, false)) return NULL;"); + w.line(format!("{item} item;")); + w.line("memset(&item, 0, sizeof item);"); + w.line("size_t item_len = 0;"); + w.line("js_error err = {0};"); + let len = if pair { "&item_len, " } else { "" }; + w.line(format!( + "int32_t has = {symbol}(({}*)h, &item, {len}&err);", + it.iter_tag + )); + if !pair { + w.line("(void)item_len;"); + } + w.line("if (err.code != 0) return js_throw(env, &err);"); + w.line("if (has == 0) return js_undefined(env);"); + w.line(format!( + "return {};", + receive( + &it.elem, + &protocol.elem, + &["item".into(), "item_len".into()], + true + ) + )); + }, + ); + w.blank(); + exports.push(symbol.clone()); + emit_handle_fn( + w, + &it.destroy_symbol, + &format!( + "{}(({}*)h);\nreturn js_undefined(env);", + it.destroy_symbol, it.iter_tag + ), + ); + exports.push(it.destroy_symbol.clone()); + } + } +} + +/// The result of a synchronous entry point: the C type of `r`, whether an +/// `out_len` slot follows the inputs, and the JavaScript value made from `r` +/// (and `out_len`). +struct SyncResult { + c_type: String, + out_len: bool, + value: String, +} + +/// Emit a synchronous entry point: marshal, call, check the error slot, and +/// convert the result (`None` for a void call). +fn emit_sync(w: &mut CodeWriter, symbol: &str, args: &Args, result: Option<&SyncResult>) { + emit_entry(w, symbol, args, |w| { + w.line("js_error err = {0};"); + let mut call = args.call.clone(); + if result.is_some_and(|r| r.out_len) { + w.line("size_t out_len = 0;"); + call.push("&out_len".into()); + } + call.push("&err".into()); + let call = format!("{symbol}({})", call.join(", ")); + match result { + Some(r) => w.line(format!("{} r = {call};", r.c_type)), + None => w.line(format!("{call};")), + }; + for t in &args.transfer { + w.line(t); + } + w.line("if (err.code != 0) {"); + w.line(" js_throw(env, &err);"); + w.line(" goto done;"); + w.line("}"); + let value = result.map_or("js_undefined(env)", |r| r.value.as_str()); + w.line(format!("ret = {value};")); + }); +} + +/// Emit an entry point's frame: the argument reads, `body` in its own +/// block, and the shared exit. +fn emit_entry(w: &mut CodeWriter, symbol: &str, args: &Args, body: impl FnOnce(&mut CodeWriter)) { + w.block( + format!("static napi_value nx_{symbol}(napi_env env, napi_callback_info info) {{"), + "}", + |w| { + w.line(format!("JS_ARGS({});", args.argc)); + w.line("napi_value ret = NULL;"); + for d in args.decls.iter().chain(&args.reads) { + w.line(d); + } + w.block("{", "}", body); + w.line("goto done;"); + w.raw("done:\n"); + for c in &args.cleanup { + w.line(c); + } + w.line("return ret;"); + }, + ); + w.blank(); +} + +/// Emit one callback interface: per method a frame, an invoker that runs on +/// the JavaScript thread, and a trampoline (the vtable entry) that calls the +/// invoker directly or hops to the JavaScript thread and waits; then the +/// dispatcher of hopped calls and the static vtable. +fn emit_callback_interface(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, prefix: &str) { + let tag = &cb.c_tag; + let protocol = cb.protocol(prefix); + let (dispatch, vtable) = cb_names(tag); + for (method, passes) in cb.methods.iter().zip(&protocol.method_args) { + let frame = format!("frame_{tag}_{}", method.name); + let slots = &method.abi_params[1..method.abi_params.len() - 1]; + let ret_c = method.abi_ret.render_c(prefix); + w.block("typedef struct {", format!("}} {frame};"), |w| { + w.line("js_cb_frame hdr;"); + for (i, s) in slots.iter().enumerate() { + w.line(format!("{} p{i};", s.ty.render_c(prefix))); + } + if method.ret.is_some() { + w.line(format!("{ret_c} result;")); + } + }); + w.blank(); + + let where_ = format!("{}.{}", cb.name, method.name); + w.block( + format!( + "static void invoke_{tag}_{}(napi_env env, js_cb* cb, {frame}* f) {{", + method.name + ), + "}", + |w| { + w.line("napi_handle_scope scope;"); + w.line("napi_open_handle_scope(env, &scope);"); + let n = method.params.len(); + w.line(format!("napi_value argv[{}];", n + 1)); + let mut slot = 0usize; + for (i, (p, pass)) in method.params.iter().zip(passes).enumerate() { + let names: Vec = (slot..slot + p.abi.len()) + .map(|k| format!("f->p{k}")) + .collect(); + slot += p.abi.len(); + w.line(format!("argv[{i}] = {};", receive(&p.ty, pass, &names, false))); + } + w.line("napi_value result;"); + w.line(format!( + "if (!js_cb_call(env, cb, \"{}\", {n}, argv, &result)) {{", + method.name + )); + w.line(format!( + " js_cb_report(env, f->hdr.out_err, \"{where_} failed\");" + )); + if let Some(ty) = &method.ret { + let (reader, tmp) = arg_reader(direct_prim(ty)); + w.line("} else {"); + w.line(format!(" {tmp} v = 0;")); + w.line(format!(" if ({reader}(env, result, &v)) {{")); + w.line(format!(" f->result = ({ret_c})v;")); + w.line(" } else {"); + w.line(format!( + " js_cb_report(env, f->hdr.out_err, \"{where_} returned a value of the wrong type\");" + )); + w.line(" }"); + } + w.line("}"); + w.line("napi_close_handle_scope(env, scope);"); + }, + ); + w.blank(); + + let mut decls = vec!["void* ctx".to_string()]; + for (i, s) in slots.iter().enumerate() { + decls.push(format!("{} p{i}", s.ty.render_c(prefix))); + } + decls.push(format!("{prefix}_error* out_err")); + let idx = cb + .methods + .iter() + .position(|m| m.name == method.name) + .unwrap_or(0); + w.block( + format!( + "static {ret_c} tramp_{tag}_{}({}) {{", + method.name, + decls.join(", ") + ), + "}", + |w| { + w.line(format!("{frame} f;")); + w.line("memset(&f, 0, sizeof f);"); + w.line("f.hdr.out_err = out_err;"); + for i in 0..slots.len() { + w.line(format!("f.p{i} = p{i};")); + } + w.line("js_cb* cb = (js_cb*)ctx;"); + w.line("if (js_cb_on_js_thread(cb)) {"); + w.line(format!(" invoke_{tag}_{}(cb->env, cb, &f);", method.name)); + w.line("} else {"); + w.line(" js_cb_req req;"); + w.line(" memset(&req, 0, sizeof req);"); + w.line(" req.cb = cb;"); + w.line(format!(" req.method = {idx};")); + w.line(" req.frame = &f;"); + w.line(" js_cb_hop(&req);"); + w.line("}"); + if method.ret.is_some() { + w.line("return f.result;"); + } + }, + ); + w.blank(); + } + + w.block( + format!("static void {dispatch}(napi_env env, napi_value fn, void* context, void* data) {{"), + "}", + |w| { + w.line("(void)fn;"); + w.line("(void)context;"); + w.line("js_cb_req* req = (js_cb_req*)data;"); + w.line("if (req->method < 0) {"); + w.line(" js_cb_release(env, req->cb);"); + w.line(" free(req);"); + w.line(" return;"); + w.line("}"); + w.line("if (env == NULL) {"); + w.line(format!( + " {prefix}_error_set(((js_cb_frame*)req->frame)->out_err, -4, \"the JavaScript environment is shutting down\");" + )); + w.line("} else {"); + w.line(" switch (req->method) {"); + for (i, method) in cb.methods.iter().enumerate() { + w.line(format!( + " case {i}: invoke_{tag}_{0}(env, req->cb, (frame_{tag}_{0}*)req->frame); break;", + method.name + )); + } + w.line(" default: break;"); + w.line(" }"); + w.line("}"); + w.line("js_cb_finish(req);"); + }, + ); + w.blank(); + let entries: Vec = cb + .methods + .iter() + .map(|m| format!("tramp_{tag}_{}", m.name)) + .chain(std::iter::once("js_cb_free".to_string())) + .collect(); + w.line(format!( + "static const {} {vtable} = {{{}}};", + cb.vtable_tag, + entries.join(", ") + )); + w.blank(); +} diff --git a/crates/weaveffi-gen/src/targets/node/mod.rs b/crates/weaveffi-gen/src/targets/node/mod.rs new file mode 100644 index 00000000..f6993255 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/node/mod.rs @@ -0,0 +1,243 @@ +//! Node.js binding generator. +//! +//! Emits an npm package that builds with `npm install`: the ES module +//! (`index.js`, `index.d.ts`, `runtime.js`) shared with the WebAssembly +//! target through the shared JavaScript layer, and its transport, a small N-API +//! addon (`{library}_node.c`, compiled by node-gyp from `binding.gyp` +//! against the bundled copy of the C header) that calls the library's C ABI. +//! Implements [`LanguageBackend`]; the shared driver bridges it into the +//! generator pipeline. + +mod addon; +mod package; + +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::{BindingModel, CallbackInterfaceBinding, ModuleBinding}; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::codegen::CodeWriter; +use crate::package::{PackageContext, PackagedFile}; +use crate::targets::c::render_c_header_from_model; +use crate::targets::js; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; + +use self::addon::render_addon_c; +use self::package::{ + node_platform_tokens, render_binding_gyp, render_package_json, render_platform_package_json, + render_readme, NodeNames, +}; + +/// The fixed transport section of `index.js`: loading the addon. +const LOADER_JS: &str = include_str!("runtime/loader.js"); + +/// Per-target configuration for [`NodeGenerator`]. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct NodeConfig { + /// npm package name (default: the package identity's name). + pub package_name: Option, + /// Basename of the IDL the CLI was invoked with. + #[serde(skip)] + pub input_basename: Option, +} + +impl NodeConfig { + /// The input IDL basename embedded in generated file headers. + fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } + + fn names<'a>(&self, identity: &'a Identity) -> NodeNames<'a> { + NodeNames { + identity, + package: self + .package_name + .clone() + .unwrap_or_else(|| identity.name.clone()), + addon: format!("{}_node", identity.library), + header: format!("{}.h", identity.library), + } + } +} + +/// Node.js backend: an ES module over an N-API addon. +pub struct NodeGenerator; + +/// The generated `index.js`. +fn render_index(model: &BindingModel, names: &NodeNames<'_>, input_basename: &str) -> String { + let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); + out.push_str(&js::render_imports(names.identity)); + out.push('\n'); + out.push_str(&LOADER_JS.replace("{{ADDON}}", &names.addon)); + out.push('\n'); + out.push_str(&js::render_verify(model, names.identity)); + out.push('\n'); + out.push_str(&js::render_api(model)); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, "index.js")); + out +} + +/// Every file of the generated package, as `(name, contents)` pairs. +fn render_files( + model: &BindingModel, + names: &NodeNames<'_>, + platforms: &[String], + default_dir: &str, + input_basename: &str, +) -> Vec<(String, String)> { + let modules: Vec = model.roots().map(js::names::module_name).collect(); + vec![ + ( + "index.js".into(), + render_index(model, names, input_basename), + ), + ( + "index.d.ts".into(), + js::render_dts(model, names.identity, input_basename, ""), + ), + ( + "runtime.js".into(), + js::render_runtime(names.identity, input_basename), + ), + ( + format!("{}.c", names.addon), + render_addon_c( + model, + &names.header, + input_basename, + &format!("{}.c", names.addon), + ), + ), + ( + names.header.clone(), + render_c_header_from_model(model, input_basename, &names.header), + ), + ( + "binding.gyp".into(), + render_binding_gyp(names, default_dir, input_basename), + ), + ( + "package.json".into(), + render_package_json(names, platforms, input_basename), + ), + ( + "README.md".into(), + render_readme(names, &modules, platforms, input_basename), + ), + ] +} + +impl LanguageBackend for NodeGenerator { + type Config = NodeConfig; + + fn name(&self) -> &'static str { + "node" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + /// The TypeScript `interface` a consumer implements for one callback + /// interface, as `index.d.ts` declares it. + fn render_callback_interface( + &self, + out: &mut String, + _module: &ModuleBinding, + cb: &CallbackInterfaceBinding, + _config: &Self::Config, + ) { + let mut w = CodeWriter::two_space(); + js::render_callback_interface_dts(&mut w, cb); + out.push_str(&w.finish()); + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + let dir = out_dir.join("node"); + let names = config.names(api.identity()); + render_files( + model, + &names, + &[], + "path.resolve('.')", + config.input_basename(), + ) + .into_iter() + .map(|(name, contents)| OutputFile::new(dir.join(name), contents)) + .collect() + } + + /// The packaged layout follows the esbuild convention: the main package + /// lists one `{package}-{os}-{cpu}` package per platform in + /// `optionalDependencies`, each gated by npm `os`/`cpu` and carrying the + /// prebuilt library, and the addon links the installed one. + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let dir = out_dir.join("node"); + let names = config.names(api.identity()); + let platforms: Vec<(crate::platform::Platform, (&str, &str), String)> = ctx + .binaries + .platforms() + .filter_map(|p| { + let tokens = node_platform_tokens(p)?; + Some(( + p, + tokens, + format!("{}-{}-{}", names.package, tokens.0, tokens.1), + )) + }) + .collect(); + let platform_names: Vec = platforms.iter().map(|(_, _, n)| n.clone()).collect(); + let default_dir = format!( + "path.dirname(require.resolve('{}-' + process.platform + '-' + process.arch + '/package.json'))", + names.package + ); + let mut files: Vec = render_files( + model, + &names, + &platform_names, + &default_dir, + config.input_basename(), + ) + .into_iter() + .map(|(name, contents)| PackagedFile::text(dir.join(name), contents)) + .collect(); + for (platform, tokens, name) in &platforms { + let pkg_dir = dir.join("npm").join(name); + files.push(PackagedFile::text( + pkg_dir.join("package.json"), + render_platform_package_json( + name, + &names.identity.library, + &names.identity.version, + *tokens, + ), + )); + let binary = ctx.binaries.get(*platform).expect("platform has a binary"); + files.push(PackagedFile::copy( + pkg_dir.join(platform.lib_filename(&names.identity.library)), + binary.source.clone(), + )); + } + Some(files) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/weaveffi-gen/src/targets/node/package.rs b/crates/weaveffi-gen/src/targets/node/package.rs new file mode 100644 index 00000000..5cf3aa7d --- /dev/null +++ b/crates/weaveffi-gen/src/targets/node/package.rs @@ -0,0 +1,186 @@ +//! Manifests and the README: `package.json`, `binding.gyp`, and the +//! per-platform packages of the packaged layout. + +use weaveffi_model::pkg::Identity; + +use crate::manifest::{JsonObject, JsonValue}; +use crate::platform::Platform; +use crate::targets::js::npm_metadata; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; + +/// The names every file of the generated package derives from. +pub(crate) struct NodeNames<'a> { + /// The library identity. + pub(crate) identity: &'a Identity, + /// The npm package name. + pub(crate) package: String, + /// The addon's base name (`{library}_node`). + pub(crate) addon: String, + /// The C header's file name (`{library}.h`). + pub(crate) header: String, +} + +impl NodeNames<'_> { + /// The files `npm pack` publishes. + fn files(&self) -> Vec { + vec![ + "index.js".into(), + "index.d.ts".into(), + "runtime.js".into(), + "binding.gyp".into(), + format!("{}.c", self.addon), + self.header.clone(), + ] + } +} + +/// Render `package.json`. `platform_packages` lists the per-platform +/// packages of the packaged layout (empty for plain generation). +pub(crate) fn render_package_json( + names: &NodeNames<'_>, + platform_packages: &[String], + input_basename: &str, +) -> String { + let mut obj = npm_metadata(names.identity, &names.package, input_basename) + .entry("files", JsonValue::str_array(names.files())) + .entry("gypfile", JsonValue::Bool(true)) + .entry( + "scripts", + JsonValue::Object(JsonObject::new().str_entry("install", "node-gyp rebuild")), + ) + .entry( + "engines", + JsonValue::Object(JsonObject::new().str_entry("node", ">=18")), + ); + if !platform_packages.is_empty() { + let mut deps = JsonObject::new(); + for p in platform_packages { + deps = deps.str_entry(p, &names.identity.version); + } + obj = obj.entry("optionalDependencies", JsonValue::Object(deps)); + } + obj.render() +} + +/// The npm `os`/`cpu` pair of a platform, or `None` for a platform Node.js +/// does not run on (Android, Wasm). +pub(crate) fn node_platform_tokens(platform: Platform) -> Option<(&'static str, &'static str)> { + Some((platform.node_os()?, platform.node_cpu()?)) +} + +/// Render a per-platform package's `package.json`, gated by npm `os` and +/// `cpu` so only the matching one installs. +pub(crate) fn render_platform_package_json( + name: &str, + library: &str, + version: &str, + (os, cpu): (&str, &str), +) -> String { + JsonObject::new() + .str_entry("name", name) + .str_entry("version", version) + .str_entry( + "description", + format!("Prebuilt {library} native library for {os}/{cpu}"), + ) + .entry("os", JsonValue::str_array([os])) + .entry("cpu", JsonValue::str_array([cpu])) + .render() +} + +/// Render `binding.gyp`. The addon links `{library}` from `native_dir`: +/// the directory (or library file) named by the `{PREFIX}_LIBRARY` +/// environment variable or the matching npm config +/// (`npm install --{prefix}-library=...`), else `default_dir` (a JavaScript +/// expression evaluated in the package directory). An rpath to the addon's +/// own directory and to `native_dir` lets the library load from either. +pub(crate) fn render_binding_gyp( + names: &NodeNames<'_>, + default_dir: &str, + input_basename: &str, +) -> String { + let id = names.identity; + let env = id.library_env_var(); + let npm = format!("npm_config_{}_library", id.prefix); + let script = format!( + "(() => {{ const path = require('path'); const p = process.env.{env} || process.env.{npm}; \ + if (!p) return {default_dir}; \ + return require('fs').statSync(p).isFile() ? path.dirname(path.resolve(p)) : path.resolve(p); }})()" + ); + let library = &id.library; + let target = JsonObject::new() + .str_entry("target_name", &names.addon) + .entry("sources", JsonValue::str_array([format!("{}.c", names.addon)])) + .entry("include_dirs", JsonValue::str_array(["."])) + .entry("library_dirs", JsonValue::str_array(["<(native_dir)"])) + .entry("libraries", JsonValue::str_array([format!("-l{library}")])) + .entry( + "conditions", + JsonValue::Array(vec![ + JsonValue::Raw( + "[\"OS=='mac'\", { \"xcode_settings\": { \"OTHER_LDFLAGS\": [\"-Wl,-rpath,@loader_path\", \"-Wl,-rpath,<(native_dir)\"] } }]" + .into(), + ), + JsonValue::Raw( + "[\"OS=='linux'\", { \"ldflags\": [\"-Wl,-rpath,'$$ORIGIN'\", \"-Wl,-rpath,<(native_dir)\"] }]" + .into(), + ), + ]), + ); + let body = JsonObject::new() + .entry( + "variables", + JsonValue::Object( + JsonObject::new().str_entry("native_dir%", format!(", + modules: &[String], + platforms: &[String], + input_basename: &str, +) -> String { + let id = names.identity; + let pkg = &names.package; + let env = id.library_env_var(); + let lib = &id.library; + let mut out = render_prelude(CommentStyle::Xml, input_basename); + out.push_str(&format!("# {pkg}\n\n{}\n\n", id.description_or_default())); + out.push_str(&format!( + "Node.js bindings for the `{lib}` native library: an ES module over a small \ + N-API addon (`{addon}.c`) that `npm install` compiles with node-gyp. The addon \ + links `{lib}`; set `{env}` to the library file (or its directory) when \ + installing, or pass `--{prefix}-library=` to npm.\n\n", + addon = names.addon, + prefix = id.prefix, + )); + if !platforms.is_empty() { + out.push_str( + "The prebuilt library ships in per-platform packages that npm selects \ + through `optionalDependencies`:\n\n", + ); + for p in platforms { + out.push_str(&format!("- `{p}`\n")); + } + out.push('\n'); + } + out.push_str("```sh\n"); + out.push_str(&format!("{env}=/path/to/lib{lib}.so npm install {pkg}\n")); + out.push_str("```\n\n"); + out.push_str(&format!( + "Each top-level module of the API is a namespace export:\n\n```js\nimport {{ {} }} from '{pkg}';\n```\n\n", + modules.join(", ") + )); + out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); + out +} diff --git a/crates/weaveffi-gen/src/targets/node/runtime/addon.c b/crates/weaveffi-gen/src/targets/node/runtime/addon.c new file mode 100644 index 00000000..abafe8ca --- /dev/null +++ b/crates/weaveffi-gen/src/targets/node/runtime/addon.c @@ -0,0 +1,603 @@ +#ifndef NAPI_VERSION +#define NAPI_VERSION 8 +#endif +#include +#include +#include +#include +#include +#include + +/* The addon wraps deprecated functions too; the JavaScript API carries the + * deprecation instead. */ +#define {{MACRO}}_DEPRECATED(msg) +#include "{{HEADER}}" + +/* + * The N-API transport of the JavaScript bindings. Every C symbol of the + * library is exported to `index.js` under its own name and called with the + * raw convention the shared JavaScript layer documents: direct values as + * numbers, bigints, and booleans; strings as strings; bytes and value + * buffers as Uint8Arrays; objects, iterators, and cancel tokens as bigint + * handles (null when absent); callback interfaces as adapter objects. A + * failure throws an instance of the runtime's `$Fault` class. + */ + +typedef {{PREFIX}}_error js_error; + +/* Helpers an API may not need. */ +#if defined(__GNUC__) || defined(__clang__) +#define JS_HELPER static inline __attribute__((unused)) +#else +#define JS_HELPER static inline +#endif + +/* Per-environment state (one per main thread or worker): the `$Fault` + * constructor `index.js` registers through `$setup`. */ +typedef struct { + napi_ref fault; +} js_env; + +static void js_env_free(napi_env env, void* data, void* hint) { + (void)hint; + js_env* e = (js_env*)data; + if (e->fault != NULL) napi_delete_reference(env, e->fault); + free(e); +} + +JS_HELPER napi_value js_undefined(napi_env env) { + napi_value v; + napi_get_undefined(env, &v); + return v; +} + +static napi_value js_setup(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e->fault != NULL) napi_delete_reference(env, e->fault); + napi_create_reference(env, argv[0], 1, &e->fault); + return js_undefined(env); +} + +/* --------------------------------------------------------------------------- + * Value conversions + * ------------------------------------------------------------------------- */ + +JS_HELPER napi_value js_new_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value ab, out; + void* data = NULL; + napi_create_arraybuffer(env, len, &data, &ab); + if (len > 0) memcpy(data, ptr, len); + napi_create_typedarray(env, napi_uint8_array, len, ab, 0, &out); + return out; +} + +JS_HELPER napi_value js_new_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out; + napi_create_string_utf8(env, len == 0 ? "" : (const char*)ptr, len, &out); + return out; +} + +JS_HELPER napi_value js_new_i32(napi_env env, int32_t v) { + napi_value out; + napi_create_int32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u32(napi_env env, uint32_t v) { + napi_value out; + napi_create_uint32(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_i64(napi_env env, int64_t v) { + napi_value out; + napi_create_bigint_int64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_u64(napi_env env, uint64_t v) { + napi_value out; + napi_create_bigint_uint64(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_f64(napi_env env, double v) { + napi_value out; + napi_create_double(env, v, &out); + return out; +} + +JS_HELPER napi_value js_new_bool(napi_env env, bool v) { + napi_value out; + napi_get_boolean(env, v, &out); + return out; +} + +/* An object, iterator, or cancel token handle: the pointer as a bigint, or + * null for NULL. */ +JS_HELPER napi_value js_new_handle(napi_env env, const void* p) { + napi_value out; + if (p == NULL) { + napi_get_null(env, &out); + } else { + napi_create_bigint_uint64(env, (uint64_t)(uintptr_t)p, &out); + } + return out; +} + +/* Returned strings and buffers: convert, then release the producer's copy. */ +JS_HELPER napi_value js_take_str(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_str(env, ptr, len); + if (ptr != NULL) {{PREFIX}}_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_take_bytes(napi_env env, const uint8_t* ptr, size_t len) { + napi_value out = js_new_bytes(env, ptr, len); + if (ptr != NULL) {{PREFIX}}_free_bytes((uint8_t*)ptr, len); + return out; +} + +JS_HELPER napi_value js_fault(napi_env env, int32_t code, const char* message, + const uint8_t* payload, size_t payload_len) { + napi_value argv[3], ctor, out = NULL; + napi_create_int32(env, code, &argv[0]); + napi_create_string_utf8(env, message != NULL ? message : "", NAPI_AUTO_LENGTH, &argv[1]); + if (payload != NULL) { + argv[2] = js_new_bytes(env, payload, payload_len); + } else { + napi_get_null(env, &argv[2]); + } + js_env* e = NULL; + napi_get_instance_data(env, (void**)&e); + if (e == NULL || e->fault == NULL || napi_get_reference_value(env, e->fault, &ctor) != napi_ok || + ctor == NULL || napi_new_instance(env, ctor, 3, argv, &out) != napi_ok) { + napi_create_error(env, NULL, argv[1], &out); + napi_set_named_property(env, out, "code", argv[0]); + } + return out; +} + +/* Throw the error a call reported, then release it. */ +JS_HELPER napi_value js_throw(napi_env env, js_error* err) { + napi_throw(env, js_fault(env, err->code, err->message, err->payload_ptr, err->payload_len)); + {{PREFIX}}_error_clear(err); + return NULL; +} + +JS_HELPER bool js_type_error(napi_env env, const char* expected) { + bool pending = false; + napi_is_exception_pending(env, &pending); + if (!pending) napi_throw_type_error(env, NULL, expected); + return false; +} + +JS_HELPER bool js_arg_i32(napi_env env, napi_value v, int32_t* out) { + return napi_get_value_int32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_u32(napi_env env, napi_value v, uint32_t* out) { + return napi_get_value_uint32(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_i8(napi_env env, napi_value v, int8_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int8_t)t; + return true; +} + +JS_HELPER bool js_arg_i16(napi_env env, napi_value v, int16_t* out) { + int32_t t = 0; + if (!js_arg_i32(env, v, &t)) return false; + *out = (int16_t)t; + return true; +} + +JS_HELPER bool js_arg_u8(napi_env env, napi_value v, uint8_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint8_t)t; + return true; +} + +JS_HELPER bool js_arg_u16(napi_env env, napi_value v, uint16_t* out) { + uint32_t t = 0; + if (!js_arg_u32(env, v, &t)) return false; + *out = (uint16_t)t; + return true; +} + +/* 64-bit integers: a bigint that fits, or an integral number. */ +JS_HELPER bool js_arg_i64(napi_env env, napi_value v, int64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_int64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in a signed 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d == (double)(int64_t)d) { + *out = (int64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_u64(napi_env env, napi_value v, uint64_t* out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_bigint) { + bool lossless = false; + napi_get_value_bigint_uint64(env, v, out, &lossless); + if (lossless) return true; + napi_throw_range_error(env, NULL, "bigint does not fit in an unsigned 64-bit integer"); + return false; + } + double d = 0; + if (t == napi_number && napi_get_value_double(env, v, &d) == napi_ok && d >= 0 && + d == (double)(uint64_t)d) { + *out = (uint64_t)d; + return true; + } + return js_type_error(env, "expected a bigint"); +} + +JS_HELPER bool js_arg_f64(napi_env env, napi_value v, double* out) { + return napi_get_value_double(env, v, out) == napi_ok || js_type_error(env, "expected a number"); +} + +JS_HELPER bool js_arg_f32(napi_env env, napi_value v, float* out) { + double d = 0; + if (!js_arg_f64(env, v, &d)) return false; + *out = (float)d; + return true; +} + +JS_HELPER bool js_arg_bool(napi_env env, napi_value v, bool* out) { + return napi_get_value_bool(env, v, out) == napi_ok || js_type_error(env, "expected a boolean"); +} + +/* A handle argument; null or undefined is NULL when `nullable`. */ +JS_HELPER bool js_arg_handle(napi_env env, napi_value v, void** out, bool nullable) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t == napi_null || t == napi_undefined) { + *out = NULL; + return nullable || js_type_error(env, "expected an object handle"); + } + uint64_t raw = 0; + bool lossless = false; + if (t != napi_bigint || napi_get_value_bigint_uint64(env, v, &raw, &lossless) != napi_ok || + raw == 0) { + return js_type_error(env, "expected an object handle"); + } + *out = (void*)(uintptr_t)raw; + return true; +} + +/* A borrowed byte run: a Uint8Array (or Buffer). */ +JS_HELPER bool js_arg_bytes(napi_env env, napi_value v, const uint8_t** ptr, size_t* len) { + bool is_typed = false; + napi_is_typedarray(env, v, &is_typed); + if (is_typed) { + napi_typedarray_type type; + void* data = NULL; + napi_get_typedarray_info(env, v, &type, len, &data, NULL, NULL); + if (type == napi_uint8_array || type == napi_uint8_clamped_array) { + *ptr = (const uint8_t*)data; + return true; + } + } + return js_type_error(env, "expected a Uint8Array"); +} + +/* A string argument encoded as UTF-8: short strings live in the inline + * buffer, longer ones on the heap until js_str_free. */ +typedef struct { + char* ptr; + size_t len; + char inline_buf[256]; +} js_str; + +#define JS_STR_INIT {NULL, 0, {0}} + +JS_HELPER bool js_arg_str(napi_env env, napi_value v, js_str* s) { + size_t len = 0; + if (napi_get_value_string_utf8(env, v, NULL, 0, &len) != napi_ok) { + return js_type_error(env, "expected a string"); + } + s->ptr = len < sizeof s->inline_buf ? s->inline_buf : (char*)malloc(len + 1); + napi_get_value_string_utf8(env, v, s->ptr, len + 1, &s->len); + return true; +} + +JS_HELPER void js_str_free(js_str* s) { + if (s->ptr != NULL && s->ptr != s->inline_buf) free(s->ptr); + s->ptr = NULL; +} + +#define JS_STR_PTR(s) ((const uint8_t*)(s).ptr) + +/* Read up to `n` arguments; missing ones are undefined. */ +#define JS_ARGS(n) \ + size_t argc = (n); \ + napi_value argv[(n) + 1]; \ + napi_get_cb_info(env, info, &argc, argv, NULL, NULL) + +/* --------------------------------------------------------------------------- + * Async calls: the completion may run on any thread, so it only records the + * result and queues the settlement on the JavaScript thread through a + * thread-safe function (which also keeps the event loop alive meanwhile). + * ------------------------------------------------------------------------- */ + +typedef enum { + JS_R_VOID, + JS_R_I32, + JS_R_U32, + JS_R_I64, + JS_R_U64, + JS_R_F64, + JS_R_BOOL, + JS_R_STR, + JS_R_BYTES, + JS_R_HANDLE +} js_kind; + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + js_kind kind; + js_error* err; + union { + int64_t i; + uint64_t u; + double f; + bool b; + const void* p; + } v; + size_t len; +} js_async; + +JS_HELPER void js_async_settle(napi_env env, napi_value cb, void* context, void* data) { + (void)cb; + (void)context; + js_async* a = (js_async*)data; + bool failed = a->err != NULL && a->err->code != 0; + if (env != NULL && failed) { + napi_reject_deferred(env, a->deferred, js_fault(env, a->err->code, a->err->message, + a->err->payload_ptr, a->err->payload_len)); + } else if (env != NULL) { + napi_value v = NULL; + switch (a->kind) { + case JS_R_VOID: v = js_undefined(env); break; + case JS_R_I32: v = js_new_i32(env, (int32_t)a->v.i); break; + case JS_R_U32: v = js_new_u32(env, (uint32_t)a->v.u); break; + case JS_R_I64: v = js_new_i64(env, a->v.i); break; + case JS_R_U64: v = js_new_u64(env, a->v.u); break; + case JS_R_F64: v = js_new_f64(env, a->v.f); break; + case JS_R_BOOL: v = js_new_bool(env, a->v.b); break; + case JS_R_STR: v = js_take_str(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_BYTES: v = js_take_bytes(env, (const uint8_t*)a->v.p, a->len); break; + case JS_R_HANDLE: v = js_new_handle(env, a->v.p); break; + } + napi_resolve_deferred(env, a->deferred, v); + } else if (!failed && (a->kind == JS_R_STR || a->kind == JS_R_BYTES) && a->v.p != NULL) { + /* The environment is shutting down: just release the result. */ + {{PREFIX}}_free_bytes((uint8_t*)a->v.p, a->len); + } + {{PREFIX}}_error_free(a->err); + napi_release_threadsafe_function(a->tsfn, napi_tsfn_release); + free(a); +} + +/* Create the promise and its settlement queue for one async call. */ +JS_HELPER js_async* js_async_begin(napi_env env, js_kind kind, const char* name, napi_value* promise) { + js_async* a = (js_async*)calloc(1, sizeof *a); + a->kind = kind; + napi_create_promise(env, &a->deferred, promise); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, + js_async_settle, &a->tsfn); + return a; +} + +/* Called by each completion once it has recorded its result. */ +JS_HELPER void js_async_done(js_async* a, js_error* err) { + a->err = err; + napi_call_threadsafe_function(a->tsfn, a, napi_tsfn_blocking); +} + +/* --------------------------------------------------------------------------- + * Callback interfaces. One registration per implementation passed to the + * producer: a reference to the adapter object, the JavaScript thread it + * belongs to, and a thread-safe function that runs calls made from any other + * thread on that thread while the caller waits. + * ------------------------------------------------------------------------- */ + +typedef struct { + napi_env env; + napi_ref ref; + napi_threadsafe_function tsfn; + uv_thread_t js_thread; +} js_cb; + +/* Every callback frame starts with this header so the dispatcher can reach + * `out_err` without knowing the method. */ +typedef struct { + js_error* out_err; +} js_cb_frame; + +/* A call queued from another thread: `method` is the vtable index, or -1 to + * release the registration. */ +typedef struct { + js_cb* cb; + int method; + void* frame; + uv_mutex_t mu; + uv_cond_t cv; + bool done; +} js_cb_req; + +JS_HELPER bool js_cb_on_js_thread(js_cb* cb) { + uv_thread_t self = uv_thread_self(); + return uv_thread_equal(&self, &cb->js_thread) != 0; +} + +JS_HELPER js_cb* js_cb_register(napi_env env, napi_value adapter, const char* name, + napi_threadsafe_function_call_js dispatch) { + js_cb* cb = (js_cb*)calloc(1, sizeof *cb); + cb->env = env; + cb->js_thread = uv_thread_self(); + napi_create_reference(env, adapter, 1, &cb->ref); + napi_value resource; + napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource); + napi_create_threadsafe_function(env, NULL, NULL, resource, 0, 1, NULL, NULL, NULL, dispatch, + &cb->tsfn); + /* A live implementation must not keep the process alive by itself. */ + napi_unref_threadsafe_function(env, cb->tsfn); + return cb; +} + +JS_HELPER void js_cb_release(napi_env env, js_cb* cb) { + if (env != NULL) napi_delete_reference(env, cb->ref); + napi_release_threadsafe_function(cb->tsfn, napi_tsfn_release); + free(cb); +} + +/* A callback-interface argument: the adapter object `index.js` built. */ +JS_HELPER bool js_arg_cb(napi_env env, napi_value v, const char* name, + napi_threadsafe_function_call_js dispatch, js_cb** out) { + napi_valuetype t; + napi_typeof(env, v, &t); + if (t != napi_object) return js_type_error(env, "expected a callback interface implementation"); + *out = js_cb_register(env, v, name, dispatch); + return true; +} + +/* The vtable's `free`: the producer is done with the implementation. The + * reference can only be deleted on its own thread. */ +JS_HELPER void js_cb_free(void* ctx) { + js_cb* cb = (js_cb*)ctx; + if (cb == NULL) return; + if (js_cb_on_js_thread(cb)) { + js_cb_release(cb->env, cb); + return; + } + js_cb_req* req = (js_cb_req*)calloc(1, sizeof *req); + req->cb = cb; + req->method = -1; + if (napi_call_threadsafe_function(cb->tsfn, req, napi_tsfn_nonblocking) != napi_ok) free(req); +} + +/* Run `req` on the JavaScript thread and wait for it. */ +JS_HELPER void js_cb_hop(js_cb_req* req) { + uv_mutex_init(&req->mu); + uv_cond_init(&req->cv); + req->done = false; + if (napi_call_threadsafe_function(req->cb->tsfn, req, napi_tsfn_blocking) == napi_ok) { + uv_mutex_lock(&req->mu); + while (!req->done) uv_cond_wait(&req->cv, &req->mu); + uv_mutex_unlock(&req->mu); + } else { + {{PREFIX}}_error_set(((js_cb_frame*)req->frame)->out_err, -4, + "the callback implementation is no longer reachable"); + } + uv_cond_destroy(&req->cv); + uv_mutex_destroy(&req->mu); +} + +JS_HELPER void js_cb_finish(js_cb_req* req) { + uv_mutex_lock(&req->mu); + req->done = true; + uv_cond_signal(&req->cv); + uv_mutex_unlock(&req->mu); +} + +/* Report the pending JavaScript exception (or `fallback`) through `out_err` + * as a foreign failure (code -4); nothing unwinds through the C frame. */ +JS_HELPER void js_cb_report(napi_env env, js_error* out_err, const char* fallback) { + char msg[512]; + msg[0] = 0; + bool pending = false; + napi_is_exception_pending(env, &pending); + if (pending) { + napi_value exc, text, str; + napi_get_and_clear_last_exception(env, &exc); + napi_valuetype t; + napi_typeof(env, exc, &t); + text = exc; + if (t == napi_object) napi_get_named_property(env, exc, "message", &text); + if (napi_coerce_to_string(env, text, &str) == napi_ok) { + napi_get_value_string_utf8(env, str, msg, sizeof msg, NULL); + } + napi_is_exception_pending(env, &pending); + if (pending) napi_get_and_clear_last_exception(env, &exc); + } + {{PREFIX}}_error_set(out_err, -4, msg[0] != 0 ? msg : fallback); +} + +/* Look up and call `method` on the adapter; false when it threw. */ +JS_HELPER bool js_cb_call(napi_env env, js_cb* cb, const char* method, size_t argc, + const napi_value* argv, napi_value* result) { + napi_value adapter, fn; + napi_get_reference_value(env, cb->ref, &adapter); + napi_get_named_property(env, adapter, method, &fn); + return napi_call_function(env, adapter, fn, argc, argv, result) == napi_ok; +} + +/* --------------------------------------------------------------------------- + * The runtime symbols every library exports. + * ------------------------------------------------------------------------- */ + +static napi_value js_abi_version(napi_env env, napi_callback_info info) { + (void)info; + return js_new_u32(env, {{PREFIX}}_abi_version()); +} + +static napi_value js_debug_live(napi_env env, napi_callback_info info) { + JS_ARGS(1); + int32_t kind = 0; + if (!js_arg_i32(env, argv[0], &kind)) return NULL; + return js_new_u64(env, {{PREFIX}}_debug_live(kind)); +} + +static napi_value js_cancel_token_create(napi_env env, napi_callback_info info) { + (void)info; + return js_new_handle(env, {{PREFIX}}_cancel_token_create()); +} + +static napi_value js_cancel_token_cancel(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + {{PREFIX}}_cancel_token_cancel(({{PREFIX}}_cancel_token*)token); + return js_undefined(env); +} + +static napi_value js_cancel_token_destroy(napi_env env, napi_callback_info info) { + JS_ARGS(1); + void* token = NULL; + if (!js_arg_handle(env, argv[0], &token, false)) return NULL; + {{PREFIX}}_cancel_token_destroy(({{PREFIX}}_cancel_token*)token); + return js_undefined(env); +} + +#define JS_RUNTIME_EXPORTS \ + {"$setup", NULL, js_setup, NULL, NULL, NULL, napi_default, NULL}, \ + {"{{PREFIX}}_abi_version", NULL, js_abi_version, NULL, NULL, NULL, napi_default, NULL}, \ + {"{{PREFIX}}_debug_live", NULL, js_debug_live, NULL, NULL, NULL, napi_default, NULL}, \ + {"{{PREFIX}}_cancel_token_create", NULL, js_cancel_token_create, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"{{PREFIX}}_cancel_token_cancel", NULL, js_cancel_token_cancel, NULL, NULL, NULL, \ + napi_default, NULL}, \ + {"{{PREFIX}}_cancel_token_destroy", NULL, js_cancel_token_destroy, NULL, NULL, NULL, \ + napi_default, NULL} diff --git a/crates/weaveffi-gen/src/targets/node/runtime/loader.js b/crates/weaveffi-gen/src/targets/node/runtime/loader.js new file mode 100644 index 00000000..c322bf6f --- /dev/null +++ b/crates/weaveffi-gen/src/targets/node/runtime/loader.js @@ -0,0 +1,26 @@ +import { createRequire } from 'node:module'; + +// The native entry points: the N-API addon `npm install` builds from +// {{ADDON}}.c (with node-gyp), or a prebuilt copy next to this file. +function $loadAddon(name) { + const require = createRequire(import.meta.url); + const tried = []; + for (const dir of ['./build/Release/', './build/Debug/', './']) { + try { + return require(dir + name); + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') throw e; + tried.push(dir + name); + } + } + throw new Error( + `the native addon ${name} was not found (tried ${tried.join(', ')}); ` + + 'build it with `npm install` or `node-gyp rebuild` in this package', + ); +} + +const $raw = $loadAddon('{{ADDON}}.node'); +$raw.$setup($Fault); + +// Object tokens in value buffers are already bigint handles here. +const $token = (t) => t; diff --git a/crates/weaveffi-gen/src/targets/node/tests.rs b/crates/weaveffi-gen/src/targets/node/tests.rs new file mode 100644 index 00000000..a1bd3fcd --- /dev/null +++ b/crates/weaveffi-gen/src/targets/node/tests.rs @@ -0,0 +1,180 @@ +use super::*; +use crate::platform::{BinarySet, Platform}; +use crate::targets::js::test_api; + +fn files(name: &str, config: &NodeConfig) -> Vec { + let api = test_api(name); + let model = BindingModel::build(&api); + NodeGenerator.files(&api, &model, Utf8Path::new("out"), config) +} + +fn file(files: &[OutputFile], name: &str) -> String { + files + .iter() + .find(|f| f.path.file_name() == Some(name)) + .unwrap_or_else(|| panic!("no {name}")) + .contents + .clone() +} + +fn has(src: &str, needle: &str) { + assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); +} + +#[test] +fn the_package_is_named_after_the_identity() { + let out = files("acme-kv", &NodeConfig::default()); + let names: Vec<&str> = out.iter().filter_map(|f| f.path.file_name()).collect(); + assert_eq!( + names, + [ + "index.js", + "index.d.ts", + "runtime.js", + "acme_kv_node.c", + "acme_kv.h", + "binding.gyp", + "package.json", + "README.md" + ] + ); + has(&file(&out, "package.json"), "\"name\": \"acme-kv\","); + has( + &file(&out, "binding.gyp"), + "\"target_name\": \"acme_kv_node\",", + ); + has(&file(&out, "binding.gyp"), "\"-lacme_kv\""); + has( + &file(&out, "binding.gyp"), + "process.env.ACME_KV_LIBRARY || process.env.npm_config_acme_kv_library", + ); + has(&file(&out, "binding.gyp"), "-Wl,-rpath,@loader_path"); + has( + &file(&out, "index.js"), + "const $raw = $loadAddon('acme_kv_node.node');", + ); + for f in &out { + if f.path.file_name() != Some("package.json") { + assert!(!f.contents.contains("weaveffi_"), "{} is branded", f.path); + } + } + + let renamed = files( + "acme-kv", + &NodeConfig { + package_name: Some("@acme/kv".into()), + ..NodeConfig::default() + }, + ); + has(&file(&renamed, "package.json"), "\"name\": \"@acme/kv\","); +} + +#[test] +fn index_checks_the_contract_at_load() { + let api = test_api("kv"); + let model = BindingModel::build(&api); + let checksum = model.modules[0].checksum.unwrap(); + let js = file(&files("kv", &NodeConfig::default()), "index.js"); + has(&js, "$raw.$setup($Fault);"); + has( + &js, + &format!("$verify($raw, 'kv', 'kv', 3, [['kv', 0x{checksum:016x}n]]);"), + ); + has( + &js, + "export { KvError, CancelledError } from './runtime.js';", + ); +} + +#[test] +fn addon_exports_every_symbol_with_the_raw_convention() { + let c = file(&files("kv", &NodeConfig::default()), "kv_node.c"); + assert!(!c.contains("{{"), "unsubstituted placeholder"); + has(&c, "#include \"kv.h\""); + has(&c, "typedef kv_error js_error;"); + for symbol in [ + "kv_kv_checksum", + "kv_kv_Store_new", + "kv_kv_Store_clone", + "kv_kv_Store_destroy", + "kv_kv_Store_ScanIterator_next", + "kv_kv_Store_ScanIterator_destroy", + "kv_kv_Store_wait", + "kv_kv_stats_count", + ] { + has( + &c, + &format!("{{\"{symbol}\", NULL, nx_{symbol}, NULL, NULL, NULL, napi_default, NULL}},"), + ); + } + // Strings cross as (ptr, len); a returned string is freed after copying. + has( + &c, + "kv_kv_Store_get((const kv_kv_Store*)self_h, JS_STR_PTR(a0), a0.len, &out_len, &err);", + ); + has(&c, "ret = js_take_bytes(env, r, out_len);"); + // A cancellable launcher takes the token handle after its inputs. + has( + &c, + "if (!js_arg_handle(env, argv[2], &token, true)) goto done;", + ); + has(&c, "kv_kv_Store_wait((const kv_kv_Store*)self_h, a0, (kv_cancel_token*)token, done_kv_kv_Store_wait, a);"); + has( + &c, + "static void done_kv_kv_Store_wait(void* context, kv_error* err, int64_t result) {", + ); + has( + &c, + "js_async* a = js_async_begin(env, JS_R_HANDLE, \"kv_kv_Store_fetch\", &ret);", + ); + // Every declaration precedes the first read that can fail. + has( + &c, + " js_cb* a0 = NULL;\n int32_t a1 = 0;\n if (!js_arg_cb(", + ); +} + +#[test] +fn callback_vtables_hop_to_the_javascript_thread() { + let c = file(&files("kv", &NodeConfig::default()), "kv_node.c"); + has(&c, "static bool tramp_kv_kv_Listener_on_message(void* ctx, const uint8_t* p0, size_t p1, uint64_t p2, kv_error* out_err) {"); + has(&c, " if (js_cb_on_js_thread(cb)) {"); + has(&c, " js_cb_hop(&req);"); + has(&c, "argv[0] = js_new_str(env, f->p0, f->p1);"); + has(&c, "argv[1] = js_new_u64(env, f->p2);"); + has(&c, "argv[2] = js_new_handle(env, f->p3);"); + has(&c, "static const kv_kv_Listener_vtable vtable_kv_kv_Listener = {tramp_kv_kv_Listener_on_message, tramp_kv_kv_Listener_on_bundle, js_cb_free};"); +} + +#[test] +fn package_ships_one_npm_package_per_node_platform() { + let api = test_api("kv"); + let model = BindingModel::build(&api); + let mut binaries = BinarySet::new("kv"); + binaries.insert(Platform::MacosArm64, "lib/libkv.dylib"); + binaries.insert(Platform::Wasm32, "lib/kv.wasm"); + let ctx = PackageContext { + binaries: &binaries, + input_basename: None, + }; + let files = NodeGenerator + .package( + &api, + &model, + &ctx, + Utf8Path::new("out"), + &NodeConfig::default(), + ) + .expect("node packages"); + let paths: Vec = files.iter().map(|f| f.path.to_string()).collect(); + assert!( + paths.contains(&"out/node/npm/kv-darwin-arm64/package.json".into()), + "{paths:?}" + ); + assert!( + paths.contains(&"out/node/npm/kv-darwin-arm64/libkv.dylib".into()), + "{paths:?}" + ); + assert!(!paths.iter().any(|p| p.contains("wasm")), "{paths:?}"); + assert_eq!(node_platform_tokens(Platform::AndroidArm64), None); +} diff --git a/crates/weaveffi-gen/src/targets/python/calls.rs b/crates/weaveffi-gen/src/targets/python/calls.rs new file mode 100644 index 00000000..9e4234a2 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/calls.rs @@ -0,0 +1,794 @@ +//! Call rendering: sync, async, and iterator wrappers, their module-level +//! `ctypes` bindings, and the consumer-facing abstract base class, static +//! vtable, and trampolines of each callback interface. +//! +//! Every C prototype is bound once, at import time, from the model's lowered +//! [`AbiFn`] (`argtypes` and `restype` come straight from its slots). +//! Marshalling dispatch is driven by the shared plans: [`ArgPass`] decides +//! how each parameter crosses and [`Family`] how a result (or a trampoline +//! argument) is received, so this module never re-derives those shapes. + +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::model::{ + AbiFn, CallShape, CallbackInterfaceBinding, CallbackMethodBinding, ErrorBinding, Family, + FnBinding, IteratorBinding, ModuleBinding, ParamBinding, Ty, +}; +use weaveffi_model::plan::ArgPass; + +use crate::targets::python::codec::{py_decode_expr, py_write_stmts}; +use crate::targets::python::docs::{emit_docstring, emit_fn_docstring}; +use crate::targets::python::entities::{py_checker_name, py_factory_name}; +use crate::targets::python::types::{ + py_binding_name, py_ctype, py_local, py_member_name, py_name, py_str_literal, py_type_hint, + py_wrapper_fn_name, Slot, +}; +use crate::targets::python::Gen; + +/// How a rendered callable is scoped and spelled in the generated Python. +#[derive(Clone, Copy)] +pub(crate) enum FnScope { + /// A module-level free function. + Free, + /// An instance method on an interface wrapper: leading `self` parameter, + /// whose pointer is lent as the leading C argument. + Method, + /// A `@staticmethod` member. + Static, + /// A `@classmethod` constructor factory returning a new wrapper instance. + Factory, + /// The canonical `new` constructor, emitted as `__init__`. + Init, +} + +impl FnScope { + /// Indentation depth of the `def` line (0 at module scope, 1 in a class). + fn depth(self) -> usize { + usize::from(!matches!(self, FnScope::Free)) + } +} + +/// The lowered C function a callable's wrapper invokes. +fn launcher(f: &FnBinding) -> &AbiFn { + match &f.shape { + CallShape::Sync(abi) => abi, + CallShape::Async(a) => &a.launch, + CallShape::Iterator(it) => &it.launch, + } +} + +/// `_bind("symbol", restype, argtypes...)` for one lowered function. The +/// async launcher's `callback` slot is typed with the completion's +/// `CFUNCTYPE` (`callback_type`). +fn bind_line(g: &Gen<'_>, abi: &AbiFn, callback_type: Option<&str>) -> String { + let mut parts = vec![ + format!("\"{}\"", abi.symbol), + py_ctype(&abi.ret, Slot::Recv), + ]; + parts.extend(abi.params.iter().map(|p| match (&p.ty, callback_type) { + (CType::Named(_), Some(cb)) if p.name == "callback" => cb.to_string(), + _ => py_ctype(&p.ty, Slot::Arg), + })); + format!( + "{} = _bind({})", + py_binding_name(&abi.symbol, g.prefix), + parts.join(", ") + ) +} + +/// The `CFUNCTYPE(...)` spelling for a C function-pointer signature. +fn cfunctype(ret: &CType, params: &[AbiParam]) -> String { + let parts: Vec = std::iter::once(py_ctype(ret, Slot::Recv)) + .chain(params.iter().map(|p| py_ctype(&p.ty, Slot::Recv))) + .collect(); + format!("ctypes.CFUNCTYPE({})", parts.join(", ")) +} + +/// The module-level stem of an async function's completion objects: the +/// launcher's binding name without `_c`. +fn completion_stem(g: &Gen<'_>, f: &FnBinding) -> String { + let binding = py_binding_name(&launcher(f).symbol, g.prefix); + binding.strip_prefix("_c").unwrap_or(&binding).to_string() +} + +/// The module-level iterator class for an iterator-returning callable: +/// `_` plus the iterator tag without its prefix. +fn iterator_class(g: &Gen<'_>, it: &IteratorBinding) -> String { + py_binding_name(&it.iter_tag, g.prefix).replacen("_c_", "_", 1) +} + +/// The expression receiving one producer-owned value of `ty` (a sync +/// return, an async result, or an iterator element) from its pointer (or +/// direct value) expression `p` and length expression `len`, releasing what +/// it owes. +fn py_recv_expr(ty: &Ty, p: &str, len: &str) -> String { + match ty.family() { + Family::String => format!("_take_str({p}, {len})"), + Family::Bytes => format!("_take_bytes({p}, {len})"), + Family::Buffer => py_decode_expr(&format!("_take_bytes({p}, {len})"), ty), + Family::Object { nullable } => { + let class = local_type_name(ty.interface_name().expect("object names an interface")); + if nullable { + format!("({class}._adopt({p}) if {p} else None)") + } else { + format!("{class}._adopt(_required({p}))") + } + } + Family::Direct => match ty { + Ty::Enum(name) => format!("{}({p})", local_type_name(name)), + _ => p.to_string(), + }, + Family::Callback | Family::Iterator => { + unreachable!("callback interfaces and iterators are never received as values") + } + } +} + +/// Emit every module-level piece a callable needs ahead of its wrapper: the +/// bound launcher, plus the iterator class (iterator shape) or the static +/// completion trampoline (async shape). `owner` names the callable in docs +/// (`Store.get` for a member); `packed` appends a sync member's binding to +/// the block the previous member started, with no blank lines between. +pub(crate) fn render_bindings( + out: &mut String, + g: &Gen<'_>, + f: &FnBinding, + error: Option<&ErrorBinding>, + owner: &str, + packed: bool, +) { + let mut w = CodeWriter::four_space(); + match &f.shape { + CallShape::Sync(abi) => { + if !packed { + w.blank().blank(); + } + w.line(bind_line(g, abi, None)); + } + CallShape::Iterator(it) => { + w.blank().blank(); + w.line(bind_line(g, &it.launch, None)); + render_iterator_class(&mut w, g, f, it, error, owner); + } + CallShape::Async(a) => { + let stem = completion_stem(g, f); + let cb_type = py_binding_name(&a.callback_type, g.prefix); + let factory = match error { + Some(eb) if f.throws => py_factory_name(eb), + _ => "_error_from".to_string(), + }; + let slots: Vec = a + .callback_params + .iter() + .map(|p| py_slot_name(&p.name)) + .collect(); + let value = match &f.ret { + None => "None".to_string(), + Some(ty) => match ty.family() { + Family::String | Family::Bytes | Family::Buffer => { + py_recv_expr(ty, "result_ptr", "result_len") + } + _ => py_recv_expr(ty, "result", ""), + }, + }; + w.blank().blank(); + w.line(format!( + "{cb_type} = {}", + cfunctype(&CType::Void, &a.callback_params) + )); + w.line(bind_line(g, &a.launch, Some(&cb_type))); + w.blank().blank(); + w.line(format!( + "def {stem}_complete({}) -> None:", + slots.join(", ") + )); + w.scope(|w| { + w.line("# Runs once, on a producer thread: take ownership of the result"); + w.line("# here, then hand it to the awaiting event loop."); + w.line("try:"); + w.scope(|w| { + w.line("if err:"); + w.scope(|w| { + w.line(format!( + "_async_settle(context, _async_error(err, {factory}), None)" + )); + }); + w.line("else:"); + w.scope(|w| { + w.line(format!("_async_settle(context, None, {value})")); + }); + }); + w.line("except BaseException as exc:"); + w.scope(|w| { + w.line("_async_settle(context, exc, None)"); + }); + }); + w.blank().blank(); + w.line(format!("{stem}_completion = {cb_type}({stem}_complete)")); + } + } + out.push_str(&w.finish()); +} + +/// Render the module-level class for one iterator-returning callable, +/// satisfying the pull contract of +/// [`weaveffi_model::plan::IteratorProtocol`]: one producer `next` call per +/// `__next__`, per-element releases after copying, and exactly one +/// `destroy` (on exhaustion, `close()`, or garbage collection). +fn render_iterator_class( + w: &mut CodeWriter, + g: &Gen<'_>, + f: &FnBinding, + it: &IteratorBinding, + error: Option<&ErrorBinding>, + owner: &str, +) { + let class = iterator_class(g, it); + let next = py_binding_name(&it.next.symbol, g.prefix); + let destroy = py_binding_name(&it.destroy_symbol, g.prefix); + let checker = py_checker_name(f, error); + let item_ty = it + .next + .params + .iter() + .find(|p| p.name == "out_item") + .and_then(|p| match &p.ty { + CType::Ptr { pointee, .. } => Some(py_ctype(pointee, Slot::Recv)), + _ => None, + }) + .expect("iterator next writes through out_item"); + let has_len = it.next.params.iter().any(|p| p.name == "out_len"); + + w.line(bind_line(g, &it.next, None)); + w.line(format!( + "{destroy} = _bind(\"{}\", None, ctypes.c_void_p)", + it.destroy_symbol + )); + w.blank().blank(); + w.line(format!("class {class}(_Iterator):")); + w.scope(|w| { + w.line(format!( + "\"\"\"The lazy iterator `{owner}` returns: each step pulls one element" + )); + w.line("from the producer.\"\"\""); + w.blank(); + w.line(format!("_destroy = staticmethod({destroy})")); + w.blank(); + w.line(format!("def __next__(self) -> {}:", py_type_hint(&it.elem))); + w.scope(|w| { + w.line(format!("_item = {item_ty}()")); + let mut args = vec!["_p".to_string(), "ctypes.byref(_item)".into()]; + if has_len { + w.line("_len = ctypes.c_size_t()"); + args.push("ctypes.byref(_len)".into()); + } + args.push("ctypes.byref(_err)".into()); + w.line("_err = _ErrorStruct()"); + w.line("_p = self._step()"); + w.line("try:"); + w.scope(|w| { + w.line(format!("_more = {next}({})", args.join(", "))); + }); + w.line("finally:"); + w.scope(|w| { + w.line("self._release()"); + }); + w.line(format!("{checker}(_err)")); + w.line("if not _more:"); + w.scope(|w| { + w.line("self.close()"); + w.line("raise StopIteration"); + }); + w.line(format!( + "return {}", + py_recv_expr(&it.elem, "_item.value", "_len.value") + )); + }); + }); +} + +/// The emitted Python name for a callable in `scope`. +fn py_fn_name(g: &Gen<'_>, module: &ModuleBinding, f: &FnBinding, scope: FnScope) -> String { + match scope { + FnScope::Free => py_wrapper_fn_name(&module.path, &f.name, g.strip_module_prefix), + FnScope::Init => "__init__".to_string(), + _ => py_member_name(&f.name), + } +} + +/// One parameter's local preparation and its C argument expressions. +struct ParamPlan { + /// Statements run before any object is lent (encoding). + prep: Vec, + /// Statements run inside the innermost borrow scope, just before the + /// call (finishing value buffers, registering callbacks). + late: Vec, + /// The object lent for the call: `(local, acquire expr, release stmt)`. + lend: Option<(String, String, String)>, + /// The C argument expressions, in slot order. + args: Vec, +} + +fn plan_param(p: &ParamBinding, next: &mut usize) -> ParamPlan { + let n = py_name(&p.name); + let mut plan = ParamPlan { + prep: vec![], + late: vec![], + lend: None, + args: vec![], + }; + match p.arg_pass() { + ArgPass::Direct { .. } => plan.args.push(n), + ArgPass::String { .. } => { + plan.prep.push(format!("_{n}_b = {n}.encode(\"utf-8\")")); + plan.args = vec![format!("_{n}_b"), format!("len(_{n}_b)")]; + } + ArgPass::Bytes { .. } => { + plan.prep.push(format!("_{n}_b = bytes({n})")); + plan.args = vec![format!("_{n}_b"), format!("len(_{n}_b)")]; + } + // Encode now, mint object tokens later: `finish()` runs once every + // parameter has encoded and every lent object is held, so a failure + // anywhere before the call leaks no strong reference. + ArgPass::Buffer { .. } => { + let mut w = CodeWriter::four_space(); + w.line(format!("_{n}_w = _Writer()")); + py_write_stmts(&mut w, &format!("_{n}_w"), &n, &p.ty, next); + plan.prep = w.finish().lines().map(str::to_string).collect(); + plan.late.push(format!("_{n}_b = _{n}_w.finish()")); + plan.args = vec![format!("_{n}_b"), format!("len(_{n}_b)")]; + } + ArgPass::Object { nullable, .. } => { + let class = local_type_name(p.ty.interface_name().expect("object names an interface")); + let (acquire, release) = if nullable { + ( + format!("_lend_opt({n}, {class})"), + format!("_release_opt({n})"), + ) + } else { + (format!("_lend({n}, {class})"), format!("{n}._release()")) + }; + plan.lend = Some((format!("_{n}_p"), acquire, release)); + plan.args.push(format!("_{n}_p")); + } + ArgPass::Callback { .. } => { + let cb = local_type_name( + p.ty.callback_interface_name() + .expect("callback names a callback interface"), + ); + plan.late + .push(format!("_{n}_ctx = _callback_register({n}, {cb})")); + plan.args = vec![ + format!("_{n}_ctx"), + format!("ctypes.addressof(_{cb}_vtable)"), + ]; + } + } + plan +} + +/// Render one callable's wrapper `def` (its bindings are emitted separately +/// by [`render_bindings`]). A throwing callable raises `module`'s error domain. +pub(crate) fn render_callable( + out: &mut String, + g: &Gen<'_>, + module: &ModuleBinding, + f: &FnBinding, + scope: FnScope, +) { + let error = module.error.as_ref(); + let depth = scope.depth(); + let raises = error.filter(|_| f.throws).map(|eb| eb.type_name.as_str()); + let abi = launcher(f); + let binding = py_binding_name(&abi.symbol, g.prefix); + + let mut sig: Vec = match scope { + FnScope::Method | FnScope::Init => vec!["self".into()], + FnScope::Factory => vec!["cls".into()], + FnScope::Free | FnScope::Static => vec![], + }; + sig.extend( + f.params + .iter() + .map(|p| format!("{}: {}", py_name(&p.name), py_type_hint(&p.ty))), + ); + let ret_hint = match scope { + FnScope::Init => "None".to_string(), + _ => f + .ret + .as_ref() + .map(py_type_hint) + .unwrap_or_else(|| "None".to_string()), + }; + + let mut w = CodeWriter::four_space().with_depth(depth); + w.blank(); + if depth == 0 { + w.blank(); + } + match scope { + FnScope::Static => { + w.line("@staticmethod"); + } + FnScope::Factory => { + w.line("@classmethod"); + } + _ => {} + } + w.line(format!( + "{}def {}({}) -> {ret_hint}:", + if f.is_async { "async " } else { "" }, + py_fn_name(g, module, f, scope), + sig.join(", "), + )); + w.indent(); + let doc = match (&f.shape, &f.doc) { + (CallShape::Iterator(_), d) => { + let streaming = "Returns a lazy iterator: each step pulls one element from the\n\ + producer. Exhaust or close() the iterator to release its native\n\ + handle (garbage collection also releases it)."; + Some(match d { + Some(d) => format!("{}\n\n{streaming}", d.trim()), + None => streaming.to_string(), + }) + } + (_, d) => d.clone(), + }; + let mut fdoc = String::new(); + emit_fn_docstring(&mut fdoc, &doc, &f.params, &w.indent_str(), raises); + w.raw(fdoc); + + if let Some(msg) = &f.deprecated { + w.line(format!( + "warnings.warn(\"{}\", DeprecationWarning, stacklevel=2)", + py_str_literal(msg) + )); + } + + let mut next = 0; + let plans: Vec = f.params.iter().map(|p| plan_param(p, &mut next)).collect(); + for line in plans.iter().flat_map(|p| &p.prep) { + w.line(line); + } + + // The C arguments: `self`, each parameter's slots, then the trailing + // slots the lowering appended, named by the model. + let mut args: Vec = Vec::new(); + let mut consumed = 0; + if f.has_self { + args.push("_self_p".into()); + consumed = 1; + } + for (p, plan) in f.params.iter().zip(&plans) { + args.extend(plan.args.iter().cloned()); + consumed += p.abi.len(); + } + for slot in &abi.params[consumed..] { + args.push(match slot.name.as_str() { + "out_len" => "ctypes.byref(_out_len)".to_string(), + "out_err" => "ctypes.byref(_err)".to_string(), + "cancel_token" => "_token".to_string(), + "callback" => format!("{}_completion", completion_stem(g, f)), + "context" => "_call".to_string(), + other => unreachable!("unexpected trailing slot `{other}`"), + }); + } + let call = format!("{binding}({})", args.join(", ")); + + let mut lends: Vec<(String, String, String)> = Vec::new(); + if f.has_self { + lends.push(( + "_self_p".into(), + "self._acquire()".into(), + "self._release()".into(), + )); + } + lends.extend(plans.iter().filter_map(|p| p.lend.clone())); + let late: Vec = plans.iter().flat_map(|p| p.late.clone()).collect(); + + let is_async = matches!(f.shape, CallShape::Async(_)); + if is_async { + w.line("_call, _future = _async_begin()"); + if f.cancellable { + w.line("_token = _cancel_token_create()"); + } + w.line("try:"); + w.indent(); + emit_lent_call(&mut w, &lends, &late, &call); + w.dedent(); + w.line("except BaseException:"); + w.scope(|w| { + w.line("_async_abandon(_call)"); + if f.cancellable { + w.line("_cancel_token_destroy(_token)"); + } + w.line("raise"); + }); + if f.cancellable { + w.line("return await _async_wait_cancellable(_future, _token)"); + } else { + w.line("return await _future"); + } + out.push_str(&w.finish()); + return; + } + + w.line("_err = _ErrorStruct()"); + if abi.params.iter().any(|p| p.name == "out_len") { + w.line("_out_len = ctypes.c_size_t()"); + } + let has_ret = f.ret.is_some(); + let call = if has_ret { + format!("_ret = {call}") + } else { + call + }; + emit_lent_call(&mut w, &lends, &late, &call); + w.line(format!("{}(_err)", py_checker_name(f, error))); + + match (scope, &f.shape) { + (FnScope::Init, _) => { + w.line("self._init_handle(_required(_ret))"); + } + (FnScope::Factory, _) => { + w.line("return cls._adopt(_required(_ret))"); + } + (_, CallShape::Iterator(it)) => { + w.line(format!("return {}._adopt(_ret)", iterator_class(g, it))); + } + _ => { + if let Some(ty) = &f.ret { + w.line(format!( + "return {}", + py_recv_expr(ty, "_ret", "_out_len.value") + )); + } + } + } + out.push_str(&w.finish()); +} + +/// Emit the C call inside one `try`/`finally` per lent object (outermost +/// first), with the late statements (buffer finishing, callback +/// registration) just before the call. +fn emit_lent_call( + w: &mut CodeWriter, + lends: &[(String, String, String)], + late: &[String], + call: &str, +) { + match lends.split_first() { + None => { + for line in late { + w.line(line); + } + w.line(call); + } + Some(((local, acquire, release), rest)) => { + w.line(format!("{local} = {acquire}")); + w.line("try:"); + w.indent(); + emit_lent_call(w, rest, late, call); + w.dedent(); + w.line("finally:"); + w.scope(|w| { + w.line(release); + }); + } + } +} + +// ── Callback interfaces ── + +/// The Python spelling of a trampoline slot name: the ABI slot name, +/// keyword-escaped. +fn py_slot_name(slot: &str) -> String { + py_local(slot) +} + +/// The expression converting one trampoline parameter's C slots into the +/// value handed to the implementation. Strings, bytes, and buffers are +/// borrowed for the dispatch and copied; an object transfers one strong +/// reference, which the wrapper adopts. +fn py_trampoline_arg(p: &ParamBinding) -> String { + let n = py_slot_name(&p.name); + let data = || { + format!( + "_peek_bytes({}, {})", + py_slot_name(&format!("{}_ptr", p.name)), + py_slot_name(&format!("{}_len", p.name)) + ) + }; + match p.ty.family() { + Family::String => format!("{}.decode(\"utf-8\")", data()), + Family::Bytes => data(), + Family::Buffer => py_decode_expr(&data(), &p.ty), + Family::Object { nullable } => { + let class = local_type_name(p.ty.interface_name().expect("object names an interface")); + if nullable { + format!("({class}._adopt({n}) if {n} else None)") + } else { + format!("{class}._adopt(_required({n}))") + } + } + Family::Direct => match &p.ty { + Ty::Enum(name) => format!("{}({n})", local_type_name(name)), + _ => n, + }, + Family::Callback | Family::Iterator => { + unreachable!("callback interfaces and iterators are never callback arguments") + } + } +} + +/// `(coercion, default)` for a callback method's direct return: the Python +/// conversion applied to the implementation's result (inside the `try`, so +/// a wrong type is reported like any other failure), and the value returned +/// after a failure. `None` for a void method. +fn py_trampoline_return(ret: Option<&Ty>) -> Option<(&'static str, &'static str)> { + match ret { + None => None, + Some(Ty::Bool) => Some(("bool", "False")), + Some(Ty::F32 | Ty::F64) => Some(("float", "0.0")), + // Integers and C-style enums (an `IntEnum` is an `int`). + Some(_) => Some(("int", "0")), + } +} + +/// Render one callback interface: the abstract base class the consumer +/// subclasses, then the ABI side satisfying +/// [`weaveffi_model::plan::CallbackProtocol`]: one `CFUNCTYPE` per method +/// plus `free`, the `ctypes.Structure` mirroring the C vtable, one +/// trampoline per method, and the single process-wide static vtable whose +/// function objects are pinned at module scope. +/// +/// Each trampoline looks its implementation up by the integer `ctx`, +/// decodes the arguments, and calls the method. Any exception is reported +/// through `{prefix}_error_set(out_err, -4, message)` and a default value is +/// returned, so nothing unwinds through the C frame. `ctypes` acquires the +/// GIL on entry, so the producer may call from any thread. +pub(crate) fn render_callback_interface( + out: &mut String, + g: &Gen<'_>, + cb: &CallbackInterfaceBinding, +) { + let name = &cb.name; + let mut w = CodeWriter::four_space(); + + w.blank().blank(); + w.line(format!("class {name}(abc.ABC):")); + w.indent(); + let base_doc = format!( + "Consumer-implemented callback interface. Subclass it, implement every\n\ + abstract method, and pass an instance wherever the API takes a `{name}`;\n\ + the producer may call the methods from any thread until it releases the\n\ + instance. An exception raised by a method is reported to the producer,\n\ + which fails the call in progress with {}.FOREIGN_ERROR_CODE (-4).", + g.root_error + ); + let class_doc = match &cb.doc { + Some(d) if !d.trim().is_empty() => format!("{}\n\n{base_doc}", d.trim()), + _ => base_doc, + }; + let mut doc = String::new(); + emit_docstring(&mut doc, &Some(class_doc), &w.indent_str()); + w.raw(doc); + for m in &cb.methods { + let mut sig: Vec = vec!["self".into()]; + sig.extend( + m.params + .iter() + .map(|p| format!("{}: {}", py_name(&p.name), py_type_hint(&p.ty))), + ); + let ret = m + .ret + .as_ref() + .map(py_type_hint) + .unwrap_or_else(|| "None".to_string()); + w.blank(); + w.line("@abc.abstractmethod"); + w.line(format!( + "def {}({}) -> {ret}:", + py_member_name(&m.name), + sig.join(", ") + )); + w.indent(); + let mut mdoc = String::new(); + emit_fn_docstring(&mut mdoc, &m.doc, &m.params, &w.indent_str(), None); + if mdoc.is_empty() { + w.line("..."); + } else { + w.raw(mdoc); + } + w.dedent(); + } + w.dedent(); + + // One function-pointer type per vtable entry, then the vtable layout: + // methods in declaration order, then `free`. + w.blank().blank(); + for m in &cb.methods { + w.line(format!( + "_{name}_{}_t = {}", + m.name, + cfunctype(&m.abi_ret, &m.abi_params) + )); + } + w.line(format!( + "_{name}_free_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p)" + )); + w.blank().blank(); + w.line(format!("class _{name}Vtable(ctypes.Structure):")); + w.scope(|w| { + w.line(format!("\"\"\"The C vtable `{}`.\"\"\"", cb.vtable_tag)); + w.blank(); + w.line("_fields_ = ["); + w.scope(|w| { + for m in &cb.methods { + w.line(format!("(\"{0}\", _{name}_{0}_t),", m.name)); + } + w.line(format!("(\"free\", _{name}_free_t),")); + }); + w.line("]"); + }); + + for m in &cb.methods { + render_trampoline(&mut w, cb, m); + } + + // The one static vtable. Each field keeps its function object alive, and + // the vtable itself lives at module scope for the process lifetime. + w.blank().blank(); + w.line(format!("_{name}_vtable = _{name}Vtable(")); + w.scope(|w| { + for m in &cb.methods { + w.line(format!("_{name}_{0}_t(_{name}_{0}),", m.name)); + } + w.line(format!("_{name}_free_t(_callback_free),")); + }); + w.line(")"); + out.push_str(&w.finish()); +} + +/// Render the trampoline for one callback method: a `def` whose parameters +/// are the vtable entry's C slots (`ctx`, the parameter slots, `out_err`). +fn render_trampoline(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, m: &CallbackMethodBinding) { + let slots: Vec = m.abi_params.iter().map(|p| py_slot_name(&p.name)).collect(); + let args: Vec = m.params.iter().map(py_trampoline_arg).collect(); + let call = format!( + "_callback_get(ctx).{}({})", + py_member_name(&m.name), + args.join(", ") + ); + let ret = py_trampoline_return(m.ret.as_ref()); + let ret_hint = match ret { + Some((coerce, _)) => coerce, + None => "None", + }; + w.blank().blank(); + w.line(format!( + "def _{}_{}({}) -> {ret_hint}:", + cb.name, + m.name, + slots.join(", ") + )); + w.scope(|w| { + w.line("try:"); + w.scope(|w| match ret { + Some((coerce, _)) => { + w.line(format!("return {coerce}({call})")); + } + None => { + w.line(&call); + } + }); + w.line("except BaseException as exc:"); + w.scope(|w| { + w.line("_callback_fail(out_err, exc)"); + if let Some((_, default)) = ret { + w.line(format!("return {default}")); + } + }); + }); +} diff --git a/crates/weaveffi-gen/src/targets/python/codec.rs b/crates/weaveffi-gen/src/targets/python/codec.rs new file mode 100644 index 00000000..a529e5ec --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/codec.rs @@ -0,0 +1,272 @@ +//! Value-buffer codec emitters: the inline read expressions and write +//! statements for any wire shape, plus the per-record and per-rich-enum +//! codec functions (`_write_X`, `_read_X`). +//! +//! Every dispatch here goes through [`Ty::wire`], and primitives dispatch +//! on [`Prim::snake`] (`write_i32`, `read_string`), so this module never +//! re-derives the wire folds. + +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::model::Ty; +use weaveffi_model::model::{EnumBinding, StructBinding}; +use weaveffi_model::model::{Prim, WireType}; + +use crate::targets::python::types::{py_field, py_variant}; + +/// `_write_{Name}`, the statement-level writer for a record or rich enum. +/// `name` may be a qualified IR reference; the emitted function uses the +/// bare local class name. +pub(crate) fn py_write_fn_name(name: &str) -> String { + format!("_write_{}", local_type_name(name)) +} + +/// `_read_{Name}`, the reader consuming one encoded value from a `_Reader`. +pub(crate) fn py_read_fn_name(name: &str) -> String { + format!("_read_{}", local_type_name(name)) +} + +/// The `struct` format character and byte width of a fixed-width numeric +/// primitive, for the packed list fast path. +fn numeric_format(p: Prim) -> Option<(char, usize)> { + Some(match p { + Prim::I8 => ('b', 1), + Prim::U8 => ('B', 1), + Prim::I16 => ('h', 2), + Prim::U16 => ('H', 2), + Prim::I32 => ('i', 4), + Prim::U32 => ('I', 4), + Prim::I64 => ('q', 8), + Prim::U64 => ('Q', 8), + Prim::F32 => ('f', 4), + Prim::F64 => ('d', 8), + Prim::Bool | Prim::String | Prim::Bytes => return None, + }) +} + +/// The packed fast path for a list of one numeric primitive, if `inner` is +/// one. +fn numeric_list(inner: &Ty) -> Option<(char, usize)> { + match inner.wire() { + WireType::Prim(p) => numeric_format(p), + _ => None, + } +} + +/// The Python expression reading one `ty` value from the reader `_r`, +/// following the value-buffer wire format. `depth` uniquifies comprehension +/// loop variables when composites nest. +/// +/// Expressions (rather than statements) compose: Python evaluates a +/// comprehension's `range(_r.read_count())` 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: &Ty, depth: usize) -> String { + match ty.wire() { + WireType::Prim(p) => format!("_r.read_{}()", p.snake()), + // An object token carries one strong reference: adopt it into a new + // wrapper. A decode that fails later in the buffer drops the + // wrapper, and its finalizer releases the adopted reference. + WireType::Object(name) => { + format!("{}._adopt(_r.read_object())", local_type_name(name)) + } + 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!( + "({} if _r.read_flag() else None)", + py_read_expr(inner, depth) + ), + WireType::List(inner) => match numeric_list(inner) { + Some((code, size)) => format!("_r.read_numbers(\"{code}\", {size})"), + None => format!( + "[{} for _i{depth} in range(_r.read_count())]", + py_read_expr(inner, depth + 1) + ), + }, + WireType::Map(k, v) => format!( + "dict(({}, {}) for _i{depth} in range(_r.read_count()))", + py_read_expr(k, depth + 1), + py_read_expr(v, depth + 1) + ), + } +} + +/// Append the statements writing `expr` (one `ty` value) into the `_Writer` +/// named `writer`, following the value-buffer wire format. `next` numbers +/// loop variables, so every loop in one function body gets its own. +pub(crate) fn py_write_stmts( + w: &mut CodeWriter, + writer: &str, + expr: &str, + ty: &Ty, + next: &mut usize, +) { + match ty.wire() { + WireType::Prim(p) => { + w.line(format!("{writer}.write_{}({expr})", p.snake())); + } + // The token written must be a reference the reader can own: the + // writer clones the wrapper's pointer when the buffer is finished, + // never writing the pointer the wrapper still holds. + WireType::Object(name) => { + w.line(format!( + "{writer}.write_object({expr}, {})", + local_type_name(name) + )); + } + // IntEnum members are ints, so the discriminant packs directly. + WireType::Enum(_) => { + w.line(format!("{writer}.write_i32({expr})")); + } + WireType::User(name) => { + w.line(format!("{}({writer}, {expr})", py_write_fn_name(name))); + } + WireType::Optional(inner) => { + w.line(format!("if {expr} is None:")); + w.scope(|w| { + w.line(format!("{writer}.write_flag(False)")); + }); + w.line("else:"); + w.scope(|w| { + w.line(format!("{writer}.write_flag(True)")); + py_write_stmts(w, writer, expr, inner, next); + }); + } + WireType::List(inner) => match numeric_list(inner) { + Some((code, _)) => { + w.line(format!("{writer}.write_numbers(\"{code}\", {expr})")); + } + None => { + let i = *next; + *next += 1; + w.line(format!("{writer}.write_count(len({expr}))")); + w.line(format!("for _e{i} in {expr}:")); + w.scope(|w| { + py_write_stmts(w, writer, &format!("_e{i}"), inner, next); + }); + } + }, + WireType::Map(k, v) => { + let i = *next; + *next += 1; + w.line(format!("{writer}.write_count(len({expr}))")); + w.line(format!("for _k{i}, _v{i} in {expr}.items():")); + w.scope(|w| { + py_write_stmts(w, writer, &format!("_k{i}"), k, next); + py_write_stmts(w, writer, &format!("_v{i}"), v, next); + }); + } + } +} + +/// The expression decoding a value-buffer `bytes` expression `data` into +/// one `ty` value, rejecting trailing data. +pub(crate) fn py_decode_expr(data: &str, ty: &Ty) -> String { + match ty.wire() { + WireType::User(name) => format!("_decode({data}, {})", py_read_fn_name(name)), + _ => format!("_decode({data}, lambda _r: {})", py_read_expr(ty, 0)), + } +} + +/// Append a record's buffer codec functions: the statement writer and the +/// reader. +pub(crate) fn render_record_codecs(w: &mut CodeWriter, s: &StructBinding) { + let name = &s.name; + w.blank().blank(); + w.line(format!( + "def {}(_w: _Writer, value: \"{name}\") -> None:", + py_write_fn_name(name) + )); + w.scope(|w| { + if s.fields.is_empty() { + w.line("pass"); + } + let mut next = 0; + for f in &s.fields { + let expr = format!("value.{}", py_field(&f.name)); + py_write_stmts(w, "_w", &expr, &f.ty, &mut next); + } + }); + + w.blank().blank(); + w.line(format!( + "def {}(_r: _Reader) -> \"{name}\":", + py_read_fn_name(name) + )); + w.scope(|w| { + if s.fields.is_empty() { + w.line(format!("return {name}()")); + return; + } + // Keyword arguments evaluate left to right, matching the wire order + // of the record's fields. + w.line(format!("return {name}(")); + w.scope(|w| { + for f in &s.fields { + w.line(format!("{}={},", py_field(&f.name), py_read_expr(&f.ty, 0))); + } + }); + w.line(")"); + }); +} + +/// Append a rich enum's buffer codec functions: the statement writer and +/// the reader. The wire shape is an `i32` tag followed by the active +/// variant's fields. +pub(crate) fn render_rich_enum_codecs(w: &mut CodeWriter, e: &EnumBinding) { + let name = &e.name; + w.blank().blank(); + w.line(format!( + "def {}(_w: _Writer, value: \"{name}\") -> None:", + py_write_fn_name(name) + )); + w.scope(|w| { + let mut next = 0; + for v in &e.variants { + 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)); + for f in &v.fields { + let expr = format!("value.{}", py_field(&f.name)); + py_write_stmts(w, "_w", &expr, &f.ty, &mut next); + } + w.line("return"); + }); + } + w.line(format!( + "raise TypeError(f\"expected a {name} variant, got {{type(value).__name__}}\")" + )); + }); + + w.blank().blank(); + w.line(format!( + "def {}(_r: _Reader) -> \"{name}\":", + py_read_fn_name(name) + )); + w.scope(|w| { + w.line("_tag = _r.read_i32()"); + for v in &e.variants { + let class = format!("{name}{}", py_variant(&v.name)); + w.line(format!("if _tag == {}:", v.value)); + w.scope(|w| { + if v.fields.is_empty() { + w.line(format!("return {class}()")); + return; + } + // Keyword arguments evaluate left to right, matching the + // wire order of the variant's fields. + w.line(format!("return {class}(")); + w.scope(|w| { + for f in &v.fields { + w.line(format!("{}={},", py_field(&f.name), py_read_expr(&f.ty, 0))); + } + }); + w.line(")"); + }); + } + w.line(format!( + "raise _malformed(f\"unknown {name} tag {{_tag}}\")" + )); + }); +} diff --git a/crates/weaveffi-gen-python/src/docs.rs b/crates/weaveffi-gen/src/targets/python/docs.rs similarity index 92% rename from crates/weaveffi-gen-python/src/docs.rs rename to crates/weaveffi-gen/src/targets/python/docs.rs index d2f6bc95..aac36ed9 100644 --- a/crates/weaveffi-gen-python/src/docs.rs +++ b/crates/weaveffi-gen/src/targets/python/docs.rs @@ -2,10 +2,10 @@ //! docstrings, and the NumPy-style function docstring with `Parameters` and //! `Raises` sections. -use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; -use weaveffi_core::model::ParamBinding; +use crate::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; +use weaveffi_model::model::ParamBinding; -use crate::types::py_name; +use crate::targets::python::types::py_name; /// Emits a Python `# ...` line comment at `indent`. Used above C ABI binding /// declarations (`attach_function`-style binds) where docstrings can't live. @@ -66,7 +66,9 @@ pub(crate) fn emit_fn_docstring( .unwrap_or(false) }) .collect(); - if trimmed_doc.is_none() && documented_params.is_empty() && raises.is_none() { + if documented_params.is_empty() && raises.is_none() { + // Nothing beyond the summary: an ordinary docstring. + emit_docstring(out, &trimmed_doc.map(str::to_string), indent); return; } out.push_str(indent); diff --git a/crates/weaveffi-gen-python/src/entities.rs b/crates/weaveffi-gen/src/targets/python/entities.rs similarity index 50% rename from crates/weaveffi-gen-python/src/entities.rs rename to crates/weaveffi-gen/src/targets/python/entities.rs index 7450264d..6851f950 100644 --- a/crates/weaveffi-gen-python/src/entities.rs +++ b/crates/weaveffi-gen/src/targets/python/entities.rs @@ -1,54 +1,82 @@ //! Entity rendering: error domains, plain and rich enums, record //! dataclasses, and interface wrapper classes. +use crate::codegen::CodeWriter; use heck::{ToShoutySnakeCase, ToSnakeCase}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::Ty; -use weaveffi_core::model::{ - CallShape, EnumBinding, ErrorBinding, ErrorCodeBinding, FieldBinding, FnBinding, +use weaveffi_model::model::{ + BindingModel, CallShape, EnumBinding, ErrorBinding, ErrorCodeBinding, FieldBinding, FnBinding, InterfaceBinding, ModuleBinding, StructBinding, }; -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, py_variant}; +use crate::targets::python::calls::{render_bindings, render_callable, FnScope}; +use crate::targets::python::codec::{py_read_expr, render_record_codecs, render_rich_enum_codecs}; +use crate::targets::python::docs::emit_docstring; +use crate::targets::python::types::{ + py_binding_name, py_field, py_field_hint, py_str_literal, py_variant, +}; +use crate::targets::python::Gen; // ── Errors ── +/// The root exception class every error the bindings raise derives from: +/// `Error` (so consumers write `except kvstore.Error`), unless the API +/// declares a type or error code with that name, in which case +/// `{PascalName}Error`, then `{PascalName}BaseError`. +pub(crate) fn root_error_name(model: &BindingModel, pascal_name: &str) -> String { + let taken = |name: &str| { + model.modules.iter().any(|m| { + m.enums.iter().any(|e| e.name == name) + || m.structs.iter().any(|s| s.name == name) + || m.interfaces.iter().any(|i| i.name == name) + || m.callback_interfaces.iter().any(|c| c.name == name) + || m.error.as_ref().is_some_and(|e| { + e.type_name == name + || e.codes.iter().any(|c| py_code_class_name(&c.name) == name) + }) + }) + }; + [ + "Error".to_string(), + format!("{pascal_name}Error"), + format!("{pascal_name}BaseError"), + ] + .into_iter() + .find(|n| !taken(n)) + .unwrap_or_else(|| format!("{pascal_name}RootError")) +} + /// The snake_case stem shared by a domain's private helper names. fn py_error_stem(eb: &ErrorBinding) -> String { eb.type_name.to_snake_case() } /// `_{stem}_from`: builds the domain exception matching an ABI code. -pub(crate) fn py_error_factory_name(eb: &ErrorBinding) -> String { +pub(crate) fn py_factory_name(eb: &ErrorBinding) -> String { format!("_{}_from", py_error_stem(eb)) } /// `_check_{stem}`: raises the domain exception for a non-zero out-err slot. -fn py_error_checker_name(eb: &ErrorBinding) -> String { +fn py_domain_checker_name(eb: &ErrorBinding) -> String { format!("_check_{}", py_error_stem(eb)) } /// The error-check call a callable's out-err slot goes through: the module /// domain's typed checker when the callable throws, the generic -/// `_check_error` (plain `WeaveFFIError`, panics and marshalling failures -/// only) otherwise. +/// `_check_error` (the root exception: panics, marshalling failures, and +/// failed callbacks only) otherwise. pub(crate) fn py_checker_name(f: &FnBinding, error: Option<&ErrorBinding>) -> String { match error { - Some(eb) if f.throws => py_error_checker_name(eb), + Some(eb) if f.throws => py_domain_checker_name(eb), _ => "_check_error".to_string(), } } /// The Python class name for one error code: plain PascalCase with no forced -/// suffix (`KeyNotFound`, not `KeyNotFoundError`), matching the new samples' -/// already-Pascal code names. Each class is also attached to its domain class -/// (`KvError.KeyNotFound`), which stays unambiguous even if two domains -/// declare codes with the same name. +/// suffix (`KeyNotFound`, not `KeyNotFoundError`). Each class is also +/// attached to its domain class (`KvError.KeyNotFound`), which stays +/// unambiguous even if two domains declare codes with the same name. pub(crate) fn py_code_class_name(name: &str) -> String { - weaveffi_core::errors::pascal(name) + weaveffi_model::errors::pascal(name) } /// `_{stem}_payload_{code}`: decodes one code's payload fields onto an @@ -62,28 +90,40 @@ fn py_payload_decoder_name(eb: &ErrorBinding, code: &ErrorCodeBinding) -> String } /// Render one module's declared error domain: a base exception named after -/// the domain (subclassing the generic `WeaveFFIError`), one exception -/// subclass per code carrying its stable `CODE` and default message, the -/// code-to-class table, per-code payload decoders, and the factory/checker -/// helpers throwing wrappers route their out-err slots through. Each code -/// class is also attached to the domain class, so consumers can catch +/// the domain (subclassing the root exception), one exception subclass per +/// code carrying its stable `CODE` and default message, the code-to-class +/// table, per-code payload decoders, and the factory/checker helpers +/// throwing wrappers route their out-err slots through. Each code class is +/// also attached to the domain class, so consumers can catch /// `KvError.KeyNotFound`. -pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorBinding) { +pub(crate) fn render_error( + out: &mut String, + g: &Gen<'_>, + module: &ModuleBinding, + eb: &ErrorBinding, +) { let domain = &eb.type_name; - let factory = py_error_factory_name(eb); - let checker = py_error_checker_name(eb); + let root = g.root_error; + let factory = py_factory_name(eb); + let checker = py_domain_checker_name(eb); let table = format!("_{}_CODES", eb.type_name.to_shouty_snake_case()); let payloads = format!("_{}_PAYLOADS", eb.type_name.to_shouty_snake_case()); let has_payloads = eb.codes.iter().any(|c| !c.fields.is_empty()); let mut w = CodeWriter::four_space(); w.blank().blank(); - w.line(format!("class {domain}(WeaveFFIError):")); + w.line(format!("class {domain}({root}):")); w.scope(|w| { w.line(format!( "\"\"\"Base exception for the `{}` module's error domain.\"\"\"", - module.path + module.dot_path )); + // The per-code classes, attached below once they exist. + w.blank(); + for c in &eb.codes { + let class = py_code_class_name(&c.name); + w.line(format!("{class}: \"Type[{class}]\"")); + } }); for c in &eb.codes { @@ -119,7 +159,7 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB } w.blank().blank(); - w.line(format!("{table}: Dict[int, type] = {{")); + w.line(format!("{table}: Dict[int, Any] = {{")); w.scope(|w| { for c in &eb.codes { let class = py_code_class_name(&c.name); @@ -132,16 +172,11 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB // reads the code's fields (in declaration order) from the payload buffer // and attaches them as attributes on the exception instance. if has_payloads { - for c in &eb.codes { - if c.fields.is_empty() { - continue; - } + for c in eb.codes.iter().filter(|c| !c.fields.is_empty()) { let decoder = py_payload_decoder_name(eb, c); let class = py_code_class_name(&c.name); w.blank().blank(); - w.line(format!( - "def {decoder}(_exc: WeaveFFIError, _r: _BufferReader) -> None:" - )); + w.line(format!("def {decoder}(_exc: {root}, _r: _Reader) -> None:")); w.scope(|w| { w.line(format!( "\"\"\"Decode the {class} payload fields onto `_exc`.\"\"\"" @@ -156,12 +191,11 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB }); } w.blank().blank(); - w.line(format!("{payloads}: Dict[int, Callable] = {{")); + w.line(format!( + "{payloads}: Dict[int, Callable[[{root}, _Reader], None]] = {{" + )); w.scope(|w| { - for c in &eb.codes { - if c.fields.is_empty() { - continue; - } + for c in eb.codes.iter().filter(|c| !c.fields.is_empty()) { w.line(format!("{}: {},", c.value, py_payload_decoder_name(eb, c))); } }); @@ -170,45 +204,39 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB w.blank().blank(); w.line(format!( - "def {factory}(code: int, message: str, payload: bytes = b\"\") -> WeaveFFIError:" + "def {factory}(code: int, message: str, payload: bytes = b\"\") -> {root}:" )); w.scope(|w| { w.line(format!( - "\"\"\"Build the {domain} subclass matching `code`, or a generic" + "\"\"\"Build the {domain} subclass matching `code`, or the plain" + )); + w.line(format!( + "{root} for codes outside the domain (panics, marshalling).\"\"\"" )); - w.line("WeaveFFIError for codes outside the domain (panics, marshalling).\"\"\""); - w.line(format!("_cls = {table}.get(code)")); - w.line("if _cls is None:"); + w.line(format!("cls = {table}.get(code)")); + w.line("if cls is None:"); w.scope(|w| { - w.line("return WeaveFFIError(code, message)"); + w.line(format!("return {root}(code, message)")); }); - w.line("_exc = _cls(message) if message else _cls()"); + w.line("exc: Any = cls(message) if message else cls()"); if has_payloads { - w.line(format!("_decoder = {payloads}.get(code)")); - w.line("if _decoder is not None and payload:"); + w.line(format!("decoder = {payloads}.get(code)")); + w.line("if decoder is not None and payload:"); w.scope(|w| { - w.line("_r = _BufferReader(payload)"); - w.line("_decoder(_exc, _r)"); - w.line("_r.expect_end()"); + w.line("r = _Reader(payload)"); + w.line("decoder(exc, r)"); + w.line("r.expect_end()"); }); } - w.line("return _exc"); + w.line("return exc"); }); w.blank().blank(); - w.line(format!("def {checker}(err: _WeaveFFIErrorStruct) -> None:")); + w.line(format!("def {checker}(err: _ErrorStruct) -> None:")); w.scope(|w| { - w.line("if err.code != 0:"); + w.line("if err.code:"); w.scope(|w| { - w.line("code = err.code"); - w.line("message = err.message.decode(\"utf-8\") if err.message else \"\""); - // The payload is copied before `weaveffi_error_clear` frees it. - w.line( - "payload = ctypes.string_at(err.payload_ptr, err.payload_len) \ -if err.payload_ptr else b\"\"", - ); - w.line("_lib.weaveffi_error_clear(ctypes.byref(err))"); - w.line(format!("raise {factory}(code, message, payload)")); + w.line(format!("raise {factory}(*_read_error(err))")); }); }); @@ -220,8 +248,6 @@ if err.payload_ptr else b\"\"", /// Render one enum: a plain `IntEnum` for a C-style enum, or the dataclass /// sum-type hierarchy for a rich (algebraic) enum. pub(crate) fn render_enum(out: &mut String, e: &EnumBinding) { - // Rich (algebraic) enums are value sum types serialized into value - // buffers; they are emitted as dataclass hierarchies, not `IntEnum`s. if e.is_rich() { render_rich_enum(out, e); return; @@ -234,26 +260,28 @@ pub(crate) fn render_enum(out: &mut String, e: &EnumBinding) { emit_docstring(&mut doc, &e.doc, " "); w.raw(doc); for v in &e.variants { - if let Some(d) = &v.doc { - let trimmed = d.trim(); - if !trimmed.is_empty() { - for line in trimmed.lines() { - w.line(format!("# {}", line)); - } - } - } + emit_comment(&mut w, &v.doc); w.line(format!("{} = {}", py_variant(&v.name), v.value)); } out.push_str(&w.finish()); } +/// Emit a doc string as `#` comment lines at the writer's indent. +fn emit_comment(w: &mut CodeWriter, doc: &Option) { + if let Some(d) = doc { + for line in d.trim().lines() { + w.line(format!("# {line}").trim_end()); + } + } +} + /// Render a rich (algebraic) enum as an idiomatic Python sum type: a base /// class holding the nested `Tag` discriminant enum and a `tag` property, /// one module-level `@dataclass` subclass per variant carrying its fields, /// scoped aliases (`Shape.Circle` is `ShapeCircle`), and the buffer codec /// functions implementing the wire shape `i32 tag + active variant's /// fields`. Consumers construct variants directly and discriminate with -/// `isinstance` (or the `tag` property); no FFI symbols are involved. +/// `isinstance` (or the `tag` property). fn render_rich_enum(out: &mut String, e: &EnumBinding) { let name = &e.name; let mut w = CodeWriter::four_space(); @@ -266,22 +294,20 @@ fn render_rich_enum(out: &mut String, e: &EnumBinding) { w.raw(doc); w.blank(); } - // Nested discriminant enum (`Shape.Tag.Circle == 1`, ...). w.line("class Tag(IntEnum):"); w.scope(|w| { for v in &e.variants { - if let Some(d) = &v.doc { - let trimmed = d.trim(); - if !trimmed.is_empty() { - for line in trimmed.lines() { - w.line(format!("# {}", line)); - } - } - } + emit_comment(w, &v.doc); w.line(format!("{} = {}", py_variant(&v.name), v.value)); } }); w.blank(); + // The variant classes, attached below once they exist. + for v in &e.variants { + w.line(format!("{0}: \"Type[{name}{0}]\"", py_variant(&v.name))); + } + w.line(format!("TAG: \"{name}.Tag\"")); + w.blank(); w.line("@property"); w.line(format!("def tag(self) -> \"{name}.Tag\":")); w.scope(|w| { @@ -290,7 +316,6 @@ fn render_rich_enum(out: &mut String, e: &EnumBinding) { }); w.dedent(); - // One module-level dataclass per variant, subclassing the base. for v in &e.variants { let class = format!("{name}{}", py_variant(&v.name)); w.blank().blank(); @@ -357,121 +382,79 @@ pub(crate) fn render_struct(out: &mut String, s: &StructBinding) { /// comments. fn render_dataclass_fields(w: &mut CodeWriter, fields: &[FieldBinding]) { for f in fields { - if let Some(d) = &f.doc { - let trimmed = d.trim(); - if !trimmed.is_empty() { - for line in trimmed.lines() { - w.line(format!("# {}", line)); - } - } - } - w.line(format!("{}: {}", py_field(&f.name), py_type_hint(&f.ty))); + emit_comment(w, &f.doc); + w.line(format!( + "{}: {}", + py_field(&f.name), + py_field_hint(fields, &f.ty) + )); } } // ── Interfaces ── -/// Render one interface as a reference-counted object wrapper class. The -/// class holds one strong reference (the raw C pointer) and releases it -/// exactly once through the interface's destroy symbol, from `close()`, the -/// context-manager exit, or the `__del__` backstop. `_from_ptr` adopts a -/// pointer the producer handed over (a return, an async result, an iterator -/// element, or a buffer token); `_clone_ptr` produces a second strong -/// reference through the clone symbol, which the buffer codec uses when it -/// writes the object into a value buffer. A constructor named `new` becomes -/// `__init__`; every other constructor becomes a `@classmethod` factory; -/// methods lend `self` as the leading borrowed C argument; statics are -/// `@staticmethod`s. -pub(crate) fn render_interface(out: &mut String, module: &ModuleBinding, i: &InterfaceBinding) { +/// Render one interface as a reference-counted object wrapper class +/// (subclassing the runtime's `_Object`), preceded by the module-level +/// bindings of its members, clone, and destroy. The class holds one strong +/// reference and releases it exactly once, from `close()`, the +/// context-manager exit, or the `__del__` backstop; calls lend the pointer so +/// a concurrent `close()` defers the release until they return. `_adopt` +/// wraps a pointer the producer handed over (a return, an async result, an +/// iterator element, or a buffer token); `_clone_ref` mints a second strong +/// reference when the object is written into a value buffer. A constructor +/// named `new` becomes `__init__`; every other constructor becomes a +/// `@classmethod` factory; methods lend `self` as the leading C argument; +/// statics are `@staticmethod`s. +pub(crate) fn render_interface( + out: &mut String, + g: &Gen<'_>, + module: &ModuleBinding, + i: &InterfaceBinding, +) { let error = module.error.as_ref(); - - // `_...Iterator` helpers are module-level classes; emit them ahead of the - // 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(Ty::Iterator(inner)), CallShape::Iterator(it)) = (&m.ret, &m.shape) { - let checker = py_checker_name(m, error); - render_iterator_class( - out, - &it.iter_tag, - &format!("{}_{}", i.name, m.name), - inner, - &checker, - ); - } - } - let name = &i.name; - let clone = &i.clone_symbol; - let destroy = &i.destroy_symbol; - - // The reference-count pair is bound once, at module scope, so the - // wrapper's disposal path never touches the binding tables. + // Consecutive sync member bindings are packed into one block. + let mut packed = false; + for m in i.constructors.iter().chain(&i.methods).chain(&i.statics) { + render_bindings(out, g, m, error, &format!("{name}.{}", m.name), packed); + packed = matches!(m.shape, CallShape::Sync(_)); + } + let clone = py_binding_name(&i.clone_symbol, g.prefix); + let destroy = py_binding_name(&i.destroy_symbol, g.prefix); + if !packed { + out.push_str("\n\n"); + } out.push_str(&format!( - "\n\n_lib.{clone}.argtypes = [ctypes.c_void_p]\n\ - _lib.{clone}.restype = ctypes.c_void_p\n\ - _lib.{destroy}.argtypes = [ctypes.c_void_p]\n\ - _lib.{destroy}.restype = None\n" + "{clone} = _bind(\"{}\", ctypes.c_void_p, ctypes.c_void_p)\n\ + {destroy} = _bind(\"{}\", None, ctypes.c_void_p)\n", + i.clone_symbol, i.destroy_symbol )); - out.push_str(&format!("\n\nclass {name}:\n")); + out.push_str(&format!("\n\nclass {name}(_Object):\n")); emit_docstring(out, &i.doc, " "); - out.push_str(&format!( - "\n @classmethod\n def _from_ptr(cls, ptr) -> \"{name}\":" + "\n _destroy = staticmethod({destroy})\n _clone = staticmethod({clone})\n" )); - out.push_str("\n \"\"\"Adopt one strong reference the producer handed over.\"\"\""); - out.push_str("\n _obj = cls.__new__(cls)"); - out.push_str("\n _obj._ptr = ptr"); - out.push_str("\n return _obj"); - - let new_ctor = i.constructors.iter().find(|c| c.name == "new"); - if let Some(c) = new_ctor { - render_callable(out, c, error, &FnScope::Init); - } else { - // No canonical constructor: instances only come from factories and - // producer returns, so `__init__` is not a public path. - out.push_str("\n\n def __init__(self) -> None:"); - out.push_str(&format!( - "\n raise TypeError(\"{name} cannot be instantiated directly\")\n" - )); - } - out.push_str("\n\n def _clone_ptr(self):"); - out.push_str("\n \"\"\"A new strong reference to the same object (a raw pointer the"); - out.push_str("\n receiver must eventually release), leaving this wrapper's own"); - out.push_str("\n reference untouched.\"\"\""); - out.push_str(&format!("\n return _lib.{clone}(_borrow(self))")); - - out.push_str("\n\n def close(self) -> None:"); - out.push_str("\n \"\"\"Release this wrapper's reference. The object itself is dropped"); - out.push_str("\n when its last reference (here or in the producer) is released."); - out.push_str("\n Idempotent; the wrapper is unusable afterwards.\"\"\""); - out.push_str("\n _p = getattr(self, \"_ptr\", None)"); - out.push_str("\n self._ptr = None"); - out.push_str("\n if _p is not None:"); - out.push_str(&format!("\n _lib.{destroy}(_p)")); - - out.push_str(&format!("\n\n def __enter__(self) -> \"{name}\":")); - out.push_str("\n return self"); - - out.push_str("\n\n def __exit__(self, *exc) -> bool:"); - out.push_str("\n self.close()"); - out.push_str("\n return False"); - - out.push_str("\n\n def __del__(self) -> None:"); - out.push_str("\n self.close()"); - - for c in &i.constructors { - if c.name != "new" { - render_callable(out, c, error, &FnScope::Factory); + let is_new = |f: &&FnBinding| f.name == "new"; + match i.constructors.iter().find(is_new) { + Some(c) => render_callable(out, g, module, c, FnScope::Init), + None => { + // No canonical constructor: instances only come from factories + // and producer returns. + out.push_str("\n def __init__(self) -> None:"); + out.push_str(&format!( + "\n raise TypeError(\"{name} cannot be instantiated directly\")\n" + )); } } + for c in i.constructors.iter().filter(|c| c.name != "new") { + render_callable(out, g, module, c, FnScope::Factory); + } for m in &i.methods { - render_callable(out, m, error, &FnScope::Method { class: &i.name }); + render_callable(out, g, module, m, FnScope::Method); } for s in &i.statics { - render_callable(out, s, error, &FnScope::Static { class: &i.name }); + render_callable(out, g, module, s, FnScope::Static); } - out.push('\n'); } diff --git a/crates/weaveffi-gen/src/targets/python/mod.rs b/crates/weaveffi-gen/src/targets/python/mod.rs new file mode 100644 index 00000000..337bd837 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/mod.rs @@ -0,0 +1,313 @@ +//! Python (`ctypes`) binding generator. +//! +//! Emits a pip-installable package of `ctypes` bindings and `.pyi` type +//! stubs over the C ABI (revision 3): distribution `{name}`, import package +//! `{prefix}`, implementation module `{prefix}/{prefix}.py`. Every C +//! prototype is bound once at import time, after the load-time ABI and +//! contract-checksum check. Records and rich enums are dataclasses crossing +//! the boundary as value buffers; interfaces are reference-counted wrapper +//! classes with `close()` and a `__del__` backstop; callback interfaces are +//! abstract base classes backed by one static vtable of `ctypes` +//! trampolines; async functions are coroutines whose cancellation cancels +//! the native call; `iter` returns are lazy Python iterators. + +mod calls; +mod codec; +mod docs; +mod entities; +mod package; +mod runtime; +mod stubs; +#[cfg(test)] +mod tests; +mod types; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::package::{PackageContext, PackagedFile}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::{BindingModel, ModuleBinding}; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; + +use crate::targets::python::calls::{ + render_bindings, render_callable, render_callback_interface, FnScope, +}; +use crate::targets::python::entities::{ + render_enum, render_error, render_interface, render_struct, root_error_name, +}; +use crate::targets::python::package::{ + render_packaged_readme, render_packaged_setup_py, render_py_typed, render_pyproject_toml, + render_readme, +}; +use crate::targets::python::runtime::{render_runtime, RuntimeNames}; +use crate::targets::python::stubs::render_pyi_module; + +/// Per-target configuration for [`PythonGenerator`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct PythonConfig { + /// The distribution name published to PyPI (default: the package + /// identity's `name`). + pub package_name: Option, + /// The import package (default: the identity's C prefix, which is + /// always a valid Python identifier). Also names the package directory + /// and the implementation module inside it. + pub import_name: Option, + /// When `true` (the default), strip the IR module name prefix from + /// emitted Python function names, so a `contacts` module exports + /// `create_contact` rather than `contacts_create_contact`. Set to + /// `false` to restore module-prefixed names. + pub strip_module_prefix: bool, + /// Basename of the IDL the CLI was invoked with. + #[serde(skip)] + pub input_basename: Option, +} + +impl Default for PythonConfig { + fn default() -> Self { + Self { + package_name: None, + import_name: None, + strip_module_prefix: true, + input_basename: None, + } + } +} + +impl PythonConfig { + /// The input IDL basename embedded in generated file headers. + pub fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } +} + +/// The names one generation run uses, resolved from the identity and the +/// configuration. +struct Names { + /// The distribution name. + dist: String, + /// The import package and implementation module name. + import: String, +} + +impl Names { + fn new(id: &Identity, config: &PythonConfig) -> Self { + Self { + dist: config + .package_name + .clone() + .unwrap_or_else(|| id.name.clone()), + import: config + .import_name + .clone() + .unwrap_or_else(|| id.prefix.clone()), + } + } +} + +/// The shared context every rendering function receives. +pub(crate) struct Gen<'a> { + /// The C symbol prefix (stripped from binding names). + pub prefix: &'a str, + /// The root exception class name. + pub root_error: &'a str, + /// Whether free functions drop their module-path prefix. + pub strip_module_prefix: bool, +} + +/// Python backend: emits a pip-installable package of `ctypes` bindings and +/// `.pyi` type stubs over the C ABI exposed by the underlying cdylib. +pub struct PythonGenerator; + +impl PythonGenerator { + /// Render the implementation module: the runtime, then every module's + /// members in canonical order (error domain, enums, records, callback + /// interfaces, interfaces, functions). + fn render_py_source( + &self, + api: &ResolvedApi, + model: &BindingModel, + config: &PythonConfig, + file_name: &str, + ) -> String { + let id = api.identity(); + let root_error = root_error_name(model, &id.pascal_name()); + let g = Gen { + prefix: &model.prefix, + root_error: &root_error, + strip_module_prefix: config.strip_module_prefix, + }; + let mut out = render_prelude(CommentStyle::Hash, config.input_basename()); + render_runtime( + &mut out, + model, + &RuntimeNames { + identity: id, + error: &root_error, + }, + ); + for m in &model.modules { + out.push_str(&format!("\n\n# === Module: {} ===\n", m.dot_path)); + emit_module(&mut out, &g, m); + } + out.push('\n'); + out.push_str(&render_trailer(CommentStyle::Hash, file_name)); + out + } + + /// The `__init__.py` re-exporting the implementation module. + fn render_init(&self, import: &str, config: &PythonConfig) -> String { + let hash = CommentStyle::Hash; + format!( + "{}from .{import} import * # noqa: F401,F403\n\n{}", + render_prelude(hash, config.input_basename()), + render_trailer(hash, "__init__.py"), + ) + } + + /// The implementation module and its stub, keyed by file name. + fn package_sources( + &self, + api: &ResolvedApi, + model: &BindingModel, + config: &PythonConfig, + names: &Names, + ) -> [(String, String); 4] { + let py = format!("{}.py", names.import); + let pyi = format!("{}.pyi", names.import); + let root_error = root_error_name(model, &api.identity().pascal_name()); + [ + ( + "__init__.py".into(), + self.render_init(&names.import, config), + ), + (py.clone(), self.render_py_source(api, model, config, &py)), + ( + pyi.clone(), + render_pyi_module( + model, + &root_error, + config.strip_module_prefix, + config.input_basename(), + &pyi, + ), + ), + ("py.typed".into(), render_py_typed(config.input_basename())), + ] + } +} + +/// Emit every member of `m` in canonical order. Callback interfaces precede +/// interfaces because interface members take them as parameters. +fn emit_module(out: &mut String, g: &Gen<'_>, m: &ModuleBinding) { + if let Some(e) = m.error.as_ref().filter(|e| e.declared_here) { + render_error(out, g, m, e); + } + for e in &m.enums { + render_enum(out, e); + } + for s in &m.structs { + render_struct(out, s); + } + for cb in &m.callback_interfaces { + render_callback_interface(out, g, cb); + } + for i in &m.interfaces { + render_interface(out, g, m, i); + } + for f in &m.functions { + render_bindings(out, g, f, m.error.as_ref(), &f.name, false); + render_callable(out, g, m, f, FnScope::Free); + } +} + +impl LanguageBackend for PythonGenerator { + type Config = PythonConfig; + + fn name(&self) -> &'static str { + "python" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + let names = Names::new(api.identity(), config); + let input_basename = config.input_basename(); + let dir = out_dir.join("python"); + let pkg_dir = dir.join(&names.import); + let mut files: Vec = self + .package_sources(api, model, config, &names) + .into_iter() + .map(|(name, contents)| OutputFile::new(pkg_dir.join(name), contents)) + .collect(); + files.push(OutputFile::new( + dir.join("pyproject.toml"), + render_pyproject_toml(api.identity(), &names.dist, &names.import, input_basename), + )); + files.push(OutputFile::new( + dir.join("README.md"), + render_readme(api.identity(), &names.dist, &names.import, input_basename), + )); + files + } + + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let id = api.identity(); + let names = Names::new(id, config); + let input_basename = config.input_basename(); + let sources = self.package_sources(api, model, config, &names); + let setup_py = + render_packaged_setup_py(&names.dist, &id.version, &names.import, input_basename); + let pyproject = render_pyproject_toml(id, &names.dist, &names.import, input_basename); + + let py_dir = out_dir.join("python"); + let mut files = Vec::new(); + // Wheels exist only for the platforms that have a wheel platform tag; + // a binary for any other platform (Android, wasm32) has no wheel to + // land in and is skipped. + for nb in &ctx.binaries.binaries { + let platform = nb.platform; + let Some(tag) = platform.python_platform_tag() else { + continue; + }; + let tree = py_dir.join(platform.id()); + let pkg_dir = tree.join(&names.import); + for (name, contents) in &sources { + files.push(PackagedFile::text(pkg_dir.join(name), contents.clone())); + } + // Bundled under the file name the loader looks for. + files.push(PackagedFile::copy( + pkg_dir.join(platform.lib_filename(&id.library)), + nb.source.clone(), + )); + files.push(PackagedFile::text( + tree.join("pyproject.toml"), + pyproject.clone(), + )); + files.push(PackagedFile::text(tree.join("setup.py"), setup_py.clone())); + files.push(PackagedFile::text( + tree.join("README.md"), + render_packaged_readme(&names.dist, &names.import, platform, tag, input_basename), + )); + } + Some(files) + } +} diff --git a/crates/weaveffi-gen/src/targets/python/package.rs b/crates/weaveffi-gen/src/targets/python/package.rs new file mode 100644 index 00000000..e3a26ec3 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/package.rs @@ -0,0 +1,195 @@ +//! Packaging manifests: `pyproject.toml` and `README.md` for the generated +//! tree, plus the `setup.py` and README of each per-platform wheel tree the +//! `package` command assembles. + +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::pkg::Identity; + +/// Escape a string for a TOML basic string. +fn toml_str(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") +} + +/// Render the `pyproject.toml`: a PEP 621 project built with setuptools, +/// shipping the stub, the `py.typed` marker, and any bundled library as +/// package data. +pub(crate) fn render_pyproject_toml( + id: &Identity, + dist: &str, + import: &str, + input_basename: &str, +) -> String { + let prelude = render_prelude(CommentStyle::Hash, input_basename); + let trailer = render_trailer(CommentStyle::Hash, "pyproject.toml"); + let version = &id.version; + let description = toml_str(&id.description_or_default()); + let mut extra = String::new(); + if let Some(license) = &id.license { + extra.push_str(&format!( + "license = {{ text = \"{}\" }}\n", + toml_str(license) + )); + } + if !id.authors.is_empty() { + let authors = id + .authors + .iter() + .map(|a| format!("{{ name = \"{}\" }}", toml_str(a))) + .collect::>() + .join(", "); + extra.push_str(&format!("authors = [{authors}]\n")); + } + let mut urls = String::new(); + if let Some(homepage) = &id.homepage { + urls.push_str(&format!("Homepage = \"{}\"\n", toml_str(homepage))); + } + if let Some(repository) = &id.repository { + urls.push_str(&format!("Repository = \"{}\"\n", toml_str(repository))); + } + if !urls.is_empty() { + urls = format!("\n[project.urls]\n{urls}"); + } + format!( + r#"{prelude}[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "{dist}" +version = "{version}" +description = "{description}" +requires-python = ">=3.9" +{extra}{urls} +[tool.setuptools] +packages = ["{import}"] + +[tool.setuptools.package-data] +"{import}" = ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"] + +{trailer}"#, + ) +} + +/// Render the PEP 561 `py.typed` marker. Type checkers only look for the +/// file's presence (its content is ignored unless it says `partial`), so it +/// carries the standard prelude like every other generated file. +pub(crate) fn render_py_typed(input_basename: &str) -> String { + render_prelude(CommentStyle::Hash, input_basename) +} + +/// Render the `README.md` for the generated tree. +pub(crate) fn render_readme( + id: &Identity, + dist: &str, + import: &str, + input_basename: &str, +) -> String { + let prelude = render_prelude(CommentStyle::Xml, input_basename); + let trailer = render_trailer(CommentStyle::Xml, "README.md"); + let (darwin, linux, windows) = id.library_files(); + let env = id.library_env_var(); + format!( + r#"{prelude}# {dist} (Python) + +Python bindings for the `{library}` native library, built on `ctypes`. + +## Requirements + +- Python 3.9 or later +- The native library (`{linux}`, `{darwin}`, or `{windows}`) + +## Install + +```bash +pip install . +``` + +## Loading the native library + +The bindings load the library from `{env}` when it is set (a full path), +then from inside the installed package, then from the system loader's search +path. + +## Usage + +```python +import {import} +``` + +{trailer}"#, + library = id.library, + ) +} + +/// Render a `setup.py` for a packaged wheel: it ships the bundled library as +/// package data and forces a non-pure (platform-tagged) wheel. +pub(crate) fn render_packaged_setup_py( + dist: &str, + version: &str, + import: &str, + input_basename: &str, +) -> String { + let prelude = render_prelude(CommentStyle::Hash, input_basename); + let trailer = render_trailer(CommentStyle::Hash, "setup.py"); + format!( + r#"{prelude}from setuptools import setup +from setuptools.dist import Distribution + + +class _BinaryDistribution(Distribution): + # Force a non-pure, platform-tagged wheel: the package bundles a native + # shared library, so it is not portable across platforms. + def has_ext_modules(self): + return True + + +setup( + name="{dist}", + version="{version}", + packages=["{import}"], + package_data={{"{import}": ["py.typed", "*.pyi", "*.so", "*.dylib", "*.dll"]}}, + include_package_data=True, + distclass=_BinaryDistribution, +) + +{trailer}"#, + ) +} + +/// README for a packaged per-platform Python wheel tree. `tag` is the +/// platform's wheel tag (`platform.python_platform_tag()`), which the caller +/// has already established exists. +pub(crate) fn render_packaged_readme( + dist: &str, + import: &str, + platform: crate::platform::Platform, + tag: &str, + input_basename: &str, +) -> String { + let prelude = render_prelude(CommentStyle::Xml, input_basename); + let trailer = render_trailer(CommentStyle::Xml, "README.md"); + format!( + r#"{prelude}# {dist} (Python, {plat}) + +Python bindings with the native library bundled for `{plat}`. The library +loads automatically; no external setup is required. + +## Build the wheel + +```bash +python -m build --wheel +``` + +Tag the resulting wheel for this platform with `{tag}` (for example via +`wheel tags --platform-tag {tag} dist/*.whl`) before publishing. + +## Usage + +```python +import {import} +``` + +{trailer}"#, + plat = platform.id(), + ) +} diff --git a/crates/weaveffi-gen/src/targets/python/runtime.rs b/crates/weaveffi-gen/src/targets/python/runtime.rs new file mode 100644 index 00000000..9d2f61ef --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/runtime.rs @@ -0,0 +1,77 @@ +//! The fixed Python runtime every generated module starts with. +//! +//! The runtime is ordinary Python kept in `runtime/*.py` and spliced in with +//! `{{PLACEHOLDER}}` substitution: `core.py` (the root exception, library +//! loading, the load-time ABI and checksum check, the owned-result helpers, +//! the handle base classes, and the value-buffer codec), `aio.py` (the async +//! completion registry), `cancel.py` (cancel tokens), and `callbacks.py` +//! (the callback-interface handle table). The last three are emitted only +//! when the API uses the feature. + +use weaveffi_model::model::{checksum_symbol, BindingModel, ABI_VERSION}; +use weaveffi_model::pkg::Identity; + +const CORE: &str = include_str!("runtime/core.py"); +const AIO: &str = include_str!("runtime/aio.py"); +const CANCEL: &str = include_str!("runtime/cancel.py"); +const CALLBACKS: &str = include_str!("runtime/callbacks.py"); + +/// The identity-driven values the runtime templates are specialized with. +pub(crate) struct RuntimeNames<'a> { + /// The resolved identity of the bound library. + pub identity: &'a Identity, + /// The root exception class name (see [`super::entities::root_error_name`]). + pub error: &'a str, +} + +/// Substitute every `{{PLACEHOLDER}}` a runtime template uses. +fn fill(template: &str, model: &BindingModel, names: &RuntimeNames<'_>) -> String { + let id = names.identity; + let (darwin, linux, windows) = id.library_files(); + let checksums: String = model + .roots() + .map(|m| { + format!( + " (\"{}\", \"{}\", {:#018x}),\n", + m.name, + checksum_symbol(&model.prefix, &m.name), + m.checksum.expect("top-level modules carry a checksum"), + ) + }) + .collect(); + template + .replace("{{NAME}}", &id.name) + .replace("{{ERROR}}", names.error) + .replace("{{PREFIX}}", &model.prefix) + .replace("{{LIBRARY_ENV}}", &id.library_env_var()) + .replace("{{LIB_DARWIN}}", &darwin) + .replace("{{LIB_LINUX}}", &linux) + .replace("{{LIB_WINDOWS}}", &windows) + .replace("{{ABI_VERSION}}", &ABI_VERSION.to_string()) + .replace("{{CHECKSUMS}}\n", &checksums) +} + +/// Append the runtime: the core, then the feature parts the API needs. +pub(crate) fn render_runtime(out: &mut String, model: &BindingModel, names: &RuntimeNames<'_>) { + let callbacks = model.has_callback_interfaces(); + let is_async = model.has_async(); + let cancellable = model.callables().any(|(_, f)| f.cancellable); + let mut imports = String::new(); + if callbacks { + imports.push_str("import abc\n"); + } + if is_async { + imports.push_str("import asyncio\n"); + } + out.push_str(&fill(CORE, model, names).replace("{{IMPORTS}}", &imports)); + for (wanted, template) in [ + (is_async, AIO), + (cancellable, CANCEL), + (callbacks, CALLBACKS), + ] { + if wanted { + out.push_str("\n\n"); + out.push_str(&fill(template, model, names)); + } + } +} diff --git a/crates/weaveffi-gen/src/targets/python/runtime/aio.py b/crates/weaveffi-gen/src/targets/python/runtime/aio.py new file mode 100644 index 00000000..8482835d --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/runtime/aio.py @@ -0,0 +1,67 @@ +# Async completions. A call registers its future under an integer key, passes +# the key as the launcher's `context`, and the function's static completion +# trampoline settles it from whichever producer thread completes the call. +# The trampolines live at module scope, so nothing a pending call needs can +# be garbage collected before the producer fires the completion, even when +# the awaiting coroutine is gone. +_pending: Dict[int, Tuple[asyncio.AbstractEventLoop, "asyncio.Future[Any]"]] = {} +_pending_lock = threading.Lock() +_pending_next = 0 + +_error_free = _bind("{{PREFIX}}_error_free", None, ctypes.POINTER(_ErrorStruct)) + + +def _async_begin() -> Tuple[int, "asyncio.Future[Any]"]: + """Register a new pending call on the running event loop.""" + global _pending_next + loop = asyncio.get_running_loop() + future = loop.create_future() + with _pending_lock: + _pending_next += 1 + key = _pending_next + _pending[key] = (loop, future) + return key, future + + +def _async_abandon(key: int) -> None: + """Forget a call whose launcher never ran.""" + with _pending_lock: + _pending.pop(key, None) + + +def _async_error(err: Any, factory: Callable[[int, str, bytes], BaseException]) -> BaseException: + """Copy and release a heap-boxed completion error, then build the + exception it reports. The cancelled code maps to CancelledError.""" + e = err.contents + code = e.code + message = e.message.decode("utf-8", "replace") if e.message else "" + payload = ctypes.string_at(e.payload_ptr, e.payload_len) if e.payload_ptr else b"" + _error_free(err) + if code == {{ERROR}}.CANCELLED_ERROR_CODE: + return asyncio.CancelledError(message) + return factory(code, message, payload) + + +def _async_settle(key: Optional[int], exc: Optional[BaseException], value: Any) -> None: + """Hand a completion back to the event loop that awaits it.""" + with _pending_lock: + entry = _pending.pop(key or 0, None) + if entry is None: + return + loop, future = entry + try: + loop.call_soon_threadsafe(_async_resolve, future, exc, value) + except RuntimeError: + # The loop is closed, so nothing awaits the result any more. + pass + + +def _async_resolve(future: "asyncio.Future[Any]", exc: Optional[BaseException], value: Any) -> None: + if future.done(): + return + if isinstance(exc, asyncio.CancelledError): + future.cancel() + elif exc is not None: + future.set_exception(exc) + else: + future.set_result(value) diff --git a/crates/weaveffi-gen/src/targets/python/runtime/callbacks.py b/crates/weaveffi-gen/src/targets/python/runtime/callbacks.py new file mode 100644 index 00000000..c67ce0b2 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/runtime/callbacks.py @@ -0,0 +1,42 @@ +# Callback-interface implementations the producer holds, keyed by the integer +# it receives as `ctx`. An entry is added when an implementation is passed to +# a call and removed when the producer calls the vtable's `free(ctx)`, so the +# implementation lives exactly as long as the producer may call it and the +# producer never holds a raw reference to a Python object. ctypes acquires +# the GIL on entry to a trampoline, so the producer may call from any thread. +_callbacks: Dict[int, Any] = {} +_callbacks_lock = threading.Lock() +_callbacks_next = 0 + +_error_set = _bind( + "{{PREFIX}}_error_set", None, ctypes.POINTER(_ErrorStruct), ctypes.c_int32, ctypes.c_char_p +) + + +def _callback_register(impl: Any, cls: type) -> int: + """Register an implementation for one call; returns its `ctx` key.""" + global _callbacks_next + if not isinstance(impl, cls): + raise TypeError(f"expected {cls.__name__}, got {type(impl).__name__}") + with _callbacks_lock: + _callbacks_next += 1 + ctx = _callbacks_next + _callbacks[ctx] = impl + return ctx + + +def _callback_get(ctx: Optional[int]) -> Any: + return _callbacks[ctx or 0] + + +def _callback_free(ctx: Optional[int]) -> None: + # The producer's last reference is gone; it never passes `ctx` again. + with _callbacks_lock: + _callbacks.pop(ctx or 0, None) + + +def _callback_fail(out_err: Any, exc: BaseException) -> None: + """Report an implementation's exception to the producer, which aborts + its call with the foreign error code. Nothing unwinds through C.""" + message = str(exc) or type(exc).__name__ + _error_set(out_err, {{ERROR}}.FOREIGN_ERROR_CODE, message.encode("utf-8", "replace")) diff --git a/crates/weaveffi-gen/src/targets/python/runtime/cancel.py b/crates/weaveffi-gen/src/targets/python/runtime/cancel.py new file mode 100644 index 00000000..b87c81f3 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/runtime/cancel.py @@ -0,0 +1,24 @@ +# Cancellation for `cancellable` async functions. Each call creates a token, +# the producer takes its own reference at launch, and cancelling the awaiting +# task cancels the token; the producer then completes the call with the +# cancelled code. +_cancel_token_create = _bind("{{PREFIX}}_cancel_token_create", ctypes.c_void_p) +_cancel_token_cancel = _bind("{{PREFIX}}_cancel_token_cancel", None, ctypes.c_void_p) +_cancel_token_destroy = _bind("{{PREFIX}}_cancel_token_destroy", None, ctypes.c_void_p) + + +async def _async_wait_cancellable(future: "asyncio.Future[Any]", token: int) -> Any: + """Await a cancellable call. Cancelling the awaiting task cancels the + native call and waits for its (cancelled) completion before raising, so + nothing outlives the call.""" + try: + return await asyncio.shield(future) + except asyncio.CancelledError: + _cancel_token_cancel(token) + try: + await future + except BaseException: + pass + raise + finally: + _cancel_token_destroy(token) diff --git a/crates/weaveffi-gen/src/targets/python/runtime/core.py b/crates/weaveffi-gen/src/targets/python/runtime/core.py new file mode 100644 index 00000000..4495ec0a --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/runtime/core.py @@ -0,0 +1,516 @@ +"""ctypes bindings for the {{NAME}} native library.""" +{{IMPORTS}}import builtins +import ctypes +import os +import platform +import struct +import threading +import warnings +from dataclasses import dataclass +from enum import IntEnum +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type + + +class {{ERROR}}(Exception): + """An error reported by the native library. + + `code` is positive for one of the API's declared error codes (raised as + the declaring module's typed subclasses) and negative for a runtime trap: + a generic failure (-1), a producer panic (-2), a marshalling failure + (-3), or an exception raised by a callback-interface implementation + (-4). A cancelled async call (-5) raises `asyncio.CancelledError` + instead. + """ + + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + FOREIGN_ERROR_CODE = -4 + CANCELLED_ERROR_CODE = -5 + + def __init__(self, code: int, message: str) -> None: + super().__init__(f"({code}) {message}") + self.code = code + self.message = message + + +class _ErrorStruct(ctypes.Structure): + """The C `{{PREFIX}}_error` out-parameter every call reports through.""" + + _fields_ = [ + ("code", ctypes.c_int32), + ("message", ctypes.c_char_p), + ("payload_ptr", ctypes.c_void_p), + ("payload_len", ctypes.c_size_t), + ] + + +def _load_library() -> ctypes.CDLL: + # An explicit path wins; then a library bundled inside this package (a + # platform wheel); then the system loader's search path. + override = os.environ.get("{{LIBRARY_ENV}}") + if override: + return ctypes.CDLL(override) + system = platform.system() + if system == "Darwin": + name = "{{LIB_DARWIN}}" + elif system == "Windows": + name = "{{LIB_WINDOWS}}" + else: + name = "{{LIB_LINUX}}" + bundled = os.path.join(os.path.dirname(os.path.abspath(__file__)), name) + if os.path.exists(bundled): + return ctypes.CDLL(bundled) + try: + return ctypes.CDLL(name) + except OSError as exc: + raise ImportError( + f"cannot load the native library {name!r} ({exc}); " + "set {{LIBRARY_ENV}} to its path" + ) from exc + + +_lib = _load_library() + + +def _bind(name: str, restype: Any, *argtypes: Any) -> Any: + """Look up one exported symbol and fix its C signature. Every prototype + is bound once, at import time.""" + fn = getattr(_lib, name) + fn.restype = restype + fn.argtypes = argtypes + return fn + + +# The C ABI revision these bindings were generated against, and the contract +# checksum of every top-level module: (module, checksum symbol, value). +_ABI_VERSION = {{ABI_VERSION}} +_CHECKSUMS: List[Tuple[str, str, int]] = [ +{{CHECKSUMS}} +] + + +def _check_contract() -> None: + """Refuse to load a library built for a different ABI revision or a + different version of any module's API, before any other symbol is + bound.""" + try: + abi_version = _bind("{{PREFIX}}_abi_version", ctypes.c_uint32) + except AttributeError: + raise ImportError( + f"{_lib._name} does not export {{PREFIX}}_abi_version; " + "it is not the library these bindings were generated for" + ) from None + found = abi_version() + if found != _ABI_VERSION: + raise ImportError( + f"C ABI mismatch: these bindings expect revision {_ABI_VERSION}, " + f"but {_lib._name} reports revision {found}" + ) + for module, symbol, expected in _CHECKSUMS: + try: + checksum = _bind(symbol, ctypes.c_uint64) + except AttributeError: + raise ImportError( + f"module {module!r}: {_lib._name} does not export {symbol}" + ) from None + found = checksum() + if found != expected: + raise ImportError( + f"module {module!r}: the native library's contract checksum " + f"{found:#018x} does not match these bindings ({expected:#018x}); " + "regenerate the bindings from the library's current API" + ) + + +_check_contract() + +_error_clear = _bind("{{PREFIX}}_error_clear", None, ctypes.POINTER(_ErrorStruct)) +_free_bytes = _bind("{{PREFIX}}_free_bytes", None, ctypes.c_void_p, ctypes.c_size_t) + + +def _debug_live(kind: int) -> int: + """The producer's live-allocation counter `kind` (0 objects, 1 callbacks, + 2 iterators, 3 cancel tokens, 4 returned allocations). Always 0 unless + the producer was built with its leak counters enabled.""" + return _bind("{{PREFIX}}_debug_live", ctypes.c_uint64, ctypes.c_int32)(kind) + + +def _read_error(err: _ErrorStruct) -> Tuple[int, str, bytes]: + """Copy a filled out-err slot's code, message, and payload, then release + them.""" + code = err.code + message = err.message.decode("utf-8", "replace") if err.message else "" + payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + _error_clear(ctypes.byref(err)) + return code, message, payload + + +def _error_from(code: int, message: str, payload: bytes = b"") -> {{ERROR}}: + """The exception for a code no declared error domain claims.""" + return {{ERROR}}(code, message) + + +def _check_error(err: _ErrorStruct) -> None: + """Raise for a non-zero out-err slot of a call that declares no errors: + only runtime traps (a panic, a marshalling failure, a failed callback) + land here.""" + if err.code: + raise _error_from(*_read_error(err)) + + +def _take_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a producer-owned byte run (a string, bytes, or value buffer) and + release it with {{PREFIX}}_free_bytes.""" + if not ptr: + return b"" + try: + return ctypes.string_at(ptr, length) + finally: + _free_bytes(ptr, length) + + +def _take_str(ptr: Optional[int], length: int) -> str: + """Copy and release a producer-owned UTF-8 string.""" + return _take_bytes(ptr, length).decode("utf-8") + + +def _peek_bytes(ptr: Optional[int], length: int) -> bytes: + """Copy a byte run the producer only lends for the current call.""" + return ctypes.string_at(ptr, length) if ptr and length else b"" + + +class _Handle: + """Owns one native reference and releases it exactly once. + + Calls lend the pointer through `_acquire`/`_release`, so `close()` from + another thread while a call is in flight defers the release until the + last such call returns instead of freeing the object mid-call. + """ + + # The native destroy (or iterator release) function, set per subclass. + _destroy: Callable[[int], None] + + def _init_handle(self, ptr: int) -> None: + self._ptr: Optional[int] = ptr + self._calls = 0 + self._closing = False + self._lock = threading.Lock() + + @classmethod + def _adopt(cls, ptr: int) -> Any: + """Wrap one strong reference the producer handed over.""" + obj = cls.__new__(cls) + obj._init_handle(ptr) + return obj + + def _acquire(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise {{ERROR}}(-1, f"{type(self).__name__} used after close()") + self._calls += 1 + return ptr + + def _release(self) -> None: + with self._lock: + self._calls -= 1 + if self._calls or not self._closing: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def close(self) -> None: + """Release this wrapper's native reference. Idempotent; the wrapper + is unusable afterwards.""" + lock = getattr(self, "_lock", None) + if lock is None: + return + with lock: + if self._closing: + return + self._closing = True + if self._calls: + return + ptr, self._ptr = self._ptr, None + if ptr is not None: + type(self)._destroy(ptr) + + def __enter__(self) -> Any: + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def __del__(self) -> None: + # An unreachable wrapper has no call in flight: release directly. + ptr = getattr(self, "_ptr", None) + if ptr is not None: + self._ptr = None + type(self)._destroy(ptr) + + +class _Object(_Handle): + """Base of every interface wrapper: a reference-counted native object.""" + + # The native clone function, set per subclass. + _clone: Callable[[int], int] + + def _clone_ref(self) -> int: + """A new strong reference to the same object, for a receiver that + adopts it (an object token inside a value buffer).""" + ptr = self._acquire() + try: + return type(self)._clone(ptr) + finally: + self._release() + + +class _Iterator(_Handle): + """Base of every lazy iterator over a producer stream: one native `next` + call per step; the handle is released on exhaustion, `close()`, or + garbage collection.""" + + def __iter__(self) -> Any: + return self + + def _step(self) -> int: + with self._lock: + ptr = self._ptr + if ptr is None or self._closing: + raise StopIteration + self._calls += 1 + return ptr + + +def _required(ptr: Optional[int]) -> int: + """A non-null object pointer the producer returned.""" + if not ptr: + raise {{ERROR}}(-1, "the producer returned a null object") + return ptr + + +def _lend(obj: Any, cls: "Type[_Object]") -> int: + """Lend `obj`'s native pointer to one call; pair with `obj._release()`.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + return obj._acquire() + + +def _lend_opt(obj: Any, cls: "Type[_Object]") -> Optional[int]: + """`_lend` for an optional object: None lends a null pointer.""" + return None if obj is None else _lend(obj, cls) + + +def _release_opt(obj: Any) -> None: + if obj is not None: + obj._release() + + +_I8 = struct.Struct(" {{ERROR}}: + return {{ERROR}}(-3, f"malformed value buffer: {what}") + + +class _Writer: + """Encodes values in the value-buffer wire format: little-endian, + packed, no alignment.""" + + __slots__ = ("_buf", "_objects") + + def __init__(self) -> None: + self._buf = bytearray() + self._objects: List[Tuple[int, _Object]] = [] + + def finish(self) -> bytes: + # Object tokens are minted last, so an encoding that fails part-way + # never leaks a strong reference. + minted: List[Tuple[_Object, int]] = [] + try: + for pos, obj in self._objects: + ptr = obj._clone_ref() + minted.append((obj, ptr)) + _U64.pack_into(self._buf, pos, ptr) + except BaseException: + for obj, ptr in minted: + type(obj)._destroy(ptr) + raise + self._objects = [] + return bytes(self._buf) + + def write_bool(self, v: bool) -> None: + self._buf.append(1 if v else 0) + + def write_i8(self, v: int) -> None: + self._buf += _I8.pack(v) + + def write_u8(self, v: int) -> None: + self._buf += _U8.pack(v) + + def write_i16(self, v: int) -> None: + self._buf += _I16.pack(v) + + def write_u16(self, v: int) -> None: + self._buf += _U16.pack(v) + + def write_i32(self, v: int) -> None: + self._buf += _I32.pack(v) + + def write_u32(self, v: int) -> None: + self._buf += _U32.pack(v) + + def write_i64(self, v: int) -> None: + self._buf += _I64.pack(v) + + def write_u64(self, v: int) -> None: + self._buf += _U64.pack(v) + + def write_f32(self, v: float) -> None: + self._buf += _F32.pack(v) + + def write_f64(self, v: float) -> None: + self._buf += _F64.pack(v) + + def write_count(self, n: int) -> None: + self._buf += _U32.pack(n) + + def write_flag(self, present: bool) -> None: + self._buf.append(1 if present else 0) + + def write_string(self, v: str) -> None: + data = v.encode("utf-8") + self._buf += _U32.pack(len(data)) + self._buf += data + + def write_bytes(self, v: bytes) -> None: + self._buf += _U32.pack(len(v)) + self._buf += v + + def write_numbers(self, code: str, values: List[Any]) -> None: + """A list of one fixed-width numeric primitive (`code` is its struct + format character), packed in one call.""" + self._buf += _U32.pack(len(values)) + self._buf += struct.pack(f"<{len(values)}{code}", *values) + + def write_object(self, obj: Any, cls: "Type[_Object]") -> None: + """Reserve an object token: one strong reference to `obj` the reader + adopts, cloned in `finish()`. A closed wrapper is rejected now.""" + if not isinstance(obj, cls): + raise TypeError(f"expected {cls.__name__}, got {type(obj).__name__}") + obj._acquire() + obj._release() + self._objects.append((len(self._buf), obj)) + self._buf += bytes(8) + + +class _Reader: + """Decodes values from the value-buffer wire format, rejecting truncated + buffers, invalid flag bytes, invalid UTF-8, and trailing data.""" + + __slots__ = ("_data", "_pos") + + def __init__(self, data: bytes) -> None: + self._data = data + self._pos = 0 + + def _take(self, n: int, what: str) -> int: + pos = self._pos + if len(self._data) - pos < n: + raise _malformed(f"truncated {what}") + self._pos = pos + n + return pos + + def read_bool(self) -> bool: + b = self._data[self._take(1, "bool")] + if b > 1: + raise _malformed("invalid bool byte") + return b == 1 + + def read_i8(self) -> int: + return int(_I8.unpack_from(self._data, self._take(1, "i8"))[0]) + + def read_u8(self) -> int: + return self._data[self._take(1, "u8")] + + def read_i16(self) -> int: + return int(_I16.unpack_from(self._data, self._take(2, "i16"))[0]) + + def read_u16(self) -> int: + return int(_U16.unpack_from(self._data, self._take(2, "u16"))[0]) + + def read_i32(self) -> int: + return int(_I32.unpack_from(self._data, self._take(4, "i32"))[0]) + + def read_u32(self) -> int: + return int(_U32.unpack_from(self._data, self._take(4, "u32"))[0]) + + def read_i64(self) -> int: + return int(_I64.unpack_from(self._data, self._take(8, "i64"))[0]) + + def read_u64(self) -> int: + return int(_U64.unpack_from(self._data, self._take(8, "u64"))[0]) + + def read_f32(self) -> float: + return float(_F32.unpack_from(self._data, self._take(4, "f32"))[0]) + + def read_f64(self) -> float: + return float(_F64.unpack_from(self._data, self._take(8, "f64"))[0]) + + def read_count(self) -> int: + # An element count, not a byte length: elements can encode to zero + # bytes, so it is not bounded by the remaining data. + return int(_U32.unpack_from(self._data, self._take(4, "count"))[0]) + + def read_flag(self) -> bool: + b = self._data[self._take(1, "option flag")] + if b > 1: + raise _malformed("invalid option flag") + return b == 1 + + def read_bytes(self) -> bytes: + n = int(_U32.unpack_from(self._data, self._take(4, "length"))[0]) + pos = self._take(n, "bytes") + return self._data[pos:pos + n] + + def read_string(self) -> str: + try: + return self.read_bytes().decode("utf-8") + except UnicodeDecodeError: + raise _malformed("string is not valid UTF-8") from None + + def read_numbers(self, code: str, size: int) -> List[Any]: + """A list of one fixed-width numeric primitive, unpacked in one call.""" + n = self.read_count() + pos = self._take(n * size, "list") + return list(struct.unpack_from(f"<{n}{code}", self._data, pos)) + + def read_object(self) -> int: + """An object token: one strong reference the caller adopts.""" + ptr = int(_U64.unpack_from(self._data, self._take(8, "object token"))[0]) + if ptr == 0: + raise _malformed("null object token") + return ptr + + def expect_end(self) -> None: + if self._pos != len(self._data): + raise _malformed("trailing bytes") + + +def _decode(data: bytes, read: Callable[[_Reader], Any]) -> Any: + """Decode exactly one value from `data`.""" + r = _Reader(data) + value = read(r) + r.expect_end() + return value diff --git a/crates/weaveffi-gen-python/src/stubs.rs b/crates/weaveffi-gen/src/targets/python/stubs.rs similarity index 74% rename from crates/weaveffi-gen-python/src/stubs.rs rename to crates/weaveffi-gen/src/targets/python/stubs.rs index 8620085d..2776dccd 100644 --- a/crates/weaveffi-gen-python/src/stubs.rs +++ b/crates/weaveffi-gen/src/targets/python/stubs.rs @@ -1,40 +1,43 @@ //! `.pyi` type-stub rendering: a typed mirror of the public surface the -//! generated `weaveffi.py` module exposes. +//! generated implementation module exposes. -use weaveffi_core::model::{ - BindingModel, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, - StructBinding, +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::model::{ + BindingModel, CallbackInterfaceBinding, EnumBinding, ErrorBinding, FieldBinding, FnBinding, + InterfaceBinding, StructBinding, }; -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_field, py_member_name, py_name, py_type_hint, py_variant, py_wrapper_fn_name, +use crate::targets::python::docs::emit_doc; +use crate::targets::python::entities::py_code_class_name; +use crate::targets::python::types::{ + py_field, py_field_hint, py_member_name, py_name, py_type_hint, py_variant, py_wrapper_fn_name, }; -/// Render the full `weaveffi.pyi` stub for the model. +/// Render the full `.pyi` stub (`file_name`) for the model. pub(crate) fn render_pyi_module( model: &BindingModel, + root_error: &str, strip_module_prefix: bool, input_basename: &str, + file_name: &str, ) -> String { let mut out = render_prelude(CommentStyle::Hash, input_basename); out.push_str( - "from abc import ABC, abstractmethod\nfrom enum import IntEnum\n\ - from typing import Callable, Dict, Iterator, List, Optional, Type\n", + "import builtins\nfrom abc import ABC, abstractmethod\nfrom enum import IntEnum\n\ + from typing import Dict, Iterator, List, Optional, Type\n", ); - out.push_str("\nclass WeaveFFIError(Exception):\n"); + out.push_str(&format!("\nclass {root_error}(Exception):\n")); out.push_str(" GENERIC_ERROR_CODE: int\n"); out.push_str(" PANIC_ERROR_CODE: int\n"); out.push_str(" MARSHAL_ERROR_CODE: int\n"); out.push_str(" FOREIGN_ERROR_CODE: int\n"); + out.push_str(" CANCELLED_ERROR_CODE: int\n"); out.push_str(" code: int\n"); out.push_str(" message: str\n"); out.push_str(" def __init__(self, code: int, message: str) -> None: ...\n"); for m in &model.modules { if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { - render_pyi_error(&mut out, eb); + render_pyi_error(&mut out, eb, root_error); } for e in &m.enums { if e.is_rich() { @@ -57,18 +60,18 @@ pub(crate) fn render_pyi_module( } } out.push('\n'); - out.push_str(&render_trailer(CommentStyle::Hash, "weaveffi.pyi")); + out.push_str(&render_trailer(CommentStyle::Hash, file_name)); out } /// `.pyi` stub for one module's error domain: the domain base class (with /// its scoped per-code aliases) plus a per-code subclass carrying its stable /// `CODE` and any structured payload fields, mirroring -/// [`crate::entities::render_error`]. -fn render_pyi_error(out: &mut String, eb: &ErrorBinding) { +/// [`crate::targets::python::entities::render_error`]. +fn render_pyi_error(out: &mut String, eb: &ErrorBinding, root_error: &str) { let domain = &eb.type_name; out.push('\n'); - out.push_str(&format!("class {domain}(WeaveFFIError):\n")); + out.push_str(&format!("class {domain}({root_error}):\n")); for c in &eb.codes { let class = py_code_class_name(&c.name); out.push_str(&format!(" {class}: Type[\"{class}\"]\n")); @@ -80,20 +83,14 @@ fn render_pyi_error(out: &mut String, eb: &ErrorBinding) { emit_doc(out, &c.doc, ""); out.push_str(&format!("class {class}({domain}):\n")); out.push_str(" CODE: int\n"); - for f in &c.fields { - out.push_str(&format!( - " {}: {}\n", - py_field(&f.name), - py_type_hint(&f.ty) - )); - } + render_field_lines(out, &c.fields); out.push_str(" def __init__(self, message: str = ...) -> None: ...\n"); } } /// `.pyi` stub for one callback interface: the abstract base class with one /// abstract method per IDL method, mirroring -/// [`crate::calls::render_callback_interface`]. +/// [`crate::targets::python::calls::render_callback_interface`]. fn render_pyi_callback_interface(out: &mut String, cb: &CallbackInterfaceBinding) { out.push('\n'); emit_doc(out, &cb.doc, ""); @@ -123,7 +120,7 @@ fn render_pyi_callback_interface(out: &mut String, cb: &CallbackInterfaceBinding /// `.pyi` stub for one interface wrapper class: `__init__` for the canonical /// `new` constructor, a classmethod per remaining constructor, the /// reference-releasing `close()` and context-manager pair, then methods and -/// statics, mirroring [`crate::entities::render_interface`]. +/// statics, mirroring [`crate::targets::python::entities::render_interface`]. fn render_pyi_interface(out: &mut String, i: &InterfaceBinding) { out.push('\n'); emit_doc(out, &i.doc, ""); @@ -146,7 +143,7 @@ fn render_pyi_interface(out: &mut String, i: &InterfaceBinding) { } out.push_str(" def close(self) -> None: ...\n"); out.push_str(&format!(" def __enter__(self) -> \"{}\": ...\n", i.name)); - out.push_str(" def __exit__(self, *exc: object) -> bool: ...\n"); + out.push_str(" def __exit__(self, *exc: object) -> None: ...\n"); for c in i.constructors.iter().filter(|c| c.name != "new") { out.push_str(&format!( " @classmethod\n def {}({}) -> \"{}\": ...\n", @@ -192,14 +189,14 @@ 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", py_variant(&v.name))); + out.push_str(&format!(" {} = {}\n", py_variant(&v.name), v.value)); } } /// `.pyi` stub for a rich (algebraic) enum: the base class with its nested /// `Tag` `IntEnum`, scoped variant aliases, and `tag` reader, plus one /// dataclass-shaped variant subclass with its fields and constructor, -/// mirroring the rich-enum rendering in [`crate::entities`]. +/// mirroring the rich-enum rendering in [`crate::targets::python::entities`]. fn render_pyi_rich_enum(out: &mut String, e: &EnumBinding) { let name = &e.name; out.push('\n'); @@ -208,7 +205,7 @@ 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", py_variant(&v.name))); + out.push_str(&format!(" {} = {}\n", py_variant(&v.name), v.value)); } for v in &e.variants { out.push_str(&format!( @@ -225,48 +222,50 @@ fn render_pyi_rich_enum(out: &mut String, e: &EnumBinding) { emit_doc(out, &v.doc, ""); out.push_str(&format!("class {class}({name}):\n")); out.push_str(&format!(" TAG: \"{name}.Tag\"\n")); - for f in &v.fields { - out.push_str(&format!( - " {}: {}\n", - py_field(&f.name), - py_type_hint(&f.ty) - )); - } - let mut params = vec!["self".to_string()]; - params.extend( - v.fields + render_field_lines(out, &v.fields); + out.push_str(&dataclass_init(&v.fields)); + } +} + +/// The `__init__` a dataclass generates for `fields`. Its receiver is named +/// like the one `dataclasses` itself uses when a field is called `self`. +fn dataclass_init(fields: &[FieldBinding]) -> String { + let receiver = if fields.iter().any(|f| f.name == "self") { + "__dataclass_self__" + } else { + "self" + }; + let params: Vec = std::iter::once(receiver.to_string()) + .chain( + fields .iter() - .map(|f| format!("{}: {}", py_field(&f.name), py_type_hint(&f.ty))), - ); + .map(|f| format!("{}: {}", py_field(&f.name), py_field_hint(fields, &f.ty))), + ) + .collect(); + format!(" def __init__({}) -> None: ...\n", params.join(", ")) +} + +/// Emit one `name: hint` attribute line per field, with field docs. +fn render_field_lines(out: &mut String, fields: &[FieldBinding]) { + for f in fields { + emit_doc(out, &f.doc, " "); out.push_str(&format!( - " def __init__({}) -> None: ...\n", - params.join(", ") + " {}: {}\n", + py_field(&f.name), + py_field_hint(fields, &f.ty) )); } } /// `.pyi` stub for a record: a dataclass-shaped value class with typed field /// attributes and the generated constructor, mirroring -/// [`crate::entities::render_struct`]. +/// [`crate::targets::python::entities::render_struct`]. fn render_pyi_struct(out: &mut String, s: &StructBinding) { out.push('\n'); emit_doc(out, &s.doc, ""); out.push_str(&format!("class {}:\n", s.name)); - for field in &s.fields { - let py_ty = py_type_hint(&field.ty); - emit_doc(out, &field.doc, " "); - out.push_str(&format!(" {}: {}\n", py_field(&field.name), py_ty)); - } - let mut params = vec!["self".to_string()]; - params.extend( - s.fields - .iter() - .map(|f| format!("{}: {}", py_field(&f.name), py_type_hint(&f.ty))), - ); - out.push_str(&format!( - " def __init__({}) -> None: ...\n", - params.join(", ") - )); + render_field_lines(out, &s.fields); + out.push_str(&dataclass_init(&s.fields)); } /// `.pyi` stub for one module-level free function. diff --git a/crates/weaveffi-gen/src/targets/python/tests.rs b/crates/weaveffi-gen/src/targets/python/tests.rs new file mode 100644 index 00000000..098ec200 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/tests.rs @@ -0,0 +1,461 @@ +//! Unit tests: render a small API that exercises every ABI revision 3 shape +//! (prefixed runtime, pointer + length strings, reference-counted objects, +//! nullable objects, objects inside buffers, iterators, async and +//! cancellable functions, and callback interfaces) and assert the generated +//! Python carries the pieces each contract clause requires. + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::package::PackageContext; +use crate::platform::{BinarySet, Platform}; +use camino::Utf8Path; +use weaveffi_model::ir::{ + Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, + TypeRef, CURRENT_SCHEMA_VERSION, +}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; +use weaveffi_model::validate::validate_api; + +use crate::targets::python::{PythonConfig, PythonGenerator}; + +fn param(name: &str, ty: TypeRef) -> Param { + Param { + name: name.into(), + ty, + doc: None, + } +} + +fn func(name: &str, params: Vec, returns: Option) -> Function { + Function { + name: name.into(), + params, + returns, + doc: None, + throws: false, + r#async: false, + cancellable: false, + deprecated: None, + } +} + +fn field(name: &str, ty: TypeRef) -> StructField { + StructField { + name: name.into(), + ty, + doc: None, + } +} + +fn named(name: &str) -> TypeRef { + TypeRef::Named(name.into()) +} + +fn module(name: &str) -> Module { + Module { + name: name.into(), + doc: None, + functions: vec![], + interfaces: vec![], + callback_interfaces: vec![], + structs: vec![], + enums: vec![], + errors: None, + modules: vec![], + } +} + +fn resolve(modules: Vec) -> ResolvedApi { + let api = Api { + version: CURRENT_SCHEMA_VERSION.into(), + modules, + }; + validate_api(api, None) + .unwrap_or_else(|d| panic!("fixture must validate: {d:?}")) + .with_identity(Identity::named("kv-lib")) +} + +/// An interface with a constructor and methods; a function taking and +/// returning `Interface?`; a record with object fields; an iterator over +/// objects; a callback interface; an async function and a cancellable one. +fn kv_api() -> ResolvedApi { + let kv = Module { + structs: vec![StructDef { + name: "Entry".into(), + doc: None, + deprecated: None, + fields: vec![ + field("key", TypeRef::StringUtf8), + field("store", named("Store")), + field("mirrors", TypeRef::List(Box::new(named("Store")))), + field("weights", TypeRef::List(Box::new(TypeRef::I32))), + ], + }], + interfaces: vec![InterfaceDef { + name: "Store".into(), + doc: Some("A key-value store.".into()), + deprecated: None, + constructors: vec![func("new", vec![param("path", TypeRef::StringUtf8)], None)], + methods: vec![ + func( + "get", + vec![param("key", TypeRef::StringUtf8)], + Some(TypeRef::StringUtf8), + ), + func( + "has", + vec![param("key", TypeRef::StringUtf8)], + Some(TypeRef::Bool), + ), + ], + statics: vec![], + }], + callback_interfaces: vec![CallbackInterfaceDef { + name: "Watcher".into(), + doc: Some("Observes store changes.".into()), + deprecated: None, + methods: vec![ + func( + "on_change", + vec![ + param("text", TypeRef::StringUtf8), + param("entry", named("Entry")), + param("store", named("Store")), + ], + None, + ), + func( + "should_stop", + vec![param("count", TypeRef::I32)], + Some(TypeRef::Bool), + ), + ], + }], + functions: vec![ + func( + "pick", + vec![param("store", TypeRef::Optional(Box::new(named("Store"))))], + Some(TypeRef::Optional(Box::new(named("Store")))), + ), + func("watch", vec![param("watcher", named("Watcher"))], None), + func( + "all_stores", + vec![], + Some(TypeRef::Iterator(Box::new(named("Store")))), + ), + Function { + r#async: true, + ..func( + "fetch", + vec![param("id", TypeRef::I64)], + Some(named("Store")), + ) + }, + Function { + r#async: true, + cancellable: true, + ..func( + "sleep", + vec![param("ms", TypeRef::I64)], + Some(TypeRef::StringUtf8), + ) + }, + ], + ..module("kv") + }; + resolve(vec![kv]) +} + +fn render_files(api: &ResolvedApi, config: &PythonConfig) -> Vec { + let model = BindingModel::build(api); + PythonGenerator.files(api, &model, Utf8Path::new("out"), config) +} + +/// Render the fixture and return `(module source, stub)`. +fn render(api: &ResolvedApi) -> (String, String) { + let files = render_files(api, &PythonConfig::default()); + let find = |suffix: &str| { + files + .iter() + .find(|f| f.path.as_str().ends_with(suffix)) + .unwrap_or_else(|| panic!("missing {suffix}")) + .contents + .clone() + }; + (find("kv_lib.py"), find("kv_lib.pyi")) +} + +fn assert_has(hay: &str, needle: &str) { + assert!(hay.contains(needle), "missing `{needle}` in:\n{hay}"); +} + +#[test] +fn layout_follows_the_identity() { + let files = render_files(&kv_api(), &PythonConfig::default()); + let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect(); + assert_eq!( + paths, + [ + "out/python/kv_lib/__init__.py", + "out/python/kv_lib/kv_lib.py", + "out/python/kv_lib/kv_lib.pyi", + "out/python/kv_lib/py.typed", + "out/python/pyproject.toml", + "out/python/README.md", + ] + ); + assert_has(&files[0].contents, "from .kv_lib import *"); + let pyproject = &files[4].contents; + assert_has(pyproject, "name = \"kv-lib\""); + assert_has(pyproject, "requires-python = \">=3.9\""); + assert_has(pyproject, "packages = [\"kv_lib\"]"); + + let config = PythonConfig { + package_name: Some("kv-python".into()), + import_name: Some("kvpy".into()), + ..PythonConfig::default() + }; + let files = render_files(&kv_api(), &config); + assert!(files.iter().any(|f| f.path == "out/python/kvpy/kvpy.py")); + let pyproject = files.iter().find(|f| f.path.ends_with("pyproject.toml")); + assert_has(&pyproject.unwrap().contents, "name = \"kv-python\""); +} + +#[test] +fn runtime_is_prefixed_and_checks_the_contract() { + let (py, pyi) = render(&kv_api()); + assert_has(&py, "_ABI_VERSION = 3\n"); + assert_has(&py, " (\"kv\", \"kv_lib_kv_checksum\", 0x"); + assert_has(&py, "_bind(\"kv_lib_abi_version\", ctypes.c_uint32)"); + assert_has(&py, "os.environ.get(\"KV_LIB_LIBRARY\")"); + assert_has(&py, "name = \"libkv_lib.dylib\""); + assert_has( + &py, + "_bind(\"kv_lib_free_bytes\", None, ctypes.c_void_p, ctypes.c_size_t)", + ); + assert_has(&py, "CANCELLED_ERROR_CODE = -5"); + assert_has(&py, "class Error(Exception):"); + assert_has(&pyi, "class Error(Exception):"); + assert!(!py.contains("free_string")); + // Nothing but the generated-file header names WeaveFFI. + for text in [&py, &pyi] { + for line in text.lines().filter(|l| !l.starts_with('#')) { + assert!( + !line.to_ascii_lowercase().contains("weaveffi"), + "branded line: {line}" + ); + } + } +} + +#[test] +fn prototypes_are_bound_once_with_c_bool_and_ptr_len_strings() { + let (py, _) = render(&kv_api()); + assert_has( + &py, + "_c_kv_Store_has = _bind(\"kv_lib_kv_Store_has\", ctypes.c_bool, ctypes.c_void_p, \ + ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_ErrorStruct))", + ); + assert_eq!(py.matches("_c_kv_Store_get = _bind(").count(), 1); + assert!(!py.contains(".argtypes = ["), "no per-call binding"); + // A string crosses as UTF-8 bytes plus a length and comes back owned. + assert_has(&py, "_key_b = key.encode(\"utf-8\")"); + assert_has(&py, "_key_b, len(_key_b)"); + assert_has(&py, "return _take_str(_ret, _out_len.value)"); +} + +#[test] +fn objects_are_lent_for_calls_and_adopted_from_returns() { + let (py, pyi) = render(&kv_api()); + assert_has(&py, "class Store(_Object):"); + assert_has(&py, " _destroy = staticmethod(_c_kv_Store_destroy)\n"); + assert_has(&py, " _clone = staticmethod(_c_kv_Store_clone)\n"); + assert_has(&py, "def __init__(self, path: str) -> None:"); + assert_has(&py, "self._init_handle(_required(_ret))"); + // `self` is lent for the call and released after it, even on error. + assert_has( + &py, + "_self_p = self._acquire()\n try:\n _ret = _c_kv_Store_get(_self_p,", + ); + assert_has(&py, " finally:\n self._release()\n"); + // Nullable objects map None to NULL both ways. + assert_has(&py, "_store_p = _lend_opt(store, Store)"); + assert_has(&py, "_release_opt(store)"); + assert_has(&py, "return (Store._adopt(_ret) if _ret else None)"); + // Objects inside buffers are cloned when the buffer is finished and + // adopted on read; numeric lists take the packed fast path. + assert_has(&py, "_w.write_object(value.store, Store)"); + assert_has(&py, "store=Store._adopt(_r.read_object()),"); + assert_has(&py, "_w.write_numbers(\"i\", value.weights)"); + assert_has(&py, "weights=_r.read_numbers(\"i\", 4),"); + assert_has(&pyi, " def close(self) -> None: ...\n"); + assert_has(&pyi, " def __enter__(self) -> \"Store\": ...\n"); +} + +#[test] +fn iterators_pull_lazily_through_prebound_functions() { + let (py, pyi) = render(&kv_api()); + assert_has(&py, "class _kv_AllStoresIterator(_Iterator):"); + assert_has( + &py, + " _destroy = staticmethod(_c_kv_AllStoresIterator_destroy)", + ); + assert_has( + &py, + "_more = _c_kv_AllStoresIterator_next(_p, ctypes.byref(_item), ctypes.byref(_err))", + ); + assert_has(&py, "return Store._adopt(_required(_item.value))"); + assert_has(&py, "return _kv_AllStoresIterator._adopt(_ret)"); + assert_has(&pyi, "def all_stores() -> Iterator[\"Store\"]: ..."); +} + +#[test] +fn async_calls_complete_through_static_trampolines() { + let (py, pyi) = render(&kv_api()); + assert_has(&py, "import asyncio\n"); + assert_has( + &py, + "_c_kv_fetch_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, \ + ctypes.POINTER(_ErrorStruct), ctypes.c_void_p)", + ); + assert_has(&py, "def _kv_fetch_complete(context, err, result) -> None:"); + assert_has( + &py, + "_async_settle(context, None, Store._adopt(_required(result)))", + ); + assert_has( + &py, + "_kv_fetch_completion = _c_kv_fetch_callback(_kv_fetch_complete)", + ); + assert_has(&py, "_c_kv_fetch(id, _kv_fetch_completion, _call)"); + assert_has(&py, "return await _future"); + // A cancellable call owns a token for its duration. + assert_has(&py, "_token = _cancel_token_create()"); + assert_has(&py, "_c_kv_sleep(ms, _token, _kv_sleep_completion, _call)"); + assert_has(&py, "return await _async_wait_cancellable(_future, _token)"); + assert_has( + &py, + "_async_settle(context, None, _take_str(result_ptr, result_len))", + ); + assert_has(&pyi, "async def sleep(ms: int) -> str: ..."); +} + +#[test] +fn callback_interfaces_render_abc_vtable_and_trampolines() { + let (py, pyi) = render(&kv_api()); + assert_has(&py, "import abc\n"); + assert_has(&py, "class Watcher(abc.ABC):"); + assert_has( + &py, + "_Watcher_on_change_t = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, \ + ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, \ + ctypes.POINTER(_ErrorStruct))", + ); + assert_has( + &py, + "_Watcher_should_stop_t = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, \ + ctypes.c_int32, ctypes.POINTER(_ErrorStruct))", + ); + assert_has(&py, "class _WatcherVtable(ctypes.Structure):"); + assert_has(&py, "The C vtable `kv_lib_kv_Watcher_vtable`."); + assert_has( + &py, + "_callback_get(ctx).on_change(_peek_bytes(text_ptr, text_len).decode(\"utf-8\"), \ + _decode(_peek_bytes(entry_ptr, entry_len), _read_Entry), \ + Store._adopt(_required(store)))", + ); + assert_has(&py, "return bool(_callback_get(ctx).should_stop(count))"); + assert_has(&py, "_callback_fail(out_err, exc)\n return False"); + assert_has(&py, "_Watcher_free_t(_callback_free),"); + assert_eq!(py.matches("_Watcher_vtable = _WatcherVtable(").count(), 1); + assert_has(&py, "_watcher_ctx = _callback_register(watcher, Watcher)"); + assert_has( + &py, + "_c_kv_watch(_watcher_ctx, ctypes.addressof(_Watcher_vtable), ctypes.byref(_err))", + ); + assert_has(&pyi, "class Watcher(ABC):"); +} + +#[test] +fn feature_runtime_is_emitted_only_when_used() { + let plain = Module { + functions: vec![func( + "add", + vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], + Some(TypeRef::I32), + )], + ..module("math") + }; + let (py, _) = render(&resolve(vec![plain])); + assert!(!py.contains("import abc")); + assert!(!py.contains("import asyncio")); + assert!(!py.contains("_callbacks")); + assert!(!py.contains("_cancel_token")); + assert_has(&py, "def add(a: int, b: int) -> int:"); +} + +#[test] +fn root_error_avoids_user_type_names() { + let m = Module { + structs: vec![StructDef { + name: "Error".into(), + doc: None, + deprecated: None, + fields: vec![field("code", TypeRef::I32)], + }], + ..module("kv") + }; + let (py, pyi) = render(&resolve(vec![m])); + assert_has(&py, "class KvLibError(Exception):"); + assert_has(&pyi, "class KvLibError(Exception):"); +} + +#[test] +fn generated_source_is_deterministic() { + let api = kv_api(); + assert_eq!(render(&api), render(&api)); +} + +#[test] +fn package_bundles_the_identity_library_per_wheel_platform() { + let api = kv_api(); + let model = BindingModel::build(&api); + let mut binaries = BinarySet::new("kv_lib"); + for p in Platform::ALL { + binaries.insert(p, format!("/tmp/{}/lib", p.id())); + } + let ctx = PackageContext { + binaries: &binaries, + input_basename: Some("kv.yml"), + }; + let files = PythonGenerator + .package( + &api, + &model, + &ctx, + Utf8Path::new("out"), + &PythonConfig::default(), + ) + .expect("python packages"); + let trees: std::collections::BTreeSet<&str> = files + .iter() + .filter_map(|f| f.path.as_str().strip_prefix("out/python/")) + .filter_map(|rest| rest.split('/').next()) + .collect(); + let expected: std::collections::BTreeSet<&str> = + Platform::DESKTOP.iter().map(|p| p.id()).collect(); + assert_eq!(trees, expected); + assert!(files.iter().any(|f| f + .path + .as_str() + .ends_with("macos-arm64/kv_lib/libkv_lib.dylib") + || f.path.as_str().ends_with("/kv_lib/libkv_lib.dylib"))); + for p in [ + Platform::AndroidArm64, + Platform::AndroidX64, + Platform::Wasm32, + ] { + assert!(!files.iter().any(|f| f.path.as_str().contains(p.id()))); + } +} diff --git a/crates/weaveffi-gen/src/targets/python/types.rs b/crates/weaveffi-gen/src/targets/python/types.rs new file mode 100644 index 00000000..960f108f --- /dev/null +++ b/crates/weaveffi-gen/src/targets/python/types.rs @@ -0,0 +1,213 @@ +//! Python type mapping and naming: the `ctypes` vocabulary for ABI slots, +//! typing hints for signatures, and the identifier policy applied to +//! user-chosen IDL names before they land in generated Python. + +use crate::lang; +use crate::utils::{local_type_name, wrapper_name}; +use heck::ToSnakeCase; +use weaveffi_model::abi::CType; +use weaveffi_model::model::{FieldBinding, Ty}; + +/// The Python typing hint for `ty` as it appears in signatures and stubs. +/// User types are quoted forward references to their bare local class name: +/// every module's declarations share one Python namespace, so a +/// cross-module reference (`kv.Store`) is just `"Store"`. +pub(crate) fn py_type_hint(ty: &Ty) -> String { + match ty { + Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::I64 | Ty::U64 => "int".into(), + Ty::F32 | Ty::F64 => "float".into(), + Ty::Bool => "bool".into(), + Ty::StringUtf8 => "str".into(), + Ty::Bytes => "bytes".into(), + Ty::Enum(name) + | Ty::Record(name) + | Ty::RichEnum(name) + | Ty::Interface(name) + | Ty::CallbackInterface(name) => format!("\"{}\"", local_type_name(name)), + 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)), + } +} + +/// The builtin types an annotation names. +const BUILTIN_HINTS: &[&str] = &["bool", "bytes", "float", "int", "str"]; + +/// The annotation for `ty` inside a class declaring `fields`. A field named +/// like a builtin type (`int: int`) shadows it for every later annotation in +/// the class body, so such classes spell the builtins `builtins.int`. +pub(crate) fn py_field_hint(fields: &[FieldBinding], ty: &Ty) -> String { + let hint = py_type_hint(ty); + if !fields + .iter() + .any(|f| BUILTIN_HINTS.contains(&f.name.as_str())) + { + return hint; + } + // Qualify bare builtin identifiers outside quoted forward references. + let mut out = String::with_capacity(hint.len()); + let mut word = String::new(); + let mut quoted = false; + let flush = |word: &mut String, out: &mut String| { + if BUILTIN_HINTS.contains(&word.as_str()) { + out.push_str("builtins."); + } + out.push_str(word); + word.clear(); + }; + for c in hint.chars() { + if !quoted && (c.is_ascii_alphanumeric() || c == '_') { + word.push(c); + continue; + } + flush(&mut word, &mut out); + if c == '"' { + quoted = !quoted; + } + out.push(c); + } + flush(&mut word, &mut out); + out +} + +/// Which side of the boundary a `ctypes` slot type describes. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Slot { + /// An argument the wrapper passes in (`argtypes`). A `const uint8_t*` + /// is `c_char_p`, which accepts a Python `bytes` object directly + /// (interior NUL bytes included, since the length travels separately). + Arg, + /// A value the wrapper receives (`restype`, or a parameter of a + /// `CFUNCTYPE` the producer calls). A `uint8_t*` stays a raw `c_void_p` + /// address so the wrapper can copy exactly `len` bytes and free them. + Recv, +} + +/// Maps a shared ABI [`CType`] onto its `ctypes` spelling. The structural +/// lowering (which slots exist, in what order) comes from the model; this is +/// only the Python vocabulary applied to each slot. Opaque object, iterator, +/// vtable, and cancel-token pointers are `c_void_p`; `bool` is `c_bool`; +/// enums are `c_int32`; the error struct is the runtime's `_ErrorStruct`. +pub(crate) fn py_ctype(ty: &CType, slot: Slot) -> String { + match ty { + CType::Int8 => "ctypes.c_int8".into(), + CType::Int16 => "ctypes.c_int16".into(), + CType::Int32 | CType::Enum { .. } => "ctypes.c_int32".into(), + CType::Int64 => "ctypes.c_int64".into(), + CType::Uint8 => "ctypes.c_uint8".into(), + CType::Uint16 => "ctypes.c_uint16".into(), + CType::Uint32 => "ctypes.c_uint32".into(), + CType::Uint64 => "ctypes.c_uint64".into(), + CType::Float => "ctypes.c_float".into(), + CType::Double => "ctypes.c_double".into(), + CType::Bool => "ctypes.c_bool".into(), + CType::Size => "ctypes.c_size_t".into(), + CType::Char => "ctypes.c_char".into(), + CType::Void => "None".into(), + CType::Error => "_ErrorStruct".into(), + CType::CancelToken + | CType::StructTag { .. } + | CType::VtableTag { .. } + | CType::Named(_) => "ctypes.c_void_p".into(), + CType::Ptr { pointee, .. } => match pointee.as_ref() { + CType::Uint8 if slot == Slot::Arg => "ctypes.c_char_p".into(), + CType::Char => "ctypes.c_char_p".into(), + CType::Ptr { .. } => "ctypes.POINTER(ctypes.c_void_p)".into(), + CType::Uint8 + | CType::StructTag { .. } + | CType::VtableTag { .. } + | CType::CancelToken + | CType::Void + | CType::Named(_) => "ctypes.c_void_p".into(), + other => format!("ctypes.POINTER({})", py_ctype(other, slot)), + }, + } +} + +/// Names a parameter must not take because wrapper and trampoline bodies +/// refer to them: the receivers, the modules, and the builtins the +/// generated code calls. Sorted for binary search. +const PY_BODY_NAMES: &[&str] = &[ + "bool", + "bytes", + "cls", + "ctypes", + "dict", + "float", + "int", + "isinstance", + "len", + "range", + "self", + "warnings", +]; + +/// The Python spelling of an IDL value identifier (parameter name): +/// snake_case via heck, then escaped with a trailing `_` when it is a +/// keyword (`class`) or a name the generated body relies on (`self`, +/// `len`). IDL names are usually already snake, so the case conversion is a +/// safety net for camelCase inputs. +pub(crate) fn py_name(name: &str) -> String { + py_local(&name.to_snake_case()) +} + +/// Escape `name` for use as a local (a parameter or a trampoline slot) +/// without changing its case. +pub(crate) fn py_local(name: &str) -> String { + let escaped = lang::escape_ident(name, lang::PYTHON_KEYWORDS); + if PY_BODY_NAMES.binary_search(&escaped.as_str()).is_ok() { + format!("{escaped}_") + } else { + escaped + } +} + +/// The Python spelling of an IDL field name, emitted verbatim except for +/// keyword escaping (a field named `class` becomes `class_`). +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 { + lang::escape_ident(&name.to_snake_case(), lang::PYTHON_KEYWORDS) +} + +/// The Python spelling of a module-level wrapper (free function): the +/// module-path prefix applied per config, then snake_case, then keyword +/// escaping. +pub(crate) fn py_wrapper_fn_name( + module_path: &str, + name: &str, + strip_module_prefix: bool, +) -> String { + lang::escape_ident( + &wrapper_name(module_path, name, strip_module_prefix).to_snake_case(), + lang::PYTHON_KEYWORDS, + ) +} + +/// The module-level name a C symbol's bound `ctypes` function is stored +/// under: `_c_` plus the symbol without its `{prefix}_`. +pub(crate) fn py_binding_name(symbol: &str, prefix: &str) -> String { + let core = symbol + .strip_prefix(prefix) + .and_then(|s| s.strip_prefix('_')) + .unwrap_or(symbol); + format!("_c_{core}") +} + +/// Escape a string for embedding in a double-quoted Python literal. +pub(crate) fn py_str_literal(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") +} diff --git a/crates/weaveffi-gen-ruby/src/callbacks.rs b/crates/weaveffi-gen/src/targets/ruby/callbacks.rs similarity index 76% rename from crates/weaveffi-gen-ruby/src/callbacks.rs rename to crates/weaveffi-gen/src/targets/ruby/callbacks.rs index bb5417a0..3ffd8d02 100644 --- a/crates/weaveffi-gen-ruby/src/callbacks.rs +++ b/crates/weaveffi-gen/src/targets/ruby/callbacks.rs @@ -4,21 +4,21 @@ //! `free`, and the single process-wide vtable instance every registration //! passes to the producer. //! -//! The trampolines follow [`CallbackProtocol`](weaveffi_core::plan::CallbackProtocol): +//! The trampolines follow [`CallbackProtocol`](weaveffi_model::plan::CallbackProtocol): //! `ctx` is resolved through the module's implementation registry, borrowed //! string, bytes, and buffer arguments are copied or decoded before the //! implementation runs, object arguments are adopted into wrappers, and any //! exception is reported through `{prefix}_error_set(out_err, -4, message)` //! instead of unwinding through the C frame. +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; use heck::ToShoutySnakeCase; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::{CallbackInterfaceBinding, CallbackMethodBinding, ModuleBinding, Ty}; -use weaveffi_core::plan::{ArgPass, RetPass}; -use weaveffi_core::utils::local_type_name; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, Ty}; +use weaveffi_model::plan::{ArgPass, RetPass}; -use crate::docs::{emit_doc, emit_param_docs}; -use crate::types::{rb_abi_types, rb_direct_default, rb_direct_from_c, rb_ffi_type, rb_param_name}; +use crate::targets::ruby::docs::{emit_doc, emit_param_docs}; +use crate::targets::ruby::types::{rb_abi_types, rb_direct_default, rb_ffi_type, rb_param_name}; /// The `FFI::Struct` subclass laying out one callback interface's vtable: /// `Listener` (or `events.Listener`) becomes `WvListenerVtable`. @@ -48,16 +48,16 @@ fn rb_free_const(cb: &str) -> String { } /// The trampoline's Ruby formal parameter names for one method: `ctx`, one -/// formal per parameter slot (a `_ptr`/`_len` pair for bytes and buffered -/// values), then `out_err`. The Ruby spellings mirror the C slot names so the -/// list lines up with [`CallbackMethodBinding::abi_params`] position for -/// position. +/// formal per parameter slot (a `_ptr`/`_len` pair for strings, bytes, and +/// buffered values), then `out_err`. The Ruby spellings mirror the C slot +/// names so the list lines up with [`CallbackMethodBinding::abi_params`] +/// position for position. fn rb_tramp_formals(m: &CallbackMethodBinding) -> Vec { let mut formals = vec!["ctx".to_string()]; for p in &m.params { let n = rb_param_name(&p.name); match p.arg_pass() { - ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { formals.push(format!("{n}_ptr")); formals.push(format!("{n}_len")); } @@ -74,11 +74,10 @@ fn rb_tramp_formals(m: &CallbackMethodBinding) -> Vec { /// parameter's receiving plan). pub(crate) fn render_callback_interface( out: &mut String, - module: &ModuleBinding, cb: &CallbackInterfaceBinding, prefix: &str, ) { - let protocol = cb.protocol(&module.path, prefix); + let protocol = cb.protocol(prefix); let local = local_type_name(&cb.name); let vtable_class = rb_vtable_class(&cb.name); let vtable_const = rb_vtable_const(&cb.name); @@ -91,16 +90,19 @@ pub(crate) fn render_callback_interface( let mut d = String::new(); emit_doc(&mut d, &cb.doc, " "); w.raw(d); - if let Some(msg) = &cb.deprecated { - w.line(format!("# @deprecated {msg}")); + if cb.doc.is_some() { + w.line("#"); } w.line("# Consumer-implemented callback interface. Any object responding to the"); w.line(format!( - "# methods below is accepted wherever a {local} parameter is expected;" + "# methods below is accepted wherever a {local} is expected; include" )); - w.line("# include this module to inherit NotImplementedError defaults. The"); - w.line("# producer may call the methods from any thread until it releases the"); + w.line("# this module to inherit NotImplementedError defaults. The producer"); + w.line("# may call the methods from any thread until it releases the"); w.line("# implementation."); + if let Some(msg) = &cb.deprecated { + w.line(format!("# @deprecated {msg}")); + } w.block(format!("module {local}"), "end", |w| { for (idx, m) in cb.methods.iter().enumerate() { if idx > 0 { @@ -195,11 +197,12 @@ pub(crate) fn render_callback_interface( } /// Render one method's trampoline: an `FFI::Function` matching the vtable -/// entry's C signature that resolves `ctx`, receives each argument per its -/// [`RetPass`] plan, invokes the implementation, and converts the direct -/// return. Any exception is reported through `_wv_cb_fail` (which calls -/// `{prefix}_error_set` with `-4`) and the method's default value is -/// returned instead. +/// entry's C signature that receives each argument per its [`RetPass`] plan, +/// resolves `ctx`, invokes the implementation, and converts the direct +/// return. Object arguments are adopted first, so their references are owned +/// by a wrapper even when a later step raises. Any exception is reported +/// through `_wv_cb_fail` (which calls `{prefix}_error_set` with `-4`) and the +/// method's default value is returned instead. fn render_trampoline( w: &mut CodeWriter, cb: &CallbackInterfaceBinding, @@ -207,7 +210,7 @@ fn render_trampoline( args: &[RetPass], ) { let local = local_type_name(&cb.name); - let types = rb_abi_types(&m.abi_params, false); + let types = rb_abi_types(&m.abi_params); let formals = rb_tramp_formals(m); let default = rb_direct_default(m.ret.as_ref()); @@ -218,7 +221,7 @@ fn render_trampoline( format!( "{} = FFI::Function.new({}, [{}]) do |{}|", rb_trampoline_const(&cb.name, &m.name), - rb_ffi_type(&m.abi_ret, true), + rb_ffi_type(&m.abi_ret), types.join(", "), formals.join(", ") ), @@ -226,10 +229,15 @@ fn render_trampoline( |w| { w.line("begin"); w.scope(|w| { - w.line("impl = _wv_cb_lookup(ctx)"); - for (p, pass) in m.params.iter().zip(args) { + let params: Vec<_> = m.params.iter().zip(args).collect(); + let is_object = |pass: &RetPass| matches!(pass, RetPass::Object { .. }); + for (p, pass) in params.iter().filter(|(_, pass)| is_object(pass)) { render_tramp_arg(w, &p.name, &p.ty, pass); } + for (p, pass) in params.iter().filter(|(_, pass)| !is_object(pass)) { + render_tramp_arg(w, &p.name, &p.ty, pass); + } + w.line("impl = _wv_cb_lookup(ctx)"); let call_args: Vec = m .params .iter() @@ -249,7 +257,7 @@ fn render_trampoline( // surfaces as a foreign error rather than escaping // through ffi's own conversion. Some(Ty::Bool) => { - w.line(format!("{call} ? 1 : 0")); + w.line(format!("{call} ? true : false")); } Some(Ty::F32 | Ty::F64) => { w.line(format!("Float({call})")); @@ -261,7 +269,7 @@ fn render_trampoline( }); // Rescue Exception, not StandardError: NotImplementedError is a // ScriptError, and nothing may unwind through the C frame. - w.line("rescue Exception => e"); + w.line("rescue Exception => e # rubocop:disable Lint/RescueException"); w.scope(|w| { w.line("_wv_cb_fail(out_err, e)"); w.line(default); @@ -272,35 +280,37 @@ fn render_trampoline( } /// Emit the statements receiving one trampoline argument into the local -/// `{name}_v`, per its [`RetPass`] plan. Strings arrive as Ruby Strings -/// (ffi copies `:string` slots); bytes and buffers are borrowed `(ptr, len)` -/// pairs copied or decoded before the implementation runs; objects transfer -/// one strong reference that is adopted into a wrapper (`nil` for a null -/// nullable slot); direct values convert per [`rb_direct_from_c`]. +/// `{name}_v`, per its [`RetPass`] plan. Strings, bytes, and buffers are +/// borrowed `(ptr, len)` pairs copied or decoded before the implementation +/// runs; objects transfer one strong reference that is adopted into a +/// wrapper (`nil` for a null nullable slot); direct values pass through. fn render_tramp_arg(w: &mut CodeWriter, name: &str, ty: &Ty, pass: &RetPass) { let n = rb_param_name(name); match pass { RetPass::Void => unreachable!("callback parameters always have a type"), RetPass::Direct => { - w.line(format!("{n}_v = {}", rb_direct_from_c(ty, &n))); + w.line(format!("{n}_v = {n}")); } RetPass::String => { - // ffi copies a `:string` slot into a fresh BINARY String; the ABI - // guarantees UTF-8, so retag it. w.line(format!( - "{n}_v = {n}.nil? ? '' : {n}.force_encoding(Encoding::UTF_8)" + "{n}_v = _wv_borrow_bytes({n}_ptr, {n}_len).force_encoding(Encoding::UTF_8)" )); } RetPass::Bytes => { - w.line(format!( - "{n}_v = {n}_ptr.null? ? ''.b : {n}_ptr.read_string({n}_len)" - )); + w.line(format!("{n}_v = _wv_borrow_bytes({n}_ptr, {n}_len)")); } RetPass::Buffer => { w.line(format!( - "{n}_r = WvBufferReader.new({n}_ptr.null? ? ''.b : {n}_ptr.read_string({n}_len))" + "{n}_r = WvBufferReader.new(_wv_borrow_bytes({n}_ptr, {n}_len))" )); - crate::codec::render_wv_read(w, &format!("{n}_r"), &format!("{n}_v"), ty, 0, ""); + crate::targets::ruby::codec::render_wv_read( + w, + &format!("{n}_r"), + &format!("{n}_v"), + ty, + 0, + "", + ); w.line(format!("{n}_r.expect_end!")); } RetPass::Object { nullable, .. } => { diff --git a/crates/weaveffi-gen/src/targets/ruby/calls.rs b/crates/weaveffi-gen/src/targets/ruby/calls.rs new file mode 100644 index 00000000..553d31a9 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/ruby/calls.rs @@ -0,0 +1,698 @@ +//! Callable rendering: FFI attachments, async completion trampolines, the +//! sync, async, and iterator wrapper bodies, and the parameter and return +//! marshalling they share. +//! +//! Marshalling dispatch goes through the shared plan layer ([`ArgPass`], +//! [`RetPass`]), so this backend can't drift from the others on +//! call-boundary semantics: strings, bytes, and value buffers cross as +//! borrowed `(ptr, len)` pairs, objects are borrowed as parameters (pinned +//! for the call through `_wv_pin`) and adopted as returns, and a callback +//! interface crosses as a handle-table key plus the interface's static +//! vtable. + +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use crate::utils::{local_type_name, wrapper_name}; +use heck::{ToShoutySnakeCase, ToSnakeCase}; +use weaveffi_model::model::{AsyncBinding, CallShape, FnBinding, IteratorBinding, ModuleBinding}; +use weaveffi_model::model::{ParamBinding, Ty}; +use weaveffi_model::plan::{self, ArgPass, ErrorStrategy, RetPass}; + +use crate::targets::ruby::callbacks::rb_vtable_const; +use crate::targets::ruby::codec::render_wv_read; +use crate::targets::ruby::codec::render_wv_write; +use crate::targets::ruby::docs::emit_param_docs; +use crate::targets::ruby::entities::{rb_checker_name, rb_error_factory_name}; +use crate::targets::ruby::types::{ + rb_abi_types, rb_direct_type, rb_ffi_type, rb_param_name, rb_str_literal, +}; + +/// How a rendered Ruby callable is scoped and spelled in the generated +/// module: at module scope as a singleton method, or inside an interface +/// class as a constructor, instance method, or class method. +pub(crate) enum RbScope<'a> { + /// A module-level free function (`def self.name` on the top-level module). + Free { + /// The owning module's underscore-joined path. + module_path: &'a str, + /// Whether the emitted name drops the module-path prefix. + strip_module_prefix: bool, + }, + /// An instance method on an interface class: `def name`, borrowing the + /// wrapper's own pointer as the leading C argument. + Method { + /// The top-level Ruby module name qualifying module singleton calls. + module_name: &'a str, + }, + /// A static member of an interface class (`def self.name`). + Static { + /// The top-level Ruby module name qualifying module singleton calls. + module_name: &'a str, + }, + /// A non-`new` constructor: a class method adopting the returned + /// reference through `_from_ptr` (never re-running `initialize`). + Factory { + /// The top-level Ruby module name qualifying module singleton calls. + module_name: &'a str, + }, + /// The canonical `new` constructor, emitted as `initialize`. + Init { + /// The top-level Ruby module name qualifying module singleton calls. + module_name: &'a str, + }, +} + +impl RbScope<'_> { + /// The receiver prefix for module singleton calls (attached C symbols, + /// error checkers, runtime helpers): `"{ModuleName}."` inside a class + /// body, empty at module scope, where `self` already is the module. + fn qualifier(&self) -> String { + match self { + RbScope::Free { .. } => String::new(), + RbScope::Method { module_name } + | RbScope::Static { module_name } + | RbScope::Factory { module_name } + | RbScope::Init { module_name } => format!("{module_name}."), + } + } + + /// Two-space indent depth of the `def` line (1 at module scope, 2 inside + /// an interface class). + fn depth(&self) -> usize { + if matches!(self, RbScope::Free { .. }) { + 1 + } else { + 2 + } + } + + /// The `def` opener for `f` with the given formal parameters. + fn def_open(&self, f: &FnBinding, params: &[String]) -> String { + let args = if params.is_empty() { + String::new() + } else { + format!("({})", params.join(", ")) + }; + match self { + RbScope::Free { + module_path, + strip_module_prefix, + } => format!( + "def self.{}{args}", + wrapper_name(module_path, &f.name, *strip_module_prefix).to_snake_case() + ), + RbScope::Method { .. } => format!("def {}{args}", f.name.to_snake_case()), + RbScope::Static { .. } | RbScope::Factory { .. } => { + format!("def self.{}{args}", f.name.to_snake_case()) + } + RbScope::Init { .. } => format!("def initialize{args}"), + } + } +} + +/// Render one callable: a free function or an interface member. `module` +/// supplies the error domain for throwing callables; `scope` picks the def +/// spelling, receiver, indent, and result handling. +pub(crate) fn render_callable( + out: &mut String, + module: &ModuleBinding, + f: &FnBinding, + scope: &RbScope, +) { + let mut w = CodeWriter::two_space().with_depth(scope.depth()); + w.blank(); + w.doc(&f.doc, DocCommentStyle::Hash); + emit_param_docs(&mut w, &f.params); + if let Some(msg) = &f.deprecated { + w.line(format!("# @deprecated {msg}")); + } + match &f.shape { + CallShape::Sync(abi) => render_sync(&mut w, module, f, &abi.symbol, scope), + CallShape::Async(a) => render_async(&mut w, f, a, scope), + CallShape::Iterator(it) => render_iterator(&mut w, module, f, it, scope), + } + out.push_str(&w.finish()); +} + +/// The trailing `, blocking: true` of an attachment that releases the GVL. +fn blocking_opt(blocking: bool) -> &'static str { + if blocking { + ", blocking: true" + } else { + "" + } +} + +/// Attach the C symbols for one callable: the plain symbol for a sync shape, +/// the launcher plus its completion trampoline for an async shape, and the +/// launch/next/destroy triple for an iterator. +/// +/// `blocking` marks calls that may call back into Ruby (every call in an API +/// with callback interfaces); they release the GVL so a producer thread can +/// run a callback while the call is in flight. Async launches and iterator +/// steps always release it. +pub(crate) fn render_attach_function( + out: &mut String, + module: &ModuleBinding, + f: &FnBinding, + blocking: bool, +) { + let mut w = CodeWriter::two_space().with_depth(1); + match &f.shape { + CallShape::Sync(abi) => { + w.line(format!( + "attach_function :{}, [{}], {}{}", + abi.symbol, + rb_abi_types(&abi.params).join(", "), + rb_ffi_type(&abi.ret), + blocking_opt(blocking) + )); + } + CallShape::Async(a) => { + w.line(format!( + "attach_function :{}, [{}], :void, blocking: true", + a.launch.symbol, + rb_abi_types(&a.launch.params).join(", ") + )); + render_async_trampoline(&mut w, module, f, a); + } + CallShape::Iterator(it) => { + w.line(format!( + "attach_function :{}, [{}], :pointer, blocking: true", + it.launch.symbol, + rb_abi_types(&it.launch.params).join(", ") + )); + w.line(format!( + "attach_function :{}, [{}], :int32, blocking: true", + it.next.symbol, + rb_abi_types(&it.next.params).join(", ") + )); + w.line(format!( + "attach_function :{}, [:pointer], :void{}", + it.destroy_symbol, + blocking_opt(blocking) + )); + } + } + out.push_str(&w.finish()); +} + +/// The constant pinning an async function's completion trampoline, named +/// after its C callback typedef: `kvstore_kv_Store_compact_callback` becomes +/// `KVSTORE_KV_STORE_COMPACT_CALLBACK`. +fn rb_async_const(a: &AsyncBinding) -> String { + a.callback_type.to_shouty_snake_case() +} + +/// The error a completion's `taken` triple becomes: the domain error for a +/// throwing function, the generic error otherwise. +fn rb_error_from_taken(module: &ModuleBinding, f: &FnBinding, q: &str) -> String { + match (f.error_strategy(), module.error.as_ref()) { + (ErrorStrategy::Throws, Some(eb)) => { + format!("{q}{}(*taken)", rb_error_factory_name(eb)) + } + _ => format!("{q}_wv_error(taken[0], taken[1])"), + } +} + +/// Render the module-level completion trampoline of one async function. It +/// resolves the call's queue from `context`, converts the error or result +/// (copying and releasing any owned buffer), and pushes it; nothing raised +/// while converting escapes into the C frame, so the waiting call always +/// wakes up. +fn render_async_trampoline( + w: &mut CodeWriter, + module: &ModuleBinding, + f: &FnBinding, + a: &AsyncBinding, +) { + let mut formals = vec!["ctx".to_string(), "err".to_string()]; + formals.extend(a.callback_params.iter().skip(2).map(|p| p.name.clone())); + w.blank(); + w.line("# @api private"); + w.line(format!("# Completion trampoline for {}.", f.name)); + w.block( + format!( + "{} = FFI::Function.new(:void, [{}]) do |{}|", + rb_async_const(a), + rb_abi_types(&a.callback_params).join(", "), + formals.join(", ") + ), + "end", + |w| { + w.line("queue = _wv_async_finish(ctx)"); + w.line("begin"); + w.scope(|w| { + w.line("taken = _wv_take_boxed_error(err)"); + w.line("if taken"); + w.scope(|w| { + w.line(format!("queue << {}", rb_error_from_taken(module, f, ""))); + }); + w.line("else"); + w.scope(|w| { + let ptr = match plan::ret_pass(f.ret.as_ref(), "") { + RetPass::String | RetPass::Bytes | RetPass::Buffer => "result_ptr", + _ => "result", + }; + receive_value(w, f.ret.as_ref(), ptr, "result_len", "", "value = "); + w.line("queue << value"); + }); + w.line("end"); + }); + w.line("rescue Exception => e # rubocop:disable Lint/RescueException"); + w.scope(|w| { + w.line("queue << e"); + }); + w.line("end"); + }, + ); + w.blank(); +} + +/// Emit the statements that turn a received value into its Ruby value: a +/// sync return (`ptr` names the C result, `len` the length read from +/// `out_len`), an async result, or an iterator element. Owned strings, bytes, +/// and buffers are copied and released; objects are adopted. The final +/// expression is prefixed with `dest` (an assignment such as `"value = "`, +/// or empty to leave it as the block's value). +fn receive_value(w: &mut CodeWriter, ty: Option<&Ty>, ptr: &str, len: &str, q: &str, dest: &str) { + match plan::ret_pass(ty, "") { + RetPass::Void => { + w.line(format!("{dest}nil")); + } + RetPass::Direct => { + w.line(format!("{dest}{ptr}")); + } + RetPass::String => { + w.line(format!("{dest}{q}_wv_take_string({ptr}, {len})")); + } + RetPass::Bytes => { + w.line(format!("{dest}{q}_wv_take_bytes({ptr}, {len})")); + } + RetPass::Buffer => { + let ty = ty.expect("buffered value has a type"); + w.block( + format!("{dest}{q}_wv_decode({ptr}, {len}) do |_wv_r|"), + "end", + |w| { + render_wv_read(w, "_wv_r", "_wv_value", ty, 0, q); + w.line("_wv_value"); + }, + ); + } + RetPass::Object { nullable, .. } => { + let class = local_type_name( + ty.and_then(Ty::interface_name) + .expect("object value names an interface"), + ); + if nullable { + w.line(format!( + "{dest}{ptr}.null? ? nil : {class}._from_ptr({ptr})" + )); + } else { + w.line(format!( + "raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if {ptr}.null?" + )); + w.line(format!("{dest}{class}._from_ptr({ptr})")); + } + } + } +} + +/// The objects a call borrows, as `(Ruby expression, pinned pointer local)` +/// pairs: the receiver for an instance method, then each object parameter. +fn pinned_objects(f: &FnBinding, scope: &RbScope) -> Vec<(String, String)> { + let mut pins = Vec::new(); + if matches!(scope, RbScope::Method { .. }) { + pins.push(("self".to_string(), "_wv_self".to_string())); + } + for p in &f.params { + if matches!(p.arg_pass(), ArgPass::Object { .. }) { + let name = rb_param_name(&p.name); + pins.push((name.clone(), format!("_wv_{name}"))); + } + } + pins +} + +/// Run `body` inside a `_wv_pin` block over `pins` (each object stays alive, +/// and its wrapper can't release its reference, until the block returns), or +/// directly when nothing is borrowed. +fn with_pins( + w: &mut CodeWriter, + q: &str, + pins: &[(String, String)], + body: impl FnOnce(&mut CodeWriter), +) { + if pins.is_empty() { + body(w); + return; + } + let exprs: Vec<&str> = pins.iter().map(|(e, _)| e.as_str()).collect(); + let vars: Vec<&str> = pins.iter().map(|(_, v)| v.as_str()).collect(); + w.block( + format!("{q}_wv_pin({}) do |{}|", exprs.join(", "), vars.join(", ")), + "end", + body, + ); +} + +/// Emit the statements converting the parameters into the locals the C call +/// borrows. Strings and bytes become private binary copies, buffered values +/// are encoded, and callback implementations are registered last, so nothing +/// that can raise runs between a registration and the call that hands it to +/// the producer. +fn render_param_prep(w: &mut CodeWriter, f: &FnBinding, q: &str) { + for p in &f.params { + let name = rb_param_name(&p.name); + match p.arg_pass() { + ArgPass::String { .. } => { + w.line(format!("{name}_s = {q}_wv_str({name})")); + } + ArgPass::Bytes { .. } => { + w.line(format!("{name}_s = {q}_wv_bytes({name})")); + } + ArgPass::Buffer { .. } => { + w.line(format!("{name}_w = WvBufferWriter.new")); + render_wv_write(w, &format!("{name}_w"), &name, &p.ty, 0, q); + } + _ => {} + } + } +} + +/// Emit the statements that hand the producer what it adopts, immediately +/// before the call: seal the buffers (minting the references their object +/// tokens carry; a buffer without objects seals to its bytes as is) and then +/// register every callback-interface parameter. Registration can't raise, +/// so nothing between here and the call can strand a minted reference or a +/// registration. +fn render_handoff(w: &mut CodeWriter, f: &FnBinding, q: &str) { + let sealed: Vec = f + .params + .iter() + .filter(|p| matches!(p.arg_pass(), ArgPass::Buffer { .. })) + .map(|p| rb_param_name(&p.name)) + .collect(); + match sealed.as_slice() { + [] => {} + [one] => { + w.line(format!("{one}_s = {q}_wv_seal({one}_w).first")); + } + many => { + w.line(format!( + "{} = {q}_wv_seal({})", + many.iter() + .map(|n| format!("{n}_s")) + .collect::>() + .join(", "), + many.iter() + .map(|n| format!("{n}_w")) + .collect::>() + .join(", ") + )); + } + } + render_callback_registrations(w, f, q); +} + +/// Register every callback-interface parameter (see [`render_handoff`]). +fn render_callback_registrations(w: &mut CodeWriter, f: &FnBinding, q: &str) { + for p in &f.params { + if matches!(p.arg_pass(), ArgPass::Callback { .. }) { + let name = rb_param_name(&p.name); + w.line(format!("{name}_ctx = {q}_wv_cb_register({name})")); + } + } +} + +/// The Ruby argument expressions one parameter contributes to the C call, +/// per its [`ArgPass`] contract. +fn rb_call_args(p: &ParamBinding) -> Vec { + let name = rb_param_name(&p.name); + match p.arg_pass() { + ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + vec![format!("{name}_s"), format!("{name}_s.bytesize")] + } + ArgPass::Object { .. } => vec![format!("_wv_{name}")], + ArgPass::Callback { .. } => { + let cb = + p.ty.callback_interface_name() + .expect("callback plan names a callback interface"); + vec![ + format!("{name}_ctx"), + format!("{}.to_ptr", rb_vtable_const(cb)), + ] + } + ArgPass::Direct { .. } if matches!(p.ty, Ty::Bool) => { + vec![format!("({name} ? true : false)")] + } + ArgPass::Direct { .. } => vec![name], + } +} + +/// The leading C arguments of a call: the pinned receiver, then every +/// parameter's slots. +fn call_args(f: &FnBinding, scope: &RbScope) -> Vec { + let mut args = Vec::new(); + if matches!(scope, RbScope::Method { .. }) { + args.push("_wv_self".to_string()); + } + for p in &f.params { + args.extend(rb_call_args(p)); + } + args +} + +fn formals(f: &FnBinding) -> Vec { + f.params.iter().map(|p| rb_param_name(&p.name)).collect() +} + +fn render_deprecation(w: &mut CodeWriter, f: &FnBinding) { + if let Some(msg) = &f.deprecated { + w.line(format!("warn '[DEPRECATED] {}'", rb_str_literal(msg))); + } +} + +/// Render the sync wrapper: convert the parameters, pin the borrowed +/// objects, make the C call with a fresh `ErrorStruct`, route the out-err +/// slot through the function's checker, then receive the result per the +/// scope. +fn render_sync( + w: &mut CodeWriter, + module: &ModuleBinding, + f: &FnBinding, + symbol: &str, + scope: &RbScope, +) { + let q = scope.qualifier(); + let checker = rb_checker_name(f, module.error.as_ref()); + let has_out_len = matches!( + plan::ret_pass(f.ret.as_ref(), ""), + RetPass::String | RetPass::Bytes | RetPass::Buffer + ); + w.block(scope.def_open(f, &formals(f)), "end", |w| { + render_deprecation(w, f); + render_param_prep(w, f, &q); + with_pins(w, &q, &pinned_objects(f, scope), |w| { + w.line("err = ErrorStruct.new"); + let mut args = call_args(f, scope); + if has_out_len { + w.line("out_len = FFI::MemoryPointer.new(:size_t)"); + args.push("out_len".into()); + } + args.push("err".into()); + render_handoff(w, f, &q); + let call = format!("{q}{symbol}({})", args.join(", ")); + if f.ret.is_some() { + w.line(format!("result = {call}")); + } else { + w.line(call); + } + w.line(format!("{q}{checker}(err)")); + match scope { + RbScope::Init { .. } => { + w.line( + "raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if result.null?", + ); + w.line("_wv_init(result)"); + } + RbScope::Factory { .. } => { + w.line( + "raise Error.new(GENERIC_ERROR_CODE, 'null object pointer') if result.null?", + ); + w.line("_from_ptr(result)"); + } + _ if f.ret.is_some() => { + receive_value(w, f.ret.as_ref(), "result", "out_len.read(:size_t)", &q, ""); + } + _ => {} + } + }); + }); +} + +/// Render the async wrapper: launch the call with the function's completion +/// trampoline and block on a `Queue` until it fires (`Queue#pop` releases +/// the GVL; the ffi gem runs the trampoline on a Ruby thread). Blocking is +/// the idiomatic Ruby surface; callers wanting concurrency run the call in a +/// Thread. +/// +/// A cancellable function takes a `cancel:` keyword. Without one, the +/// wrapper still passes a private token, so interrupting the wait +/// (`Thread#raise`, `Timeout`) cancels the producer's work too. +fn render_async(w: &mut CodeWriter, f: &FnBinding, a: &AsyncBinding, scope: &RbScope) { + let q = scope.qualifier(); + let mut params = formals(f); + if f.cancellable { + params.push("cancel: nil".into()); + } + w.line("# Blocks until the call completes on a producer thread."); + if f.cancellable { + w.line("# @param cancel [CancelToken, nil] cancels the call; it then raises Cancelled"); + } + w.block(scope.def_open(f, ¶ms), "end", |w| { + render_deprecation(w, f); + render_param_prep(w, f, &q); + if f.cancellable { + w.line("own_token = CancelToken.new if cancel.nil?"); + w.line("token = cancel || own_token"); + } + let pins = pinned_objects(f, scope); + if !pins.is_empty() { + // Assigned inside the pin block, read after it. + w.line("queue = nil"); + } + with_pins(w, &q, &pins, |w| { + render_handoff(w, f, &q); + w.line(format!("ctx, queue = {q}_wv_async_begin")); + let mut args = call_args(f, scope); + if f.cancellable { + args.push("token._wv_ptr".into()); + } + args.push(rb_async_const(a)); + args.push("ctx".into()); + w.line(format!("{q}{}({})", a.launch.symbol, args.join(", "))); + }); + if f.cancellable { + w.line(format!("{q}_wv_async_wait(queue, token)")); + } else { + w.line(format!("{q}_wv_async_wait(queue)")); + } + if f.cancellable { + w.dedent(); + w.line("ensure"); + w.indent(); + w.line("own_token&.close"); + } + }); +} + +/// Render the iterator wrapper: a lazy `Enumerator` per the pull contract of +/// [`weaveffi_model::plan::IteratorProtocol`]. +/// +/// The producer iterator launches inside the enumerator block, on the first +/// pull, so nothing leaks when the enumerator is never started (launch +/// errors therefore raise on the first pull). Each step issues exactly one +/// `next` call, each element is received like a return of its type, and +/// `destroy` runs exactly once from an `ensure`, so an early `break` or an +/// error mid-iteration still releases the handle. +fn render_iterator( + w: &mut CodeWriter, + module: &ModuleBinding, + f: &FnBinding, + it: &IteratorBinding, + scope: &RbScope, +) { + let q = scope.qualifier(); + let checker = rb_checker_name(f, module.error.as_ref()); + let elem_pass = plan::ret_pass(Some(&it.elem), ""); + let needs_len = matches!( + elem_pass, + RetPass::String | RetPass::Bytes | RetPass::Buffer + ); + w.line("# Returns a lazy Enumerator that pulls one element per step. The"); + w.line("# producer iterator starts on the first pull and is released when"); + w.line("# iteration finishes or is abandoned early."); + w.block(scope.def_open(f, &formals(f)), "end", |w| { + render_deprecation(w, f); + render_param_prep(w, f, &q); + // The block closes over the converted arguments, so they stay + // referenced until the launch call runs. + w.block("Enumerator.new do |y|", "end", |w| { + w.line("err = ErrorStruct.new"); + let mut args = call_args(f, scope); + args.push("err".into()); + let launch = format!("{q}{}({})", it.launch.symbol, args.join(", ")); + let pins = pinned_objects(f, scope); + let mut handoff = CodeWriter::two_space(); + render_handoff(&mut handoff, f, &q); + let handoff = handoff.finish(); + if pins.is_empty() { + w.block_raw(&handoff); + w.line(format!("iter = {launch}")); + } else if handoff.is_empty() { + w.line(format!( + "iter = {q}_wv_pin({}) {{ |{}| {launch} }}", + pins.iter() + .map(|(e, _)| e.as_str()) + .collect::>() + .join(", "), + pins.iter() + .map(|(_, v)| v.as_str()) + .collect::>() + .join(", ") + )); + } else { + w.line("iter = nil"); + with_pins(w, &q, &pins, |w| { + w.block_raw(&handoff); + w.line(format!("iter = {launch}")); + }); + } + w.line("begin"); + w.scope(|w| { + w.line(format!("{q}{checker}(err)")); + let item_type = match elem_pass { + RetPass::Direct => rb_direct_type(&it.elem), + _ => ":pointer", + }; + w.line(format!("out_item = FFI::MemoryPointer.new({item_type})")); + let mut next_args = vec!["iter", "out_item"]; + if needs_len { + w.line("out_len = FFI::MemoryPointer.new(:size_t)"); + next_args.push("out_len"); + } + next_args.push("err"); + w.block("loop do", "end", |w| { + w.line(format!( + "has_item = {q}{}({})", + it.next.symbol, + next_args.join(", ") + )); + w.line(format!("{q}{checker}(err)")); + w.line("break if has_item.zero?"); + let item = match elem_pass { + RetPass::Direct => format!("out_item.read({item_type})"), + _ => "out_item.read_pointer".to_string(), + }; + w.line(format!("item = {item}")); + receive_value( + w, + Some(&it.elem), + "item", + "out_len.read(:size_t)", + &q, + "value = ", + ); + w.line("y << value"); + }); + }); + w.line("ensure"); + w.scope(|w| { + w.line(format!("{q}{}(iter) unless iter.null?", it.destroy_symbol)); + }); + w.line("end"); + }); + }); +} diff --git a/crates/weaveffi-gen-ruby/src/codec.rs b/crates/weaveffi-gen/src/targets/ruby/codec.rs similarity index 73% rename from crates/weaveffi-gen-ruby/src/codec.rs rename to crates/weaveffi-gen/src/targets/ruby/codec.rs index 5579a76e..c8de152d 100644 --- a/crates/weaveffi-gen-ruby/src/codec.rs +++ b/crates/weaveffi-gen/src/targets/ruby/codec.rs @@ -6,19 +6,21 @@ //! re-derives the wire folds (records and rich enums as one user-codec //! shape, C-style enums as `i32`, interfaces as `u64` object tokens). //! -//! An object token carries one strong reference. Writing one calls the -//! wrapper's `_wv_clone_ptr` (the interface's `_clone` symbol) so the -//! wrapper keeps its own reference; reading one adopts the pointer into a -//! fresh wrapper through `_from_ptr`, whose finalizer owes the `_destroy`. +//! An object token carries one strong reference. Writing one reserves the +//! token (`write_object`); the runtime's `_wv_seal` mints the references +//! (the interface's `_clone` symbol) just before the call, so the wrapper +//! keeps its own reference and an abandoned encoding strands none. Reading +//! one adopts the pointer into a fresh wrapper through `_from_ptr`, whose +//! finalizer owes the `_destroy`. +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; 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_model::model::Ty; +use weaveffi_model::model::{EnumBinding, StructBinding}; +use weaveffi_model::model::{Prim, WireType}; -use crate::types::rb_field_name; +use crate::targets::ruby::types::rb_field_name; /// The snake_case stem naming a record's or rich enum's generated pack and /// unpack helpers: `Contact` (or `other.Contact`) becomes `contact`, naming @@ -27,52 +29,21 @@ pub(crate) fn wv_stem(name: &str) -> String { local_type_name(name).to_snake_case() } -/// The `WvBufferWriter` method encoding one scalar wire shape, or `None` for -/// the composite shapes that need statement-level rendering. C-style enums -/// encode as their `i32` discriminant. -fn wv_scalar_writer(shape: &WireType) -> Option<&'static str> { - Some(match shape { - 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) => "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, - }) -} - -/// The `WvBufferReader` method decoding one scalar wire shape, mirroring -/// [`wv_scalar_writer`]. -fn wv_scalar_reader(shape: &WireType) -> Option<&'static str> { - Some(match shape { - 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) => "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, - }) +/// The `WvBufferWriter`/`WvBufferReader` method stem (`i32`, `string`, ...) +/// for one scalar wire shape, or `None` for the composite shapes that need +/// statement-level rendering. C-style enums travel as their `i32` +/// discriminant. +fn wv_scalar(shape: &WireType) -> Option<&'static str> { + match shape { + WireType::Prim(p) => Some(p.snake()), + WireType::Enum(_) => Some(Prim::I32.snake()), + _ => None, + } } /// Emit the Ruby statements appending `expr` (a value of IR type `ty`) to /// the buffer writer named `wvar`, following the value-buffer wire format. -/// `q` is the dotted receiver (`"WeaveFFI."` or `""`) qualifying +/// `q` is the dotted receiver (`"Kvstore."` or `""`) qualifying /// module-singleton codec calls inside class bodies. pub(crate) fn render_wv_write( w: &mut CodeWriter, @@ -83,8 +54,8 @@ pub(crate) fn render_wv_write( q: &str, ) { let shape = ty.wire(); - if let Some(m) = wv_scalar_writer(&shape) { - w.line(format!("{wvar}.{m}({expr})")); + if let Some(m) = wv_scalar(&shape) { + w.line(format!("{wvar}.write_{m}({expr})")); return; } match shape { @@ -119,10 +90,9 @@ pub(crate) fn render_wv_write( WireType::User(n) => { w.line(format!("{q}_wv_write_{}({wvar}, {expr})", wv_stem(n))); } - // An object token carries one strong reference, so the wrapper's - // `_clone` symbol mints a fresh one; the wrapper keeps its own. + // Reserved here; `_wv_seal` mints the token's reference. WireType::Object(_) => { - w.line(format!("{wvar}.write_u64({expr}._wv_clone_ptr.address)")); + w.line(format!("{wvar}.write_object({expr})")); } _ => unreachable!("scalar handled above"), } @@ -140,8 +110,8 @@ pub(crate) fn render_wv_read( q: &str, ) { let shape = ty.wire(); - if let Some(m) = wv_scalar_reader(&shape) { - w.line(format!("{var} = {rvar}.{m}")); + if let Some(m) = wv_scalar(&shape) { + w.line(format!("{var} = {rvar}.read_{m}")); return; } match shape { @@ -265,7 +235,10 @@ pub(crate) fn render_rich_enum_codec(out: &mut String, e: &EnumBinding) { } w.line("else"); w.scope(|w| { - w.line(format!("raise Error.new(-1, 'unknown {} variant')", e.name)); + w.line(format!( + "raise Error.new(MARSHAL_ERROR_CODE, 'unknown {} variant')", + e.name + )); }); w.line("end"); }); @@ -304,7 +277,7 @@ pub(crate) fn render_rich_enum_codec(out: &mut String, e: &EnumBinding) { w.line("else"); w.scope(|w| { w.line(format!( - "raise Error.new(-1, \"malformed value buffer: unknown {} tag #{{tag}}\")", + "raise Error.new(MARSHAL_ERROR_CODE, \"malformed value buffer: unknown {} tag #{{tag}}\")", e.name )); }); diff --git a/crates/weaveffi-gen-ruby/src/docs.rs b/crates/weaveffi-gen/src/targets/ruby/docs.rs similarity index 85% rename from crates/weaveffi-gen-ruby/src/docs.rs rename to crates/weaveffi-gen/src/targets/ruby/docs.rs index c5b712f9..95933529 100644 --- a/crates/weaveffi-gen-ruby/src/docs.rs +++ b/crates/weaveffi-gen/src/targets/ruby/docs.rs @@ -1,9 +1,9 @@ //! Doc-comment emission: the Ruby `# ...` spelling of IDL doc strings and //! `@param` tags. -use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::ParamBinding; +use crate::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; +use crate::codegen::CodeWriter; +use weaveffi_model::model::ParamBinding; /// Emits a Ruby `# ...` doc comment at `indent`. Each input line is prefixed /// with `# `; blank lines become `#`. @@ -25,7 +25,7 @@ pub(crate) fn emit_param_docs(w: &mut CodeWriter, params: &[ParamBinding]) { if let Some(first) = lines.next() { w.line(format!( "# @param {} [Object] {}", - crate::types::rb_param_name(&p.name), + crate::targets::ruby::types::rb_param_name(&p.name), first )); } diff --git a/crates/weaveffi-gen-ruby/src/entities.rs b/crates/weaveffi-gen/src/targets/ruby/entities.rs similarity index 72% rename from crates/weaveffi-gen-ruby/src/entities.rs rename to crates/weaveffi-gen/src/targets/ruby/entities.rs index 32ab3ff3..cf261afd 100644 --- a/crates/weaveffi-gen-ruby/src/entities.rs +++ b/crates/weaveffi-gen/src/targets/ruby/entities.rs @@ -1,17 +1,18 @@ //! Entity renderers: plain enums, records, rich enums, interfaces, and the //! typed error surface of a module's declared error domain. +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; use heck::{ToShoutySnakeCase, ToSnakeCase}; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::model::{ +use weaveffi_model::model::{ EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, ModuleBinding, StructBinding, }; -use weaveffi_core::plan::ErrorStrategy; +use weaveffi_model::plan::ErrorStrategy; -use crate::calls::{render_attach_function, render_callable, RbScope}; -use crate::codec::render_wv_read; -use crate::docs::emit_doc; -use crate::types::{rb_field_name, rb_str_literal}; +use crate::targets::ruby::calls::{render_attach_function, render_callable, RbScope}; +use crate::targets::ruby::codec::render_wv_read; +use crate::targets::ruby::docs::emit_doc; +use crate::targets::ruby::types::{rb_field_name, rb_str_literal}; /// The snake_case stem of a domain's generated helpers: `KvError` becomes /// `kv_error`, naming `kv_error_from` and `check_kv_error!`. Domain type @@ -51,10 +52,10 @@ pub(crate) fn rb_checker_name(f: &FnBinding, error: Option<&ErrorBinding>) -> St /// /// Domain codes are validated positive-only; the negative range is reserved /// for the runtime (`-1` generic, `-2` producer panic, `-3` marshalling -/// failure, `-4` a callback-interface implementation that raised). The -/// factory therefore maps only declared codes onto typed classes and lets -/// everything else, negative runtime codes included, fall through to the -/// generic branded `Error`. +/// failure, `-4` a callback-interface implementation that raised, `-5` +/// cancelled). The factory therefore maps only declared codes onto typed +/// classes and lets everything else fall through to `_wv_error` (the +/// generic `Error`, or `Cancelled`). pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorBinding) { let domain = &eb.type_name; let factory = rb_error_factory_name(eb); @@ -73,7 +74,7 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB if idx > 0 { w.blank(); } - let class = weaveffi_core::errors::pascal(&c.name); + let class = weaveffi_model::errors::pascal(&c.name); let doc = c.doc.clone().unwrap_or_else(|| c.message.clone()); let mut d = String::new(); emit_doc(&mut d, &Some(doc), " "); @@ -124,7 +125,7 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB w.line(format!( "{} => {domain}::{},", c.value, - weaveffi_core::errors::pascal(&c.name) + weaveffi_model::errors::pascal(&c.name) )); } }); @@ -143,7 +144,7 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB if eb.codes.iter().any(|c| !c.fields.is_empty()) { w.line("case code"); for c in eb.codes.iter().filter(|c| !c.fields.is_empty()) { - let class = weaveffi_core::errors::pascal(&c.name); + let class = weaveffi_model::errors::pascal(&c.name); w.line(format!("when {}", c.value)); w.scope(|w| { w.line("r = WvBufferReader.new(payload || ''.b)"); @@ -170,7 +171,7 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB w.line("end"); } w.line(format!("cls = {table}[code]")); - w.line("return Error.new(code, message) if cls.nil?"); + w.line("return _wv_error(code, message) if cls.nil?"); w.line("message.empty? ? cls.new : cls.new(message)"); }, ); @@ -180,14 +181,8 @@ pub(crate) fn render_error(out: &mut String, module: &ModuleBinding, eb: &ErrorB "# Raises the typed {domain} for a non-zero error slot." )); w.block(format!("def self.{checker}(err)"), "end", |w| { - w.line("return if err[:code].zero?"); - w.line("code = err[:code]"); - w.line("msg_ptr = err[:message]"); - w.line("msg = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8)"); - w.line("payload_ptr = err[:payload_ptr]"); - w.line("payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len])"); - w.line("weaveffi_error_clear(err.to_ptr)"); - w.line(format!("raise {factory}(code, msg, payload)")); + w.line("taken = _wv_take_error(err)"); + w.line(format!("raise {factory}(*taken) if taken")); }); out.push_str(&w.finish()); } @@ -338,17 +333,24 @@ pub(crate) fn render_rich_enum_class(out: &mut String, e: &EnumBinding) { /// lifecycle symbols plus every constructor, method, and static through the /// shared attach path. Methods carry their implicit leading `self` pointer /// slot in the precomputed ABI signatures, so no special casing is needed -/// here. -pub(crate) fn render_interface_ffi(out: &mut String, i: &InterfaceBinding) { +/// here. `blocking` is the API-wide GVL policy (see +/// [`render_attach_function`]); destroying an object can release callback +/// implementations it holds, so it follows the same policy. +pub(crate) fn render_interface_ffi( + out: &mut String, + module: &ModuleBinding, + i: &InterfaceBinding, + blocking: bool, +) { let mut w = CodeWriter::two_space().with_depth(1); - w.blank(); w.line(format!( "attach_function :{}, [:pointer], :pointer", i.clone_symbol )); w.line(format!( - "attach_function :{}, [:pointer], :void", - i.destroy_symbol + "attach_function :{}, [:pointer], :void{}", + i.destroy_symbol, + if blocking { ", blocking: true" } else { "" } )); out.push_str(&w.finish()); for f in i @@ -357,24 +359,21 @@ pub(crate) fn render_interface_ffi(out: &mut String, i: &InterfaceBinding) { .chain(i.methods.iter()) .chain(i.statics.iter()) { - render_attach_function(out, f); + render_attach_function(out, module, f, blocking); } } -/// Render one interface as a reference-counted object wrapper class. A -/// `{Name}Ptr < FFI::AutoPointer` subclass owns exactly one strong reference -/// and releases it through the interface's `_destroy` symbol either from -/// `close` or, as a backstop, from the GC finalizer (`AutoPointer#free` -/// disables the finalizer, so the two paths never both fire). The wrapper -/// exposes `handle` (the borrowed pointer passed to producer calls), `close`, -/// `closed?`, `_wv_clone_ptr` (a fresh strong reference for the value-buffer -/// codec), and an `initialize_copy` so `dup`/`clone` yield an independent -/// wrapper holding its own reference. A constructor named `new` becomes -/// `initialize`; every other constructor becomes a class-method factory; -/// methods pass `handle` as the leading C argument; statics are class -/// methods. `_from_ptr` adopts an owned pointer the producer already handed -/// over (a return, an async result, an iterator element, a buffer token) -/// without re-running `initialize`. +/// Render one interface as a reference-counted wrapper class on the +/// runtime's `WvObject` base (which supplies `handle`, `close`, `closed?`, +/// `dup`/`clone`, and the call pinning). A `{Name}Ptr < FFI::AutoPointer` +/// subclass owns exactly one strong reference and releases it through the +/// interface's `_destroy` symbol, either from `close` or, as a backstop, +/// from the GC finalizer. A constructor named `new` becomes `initialize`; +/// every other constructor becomes a class-method factory; methods borrow +/// the wrapper's pointer as the leading C argument; statics are class +/// methods. `_from_ptr` adopts a reference the producer handed over (a +/// return, an async result, an iterator element, a buffer token) without +/// re-running `initialize`. pub(crate) fn render_interface_class( out: &mut String, module: &ModuleBinding, @@ -399,93 +398,43 @@ pub(crate) fn render_interface_class( }, ); w.blank(); - - let mut d = String::new(); - emit_doc(&mut d, &i.doc, " "); - w.raw(d); + w.doc(&i.doc, DocCommentStyle::Hash); if let Some(msg) = &i.deprecated { w.line(format!("# @deprecated {msg}")); } - w.line(format!("class {}", i.name)); + w.line(format!("class {} < WvObject", i.name)); w.scope(|w| { - w.line("# @api private"); - w.line("# Adopts one strong reference the producer handed over, without"); - w.line("# re-running initialize."); - w.block("def self._from_ptr(ptr)", "end", |w| { - w.line("obj = allocate"); - w.line(format!( - "obj.instance_variable_set(:@handle, {ptr_class}.new(ptr))" - )); - w.line("obj"); - }); - w.blank(); - w.line("# The borrowed object pointer passed to producer calls."); - w.block("def handle", "end", |w| { - w.line(format!( - "raise Error.new(-1, '{} used after close') if @handle.nil?", - i.name - )); - w.line("@handle"); - }); - w.blank(); - w.line("# Whether close has released this wrapper's reference."); - w.block("def closed?", "end", |w| { - w.line("@handle.nil?"); - }); - w.blank(); - w.line("# Releases this wrapper's reference now rather than at GC time."); - w.line("# Idempotent; the object itself is dropped when the last reference"); - w.line("# anywhere (another wrapper, a record field, the producer) goes."); - w.block("def close", "end", |w| { - w.line("return if @handle.nil?"); - w.line("@handle.free"); - w.line("@handle = nil"); - }); + w.line(format!("WV_PTR = {ptr_class}")); + if !i.constructors.iter().any(|c| c.name == "new") { + w.line("private_class_method :new"); + } w.blank(); w.line("# @api private"); - w.line("# Mints a new strong reference the caller owns (used when this"); - w.line("# object is written into a value buffer)."); - w.block("def _wv_clone_ptr", "end", |w| { - w.line(format!("{rb_module_name}.{}(handle)", i.clone_symbol)); - }); - w.blank(); - w.line("# dup and clone produce an independent wrapper with its own reference."); - w.block("def initialize_copy(other)", "end", |w| { - w.line("super"); - w.line(format!( - "@handle = {ptr_class}.new({rb_module_name}.{}(other.handle))", - i.clone_symbol - )); + w.block("def self._wv_clone(ptr)", "end", |w| { + w.line(format!("{rb_module_name}.{}(ptr)", i.clone_symbol)); }); }); out.push_str(&w.finish()); // Members render at class depth through the shared callable paths, so // sync, async, and iterator members reuse the free-function marshalling. - if let Some(c) = i.constructors.iter().find(|c| c.name == "new") { - let scope = RbScope::Init { - module_name: rb_module_name, - ptr_class: &ptr_class, - }; - render_callable(out, module, c, &scope); - } - for c in i.constructors.iter().filter(|c| c.name != "new") { - let scope = RbScope::Factory { - module_name: rb_module_name, - }; - render_callable(out, module, c, &scope); - } - for f in &i.methods { - let scope = RbScope::Method { - module_name: rb_module_name, - }; - render_callable(out, module, f, &scope); + let scope_of = |f: &FnBinding, ctor: bool| -> RbScope { + let module_name = rb_module_name; + if ctor && f.name == "new" { + RbScope::Init { module_name } + } else if ctor { + RbScope::Factory { module_name } + } else if f.has_self { + RbScope::Method { module_name } + } else { + RbScope::Static { module_name } + } + }; + for c in &i.constructors { + render_callable(out, module, c, &scope_of(c, true)); } - for f in &i.statics { - let scope = RbScope::Static { - module_name: rb_module_name, - }; - render_callable(out, module, f, &scope); + for f in i.methods.iter().chain(i.statics.iter()) { + render_callable(out, module, f, &scope_of(f, false)); } let mut close = CodeWriter::two_space().with_depth(1); diff --git a/crates/weaveffi-gen/src/targets/ruby/mod.rs b/crates/weaveffi-gen/src/targets/ruby/mod.rs new file mode 100644 index 00000000..703b7000 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/ruby/mod.rs @@ -0,0 +1,748 @@ +//! Ruby (ffi gem) binding generator. +//! +//! Emits a gem that binds the C ABI (revision 3) with the `ffi` gem: +//! `lib/{prefix}.rb` (the generated bindings, required as `{prefix}`), +//! `lib/{prefix}/runtime.rb` (the fixed runtime from `runtime/runtime.rb`), +//! a `{name}.gemspec`, and a README. Everything lives in one Ruby module, +//! `PascalCase(name)` unless configured. Interfaces become reference-counted +//! wrapper classes (`close` plus a GC finalizer backstop, `dup`/`clone` for a +//! second reference), records and rich enums become value classes packed +//! into value buffers, async functions block on a queue fed by a +//! module-level completion trampoline (cancellable ones take a `cancel:` +//! token), iterators are lazy `Enumerator`s, and callback interfaces are +//! duck-typed modules backed by one static vtable of pinned `FFI::Function` +//! trampolines per interface. + +mod callbacks; +mod calls; +mod codec; +mod docs; +mod entities; +mod package; +mod runtime; +mod types; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::package::{PackageContext, PackagedFile}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::{checksum_symbol, BindingModel}; +use weaveffi_model::resolved::ResolvedApi; + +use crate::targets::ruby::calls::{render_attach_function, render_callable, RbScope}; +use crate::targets::ruby::entities::{ + render_enum, render_error, render_interface_class, render_interface_ffi, + render_rich_enum_class, render_struct_class, +}; +use crate::targets::ruby::package::{ + render_gemspec, render_packaged_gemspec, render_packaged_readme, render_readme, GemNames, +}; +use crate::targets::ruby::runtime::render_runtime; + +/// Per-target configuration for [`RubyGenerator`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct RubyConfig { + /// Top-level Ruby module name (default: the package name in PascalCase). + pub module_name: Option, + /// Gem name written into the gemspec (default: the package name). + pub gem_name: Option, + /// When `true` (the default), strip the IR module name prefix from + /// emitted Ruby method names, so a `contacts` module exports + /// `create_contact` rather than `contacts_create_contact`. Set to + /// `false` to restore module-prefixed names. + pub strip_module_prefix: bool, + /// Basename of the IDL the CLI was invoked with. + #[serde(skip)] + pub input_basename: Option, +} + +impl Default for RubyConfig { + fn default() -> Self { + Self { + module_name: None, + gem_name: None, + strip_module_prefix: true, + input_basename: None, + } + } +} + +impl RubyConfig { + /// Returns the input IDL basename embedded in generated file headers, + /// falling back to `"api.yml"`. + pub fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } + + /// The gem, module, and require names for `api`: the identity's, unless + /// overridden here. + fn names<'a>(&self, api: &'a ResolvedApi) -> GemNames<'a> { + let identity = api.identity(); + let configured = |v: &Option| { + v.as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(str::to_string) + }; + GemNames { + identity, + gem: configured(&self.gem_name).unwrap_or_else(|| identity.name.clone()), + module: configured(&self.module_name).unwrap_or_else(|| identity.pascal_name()), + } + } +} + +/// Ruby backend: emits an `ffi`-gem package binding the C ABI exposed by the +/// underlying cdylib. +pub struct RubyGenerator; + +impl LanguageBackend for RubyGenerator { + type Config = RubyConfig; + + fn name(&self) -> &'static str { + "ruby" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + let names = config.names(api); + let input_basename = config.input_basename(); + let dir = out_dir.join("ruby"); + let lib_dir = dir.join("lib"); + let lib_file = format!("{}.rb", names.require()); + vec![ + OutputFile::new( + lib_dir.join(&lib_file), + render_bindings(model, &names, config, &lib_file), + ), + OutputFile::new( + lib_dir.join(names.require()).join("runtime.rb"), + render_runtime(names.identity, &names.module, input_basename, "runtime.rb"), + ), + OutputFile::new( + dir.join(names.gemspec_file()), + render_gemspec(&names, input_basename), + ), + OutputFile::new(dir.join("README.md"), render_readme(&names, input_basename)), + ] + } + + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let names = config.names(api); + let input_basename = config.input_basename(); + let lib_file = format!("{}.rb", names.require()); + let bindings = render_bindings(model, &names, config, &lib_file); + let runtime = render_runtime(names.identity, &names.module, input_basename, "runtime.rb"); + let readme = render_packaged_readme(&names, input_basename); + + let ruby_dir = out_dir.join("ruby"); + let mut files = Vec::new(); + for nb in &ctx.binaries.binaries { + let platform = nb.platform; + // RubyGems has no platform string for Android or wasm32 builds, + // so those binaries have no gem to land in. + let Some(ruby_platform) = platform.ruby_platform() else { + continue; + }; + let gem_dir = ruby_dir.join(platform.id()); + let lib_dir = gem_dir.join("lib"); + files.push(PackagedFile::text( + lib_dir.join(&lib_file), + bindings.clone(), + )); + files.push(PackagedFile::text( + lib_dir.join(names.require()).join("runtime.rb"), + runtime.clone(), + )); + // The runtime loader looks for the bundled library under + // `lib/native/` by its platform file name. + files.push(PackagedFile::copy( + lib_dir + .join("native") + .join(platform.lib_filename(&names.identity.library)), + nb.source.clone(), + )); + files.push(PackagedFile::text( + gem_dir.join(names.gemspec_file()), + render_packaged_gemspec(&names, ruby_platform, input_basename), + )); + files.push(PackagedFile::text( + gem_dir.join("README.md"), + readme.clone(), + )); + } + Some(files) + } +} + +/// Render `lib/{prefix}.rb`: the runtime require, the contract check, then +/// each module's typed error surface, entities, codecs, FFI attachments, +/// callback interfaces, interface classes, and free functions. +fn render_bindings( + model: &BindingModel, + names: &GemNames, + config: &RubyConfig, + lib_file: &str, +) -> String { + let module_name = &names.module; + // Any call can reach a stored callback implementation, so an API with + // callback interfaces releases the GVL on every call. + let blocking = model.has_callback_interfaces(); + let mut out = render_prelude(CommentStyle::Hash, config.input_basename()); + out.push_str("# frozen_string_literal: true\n\n"); + out.push_str(&format!( + "require_relative '{}/runtime'\n\n", + names.require() + )); + out.push_str(&format!("# Ruby bindings for {}.\n", names.identity.name)); + out.push_str(&format!("module {module_name}\n")); + out.push_str(" # Contract checksums of the top-level modules these bindings were\n"); + out.push_str(" # generated from; a library built from a different API fails to load.\n"); + out.push_str(" _wv_check_contract!(\n"); + for root in model.roots() { + let checksum = root.checksum.expect("top-level modules carry a checksum"); + out.push_str(&format!( + " '{}' => [:{}, 0x{checksum:016x}],\n", + root.name, + checksum_symbol(&model.prefix, &root.name) + )); + } + out.push_str(" )\n"); + for m in &model.modules { + out.push_str(&format!("\n # === Module: {} ===\n", m.dot_path)); + // The typed error surface comes first so the domain class exists + // before any wrapper references its checker. + if let Some(eb) = m.error.as_ref().filter(|e| e.declared_here) { + render_error(&mut out, m, eb); + } + for e in &m.enums { + // A plain C-style enum is a module of integer constants; a rich + // (algebraic) enum is a tagged value-class hierarchy packed into + // value buffers by the codec helpers below. + if e.is_rich() { + render_rich_enum_class(&mut out, e); + } else { + render_enum(&mut out, e); + } + } + for s in &m.structs { + render_struct_class(&mut out, s); + } + // Value-buffer codecs: one pack/unpack pair per record and rich enum. + for s in &m.structs { + codec::render_struct_codec(&mut out, s); + } + for e in m.enums.iter().filter(|e| e.is_rich()) { + codec::render_rich_enum_codec(&mut out, e); + } + let attaches = !m.interfaces.is_empty() || !m.functions.is_empty(); + if attaches { + out.push('\n'); + } + for i in &m.interfaces { + render_interface_ffi(&mut out, m, i, blocking); + } + for f in &m.functions { + render_attach_function(&mut out, m, f, blocking); + } + // Callback interfaces precede interfaces and functions because their + // static vtables are what interface members and free functions pass. + for cb in &m.callback_interfaces { + callbacks::render_callback_interface(&mut out, cb, &model.prefix); + } + for i in &m.interfaces { + render_interface_class(&mut out, m, i, module_name); + } + for f in &m.functions { + let scope = RbScope::Free { + module_path: &m.path, + strip_module_prefix: config.strip_module_prefix, + }; + render_callable(&mut out, m, f, &scope); + } + } + out.push_str("end\n\n"); + out.push_str(&render_trailer(CommentStyle::Hash, lib_file)); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::platform::{BinarySet, Platform}; + use weaveffi_model::ir::{ + Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, + TypeRef, + }; + use weaveffi_model::pkg::Identity; + + fn param(name: &str, ty: TypeRef) -> Param { + Param { + name: name.into(), + ty, + doc: None, + } + } + + fn func(name: &str, params: Vec, returns: Option) -> Function { + Function { + name: name.into(), + params, + returns, + doc: None, + throws: false, + r#async: false, + cancellable: false, + deprecated: None, + } + } + + fn field(name: &str, ty: TypeRef) -> StructField { + StructField { + name: name.into(), + ty, + doc: None, + } + } + + fn named(name: &str) -> TypeRef { + TypeRef::Named(name.into()) + } + + /// One module exercising every object shape plus strings, an iterator, + /// a callback interface, and a cancellable async method, under the + /// identity `kv-store` (prefix `kv_store`, module `KvStore`). + fn fixture() -> ResolvedApi { + let mut compact = func("compact", vec![], Some(TypeRef::I64)); + compact.r#async = true; + compact.cancellable = true; + let kv = Module { + name: "kv".into(), + doc: None, + functions: vec![ + func( + "maybe_store", + vec![param("store", TypeRef::Optional(Box::new(named("Store"))))], + Some(TypeRef::Optional(Box::new(named("Store")))), + ), + func( + "scan", + vec![], + Some(TypeRef::Iterator(Box::new(named("Store")))), + ), + func( + "subscribe", + vec![param("listener", named("Listener"))], + None, + ), + func( + "describe", + vec![param("bundle", named("Bundle"))], + Some(named("Bundle")), + ), + ], + interfaces: vec![InterfaceDef { + name: "Store".into(), + doc: Some("A key-value store.".into()), + deprecated: None, + constructors: vec![func("new", vec![param("path", TypeRef::StringUtf8)], None)], + methods: vec![ + func( + "get", + vec![param("key", TypeRef::StringUtf8)], + Some(TypeRef::StringUtf8), + ), + compact, + ], + statics: vec![], + }], + callback_interfaces: vec![CallbackInterfaceDef { + name: "Listener".into(), + doc: Some("Receives store events.".into()), + deprecated: None, + methods: vec![ + func( + "on_message", + vec![ + param("text", TypeRef::StringUtf8), + param("weight", TypeRef::I32), + ], + Some(TypeRef::Bool), + ), + func( + "on_bundle", + vec![ + param("bundle", named("Bundle")), + param("store", named("Store")), + param("alt", TypeRef::Optional(Box::new(named("Store")))), + ], + None, + ), + ], + }], + structs: vec![StructDef { + name: "Bundle".into(), + doc: None, + deprecated: None, + fields: vec![ + field("primary", named("Store")), + field("extras", TypeRef::List(Box::new(named("Store")))), + ], + }], + enums: vec![], + errors: None, + modules: vec![], + }; + ResolvedApi::assume_valid(Api { + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![kv], + }) + .with_identity(Identity::named("kv-store")) + } + + fn generate(config: &RubyConfig) -> Vec { + let api = fixture(); + let model = BindingModel::build(&api); + RubyGenerator.files(&api, &model, Utf8Path::new("out"), config) + } + + fn file<'a>(files: &'a [OutputFile], path: &str) -> &'a str { + &files + .iter() + .find(|f| f.path.as_str() == path) + .unwrap_or_else(|| { + panic!( + "no {path} in {:?}", + files.iter().map(|f| &f.path).collect::>() + ) + }) + .contents + } + + fn bindings() -> String { + file( + &generate(&RubyConfig::default()), + "out/ruby/lib/kv_store.rb", + ) + .to_string() + } + + fn assert_has(src: &str, needle: &str) { + assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); + } + + /// Nothing but the generated-file banner mentions WeaveFFI. + fn assert_unbranded(src: &str) { + let banner = |l: &str| { + l.contains("Generated by WeaveFFI") || l.contains("To regenerate: weaveffi generate") + }; + for line in src.lines().filter(|l| !banner(l)) { + assert!( + !line.to_ascii_lowercase().contains("weaveffi"), + "branded line: {line}" + ); + } + } + + #[test] + fn layout_follows_the_identity() { + let files = generate(&RubyConfig::default()); + let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect(); + assert_eq!( + paths, + [ + "out/ruby/lib/kv_store.rb", + "out/ruby/lib/kv_store/runtime.rb", + "out/ruby/kv-store.gemspec", + "out/ruby/README.md", + ] + ); + let gemspec = file(&files, "out/ruby/kv-store.gemspec"); + assert_has(gemspec, "s.name = 'kv-store'"); + assert_has(gemspec, "s.authors = ['kv-store']"); + let src = file(&files, "out/ruby/lib/kv_store.rb"); + assert_has(src, "require_relative 'kv_store/runtime'"); + assert_has(src, "module KvStore\n"); + for f in &files { + assert_unbranded(&f.contents); + } + } + + #[test] + fn config_overrides_gem_and_module_names() { + let config = RubyConfig { + gem_name: Some("acme-kv".into()), + module_name: Some("Acme".into()), + ..RubyConfig::default() + }; + let files = generate(&config); + assert_has( + file(&files, "out/ruby/acme-kv.gemspec"), + "s.name = 'acme-kv'", + ); + // The require path stays the identity prefix. + let src = file(&files, "out/ruby/lib/kv_store.rb"); + assert_has(src, "module Acme\n"); + assert_has(src, "Acme.kv_store_kv_Store_destroy(ptr)"); + assert_has( + file(&files, "out/ruby/lib/kv_store/runtime.rb"), + "module Acme\n", + ); + } + + #[test] + fn runtime_loads_the_identity_library_and_checks_abi_3() { + let files = generate(&RubyConfig::default()); + let rt = file(&files, "out/ruby/lib/kv_store/runtime.rb"); + assert_has(rt, "ENV['KV_STORE_LIBRARY']"); + assert_has(rt, "'libkv_store.dylib'"); + assert_has(rt, "'libkv_store.so'"); + assert_has(rt, "'kv_store.dll'"); + assert_has(rt, &format!("ABI_VERSION = {}", crate::cabi::ABI_VERSION)); + assert_has(rt, "attach_function :kv_store_abi_version, [], :uint32"); + assert_has( + rt, + "attach_function :kv_store_free_bytes, [:pointer, :size_t], :void", + ); + assert_has( + rt, + "attach_function :kv_store_cancel_token_create, [], :pointer", + ); + assert_has(rt, "CANCELLED_ERROR_CODE = -5"); + assert_has(rt, "class Cancelled < Error"); + assert!(!rt.contains("free_string"), "{rt}"); + assert!(!rt.contains("{{"), "unsubstituted placeholder in:\n{rt}"); + } + + #[test] + fn bindings_check_every_root_checksum_at_load() { + let api = fixture(); + let model = BindingModel::build(&api); + let checksum = model.roots().next().unwrap().checksum.unwrap(); + assert_has( + &bindings(), + &format!("'kv' => [:kv_store_kv_checksum, 0x{checksum:016x}],"), + ); + } + + #[test] + fn interface_wrapper_owns_one_reference() { + let src = bindings(); + assert_has( + &src, + "attach_function :kv_store_kv_Store_clone, [:pointer], :pointer", + ); + assert_has( + &src, + "attach_function :kv_store_kv_Store_destroy, [:pointer], :void, blocking: true", + ); + assert_has(&src, "class StorePtr < FFI::AutoPointer"); + assert_has(&src, "class Store < WvObject\n WV_PTR = StorePtr\n"); + assert_has(&src, "KvStore.kv_store_kv_Store_clone(ptr)"); + // The constructor adopts the returned reference; methods pin `self`. + assert_has(&src, "def initialize(path)"); + assert_has(&src, "_wv_init(result)"); + assert_has(&src, "KvStore._wv_pin(self) do |_wv_self|"); + } + + #[test] + fn strings_cross_as_pointer_and_length() { + let src = bindings(); + assert_has(&src, "key_s = KvStore._wv_str(key)"); + assert_has( + &src, + "result = KvStore.kv_store_kv_Store_get(_wv_self, key_s, key_s.bytesize, out_len, err)", + ); + assert_has( + &src, + "KvStore._wv_take_string(result, out_len.read(:size_t))", + ); + // Callback string arguments are borrowed (ptr, len) runs. + assert_has(&src, "do |ctx, text_ptr, text_len, weight, out_err|"); + assert_has( + &src, + "text_v = _wv_borrow_bytes(text_ptr, text_len).force_encoding(Encoding::UTF_8)", + ); + } + + #[test] + fn nullable_objects_map_to_nil_in_both_directions() { + let src = bindings(); + assert_has(&src, "def self.maybe_store(store)"); + assert_has(&src, "_wv_pin(store) do |_wv_store|"); + assert_has(&src, "result.null? ? nil : Store._from_ptr(result)"); + } + + #[test] + fn records_reserve_object_tokens_and_seal_them_before_the_call() { + let src = bindings(); + assert_has(&src, "w.write_object(v.primary)"); + assert_has(&src, "bundle_s = _wv_seal(bundle_w).first"); + assert_has(&src, "_wv_primary = Store._from_ptr(r.read_object_token)"); + } + + #[test] + fn iterator_is_a_lazy_enumerator_adopting_objects() { + let src = bindings(); + assert_has(&src, "Enumerator.new do |y|"); + assert_has( + &src, + "has_item = kv_store_kv_ScanIterator_next(iter, out_item, err)", + ); + assert_has(&src, "value = Store._from_ptr(item)"); + assert_has( + &src, + "kv_store_kv_ScanIterator_destroy(iter) unless iter.null?", + ); + } + + #[test] + fn cancellable_async_takes_a_cancel_token() { + let src = bindings(); + assert_has(&src, "def compact(cancel: nil)"); + assert_has(&src, "own_token = CancelToken.new if cancel.nil?"); + assert_has( + &src, + "KvStore.kv_store_kv_Store_compact(_wv_self, token._wv_ptr, KV_STORE_KV_STORE_COMPACT_CALLBACK, ctx)", + ); + assert_has(&src, "KvStore._wv_async_wait(queue, token)"); + assert_has(&src, "own_token&.close"); + assert_has( + &src, + "attach_function :kv_store_kv_Store_compact, [:pointer, :pointer, :pointer, :pointer], :void, blocking: true", + ); + assert_has( + &src, + "KV_STORE_KV_STORE_COMPACT_CALLBACK = FFI::Function.new(:void, [:pointer, :pointer, :int64]) do |ctx, err, result|", + ); + assert_has( + &src, + "rescue Exception => e # rubocop:disable Lint/RescueException\n queue << e", + ); + } + + #[test] + fn callback_interface_renders_module_vtable_and_trampolines() { + let src = bindings(); + assert_has(&src, "module Listener\n"); + assert_has(&src, "raise NotImplementedError"); + assert_has(&src, "class WvListenerVtable < FFI::Struct"); + assert_has( + &src, + "layout :on_message, :pointer,\n :on_bundle, :pointer,\n :free, :pointer", + ); + assert_has(&src, "WV_LISTENER_ON_MESSAGE = FFI::Function.new(:bool,"); + assert_has(&src, "impl.on_message(text_v, weight_v) ? true : false"); + // Objects are adopted before anything that can raise. + let bundle = src.find("WV_LISTENER_ON_BUNDLE").unwrap(); + let tail = &src[bundle..]; + assert!( + tail.find("store_v = Store._from_ptr(store)").unwrap() + < tail.find("bundle_r = ").unwrap() + ); + assert_has(&src, "alt_v = alt.null? ? nil : Store._from_ptr(alt)"); + assert_has(&src, "WV_LISTENER_VTABLE[:free] = WV_LISTENER_FREE"); + assert_eq!(src.matches("= WvListenerVtable.new").count(), 1); + // Passing an implementation registers it right before the call. + assert_has(&src, "listener_ctx = _wv_cb_register(listener)"); + assert_has( + &src, + "kv_store_kv_subscribe(listener_ctx, WV_LISTENER_VTABLE.to_ptr, err)", + ); + } + + #[test] + fn calls_keep_the_gvl_without_callback_interfaces() { + let api = ResolvedApi::assume_valid(Api { + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![Module { + name: "math".into(), + doc: None, + functions: vec![func( + "add", + vec![param("a", TypeRef::I32), param("b", TypeRef::Bool)], + Some(TypeRef::I32), + )], + interfaces: vec![], + callback_interfaces: vec![], + structs: vec![], + enums: vec![], + errors: None, + modules: vec![], + }], + }) + .with_identity(Identity::named("calc")); + let model = BindingModel::build(&api); + let files = RubyGenerator.files(&api, &model, Utf8Path::new("out"), &RubyConfig::default()); + let src = file(&files, "out/ruby/lib/calc.rb").to_string(); + assert_has( + &src, + "attach_function :calc_math_add, [:int32, :bool, :pointer], :int32\n", + ); + assert_has(&src, "calc_math_add(a, (b ? true : false), err)"); + } + + #[test] + fn rendering_is_deterministic() { + assert_eq!(bindings(), bindings()); + } + + #[test] + fn package_skips_platforms_without_a_gem_string() { + let api = fixture(); + let model = BindingModel::build(&api); + let mut binaries = BinarySet::new("kv_store"); + binaries.insert(Platform::MacosArm64, "/tmp/darwin-arm64/libkv_store.dylib"); + binaries.insert(Platform::AndroidArm64, "/tmp/android-arm64/libkv_store.so"); + binaries.insert(Platform::Wasm32, "/tmp/wasm32/kv_store.wasm"); + let ctx = PackageContext { + binaries: &binaries, + input_basename: Some("kv.yml"), + }; + let files = RubyGenerator + .package( + &api, + &model, + &ctx, + Utf8Path::new("out"), + &RubyConfig::default(), + ) + .expect("ruby supports packaging"); + let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect(); + assert!( + paths + .iter() + .all(|p| p.starts_with("out/ruby/darwin-arm64/")), + "{paths:?}" + ); + assert!(paths.contains(&"out/ruby/darwin-arm64/lib/native/libkv_store.dylib")); + assert!(paths.contains(&"out/ruby/darwin-arm64/lib/kv_store/runtime.rb")); + let gemspec = files + .iter() + .find(|f| f.path.as_str().ends_with("kv-store.gemspec")) + .unwrap(); + let crate::package::FileContent::Text(text) = &gemspec.content else { + panic!("gemspec is text"); + }; + assert_has(text, "s.platform = 'arm64-darwin'"); + } +} diff --git a/crates/weaveffi-gen/src/targets/ruby/package.rs b/crates/weaveffi-gen/src/targets/ruby/package.rs new file mode 100644 index 00000000..efce0fe0 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/ruby/package.rs @@ -0,0 +1,188 @@ +//! Gem packaging surfaces: the `.gemspec` (source-only and per-platform) +//! and the README. +//! +//! A gemspec is Ruby source, so every interpolated user string (summary, +//! authors, license, homepage) goes through the single-quote escape in +//! [`rb_str_literal`]; a quote or trailing backslash in package metadata +//! can't corrupt the emitted spec. + +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::pkg::Identity; + +use crate::targets::ruby::types::rb_str_literal; + +/// The names one generated gem is published and loaded under. +pub(crate) struct GemNames<'a> { + /// The package identity. + pub identity: &'a Identity, + /// The gem name (`{name}` unless configured). + pub gem: String, + /// The top-level Ruby module (`PascalCase(name)` unless configured). + pub module: String, +} + +impl GemNames<'_> { + /// The `require` path: the identity prefix. + pub(crate) fn require(&self) -> &str { + &self.identity.prefix + } + + /// The gemspec file name. + pub(crate) fn gemspec_file(&self) -> String { + format!("{}.gemspec", self.gem) + } +} + +/// The metadata lines shared by both gemspec shapes. RubyGems requires a +/// non-empty author list, so an identity without authors falls back to the +/// gem name. +fn gemspec_metadata(names: &GemNames) -> String { + let id = names.identity; + let authors = if id.authors.is_empty() { + vec![names.gem.clone()] + } else { + id.authors.clone() + }; + let authors = authors + .iter() + .map(|a| format!("'{}'", rb_str_literal(a))) + .collect::>() + .join(", "); + let mut out = format!( + " s.name = '{}'\n s.version = '{}'\n", + rb_str_literal(&names.gem), + rb_str_literal(&id.version) + ); + out.push_str(&format!( + " s.summary = '{}'\n", + rb_str_literal(&id.description_or_default()) + )); + out.push_str(&format!(" s.authors = [{authors}]\n")); + if let Some(license) = &id.license { + out.push_str(&format!( + " s.license = '{}'\n", + rb_str_literal(license) + )); + } + if let Some(homepage) = id.homepage.as_ref().or(id.repository.as_ref()) { + out.push_str(&format!( + " s.homepage = '{}'\n", + rb_str_literal(homepage) + )); + } + out +} + +/// Render the source-only gemspec emitted by `generate`. +pub(crate) fn render_gemspec(names: &GemNames, input_basename: &str) -> String { + let prelude = render_prelude(CommentStyle::Hash, input_basename); + let trailer = render_trailer(CommentStyle::Hash, &names.gemspec_file()); + let meta = gemspec_metadata(names); + format!( + "{prelude}Gem::Specification.new do |s| +{meta} s.files = Dir['lib/**/*.rb'] + s.require_paths = ['lib'] + s.required_ruby_version = '>= 2.7' + + s.add_dependency 'ffi', '~> 1.15' +end + +{trailer}" + ) +} + +/// Render a platform gemspec: it stamps `s.platform` with the RubyGems +/// platform string (`Platform::ruby_platform`) and ships the bundled native +/// library alongside the Ruby sources. +pub(crate) fn render_packaged_gemspec( + names: &GemNames, + ruby_platform: &str, + input_basename: &str, +) -> String { + let prelude = render_prelude(CommentStyle::Hash, input_basename); + let trailer = render_trailer(CommentStyle::Hash, &names.gemspec_file()); + let meta = gemspec_metadata(names); + format!( + "{prelude}Gem::Specification.new do |s| +{meta} s.platform = '{ruby_platform}' + s.files = Dir['lib/**/*.{{rb,so,dylib,dll}}'] + s.require_paths = ['lib'] + s.required_ruby_version = '>= 2.7' + + s.add_dependency 'ffi', '~> 1.15' +end + +{trailer}" + ) +} + +/// README for the source-only gem layout. +pub(crate) fn render_readme(names: &GemNames, input_basename: &str) -> String { + let prelude = render_prelude(CommentStyle::Xml, input_basename); + let trailer = render_trailer(CommentStyle::Xml, "README.md"); + let gem = &names.gem; + let version = &names.identity.version; + let require = names.require(); + let module = &names.module; + let env = names.identity.library_env_var(); + let (macos, linux, windows) = names.identity.library_files(); + format!( + r#"{prelude}# {gem} (Ruby) + +Ruby bindings for `{gem}` using the [ffi](https://github.com/ffi/ffi) gem. + +## Prerequisites + +- Ruby 2.7 or newer +- The native library (`{linux}`, `{macos}`, or `{windows}`) on the library + search path, or its path in the `{env}` environment variable. + +## Install + +```bash +gem build {gem}.gemspec +gem install {gem}-{version}.gem +``` + +## Usage + +```ruby +require '{require}' + +{module}::ABI_VERSION +``` + +{trailer}"# + ) +} + +/// README for a packaged Ruby platform gem. +pub(crate) fn render_packaged_readme(names: &GemNames, input_basename: &str) -> String { + let prelude = render_prelude(CommentStyle::Xml, input_basename); + let trailer = render_trailer(CommentStyle::Xml, "README.md"); + let gem = &names.gem; + let version = &names.identity.version; + let require = names.require(); + format!( + r#"{prelude}# {gem} (Ruby) + +Ruby bindings for `{gem}` using the [ffi](https://github.com/ffi/ffi) gem, +with the native library bundled for this platform. The library loads +automatically; no external setup is required. + +## Install + +```bash +gem build {gem}.gemspec +gem install {gem}-{version}-*.gem +``` + +## Usage + +```ruby +require '{require}' +``` + +{trailer}"# + ) +} diff --git a/crates/weaveffi-gen/src/targets/ruby/runtime.rs b/crates/weaveffi-gen/src/targets/ruby/runtime.rs new file mode 100644 index 00000000..af22dcfe --- /dev/null +++ b/crates/weaveffi-gen/src/targets/ruby/runtime.rs @@ -0,0 +1,40 @@ +//! The fixed Ruby runtime every generated gem ships as +//! `lib/{prefix}/runtime.rb`: the library loader (honoring +//! `{PREFIX}_LIBRARY`, then a bundled `lib/native/` copy, then the system +//! search path), the load-time ABI check, the error classes, the value-buffer +//! reader and writer, cancel tokens, the handle table behind callback +//! interfaces and async completions, and the `WvObject` wrapper base class. +//! +//! The source lives in `runtime/runtime.rb` as ordinary Ruby; rendering only +//! substitutes the `{{PLACEHOLDER}}` names below. + +use crate::cabi::ABI_VERSION; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use weaveffi_model::pkg::Identity; + +const RUNTIME_RB: &str = include_str!("runtime/runtime.rb"); + +/// Render `lib/{prefix}/runtime.rb` for `identity`, opening the Ruby module +/// `module_name`. +pub(crate) fn render_runtime( + identity: &Identity, + module_name: &str, + input_basename: &str, + file_name: &str, +) -> String { + let (lib_macos, lib_linux, lib_windows) = identity.library_files(); + let body = RUNTIME_RB + .replace("{{MODULE}}", module_name) + .replace("{{PREFIX}}", &identity.prefix) + .replace("{{LIBRARY}}", &identity.library) + .replace("{{LIBRARY_ENV}}", &identity.library_env_var()) + .replace("{{LIB_MACOS}}", &lib_macos) + .replace("{{LIB_LINUX}}", &lib_linux) + .replace("{{LIB_WINDOWS}}", &lib_windows) + .replace("{{ABI_VERSION}}", &ABI_VERSION.to_string()); + format!( + "{}{body}\n{}", + render_prelude(CommentStyle::Hash, input_basename), + render_trailer(CommentStyle::Hash, file_name) + ) +} diff --git a/crates/weaveffi-gen/src/targets/ruby/runtime/runtime.rb b/crates/weaveffi-gen/src/targets/ruby/runtime/runtime.rb new file mode 100644 index 00000000..15f31c2f --- /dev/null +++ b/crates/weaveffi-gen/src/targets/ruby/runtime/runtime.rb @@ -0,0 +1,696 @@ +# frozen_string_literal: true + +require 'ffi' +require 'monitor' + +# The fixed runtime every generated {{MODULE}} binding builds on: the native +# library loader, the load-time ABI check, the error classes, the +# value-buffer reader and writer, cancel tokens, the handle table behind +# callback interfaces and async completions, and the object wrapper base. +module {{MODULE}} + extend FFI::Library + + # An explicit path in {{LIBRARY_ENV}} wins. Otherwise a library bundled + # under lib/native/ (a platform gem) is preferred over the system search + # path. + _wv_override = ENV['{{LIBRARY_ENV}}'] + if _wv_override && !_wv_override.empty? + ffi_lib _wv_override + else + _wv_name = + case FFI::Platform::OS + when /darwin/ then '{{LIB_MACOS}}' + when /mswin|mingw|windows/ then '{{LIB_WINDOWS}}' + else '{{LIB_LINUX}}' + end + _wv_bundled = File.expand_path(File.join('..', 'native', _wv_name), __dir__) + ffi_lib(File.exist?(_wv_bundled) ? _wv_bundled : _wv_name) + end + + # The C ABI revision these bindings speak. Checked before anything else is + # attached, so a mismatched library fails at require time instead of + # misreading an error struct or a value buffer later. + ABI_VERSION = {{ABI_VERSION}} + begin + attach_function :{{PREFIX}}_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, "the loaded {{LIBRARY}} library does not export {{PREFIX}}_abi_version " \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = {{PREFIX}}_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "{{LIBRARY}} ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + + # The `{{PREFIX}}_error` out-parameter every call reports through. + class ErrorStruct < FFI::Struct + layout :code, :int32, + :message, :pointer, + :payload_ptr, :pointer, + :payload_len, :size_t + end + + # Codes reserved by the runtime. Positive codes belong to a module's error + # domain; these negative codes are raised as a plain Error (or Cancelled) + # regardless of whether the function declares errors. + GENERIC_ERROR_CODE = -1 + PANIC_ERROR_CODE = -2 + MARSHAL_ERROR_CODE = -3 + # A callback-interface implementation raised; the message carries the + # exception text. + FOREIGN_ERROR_CODE = -4 + # The call was cancelled through its CancelToken. + CANCELLED_ERROR_CODE = -5 + + # The base class of every error the library raises. + class Error < StandardError + # The ABI error code: positive for a domain error, negative for a runtime + # failure. + attr_reader :code + + def initialize(code, message) + @code = code + super(message) + end + end + + # Raised when a cancellable call completes because its CancelToken fired. + class Cancelled < Error + def initialize(message = 'cancelled') + super(CANCELLED_ERROR_CODE, message) + end + end + + attach_function :{{PREFIX}}_error_set, [:pointer, :int32, :string], :void + attach_function :{{PREFIX}}_error_clear, [:pointer], :void + attach_function :{{PREFIX}}_error_free, [:pointer], :void + attach_function :{{PREFIX}}_free_bytes, [:pointer, :size_t], :void + attach_function :{{PREFIX}}_cancel_token_create, [], :pointer + attach_function :{{PREFIX}}_cancel_token_cancel, [:pointer], :void + attach_function :{{PREFIX}}_cancel_token_is_cancelled, [:pointer], :bool + attach_function :{{PREFIX}}_cancel_token_destroy, [:pointer], :void + + # @api private + # The error for a code outside any declared domain. + def self._wv_error(code, message) + return Cancelled.new(message.empty? ? 'cancelled' : message) if code == CANCELLED_ERROR_CODE + + Error.new(code, message) + end + + # @api private + # Copies the code, message, and payload out of a reported error and clears + # it. Returns nil when the struct reports success. + def self._wv_take_error(err) + code = err[:code] + return nil if code.zero? + + msg_ptr = err[:message] + message = msg_ptr.null? ? '' : msg_ptr.read_string.force_encoding(Encoding::UTF_8) + payload_ptr = err[:payload_ptr] + payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) + {{PREFIX}}_error_clear(err.to_ptr) + [code, message, payload] + end + + # @api private + # Like _wv_take_error for the heap-boxed error an async completion + # receives; the box is released with {{PREFIX}}_error_free. + def self._wv_take_boxed_error(err_ptr) + return nil if err_ptr.null? + + taken = _wv_take_error(ErrorStruct.new(err_ptr)) + {{PREFIX}}_error_free(err_ptr) + taken + end + + # @api private + # Raises the generic error for a call that declares no error domain. + def self.check_error!(err) + taken = _wv_take_error(err) + return if taken.nil? + + raise _wv_error(taken[0], taken[1]) + end + + # @api private + # The UTF-8 bytes of a String argument, as a private binary copy the + # producer borrows for the call. + def self._wv_str(value) + value.to_s.encode(Encoding::UTF_8).b + end + + # @api private + # A private binary copy of a bytes argument. + def self._wv_bytes(value) + value.to_s.b + end + + # @api private + # Copies a borrowed (ptr, len) run without releasing it. + def self._wv_borrow_bytes(ptr, len) + ptr.null? || len.zero? ? ''.b : ptr.read_string(len) + end + + # @api private + # Copies a returned (ptr, len) allocation, then releases it. + def self._wv_take_bytes(ptr, len) + return ''.b if ptr.null? + + data = len.zero? ? ''.b : ptr.read_string(len) + {{PREFIX}}_free_bytes(ptr, len) + data + end + + # @api private + # Copies a returned UTF-8 string, then releases it. + def self._wv_take_string(ptr, len) + _wv_take_bytes(ptr, len).force_encoding(Encoding::UTF_8) + end + + # A cancellation signal for a cancellable async call. Pass one as the + # `cancel:` keyword, then call #cancel from any thread; the call raises + # Cancelled unless it already completed. A token may be shared by several + # calls and stays cancelled once cancelled. + class CancelToken + # @api private + # Owns the consumer's reference to the native token. + class Ptr < FFI::AutoPointer + def self.release(ptr) + {{MODULE}}.{{PREFIX}}_cancel_token_destroy(ptr) + end + end + + def initialize + @ptr = Ptr.new({{MODULE}}.{{PREFIX}}_cancel_token_create) + end + + # Requests cancellation. Idempotent. + def cancel + {{MODULE}}.{{PREFIX}}_cancel_token_cancel(@ptr) unless @ptr.nil? + nil + end + + # Whether #cancel has been called. + def cancelled? + !@ptr.nil? && {{MODULE}}.{{PREFIX}}_cancel_token_is_cancelled(@ptr) + end + + # Releases the native token now rather than at GC time. Calls already + # launched with it keep their own reference. + def close + return if @ptr.nil? + + @ptr.free + @ptr = nil + end + + # @api private + def _wv_ptr + raise Error.new(GENERIC_ERROR_CODE, 'CancelToken used after close') if @ptr.nil? + + @ptr + end + end + + # @api private + # Appends values in the value-buffer wire format: little-endian, packed, + # no alignment. + class WvBufferWriter + def initialize + @buf = +''.b + @objects = [] + end + + # Reserves an object token. The strong reference it carries is minted + # by #seal, just before the buffer is handed to the producer, so an + # encoding abandoned on the way never strands a reference. + def write_object(object) + raise TypeError, "expected a wrapper object, got #{object.class}" unless object.is_a?(WvObject) + + @objects << [@buf.bytesize, object] + write_u64(0) + end + + # Mints the reference each reserved object token carries, recording it + # in `minted`, and returns the finished encoding. + def seal(minted) + @objects.each do |offset, object| + ptr = object._wv_clone_ptr + minted << [object.class, ptr] + @buf[offset, 8] = [ptr.address].pack('Q<') + end + @buf + end + + # Every append is a binary String, so the buffer stays binary and byte + # offsets index it directly. + def write_bool(value) + @buf << [value ? 1 : 0].pack('C') + end + + def write_flag(value) + write_bool(value) + end + + def write_i8(value) + @buf << [value].pack('c') + end + + def write_u8(value) + @buf << [value].pack('C') + end + + def write_i16(value) + @buf << [value].pack('s<') + end + + def write_u16(value) + @buf << [value].pack('S<') + end + + def write_i32(value) + @buf << [value].pack('l<') + end + + def write_u32(value) + @buf << [value].pack('L<') + end + + def write_len(value) + write_u32(value) + end + + def write_i64(value) + @buf << [value].pack('q<') + end + + def write_u64(value) + @buf << [value].pack('Q<') + end + + def write_f32(value) + @buf << [value].pack('e') + end + + def write_f64(value) + @buf << [value].pack('E') + end + + def write_string(value) + write_bytes(value.to_s.encode(Encoding::UTF_8)) + end + + def write_bytes(value) + bytes = value.to_s.b + write_u32(bytes.bytesize) + @buf << bytes + end + end + + # @api private + # Reads values in the 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(count, what) + if @pos + count > @data.bytesize + raise Error.new(MARSHAL_ERROR_CODE, "malformed value buffer: truncated #{what}") + end + + bytes = @data.byteslice(@pos, count) + @pos += count + bytes + end + + def read_bool + byte = take(1, 'bool').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: bool byte out of range') if byte > 1 + + byte == 1 + end + + def read_flag + byte = take(1, 'option flag').unpack1('C') + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: option flag out of range') if byte > 1 + + byte == 1 + end + + def read_i8 + take(1, 'i8').unpack1('c') + end + + def read_u8 + take(1, 'u8').unpack1('C') + end + + def read_i16 + take(2, 'i16').unpack1('s<') + end + + def read_u16 + take(2, 'u16').unpack1('S<') + end + + def read_i32 + take(4, 'i32').unpack1('l<') + end + + def read_u32 + take(4, 'u32').unpack1('L<') + end + + # A u32 element count. Counts are not checked against the remaining + # bytes, since an element can encode to zero bytes; a short buffer still + # fails on the first truncated read. + def read_len + read_u32 + end + + def read_i64 + take(8, 'i64').unpack1('q<') + end + + def read_u64 + take(8, 'u64').unpack1('Q<') + end + + def read_f32 + take(4, 'f32').unpack1('e') + end + + def read_f64 + take(8, 'f64').unpack1('E') + end + + def read_string + text = read_bytes.force_encoding(Encoding::UTF_8) + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: string is not valid UTF-8') unless text.valid_encoding? + + text + end + + def read_bytes + take(read_u32, 'byte run') + end + + # An object token: a non-zero u64 carrying one strong reference the + # caller adopts into a wrapper. + def read_object_token + addr = read_u64 + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: null object token') if addr.zero? + + FFI::Pointer.new(addr) + end + + def expect_end! + return if @pos == @data.bytesize + + raise Error.new(MARSHAL_ERROR_CODE, 'malformed value buffer: trailing bytes after value') + end + end + + # @api private + # Seals the writers of one call's object-carrying buffers (see + # WvBufferWriter#seal) and returns their encodings. If any reference can't + # be minted (a closed wrapper), the ones already minted are released before + # the error propagates. The producer adopts every minted reference once the + # call is made. + def self._wv_seal(*writers) + minted = [] + writers.map { |writer| writer.seal(minted) } + rescue Exception # rubocop:disable Lint/RescueException + minted.each { |cls, ptr| cls::WV_PTR.release(ptr) } + raise + end + + # @api private + # Decodes a returned value buffer: copies and releases it, yields a reader + # to the block, and checks that the block consumed every byte. + def self._wv_decode(ptr, len) + reader = WvBufferReader.new(_wv_take_bytes(ptr, len)) + value = yield reader + reader.expect_end! + value + end + + # Ruby objects the producer refers to by an integer key: callback-interface + # implementations (the key is the vtable `ctx`) and pending async + # completions (the key is the completion `context`). The producer never + # sees a Ruby object address. A Monitor rather than a Mutex, so a + # finalizer that runs while this thread holds the lock can't deadlock it. + @wv_handles = {} + @wv_next_handle = 0 + @wv_handles_lock = Monitor.new + + # @api private + def self._wv_handle_put(value) + @wv_handles_lock.synchronize do + @wv_next_handle += 1 + @wv_handles[@wv_next_handle] = value + FFI::Pointer.new(@wv_next_handle) + end + end + + # @api private + def self._wv_handle_get(key) + @wv_handles_lock.synchronize { @wv_handles[key.address] } + end + + # @api private + def self._wv_handle_take(key) + @wv_handles_lock.synchronize { @wv_handles.delete(key.address) } + end + + # @api private + # Registers a callback-interface implementation and returns its `ctx`. + # Never raises, so nothing can strand the registration between this and + # the call that hands it to the producer (an unusable implementation fails + # when the producer calls it, with FOREIGN_ERROR_CODE). + def self._wv_cb_register(impl) + _wv_handle_put([:callback, impl]) + end + + # @api private + # Resolves a trampoline's `ctx` back to the registered implementation. + def self._wv_cb_lookup(ctx) + kind, impl = _wv_handle_get(ctx) + raise Error.new(FOREIGN_ERROR_CODE, 'callback context is not registered') unless kind == :callback + + impl + end + + # @api private + # Drops an implementation when the producer releases its last reference. + def self._wv_cb_free(ctx) + _wv_handle_take(ctx) + nil + end + + # @api private + # Reports a callback implementation's exception through `out_err`. The + # producer copies the message. + def self._wv_cb_fail(out_err, exception) + message = exception.message.to_s + message = exception.class.name if message.empty? + message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).delete("\0") + {{PREFIX}}_error_set(out_err, FOREIGN_ERROR_CODE, message) + nil + end + + # @api private + # Registers a pending async call; returns its completion context and the + # queue its completion is delivered on. + def self._wv_async_begin + queue = Queue.new + [_wv_handle_put([:async, queue]), queue] + end + + # @api private + # The queue of the pending call a completion belongs to. Each context + # completes exactly once, so its entry is removed here. + def self._wv_async_finish(ctx) + kind, queue = _wv_handle_take(ctx) + kind == :async ? queue : Queue.new + end + + # @api private + # Blocks until a pending call completes, raising its error. If the wait is + # interrupted (Thread#raise, Thread#kill, Timeout), the call's token is + # cancelled before the interruption propagates. + def self._wv_async_wait(queue, token = nil) + value = + begin + queue.pop + rescue Exception # rubocop:disable Lint/RescueException + token&.cancel + raise + end + raise value if value.is_a?(Exception) + + value + end + + # @api private + # The base of every interface wrapper. A wrapper owns one strong reference + # to its native object and releases it exactly once, from #close or (as a + # backstop) from the GC finalizer of its FFI::AutoPointer. Calls borrow the + # pointer through #_wv_call, so a #close that races an in-flight call on + # another thread only releases the reference once the call returns. + class WvObject + # @api private + # Adopts one strong reference the producer handed over. + def self._from_ptr(ptr) + obj = allocate + obj._wv_init(ptr) + obj + end + + # @api private + def _wv_init(ptr) + @wv_handle = self.class::WV_PTR.new(ptr) + @wv_lock = Mutex.new + @wv_calls = 0 + @wv_closing = false + end + + # The native object pointer (borrowed; valid until #close). + def handle + handle = @wv_handle + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if handle.nil? || @wv_closing + + handle + end + + # Whether #close has released this wrapper's reference. + def closed? + @wv_handle.nil? || @wv_closing + end + + # Releases this wrapper's reference now rather than at GC time. + # Idempotent. The object itself lives until its last reference anywhere + # (another wrapper, a record field, the producer) goes. + def close + handle = @wv_lock.synchronize do + next nil if @wv_handle.nil? || @wv_closing + + if @wv_calls.zero? + taken = @wv_handle + @wv_handle = nil + taken + else + @wv_closing = true + nil + end + end + handle&.free + nil + end + + # @api private + # Yields the native pointer, keeping the reference alive until the block + # returns. + def _wv_call + handle = @wv_lock.synchronize do + raise Error.new(GENERIC_ERROR_CODE, "#{self.class.name} used after close") if @wv_handle.nil? || @wv_closing + + @wv_calls += 1 + @wv_handle + end + begin + yield handle + ensure + _wv_leave + end + end + + # @api private + # Mints a new strong reference the caller owns (an object written into a + # value buffer carries its own reference). + def _wv_clone_ptr + _wv_call { |ptr| self.class._wv_clone(ptr) } + end + + # dup and clone produce an independent wrapper with its own reference to + # the same object. + def initialize_copy(other) + super + _wv_init(other._wv_clone_ptr) + end + + private + + def _wv_leave + release = @wv_lock.synchronize do + @wv_calls -= 1 + next nil unless @wv_closing && @wv_calls.zero? + + taken = @wv_handle + @wv_handle = nil + @wv_closing = false + taken + end + release&.free + end + end + + # @api private + # Yields the native pointers of `objects` (NULL for nil), each kept alive + # until the block returns. + def self._wv_pin(*objects, &block) + _wv_pin_from(objects, 0, [], &block) + end + + # @api private + def self._wv_pin_from(objects, index, pointers, &block) + return yield(*pointers) if index == objects.length + + object = objects[index] + if object.nil? + _wv_pin_from(objects, index + 1, pointers + [FFI::Pointer::NULL], &block) + else + object._wv_call { |ptr| _wv_pin_from(objects, index + 1, pointers + [ptr], &block) } + end + end + + # @api private + # Checks each top-level module's contract checksum (`module name => + # [checksum function, expected value]`) against the library, so bindings + # generated from a different API fail at require time. + def self._wv_check_contract!(checksums) + checksums.each do |mod, (symbol, expected)| + begin + attach_function symbol, [], :uint64 + rescue FFI::NotFoundError + raise LoadError, "the loaded {{LIBRARY}} library has no module '#{mod}' (#{symbol} is missing)" + end + actual = public_send(symbol) + next if actual == expected + + raise LoadError, "{{LIBRARY}} module '#{mod}' does not match these bindings: expected contract " \ + "checksum 0x#{expected.to_s(16)}, the library reports 0x#{actual.to_s(16)}; " \ + 'regenerate the bindings' + end + end + + # @api private + # Live native resources of one kind (0 objects, 1 callbacks, 2 iterators, + # 3 cancel tokens, 4 byte allocations). Only libraries built with the + # `leak-check` feature export the counter. + def self._wv_debug_live(kind) + unless respond_to?(:{{PREFIX}}_debug_live) + begin + attach_function :{{PREFIX}}_debug_live, [:int32], :uint64 + rescue FFI::NotFoundError + raise Error.new(GENERIC_ERROR_CODE, '{{LIBRARY}} was built without the leak-check feature') + end + end + {{PREFIX}}_debug_live(kind) + end +end diff --git a/crates/weaveffi-gen/src/targets/ruby/types.rs b/crates/weaveffi-gen/src/targets/ruby/types.rs new file mode 100644 index 00000000..10636283 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/ruby/types.rs @@ -0,0 +1,90 @@ +//! Ruby type mapping and naming: the FFI-gem vocabulary for ABI slots, the +//! identifier policy for user-chosen IDL names, and string escaping for Ruby +//! literals. + +use crate::lang::{escape_ident, RUBY_KEYWORDS}; +use heck::ToSnakeCase; +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::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 `_`). +/// IDL names are usually already snake, so the case conversion is a safety +/// net for camelCase inputs. +pub(crate) fn rb_param_name(name: &str) -> String { + escape_ident(&name.to_snake_case(), RUBY_KEYWORDS) +} + +/// The Ruby spelling of a user-chosen field name: verbatim unless it's a +/// Ruby reserved word, in which case it gains a trailing `_` so keyword +/// arguments and locals derived from it stay parseable. +pub(crate) fn rb_field_name(name: &str) -> String { + escape_ident(name, RUBY_KEYWORDS) +} + +/// Maps a shared ABI [`CType`] onto its Ruby FFI type symbol. Every pointer +/// (strings and buffers cross as `const uint8_t*`, objects and out-slots as +/// typed pointers) is `:pointer`; `bool` is the 1-byte C `bool`. +pub(crate) fn rb_ffi_type(ty: &CType) -> &'static str { + match ty { + CType::Bool => ":bool", + CType::Int8 => ":int8", + CType::Int16 => ":int16", + CType::Int32 | CType::Enum { .. } => ":int32", + CType::Uint8 => ":uint8", + CType::Uint16 => ":uint16", + CType::Uint32 => ":uint32", + CType::Int64 => ":int64", + CType::Uint64 => ":uint64", + CType::Float => ":float", + CType::Double => ":double", + CType::Size => ":size_t", + CType::Void => ":void", + _ => ":pointer", + } +} + +/// Map lowered ABI slots onto Ruby FFI type symbols. +pub(crate) fn rb_abi_types(params: &[AbiParam]) -> Vec { + params + .iter() + .map(|p| rb_ffi_type(&p.ty).to_string()) + .collect() +} + +/// The `FFI::MemoryPointer` type of a direct iterator element out-slot, read +/// back with `Pointer#read`. +pub(crate) fn rb_direct_type(ty: &Ty) -> &'static str { + match ty { + Ty::Bool => ":bool", + Ty::I8 => ":int8", + Ty::I16 => ":int16", + Ty::I32 | Ty::Enum(_) => ":int32", + Ty::U8 => ":uint8", + Ty::U16 => ":uint16", + Ty::U32 => ":uint32", + Ty::I64 => ":int64", + Ty::U64 => ":uint64", + Ty::F32 => ":float", + Ty::F64 => ":double", + other => unreachable!("{other} is not a direct type"), + } +} + +/// The Ruby literal a callback-interface trampoline returns to the producer +/// after its implementation raised: the zero value of the method's direct +/// return type, or `nil` for a void method. +pub(crate) fn rb_direct_default(ty: Option<&Ty>) -> &'static str { + match ty { + None => "nil", + Some(Ty::Bool) => "false", + Some(Ty::F32 | Ty::F64) => "0.0", + Some(_) => "0", + } +} + +/// Escape a string for embedding in a single-quoted Ruby literal (the two +/// characters with meaning there: backslash and the quote itself). +pub(crate) fn rb_str_literal(s: &str) -> String { + s.replace('\\', "\\\\").replace('\'', "\\'") +} diff --git a/crates/weaveffi-gen-swift/src/callbacks.rs b/crates/weaveffi-gen/src/targets/swift/callbacks.rs similarity index 50% rename from crates/weaveffi-gen-swift/src/callbacks.rs rename to crates/weaveffi-gen/src/targets/swift/callbacks.rs index 3d6b0def..4bd5729d 100644 --- a/crates/weaveffi-gen-swift/src/callbacks.rs +++ b/crates/weaveffi-gen/src/targets/swift/callbacks.rs @@ -1,30 +1,28 @@ //! Callback-interface rendering: the Swift `protocol` a consumer implements, //! the box class that retains one implementation across the C boundary, and -//! the process-wide vtable of `@convention(c)` trampolines the producer calls +//! the process-wide vtable of `@convention(c)` trampolines the library calls //! through. //! //! Every clause of [`CallbackProtocol`] is rendered here: one static vtable //! per interface, a pointer-keyed context (the retained box), arguments //! received per [`RetPass`] (borrowed strings, bytes, and buffers are copied //! or decoded; object arguments are adopted), and a `do`/`catch` around every -//! implementation call so a thrown Swift error is reported through -//! `weaveffi_error_set` with the foreign error code instead of unwinding +//! implementation call so a thrown Swift error is reported through the +//! runtime's `error_set` with the foreign error code instead of unwinding //! through the C frame. -use weaveffi_core::cabi::c_param_name; -use weaveffi_core::codegen::common::DocCommentStyle; -use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::lang; -use weaveffi_core::model::{ - CallbackInterfaceBinding, CallbackMethodBinding, ModuleBinding, ParamBinding, Ty, -}; -use weaveffi_core::plan::{CallbackProtocol, RetPass}; -use weaveffi_core::utils::local_type_name; +use crate::cabi::c_param_name; +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use crate::lang; +use crate::utils::local_type_name; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding, Ty}; +use weaveffi_model::plan::{CallbackProtocol, RetPass}; -use crate::codec::{fresh, read_value_stmts}; -use crate::docs::emit_fn_doc; -use crate::types::{ - c_enum_type, callback_box_name, callback_vtable_name, swift_ident, swift_type_ctx, SwiftCtx, +use crate::targets::swift::codec::decode_closure; +use crate::targets::swift::docs::emit_fn_doc; +use crate::targets::swift::types::{ + callback_box_name, callback_vtable_name, deprecated_attr, swift_ident, SwiftCtx, }; /// The Swift spelling of one trampoline closure formal: the ABI slot name, @@ -45,59 +43,31 @@ fn camel_method(m: &CallbackMethodBinding) -> CallbackMethodBinding { m } -/// The Swift return clause of a protocol requirement: empty for `void`. -fn ret_clause(m: &CallbackMethodBinding, ctx: SwiftCtx) -> String { - m.ret - .as_ref() - .map(|t| format!(" -> {}", swift_type_ctx(t, ctx))) - .unwrap_or_default() -} - -/// The Swift literal a trampoline returns after reporting a failure, in the -/// method's C return type: `false`, `0`, or a zero C enum. -fn c_default(m: &CallbackMethodBinding, c_prefix: &str, module: &str) -> Option { - m.ret.as_ref().map(|t| match t { - Ty::Bool => "false".to_string(), - Ty::Enum(name) => format!("{}(0)", c_enum_type(name, c_prefix, module)), - _ => "0".to_string(), - }) -} - -/// Emit the statements receiving one trampoline argument per its [`RetPass`] -/// and return the expression handed to the implementation. Borrowed strings, -/// bytes, and buffers are copied or decoded before the implementation runs -/// (the producer owns them only for the dispatch); object arguments carry -/// one strong reference the new wrapper adopts. -fn receive_arg( - w: &mut CodeWriter, - p: &ParamBinding, - rp: &RetPass, - ctx: SwiftCtx, - counter: &mut usize, -) -> String { +/// The expression handing one trampoline argument to the implementation, +/// received per its [`RetPass`]: borrowed strings, bytes, and buffers are +/// copied or decoded before the implementation runs (the library owns them +/// only for the dispatch); object arguments carry one strong reference the +/// new wrapper adopts. +fn receive_arg(w: &CodeWriter, p: &ParamBinding, rp: &RetPass, ctx: &SwiftCtx) -> String { let n0 = slot_ident(&p.abi[0].name); + let n1 = || slot_ident(&p.abi[1].name); match rp { RetPass::Direct => match &p.ty { - Ty::Enum(_) => format!("{}(rawValue: {n0}.rawValue)!", swift_type_ctx(&p.ty, ctx)), + Ty::Enum(name) => format!( + "wvEnumCase({}.self, numericCast({n0}.rawValue))", + ctx.ty_name(local_type_name(name)) + ), _ => n0, }, - RetPass::String => format!("String(cString: {n0}!)"), - RetPass::Bytes => { - let n1 = slot_ident(&p.abi[1].name); - format!("{n0}.map {{ Data(bytes: $0, count: {n1}) }} ?? Data()") - } + RetPass::String => format!("wvBorrowString({n0}, {})", n1()), + RetPass::Bytes => format!("wvBorrowBytes({n0}, {})", n1()), RetPass::Buffer => { - let n1 = slot_ident(&p.abi[1].name); - let buf = fresh(counter, "b"); - let reader = fresh(counter, "r"); - w.line(format!( - "let {buf} = [UInt8](UnsafeBufferPointer(start: {n0}, count: {n1}))" - )); - w.line(format!("var {reader} = WvReader(bytes: {buf})")); - let v = fresh(counter, "v"); - read_value_stmts(w, &p.ty, &v, &reader, ctx, counter); - w.line(format!("{reader}.finish()")); - v + let decode = decode_closure(w, &p.ty, ctx); + if decode.starts_with('{') { + format!("wvBorrowBuffer({n0}, {}) {decode}", n1()) + } else { + format!("wvBorrowBuffer({n0}, {}, {decode})", n1()) + } } RetPass::Object { nullable, .. } => { let wrapper = ctx.ty_name(local_type_name( @@ -107,7 +77,7 @@ fn receive_arg( if *nullable { format!("{n0}.map {{ {wrapper}(ptr: $0) }}") } else { - format!("{wrapper}(ptr: {n0}!)") + format!("{wrapper}(ptr: wvNonNull({n0}))") } } RetPass::Void => unreachable!("a parameter is never void"), @@ -115,26 +85,24 @@ fn receive_arg( } /// Render the `public protocol` the consumer conforms to: one `throws` -/// requirement per method with idiomatic labels and types. A conforming -/// method may omit `throws`; one that throws is reported to the producer as a -/// foreign failure. -fn render_protocol(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, ctx: SwiftCtx) { +/// requirement per method with idiomatic labels and types. Implementations +/// are classes (`AnyObject`) the library may call from any thread +/// (`Sendable`). +fn render_protocol(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, ctx: &SwiftCtx) { let name = ctx.ty_name(local_type_name(&cb.name)); if cb.doc.is_some() { w.doc(&cb.doc, DocCommentStyle::TripleSlash); w.line("///"); } - w.line("/// Implement this protocol and pass the value where the API expects it; the"); - w.line("/// producer keeps it alive for as long as it holds the callback and may call"); - w.line("/// any method from any thread. A thrown error aborts the producer's call and"); - w.line("/// surfaces to the original caller as a foreign error (code -4)."); + w.line("/// Implement this protocol with a class and pass an instance where the API"); + w.line("/// expects it; the library keeps it alive for as long as it holds the"); + w.line("/// callback and may call any method from any thread. A thrown error aborts"); + w.line("/// the library's call and surfaces to the original caller as a foreign error"); + w.line("/// (code -4)."); if let Some(msg) = &cb.deprecated { - w.line(format!( - "@available(*, deprecated, message: \"{}\")", - msg.replace('"', "\\\"") - )); + w.line(deprecated_attr(msg)); } - w.line(format!("public protocol {name} {{")); + w.line(format!("public protocol {name}: AnyObject, Sendable {{")); w.indent(); for m in &cb.methods { let m = camel_method(m); @@ -144,21 +112,22 @@ fn render_protocol(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, ctx: Swift w.raw(tmp); } if let Some(msg) = &m.deprecated { - w.line(format!( - "@available(*, deprecated, message: \"{}\")", - msg.replace('"', "\\\"") - )); + w.line(deprecated_attr(msg)); } let params = m .params .iter() - .map(|p| format!("{}: {}", p.name, swift_type_ctx(&p.ty, ctx))) + .map(|p| format!("{}: {}", p.name, ctx.swift_type(&p.ty))) .collect::>() .join(", "); + let ret = m + .ret + .as_ref() + .map(|t| format!(" -> {}", ctx.swift_type(t))) + .unwrap_or_default(); w.line(format!( - "func {}({params}) throws{}", - swift_ident(&m.name), - ret_clause(&m, ctx) + "func {}({params}) throws{ret}", + swift_ident(&m.name) )); } w.dedent(); @@ -167,16 +136,16 @@ fn render_protocol(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, ctx: Swift } /// Render the internal box that pins one implementation behind the `void* -/// ctx` the producer holds: `Unmanaged.passRetained` when passing, one +/// ctx` the library holds: `Unmanaged.passRetained` when passing, one /// `release` from the vtable's `free`. -fn render_box(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, ctx: SwiftCtx) { +fn render_box(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, ctx: &SwiftCtx) { let proto = ctx.ty_name(local_type_name(&cb.name)); let box_name = callback_box_name(&cb.name); w.line(format!( - "/// Retains one `{proto}` implementation across the C boundary; the producer's" + "/// Retains one `{proto}` implementation across the C boundary; the library's" )); w.line("/// `free` entry releases it."); - w.line(format!("final class {box_name} {{")); + w.line(format!("final class {box_name}: Sendable {{")); w.scope(|w| { w.line(format!("let impl: any {proto}")); w.line(format!("init(_ impl: any {proto}) {{ self.impl = impl }}")); @@ -192,8 +161,7 @@ fn render_trampoline( m: &CallbackMethodBinding, args: &[RetPass], box_name: &str, - module: &str, - ctx: SwiftCtx, + ctx: &SwiftCtx, ) { let camel = camel_method(m); let formals = m @@ -209,33 +177,33 @@ fn render_trampoline( w.line(format!( "let wvBox = Unmanaged<{box_name}>.fromOpaque(ctx!).takeUnretainedValue()" )); - let mut counter = 0usize; - let mut call_args = Vec::new(); - for (p, rp) in camel.params.iter().zip(args) { - let expr = receive_arg(w, p, rp, ctx, &mut counter); - call_args.push(format!("{}: {expr}", p.name)); - } - let call = format!( - "try wvBox.impl.{}({})", - swift_ident(&m.name), - call_args.join(", ") - ); - let default = c_default(m, ctx.c_prefix, module); w.line("do {"); w.indent(); - match (&m.ret, &default) { - (Some(Ty::Enum(name)), Some(_)) => { + let call_args = camel + .params + .iter() + .zip(args) + .map(|(p, rp)| format!("{}: {}", p.name, receive_arg(w, p, rp, ctx))) + .collect::>() + .join(", "); + let call = format!("try wvBox.impl.{}({call_args})", swift_ident(&m.name)); + let default = match &m.ret { + None => None, + Some(Ty::Enum(name)) => { + let c_type = ctx.c_enum_type(name); + w.line(format!("let value = {call}")); w.line(format!( - "return {}({call}.rawValue)", - c_enum_type(name, ctx.c_prefix, module) + "return {c_type}(rawValue: numericCast(value.rawValue))" )); + Some(format!("{c_type}(rawValue: 0)")) } - (Some(_), Some(_)) => { + Some(t) => { w.line(format!("return {call}")); + Some(if matches!(t, Ty::Bool) { "false" } else { "0" }.to_string()) } - _ => { - w.line(call); - } + }; + if default.is_none() { + w.line(call); } w.dedent(); w.line("} catch {"); @@ -250,33 +218,33 @@ fn render_trampoline( w.line("},"); } -/// Render the process-wide vtable namespace: `value` is the C struct filled -/// with capture-free trampolines (implicitly `@convention(c)`), and -/// `pointer` is a heap cell initialized once with that value so the -/// producer can hold its address for the process lifetime. +/// Render the process-wide vtable namespace: `shared` holds the C struct +/// filled with capture-free trampolines (implicitly `@convention(c)`) at a +/// stable address the library can hold for the process lifetime. fn render_vtable( w: &mut CodeWriter, - module: &ModuleBinding, cb: &CallbackInterfaceBinding, protocol: &CallbackProtocol<'_>, - ctx: SwiftCtx, + ctx: &SwiftCtx, ) { let proto = ctx.ty_name(local_type_name(&cb.name)); let box_name = callback_box_name(&cb.name); let vtable_name = callback_vtable_name(&cb.name); let vtable_tag = &cb.vtable_tag; w.line(format!( - "/// The process-wide `{vtable_tag}` the producer calls `{proto}`" + "/// The process-wide `{vtable_tag}` the library calls `{proto}`" )); w.line("/// implementations through. Every entry recovers the box from `ctx`, copies"); w.line("/// or adopts its arguments, and reports a thrown error through `out_err`"); w.line("/// instead of unwinding."); w.line(format!("enum {vtable_name} {{")); w.indent(); - w.line(format!("static let value = {vtable_tag}(")); + w.line(format!( + "static let shared = WvVtable<{vtable_tag}>({vtable_tag}(" + )); w.indent(); for (m, args) in cb.methods.iter().zip(&protocol.method_args) { - render_trampoline(w, m, args, &box_name, &module.path, ctx); + render_trampoline(w, m, args, &box_name, ctx); } w.line("free: { ctx in"); w.scope(|w| { @@ -284,18 +252,7 @@ fn render_vtable( }); w.line("}"); w.dedent(); - w.line(")"); - w.line(format!( - "static let pointer: UnsafePointer<{vtable_tag}> = {{" - )); - w.scope(|w| { - w.line(format!( - "let cell = UnsafeMutablePointer<{vtable_tag}>.allocate(capacity: 1)" - )); - w.line("cell.initialize(to: value)"); - w.line("return UnsafePointer(cell)"); - }); - w.line("}()"); + w.line("))"); w.dedent(); w.line("}"); w.blank(); @@ -303,17 +260,17 @@ fn render_vtable( /// Render everything one callback interface contributes at file scope: the /// public protocol, the retaining box, and the vtable namespace whose -/// `pointer` every call site passing this interface hands to the producer. +/// `shared.pointer` every call site passing this interface hands to the +/// library. pub(crate) fn render_swift_callback_interface( out: &mut String, - module: &ModuleBinding, cb: &CallbackInterfaceBinding, - ctx: SwiftCtx, + ctx: &SwiftCtx, ) { - let protocol = cb.protocol(&module.path, ctx.c_prefix); + let protocol = cb.protocol(ctx.c_prefix); let mut w = CodeWriter::four_space(); render_protocol(&mut w, cb, ctx); render_box(&mut w, cb, ctx); - render_vtable(&mut w, module, cb, &protocol, ctx); + render_vtable(&mut w, cb, &protocol, ctx); out.push_str(&w.finish()); } diff --git a/crates/weaveffi-gen/src/targets/swift/calls.rs b/crates/weaveffi-gen/src/targets/swift/calls.rs new file mode 100644 index 00000000..68ec473b --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/calls.rs @@ -0,0 +1,632 @@ +//! Callable rendering: sync and async wrapper functions and the lazy +//! sequence classes backing `iter` returns. +//! +//! Parameter marshalling dispatches on the shared [`ArgPass`] plan and +//! return handling on [`RetPass`], so how a value crosses the ABI is decided +//! centrally; this module only renders the Swift spelling. + +use std::fmt::Write; + +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::model::{CallShape, FnBinding, IteratorBinding, ParamBinding, Ty}; +use weaveffi_model::plan::{ret_pass, ArgPass, ErrorStrategy, RetPass}; + +use crate::targets::swift::codec::{decode_closure, write_value_stmts}; +use crate::targets::swift::docs::emit_fn_doc; +use crate::targets::swift::types::{ + callback_box_name, callback_vtable_name, deprecated_attr, iterator_class_name, + scalar_swift_type, swift_ident, SwiftCtx, +}; + +/// How a wrapper reports a filled error slot. +/// +/// A callable declaring `throws` maps codes through its domain's typed +/// mapper (`checkKv`/`mapKv`) and throws; a cancellable async callable also +/// throws (`CancellationError` for code `-5`). Any other callable has a plain +/// signature and stops the process with `fatalError`, naming the code and +/// message, since a reported error can only be a runtime trap. +#[derive(Clone, Copy)] +pub(crate) struct ErrCtx<'a> { + /// `true` when the Swift signature `throws`. + throws: bool, + /// `true` when the IDL declares `throws` (domain codes are typed). + typed: bool, + /// `true` for a cancellable async callable. + cancellable: bool, + /// PascalCase stem of the domain in effect (`Kv` names `checkKv` and + /// `mapKv`); `None` falls back to the runtime helpers. + domain: Option<&'a str>, +} + +impl<'a> ErrCtx<'a> { + /// Build the error context for `f` from its [`ErrorStrategy`] and + /// cancellability. + pub(crate) fn for_fn(f: &FnBinding, domain: Option<&'a str>) -> Self { + let typed = f.error_strategy() == ErrorStrategy::Throws; + let cancellable = f.is_async && f.cancellable; + Self { + throws: typed || cancellable, + typed, + cancellable, + domain, + } + } + + /// The mapper turning an error slot into a Swift error, when the wrapper + /// throws: the domain's `map{Stem}`, else the runtime's. + fn mapper(&self) -> String { + match (self.typed, self.domain) { + (true, Some(stem)) => format!("map{stem}"), + (true, None) => "wvRuntimeError".to_string(), + (false, _) => "wvCancelledOrTrap".to_string(), + } + } + + /// The statement checking the error slot of a synchronous call. + fn check_stmt(&self) -> String { + match (self.typed, self.domain) { + (true, Some(stem)) => format!("try check{stem}(&err)"), + (true, None) => "try wvCheck(&err)".to_string(), + (false, _) => "wvTrap(&err)".to_string(), + } + } +} + +/// Clone a callable with its parameter names camel-cased and keyword-escaped, +/// so the Swift argument labels, bound locals, and every staged +/// `_ptr`/`_len` variable derived from them agree (and never collide with a +/// reserved word). +pub(crate) fn camel_params(f: &FnBinding) -> FnBinding { + let mut f = f.clone(); + for p in &mut f.params { + p.name = swift_ident(&p.name); + } + f +} + +/// Where a wrapper sits, which decides how it reaches its object and whether +/// it runs the contract check. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Receiver { + /// A free function or static member: runs the contract check first. + Static, + /// An instance method passing its own object as the leading argument. + Instance, + /// The `new` constructor, rendered as `public init`. + Init, +} + +/// Render one wrapper for `f` (sync, async, or iterator-returning) named +/// `swift_name`, at the indentation depth `depth`. +pub(crate) fn render_callable( + out: &mut String, + f: &FnBinding, + swift_name: &str, + receiver: Receiver, + err: ErrCtx, + ctx: &SwiftCtx, + depth: usize, +) { + let mut w = CodeWriter::four_space().with_depth(depth); + { + let mut tmp = String::new(); + emit_fn_doc(&mut tmp, &f.doc, &f.params, &w.indent_str()); + w.raw(tmp); + } + if let CallShape::Iterator(_) = &f.shape { + w.line("/// - Returns: A lazy sequence that pulls one element per step from the"); + w.line("/// library; the underlying iterator is destroyed when the sequence is"); + w.line("/// exhausted or deinitialized."); + } + if err.cancellable { + w.line("/// - Throws: `CancellationError` when the calling task is cancelled, which"); + w.line("/// also cancels the native call."); + } + if let Some(msg) = &f.deprecated { + w.line(deprecated_attr(msg)); + } + let mut sig = String::new(); + for (i, p) in f.params.iter().enumerate() { + if i > 0 { + sig.push_str(", "); + } + let _ = write!(sig, "{}: {}", p.name, ctx.swift_type(&p.ty)); + } + let is_async = matches!(f.shape, CallShape::Async(_)); + let effects = match (is_async, err.throws) { + (true, true) => " async throws", + (true, false) => " async", + (false, true) => " throws", + (false, false) => "", + }; + if receiver == Receiver::Init { + w.line(format!("public init({sig}){effects} {{")); + } else { + let ret = match (&f.shape, &f.ret) { + (CallShape::Iterator(it), _) => format!( + " -> {}", + ctx.ty_name(&iterator_class_name(it, ctx.c_prefix)) + ), + (_, Some(t)) => format!(" -> {}", ctx.swift_type(t)), + (_, None) => String::new(), + }; + let static_kw = if receiver == Receiver::Instance { + "" + } else { + "static " + }; + w.line(format!( + "public {static_kw}func {swift_name}({sig}){effects}{ret} {{" + )); + } + w.indent(); + if receiver != Receiver::Instance { + w.line("wvLoad()"); + } + if is_async { + render_async_body(&mut w, f, receiver, err, ctx); + } else { + render_sync_body(&mut w, f, receiver, err, ctx); + } + w.dedent(); + w.line("}"); + out.push_str(&w.finish()); +} + +/// The C argument list for `params`, prefixed with the receiver's object for +/// an instance method. +fn c_call_args(params: &[ParamBinding], self_arg: Option<&str>, ctx: &SwiftCtx) -> Vec { + let mut args: Vec = self_arg.into_iter().map(str::to_string).collect(); + for p in params { + let n = &p.name; + match p.arg_pass() { + ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + args.push(format!("{n}_ptr")); + args.push(format!("{n}_len")); + } + // The wrapper borrows its own reference for the call; Swift + // keeps every parameter alive until the wrapper returns. + ArgPass::Object { nullable, .. } => { + args.push(if nullable { + format!("{n}?.ptr") + } else { + format!("{n}.ptr") + }); + } + ArgPass::Callback { .. } => { + let cb = + p.ty.callback_interface_name() + .expect("callback family names a callback interface"); + args.push(format!("{n}_ctx")); + args.push(format!("{}.shared.pointer", callback_vtable_name(cb))); + } + ArgPass::Direct { .. } => match &p.ty { + Ty::Enum(name) => args.push(format!( + "{}(rawValue: numericCast({n}.rawValue))", + ctx.c_enum_type(name) + )), + _ => args.push(n.clone()), + }, + } + } + args +} + +/// Emit the pre-call staging: buffered params are packed into a writer, and +/// callback interfaces are boxed with a +1 retain the library's `free` entry +/// releases. +fn render_arg_staging(w: &mut CodeWriter, params: &[ParamBinding]) { + let mut counter = 0usize; + for p in params { + match p.arg_pass() { + ArgPass::Buffer { .. } => { + let writer = format!("{}Writer", p.name); + w.line(format!("var {writer} = WvWriter()")); + write_value_stmts(w, &p.ty, &p.name, &writer, &mut counter); + } + ArgPass::Callback { .. } => { + let cb = + p.ty.callback_interface_name() + .expect("callback family names a callback interface"); + w.line(format!( + "let {n}_ctx = Unmanaged.passRetained({}({n})).toOpaque()", + callback_box_name(cb), + n = p.name + )); + } + _ => {} + } + } +} + +/// Open the pointer-lending closures every string, bytes, and buffered +/// parameter needs, binding `{name}_ptr`/`{name}_len`. `bind` prefixes the +/// first opening line (`let rv: T = ` or nothing); nested ones return their +/// inner value implicitly. Returns how many closures were opened. +fn open_lending_closures(w: &mut CodeWriter, params: &[ParamBinding], bind: &str) -> usize { + let mut opened = 0; + for p in params { + let n = &p.name; + let open = match p.arg_pass() { + ArgPass::String { .. } => format!("wvWithUTF8({n})"), + ArgPass::Bytes { .. } => format!("wvWithBytes({n})"), + ArgPass::Buffer { .. } => format!("wvWithBuffer({n}Writer)"), + _ => continue, + }; + let prefix = if opened == 0 { bind } else { "" }; + w.line(format!("{prefix}{open} {{ {n}_ptr, {n}_len in")); + w.indent(); + opened += 1; + } + opened +} + +/// Close `n` closures opened by [`open_lending_closures`]. +fn close_closures(w: &mut CodeWriter, n: usize) { + for _ in 0..n { + w.dedent(); + w.line("}"); + } +} + +/// The Swift type of the raw C result of a synchronous call, used to annotate +/// its binding when the call sits inside lending closures. +fn raw_return_swift(f: &FnBinding, rp: Option<&RetPass>, ctx: &SwiftCtx) -> String { + match rp { + None | Some(RetPass::Object { .. }) => "OpaquePointer?".to_string(), + Some(RetPass::Void) => "Void".to_string(), + Some(RetPass::String | RetPass::Bytes | RetPass::Buffer) => { + "UnsafePointer?".to_string() + } + Some(RetPass::Direct) => match f.ret.as_ref() { + Some(Ty::Enum(name)) => ctx.c_enum_type(name).to_string(), + Some(other) => scalar_swift_type(other).to_string(), + None => unreachable!("a direct return carries a type"), + }, + } +} + +/// The interface name behind an object-returning callable (direct or +/// nullable). +fn ret_interface_name(ty: Option<&Ty>) -> &str { + ty.and_then(Ty::interface_name) + .expect("an object return names an interface") +} + +/// Render the body of a synchronous (or iterator-returning) callable: the +/// error slot, input staging, the C call wrapped in whatever lending closures +/// the inputs need, the error check, and the return conversion. +fn render_sync_body( + w: &mut CodeWriter, + f: &FnBinding, + receiver: Receiver, + err: ErrCtx, + ctx: &SwiftCtx, +) { + w.line("var err = WvError()"); + render_arg_staging(w, &f.params); + + // An iterator launch returns the handle; its plan is the iterator + // protocol rather than a `RetPass`. + let (symbol, rp) = match &f.shape { + CallShape::Iterator(it) => (&it.launch.symbol, None), + CallShape::Sync(abi) => (&abi.symbol, Some(ret_pass(f.ret.as_ref(), ctx.c_prefix))), + CallShape::Async(_) => unreachable!("async callables render through render_async_body"), + }; + let needs_out_len = matches!(rp, Some(RetPass::String | RetPass::Bytes | RetPass::Buffer)); + if needs_out_len { + w.line("var outLen = 0"); + } + + let self_arg = (receiver == Receiver::Instance).then_some("ptr"); + let mut args = c_call_args(&f.params, self_arg, ctx); + if needs_out_len { + args.push("&outLen".to_string()); + } + args.push("&err".to_string()); + let call = format!("{symbol}({})", args.join(", ")); + + let has_ret = !matches!(rp, Some(RetPass::Void)); + let bind = if has_ret { + format!("let rv: {} = ", raw_return_swift(f, rp.as_ref(), ctx)) + } else { + String::new() + }; + let opened = open_lending_closures(w, &f.params, &bind); + if opened == 0 { + if has_ret { + w.line(format!("let rv = {call}")); + } else { + w.line(call); + } + } else { + w.line(call); + close_closures(w, opened); + } + w.line(err.check_stmt()); + + match rp { + None => { + let CallShape::Iterator(it) = &f.shape else { + unreachable!("only an iterator launch has no return plan") + }; + let class_name = ctx.ty_name(&iterator_class_name(it, ctx.c_prefix)); + w.line(format!("return {class_name}(handle: wvNonNull(rv))")); + } + Some(RetPass::Void) => {} + // The constructor adopts the returned reference as its own. + Some(_) if receiver == Receiver::Init => { + w.line("self.ptr = wvNonNull(rv)"); + } + Some(rp) => { + let value = receive_value(w, f.ret.as_ref(), &rp, "rv", "outLen", ctx); + w.line(format!("return {value}")); + } + } +} + +/// The Swift expression converting a value the library returned (an owned +/// result) into its Swift form, releasing what the conversion copied. +/// `raw` names the result slot and `len` its length slot, if any. +fn receive_value( + w: &CodeWriter, + ty: Option<&Ty>, + rp: &RetPass, + raw: &str, + len: &str, + ctx: &SwiftCtx, +) -> String { + match rp { + RetPass::Void => "()".to_string(), + RetPass::Direct => match ty { + Some(Ty::Enum(name)) => format!( + "wvEnumCase({}.self, numericCast({raw}.rawValue))", + ctx.ty_name(local_type_name(name)) + ), + _ => raw.to_string(), + }, + RetPass::String => format!("wvTakeString({raw}, {len})"), + RetPass::Bytes => format!("wvTakeBytes({raw}, {len})"), + RetPass::Buffer => { + let ty = ty.expect("a buffered value carries a type"); + let decode = decode_closure(w, ty, ctx); + if decode.starts_with('{') { + format!("wvTakeBuffer({raw}, {len}) {decode}") + } else { + format!("wvTakeBuffer({raw}, {len}, {decode})") + } + } + RetPass::Object { nullable, .. } => { + let class = ctx.ty_name(local_type_name(ret_interface_name(ty))); + if *nullable { + format!("{raw}.map {{ {class}(ptr: $0) }}") + } else { + format!("{class}(ptr: wvNonNull({raw}))") + } + } + } +} + +/// Render the body of an async callable: a checked continuation resumed +/// exactly once from the completion callback, wrapped in a cancellation +/// handler that cancels the native token for a cancellable callable. +fn render_async_body( + w: &mut CodeWriter, + f: &FnBinding, + receiver: Receiver, + err: ErrCtx, + ctx: &SwiftCtx, +) { + let CallShape::Async(binding) = &f.shape else { + unreachable!("render_async_body renders async callables") + }; + let protocol = binding.protocol(f, ctx.c_prefix); + let ret = f + .ret + .as_ref() + .map_or_else(|| "Void".to_string(), |t| ctx.swift_type(t)); + let err_ty = if err.throws { "Error" } else { "Never" }; + + if protocol.cancellable { + w.line("let token = WvCancelToken()"); + w.line("return try await withTaskCancellationHandler {"); + w.indent(); + } + if err.throws { + w.line(format!( + "{}try await withCheckedThrowingContinuation {{ (continuation: CheckedContinuation<{ret}, Error>) in", + if protocol.cancellable { "" } else { "return " } + )); + } else { + w.line(format!( + "return await withCheckedContinuation {{ (continuation: CheckedContinuation<{ret}, Never>) in" + )); + } + w.indent(); + + // The library copies every input during the launch, so lending them for + // the launch call's duration is enough. + render_arg_staging(w, &f.params); + w.line("let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque()"); + let opened = open_lending_closures(w, &f.params, ""); + + let self_arg = (receiver == Receiver::Instance).then_some("self.ptr"); + let mut args = c_call_args(&f.params, self_arg, ctx); + if protocol.cancellable { + args.push("token.raw".to_string()); + } + let slots = match &protocol.result { + RetPass::Void => "context, err", + RetPass::String | RetPass::Bytes | RetPass::Buffer => "context, err, resultPtr, resultLen", + _ => "context, err, result", + }; + let mut launch = args.join(", "); + if !launch.is_empty() { + launch.push_str(", "); + } + w.line(format!("{}({launch}{{ {slots} in", binding.launch.symbol)); + w.indent(); + w.line(format!( + "let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value" + )); + w.line("if let err = err {"); + w.indent(); + if err.throws { + w.line(format!( + "cont.resume(throwing: wvTakeError(err, {}))", + err.mapper() + )); + w.line("return"); + } else { + w.line("wvTrapBoxed(err)"); + } + w.dedent(); + w.line("}"); + let raw = if slots.ends_with("resultLen") { + "resultPtr" + } else { + "result" + }; + let value = receive_value(w, f.ret.as_ref(), &protocol.result, raw, "resultLen", ctx); + w.line(format!("cont.resume(returning: {value})")); + w.dedent(); + w.line("}, context)"); + close_closures(w, opened); + w.dedent(); + w.line("}"); + if protocol.cancellable { + w.dedent(); + w.line("} onCancel: {"); + w.scope(|w| { + w.line("token.cancel()"); + }); + w.line("}"); + } +} + +/// Emit the lazy sequence class backing one `iter` function. +/// +/// The class conforms to `Sequence & IteratorProtocol` and owns the C +/// iterator handle. Each `next()` issues exactly one library `next` call; +/// the handle is destroyed eagerly on exhaustion (or on a mid-stream error) +/// and from `deinit` when iteration is abandoned early. Elements are received +/// exactly like returns of the same type. +/// +/// `next()` can't throw under `IteratorProtocol`, so for a throwing function +/// a mid-stream error ends iteration and is stored in the sequence's public +/// `error` property; for a non-throwing one a reported error stops the +/// process. +pub(crate) fn render_swift_iterator_class( + out: &mut String, + f: &FnBinding, + it: &IteratorBinding, + err: ErrCtx, + ctx: &SwiftCtx, +) { + let protocol = it.protocol(f, ctx.c_prefix); + let class_name = iterator_class_name(it, ctx.c_prefix); + let elem = &it.elem; + let elem_swift = ctx.swift_type(elem); + let has_len_slot = matches!( + protocol.elem, + RetPass::String | RetPass::Bytes | RetPass::Buffer + ); + + // The `out_item` slot declaration. + let item_decl = match &protocol.elem { + RetPass::String | RetPass::Bytes | RetPass::Buffer => { + "var item: UnsafePointer? = nil".to_string() + } + RetPass::Object { .. } => "var item: OpaquePointer? = nil".to_string(), + RetPass::Direct => match elem { + Ty::Enum(name) => format!("var item = {}(rawValue: 0)", ctx.c_enum_type(name)), + Ty::Bool => "var item = false".to_string(), + other => format!("var item: {} = 0", scalar_swift_type(other)), + }, + RetPass::Void => unreachable!("an iterator element is never void"), + }; + + let mut w = CodeWriter::four_space(); + w.line(format!( + "/// A lazy sequence over the `{elem_swift}` elements streamed by `{}`.", + it.launch.symbol + )); + w.line("///"); + w.line("/// Each `next()` call pulls exactly one element from the library. The"); + w.line("/// underlying iterator is destroyed eagerly on exhaustion and from `deinit`"); + w.line("/// when iteration is abandoned early."); + if err.throws { + w.line("///"); + w.line("/// If the library reports an error mid-stream, iteration ends and the"); + w.line("/// error is stored in ``error`` for the caller to inspect after the loop."); + } + w.line(format!( + "public final class {class_name}: Sequence, IteratorProtocol {{" + )); + w.indent(); + w.line("private var handle: OpaquePointer?"); + if err.throws { + w.line("/// The error that ended iteration early, if any."); + w.line("public private(set) var error: Error?"); + } + w.blank(); + w.line("init(handle: OpaquePointer) {"); + w.scope(|w| { + w.line("self.handle = handle"); + }); + w.line("}"); + w.blank(); + w.line("deinit {"); + w.scope(|w| { + w.line("destroyHandle()"); + }); + w.line("}"); + w.blank(); + w.line("private func destroyHandle() {"); + w.scope(|w| { + w.line("guard let handle = handle else { return }"); + w.line(format!("{}(handle)", it.destroy_symbol)); + w.line("self.handle = nil"); + }); + w.line("}"); + w.blank(); + w.line("/// Pulls the next element from the library, or returns `nil` once the"); + w.line("/// stream is exhausted (destroying the underlying iterator)."); + w.line(format!("public func next() -> {elem_swift}? {{")); + w.indent(); + w.line("guard let handle = handle else { return nil }"); + w.line(item_decl); + if has_len_slot { + w.line("var itemLen = 0"); + } + w.line("var err = WvError()"); + let len_arg = if has_len_slot { ", &itemLen" } else { "" }; + w.line(format!( + "if {}(handle, &item{len_arg}, &err) == 0 {{", + it.next.symbol + )); + w.indent(); + if err.throws { + w.line("if err.code != 0 {"); + w.scope(|w| { + w.line(format!("error = {}(err)", err.mapper())); + w.line(format!("{}_error_clear(&err)", ctx.c_prefix)); + }); + w.line("}"); + } else { + w.line("wvTrap(&err)"); + } + w.line("destroyHandle()"); + w.line("return nil"); + w.dedent(); + w.line("}"); + let value = receive_value(&w, Some(elem), &protocol.elem, "item", "itemLen", ctx); + w.line(format!("return {value}")); + w.dedent(); + w.line("}"); + w.dedent(); + w.line("}"); + w.blank(); + out.push_str(&w.finish()); +} diff --git a/crates/weaveffi-gen/src/targets/swift/codec.rs b/crates/weaveffi-gen/src/targets/swift/codec.rs new file mode 100644 index 00000000..d7c18129 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/codec.rs @@ -0,0 +1,190 @@ +//! Value-buffer codec emitters: the Swift statements serializing and +//! decoding one value in the wire format. +//! +//! Both emitters dispatch on the shared [`Ty::wire`] classification, so the +//! non-obvious folds (interfaces as `u64` object tokens carrying one strong +//! reference, records and rich enums through one user codec) are decided +//! centrally rather than re-derived from `Ty` here. Primitives map onto the +//! runtime's `write{Prim}`/`read{Prim}` pairs by name. + +use crate::codegen::CodeWriter; +use crate::utils::local_type_name; +use weaveffi_model::model::{Ty, WireType}; + +use crate::targets::swift::types::SwiftCtx; + +/// A fresh generated-variable name (`v0`, `n1`, ...) unique within one +/// rendering scope. +pub(crate) fn fresh(counter: &mut usize, prefix: &str) -> String { + let id = *counter; + *counter += 1; + format!("{prefix}{id}") +} + +/// Emit statements serializing `expr` (of type `ty`) into the `WvWriter` +/// variable named `writer`, recursing through optionals, lists, and maps and +/// delegating records and rich enums to their generated `wvWrite*` codecs. +pub(crate) fn write_value_stmts( + w: &mut CodeWriter, + ty: &Ty, + expr: &str, + writer: &str, + counter: &mut usize, +) { + match ty.wire() { + WireType::Prim(p) => { + w.line(format!("{writer}.write{}({expr})", p.pascal())); + } + // An object token carries one strong reference, so the wrapper + // writes a freshly cloned pointer and keeps its own. + WireType::Object(_) => { + w.line(format!("{writer}.writeObject({expr}.clonePtr())")); + } + // C-style enums cross as their `i32` discriminant. + WireType::Enum(_) => { + w.line(format!("{writer}.writeI32({expr}.rawValue)")); + } + WireType::User(name) => { + w.line(format!( + "wvWrite{}({expr}, into: &{writer})", + local_type_name(name) + )); + } + WireType::Optional(inner) => { + let v = fresh(counter, "v"); + w.line(format!("if let {v} = {expr} {{")); + w.indent(); + w.line(format!("{writer}.writeOptionFlag(true)")); + write_value_stmts(w, inner, &v, writer, counter); + w.dedent(); + w.line("} else {"); + w.indent(); + w.line(format!("{writer}.writeOptionFlag(false)")); + w.dedent(); + w.line("}"); + } + WireType::List(inner) => { + let v = fresh(counter, "v"); + w.line(format!("{writer}.writeLen({expr}.count)")); + w.line(format!("for {v} in {expr} {{")); + w.indent(); + write_value_stmts(w, inner, &v, writer, counter); + w.dedent(); + w.line("}"); + } + WireType::Map(k, val) => { + let kv = fresh(counter, "v"); + let vv = fresh(counter, "v"); + w.line(format!("{writer}.writeLen({expr}.count)")); + w.line(format!("for ({kv}, {vv}) in {expr} {{")); + w.indent(); + write_value_stmts(w, k, &kv, writer, counter); + write_value_stmts(w, val, &vv, writer, counter); + w.dedent(); + w.line("}"); + } + } +} + +/// Emit statements deserializing one value of type `ty` from the `WvReader` +/// variable named `reader`, binding the result to `out`. +pub(crate) fn read_value_stmts( + w: &mut CodeWriter, + ty: &Ty, + out: &str, + reader: &str, + ctx: &SwiftCtx, + counter: &mut usize, +) { + match ty.wire() { + WireType::Prim(p) => { + w.line(format!("let {out} = {reader}.read{}()", p.pascal())); + } + // The token's reference is adopted by a new wrapper, whose deinit + // owes the `_destroy`. + WireType::Object(name) => { + w.line(format!( + "let {out} = {}(ptr: {reader}.readObject())", + ctx.ty_name(local_type_name(name)) + )); + } + WireType::Enum(name) => { + w.line(format!( + "let {out} = wvEnumCase({}.self, {reader}.readI32())", + ctx.ty_name(local_type_name(name)) + )); + } + WireType::User(name) => { + w.line(format!( + "let {out} = wvRead{}(&{reader})", + local_type_name(name) + )); + } + WireType::Optional(inner) => { + let t = ctx.swift_type(inner); + w.line(format!("var {out}: {t}? = nil")); + w.line(format!("if {reader}.readOptionFlag() {{")); + w.indent(); + let v = fresh(counter, "v"); + read_value_stmts(w, inner, &v, reader, ctx, counter); + w.line(format!("{out} = {v}")); + w.dedent(); + w.line("}"); + } + WireType::List(inner) => { + let t = ctx.swift_type(inner); + let cnt = fresh(counter, "n"); + w.line(format!("let {cnt} = {reader}.readCount()")); + w.line(format!("var {out}: [{t}] = []")); + w.line(format!( + "{out}.reserveCapacity(min({cnt}, {reader}.remaining))" + )); + w.line(format!("for _ in 0..<{cnt} {{")); + w.indent(); + let v = fresh(counter, "v"); + read_value_stmts(w, inner, &v, reader, ctx, counter); + w.line(format!("{out}.append({v})")); + w.dedent(); + w.line("}"); + } + WireType::Map(k, val) => { + let kt = ctx.swift_type(k); + let vt = ctx.swift_type(val); + let cnt = fresh(counter, "n"); + w.line(format!("let {cnt} = {reader}.readCount()")); + w.line(format!("var {out}: [{kt}: {vt}] = [:]")); + w.line(format!( + "{out}.reserveCapacity(min({cnt}, {reader}.remaining))" + )); + w.line(format!("for _ in 0..<{cnt} {{")); + w.indent(); + let kv = fresh(counter, "v"); + let vv = fresh(counter, "v"); + read_value_stmts(w, k, &kv, reader, ctx, counter); + read_value_stmts(w, val, &vv, reader, ctx, counter); + w.line(format!("{out}[{kv}] = {vv}")); + w.dedent(); + w.line("}"); + } + } +} + +/// A closure literal decoding one value of type `ty` from a reader named `r`, +/// for the runtime's `wvTakeBuffer`/`wvBorrowBuffer`, rendered at `w`'s +/// depth. A record or rich enum decodes through its codec directly. +pub(crate) fn decode_closure(w: &CodeWriter, ty: &Ty, ctx: &SwiftCtx) -> String { + if let WireType::User(name) = ty.wire() { + return format!("wvRead{}", local_type_name(name)); + } + let mut inner = CodeWriter::four_space().with_depth(w.depth() + 1); + let mut counter = 0usize; + let v = fresh(&mut counter, "v"); + read_value_stmts(&mut inner, ty, &v, "r", ctx, &mut counter); + inner.line(format!("return {v}")); + format!( + "{{ (r: inout WvReader) -> {} in\n{}{}}}", + ctx.swift_type(ty), + inner.finish(), + w.indent_str() + ) +} diff --git a/crates/weaveffi-gen-swift/src/docs.rs b/crates/weaveffi-gen/src/targets/swift/docs.rs similarity index 93% rename from crates/weaveffi-gen-swift/src/docs.rs rename to crates/weaveffi-gen/src/targets/swift/docs.rs index 821cd484..7adc7ceb 100644 --- a/crates/weaveffi-gen-swift/src/docs.rs +++ b/crates/weaveffi-gen/src/targets/swift/docs.rs @@ -1,8 +1,8 @@ //! Swift doc-comment emission: item docs and per-parameter `- Parameter` //! lines. -use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; -use weaveffi_core::model::ParamBinding; +use crate::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; +use weaveffi_model::model::ParamBinding; /// Emits a `///`-prefixed Swift doc comment at `indent`. Each line of the /// (possibly multi-line) doc gets its own `///` prefix. diff --git a/crates/weaveffi-gen/src/targets/swift/entities.rs b/crates/weaveffi-gen/src/targets/swift/entities.rs new file mode 100644 index 00000000..bbc62cf9 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/entities.rs @@ -0,0 +1,551 @@ +//! Entity rendering: enums, rich enums, records, interfaces, typed error +//! domains, and the recursive module walk emitting them (callback interfaces +//! render through [`crate::targets::swift::callbacks`]). + +use crate::codegen::common::DocCommentStyle; +use crate::codegen::CodeWriter; +use crate::utils::wrapper_name; +use heck::ToUpperCamelCase; +use weaveffi_model::model::{ + BindingModel, CallShape, EnumBinding, ErrorBinding, FieldBinding, InterfaceBinding, + ModuleBinding, StructBinding, +}; + +use crate::targets::swift::callbacks::render_swift_callback_interface; +use crate::targets::swift::calls::{ + camel_params, render_callable, render_swift_iterator_class, ErrCtx, Receiver, +}; +use crate::targets::swift::codec::{fresh, read_value_stmts, write_value_stmts}; +use crate::targets::swift::types::{deprecated_attr, swift_ident, swift_str, SwiftCtx}; + +/// The PascalCase helper stem of the domain in effect for `module`, naming the +/// per-domain `check{Stem}`/`map{Stem}` helpers (derived from the *declaring* +/// module's path, so inheriting submodules reference the ancestor's helper). +fn domain_stem(module: &ModuleBinding) -> Option { + module + .error + .as_ref() + .map(|e| e.owner_path.to_upper_camel_case()) +} + +/// The conformance list of a value type: `Hashable` (which implies +/// `Equatable`) when every field is, and always `Sendable`. +fn value_conformances<'t>( + ctx: &SwiftCtx, + fields: impl IntoIterator, +) -> &'static str { + if fields.into_iter().all(|f| ctx.is_hashable(&f.ty)) { + "Hashable, Sendable" + } else { + "Sendable" + } +} + +/// Render a C-style enum as an `Int32` raw-value Swift enum, one case per +/// variant. Wrappers convert to and from the imported C type at the call. +fn render_swift_enum(out: &mut String, e: &EnumBinding) { + let mut w = CodeWriter::four_space(); + w.doc(&e.doc, DocCommentStyle::TripleSlash); + if let Some(msg) = &e.deprecated { + w.line(deprecated_attr(msg)); + } + w.line(format!("public enum {}: Int32, Sendable {{", e.name)); + w.scope(|w| { + for v in &e.variants { + w.doc(&v.doc, DocCommentStyle::TripleSlash); + w.line(format!("case {} = {}", swift_ident(&v.name), v.value)); + } + }); + w.line("}"); + w.blank(); + out.push_str(&w.finish()); +} + +/// Render a rich (algebraic) enum as a native Swift enum with associated +/// values: one case per variant, with labeled associated values matching the +/// variant's field names, plus its `wvWrite`/`wvRead` codec pair. The writer +/// switches on the case, writes the `i32` tag, then the active variant's +/// fields in order; the reader inverts it and stops on an unknown tag. +fn render_swift_rich_enum(out: &mut String, e: &EnumBinding, ctx: &SwiftCtx) { + let name = &e.name; + let ty_name = ctx.ty_name(name); + let fields = e.variants.iter().flat_map(|v| v.fields.iter()); + let mut w = CodeWriter::four_space(); + w.doc(&e.doc, DocCommentStyle::TripleSlash); + if let Some(msg) = &e.deprecated { + w.line(deprecated_attr(msg)); + } + w.line(format!( + "public enum {name}: {} {{", + value_conformances(ctx, fields) + )); + w.scope(|w| { + for v in &e.variants { + w.doc(&v.doc, DocCommentStyle::TripleSlash); + let case_name = swift_ident(&v.name); + if v.fields.is_empty() { + w.line(format!("case {case_name}")); + } else { + let assoc = v + .fields + .iter() + .map(|f| format!("{}: {}", swift_ident(&f.name), ctx.swift_type(&f.ty))) + .collect::>() + .join(", "); + w.line(format!("case {case_name}({assoc})")); + } + } + }); + w.line("}"); + w.blank(); + + let mut counter = 0usize; + // The codecs of a deprecated type are deprecated too, so they can name it + // without a warning. + let codec_attr = e.deprecated.as_deref().map(deprecated_attr); + if let Some(attr) = &codec_attr { + w.line(attr); + } + w.line(format!( + "func wvWrite{name}(_ value: {ty_name}, into w: inout WvWriter) {{" + )); + w.indent(); + w.line("switch value {"); + for v in &e.variants { + let case_name = swift_ident(&v.name); + if v.fields.is_empty() { + w.line(format!("case .{case_name}:")); + w.scope(|w| { + w.line(format!("w.writeI32({})", v.value)); + }); + } else { + let binds: Vec = v.fields.iter().map(|_| fresh(&mut counter, "v")).collect(); + w.line(format!("case let .{case_name}({}):", binds.join(", "))); + w.indent(); + w.line(format!("w.writeI32({})", v.value)); + for (f, bind) in v.fields.iter().zip(&binds) { + write_value_stmts(&mut w, &f.ty, bind, "w", &mut counter); + } + w.dedent(); + } + } + w.line("}"); + w.dedent(); + w.line("}"); + w.blank(); + + if let Some(attr) = &codec_attr { + w.line(attr); + } + w.line(format!( + "func wvRead{name}(_ r: inout WvReader) -> {ty_name} {{" + )); + w.indent(); + w.line("let tag = r.readI32()"); + w.line("switch tag {"); + for v in &e.variants { + let case_name = swift_ident(&v.name); + w.line(format!("case {}:", v.value)); + w.indent(); + if v.fields.is_empty() { + w.line(format!("return .{case_name}")); + } else { + let mut labeled = Vec::new(); + for f in &v.fields { + let var = fresh(&mut counter, "v"); + read_value_stmts(&mut w, &f.ty, &var, "r", ctx, &mut counter); + labeled.push(format!("{}: {var}", swift_ident(&f.name))); + } + w.line(format!("return .{case_name}({})", labeled.join(", "))); + } + w.dedent(); + } + w.line("default:"); + w.scope(|w| { + w.line(format!("wvDecodeFailure(\"unknown {name} tag \\(tag)\")")); + }); + w.line("}"); + w.dedent(); + w.line("}"); + w.blank(); + out.push_str(&w.finish()); +} + +/// Render a record as a plain Swift struct with one `public var` per field +/// and an explicit public memberwise initializer (the synthesized one is +/// internal), plus its `wvWrite`/`wvRead` codec pair (fields in declaration +/// order). +fn render_swift_struct(out: &mut String, s: &StructBinding, ctx: &SwiftCtx) { + let name = &s.name; + let ty_name = ctx.ty_name(name); + let mut w = CodeWriter::four_space(); + w.doc(&s.doc, DocCommentStyle::TripleSlash); + if let Some(msg) = &s.deprecated { + w.line(deprecated_attr(msg)); + } + w.line(format!( + "public struct {name}: {} {{", + value_conformances(ctx, &s.fields) + )); + w.indent(); + for f in &s.fields { + w.doc(&f.doc, DocCommentStyle::TripleSlash); + w.line(format!( + "public var {}: {}", + swift_ident(&f.name), + ctx.swift_type(&f.ty) + )); + } + if !s.fields.is_empty() { + w.blank(); + } + let params = s + .fields + .iter() + .map(|f| format!("{}: {}", swift_ident(&f.name), ctx.swift_type(&f.ty))) + .collect::>() + .join(", "); + w.line("/// Creates a value from its fields."); + w.line(format!("public init({params}) {{")); + w.scope(|w| { + for f in &s.fields { + let prop = swift_ident(&f.name); + w.line(format!("self.{prop} = {prop}")); + } + }); + w.line("}"); + w.dedent(); + w.line("}"); + w.blank(); + + let mut counter = 0usize; + let value = if s.fields.is_empty() { "_" } else { "value" }; + let writer = if s.fields.is_empty() { "_" } else { "w" }; + let codec_attr = s.deprecated.as_deref().map(deprecated_attr); + if let Some(attr) = &codec_attr { + w.line(attr); + } + w.line(format!( + "func wvWrite{name}(_ {value}: {ty_name}, into {writer}: inout WvWriter) {{" + )); + w.indent(); + for f in &s.fields { + let expr = format!("value.{}", swift_ident(&f.name)); + write_value_stmts(&mut w, &f.ty, &expr, "w", &mut counter); + } + w.dedent(); + w.line("}"); + w.blank(); + + let reader = if s.fields.is_empty() { "_" } else { "r" }; + if let Some(attr) = &codec_attr { + w.line(attr); + } + w.line(format!( + "func wvRead{name}(_ {reader}: inout WvReader) -> {ty_name} {{" + )); + w.indent(); + let mut labeled = Vec::new(); + for f in &s.fields { + let var = fresh(&mut counter, "v"); + read_value_stmts(&mut w, &f.ty, &var, "r", ctx, &mut counter); + labeled.push(format!("{}: {var}", swift_ident(&f.name))); + } + w.line(format!("return {ty_name}({})", labeled.join(", "))); + w.dedent(); + w.line("}"); + w.blank(); + out.push_str(&w.finish()); +} + +/// Render one declaring module's typed error surface: a `public enum +/// {TypeName}: Error` whose lowerCamel cases carry the runtime message plus, +/// for codes that declare payload fields, one labeled associated value per +/// field. Also emits the file-scope `map{Stem}` and `check{Stem}` helpers +/// that convert a filled error slot into it, decoding the payload buffer for +/// codes with fields. +/// +/// Only declared codes get typed cases. Domain codes are validated positive, +/// so the mapper's `default` arm is what every reserved negative runtime +/// code falls through to: the runtime error, or `CancellationError` for `-5`. +fn render_swift_error(out: &mut String, module: &ModuleBinding, eb: &ErrorBinding, ctx: &SwiftCtx) { + let stem = eb.owner_path.to_upper_camel_case(); + let ty = &eb.type_name; + + let case_decl = |fields: &[FieldBinding]| -> String { + let mut parts = vec!["message: String".to_string()]; + for f in fields { + parts.push(format!( + "{}: {}", + swift_ident(&f.name), + ctx.swift_type(&f.ty) + )); + } + parts.join(", ") + }; + + let mut w = CodeWriter::four_space(); + w.doc( + &Some(format!( + "Typed errors reported by the `{}` module.", + module.segments.join(".") + )), + DocCommentStyle::TripleSlash, + ); + w.line(format!( + "public enum {ty}: Error, LocalizedError, Sendable {{" + )); + w.indent(); + for c in &eb.codes { + w.doc(&c.doc, DocCommentStyle::TripleSlash); + w.line(format!( + "case {}({})", + swift_ident(&c.name), + case_decl(&c.fields) + )); + } + w.blank(); + w.line("/// The message the library reported, or the code's documented default."); + w.line("public var errorDescription: String? {"); + w.scope(|w| { + w.line("switch self {"); + for c in &eb.codes { + // Bind only the message; wildcard the payload fields. + let mut binds = vec!["message".to_string()]; + binds.extend(c.fields.iter().map(|_| "_".to_string())); + w.line(format!( + "case let .{}({}): return message", + swift_ident(&c.name), + binds.join(", ") + )); + } + w.line("}"); + }); + w.line("}"); + w.blank(); + w.line("/// The numeric ABI code carried by this error."); + w.line("public var errorCode: Int32 {"); + w.scope(|w| { + w.line("switch self {"); + for c in &eb.codes { + w.line(format!( + "case .{}: return {}", + swift_ident(&c.name), + c.value + )); + } + w.line("}"); + }); + w.line("}"); + w.dedent(); + w.line("}"); + w.blank(); + + // `map{Stem}`: code -> typed case (default message when the slot carried + // none), decoding the payload buffer for codes that declare fields. + w.line(format!("func map{stem}(_ err: WvError) -> Error {{")); + w.indent(); + w.line("let message = wvErrorMessage(err)"); + w.line("switch err.code {"); + for c in &eb.codes { + let case_name = swift_ident(&c.name); + let message_arg = format!( + "message: message.isEmpty ? \"{}\" : message", + swift_str(&c.message) + ); + if c.fields.is_empty() { + w.line(format!( + "case {}: return {ty}.{case_name}({message_arg})", + c.value + )); + } else { + w.line(format!("case {}:", c.value)); + w.indent(); + w.line("var r = WvReader(err.payload_ptr, err.payload_len)"); + let mut counter = 0usize; + let mut args = vec![message_arg]; + for f in &c.fields { + let var = fresh(&mut counter, "v"); + read_value_stmts(&mut w, &f.ty, &var, "r", ctx, &mut counter); + args.push(format!("{}: {var}", swift_ident(&f.name))); + } + w.line("r.finish()"); + w.line(format!("return {ty}.{case_name}({})", args.join(", "))); + w.dedent(); + } + } + w.line("default: return wvRuntimeError(err)"); + w.line("}"); + w.dedent(); + w.line("}"); + w.blank(); + + w.line(format!("func check{stem}(_ err: inout WvError) throws {{")); + w.indent(); + w.line("guard err.code != 0 else { return }"); + w.line(format!("let error = map{stem}(err)")); + w.line(format!("{}_error_clear(&err)", ctx.c_prefix)); + w.line("throw error"); + w.dedent(); + w.line("}"); + w.blank(); + out.push_str(&w.finish()); +} + +/// Render one interface as a `public final class` owning one strong +/// reference to its object: a stored `ptr`, an internal reference-adopting +/// `init(ptr:)`, a `deinit` that releases the reference exactly once through +/// the destroy symbol, and an internal `clonePtr()` that mints a second +/// reference for positions that take ownership (an object token inside a +/// value buffer). The native object is thread-safe, so the class is +/// `@unchecked Sendable`. +/// +/// The constructor named `new` surfaces as `public init`; every other +/// constructor becomes a `public static func` factory. Methods are instance +/// funcs passing `ptr` as the leading C argument; statics are plain `public +/// static func`s. +fn render_swift_interface( + out: &mut String, + module: &ModuleBinding, + iface: &InterfaceBinding, + ctx: &SwiftCtx, +) { + let stem = domain_stem(module); + let class_name = &iface.name; + + let mut w = CodeWriter::four_space(); + w.doc(&iface.doc, DocCommentStyle::TripleSlash); + if let Some(msg) = &iface.deprecated { + w.line(deprecated_attr(msg)); + } + w.line(format!( + "public final class {class_name}: @unchecked Sendable {{" + )); + w.indent(); + w.line("let ptr: OpaquePointer"); + w.blank(); + w.line("init(ptr: OpaquePointer) {"); + w.scope(|w| { + w.line("self.ptr = ptr"); + }); + w.line("}"); + w.blank(); + w.line("deinit {"); + w.scope(|w| { + w.line(format!("{}(ptr)", iface.destroy_symbol)); + }); + w.line("}"); + w.blank(); + w.line("/// Returns a new strong reference to the same object, for a position that"); + w.line("/// takes ownership of it (an object token inside a value buffer)."); + w.line("func clonePtr() -> OpaquePointer {"); + w.scope(|w| { + w.line(format!("wvNonNull({}(ptr))", iface.clone_symbol)); + }); + w.line("}"); + w.dedent(); + + let mut members = String::new(); + let mut render = |f: &weaveffi_model::model::FnBinding, receiver: Receiver| { + let f = camel_params(f); + let err = ErrCtx::for_fn(&f, stem.as_deref()); + members.push('\n'); + let receiver = if receiver == Receiver::Static && f.name == "new" { + Receiver::Init + } else { + receiver + }; + render_callable( + &mut members, + &f, + &swift_ident(&f.name), + receiver, + err, + ctx, + 1, + ); + }; + for c in &iface.constructors { + render(c, Receiver::Static); + } + for m in &iface.methods { + render(m, Receiver::Instance); + } + for s in &iface.statics { + render(s, Receiver::Static); + } + w.raw(members); + + w.line("}"); + w.blank(); + out.push_str(&w.finish()); +} + +/// Emit every file-scope type one module contributes: the typed error +/// surface, enums and records with their codecs, callback-interface +/// protocols with their vtables, interface classes, and the sequence classes +/// backing `iter` callables. +pub(crate) fn render_swift_module_types(out: &mut String, mb: &ModuleBinding, ctx: &SwiftCtx) { + if let Some(eb) = mb.error.as_ref().filter(|e| e.declared_here) { + render_swift_error(out, mb, eb, ctx); + } + for e in &mb.enums { + if e.is_rich() { + render_swift_rich_enum(out, e, ctx); + } else { + render_swift_enum(out, e); + } + } + for s in &mb.structs { + render_swift_struct(out, s, ctx); + } + for cb in &mb.callback_interfaces { + render_swift_callback_interface(out, cb, ctx); + } + for i in &mb.interfaces { + render_swift_interface(out, mb, i, ctx); + } + let stem = domain_stem(mb); + for f in mb.callables() { + if let CallShape::Iterator(it) = &f.shape { + render_swift_iterator_class(out, f, it, ErrCtx::for_fn(f, stem.as_deref()), ctx); + } + } +} + +/// Emit one module's namespace `enum` at `depth`: its function wrappers, +/// then one nested namespace per submodule. +pub(crate) fn render_swift_namespace( + out: &mut String, + model: &BindingModel, + mb: &ModuleBinding, + depth: usize, + strip_module_prefix: bool, + ctx: &SwiftCtx, +) { + let indent = " ".repeat(depth); + let stem = domain_stem(mb); + out.push_str(&format!( + "{indent}public enum {} {{\n", + mb.name.to_upper_camel_case() + )); + let mut first = true; + for f in &mb.functions { + if !first { + out.push('\n'); + } + first = false; + let f = camel_params(f); + let swift_name = swift_ident(&wrapper_name(&mb.path, &f.name, strip_module_prefix)); + let err = ErrCtx::for_fn(&f, stem.as_deref()); + render_callable(out, &f, &swift_name, Receiver::Static, err, ctx, depth + 1); + } + for sub in model.children(mb) { + if !first { + out.push('\n'); + } + first = false; + render_swift_namespace(out, model, sub, depth + 1, strip_module_prefix, ctx); + } + out.push_str(&format!("{indent}}}\n")); +} diff --git a/crates/weaveffi-gen/src/targets/swift/mod.rs b/crates/weaveffi-gen/src/targets/swift/mod.rs new file mode 100644 index 00000000..77244c9f --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/mod.rs @@ -0,0 +1,564 @@ +//! Swift binding generator. +//! +//! Emits a standalone SwiftPM package: the Swift wrapper module over the C +//! ABI, plus a `C{Module}` system-library target holding a copy of the C +//! header and a module map that links the native library. Implements +//! [`LanguageBackend`]; the shared driver bridges it into the generator +//! pipeline. +//! +//! Records, rich enums, optionals, lists, and maps cross the C ABI as value +//! buffers. The wrapper ships a small private writer/reader pair +//! (`WvWriter`/`WvReader`, decoding in place from the library's memory) plus +//! one pack and one unpack routine per record and rich enum, so records +//! surface as plain Swift structs and rich enums as native Swift enums with +//! associated values. Objects surface as `final class` wrappers owning one +//! strong reference each, callback interfaces as class-bound `Sendable` +//! protocols whose implementations cross the boundary through a process-wide +//! vtable of `@convention(c)` trampolines, and async functions as `async` +//! functions over checked continuations, with task cancellation wired to the +//! native cancel token. +//! +//! The fixed Swift sources (runtime, manifest, module map, README) live +//! under `runtime/` and are spliced with the package's names. + +mod callbacks; +mod calls; +mod codec; +mod docs; +mod entities; +mod package; +mod runtime; +mod types; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::package::{PackageContext, PackagedFile}; +use crate::targets::c::{header_name, render_c_header_from_model}; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; +use camino::{Utf8Path, Utf8PathBuf}; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::resolved::ResolvedApi; + +use crate::targets::swift::entities::{render_swift_module_types, render_swift_namespace}; +use crate::targets::swift::package::{ + render_modulemap, render_package_swift, render_packaged_readme, +}; +use crate::targets::swift::runtime::{render_runtime, Names}; +use crate::targets::swift::types::SwiftCtx; + +/// Per-target configuration for [`SwiftGenerator`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct SwiftConfig { + /// SwiftPM package, product, and module name. Defaults to the package + /// name in PascalCase (`kvstore` becomes `Kvstore`); the C module is + /// always this name with a `C` prefix. + pub module_name: Option, + /// When `true` (the default), strip the IR module path from emitted + /// function names, so `enum Kv` exposes `openStore` rather than + /// `kvOpenStore`. Set to `false` to restore the module-prefixed spelling. + pub strip_module_prefix: bool, + /// Basename of the IDL the CLI was invoked with. + /// Populated by the CLI; not user-configurable via `[swift]`. + #[serde(skip)] + pub input_basename: Option, +} + +impl Default for SwiftConfig { + fn default() -> Self { + Self { + module_name: None, + strip_module_prefix: true, + input_basename: None, + } + } +} + +impl SwiftConfig { + /// The input IDL basename embedded in generated file headers, falling + /// back to `"api.yml"`. + pub fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } +} + +/// The resolved names of one generated package. +struct Layout { + /// The Swift module (and package and product) name. + module: String, + /// The C module name, `C{module}`. + c_module: String, + /// The native library base name. + library: String, + /// The C header file name. + header: String, +} + +impl Layout { + fn new(api: &ResolvedApi, config: &SwiftConfig) -> Self { + let identity = api.identity(); + let module = config + .module_name + .clone() + .unwrap_or_else(|| identity.pascal_name()); + Self { + c_module: format!("C{module}"), + module, + library: identity.library.clone(), + header: header_name(api), + } + } + + fn names(&self) -> Names<'_> { + Names { + module: &self.module, + c_module: &self.c_module, + library: &self.library, + header: &self.header, + } + } + + /// The error type for codes outside any declared domain. + fn runtime_error(&self) -> String { + format!("{}RuntimeError", self.module) + } + + /// The `(path, contents)` of every source file of the package rooted at + /// `dir`. + fn sources( + &self, + model: &BindingModel, + dir: &Utf8Path, + config: &SwiftConfig, + ) -> Vec<(Utf8PathBuf, String)> { + let input_basename = config.input_basename(); + let c_dir = dir.join("Sources").join(&self.c_module); + let swift_file = format!("{}.swift", self.module); + vec![ + ( + dir.join("Package.swift"), + render_package_swift(&self.names(), input_basename), + ), + ( + c_dir.join("module.modulemap"), + render_modulemap(&self.names(), input_basename), + ), + ( + c_dir.join(&self.header), + render_c_header_from_model(model, input_basename, &self.header), + ), + ( + dir.join("Sources").join(&self.module).join(&swift_file), + render_swift_wrapper(self, model, config, &swift_file), + ), + ] + } +} + +/// Swift backend: emits a standalone SwiftPM package wrapping the C ABI. +pub struct SwiftGenerator; + +impl LanguageBackend for SwiftGenerator { + type Config = SwiftConfig; + + fn name(&self) -> &'static str { + "swift" + } + + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + Layout::new(api, config) + .sources(model, &out_dir.join("swift"), config) + .into_iter() + .map(|(path, contents)| OutputFile::new(path, contents)) + .collect() + } + + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let layout = Layout::new(api, config); + let dir = out_dir.join("swift"); + let mut files: Vec = layout + .sources(model, &dir, config) + .into_iter() + .map(|(path, contents)| PackagedFile::text(path, contents)) + .collect(); + files.push(PackagedFile::text( + dir.join("README.md"), + render_packaged_readme(&layout.names(), ctx, config.input_basename()), + )); + // Desktop slices only: they link through the system-library target + // or fuse into the XCFramework the README describes. + for nb in ctx + .binaries + .binaries + .iter() + .filter(|nb| nb.platform.is_desktop()) + { + let dest = dir + .join("lib") + .join(nb.platform.id()) + .join(ctx.binaries.bundled_filename(nb.platform)); + files.push(PackagedFile::copy(dest, nb.source.clone())); + } + Some(files) + } +} + +/// Render the complete Swift wrapper file: prelude and imports, the private +/// runtime, every module's file-scope types, and one namespace `enum` per +/// top-level module. +fn render_swift_wrapper( + layout: &Layout, + model: &BindingModel, + config: &SwiftConfig, + filename: &str, +) -> String { + let runtime_error = layout.runtime_error(); + let ctx = SwiftCtx::new(model, &layout.module); + let mut out = String::with_capacity(16 * 1024); + out.push_str(&render_prelude( + CommentStyle::DoubleSlash, + config.input_basename(), + )); + out.push_str(&format!( + "import {}\nimport Foundation\n\n", + layout.c_module + )); + out.push_str(&render_runtime(model, &layout.names(), &runtime_error)); + out.push_str("\n// MARK: - API\n\n"); + for mb in &model.modules { + render_swift_module_types(&mut out, mb, &ctx); + } + for root in model.roots() { + render_swift_namespace(&mut out, model, root, 0, config.strip_module_prefix, &ctx); + out.push('\n'); + } + out.push_str(&render_trailer(CommentStyle::DoubleSlash, filename)); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use weaveffi_model::ir::{ + Api, CallbackInterfaceDef, Function, InterfaceDef, Module, Param, StructDef, StructField, + TypeRef, + }; + use weaveffi_model::pkg::Identity; + + fn param(name: &str, ty: TypeRef) -> Param { + Param { + name: name.into(), + ty, + doc: None, + } + } + + fn func(name: &str, params: Vec, returns: Option) -> Function { + Function { + name: name.into(), + params, + returns, + doc: None, + throws: false, + r#async: false, + cancellable: false, + deprecated: None, + } + } + + fn named(name: &str) -> TypeRef { + TypeRef::Named(name.into()) + } + + fn opt(ty: TypeRef) -> TypeRef { + TypeRef::Optional(Box::new(ty)) + } + + /// One module exercising every object and callback shape the ABI + /// contract distinguishes, plus a cancellable async function. + fn fixture() -> ResolvedApi { + let mut wait = func("wait", vec![param("ms", TypeRef::I64)], Some(TypeRef::I64)); + wait.r#async = true; + wait.cancellable = true; + ResolvedApi::assume_valid(Api { + version: weaveffi_model::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![Module { + name: "shop".into(), + doc: None, + functions: vec![ + func( + "find_cart", + vec![param("current", opt(named("shop.Cart")))], + Some(opt(named("shop.Cart"))), + ), + func( + "all_carts", + vec![], + Some(TypeRef::Iterator(Box::new(named("shop.Cart")))), + ), + func("watch", vec![param("watcher", named("shop.Watcher"))], None), + func( + "echo", + vec![param("text", TypeRef::StringUtf8)], + Some(TypeRef::StringUtf8), + ), + wait, + ], + interfaces: vec![InterfaceDef { + name: "Cart".into(), + doc: None, + deprecated: None, + constructors: vec![func( + "new", + vec![param("owner", TypeRef::StringUtf8)], + None, + )], + methods: vec![func( + "add", + vec![param("sku", TypeRef::StringUtf8)], + Some(TypeRef::Bool), + )], + statics: vec![], + }], + callback_interfaces: vec![CallbackInterfaceDef { + name: "Watcher".into(), + doc: None, + deprecated: None, + methods: vec![ + func( + "on_event", + vec![ + param("name", TypeRef::StringUtf8), + param("count", TypeRef::I32), + param("order", named("shop.Order")), + param("cart", named("shop.Cart")), + ], + Some(TypeRef::Bool), + ), + func("on_done", vec![], None), + ], + }], + structs: vec![StructDef { + name: "Order".into(), + doc: None, + deprecated: None, + fields: vec![ + StructField { + name: "cart".into(), + ty: named("shop.Cart"), + doc: None, + }, + StructField { + name: "history".into(), + ty: TypeRef::List(Box::new(named("shop.Cart"))), + doc: None, + }, + ], + }], + enums: vec![], + errors: None, + modules: vec![], + }], + }) + .with_identity(Identity::named("shop_kit")) + } + + fn render() -> String { + let api = fixture(); + let model = BindingModel::build(&api); + let config = SwiftConfig::default(); + let layout = Layout::new(&api, &config); + render_swift_wrapper(&layout, &model, &config, "ShopKit.swift") + } + + #[track_caller] + fn assert_has(out: &str, needle: &str) { + assert!(out.contains(needle), "missing {needle:?} in:\n{out}"); + } + + #[test] + fn names_come_from_the_identity() { + let api = fixture(); + let model = BindingModel::build(&api); + let config = SwiftConfig::default(); + let files = Layout::new(&api, &config).sources(&model, Utf8Path::new("out"), &config); + let paths: Vec = files + .iter() + .map(|(p, _)| p.as_str().replace('\\', "/")) + .collect(); + assert_eq!( + paths, + [ + "out/Package.swift", + "out/Sources/CShopKit/module.modulemap", + "out/Sources/CShopKit/shop_kit.h", + "out/Sources/ShopKit/ShopKit.swift", + ] + ); + assert_has(&files[0].1, "name: \"ShopKit\""); + assert_has(&files[0].1, ".systemLibrary(name: \"CShopKit\")"); + assert_has( + &files[0].1, + ".binaryTarget(name: \"CShopKit\", path: xcframework)", + ); + assert_has(&files[1].1, "header \"shop_kit.h\""); + assert_has(&files[1].1, "link \"shop_kit\""); + let all = files.iter().map(|(_, c)| c.as_str()).collect::(); + assert!(!all.contains("weaveffi_"), "{all}"); + assert!(!all.contains("WeaveFFIError"), "{all}"); + } + + #[test] + fn module_name_override_wins() { + let api = fixture(); + let config = SwiftConfig { + module_name: Some("Shop".into()), + ..SwiftConfig::default() + }; + let layout = Layout::new(&api, &config); + assert_eq!(layout.module, "Shop"); + assert_eq!(layout.c_module, "CShop"); + assert_eq!(layout.library, "shop_kit"); + } + + #[test] + fn contract_is_checked_before_the_first_call() { + let out = render(); + assert_has(&out, "let abi = shop_kit_abi_version()"); + assert_has(&out, "wvCheckModule(\"shop\", shop_kit_shop_checksum(), 0x"); + assert_has(&out, "public struct ShopKitRuntimeError: Error"); + assert_has( + &out, + "public static func echo(text: String) -> String {\n wvLoad()", + ); + } + + #[test] + fn strings_cross_as_pointer_and_length() { + let out = render(); + assert_has(&out, "wvWithUTF8(text) { text_ptr, text_len in"); + assert_has( + &out, + "shop_kit_shop_echo(text_ptr, text_len, &outLen, &err)", + ); + assert_has(&out, "return wvTakeString(rv, outLen)"); + } + + #[test] + fn interface_class_owns_one_reference() { + let out = render(); + assert_has(&out, "public final class Cart: @unchecked Sendable {"); + assert_has(&out, "let ptr: OpaquePointer"); + assert_has(&out, "shop_kit_shop_Cart_destroy(ptr)"); + assert_has(&out, "wvNonNull(shop_kit_shop_Cart_clone(ptr))"); + assert_eq!(out.matches("shop_kit_shop_Cart_destroy(").count(), 1); + assert_has(&out, "public init(owner: String) {"); + assert_has(&out, "self.ptr = wvNonNull(rv)"); + assert_has(&out, "public func add(sku: String) -> Bool {"); + assert_has(&out, "shop_kit_shop_Cart_add(ptr, sku_ptr, sku_len, &err)"); + } + + #[test] + fn nullable_objects_map_to_optional_wrappers() { + let out = render(); + assert_has( + &out, + "public static func findCart(current: Cart?) -> Cart? {", + ); + assert_has(&out, "shop_kit_shop_find_cart(current?.ptr, &err)"); + assert_has(&out, "return rv.map { Cart(ptr: $0) }"); + } + + #[test] + fn records_carry_object_tokens() { + let out = render(); + assert_has(&out, "public struct Order: Sendable {"); + assert_has(&out, "w.writeObject(value.cart.clonePtr())"); + assert_has(&out, "Cart(ptr: r.readObject())"); + } + + #[test] + fn object_iterator_adopts_each_element() { + let out = render(); + assert_has( + &out, + "public final class ShopAllCartsIterator: Sequence, IteratorProtocol {", + ); + assert_has(&out, "var item: OpaquePointer? = nil"); + assert_has( + &out, + "shop_kit_shop_AllCartsIterator_next(handle, &item, &err)", + ); + assert_has(&out, "shop_kit_shop_AllCartsIterator_destroy(handle)"); + assert_has(&out, "return Cart(ptr: wvNonNull(item))"); + } + + #[test] + fn callback_interface_renders_protocol_box_and_vtable() { + let out = render(); + assert_has(&out, "public protocol Watcher: AnyObject, Sendable {"); + assert_has( + &out, + "func onEvent(name: String, count: Int32, order: Order, cart: Cart) throws -> Bool", + ); + assert_has(&out, "final class WvWatcherBox: Sendable {"); + assert_has( + &out, + "static let shared = WvVtable(shop_kit_shop_Watcher_vtable(", + ); + assert_has( + &out, + "on_event: { ctx, name_ptr, name_len, count, order_ptr, order_len, cart, out_err in", + ); + assert_has(&out, "name: wvBorrowString(name_ptr, name_len)"); + assert_has( + &out, + "order: wvBorrowBuffer(order_ptr, order_len, wvReadOrder)", + ); + assert_has(&out, "cart: Cart(ptr: wvNonNull(cart))"); + assert_has(&out, "wvForeignError(out_err, error)"); + assert_has(&out, "Unmanaged.fromOpaque(ctx!).release()"); + assert_has( + &out, + "shop_kit_shop_watch(watcher_ctx, WvWatcherVtable.shared.pointer, &err)", + ); + } + + #[test] + fn cancellable_async_cancels_the_native_token() { + let out = render(); + assert_has( + &out, + "public static func wait(ms: Int64) async throws -> Int64 {", + ); + assert_has(&out, "let token = WvCancelToken()"); + assert_has(&out, "return try await withTaskCancellationHandler {"); + assert_has( + &out, + "shop_kit_shop_wait(ms, token.raw, { context, err, result in", + ); + assert_has( + &out, + "cont.resume(throwing: wvTakeError(err, wvCancelledOrTrap))", + ); + assert_has(&out, "} onCancel: {\n token.cancel()"); + } +} diff --git a/crates/weaveffi-gen/src/targets/swift/package.rs b/crates/weaveffi-gen/src/targets/swift/package.rs new file mode 100644 index 00000000..802c5d3e --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/package.rs @@ -0,0 +1,59 @@ +//! SwiftPM packaging surfaces: `Package.swift`, the system-library module +//! map, and the packaged-artifact README, framed with the generated-file +//! prelude and trailer. + +use crate::package::PackageContext; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; + +use crate::targets::swift::runtime::{render_module_map, render_package, render_readme, Names}; + +/// The SwiftPM tools version the manifest declares (Swift 5 language mode). +const TOOLS_VERSION: &str = "5.9"; + +/// Render `Package.swift`. The manifest picks the C module's source at +/// resolution time: a prebuilt `C{Module}.xcframework` next to it when one +/// exists, otherwise the bundled system-library target. +pub(crate) fn render_package_swift(names: &Names, input_basename: &str) -> String { + // `swift-tools-version` must be the very first line of the manifest, so + // the generated-file prelude follows it. + format!( + "// swift-tools-version:{TOOLS_VERSION}\n{}{}\n{}", + render_prelude(CommentStyle::DoubleSlash, input_basename), + render_package(names), + render_trailer(CommentStyle::DoubleSlash, "Package.swift"), + ) +} + +/// Render the module map of the `C{Module}` system-library target, which +/// sits next to its copy of the C header. +pub(crate) fn render_modulemap(names: &Names, input_basename: &str) -> String { + format!( + "{}{}\n{}", + render_prelude(CommentStyle::DoubleSlash, input_basename), + render_module_map(names), + render_trailer(CommentStyle::DoubleSlash, "module.modulemap"), + ) +} + +/// README for a packaged Swift artifact: linking the bundled libraries and +/// assembling the optional `XCFramework`, the one step that needs Apple +/// tooling. +pub(crate) fn render_packaged_readme( + names: &Names, + ctx: &PackageContext, + input_basename: &str, +) -> String { + let platforms = ctx + .binaries + .platforms() + .filter(|p| p.is_desktop()) + .map(|p| format!("- `lib/{}/`", p.id())) + .collect::>() + .join("\n"); + format!( + "{}{}\n{}", + render_prelude(CommentStyle::Xml, input_basename), + render_readme(names, &platforms), + render_trailer(CommentStyle::Xml, "README.md"), + ) +} diff --git a/crates/weaveffi-gen/src/targets/swift/runtime.rs b/crates/weaveffi-gen/src/targets/swift/runtime.rs new file mode 100644 index 00000000..785be7dc --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/runtime.rs @@ -0,0 +1,108 @@ +//! The fixed Swift sources the generator splices names into: the private +//! runtime (codec, error helpers, contract check, async and callback +//! support), the `Package.swift` manifest, the C module map, and the +//! packaged README. Each lives as a real file under `runtime/` with +//! `{{PLACEHOLDER}}` markers. + +use std::fmt::Write; + +use weaveffi_model::model::{checksum_symbol, BindingModel, ABI_VERSION}; + +const RUNTIME: &str = include_str!("runtime/Runtime.swift"); +const PACKAGE: &str = include_str!("runtime/Package.swift"); +const MODULE_MAP: &str = include_str!("runtime/module.modulemap"); +const README: &str = include_str!("runtime/README.md"); + +/// Replace every `{{KEY}}` in `template` with its value. +/// +/// # Panics +/// +/// Panics (in debug builds) when a placeholder is left unfilled, which is a +/// bug in the caller's substitution list. +fn fill(template: &str, vars: &[(&str, &str)]) -> String { + let mut out = template.to_string(); + for (key, value) in vars { + out = out.replace(&format!("{{{{{key}}}}}"), value); + } + debug_assert!(!out.contains("{{"), "unfilled placeholder in:\n{out}"); + out +} + +/// The names the fixed sources are spliced with. +pub(crate) struct Names<'a> { + /// The Swift module (and package and product) name. + pub(crate) module: &'a str, + /// The C module name, `C{module}`. + pub(crate) c_module: &'a str, + /// The native library base name. + pub(crate) library: &'a str, + /// The C header file name. + pub(crate) header: &'a str, +} + +/// The private runtime the wrapper file starts with, including the contract +/// check against every top-level module's checksum. +pub(crate) fn render_runtime(model: &BindingModel, names: &Names, runtime_error: &str) -> String { + let mut checks = String::new(); + for root in model.roots() { + let checksum = root + .checksum + .expect("every top-level module carries a checksum"); + let _ = writeln!( + checks, + " wvCheckModule(\"{}\", {}(), 0x{checksum:016x})", + root.name, + checksum_symbol(&model.prefix, &root.name) + ); + } + let checks = checks.trim_end_matches('\n'); + fill( + RUNTIME, + &[ + ("RUNTIME_ERROR", runtime_error), + ("PREFIX", &model.prefix), + ("MODULE", names.module), + ("LIBRARY", names.library), + ("ABI_VERSION", &ABI_VERSION.to_string()), + ("CONTRACT_CHECKS", checks), + ], + ) +} + +/// The `Package.swift` body (after the tools-version line and prelude). +pub(crate) fn render_package(names: &Names) -> String { + fill( + PACKAGE, + &[ + ("MODULE", names.module), + ("C_MODULE", names.c_module), + ("LIBRARY", names.library), + ], + ) +} + +/// The system-library module map declaring the bundled header. +pub(crate) fn render_module_map(names: &Names) -> String { + fill( + MODULE_MAP, + &[ + ("C_MODULE", names.c_module), + ("HEADER", names.header), + ("LIBRARY", names.library), + ], + ) +} + +/// The packaged README, listing the bundled `platforms`. +pub(crate) fn render_readme(names: &Names, platforms: &str) -> String { + fill( + README, + &[ + ("MODULE", names.module), + ("C_MODULE", names.c_module), + ("LIBRARY", names.library), + ("HEADER", names.header), + ("PLATFORMS", platforms), + ], + ) +} diff --git a/crates/weaveffi-gen/src/targets/swift/runtime/Package.swift b/crates/weaveffi-gen/src/targets/swift/runtime/Package.swift new file mode 100644 index 00000000..189fe68b --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/runtime/Package.swift @@ -0,0 +1,24 @@ +import Foundation +import PackageDescription + +// The C module `{{C_MODULE}}` comes from one of two places. Next to this +// manifest, a prebuilt `{{C_MODULE}}.xcframework` (assembled from the native +// libraries, see the README) wins. Otherwise `Sources/{{C_MODULE}}` declares +// the generated header as a system library that links `{{LIBRARY}}` from the +// linker search path, e.g. `swift build -Xlinker -L/path/to/lib`. +let xcframework = "{{C_MODULE}}.xcframework" +let cModule: Target = FileManager.default.fileExists(atPath: "\(Context.packageDirectory)/\(xcframework)") + ? .binaryTarget(name: "{{C_MODULE}}", path: xcframework) + : .systemLibrary(name: "{{C_MODULE}}") + +let package = Package( + name: "{{MODULE}}", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], + products: [ + .library(name: "{{MODULE}}", targets: ["{{MODULE}}"]), + ], + targets: [ + cModule, + .target(name: "{{MODULE}}", dependencies: ["{{C_MODULE}}"]), + ] +) diff --git a/crates/weaveffi-gen/src/targets/swift/runtime/README.md b/crates/weaveffi-gen/src/targets/swift/runtime/README.md new file mode 100644 index 00000000..23300db3 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/runtime/README.md @@ -0,0 +1,25 @@ +# {{MODULE}} (Swift) + +A SwiftPM package (product and module `{{MODULE}}`) over the native library +`{{LIBRARY}}`, whose C declarations are the module `{{C_MODULE}}`. + +## Linking the bundled library + +The prebuilt libraries are bundled under `lib//`. Without further +setup the package links `{{LIBRARY}}` from the linker search path: + +```bash +swift build -Xlinker -L"$PWD/lib/darwin-arm64" +``` + +## iOS and the XCFramework + +When `{{C_MODULE}}.xcframework` sits next to `Package.swift`, the manifest uses +it as a binary target instead, so apps (including iOS apps) link the library +with no flags. `weaveffi package --target swift` assembles it on macOS +whenever the requested platforms include an iOS slice, for example +`--platforms ios-arm64,ios-sim-arm64,darwin-arm64`. + +## Bundled platforms + +{{PLATFORMS}} diff --git a/crates/weaveffi-gen/src/targets/swift/runtime/Runtime.swift b/crates/weaveffi-gen/src/targets/swift/runtime/Runtime.swift new file mode 100644 index 00000000..0d306318 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/runtime/Runtime.swift @@ -0,0 +1,400 @@ +// MARK: - Runtime support + +/// A failure the native library reports outside any declared error domain: +/// an unknown error code or a runtime trap. Throwing wrappers raise it; +/// non-throwing wrappers stop the process with `fatalError` instead, since +/// Swift has no unchecked errors. +public struct {{RUNTIME_ERROR}}: Error, LocalizedError, Hashable, Sendable { + /// The code of a generic runtime failure. + public static let genericCode: Int32 = -1 + /// The code of a panic caught at the library boundary. + public static let panicCode: Int32 = -2 + /// The code of an argument or result that failed to marshal. + public static let marshalCode: Int32 = -3 + /// The code of a callback implementation that threw. + public static let foreignCode: Int32 = -4 + + /// The numeric code the native library reported. + public let errorCode: Int32 + /// The message the native library reported. + public let message: String + + /// Creates an error carrying a native code and message. + public init(errorCode: Int32, message: String) { + self.errorCode = errorCode + self.message = message + } + + public var errorDescription: String? { message } +} + +/// The error slot every native call reports through. +typealias WvError = {{PREFIX}}_error + +/// The C ABI revision these bindings were generated against. +let wvAbiVersion: UInt32 = {{ABI_VERSION}} + +/// The contract check, run once before the first native call: the library +/// must implement this ABI revision and every top-level module must carry +/// the checksum these bindings were generated from. +let wvContract: Void = { + let abi = {{PREFIX}}_abi_version() + guard abi == wvAbiVersion else { + fatalError("{{MODULE}}: the native library '{{LIBRARY}}' implements C ABI revision \(abi), but these bindings need revision \(wvAbiVersion)") + } +{{CONTRACT_CHECKS}} +}() + +/// Runs the contract check if it hasn't run yet. +@inline(__always) +func wvLoad() { + _ = wvContract +} + +/// Stops the process when a module's contract checksum doesn't match. +func wvCheckModule(_ module: StaticString, _ actual: UInt64, _ expected: UInt64) { + guard actual == expected else { + fatalError("{{MODULE}}: module '\(module)' of the native library '{{LIBRARY}}' doesn't match these bindings (checksum 0x\(String(actual, radix: 16)), expected 0x\(String(expected, radix: 16))); regenerate the bindings or rebuild the library") + } +} + +/// The message carried by an error slot. +func wvErrorMessage(_ err: WvError) -> String { + err.message.map { String(cString: $0) } ?? "" +} + +/// The Swift error for a code outside any declared domain: cancellation +/// (`-5`) becomes `CancellationError`, anything else the runtime error. +func wvRuntimeError(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + return {{RUNTIME_ERROR}}(errorCode: err.code, message: wvErrorMessage(err)) +} + +/// Stops the process for a failed call that can't throw, naming the code and +/// message the library reported. +func wvFatal(_ code: Int32, _ message: String, _ function: StaticString) -> Never { + fatalError("{{MODULE}}.\(function) failed with code \(code): \(message)") +} + +/// Throws the runtime error for a filled error slot, clearing it. +@inline(__always) +func wvCheck(_ err: inout WvError) throws { + guard err.code != 0 else { return } + let error = wvRuntimeError(err) + {{PREFIX}}_error_clear(&err) + throw error +} + +/// Stops the process for a filled error slot of a call that can't throw. +@inline(__always) +func wvTrap(_ err: inout WvError, _ function: StaticString = #function) { + guard err.code != 0 else { return } + let code = err.code + let message = wvErrorMessage(err) + {{PREFIX}}_error_clear(&err) + wvFatal(code, message, function) +} + +/// Maps and releases the heap-boxed error an async completion received. +func wvTakeError(_ err: UnsafeMutablePointer, _ map: (WvError) -> Error) -> Error { + let error = map(err.pointee) + {{PREFIX}}_error_free(err) + return error +} + +/// The error mapping of a cancellable call that declares no error domain: +/// cancellation throws `CancellationError`, anything else stops the process. +func wvCancelledOrTrap(_ err: WvError) -> Error { + if err.code == -5 { + return CancellationError() + } + wvFatal(err.code, wvErrorMessage(err), "async call") +} + +/// Stops the process for the boxed error of an async call that can't throw. +func wvTrapBoxed(_ err: UnsafeMutablePointer, _ function: StaticString = #function) -> Never { + wvFatal(err.pointee.code, wvErrorMessage(err.pointee), function) +} + +/// Unwraps a pointer the library promised is non-null. +@inline(__always) +func wvNonNull

(_ ptr: P?, _ function: StaticString = #function) -> P { + guard let ptr = ptr else { + wvFatal({{RUNTIME_ERROR}}.marshalCode, "the native library returned a null pointer", function) + } + return ptr +} + +/// The Swift enum case for a C-style enum discriminant. +func wvEnumCase(_ type: E.Type, _ raw: Int32) -> E where E.RawValue == Int32 { + guard let value = E(rawValue: raw) else { + wvDecodeFailure("unknown \(E.self) discriminant \(raw)") + } + return value +} + +// MARK: Strings, bytes, and buffers + +/// Lends the UTF-8 bytes of `s` (not NUL-terminated) for one call. +@inline(__always) +func wvWithUTF8(_ s: String, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + var s = s + return try s.withUTF8 { try body($0.baseAddress, $0.count) } +} + +/// Lends the bytes of `d` for one call. +@inline(__always) +func wvWithBytes(_ d: Data, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try d.withUnsafeBytes { try body($0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count) } +} + +/// Lends an encoded value buffer for one call. +@inline(__always) +func wvWithBuffer(_ w: WvWriter, _ body: (UnsafePointer?, Int) throws -> R) rethrows -> R { + try w.bytes.withUnsafeBufferPointer { try body($0.baseAddress, $0.count) } +} + +/// Copies a returned string, then releases the library's allocation. +func wvTakeString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + defer { {{PREFIX}}_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies returned bytes, then releases the library's allocation. +func wvTakeBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + defer { {{PREFIX}}_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + return Data(bytes: ptr, count: len) +} + +/// Decodes a returned value buffer in place, then releases it. +func wvTakeBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + defer { + if let ptr = ptr { {{PREFIX}}_free_bytes(UnsafeMutablePointer(mutating: ptr), len) } + } + return wvBorrowBuffer(ptr, len, decode) +} + +/// Copies a borrowed string. +func wvBorrowString(_ ptr: UnsafePointer?, _ len: Int) -> String { + guard let ptr = ptr else { return "" } + return String(decoding: UnsafeBufferPointer(start: ptr, count: len), as: UTF8.self) +} + +/// Copies borrowed bytes. +func wvBorrowBytes(_ ptr: UnsafePointer?, _ len: Int) -> Data { + guard let ptr = ptr else { return Data() } + return Data(bytes: ptr, count: len) +} + +/// Decodes a borrowed value buffer in place. +func wvBorrowBuffer(_ ptr: UnsafePointer?, _ len: Int, _ decode: (inout WvReader) -> T) -> T { + var r = WvReader(UnsafeRawPointer(ptr), len) + let value = decode(&r) + r.finish() + return value +} + +/// Stops the process on a malformed value buffer, which the wire format +/// treats like a marshalling failure. +func wvDecodeFailure(_ context: String) -> Never { + fatalError("{{MODULE}}: malformed value buffer: \(context)") +} + +/// Serializes values in the value-buffer wire format: little-endian, packed, +/// no alignment. +struct WvWriter { + var bytes: [UInt8] = [] + + init() { + bytes.reserveCapacity(64) + } + + @inline(__always) + private mutating func append(_ v: T) { + withUnsafeBytes(of: v.littleEndian) { bytes.append(contentsOf: $0) } + } + + mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } + mutating func writeI8(_ v: Int8) { append(v) } + mutating func writeU8(_ v: UInt8) { append(v) } + mutating func writeI16(_ v: Int16) { append(v) } + mutating func writeU16(_ v: UInt16) { append(v) } + mutating func writeI32(_ v: Int32) { append(v) } + mutating func writeU32(_ v: UInt32) { append(v) } + mutating func writeI64(_ v: Int64) { append(v) } + mutating func writeU64(_ v: UInt64) { append(v) } + mutating func writeF32(_ v: Float) { append(v.bitPattern) } + mutating func writeF64(_ v: Double) { append(v.bitPattern) } + + /// Writes a `u32` length or element count. + mutating func writeLen(_ n: Int) { + precondition(n >= 0 && n <= Int(UInt32.max), "value buffer length exceeds UInt32.max") + append(UInt32(n)) + } + + mutating func writeString(_ v: String) { + let utf8 = v.utf8 + writeLen(utf8.count) + bytes.append(contentsOf: utf8) + } + + mutating func writeBytes(_ v: Data) { + writeLen(v.count) + bytes.append(contentsOf: v) + } + + mutating func writeOptionFlag(_ present: Bool) { bytes.append(present ? 1 : 0) } + + /// Writes an object token. `p` must be a strong reference the buffer now + /// owns (a freshly cloned pointer), never one a wrapper still holds. + mutating func writeObject(_ p: OpaquePointer) { append(UInt64(UInt(bitPattern: p))) } +} + +/// Deserializes values from the value-buffer wire format directly from the +/// library's memory, rejecting truncated buffers, invalid flag bytes, +/// oversized length prefixes, and trailing bytes. +struct WvReader { + private let base: UnsafeRawPointer? + private let count: Int + private var pos = 0 + + init(_ base: UnsafeRawPointer?, _ count: Int) { + self.base = base + self.count = base == nil ? 0 : count + } + + /// The number of unread bytes. + var remaining: Int { count - pos } + + private mutating func take(_ n: Int, _ what: StaticString) -> UnsafeRawPointer { + guard n <= remaining, let base = base else { wvDecodeFailure("truncated \(what)") } + defer { pos += n } + return base + pos + } + + @inline(__always) + private mutating func load(_ type: T.Type, _ what: StaticString) -> T { + T(littleEndian: take(MemoryLayout.size, what).loadUnaligned(as: T.self)) + } + + mutating func readBool() -> Bool { + switch load(UInt8.self, "bool") { + case 0: return false + case 1: return true + default: wvDecodeFailure("bool byte out of range") + } + } + mutating func readI8() -> Int8 { load(Int8.self, "i8") } + mutating func readU8() -> UInt8 { load(UInt8.self, "u8") } + mutating func readI16() -> Int16 { load(Int16.self, "i16") } + mutating func readU16() -> UInt16 { load(UInt16.self, "u16") } + mutating func readI32() -> Int32 { load(Int32.self, "i32") } + mutating func readU32() -> UInt32 { load(UInt32.self, "u32") } + mutating func readI64() -> Int64 { load(Int64.self, "i64") } + mutating func readU64() -> UInt64 { load(UInt64.self, "u64") } + mutating func readF32() -> Float { Float(bitPattern: load(UInt32.self, "f32")) } + mutating func readF64() -> Double { Double(bitPattern: load(UInt64.self, "f64")) } + + /// Reads a byte length, which can't exceed the unread bytes. + mutating func readLen() -> Int { + let n = Int(load(UInt32.self, "length")) + guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } + return n + } + + /// Reads an element count. Elements may encode to zero bytes, so the + /// count isn't bounded by the unread bytes; callers cap preallocation. + mutating func readCount() -> Int { + Int(load(UInt32.self, "count")) + } + + mutating func readString() -> String { + let n = readLen() + if n == 0 { return "" } + let p = take(n, "string") + guard let s = String(bytes: UnsafeRawBufferPointer(start: p, count: n), encoding: .utf8) else { + wvDecodeFailure("string is not valid UTF-8") + } + return s + } + + mutating func readBytes() -> Data { + let n = readLen() + if n == 0 { return Data() } + return Data(bytes: take(n, "bytes"), count: n) + } + + mutating func readOptionFlag() -> Bool { + switch load(UInt8.self, "option flag") { + case 0: return false + case 1: return true + default: wvDecodeFailure("option flag byte out of range") + } + } + + /// Reads an object token: one strong reference the caller adopts into a + /// wrapper whose deinit releases it. + mutating func readObject() -> OpaquePointer { + guard let p = OpaquePointer(bitPattern: UInt(load(UInt64.self, "object token"))) else { + wvDecodeFailure("null object token") + } + return p + } + + func finish() { + if remaining != 0 { wvDecodeFailure("trailing bytes after value") } + } +} + +// MARK: Async and callbacks + +/// A native cancel token owned by one cancellable call. The library takes its +/// own reference at launch, so this one is released whenever the wrapper is +/// done with it. +final class WvCancelToken: @unchecked Sendable { + let raw: OpaquePointer + + init() { + raw = wvNonNull({{PREFIX}}_cancel_token_create()) + } + + deinit { + {{PREFIX}}_cancel_token_destroy(raw) + } + + func cancel() { + {{PREFIX}}_cancel_token_cancel(raw) + } +} + +/// A continuation boxed for the C `context` slot of an async launch. +final class WvContinuation { + let value: CheckedContinuation + + init(_ value: CheckedContinuation) { + self.value = value + } +} + +/// One process-wide callback vtable at a stable address the library may hold +/// for the life of the process. +final class WvVtable: @unchecked Sendable { + let pointer: UnsafePointer + + init(_ value: T) { + let cell = UnsafeMutablePointer.allocate(capacity: 1) + cell.initialize(to: value) + pointer = UnsafePointer(cell) + } +} + +/// Reports a thrown Swift error to the library as a callback failure +/// (code -4); the library aborts its current call with that code and message. +func wvForeignError(_ outErr: UnsafeMutablePointer?, _ error: Error) { + let message = (error as? LocalizedError)?.errorDescription ?? String(describing: error) + message.withCString { {{PREFIX}}_error_set(outErr, {{RUNTIME_ERROR}}.foreignCode, $0) } +} diff --git a/crates/weaveffi-gen/src/targets/swift/runtime/module.modulemap b/crates/weaveffi-gen/src/targets/swift/runtime/module.modulemap new file mode 100644 index 00000000..f2bc0b53 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/runtime/module.modulemap @@ -0,0 +1,5 @@ +module {{C_MODULE}} [system] { + header "{{HEADER}}" + link "{{LIBRARY}}" + export * +} diff --git a/crates/weaveffi-gen/src/targets/swift/types.rs b/crates/weaveffi-gen/src/targets/swift/types.rs new file mode 100644 index 00000000..a2c74181 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/swift/types.rs @@ -0,0 +1,210 @@ +//! Swift type spellings and identifier policy: how model types and +//! user-chosen names render in Swift source. + +use std::collections::{HashMap, HashSet}; + +use crate::lang; +use crate::utils::local_type_name; +use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use weaveffi_model::model::{BindingModel, IteratorBinding, Ty}; + +/// The Swift spelling of a user-chosen identifier in a lowerCamel position +/// (parameters, fields, enum cases, wrapper names): camel-cased, then +/// keyword-escaped through the shared rule, so a parameter named `in` +/// becomes `in_` rather than emitting broken Swift. +pub(crate) fn swift_ident(name: &str) -> String { + lang::escape_ident(&name.to_lower_camel_case(), lang::SWIFT_KEYWORDS) +} + +/// Escape a string for embedding inside a Swift double-quoted literal. +pub(crate) fn swift_str(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") +} + +/// The `@available(*, deprecated, ...)` attribute line for a deprecation +/// message. +pub(crate) fn deprecated_attr(msg: &str) -> String { + format!("@available(*, deprecated, message: \"{}\")", swift_str(msg)) +} + +/// Facts about the whole API that type spellings and codecs consult: +/// namespace collisions, C enum type names, and which value types can be +/// `Hashable`. +pub(crate) struct SwiftCtx<'a> { + /// C ABI symbol prefix (e.g. `kvstore`). + pub(crate) c_prefix: &'a str, + /// SwiftPM module name (e.g. `Kvstore`). + pub(crate) swift_module: &'a str, + /// Every module name in the API, PascalCased: the namespace `enum` + /// names a wrapper-type reference can be shadowed by. + module_names: HashSet, + /// The C type of every C-style enum, keyed by its absolute name. + enum_c_types: HashMap, + /// Absolute names of the records and rich enums that are `Hashable` + /// (every field is). + hashable: HashSet, +} + +impl<'a> SwiftCtx<'a> { + /// Collect the API-wide facts from the binding model. + pub(crate) fn new(model: &'a BindingModel, swift_module: &'a str) -> SwiftCtx<'a> { + let module_names = model + .modules + .iter() + .map(|m| m.name.to_upper_camel_case()) + .collect(); + let enum_c_types = model + .modules + .iter() + .flat_map(|m| { + m.enums + .iter() + .filter(|e| !e.is_rich()) + .map(move |e| (format!("{}.{}", m.dot_path, e.name), e.c_tag.clone())) + }) + .collect(); + SwiftCtx { + c_prefix: model.prefix.as_str(), + swift_module, + module_names, + enum_c_types, + hashable: hashable_types(model), + } + } + + /// Qualify a top-level wrapper type name with the Swift module when its + /// name collides with a namespace `enum`. Inside `enum Kv { enum Stats { + /// ... } }` the bare name `Stats` resolves to the namespace, not the + /// top-level type; `Kvstore.Stats` forces the type. + pub(crate) fn ty_name(&self, local: &str) -> String { + if self.module_names.contains(local) { + format!("{}.{}", self.swift_module, local) + } else { + local.to_string() + } + } + + /// The Swift surface type of a model type. + /// + /// # Panics + /// + /// Panics on an iterator, which only appears as a function return and + /// renders as its per-function sequence class instead. + pub(crate) fn swift_type(&self, t: &Ty) -> String { + match t { + Ty::Enum(name) + | Ty::Record(name) + | Ty::RichEnum(name) + | Ty::Interface(name) + | Ty::CallbackInterface(name) => self.ty_name(local_type_name(name)), + Ty::Optional(inner) => format!("{}?", self.swift_type(inner)), + Ty::List(inner) => format!("[{}]", self.swift_type(inner)), + Ty::Map(k, v) => format!("[{}: {}]", self.swift_type(k), self.swift_type(v)), + other => scalar_swift_type(other).to_string(), + } + } + + /// The imported C type of the C-style enum named `name`. + /// + /// # Panics + /// + /// Panics when no C-style enum has that name, which validation rules out. + pub(crate) fn c_enum_type(&self, name: &str) -> &str { + self.enum_c_types + .get(name) + .unwrap_or_else(|| panic!("unknown C-style enum `{name}`")) + } + + /// `true` when values of `t` can be `Hashable` (and so `Equatable`). + pub(crate) fn is_hashable(&self, t: &Ty) -> bool { + type_is_hashable(t, &self.hashable) + } +} + +/// The Swift spelling of a scalar, string, or bytes type. +/// +/// # Panics +/// +/// Panics on a user, container, or iterator type. +pub(crate) fn scalar_swift_type(t: &Ty) -> &'static str { + match t { + Ty::I8 => "Int8", + Ty::I16 => "Int16", + Ty::I32 => "Int32", + Ty::I64 => "Int64", + Ty::U8 => "UInt8", + Ty::U16 => "UInt16", + Ty::U32 => "UInt32", + Ty::U64 => "UInt64", + Ty::F32 => "Float", + Ty::F64 => "Double", + Ty::Bool => "Bool", + Ty::StringUtf8 => "String", + Ty::Bytes => "Data", + other => unreachable!("`{other}` isn't a scalar type"), + } +} + +fn type_is_hashable(t: &Ty, hashable: &HashSet) -> bool { + match t { + Ty::Interface(_) | Ty::CallbackInterface(_) | Ty::Iterator(_) => false, + Ty::Record(name) | Ty::RichEnum(name) => hashable.contains(name), + Ty::Optional(inner) | Ty::List(inner) => type_is_hashable(inner, hashable), + Ty::Map(k, v) => type_is_hashable(k, hashable) && type_is_hashable(v, hashable), + _ => true, + } +} + +/// The records and rich enums whose every field is `Hashable`, as a greatest +/// fixed point so mutually referencing types resolve. +fn hashable_types(model: &BindingModel) -> HashSet { + let mut types: Vec<(String, Vec<&Ty>)> = Vec::new(); + for m in &model.modules { + for s in &m.structs { + let fields = s.fields.iter().map(|f| &f.ty).collect(); + types.push((format!("{}.{}", m.dot_path, s.name), fields)); + } + for e in m.enums.iter().filter(|e| e.is_rich()) { + let fields = e + .variants + .iter() + .flat_map(|v| v.fields.iter().map(|f| &f.ty)) + .collect(); + types.push((format!("{}.{}", m.dot_path, e.name), fields)); + } + } + let mut hashable: HashSet = types.iter().map(|(n, _)| n.clone()).collect(); + loop { + let before = hashable.len(); + for (name, fields) in &types { + if hashable.contains(name) && !fields.iter().all(|t| type_is_hashable(t, &hashable)) { + hashable.remove(name); + } + } + if hashable.len() == before { + return hashable; + } + } +} + +/// The Swift name of the lazy sequence class emitted for one `iter` +/// function: the iterator tag minus the C prefix, PascalCased +/// (`kvstore_kv_Store_ListKeysIterator` becomes `KvStoreListKeysIterator`). +pub(crate) fn iterator_class_name(it: &IteratorBinding, c_prefix: &str) -> String { + it.iter_tag + .strip_prefix(&format!("{c_prefix}_")) + .unwrap_or(&it.iter_tag) + .to_upper_camel_case() +} + +/// The internal box class retaining one implementation of the callback +/// interface named `name` across the C boundary: `Wv{Name}Box`. +pub(crate) fn callback_box_name(name: &str) -> String { + format!("Wv{}Box", local_type_name(name)) +} + +/// The internal namespace holding the process-wide vtable for the callback +/// interface named `name`: `Wv{Name}Vtable`. +pub(crate) fn callback_vtable_name(name: &str) -> String { + format!("Wv{}Vtable", local_type_name(name)) +} diff --git a/crates/weaveffi-gen/src/targets/wasm/glue.rs b/crates/weaveffi-gen/src/targets/wasm/glue.rs new file mode 100644 index 00000000..19b4d963 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/wasm/glue.rs @@ -0,0 +1,464 @@ +//! The linear-memory glue: one raw entry point per C symbol, built on the +//! fixed `$Linear` transport (`runtime/linear.js`). +//! +//! Each entry point follows the raw calling convention of the shared +//! JavaScript layer ([`crate::targets::js`]): it checks and converts direct +//! arguments, stages strings and byte runs in linear memory for the call, +//! passes the reused error and `out_len` slots, throws the reported error, +//! and turns results back into JavaScript values (copying and freeing +//! returned strings and buffers). Callback-interface vtables and async +//! completions are JavaScript functions installed in the module's function +//! table. + +use weaveffi_model::abi::{AbiParam, CType}; +use weaveffi_model::model::{ + BindingModel, CallShape, CallbackInterfaceBinding, FnBinding, Prim, Ty, +}; +use weaveffi_model::plan::{self, ArgPass, RetPass}; + +use crate::codegen::CodeWriter; +use crate::targets::js::names::direct_prim; + +/// The wasm value type of one C slot. +fn valtype(ty: &CType) -> &'static str { + match ty { + CType::Int64 | CType::Uint64 => "i64", + CType::Float => "f32", + CType::Double => "f64", + _ => "i32", + } +} + +/// The Emscripten signature character of a wasm value type. +fn sig_char(valtype: &str) -> char { + match valtype { + "i64" => 'j', + "f32" => 'f', + "f64" => 'd', + _ => 'i', + } +} + +/// The `[params], [results], 'sig'` triple of a table function. +fn signature(params: &[AbiParam], ret: &CType) -> String { + let ps: Vec<&str> = params.iter().map(|p| valtype(&p.ty)).collect(); + let (results, ret_char) = match ret { + CType::Void => (String::new(), 'v'), + t => (format!("'{}'", valtype(t)), sig_char(valtype(t))), + }; + let sig: String = std::iter::once(ret_char) + .chain(ps.iter().map(|t| sig_char(t))) + .collect(); + let quoted: Vec = ps.iter().map(|t| format!("'{t}'")).collect(); + format!("[{}], [{results}], '{sig}'", quoted.join(", ")) +} + +/// The conversion of a direct JavaScript argument `v` for a wasm call. +fn direct_arg(p: Prim, v: &str) -> String { + match p { + Prim::Bool => format!("m.bool({v})"), + Prim::I64 => format!("m.i64({v})"), + Prim::U64 => format!("m.u64({v})"), + _ => format!("m.num({v})"), + } +} + +/// The JavaScript value of a direct wasm value `r`: unsigned integers and +/// bools are reinterpreted, everything else is already right. +fn direct_value(p: Prim, r: &str) -> String { + match p { + Prim::Bool => format!("{r} !== 0"), + Prim::U32 => format!("{r} >>> 0"), + Prim::U64 => format!("BigInt.asUintN(64, {r})"), + _ => r.to_string(), + } +} + +/// The JavaScript value of a received value whose slots are `slots` (one, +/// or a pointer and a length). Owned values are freed after copying; +/// borrowed ones (callback arguments) are only copied. +fn receive(ty: &Ty, pass: &RetPass, slots: &[&str], owned: bool) -> String { + match pass { + RetPass::Void => "undefined".into(), + RetPass::Direct => direct_value(direct_prim(ty), slots[0]), + RetPass::String => format!( + "m.{}({}, {})", + if owned { "takeStr" } else { "readStr" }, + slots[0], + slots[1] + ), + RetPass::Bytes | RetPass::Buffer => format!( + "m.{}({}, {})", + if owned { "takeData" } else { "readData" }, + slots[0], + slots[1] + ), + RetPass::Object { nullable: true, .. } => format!("{0} === 0 ? null : {0}", slots[0]), + RetPass::Object { .. } => slots[0].to_string(), + } +} + +/// The `DataView` read of one iterator element of direct type `p` from the +/// `out_item` slot. +fn item_read(p: Prim) -> String { + let (get, le) = match p { + Prim::Bool => return "m.view().getUint8(m.item) !== 0".into(), + Prim::I8 => ("getInt8", false), + Prim::U8 => ("getUint8", false), + Prim::I16 => ("getInt16", true), + Prim::U16 => ("getUint16", true), + Prim::I32 => ("getInt32", true), + Prim::U32 => ("getUint32", true), + Prim::I64 => ("getBigInt64", true), + Prim::U64 => ("getBigUint64", true), + Prim::F32 => ("getFloat32", true), + Prim::F64 => ("getFloat64", true), + Prim::String | Prim::Bytes => unreachable!("not direct"), + }; + if le { + format!("m.view().{get}(m.item, true)") + } else { + format!("m.view().{get}(m.item)") + } +} + +/// The marshalled arguments of one entry point. +#[derive(Default)] +struct Args { + /// The JavaScript parameter names. + params: Vec, + /// Staging statements (inside the `try`). + stage: Vec, + /// Staged locals released in the `finally`. + staged: Vec, + /// The wasm argument expressions, in slot order. + call: Vec, +} + +fn marshal(model: &BindingModel, f: &FnBinding) -> Args { + let mut a = Args::default(); + if f.has_self { + a.params.push("self".into()); + a.call.push("m.handle(self)".into()); + } + for (n, p) in f.params.iter().enumerate() { + let v = format!("a{n}"); + a.params.push(v.clone()); + match p.arg_pass() { + ArgPass::Direct { .. } => a.call.push(direct_arg(direct_prim(&p.ty), &v)), + ArgPass::String { .. } | ArgPass::Bytes { .. } | ArgPass::Buffer { .. } => { + let s = format!("s{n}"); + let stage = if matches!(p.ty, Ty::StringUtf8) { + "str" + } else { + "data" + }; + a.stage.push(format!("{s} = m.{stage}({v});")); + a.staged.push(s.clone()); + a.call.push(format!("{s}[0]")); + a.call.push(format!("{s}[1]")); + } + ArgPass::Object { nullable, .. } => a.call.push(format!( + "m.{}({v})", + if nullable { "handleOpt" } else { "handle" } + )), + ArgPass::Callback { .. } => { + let cb = find_callback( + model, + p.ty.callback_interface_name().expect("callback parameter"), + ); + a.call.push(format!("m.register({v})")); + a.call.push(format!( + "m.vtable('{}', () => $vt_{}(m))", + cb.c_tag, cb.c_tag + )); + } + } + } + if f.cancellable { + a.params.push("token".into()); + a.call.push("m.handleOpt(token)".into()); + } + a +} + +/// The callback interface named by the absolute dotted path `dotted`. +fn find_callback<'a>(model: &'a BindingModel, dotted: &str) -> &'a CallbackInterfaceBinding { + let (module, name) = dotted.rsplit_once('.').expect("absolute type path"); + model + .modules + .iter() + .find(|m| m.dot_path == module) + .and_then(|m| m.callback_interface(name)) + .expect("callback interface exists") +} + +/// Emit `body` (the lines of an entry point) wrapped in the staging +/// `try`/`finally` when anything is staged. +fn emit_staged(w: &mut CodeWriter, a: &Args, body: &[String]) { + if a.staged.is_empty() { + for l in body { + w.line(l); + } + return; + } + let locals: Vec = a.staged.iter().map(|s| format!("{s} = null")).collect(); + w.line(format!("let {};", locals.join(", "))); + w.line("try {"); + w.scope(|w| { + for s in &a.stage { + w.line(s); + } + for l in body { + w.line(l); + } + }); + w.line("} finally {"); + w.scope(|w| { + for s in &a.staged { + w.line(format!("m.unstage({s});")); + } + }); + w.line("}"); +} + +/// Render `$bind(m)`, which builds the raw entry points over a loaded +/// `$Linear`, plus the callback-interface vtable builders, and return it +/// with every C symbol the glue calls (Emscripten mode binds them by name). +pub(crate) fn render_bind(model: &BindingModel) -> (String, Vec) { + let prefix = &model.prefix; + let mut symbols: Vec = [ + "abi_version", + "alloc", + "dealloc", + "free_bytes", + "error_set", + "error_clear", + "error_free", + "debug_live", + "cancel_token_create", + "cancel_token_cancel", + "cancel_token_destroy", + ] + .iter() + .map(|s| format!("{prefix}_{s}")) + .collect(); + let mut w = CodeWriter::two_space(); + + for (_, cb) in model.callback_interfaces() { + emit_vtable(&mut w, cb, prefix); + } + + w.line("// The raw entry points, one per C symbol."); + w.block("function $bind(m) {", "}", |w| { + w.line("const x = m.x;"); + w.block("return {", "};", |w| { + w.line(format!( + "{prefix}_abi_version: () => x.{prefix}_abi_version() >>> 0," + )); + w.line(format!( + "{prefix}_debug_live: (kind) => BigInt.asUintN(64, x.{prefix}_debug_live(m.num(kind)))," + )); + w.line(format!( + "{prefix}_cancel_token_create: () => x.{prefix}_cancel_token_create()," + )); + for op in ["cancel", "destroy"] { + w.line(format!( + "{prefix}_cancel_token_{op}: (t) => x.{prefix}_cancel_token_{op}(m.handle(t))," + )); + } + for root in model.roots() { + let sym = weaveffi_model::model::checksum_symbol(prefix, &root.name); + w.line(format!("{sym}: () => BigInt.asUintN(64, x.{sym}()),")); + symbols.push(sym); + } + for m in &model.modules { + for i in &m.interfaces { + for sym in [&i.clone_symbol, &i.destroy_symbol] { + w.line(format!("{sym}: (h) => x.{sym}(m.handle(h)),")); + symbols.push(sym.clone()); + } + } + for f in m.callables() { + emit_callable(w, model, f, &mut symbols); + } + } + }); + }); + (w.finish(), symbols) +} + +fn emit_callable( + w: &mut CodeWriter, + model: &BindingModel, + f: &FnBinding, + symbols: &mut Vec, +) { + let prefix = &model.prefix; + let a = marshal(model, f); + let head = |sym: &str| format!("{sym}: ({}) => {{", a.params.join(", ")); + match &f.shape { + CallShape::Sync(abi) => { + let pass = plan::ret_pass(f.ret.as_ref(), prefix); + let pair = matches!(pass, RetPass::String | RetPass::Bytes | RetPass::Buffer); + let mut call = a.call.clone(); + if pair { + call.push("m.len".into()); + } + call.push("m.err".into()); + let call = format!("x.{}({})", abi.symbol, call.join(", ")); + let mut body = Vec::new(); + match &pass { + RetPass::Void => { + body.push(format!("{call};")); + body.push("m.check();".into()); + } + pass => { + body.push(format!("const r = {call};")); + body.push("m.check();".into()); + let ty = f.ret.as_ref().expect("non-void"); + body.push(format!( + "return {};", + receive(ty, pass, &["r", "m.outLen()"], true) + )); + } + } + w.block(head(&abi.symbol), "},", |w| emit_staged(w, &a, &body)); + symbols.push(abi.symbol.clone()); + } + CallShape::Iterator(it) => { + let mut call = a.call.clone(); + call.push("m.err".into()); + let body = vec![ + format!("const r = x.{}({});", it.launch.symbol, call.join(", ")), + "m.check();".into(), + "return r;".into(), + ]; + w.block(head(&it.launch.symbol), "},", |w| emit_staged(w, &a, &body)); + symbols.push(it.launch.symbol.clone()); + + let protocol = it.protocol(f, prefix); + let pair = matches!( + protocol.elem, + RetPass::String | RetPass::Bytes | RetPass::Buffer + ); + let len = if pair { "m.len, " } else { "" }; + w.block(format!("{}: (h) => {{", it.next.symbol), "},", |w| { + w.line(format!( + "const has = x.{}(m.handle(h), m.item, {len}m.err);", + it.next.symbol + )); + w.line("m.check();"); + w.line("if (has === 0) return undefined;"); + match &protocol.elem { + RetPass::Direct => { + w.line(format!("return {};", item_read(direct_prim(&it.elem)))); + } + pass => { + w.line("const p = m.view().getUint32(m.item, true);"); + w.line(format!( + "return {};", + receive(&it.elem, pass, &["p", "m.outLen()"], true) + )); + } + } + }); + w.line(format!( + "{0}: (h) => x.{0}(m.handle(h)),", + it.destroy_symbol + )); + symbols.push(it.next.symbol.clone()); + symbols.push(it.destroy_symbol.clone()); + } + CallShape::Async(ab) => { + let pass = plan::ret_pass(f.ret.as_ref(), prefix); + let names: Vec = ab.callback_params[2..] + .iter() + .map(|p| p.name.clone()) + .collect(); + let slots: Vec<&str> = names.iter().map(String::as_str).collect(); + let convert = match &f.ret { + None => "() => undefined".to_string(), + Some(ty) => format!( + "({}) => {}", + names.join(", "), + receive(ty, &pass, &slots, true) + ), + }; + let mut call = a.call.clone(); + call.push("cb".into()); + call.push("ctx".into()); + let body = vec![format!( + "return m.launch({}, (cb, ctx) => x.{}({}), {convert});", + signature(&ab.callback_params, &CType::Void), + ab.launch.symbol, + call.join(", ") + )]; + w.block(head(&ab.launch.symbol), "},", |w| emit_staged(w, &a, &body)); + symbols.push(ab.launch.symbol.clone()); + } + } +} + +/// Emit `$vt_{tag}(m)`, the table functions of one callback interface's +/// vtable: each converts the raw slots into the values the adapter takes, +/// calls it, and converts the result back, reporting an exception through +/// `out_err` (code -4) instead of letting it unwind into the module. +fn emit_vtable(w: &mut CodeWriter, cb: &CallbackInterfaceBinding, prefix: &str) { + let protocol = cb.protocol(prefix); + w.block(format!("function $vt_{}(m) {{", cb.c_tag), "}", |w| { + w.block("return [", "];", |w| { + for (method, passes) in cb.methods.iter().zip(&protocol.method_args) { + let slots = &method.abi_params[1..method.abi_params.len() - 1]; + let names: Vec = (0..slots.len()).map(|i| format!("p{i}")).collect(); + let mut k = 0usize; + let args: Vec = method + .params + .iter() + .zip(passes) + .map(|(p, pass)| { + let s: Vec<&str> = names[k..k + p.abi.len()] + .iter() + .map(String::as_str) + .collect(); + k += p.abi.len(); + receive(&p.ty, pass, &s, false) + }) + .collect(); + let call = format!("m.adapter(ctx).{}({})", method.name, args.join(", ")); + let (ret, fallback) = match &method.ret { + None => (format!("{call};"), None), + Some(ty) => match direct_prim(ty) { + Prim::Bool => (format!("return {call} ? 1 : 0;"), Some("return 0;")), + Prim::I64 | Prim::U64 => (format!("return {call};"), Some("return 0n;")), + _ => (format!("return {call};"), Some("return 0;")), + }, + }; + let mut params = vec!["ctx".to_string()]; + params.extend(names.iter().cloned()); + params.push("err".into()); + w.line(format!( + "[{}, ({}) => {{", + signature(&method.abi_params, &method.abi_ret), + params.join(", ") + )); + w.scope(|w| { + w.line("try {"); + w.scope(|w| { + w.line(&ret); + }); + w.line("} catch (e) {"); + w.scope(|w| { + w.line("m.foreign(err, e);"); + if let Some(f) = fallback { + w.line(f); + } + }); + w.line("}"); + }); + w.line("}],"); + } + }); + }); + w.blank(); +} diff --git a/crates/weaveffi-gen/src/targets/wasm/mod.rs b/crates/weaveffi-gen/src/targets/wasm/mod.rs new file mode 100644 index 00000000..10af5ada --- /dev/null +++ b/crates/weaveffi-gen/src/targets/wasm/mod.rs @@ -0,0 +1,262 @@ +//! WebAssembly binding generator. +//! +//! Emits an npm package for a `wasm32-unknown-unknown` build of the library +//! (or, in Emscripten mode, an Emscripten build): the ES module (`index.js`, +//! `index.d.ts`, `runtime.js`) shared with the Node.js target through +//! the shared JavaScript layer, and its transport, the linear-memory glue +//! (`linear.js` plus one generated entry point per C symbol) that stages +//! arguments in the module's memory and installs async completions and +//! callback-interface vtables in its function table. `init()` loads the +//! module and checks its ABI revision and contract checksums before any +//! other export works. Implements [`LanguageBackend`]; the shared driver +//! bridges it into the generator pipeline. + +mod glue; +mod package; + +use camino::Utf8Path; +use serde::{Deserialize, Serialize}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; + +use crate::backend::{LanguageBackend, OutputFile}; +use crate::capabilities::TargetCapabilities; +use crate::package::{PackageContext, PackagedFile}; +use crate::platform::Platform; +use crate::targets::js; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; + +use self::glue::render_bind; +use self::package::{render_package_json, render_readme}; + +/// The fixed transport: loading, staging, and table functions. +const LINEAR_JS: &str = include_str!("runtime/linear.js"); + +/// Per-target configuration for [`WasmGenerator`]. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct WasmConfig { + /// npm package name (default: the package identity's name). + pub package_name: Option, + /// Target an Emscripten build instead of a `wasm32-unknown-unknown` + /// one: `init` then takes the initialized Emscripten module (or the + /// promise its `MODULARIZE` factory returns), binds its + /// underscore-prefixed exports, and installs table functions with + /// `addFunction`. + pub emscripten: bool, + /// Basename of the IDL the CLI was invoked with. + #[serde(skip)] + pub input_basename: Option, +} + +impl WasmConfig { + fn input_basename(&self) -> &str { + self.input_basename.as_deref().unwrap_or("api.yml") + } + + fn package_name(&self, identity: &Identity) -> String { + self.package_name + .clone() + .unwrap_or_else(|| identity.name.clone()) + } +} + +/// WebAssembly backend: an ES module over linear-memory glue. +pub struct WasmGenerator; + +/// The generated `index.js`. +fn render_index( + model: &BindingModel, + identity: &Identity, + emscripten: bool, + input_basename: &str, +) -> String { + let name = js::names::js_string(&identity.name); + let prefix = &model.prefix; + let (bind, symbols) = render_bind(model); + let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); + out.push_str(&js::render_imports(identity)); + let loader = if emscripten { + "$loadEmscripten" + } else { + "$loadWasm" + }; + out.push_str(&format!( + "import {{ {loader}, $unloaded }} from './linear.js';\n\n" + )); + out.push_str(&format!("let $raw = $unloaded({name});\n")); + out.push_str("// Handles are linear-memory addresses; object tokens are u64s.\n"); + out.push_str("const $token = Number;\n"); + out.push_str("let $loading = null;\n\n"); + let load = if emscripten { + let list: Vec = symbols.iter().map(|s| format!("'{s}'")).collect(); + out.push_str(&format!("const $SYMBOLS = [{}];\n\n", list.join(", "))); + out.push_str("/**\n"); + out.push_str(" * Adopt the initialized Emscripten module (or the promise its factory\n"); + out.push_str(" * returns) and check that it matches these bindings. Every other export\n"); + out.push_str(" * throws until the returned promise resolves.\n"); + out.push_str(" */\n"); + out.push_str("export function init(module) {\n"); + format!("$loadEmscripten(module, '{prefix}', $SYMBOLS)") + } else { + out.push_str("/**\n"); + out.push_str(&format!( + " * Load the WebAssembly module (by default `{}.wasm` next to this file, or\n", + identity.library + )); + out.push_str(&format!( + " * the file named by `{}` on Node.js) and check that it matches\n", + identity.library_env_var() + )); + out.push_str(" * these bindings. Every other export throws until the returned promise\n"); + out.push_str(" * resolves.\n"); + out.push_str(" */\n"); + out.push_str("export function init(source) {\n"); + format!( + "$loadWasm(source, '{prefix}', '{}', new URL('./{}.wasm', import.meta.url))", + identity.library_env_var(), + identity.library + ) + }; + out.push_str(&format!(" $loading ??= {load}\n")); + out.push_str(" .then((m) => {\n"); + out.push_str(" $raw = $bind(m);\n"); + out.push_str(" try {\n"); + out.push_str(&format!(" {}", js::render_verify(model, identity))); + out.push_str(" } catch (e) {\n"); + out.push_str(&format!(" $raw = $unloaded({name});\n")); + out.push_str(" throw e;\n"); + out.push_str(" }\n"); + out.push_str(" })\n"); + out.push_str(" .catch((e) => {\n"); + out.push_str(" $loading = null;\n"); + out.push_str(" throw e;\n"); + out.push_str(" });\n"); + out.push_str(" return $loading;\n"); + out.push_str("}\n\n"); + out.push_str(&bind); + out.push('\n'); + out.push_str(&js::render_api(model)); + out.push_str(&render_trailer(CommentStyle::DoubleSlash, "index.js")); + out +} + +/// The transport's own declarations in `index.d.ts`. +fn dts_extra(emscripten: bool) -> String { + let mut out = String::from( + "/**\n * Load the WebAssembly module and check that it matches these bindings.\n \ + * Every other export throws until the returned promise resolves.\n */\n", + ); + if emscripten { + out.push_str( + "export declare function init(module: object | Promise): Promise;\n", + ); + } else { + out.push_str( + "export declare function init(\n source?: string | URL | BufferSource | WebAssembly.Module | Response,\n): Promise;\n", + ); + } + out +} + +/// Every file of the generated package, as `(name, contents)` pairs. +fn render_files( + model: &BindingModel, + identity: &Identity, + config: &WasmConfig, + wasm: Option<&str>, +) -> Vec<(String, String)> { + let input = config.input_basename(); + let name = config.package_name(identity); + let modules: Vec = model.roots().map(js::names::module_name).collect(); + let mut linear = render_prelude(CommentStyle::DoubleSlash, input); + linear.push_str(LINEAR_JS); + linear.push('\n'); + linear.push_str(&render_trailer(CommentStyle::DoubleSlash, "linear.js")); + vec![ + ( + "index.js".into(), + render_index(model, identity, config.emscripten, input), + ), + ( + "index.d.ts".into(), + js::render_dts(model, identity, input, &dts_extra(config.emscripten)), + ), + ("runtime.js".into(), js::render_runtime(identity, input)), + ("linear.js".into(), linear), + ( + "package.json".into(), + render_package_json(identity, &name, wasm, input), + ), + ( + "README.md".into(), + render_readme(identity, &name, &modules, config.emscripten, input), + ), + ] +} + +impl LanguageBackend for WasmGenerator { + type Config = WasmConfig; + + fn name(&self) -> &'static str { + "wasm" + } + + /// Every feature is supported. Async completions and callback-interface + /// methods are JavaScript functions installed in the module's function + /// table. The target is single-threaded, so a callback only ever runs + /// while a call into the module is on the stack, and an async call + /// usually completes before its launcher returns. + fn capabilities(&self, _config: &Self::Config) -> TargetCapabilities { + TargetCapabilities::full() + } + + fn files( + &self, + api: &ResolvedApi, + model: &BindingModel, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Vec { + let dir = out_dir.join("wasm"); + render_files(model, api.identity(), config, None) + .into_iter() + .map(|(name, contents)| OutputFile::new(dir.join(name), contents)) + .collect() + } + + /// The npm package with the prebuilt `wasm32-unknown-unknown` module + /// bundled as `{library}.wasm`, where `init()` finds it by default. + /// Emscripten mode ships glue only (the consumer builds the module), and + /// without a wasm32 binary there is nothing to package. + fn package( + &self, + api: &ResolvedApi, + model: &BindingModel, + ctx: &PackageContext, + out_dir: &Utf8Path, + config: &Self::Config, + ) -> Option> { + let dir = out_dir.join("wasm"); + let identity = api.identity(); + let wasm_name = format!("{}.wasm", identity.library); + let binary = if config.emscripten { + None + } else { + Some(ctx.binaries.get(Platform::Wasm32)?) + }; + let mut files: Vec = + render_files(model, identity, config, binary.map(|_| wasm_name.as_str())) + .into_iter() + .map(|(name, contents)| PackagedFile::text(dir.join(name), contents)) + .collect(); + if let Some(b) = binary { + files.push(PackagedFile::copy(dir.join(&wasm_name), b.source.clone())); + } + Some(files) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/weaveffi-gen/src/targets/wasm/package.rs b/crates/weaveffi-gen/src/targets/wasm/package.rs new file mode 100644 index 00000000..c6d9d7ee --- /dev/null +++ b/crates/weaveffi-gen/src/targets/wasm/package.rs @@ -0,0 +1,63 @@ +//! `package.json` and the README. + +use weaveffi_model::pkg::Identity; + +use crate::manifest::JsonValue; +use crate::targets::js::npm_metadata; +use crate::utils::{render_prelude, render_trailer, CommentStyle}; + +/// Render `package.json`. `wasm` is the bundled module's file name, when +/// the package carries one. +pub(crate) fn render_package_json( + identity: &Identity, + name: &str, + wasm: Option<&str>, + input_basename: &str, +) -> String { + let mut files = vec!["index.js", "index.d.ts", "runtime.js", "linear.js"]; + if let Some(w) = wasm { + files.push(w); + } + npm_metadata(identity, name, input_basename) + .entry("files", JsonValue::str_array(files)) + .render() +} + +/// Render the README. +pub(crate) fn render_readme( + identity: &Identity, + name: &str, + modules: &[String], + emscripten: bool, + input_basename: &str, +) -> String { + let lib = &identity.library; + let env = identity.library_env_var(); + let mut out = render_prelude(CommentStyle::Xml, input_basename); + out.push_str(&format!( + "# {name}\n\n{}\n\n", + identity.description_or_default() + )); + let imports = modules.join(", "); + if emscripten { + out.push_str(&format!( + "WebAssembly bindings for an Emscripten build of the `{lib}` library. Build \ + the module with `-sMODULARIZE -sALLOW_TABLE_GROWTH -sWASM_BIGINT \ + -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8` and pass it to `init`:\n\n\ + ```js\nimport Module from './{lib}.js';\nimport {{ init, {imports} }} from '{name}';\n\n\ + await init(Module());\n```\n\n" + )); + } else { + out.push_str(&format!( + "WebAssembly bindings for a `wasm32-unknown-unknown` build of the `{lib}` \ + library, linked with `--export-table --growable-table` (for example \ + `RUSTFLAGS=\"-C link-arg=--export-table -C link-arg=--growable-table\"`). \ + `init()` loads `{lib}.wasm` from next to this package, or from the path in \ + `{env}` on Node.js; pass a URL, bytes, or a compiled module to load it from \ + elsewhere:\n\n```js\nimport {{ init, {imports} }} from '{name}';\n\n\ + await init();\n```\n\n" + )); + } + out.push_str(&render_trailer(CommentStyle::Xml, "README.md")); + out +} diff --git a/crates/weaveffi-gen/src/targets/wasm/runtime/linear.js b/crates/weaveffi-gen/src/targets/wasm/runtime/linear.js new file mode 100644 index 00000000..21a432d8 --- /dev/null +++ b/crates/weaveffi-gen/src/targets/wasm/runtime/linear.js @@ -0,0 +1,394 @@ +// The WebAssembly transport: loading the module, staging arguments in its +// linear memory, reading results and errors back, and installing JavaScript +// functions in its function table (async completions and callback +// interface vtables). The generated `index.js` builds one native entry point +// per C symbol on top of this. + +import { $Fault } from './runtime.js'; + +const $enc = new TextEncoder(); +const $dec = new TextDecoder(); + +function $expected(what, v) { + return new TypeError(`expected ${what}, got ${v === null ? 'null' : typeof v}`); +} + +function $bigint(v, bits, what) { + let b; + if (typeof v === 'bigint') { + b = v; + } else if (typeof v === 'number' && Number.isInteger(v)) { + b = BigInt(v); + } else { + throw $expected('a bigint', v); + } + if (bits(64, b) !== b) throw new RangeError(`bigint does not fit in ${what} 64-bit integer`); + return b; +} + +/** + * One loaded module's linear memory and runtime symbols. `x` holds the + * module's exports by C symbol name; `memory()` returns the current + * `ArrayBuffer`; `fnptr(params, results, sig, fn)` installs `fn` in the + * function table and returns its index. + */ +export class $Linear { + constructor(x, prefix, memory, fnptr) { + this.x = x; + this.memory = memory; + this.fnptr = fnptr; + this.allocFn = x[`${prefix}_alloc`]; + this.deallocFn = x[`${prefix}_dealloc`]; + this.freeBytesFn = x[`${prefix}_free_bytes`]; + this.errorClearFn = x[`${prefix}_error_clear`]; + this.errorFreeFn = x[`${prefix}_error_free`]; + this.errorSetFn = x[`${prefix}_error_set`]; + if (typeof this.allocFn !== 'function' || typeof this.deallocFn !== 'function') { + throw new Error(`the WebAssembly module does not export ${prefix}_alloc and ${prefix}_dealloc`); + } + // One scratch block reused by every call: the error slot (16 bytes), + // the out_len slot (4), and the iterator out_item slot (8). The producer + // only writes them as a call returns, so a call made from a callback in + // the middle of another call can reuse them safely. + const scratch = this.allocFn(32); + this.bytes().fill(0, scratch, scratch + 32); + this.err = scratch; + this.len = scratch + 16; + this.item = scratch + 24; + this.callbacks = new Map(); + this.nextCallback = 1; + this.pending = new Map(); + this.nextPending = 1; + this.completions = new Map(); + this.vtables = new Map(); + } + + bytes() { + const buffer = this.memory(); + if (buffer !== this.buffer) { + this.buffer = buffer; + this.u8 = new Uint8Array(buffer); + this.dv = new DataView(buffer); + } + return this.u8; + } + + view() { + this.bytes(); + return this.dv; + } + + // Argument checks and conversions for by-value slots. + num(v) { + if (typeof v !== 'number') throw $expected('a number', v); + return v; + } + + i64(v) { + return $bigint(v, BigInt.asIntN, 'a signed'); + } + + u64(v) { + return $bigint(v, BigInt.asUintN, 'an unsigned'); + } + + bool(v) { + if (typeof v !== 'boolean') throw $expected('a boolean', v); + return v ? 1 : 0; + } + + handle(v) { + if (typeof v !== 'number' || v === 0) throw $expected('an object handle', v); + return v; + } + + handleOpt(v) { + return v === null || v === undefined ? 0 : this.handle(v); + } + + // Staging: copy a string or bytes into linear memory for one call. A + // staged value is [ptr, len, size]; release it with unstage(). + str(s) { + if (typeof s !== 'string') throw $expected('a string', s); + if (s.length === 0) return [0, 0, 0]; + const size = s.length * 3; + const ptr = this.allocFn(size); + const { written } = $enc.encodeInto(s, this.bytes().subarray(ptr, ptr + size)); + return [ptr, written, size]; + } + + data(b) { + if (!(b instanceof Uint8Array)) throw $expected('a Uint8Array', b); + if (b.length === 0) return [0, 0, 0]; + const ptr = this.allocFn(b.length); + this.bytes().set(b, ptr); + return [ptr, b.length, b.length]; + } + + unstage(staged) { + if (staged !== null && staged[2] !== 0) this.deallocFn(staged[0], staged[2]); + } + + // Results. + outLen() { + return this.view().getUint32(this.len, true); + } + + readStr(ptr, len) { + return len === 0 ? '' : $dec.decode(this.bytes().subarray(ptr, ptr + len)); + } + + readData(ptr, len) { + return len === 0 ? new Uint8Array(0) : this.bytes().slice(ptr, ptr + len); + } + + takeStr(ptr, len) { + const s = this.readStr(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return s; + } + + takeData(ptr, len) { + const b = this.readData(ptr, len); + if (ptr !== 0) this.freeBytesFn(ptr, len); + return b; + } + + cstr(ptr) { + if (ptr === 0) return ''; + const u8 = this.bytes(); + let end = ptr; + while (u8[end] !== 0) end++; + return $dec.decode(u8.subarray(ptr, end)); + } + + fault(errPtr) { + const dv = this.view(); + const code = dv.getInt32(errPtr, true); + if (code === 0) return null; + const payloadPtr = dv.getUint32(errPtr + 8, true); + const payloadLen = dv.getUint32(errPtr + 12, true); + return new $Fault( + code, + this.cstr(dv.getUint32(errPtr + 4, true)), + payloadPtr === 0 ? null : this.bytes().slice(payloadPtr, payloadPtr + payloadLen), + ); + } + + /** Throw the error the last call wrote to the scratch error slot. */ + check() { + if (this.view().getInt32(this.err, true) === 0) return; + const fault = this.fault(this.err); + this.errorClearFn(this.err); + throw fault; + } + + // Callback interfaces: implementations live in a table keyed by the + // integer passed as `ctx`, until the producer calls the vtable's `free`. + register(adapter) { + const id = this.nextCallback++; + this.callbacks.set(id, adapter); + return id; + } + + adapter(id) { + const adapter = this.callbacks.get(id); + if (adapter === undefined) throw new Error('callback implementation was already released'); + return adapter; + } + + unregister(id) { + this.callbacks.delete(id); + } + + /** Report a callback implementation's exception through `errPtr` (code -4). */ + foreign(errPtr, e) { + const message = e instanceof Error ? e.message || e.name : String(e); + const bytes = $enc.encode(message); + const ptr = this.allocFn(bytes.length + 1); + const u8 = this.bytes(); + u8.set(bytes, ptr); + u8[ptr + bytes.length] = 0; + this.errorSetFn(errPtr, -4, ptr); + this.deallocFn(ptr, bytes.length + 1); + } + + /** + * The static vtable of one callback interface: one table entry per + * method (`methods()` returns each as `[params, results, sig, fn]`), then + * `free`. Built on first use. + */ + vtable(name, methods) { + let ptr = this.vtables.get(name); + if (ptr !== undefined) return ptr; + const entries = [...methods(), [['i32'], [], 'vi', (ctx) => this.unregister(ctx)]]; + ptr = this.allocFn(entries.length * 4); + const dv = this.view(); + entries.forEach(([params, results, sig, fn], i) => { + dv.setUint32(ptr + i * 4, this.fnptr(params, results, sig, fn), true); + }); + this.vtables.set(name, ptr); + return ptr; + } + + /** + * Launch an async call. `call(callback, context)` invokes the launcher; + * the completion (one table entry per signature, shared by every call) + * settles the returned promise, converting the result slots with + * `convert`. On this single-threaded target the completion normally + * fires before the launcher returns. + */ + launch(params, results, sig, call, convert) { + let callback = this.completions.get(sig); + if (callback === undefined) { + const complete = (ctx, errPtr, ...result) => this.complete(ctx, errPtr, result); + callback = this.fnptr(params, results, sig, complete); + this.completions.set(sig, callback); + } + const id = this.nextPending++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject, convert }); + try { + call(callback, id); + } catch (e) { + this.pending.delete(id); + reject(e); + } + }); + } + + complete(id, errPtr, result) { + const pending = this.pending.get(id); + if (pending === undefined) return; + this.pending.delete(id); + try { + const fault = errPtr === 0 ? null : this.fault(errPtr); + if (errPtr !== 0) this.errorFreeFn(errPtr); + if (fault !== null) { + pending.reject(fault); + } else { + pending.resolve(pending.convert(...result)); + } + } catch (e) { + pending.reject(e); + } + } +} + +/** A stand-in for the native entry points until `init()` has finished. */ +export function $unloaded(library) { + return new Proxy( + {}, + { + get() { + throw new Error(`${library}: call init() before using the bindings`); + }, + }, + ); +} + +const $valtype = { i32: 0x7f, i64: 0x7e, f32: 0x7d, f64: 0x7c }; + +function $leb(n) { + const out = []; + do { + let b = n & 0x7f; + n >>>= 7; + if (n !== 0) b |= 0x80; + out.push(b); + } while (n !== 0); + return out; +} + +function $section(id, body) { + return [id, ...$leb(body.length), ...body]; +} + +/** + * Turn a JavaScript function into a WebAssembly function with the given + * signature: `WebAssembly.Function` where the engine has it, otherwise a + * tiny module that imports `fn` and re-exports it. + */ +function $wasmFunction(params, results, fn) { + if (typeof WebAssembly.Function === 'function') { + return new WebAssembly.Function({ parameters: params, results }, fn); + } + const type = [0x60, ...$leb(params.length), ...params.map((t) => $valtype[t])]; + type.push(...$leb(results.length), ...results.map((t) => $valtype[t])); + const bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + ...$section(1, [1, ...type]), + ...$section(2, [1, 1, 0x65, 1, 0x66, 0x00, 0x00]), + ...$section(7, [1, 1, 0x66, 0x00, 0x00]), + ]); + const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), { e: { f: fn } }); + return instance.exports.f; +} + +async function $bytesOf(source, envVar, fallback) { + if (source === undefined || source === null) { + const env = typeof process === 'object' && process.env ? process.env[envVar] : undefined; + if (env) { + const { readFile } = await import('node:fs/promises'); + return readFile(env); + } + source = fallback; + } + if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { + return source; + } + if (typeof Response === 'function' && source instanceof Response) { + return source.arrayBuffer(); + } + const url = source instanceof URL ? source : new URL(String(source), fallback); + if (url.protocol === 'file:') { + const { readFile } = await import('node:fs/promises'); + return readFile(url); + } + const response = await fetch(url); + if (!response.ok) throw new Error(`failed to fetch ${url}: ${response.status}`); + return response.arrayBuffer(); +} + +/** + * Instantiate a `wasm32-unknown-unknown` module from `source` (a URL, a + * path, bytes, a `Response`, or a compiled module). Without a source it + * reads the file named by the `envVar` environment variable (on Node.js), + * else `fallback`. The module must export its memory and a growable + * function table (`__indirect_function_table`). + */ +export async function $loadWasm(source, prefix, envVar, fallback) { + const bytes = await $bytesOf(source, envVar, fallback); + const module = bytes instanceof WebAssembly.Module ? bytes : await WebAssembly.compile(bytes); + const { exports } = await WebAssembly.instantiate(module, {}); + const table = exports.__indirect_function_table; + const fnptr = (params, results, sig, fn) => { + if (!(table instanceof WebAssembly.Table)) { + throw new Error('the WebAssembly module does not export __indirect_function_table (link with --export-table --growable-table)'); + } + const index = table.grow(1); + table.set(index, $wasmFunction(params, results, fn)); + return index; + }; + return new $Linear(exports, prefix, () => exports.memory.buffer, fnptr); +} + +/** + * Adopt an initialized Emscripten module (or the promise its `MODULARIZE` + * factory returns). Its exports carry a leading underscore, its memory is + * read through `HEAPU8`, and table entries are installed with + * `addFunction`, so it must be linked with `-sALLOW_TABLE_GROWTH`, + * `-sWASM_BIGINT`, and `-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`. + */ +export async function $loadEmscripten(module, prefix, symbols) { + const m = await module; + const x = {}; + for (const symbol of symbols) x[symbol] = m[`_${symbol}`]; + const fnptr = (params, results, sig, fn) => { + if (typeof m.addFunction !== 'function') { + throw new Error('the Emscripten module does not export addFunction (link with -sALLOW_TABLE_GROWTH and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8)'); + } + return m.addFunction(fn, sig); + }; + return new $Linear(x, prefix, () => m.HEAPU8.buffer, fnptr); +} diff --git a/crates/weaveffi-gen/src/targets/wasm/tests.rs b/crates/weaveffi-gen/src/targets/wasm/tests.rs new file mode 100644 index 00000000..40d46d3c --- /dev/null +++ b/crates/weaveffi-gen/src/targets/wasm/tests.rs @@ -0,0 +1,192 @@ +use super::*; +use crate::platform::BinarySet; +use crate::targets::js::test_api; + +fn index(config: &WasmConfig) -> String { + let api = test_api("acme-kv"); + let model = BindingModel::build(&api); + WasmGenerator + .files(&api, &model, Utf8Path::new("out"), config) + .into_iter() + .find(|f| f.path.file_name() == Some("index.js")) + .expect("index.js") + .contents +} + +fn has(src: &str, needle: &str) { + assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); +} + +#[test] +fn files_are_named_for_the_package() { + let api = test_api("acme-kv"); + let model = BindingModel::build(&api); + let out = WasmGenerator.files(&api, &model, Utf8Path::new("out"), &WasmConfig::default()); + let names: Vec<&str> = out.iter().filter_map(|f| f.path.file_name()).collect(); + assert_eq!( + names, + [ + "index.js", + "index.d.ts", + "runtime.js", + "linear.js", + "package.json", + "README.md" + ] + ); + let manifest = &out[4].contents; + has(manifest, "\"name\": \"acme-kv\","); + has(manifest, "\"type\": \"module\","); + has(&out[1].contents, "export declare function init("); +} + +#[test] +fn init_loads_and_verifies_the_module() { + let js = index(&WasmConfig::default()); + has(&js, "import { $loadWasm, $unloaded } from './linear.js';"); + has(&js, "let $raw = $unloaded('acme-kv');"); + has( + &js, + "$loading ??= $loadWasm(source, 'acme_kv', 'ACME_KV_LIBRARY', new URL('./acme_kv.wasm', import.meta.url))", + ); + has(&js, "$verify($raw, 'acme-kv', 'acme_kv', 3, [['kv', 0x"); + assert!(!js.contains("weaveffi_"), "{js}"); +} + +#[test] +fn glue_stages_strings_and_reuses_the_error_slot() { + let js = index(&WasmConfig::default()); + has( + &js, + "acme_kv_kv_Store_get: (self, a0) => {\n let s0 = null;\n try {\n s0 = m.str(a0);\n const r = x.acme_kv_kv_Store_get(m.handle(self), s0[0], s0[1], m.len, m.err);\n m.check();\n return m.takeData(r, m.outLen());\n } finally {\n m.unstage(s0);\n }\n },", + ); + has( + &js, + "const r = x.acme_kv_kv_widen(m.i64(a0), m.u64(a1), m.err);", + ); + has(&js, "return BigInt.asUintN(64, r);"); + has( + &js, + "const r = x.acme_kv_kv_delete(m.handleOpt(a0), m.err);", + ); + has(&js, "return r !== 0;"); +} + +#[test] +fn async_and_callbacks_install_table_functions() { + let js = index(&WasmConfig::default()); + has( + &js, + "return m.launch(['i32', 'i32', 'i64'], [], 'viij', (cb, ctx) => x.acme_kv_kv_Store_wait(m.handle(self), m.i64(a0), m.handleOpt(token), cb, ctx), (result) => result);", + ); + has(&js, "(result) => result === 0 ? null : result"); + has( + &js, + "m.register(a0), m.vtable('acme_kv_kv_Listener', () => $vt_acme_kv_kv_Listener(m))", + ); + has( + &js, + "[['i32', 'i32', 'i32', 'i64', 'i32'], ['i32'], 'iiiiji', (ctx, p0, p1, p2, err) => {", + ); + has( + &js, + "return m.adapter(ctx).on_message(m.readStr(p0, p1), BigInt.asUintN(64, p2)) ? 1 : 0;", + ); + has(&js, "m.foreign(err, e);"); + has( + &js, + "m.adapter(ctx).on_bundle(m.readData(p0, p1), p2, p3 === 0 ? null : p3);", + ); +} + +#[test] +fn iterators_read_the_item_slot() { + let js = index(&WasmConfig::default()); + has( + &js, + "acme_kv_kv_Store_ScanIterator_next: (h) => {\n const has = x.acme_kv_kv_Store_ScanIterator_next(m.handle(h), m.item, m.err);\n m.check();\n if (has === 0) return undefined;\n const p = m.view().getUint32(m.item, true);\n return p;\n },", + ); + has( + &js, + "const has = x.acme_kv_kv_ShapesIterator_next(m.handle(h), m.item, m.len, m.err);", + ); + has(&js, "return m.takeData(p, m.outLen());"); +} + +#[test] +fn emscripten_mode_binds_underscored_exports() { + let js = index(&WasmConfig { + emscripten: true, + ..WasmConfig::default() + }); + has( + &js, + "import { $loadEmscripten, $unloaded } from './linear.js';", + ); + has( + &js, + "const $SYMBOLS = ['acme_kv_abi_version', 'acme_kv_alloc',", + ); + has(&js, "'acme_kv_kv_Store_wait'"); + has( + &js, + "$loading ??= $loadEmscripten(module, 'acme_kv', $SYMBOLS)", + ); +} + +#[test] +fn package_bundles_the_wasm_module() { + let api = test_api("kv"); + let model = BindingModel::build(&api); + let mut binaries = BinarySet::new("kv"); + binaries.insert(Platform::Wasm32, "target/kv.wasm"); + let ctx = PackageContext { + binaries: &binaries, + input_basename: None, + }; + let files = WasmGenerator + .package( + &api, + &model, + &ctx, + Utf8Path::new("out"), + &WasmConfig::default(), + ) + .expect("a wasm32 binary packages"); + let wasm = files + .iter() + .find(|f| f.path == "out/wasm/kv.wasm") + .expect("bundled module"); + assert!(wasm.is_binary()); + let PackagedFile { content, .. } = files + .iter() + .find(|f| f.path == "out/wasm/package.json") + .expect("manifest"); + let crate::package::FileContent::Text(manifest) = content else { + panic!("manifest is text") + }; + has(manifest, "\"kv.wasm\""); + + let none = BinarySet::new("kv"); + let ctx = PackageContext { + binaries: &none, + input_basename: None, + }; + assert!(WasmGenerator + .package( + &api, + &model, + &ctx, + Utf8Path::new("out"), + &WasmConfig::default() + ) + .is_none()); + let emscripten = WasmConfig { + emscripten: true, + ..WasmConfig::default() + }; + let glue = WasmGenerator + .package(&api, &model, &ctx, Utf8Path::new("out"), &emscripten) + .expect("Emscripten mode ships glue only"); + assert!(glue.iter().all(|f| !f.is_binary())); +} diff --git a/crates/weaveffi-core/src/utils.rs b/crates/weaveffi-gen/src/utils.rs similarity index 77% rename from crates/weaveffi-core/src/utils.rs rename to crates/weaveffi-gen/src/utils.rs index edd122ca..cc04bf60 100644 --- a/crates/weaveffi-core/src/utils.rs +++ b/crates/weaveffi-gen/src/utils.rs @@ -70,51 +70,6 @@ pub fn render_json_prelude(input_basename: &str) -> String { ) } -/// Runtime symbols (functions and types) that consumer code links against from -/// the `weaveffi-abi` runtime: the `weaveffi_error` and `weaveffi_cancel_token` -/// types and every `#[no_mangle]` entry point that -/// `weaveffi_abi::export_runtime!` emits into the consumer cdylib. -/// -/// Generators that emit C/C++ headers use this list to produce -/// `#define {prefix}_{name} weaveffi_{name}` aliases at the top of the header -/// when a non-default `c_prefix` is configured, so consumer code can refer to -/// runtime helpers by the prefixed name while still linking against the -/// canonical `weaveffi_*` symbols supplied by `weaveffi-abi`. -/// -/// This list must stay in lockstep with `export_runtime!`: every entry has to -/// be a real exported C symbol (or a public C type), otherwise the generated -/// `#define` would alias a name that does not exist. `alloc`/`dealloc` are -/// absent because they're exported by wasm32 producers only. -pub const ABI_RUNTIME_SYMBOLS: &[&str] = &[ - "error", - "abi_version", - "error_set", - "error_clear", - "error_free", - "free_string", - "free_bytes", - "cancel_token", - "cancel_token_create", - "cancel_token_cancel", - "cancel_token_is_cancelled", - "cancel_token_destroy", -]; - -/// Render a `#define {prefix}_{name} weaveffi_{name}` block for runtime ABI -/// symbols. Returns an empty string when `prefix == "weaveffi"`. -pub fn render_abi_prefix_aliases(prefix: &str) -> String { - if prefix == "weaveffi" { - return String::new(); - } - let mut out = String::new(); - out.push_str("/* Aliases for weaveffi-abi runtime symbols */\n"); - for sym in ABI_RUNTIME_SYMBOLS { - out.push_str(&format!("#define {prefix}_{sym} weaveffi_{sym}\n")); - } - out.push('\n'); - out -} - /// Build the wrapper function name exposed to the foreign language. /// /// When `strip_module_prefix` is `true`, returns just `func`. @@ -199,20 +154,6 @@ mod tests { ); } - #[test] - fn abi_prefix_aliases_default_is_empty() { - assert!(render_abi_prefix_aliases("weaveffi").is_empty()); - } - - #[test] - fn abi_prefix_aliases_custom_lists_every_symbol() { - let out = render_abi_prefix_aliases("myffi"); - for sym in ABI_RUNTIME_SYMBOLS { - let line = format!("#define myffi_{sym} weaveffi_{sym}"); - assert!(out.contains(&line), "missing alias `{line}` in:\n{out}"); - } - } - #[test] fn prelude_double_slash_carries_required_phrases() { let p = render_prelude(CommentStyle::DoubleSlash, "calc.yml"); diff --git a/crates/weaveffi-gen/tests/c_buffer.rs b/crates/weaveffi-gen/tests/c_buffer.rs new file mode 100644 index 00000000..eb39a910 --- /dev/null +++ b/crates/weaveffi-gen/tests/c_buffer.rs @@ -0,0 +1,320 @@ +//! Checks for the C target's `{library}_buffer.h` value-buffer helper +//! header: which files the generator emits, the shapes it renders, and (when +//! a C compiler is available) that its codecs round-trip values and reject +//! malformed buffers. + +use std::process::Command; + +use camino::Utf8Path; +use weaveffi_gen::backend::LanguageBackend; +use weaveffi_gen::targets::c::{CConfig, CGenerator}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::parse::parse_api_str; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; +use weaveffi_model::validate::validate_api; + +const SHOP: &str = r#" +version: "0.10.0" +modules: + - name: shop + enums: + - name: Size + variants: + - { name: Small, value: 1 } + - { name: Large, value: 2 } + - name: Price + variants: + - { name: Free, value: 0 } + - name: Fixed + value: 1 + fields: + - { name: cents, type: i64 } + - name: Note + value: 2 + fields: + - { name: text, type: string } + structs: + - name: Item + fields: + - { name: name, type: string } + - { name: size, type: Size } + - { name: tags, type: "[string]" } + - { name: stock, type: "{string:i32}" } + - { name: discount, type: "i32?" } + - { name: price, type: Price } + - { name: blob, type: bytes } + - { name: matrix, type: "[[u8]]" } + errors: + name: ShopError + codes: + - name: OutOfStock + code: 1 + message: "out of stock" + fields: + - { name: missing, type: "[string]" } + functions: + - name: echo + params: + - { name: item, type: Item } + return: "Item?" + throws: true +"#; + +const PLAIN: &str = r#" +version: "0.10.0" +modules: + - name: math + functions: + - name: add + params: + - { name: a, type: i32 } + - { name: b, type: i32 } + return: i32 +"#; + +fn load(yaml: &str, name: &str) -> ResolvedApi { + let api = parse_api_str(yaml, "yaml").expect("parse"); + validate_api(api, None) + .expect("validate") + .with_identity(Identity::named(name)) +} + +fn generate(api: &ResolvedApi, config: &CConfig, dir: &Utf8Path) -> Vec<(String, String)> { + let model = BindingModel::build(api); + CGenerator + .files(api, &model, dir, config) + .into_iter() + .map(|f| { + let name = f.path.file_name().unwrap_or_default().to_string(); + (name, f.contents) + }) + .collect() +} + +fn buffer_header(yaml: &str, name: &str) -> String { + let api = load(yaml, name); + let files = generate(&api, &CConfig::default(), Utf8Path::new("out")); + files + .into_iter() + .find(|(n, _)| n == &format!("{name}_buffer.h")) + .map(|(_, c)| c) + .expect("buffer header emitted") +} + +#[test] +fn emits_the_helper_header_only_when_buffers_cross_the_abi() { + let api = load(SHOP, "shop"); + let names: Vec = generate(&api, &CConfig::default(), Utf8Path::new("out")) + .into_iter() + .map(|(n, _)| n) + .collect(); + assert_eq!(names, ["shop.h", "shop_buffer.h"]); + + let off = CConfig { + buffer_helpers: false, + ..CConfig::default() + }; + let names: Vec = generate(&api, &off, Utf8Path::new("out")) + .into_iter() + .map(|(n, _)| n) + .collect(); + assert_eq!(names, ["shop.h"]); + + let plain = load(PLAIN, "math"); + let names: Vec = generate(&plain, &CConfig::default(), Utf8Path::new("out")) + .into_iter() + .map(|(n, _)| n) + .collect(); + assert_eq!(names, ["math.h"]); +} + +#[test] +fn config_defaults_to_helpers_on_and_rejects_unknown_keys() { + let cfg: CConfig = serde_json::from_str("{}").expect("empty config"); + assert!(cfg.buffer_helpers); + let cfg: CConfig = serde_json::from_str(r#"{"buffer_helpers": false}"#).expect("flag"); + assert!(!cfg.buffer_helpers); + assert!(serde_json::from_str::(r#"{"prefix": "x"}"#).is_err()); +} + +#[test] +fn renders_structs_unions_and_payloads() { + let h = buffer_header(SHOP, "shop"); + assert!(h.contains("#include \"shop.h\"")); + assert!(h.contains("struct shop_shop_Item {\n shop_str name;\n shop_shop_Size size;")); + assert!(h.contains(" int32_t* discount;\n")); + assert!(h.contains("struct shop_list_string {\n shop_str* items;\n size_t len;\n};")); + assert!(h.contains("struct shop_map_string_i32 {\n shop_str* keys;\n int32_t* values;")); + assert!(h.contains(" shop_shop_Price_Tag tag;\n union {")); + assert!(h.contains(" struct {\n int64_t cents;\n } Fixed;")); + assert!(h.contains("struct shop_shop_ShopError_OutOfStock_payload {")); + assert!(h.contains( + "static inline bool shop_shop_ShopError_OutOfStock_payload_decode(const uint8_t* ptr, size_t len, shop_shop_ShopError_OutOfStock_payload* out);" + )); + assert!(h.contains( + "static inline void shop_opt_shop_Item_write(shop_writer* w, const shop_shop_Item* v);" + )); + // The price union is defined before the record that embeds it. + let price = h.find("struct shop_shop_Price {").expect("price struct"); + let item = h.find("struct shop_shop_Item {").expect("item struct"); + assert!(price < item); + // Nothing is branded after the tool. + assert!(!h.contains("weaveffi_")); +} + +/// A C program that encodes an `Item`, decodes it back, and checks every +/// field (including a string with an interior NUL), then feeds the decoder +/// malformed buffers. +const ROUNDTRIP_C: &str = r#" +#include +#include +#include "shop_buffer.h" + +int main(void) { + shop_str tags[2] = {{"a\0b", 3}, {"", 0}}; + shop_str keys[1] = {{"k", 1}}; + int32_t counts[1] = {-7}; + int32_t discount = 15; + uint8_t row0[2] = {1, 2}; + shop_bytes rows[2] = {{row0, 2}, {NULL, 0}}; + shop_shop_Item item; + memset(&item, 0, sizeof item); + item.name = shop_str_of("caf\xC3\xA9"); + item.size = shop_shop_Size_Large; + item.tags.items = tags; + item.tags.len = 2; + item.stock.keys = keys; + item.stock.values = counts; + item.stock.len = 1; + item.discount = &discount; + item.price.tag = shop_shop_Price_Note; + item.price.as.Note.text = shop_str_of("half"); + item.blob = shop_bytes_of(row0, 2); + item.matrix.items = rows; + item.matrix.len = 2; + + shop_writer w; + memset(&w, 0, sizeof w); + shop_opt_shop_Item_write(&w, &item); + assert(!w.failed); + + shop_shop_Item* back = NULL; + assert(shop_opt_shop_Item_decode(w.ptr, w.len, &back)); + assert(back != NULL); + assert(back->name.len == 5 && memcmp(back->name.ptr, "caf\xC3\xA9", 5) == 0); + assert(back->name.ptr[5] == '\0'); + assert(back->size == shop_shop_Size_Large); + assert(back->tags.len == 2 && back->tags.items[0].len == 3); + assert(memcmp(back->tags.items[0].ptr, "a\0b", 3) == 0); + assert(back->tags.items[1].len == 0 && back->tags.items[1].ptr[0] == '\0'); + assert(back->stock.len == 1 && back->stock.values[0] == -7); + assert(back->discount != NULL && *back->discount == 15); + assert(back->price.tag == shop_shop_Price_Note); + assert(strcmp(back->price.as.Note.text.ptr, "half") == 0); + assert(back->blob.len == 2 && back->blob.ptr[1] == 2); + assert(back->matrix.len == 2 && back->matrix.items[0].len == 2 && back->matrix.items[1].len == 0); + shop_opt_shop_Item_free(&back); + assert(back == NULL); + + /* Truncation anywhere fails cleanly and leaves nothing allocated. */ + for (size_t cut = 0; cut < w.len; cut++) { + shop_shop_Item* partial = NULL; + assert(!shop_opt_shop_Item_decode(w.ptr, cut, &partial)); + assert(partial == NULL); + } + /* Trailing bytes are rejected. */ + shop_writer_put_u8(&w, 0); + assert(!shop_opt_shop_Item_decode(w.ptr, w.len, &back)); + shop_writer_free(&w); + + /* An absent optional, a bad presence byte, a bad tag, and a count the + buffer can't hold. */ + const uint8_t absent[1] = {0}; + assert(shop_opt_shop_Item_decode(absent, 1, &back) && back == NULL); + const uint8_t bad_flag[1] = {2}; + assert(!shop_opt_shop_Item_decode(bad_flag, 1, &back)); + const uint8_t bad_tag[4] = {9, 0, 0, 0}; + shop_shop_Price price; + assert(!shop_shop_Price_decode(bad_tag, 4, &price)); + const uint8_t huge[4] = {0xff, 0xff, 0xff, 0x7f}; + shop_list_string list; + assert(!shop_list_string_decode(huge, 4, &list)); + assert(list.items == NULL && list.len == 0); + + /* Error payloads decode through their own struct. */ + shop_writer pw; + memset(&pw, 0, sizeof pw); + shop_shop_ShopError_OutOfStock_payload payload; + memset(&payload, 0, sizeof payload); + payload.missing.items = tags; + payload.missing.len = 1; + shop_shop_ShopError_OutOfStock_payload_write(&pw, &payload); + shop_shop_ShopError_OutOfStock_payload decoded; + assert(shop_shop_ShopError_OutOfStock_payload_decode(pw.ptr, pw.len, &decoded)); + assert(decoded.missing.len == 1 && decoded.missing.items[0].len == 3); + shop_shop_ShopError_OutOfStock_payload_free(&decoded); + shop_writer_free(&pw); + + puts("ok"); + return 0; +} +"#; + +fn have(tool: &str) -> bool { + Command::new(tool) + .arg("--version") + .output() + .is_ok_and(|o| o.status.success()) +} + +#[test] +fn codecs_round_trip_in_c_and_cpp() { + let compilers = [("cc", "c", "-std=c11"), ("c++", "cpp", "-std=c++17")]; + if !compilers.iter().any(|(cc, _, _)| have(cc)) { + eprintln!("no C compiler found; skipping"); + return; + } + let tmp = tempfile::tempdir().expect("tempdir"); + let dir = Utf8Path::from_path(tmp.path()).expect("utf8"); + let api = load(SHOP, "shop"); + for (name, contents) in generate(&api, &CConfig::default(), dir) { + std::fs::write(dir.join(name), contents).expect("write header"); + } + for (cc, ext, std) in compilers { + if !have(cc) { + continue; + } + let src = dir.join(format!("roundtrip.{ext}")); + let exe = dir.join(format!("roundtrip_{ext}")); + std::fs::write(&src, ROUNDTRIP_C).expect("write source"); + // MinGW (the `cc` on Windows runners) ships no sanitizer runtimes. + let sanitize: &[&str] = if cfg!(windows) { + &[] + } else { + &["-fsanitize=address,undefined"] + }; + let out = Command::new(cc) + .args([std, "-Wall", "-Wextra", "-Werror"]) + .args(sanitize) + .arg("-I") + .arg(dir.as_str()) + .arg(src.as_str()) + .arg("-o") + .arg(exe.as_str()) + .output() + .expect("run compiler"); + assert!( + out.status.success(), + "{cc} failed:\n{}", + String::from_utf8_lossy(&out.stderr) + ); + let run = Command::new(exe.as_str()).output().expect("run program"); + assert!( + run.status.success(), + "{cc} program failed:\n{}{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + } +} diff --git a/crates/weaveffi-gen/tests/dart_render.rs b/crates/weaveffi-gen/tests/dart_render.rs new file mode 100644 index 00000000..9cf19f17 --- /dev/null +++ b/crates/weaveffi-gen/tests/dart_render.rs @@ -0,0 +1,457 @@ +//! Dart rendering checks: identity-driven names and loading, ABI 3 string +//! and runtime symbols, the load-time contract check, object lifetimes, +//! nullable objects, object tokens in value buffers, iterators, async +//! cancellation, callback-interface vtables (isolate-local value methods and +//! forwarded void methods), and packaging. + +use camino::Utf8Path; +use weaveffi_gen::backend::LanguageBackend; +use weaveffi_gen::package::{FileContent, PackageContext}; +use weaveffi_gen::platform::{BinarySet, Platform}; +use weaveffi_gen::targets::dart::{DartConfig, DartGenerator}; +use weaveffi_model::ir::{ + Api, CallbackInterfaceDef, EnumDef, EnumVariant, Function, InterfaceDef, Module, Param, + StructDef, StructField, TypeRef, CURRENT_SCHEMA_VERSION, +}; +use weaveffi_model::model::BindingModel; +use weaveffi_model::pkg::Identity; +use weaveffi_model::resolved::ResolvedApi; +use weaveffi_model::validate::validate_api; + +fn param(name: &str, ty: TypeRef) -> Param { + Param { + name: name.into(), + ty, + doc: None, + } +} + +fn func(name: &str, params: Vec, returns: Option) -> Function { + Function { + name: name.into(), + params, + returns, + doc: None, + throws: false, + r#async: false, + cancellable: false, + deprecated: None, + } +} + +fn field(name: &str, ty: TypeRef) -> StructField { + StructField { + name: name.into(), + ty, + doc: None, + } +} + +fn named(name: &str) -> TypeRef { + TypeRef::Named(name.into()) +} + +/// One module exercising every shape: an interface, `Interface?` both ways, +/// a record holding objects, an iterator of objects, a string echo, a +/// cancellable async call, and a callback interface with value-returning +/// and void methods. +fn fixture() -> Api { + Api { + version: CURRENT_SCHEMA_VERSION.into(), + modules: vec![Module { + name: "bus".into(), + doc: None, + functions: vec![ + func( + "lookup", + vec![param( + "fallback", + TypeRef::Optional(Box::new(named("Ticker"))), + )], + Some(TypeRef::Optional(Box::new(named("Ticker")))), + ), + func( + "tickers", + vec![], + Some(TypeRef::Iterator(Box::new(named("Ticker")))), + ), + func( + "echo", + vec![param("text", TypeRef::StringUtf8)], + Some(TypeRef::StringUtf8), + ), + func( + "subscribe", + vec![param("listener", named("Subscriber"))], + None, + ), + func( + "describe", + vec![param("env", named("Envelope"))], + Some(named("Envelope")), + ), + Function { + r#async: true, + cancellable: true, + ..func( + "fetch", + vec![param("id", TypeRef::I64)], + Some(TypeRef::Optional(Box::new(named("Ticker")))), + ) + }, + ], + interfaces: vec![InterfaceDef { + name: "Ticker".into(), + doc: Some("A counter.".into()), + deprecated: None, + constructors: vec![func("new", vec![param("start", TypeRef::I64)], None)], + methods: vec![func("value", vec![], Some(TypeRef::I64))], + statics: vec![], + }], + callback_interfaces: vec![CallbackInterfaceDef { + name: "Subscriber".into(), + doc: Some("Receives bus events.".into()), + deprecated: None, + methods: vec![ + func( + "on_message", + vec![ + param("text", TypeRef::StringUtf8), + param("weight", TypeRef::I32), + param("envelope", named("Envelope")), + ], + Some(TypeRef::Bool), + ), + func( + "on_ticker", + vec![ + param("label", TypeRef::StringUtf8), + param("ticker", named("Ticker")), + param("alt", TypeRef::Optional(Box::new(named("Ticker")))), + ], + None, + ), + func( + "classify", + vec![param("weight", TypeRef::I32)], + Some(named("Priority")), + ), + ], + }], + structs: vec![StructDef { + name: "Envelope".into(), + doc: None, + deprecated: None, + fields: vec![ + field("topic", TypeRef::StringUtf8), + field("primary", named("Ticker")), + field("others", TypeRef::List(Box::new(named("Ticker")))), + ], + }], + enums: vec![EnumDef { + name: "Priority".into(), + doc: None, + deprecated: None, + variants: vec![ + EnumVariant { + name: "Low".into(), + value: 0, + doc: None, + fields: vec![], + }, + EnumVariant { + name: "High".into(), + value: 1, + doc: None, + fields: vec![], + }, + ], + }], + errors: None, + modules: vec![], + }], + } +} + +fn api() -> ResolvedApi { + validate_api(fixture(), None) + .expect("fixture validates") + .with_identity(Identity::named("bus-kit")) +} + +fn render() -> String { + let api = api(); + let model = BindingModel::build(&api); + let files = DartGenerator.files(&api, &model, Utf8Path::new("out"), &DartConfig::default()); + let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect(); + assert_eq!( + paths, + [ + "out/dart/lib/bus_kit.dart", + "out/dart/pubspec.yaml", + "out/dart/README.md" + ] + ); + files[0].contents.clone() +} + +fn assert_has(src: &str, needle: &str) { + assert!(src.contains(needle), "missing `{needle}` in:\n{src}"); +} + +#[test] +fn identity_names_the_package_and_library() { + let api = api(); + let model = BindingModel::build(&api); + let files = DartGenerator.files(&api, &model, Utf8Path::new("out"), &DartConfig::default()); + assert_has(&files[1].contents, "name: bus_kit\n"); + assert_has(&files[2].contents, "import 'package:bus_kit/bus_kit.dart';"); + + let config = DartConfig { + package_name: Some("custom".into()), + ..DartConfig::default() + }; + let files = DartGenerator.files(&api, &model, Utf8Path::new("out"), &config); + assert_eq!(files[0].path.as_str(), "out/dart/lib/custom.dart"); + assert_has(&files[1].contents, "name: custom\n"); + + let src = render(); + assert_has(&src, "Platform.environment['BUS_KIT_LIBRARY']"); + assert_has(&src, "const ['libbus_kit.dylib']"); + assert_has(&src, "const ['bus_kit.dll']"); + assert_has(&src, "if (Platform.isIOS) return DynamicLibrary.process();"); + assert!( + !src.to_lowercase().contains("weaveffi_"), + "branded symbol in output" + ); +} + +#[test] +fn library_load_checks_abi_and_checksums() { + let api = api(); + let model = BindingModel::build(&api); + let checksum = model.modules[0].checksum.expect("root checksum"); + let src = render(); + assert_has(&src, "const int _abiVersion = 3;"); + assert_has(&src, "'bus_kit_abi_version'"); + assert_has( + &src, + &format!(" ('bus', 'bus_kit_bus_checksum', 0x{checksum:016x}),"), + ); + assert_has(&src, "regenerate the bindings"); +} + +#[test] +fn strings_cross_as_pointer_and_length() { + let src = render(); + assert_has( + &src, + "Pointer Function(Pointer, Size, Pointer, Pointer<_Error>)", + ); + assert_has(&src, "final _textBytes = utf8.encode(text);"); + assert_has( + &src, + "_busKitBusEcho(_stage(_arena, _textBytes), _textBytes.length, _outLen, _err)", + ); + assert_has(&src, "return _takeString(_result, _outLen.value);"); + assert_has(&src, "'bus_kit_free_bytes'"); + assert!(!src.contains("free_string"), "ABI 2 free_string in output"); +} + +#[test] +fn objects_are_guarded_reference_counted_wrappers() { + let src = render(); + assert_has(&src, "final class Ticker extends _NativeObject {"); + assert_has(&src, "('bus_kit_bus_Ticker_clone')"); + assert_has(&src, "('bus_kit_bus_Ticker_destroy')"); + assert_has( + &src, + "NativeFinalizer get _finalizer => _busKitBusTickerDestroyFinalizer;", + ); + // A dispose during an in-flight call defers the release. + assert_has(&src, "if (_calls == 0) _release();"); + assert_has(&src, "if (--_calls == 0 && _disposed) _release();"); + assert_has(&src, "factory Ticker(int start) {"); + assert_has(&src, "return Ticker._(_result);"); + assert_has(&src, "final _self = _enter();"); + assert_has(&src, "_busKitBusTickerValue(_self, _err)"); +} + +#[test] +fn nullable_objects_are_borrowed_and_adopted() { + let src = render(); + assert_has(&src, "Ticker? lookup(Ticker? fallback) {"); + assert_has( + &src, + "final _fallbackPtr = fallback == null ? nullptr : _borrow(_arena, fallback);", + ); + assert_has( + &src, + "return _result == nullptr ? null : Ticker._(_result);", + ); +} + +#[test] +fn objects_inside_records_are_cloned_tokens() { + let src = render(); + assert_has(&src, "w.writeU64(v.primary._cloneRef().address);"); + assert_has(&src, "w.writeU64(_t1._cloneRef().address);"); + assert_has( + &src, + "primary: Ticker._(Pointer.fromAddress(r.readU64())),", + ); + assert_has( + &src, + "others: [for (var i = r.readLength(); i > 0; i--) Ticker._(Pointer.fromAddress(r.readU64()))],", + ); +} + +#[test] +fn iterators_pull_lazily_and_destroy_once() { + let src = render(); + assert_has(&src, "Iterable tickers() sync* {"); + assert_has( + &src, + "_busKitBusTickersIteratorNext(_iter, _outItem.cast(), _err)", + ); + assert_has( + &src, + "yield Ticker._(_outItem.cast>().value);", + ); + assert_has( + &src, + "_busKitBusTickersIteratorDestroyFinalizer.attach(_anchor, _iter, detach: _anchor);", + ); + assert_has(&src, "_busKitBusTickersIteratorDestroy(_iter);"); +} + +#[test] +fn cancellable_async_binds_a_native_token() { + let src = render(); + assert_has( + &src, + "Future fetch(int id, {CancelToken? cancelToken}) {", + ); + assert_has(&src, "final _cancel = _NativeCancel.bind(cancelToken);"); + assert_has(&src, "NativeCallable<_BusKitBusFetchCallback>.listener(("); + assert_has( + &src, + "_busKitBusFetch(id, _cancel?.pointer ?? nullptr, _callback.nativeFunction, nullptr);", + ); + assert_has(&src, "_cancel?.release();"); + assert_has(&src, "'bus_kit_cancel_token_create'"); + assert_has(&src, "class CancelledException extends NativeException {"); + assert_has(&src, "static const int cancelledCode = -5;"); +} + +#[test] +fn callback_interfaces_split_value_and_void_methods() { + let src = render(); + assert_has(&src, "abstract class Subscriber {"); + assert_has( + &src, + "bool onMessage(String text, int weight, Envelope envelope);", + ); + assert_has( + &src, + "void onTicker(String label, Ticker ticker, Ticker? alt);", + ); + // A value-returning method is an isolate-local trampoline that reports + // a thrown exception as the foreign code. + assert_has( + &src, + "..onMessage = _pin(NativeCallable<_BusKitBusSubscriberOnMessage>.isolateLocal(_subscriberOnMessage, exceptionalReturn: false))", + ); + assert_has( + &src, + "return (_callbackTarget(ctx) as Subscriber).onMessage(_readString(textPtr, textLen), weight, _decode(_copyBytes(envelopePtr, envelopeLen), _unpackEnvelope));", + ); + assert_has(&src, "_foreignError(outErr, e);"); + assert_has( + &src, + "return (_callbackTarget(ctx) as Subscriber).classify(weight).value;", + ); + // A void method is forwarded from any thread and dispatched on the + // event loop, adopting objects before decoding the rest. + assert_has( + &src, + "..onTicker = _pin(NativeCallable<_BusKitBusSubscriberOnTicker>.isolateGroupBound(_subscriberOnTicker))", + ); + assert_has( + &src, + " _CallbackMessage(ctx, 1, 3)\n ..bytes(labelPtr, labelLen)\n ..pointer(ticker)\n ..pointer(alt)\n ..send();", + ); + assert_has( + &src, + "final _a3Address = message[3]! as int;\n final _a3 = Ticker._(Pointer.fromAddress(_a3Address));", + ); + assert_has(&src, "final _a2 = utf8.decode(message[2]! as Uint8List);"); + assert_has(&src, "target.onTicker(_a2, _a3, _a4);"); + assert_has(&src, "..free = _callbackFree;"); + // Passing an implementation registers it last. + assert_has( + &src, + "final _listenerCtx = _registerCallback(listener, _subscriberDispatch);", + ); + assert_has( + &src, + "_busKitBusSubscribe(_listenerCtx, _subscriberVtable.cast(), _err);", + ); + // No call may be a leaf call when a callback can re-enter Dart. + assert!(!src.contains("isLeaf: true);\n\nfinal _busKitBus")); + assert_has(&src, "'bus_kit_error_set'"); +} + +#[test] +fn package_bundles_only_desktop_binaries() { + let api = api(); + let model = BindingModel::build(&api); + let mut binaries = BinarySet::new("bus_kit"); + for p in Platform::ALL { + binaries.insert(p, format!("/prebuilt/{}/lib", p.id())); + } + let ctx = PackageContext { + binaries: &binaries, + input_basename: Some("bus.yml"), + }; + let files = DartGenerator + .package( + &api, + &model, + &ctx, + Utf8Path::new("out"), + &DartConfig::default(), + ) + .expect("dart packages"); + let bundled: Vec<&str> = files + .iter() + .filter(|f| f.is_binary()) + .map(|f| f.path.as_str()) + .collect(); + assert_eq!(bundled.len(), Platform::DESKTOP.len()); + for p in [ + Platform::AndroidArm64, + Platform::AndroidX64, + Platform::Wasm32, + ] { + assert!( + !bundled.iter().any(|b| b.contains(p.id())), + "{} bundled", + p.id() + ); + } + let src = files + .iter() + .find(|f| f.path.as_str().ends_with("lib/bus_kit.dart")) + .and_then(|f| match &f.content { + FileContent::Text(s) => Some(s.as_str()), + FileContent::Copy(_) => None, + }) + .expect("packaged source"); + assert_has( + src, + "'native/darwin-arm64/libbus_kit.dylib', 'native/darwin-x64/libbus_kit.dylib', 'libbus_kit.dylib'", + ); + assert_has(src, "'native/windows-x64/bus_kit.dll', 'bus_kit.dll'"); + assert_has(src, "Platform.environment['BUS_KIT_LIBRARY']"); +} diff --git a/crates/weaveffi-ir/Cargo.toml b/crates/weaveffi-ir/Cargo.toml deleted file mode 100644 index 8fd8a121..00000000 --- a/crates/weaveffi-ir/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "weaveffi-ir" -version = "0.23.0" -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -description = "Intermediate representation model and multi-format parser for WeaveFFI API definitions" -keywords = ["ffi", "codegen", "bindings", "ir", "parser"] -categories = ["development-tools::ffi"] - -[dependencies] -serde = { workspace = true } -serde_yaml = { workspace = true } -serde_json = { workspace = true } -toml = { workspace = true } -thiserror = { workspace = true } -miette = { workspace = true } -schemars = { workspace = true } - -[lints] -workspace = true diff --git a/crates/weaveffi-ir/src/lib.rs b/crates/weaveffi-ir/src/lib.rs deleted file mode 100644 index df9bc76d..00000000 --- a/crates/weaveffi-ir/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! In-memory IR types and (de)serialization for WeaveFFI. -#![deny(missing_docs)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::doc_markdown)] - -pub mod ir; -pub mod parse; diff --git a/crates/weaveffi-macros/Cargo.toml b/crates/weaveffi-macros/Cargo.toml index 55b95caf..d28b8b7f 100644 --- a/crates/weaveffi-macros/Cargo.toml +++ b/crates/weaveffi-macros/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "weaveffi-macros" -version = "0.23.0" +version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true @@ -13,9 +14,7 @@ categories = ["development-tools::ffi"] proc-macro = true [dependencies] -weaveffi-ir = { path = "../weaveffi-ir", version = "0.23.0" } -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } -weaveffi-bridge = { path = "../weaveffi-bridge", version = "0.23.0" } +weaveffi-model = { workspace = true } syn = { workspace = true } quote = { workspace = true } proc-macro2 = "1.0" diff --git a/crates/weaveffi-macros/src/codegen/async_fns.rs b/crates/weaveffi-macros/src/codegen/async_fns.rs index 2581882f..6d8c5584 100644 --- a/crates/weaveffi-macros/src/codegen/async_fns.rs +++ b/crates/weaveffi-macros/src/codegen/async_fns.rs @@ -1,128 +1,103 @@ //! Thunk emission for `async fn` exports: the completion-callback typedef and -//! the `_async` launcher. +//! the launcher. //! //! This is the producer half of the completion contract stated by -//! [`weaveffi_core::plan::AsyncProtocol`]: the callback fires exactly once, +//! [`weaveffi_model::plan::AsyncProtocol`]: the callback fires exactly once, //! from an arbitrary producer thread, and everything it delivers is *owned by //! the consumer*. A non-null `err` is heap-boxed and released with -//! `weaveffi_error_free`; a string result is released with -//! `weaveffi_free_string`; a buffered or bytes result is released with -//! `weaveffi_free_bytes`; an object result transfers one strong reference. -//! This ownership transfer (unlike the borrow-and-copy contract of synchronous -//! `out_err` slots) is what lets consumers defer decoding past the callback's -//! return, which runtimes such as Dart's `NativeCallable.listener` require. -//! The callback's `err` slot follows the owning function's -//! [`ErrorStrategy`](weaveffi_core::plan::ErrorStrategy). +//! `{prefix}_error_free`; a string, bytes, or buffered result is a +//! `(result_ptr, result_len)` run released with `{prefix}_free_bytes`; an +//! object result transfers one strong reference. This ownership transfer +//! (unlike the borrow-and-copy contract of synchronous `out_err` slots) is +//! what lets consumers defer decoding past the callback's return, which +//! runtimes such as Dart's `NativeCallable.listener` require. //! -//! The future runs on the process-wide [`weaveffi_abi::spawn`] executor (the -//! default thread-per-future spawner, or whatever the producer installed with -//! [`weaveffi_abi::set_spawner`]), wrapped in [`weaveffi_abi::CatchUnwind`] -//! so a panic is delivered through the callback rather than into the executor. +//! The launcher lifts its inputs on the caller's thread and hands the rest to +//! [`weaveffi_abi::run_async`], which owns the exactly-once guarantee: it +//! catches panics, races a `cancellable` call against its token (dropping the +//! producer's future and completing with the cancelled code when the token +//! fires), and completes with the cancelled code from a drop guard if the +//! executor drops the task early. use proc_macro2::TokenStream; use quote::quote; -use weaveffi_core::model::{AsyncBinding, FnBinding, ParamBinding, Ty}; +use weaveffi_model::model::{AsyncBinding, FnBinding, ParamBinding, Ty}; use super::helpers::{ - ctype_to_rust, fn_slots, ident, is_copy, rust_type_ident, sentinel, CallTarget, UserSig, + ctype_to_rust, fn_slots, ident, is_copy, rust_type_ident, sentinel, thunk_attrs, CallTarget, + UserSig, }; use super::sync::throws; use super::unsupported; -/// The two "fire the callback with an error and stop" tails an async launcher -/// needs: one for the caller's thread (before the spawn, where `context` is -/// still a raw pointer and `return` leaves the launcher) and one for inside -/// the spawned future (where `__wv_ctx` is a `usize` and `return` leaves the -/// async block). Both replay the result slots as their zero/null sentinels. -struct Fail { - sentinels: Vec, -} - -impl Fail { - fn with(&self, ctx: TokenStream, build_err: TokenStream) -> TokenStream { - let sentinels = &self.sentinels; - quote! {{ - let mut __wv_e = ::weaveffi::abi::weaveffi_error::default(); - #build_err - callback( - #ctx, - ::std::boxed::Box::into_raw(::std::boxed::Box::new(__wv_e)) - #(, #sentinels)* - ); - return; - }} - } - - fn marshal(msg: &str) -> TokenStream { - quote! { - ::weaveffi::abi::error_set( - &mut __wv_e, +/// The "fire the callback with a marshalling error and leave the launcher" +/// tail for an input rejected on the caller's thread. +fn reject_pre(msg: &str, sentinels: &[TokenStream]) -> TokenStream { + quote! {{ + callback( + context, + ::weaveffi::abi::boxed_error(::weaveffi::abi::FfiError::new( ::weaveffi::abi::MARSHAL_ERROR_CODE, #msg, - ); - } - } - - /// Reject on the caller's thread with a marshalling error. - fn pre(&self, msg: &str) -> TokenStream { - self.with(quote!(context), Self::marshal(msg)) - } + )) + #(, #sentinels)* + ); + return; + }} +} - /// Reject inside the future with a marshalling error. - fn inner(&self, msg: &str) -> TokenStream { - self.with( - quote!(__wv_ctx as *mut ::std::ffi::c_void), - Self::marshal(msg), - ) +/// The `return Err(..)` tail for an input rejected inside the future. +fn reject_in_future(msg: &str) -> TokenStream { + quote! { + return ::std::result::Result::Err(::weaveffi::abi::FfiError::new( + ::weaveffi::abi::MARSHAL_ERROR_CODE, + #msg, + )) } } -/// Lift one async-launcher input slot into an *owned* Rust value and give the -/// call argument the user's `async fn` receives, as `(pre_spawn, in_future, -/// arg)`. +/// Lift one async-launcher input into an *owned* Rust value, as `(pre_spawn, +/// in_future, arg)`. /// -/// Async inputs must own their data before the future is spawned: the foreign -/// caller may free or reuse the argument buffers as soon as the launcher -/// returns. So strings and bytes are copied and objects retained (a new strong -/// reference) on the caller's thread; a borrowed spelling (`&str`, `&T`) is -/// then satisfied by lending the owned value. There is no `out_err` slot on a -/// launcher, so an invalid input fires the completion callback with a -/// marshalling error, which keeps the "exactly once" promise. +/// Async inputs must own their data before the future is spawned: the +/// consumer may free or reuse the argument buffers as soon as the launcher +/// returns. So strings and bytes are copied and objects retained (a new +/// strong reference) on the caller's thread; a borrowed spelling (`&str`, +/// `&T`) is then satisfied by lending the owned value. There is no `out_err` +/// slot on a launcher, so an invalid input fires the completion callback with +/// a marshalling error, which keeps the "exactly once" promise. fn lift_async_input( pb: &ParamBinding, user: &UserSig<'_>, - fail: &Fail, + sentinels: &[TokenStream], ) -> syn::Result<(TokenStream, TokenStream, TokenStream)> { let name = ident(&pb.name); let none = TokenStream::new(); - let is_ref = user.param_is_ref(&pb.name); - let arg = if is_ref { + let arg = if user.param_is_ref(&pb.name) { quote!(&#name) } else { quote!(#name) }; - let invalid = fail.pre(&format!("{} is null or invalid", pb.name)); + let invalid = reject_pre(&format!("{} is null or invalid", pb.name), sentinels); + let ptr = ident(&format!("{}_ptr", pb.name)); + let len = ident(&format!("{}_len", pb.name)); - // A buffered parameter arrives as a borrowed value buffer the caller may - // free as soon as the launcher returns: copy the raw bytes on the caller's - // thread and decode inside the future (a malformed buffer is delivered - // through the callback's `err` with the marshalling code). + // A buffered parameter is copied on the caller's thread and decoded + // inside the future (a malformed buffer is delivered through the + // callback's `err` with the marshalling code). if pb.ty.is_buffered() { - let ptr = ident(&format!("{}_ptr", pb.name)); - let len = ident(&format!("{}_len", pb.name)); - let decode_fail = fail.inner(&format!("{}: malformed value buffer", pb.name)); + let decode_fail = reject_in_future(&format!("{}: malformed value buffer", pb.name)); return Ok(( quote! { - let #name: ::std::vec::Vec = if #ptr.is_null() { - ::std::vec::Vec::new() - } else { - unsafe { ::std::slice::from_raw_parts(#ptr, #len) }.to_vec() + let #name = match ::weaveffi::abi::lift_bytes(#ptr, #len) { + ::std::option::Option::Some(__wv_v) => __wv_v, + ::std::option::Option::None => #invalid }; }, quote! { let #name = match ::weaveffi::abi::decode_value(&#name) { - ::std::result::Result::Ok(__v) => __v, - ::std::result::Result::Err(_) => #decode_fail + ::std::result::Result::Ok(__wv_v) => __wv_v, + ::std::result::Result::Err(_) => { #decode_fail } }; }, arg, @@ -136,7 +111,7 @@ fn lift_async_input( ( quote! { let #name = match <#et>::__weaveffi_from_i32(#name) { - ::std::option::Option::Some(__v) => __v, + ::std::option::Option::Some(__wv_v) => __wv_v, ::std::option::Option::None => #invalid }; }, @@ -145,21 +120,19 @@ fn lift_async_input( ) } ty if is_copy(ty) => (none.clone(), none, arg), - Ty::StringUtf8 => ( - quote! { - let #name = match ::weaveffi::abi::c_ptr_to_string(#name) { - ::std::option::Option::Some(__s) => __s, - ::std::option::Option::None => #invalid - }; - }, - none, - arg, - ), - Ty::Bytes => { - let ptr = ident(&format!("{}_ptr", pb.name)); - let len = ident(&format!("{}_len", pb.name)); + Ty::StringUtf8 | Ty::Bytes => { + let lift = if matches!(pb.ty, Ty::StringUtf8) { + quote!(lift_string) + } else { + quote!(lift_bytes) + }; ( - quote!(let #name = unsafe { ::weaveffi::abi::lift_bytes(#ptr, #len) };), + quote! { + let #name = match ::weaveffi::abi::#lift(#ptr, #len) { + ::std::option::Option::Some(__wv_v) => __wv_v, + ::std::option::Option::None => #invalid + }; + }, none, arg, ) @@ -175,7 +148,7 @@ fn lift_async_input( }; ( quote! { - let #name = match unsafe { ::weaveffi::abi::object_arc(#name) } { + let #name = match ::weaveffi::abi::object_arc(#name) { ::std::option::Option::Some(__wv_o) => __wv_o, ::std::option::Option::None => #invalid }; @@ -191,7 +164,7 @@ fn lift_async_input( quote!(#name.as_deref()) }; ( - quote!(let #name = unsafe { ::weaveffi::abi::object_arc(#name) };), + quote!(let #name = ::weaveffi::abi::object_arc(#name);), none, arg, ) @@ -200,12 +173,10 @@ fn lift_async_input( let dyn_ty = user.param_callback(&pb.name)?; let ctx = ident(&format!("{}_ctx", pb.name)); let vtable = ident(&format!("{}_vtable", pb.name)); - let null_fail = fail.pre(&format!("{}: null callback vtable", pb.name)); + let null_fail = reject_pre(&format!("{}: null callback vtable", pb.name), sentinels); ( quote! { - let #name = match unsafe { - ::weaveffi::abi::lift_callback::<#dyn_ty>(#ctx, #vtable) - } { + let #name = match ::weaveffi::abi::lift_callback::<#dyn_ty>(#ctx, #vtable) { ::std::option::Option::Some(__wv_cb) => __wv_cb, ::std::option::Option::None => #null_fail }; @@ -214,80 +185,66 @@ fn lift_async_input( arg, ) } - _ => return Err(unsupported(&pb.name, "async parameter type")), + _ => { + return Err(unsupported( + user.param_span(&pb.name), + &pb.name, + "async parameter type", + )) + } }) } -/// Lower the future's output into the completion callback's *result* arguments -/// (the slots after `context` and `err`), returning `(preamble, args)`. -/// -/// Every result transfers ownership to the consumer, per the plan's async -/// contract ([`weaveffi_core::plan::AsyncProtocol`]): a string is released -/// with `weaveffi_free_string`, a value or byte buffer with -/// `weaveffi_free_bytes`, and an object result is one strong reference the -/// consumer eventually releases with `_destroy`. -fn async_result_args( +/// The expression lowering the future's output `__wv_val` into the +/// completion callback's *result* arguments (the slots after `context` and +/// `err`), as a tuple. Every result transfers ownership to the consumer. +fn async_result_slots( ty: &Ty, - value: TokenStream, object: Option<&TokenStream>, + user: &UserSig<'_>, ) -> syn::Result<(TokenStream, Vec)> { - let none = TokenStream::new(); - let owned_bytes = |bytes: TokenStream| { + let pair = |bytes: TokenStream| { ( - quote! { - let __wv_res_buf = (#bytes).into_boxed_slice(); - let __wv_res_len = __wv_res_buf.len(); - let __wv_res_ptr = ::std::boxed::Box::into_raw(__wv_res_buf) as *const u8; - }, - vec![quote!(__wv_res_ptr), quote!(__wv_res_len)], + quote!(let (__wv_r0, __wv_r1) = ::weaveffi::abi::bytes_into_raw(#bytes);), + vec![quote!(__wv_r0), quote!(__wv_r1)], ) }; if ty.is_buffered() { - return Ok(owned_bytes( - quote!(::weaveffi::abi::encode_value(&(#value))), - )); + return Ok(pair(quote!(::weaveffi::abi::encode_value(&__wv_val)))); } let typed = |call: TokenStream| match object { - Some(obj) => quote!(let __wv_res: *mut #obj = #call;), - None => quote!(let __wv_res = #call;), + Some(obj) => quote!(let __wv_r0: *mut #obj = #call;), + None => quote!(let __wv_r0 = #call;), }; + let one = vec![quote!(__wv_r0)]; Ok(match ty { - Ty::Enum(_) => (none, vec![quote!((#value).__weaveffi_to_i32())]), - t if is_copy(t) => (none, vec![value]), - Ty::StringUtf8 => ( - quote!(let __wv_res = ::weaveffi::abi::string_to_c_ptr(&(#value));), - vec![quote!(__wv_res)], - ), - Ty::Bytes => owned_bytes(value), - Ty::Interface(_) => ( - typed(quote!(::weaveffi::abi::lower_object(#value))), - vec![quote!(__wv_res)], - ), + Ty::Enum(_) => (quote!(let __wv_r0 = __wv_val.__weaveffi_to_i32();), one), + t if is_copy(t) => (quote!(let __wv_r0 = __wv_val;), one), + Ty::StringUtf8 => pair(quote! { + ::std::string::String::into_bytes(::std::convert::Into::into(__wv_val)) + }), + Ty::Bytes => pair(quote!(::std::convert::Into::<::std::vec::Vec>::into( + __wv_val + ))), + Ty::Interface(_) => (typed(quote!(::weaveffi::abi::lower_object(__wv_val))), one), Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => ( - typed(quote!(::weaveffi::abi::lower_object_opt(#value))), - vec![quote!(__wv_res)], + typed(quote!(::weaveffi::abi::lower_object_opt(__wv_val))), + one, ), - _ => return Err(unsupported("async return", "result type")), + _ => return Err(unsupported(user.ret_span(), "async return", "result type")), }) } -/// Generate the completion-callback typedef and the `_async` launcher for an +/// Generate the completion-callback typedef and the launcher for an /// `async fn`. -/// -/// The launcher lifts inputs into owned values on the caller's thread, then -/// hands a `Send + 'static` future to [`weaveffi_abi::spawn`]. The future -/// awaits the producer's `async fn` under [`weaveffi_abi::CatchUnwind`] and -/// invokes the host's callback with `(context, err, result…)` exactly once: a -/// `Result` return routes its `Err` through a boxed `weaveffi_error`, a panic -/// (or a consumer callback's failure) is reported with the reserved code, and -/// success passes a null `err`. The foreign caller's `context` is carried as a -/// `usize` so the future is `Send`; the callback pointer is `Send` on its own. pub(crate) fn gen_async_function( f: &FnBinding, a: &AsyncBinding, user: &UserSig<'_>, target: &CallTarget, + prefix: &str, ) -> syn::Result { + let is_object_ret = f.ret.as_ref().is_some_and(|t| t.interface_name().is_some()); let cb_ty = ident(&a.callback_type); let cb_slots: Vec = a .callback_params @@ -295,10 +252,9 @@ pub(crate) fn gen_async_function( .map(|p| { // The result slot of an object return is spelled with the // producer's pointee so `super::T` stays in scope. - let is_object_ret = f.ret.as_ref().is_some_and(|t| t.interface_name().is_some()); match (p.name.as_str(), user.ret_object()) { ("result", Some(obj)) if is_object_ret => quote!(*mut #obj), - _ => ctype_to_rust(&p.ty), + _ => ctype_to_rust(&p.ty, prefix), } }) .collect(); @@ -309,19 +265,16 @@ pub(crate) fn gen_async_function( }; let launch_sym = ident(&a.launch.symbol); - let launch_params = fn_slots(&a.launch.params, &f.params, user)?; - - let fail = Fail { - sentinels: a - .callback_params - .iter() - .skip(2) - .map(|p| sentinel(&p.ty)) - .collect(), - }; + let launch_params = fn_slots(&a.launch.params, &f.params, user, prefix)?; + let sentinels: Vec = a + .callback_params + .iter() + .skip(2) + .filter_map(|p| sentinel(&p.ty)) + .collect(); - // Lift each logical input into three parts: a pre-spawn statement that runs - // on the caller's thread (owning borrowed data, retaining objects), an + // Lift each input into three parts: a pre-spawn statement on the + // caller's thread (owning borrowed data, retaining objects), an // in-future statement that finishes the lift (decoding a value buffer), // and the argument forwarded to the producer. let mut pre_spawn = TokenStream::new(); @@ -330,12 +283,10 @@ pub(crate) fn gen_async_function( // An async method retains its receiver for the life of the call: the // consumer may release its own reference the moment the launcher returns. - // A null receiver still fires the callback (with an error), so the - // continuation never hangs. if let CallTarget::Method(ty) = target { - let null_self = fail.pre("self is null"); + let null_self = reject_pre("self is null", &sentinels); pre_spawn.extend(quote! { - let __wv_obj = match unsafe { ::weaveffi::abi::object_arc::<#ty>(__wv_self) } { + let __wv_obj = match ::weaveffi::abi::object_arc::<#ty>(__wv_self) { ::std::option::Option::Some(__wv_o) => __wv_o, ::std::option::Option::None => #null_self }; @@ -343,123 +294,101 @@ pub(crate) fn gen_async_function( } for pb in &f.params { - let (pre, inc, arg) = lift_async_input(pb, user, &fail)?; + let (pre, inc, arg) = lift_async_input(pb, user, &sentinels)?; pre_spawn.extend(pre); in_future.extend(inc); call_args.push(arg); } - // A `#[weaveffi::cancellable]` function receives the launcher's - // `cancel_token` slot as a `Send` [`CancelToken`] appended after its - // logical inputs (the producer declares it as the final parameter). - // Building it before the spawn keeps the raw pointer off the capture list. - if f.cancellable { + // A cancellable function takes its own reference on the token (so the + // consumer may destroy it at any time) and hands one handle to the + // producer, as its final parameter, and one to the runtime, which drops + // the future and completes with the cancelled code when it fires. + let cancel = if f.cancellable { pre_spawn.extend(quote! { - let __wv_cancel = ::weaveffi::CancelToken::from_raw(cancel_token); + let __wv_cancel = ::weaveffi::abi::CancelToken::from_raw(cancel_token); + let __wv_cancel_arg = ::std::clone::Clone::clone(&__wv_cancel); }); - call_args.push(quote!(__wv_cancel)); - } - - let is_throws = throws(f); - let object = user.ret_object(); - let (result_pre, success_args) = match &f.ret { - Some(ty) => async_result_args(ty, quote!(__wv_val), object.as_ref())?, - None => (TokenStream::new(), Vec::new()), - }; - let success_call = quote! { - #result_pre - callback(__wv_ctx as *mut ::std::ffi::c_void, ::std::ptr::null_mut() #(, #success_args)*); + call_args.push(quote!(__wv_cancel_arg)); + quote!(::std::option::Option::Some(__wv_cancel)) + } else { + quote!(::std::option::Option::None) }; - let sentinels = &fail.sentinels; - let dispatch = if is_throws { - let bind = if f.ret.is_some() { - quote!(__wv_val) - } else { - quote!(_) - }; - quote! { - match __wv_out { - ::std::result::Result::Ok(#bind) => { #success_call } - ::std::result::Result::Err(__wv_err) => { - let mut __wv_e = ::weaveffi::abi::weaveffi_error::default(); - ::weaveffi::abi::error_set_with_payload( - &mut __wv_e, - ::weaveffi::abi::ErrorReport::code(&__wv_err), - &::weaveffi::abi::ErrorReport::message(&__wv_err), - ::weaveffi::abi::ErrorReport::payload(&__wv_err), - ); - callback( - __wv_ctx as *mut ::std::ffi::c_void, - ::std::boxed::Box::into_raw(::std::boxed::Box::new(__wv_e)) - #(, #sentinels)* - ); - } - } - } - } else if f.ret.is_some() { + let call = target.call(&f.name, &call_args); + let output = if throws(f) { quote! { - let __wv_val = __wv_out; - #success_call + __wv_out.map_err(|__wv_e| ::weaveffi::abi::FfiError::from_report(&__wv_e)) } } else { - quote! { - let () = __wv_out; - #success_call - } + quote!(::std::result::Result::Ok(__wv_out)) }; - let call = target.call(&f.name, &call_args); - - Ok(quote! { - #callback_typedef - #[no_mangle] - #[allow(unsafe_code, deprecated, clippy::not_unsafe_ptr_arg_deref, clippy::missing_safety_doc)] - pub extern "C" fn #launch_sym(#(#launch_params),*) { - #pre_spawn - let __wv_ctx = context as usize; - ::weaveffi::abi::spawn(async move { - // The inner future finishes lifting, awaits the producer, and - // fires the callback. A panic anywhere inside (including a - // consumer callback-interface failure surfacing as a - // `ForeignError`) is caught and delivered through `err`, so - // the continuation fires exactly once. A failure the producer - // recorded instead of unwinding (`panic = "abort"` builds) is - // taken before the success callback can fire, for the same - // reason. - let __wv_run = ::weaveffi::abi::CatchUnwind::new(async move { - #in_future - let __wv_out = #call.await; - match ::weaveffi::abi::take_foreign_error() { - ::std::option::Option::Some(__wv_foreign) => { - let mut __wv_e = ::weaveffi::abi::weaveffi_error::default(); - ::weaveffi::abi::error_set( - &mut __wv_e, - __wv_foreign.code, - &__wv_foreign.message, - ); - callback( - __wv_ctx as *mut ::std::ffi::c_void, - ::std::boxed::Box::into_raw(::std::boxed::Box::new(__wv_e)) - #(, #sentinels)* - ); + let object = user.ret_object(); + // Lowering can panic (an oversized buffer, say) and must not unwind past + // the exactly-once promise, so it's caught and reported like a producer + // panic. A `void` result has nothing to lower. + let success_arm = match &f.ret { + Some(ty) => { + let (lower, slots) = async_result_slots(ty, object.as_ref(), user)?; + quote! { + ::std::result::Result::Ok(__wv_val) => { + match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe( + move || { + #lower + (#(#slots,)*) + }, + )) { + ::std::result::Result::Ok((#(#slots,)*)) => { + callback(__wv_ctx, ::std::ptr::null_mut() #(, #slots)*); + return; } - ::std::option::Option::None => { - #dispatch + ::std::result::Result::Err(__wv_panic) => { + ::weaveffi::abi::FfiError::from_panic(&*__wv_panic) } } - }) - .await; - if let ::std::result::Result::Err(__wv_panic) = __wv_run { - let mut __wv_e = ::weaveffi::abi::weaveffi_error::default(); - ::weaveffi::abi::error_set_panic(&mut __wv_e, &*__wv_panic); - callback( - __wv_ctx as *mut ::std::ffi::c_void, - ::std::boxed::Box::into_raw(::std::boxed::Box::new(__wv_e)) - #(, #sentinels)* - ); } - }); + } + } + None => quote! { + ::std::result::Result::Ok(()) => { + callback(__wv_ctx, ::std::ptr::null_mut()); + return; + } + }, + }; + let attrs = thunk_attrs(); + + Ok(quote! { + #callback_typedef + + #attrs + pub unsafe extern "C" fn #launch_sym(#(#launch_params),*) { + #[allow(unused_unsafe)] + unsafe { + #pre_spawn + let __wv_ctx = context as usize; + ::weaveffi::abi::run_async( + async move { + #in_future + let __wv_out = #call.await; + #output + }, + #cancel, + move |__wv_res: ::std::result::Result<_, ::weaveffi::abi::FfiError>| { + let __wv_ctx = __wv_ctx as *mut ::std::ffi::c_void; + let __wv_err = match __wv_res { + #success_arm + ::std::result::Result::Err(__wv_e) => __wv_e, + }; + callback( + __wv_ctx, + ::weaveffi::abi::boxed_error(__wv_err) + #(, #sentinels)* + ); + }, + ); + } } }) } diff --git a/crates/weaveffi-macros/src/codegen/callbacks.rs b/crates/weaveffi-macros/src/codegen/callbacks.rs index f8e87c33..e00b18e1 100644 --- a/crates/weaveffi-macros/src/codegen/callbacks.rs +++ b/crates/weaveffi-macros/src/codegen/callbacks.rs @@ -4,65 +4,60 @@ //! lift `Arc` parameters. //! //! This is the producer half of the contract stated by -//! [`weaveffi_core::plan::CallbackProtocol`]: each method lowers its -//! arguments (strings and buffers are borrowed for the call and freed after -//! the vtable entry returns; objects transfer one strong reference the -//! consumer adopts), calls the entry with `(ctx, args…, &mut err)`, and then -//! checks `err`. A consumer failure is raised through -//! `weaveffi::abi::raise_foreign_error`: on unwinding builds it unwinds with a -//! `ForeignError` payload that the enclosing thunk reports as -//! `FOREIGN_ERROR_CODE`; on `panic = "abort"` builds it is recorded and the -//! thunk picks it up after the producer returns. +//! [`weaveffi_model::plan::CallbackProtocol`]: each method lowers its +//! arguments (strings, bytes, and buffers are borrowed for the call as +//! `(ptr, len)`; objects transfer one strong reference the consumer adopts), +//! calls the entry with `(ctx, args…, &mut err)`, and then checks `err`. A +//! method declared to return `Result` returns the +//! consumer's failure as an `Err`; one returning a plain `T` raises it +//! through `weaveffi::abi::raise_foreign_error` (an unwind on unwinding +//! builds, a deferred report to the enclosing thunk on `panic = "abort"` +//! builds). -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use weaveffi_core::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding, Ty}; +use syn::spanned::Spanned as _; +use weaveffi_model::model::{CallbackInterfaceBinding, CallbackMethodBinding, ParamBinding, Ty}; use super::helpers::{ident, is_copy, ret_arrow, rust_type_ident, slot_type_for, UserSig}; use super::unsupported; /// Build the lowering for one callback-method argument as `(preamble, C -/// arguments, cleanup)`. The Rust parameter is whatever the producer's trait -/// declares; `is_ref` says whether it arrives borrowed. +/// arguments)`. The Rust parameter is whatever the producer's trait +/// declares; everything is borrowed for the duration of the vtable call. fn lower_callback_arg( pb: &ParamBinding, user: &UserSig<'_>, -) -> syn::Result<(TokenStream, Vec, TokenStream)> { +) -> syn::Result<(TokenStream, Vec)> { let n = ident(&pb.name); let is_ref = user.param_is_ref(&pb.name); let none = TokenStream::new(); - // Borrow the value for encoding whether it arrived owned or borrowed. + // Borrow the value whether it arrived owned or borrowed. let borrowed = if is_ref { quote!(#n) } else { quote!(&#n) }; + let tmp = ident(&format!("__wv_cb_{}", pb.name)); // A buffered payload is borrowed for the call: encode it into a local // value buffer, hand the consumer its `(ptr, len)` view, and let the // encoding drop afterward. if pb.ty.is_buffered() { - let tmp = ident(&format!("__wv_cb_{}", pb.name)); return Ok(( quote!(let #tmp = ::weaveffi::abi::encode_value(#borrowed);), vec![quote!(#tmp.as_ptr()), quote!(#tmp.len())], - none, )); } Ok(match &pb.ty { - Ty::Enum(_) => (none.clone(), vec![quote!(#n.__weaveffi_to_i32())], none), + Ty::Enum(_) => (none, vec![quote!(#n.__weaveffi_to_i32())]), ty if is_copy(ty) => { let v = if is_ref { quote!(*#n) } else { quote!(#n) }; - (none.clone(), vec![v], none) - } - Ty::StringUtf8 => { - let tmp = ident(&format!("__wv_cb_{}", pb.name)); - ( - quote!(let #tmp = ::weaveffi::abi::string_to_c_ptr(&#n);), - vec![quote!(#tmp)], - quote!(::weaveffi::abi::free_string(#tmp);), - ) + (none, vec![v]) } + Ty::StringUtf8 => ( + quote!(let #tmp: &str = ::std::convert::AsRef::::as_ref(#borrowed);), + vec![quote!(#tmp.as_ptr()), quote!(#tmp.len())], + ), Ty::Bytes => ( - none.clone(), - vec![quote!(#n.as_ptr()), quote!(#n.len())], - none, + quote!(let #tmp: &[u8] = ::std::convert::AsRef::<[u8]>::as_ref(#borrowed);), + vec![quote!(#tmp.as_ptr()), quote!(#tmp.len())], ), // An object transfers one strong reference the consumer adopts (and // eventually `_destroy`s). The producer must hold it as an `Arc`, @@ -70,6 +65,7 @@ fn lower_callback_arg( Ty::Interface(_) => { if !user.param_wants_arc(&pb.name) { return Err(unsupported( + user.param_span(&pb.name), &pb.name, "callback-interface object parameter that is not an `Arc` (the consumer \ adopts a reference, so spell it `Arc`)", @@ -77,57 +73,79 @@ fn lower_callback_arg( } let obj = user.param_object(&pb.name); ( - none.clone(), + none, vec![ quote!(::weaveffi::abi::lower_object::<#obj>(::std::sync::Arc::clone(#borrowed))), ], - none, ) } Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => { if !user.param_wants_arc(&pb.name) { return Err(unsupported( + user.param_span(&pb.name), &pb.name, "callback-interface object parameter that is not an `Option>`", )); } let obj = user.param_object(&pb.name); ( - none.clone(), - vec![quote!(::weaveffi::abi::lower_object_opt::<#obj>(#n.clone()))], none, + vec![ + quote!(::weaveffi::abi::lower_object_opt::<#obj>(::std::clone::Clone::clone(#borrowed))), + ], ) } - _ => return Err(unsupported(&pb.name, "callback-interface parameter type")), + _ => { + return Err(unsupported( + user.param_span(&pb.name), + &pb.name, + "callback-interface parameter type", + )) + } }) } -/// The expression that lifts a vtable entry's return value `__wv_ret` into -/// the trait method's return type. Callback returns are direct-family only. -fn lift_callback_ret(ret: Option<&Ty>, user: &UserSig<'_>) -> syn::Result { +/// The lift of a successful vtable return `__wv_ret` into the method's value +/// type, as `(lift, fallback)`. `lift` evaluates to +/// `Result` (an out-of-range enum discriminant is a +/// marshalling failure); `fallback` is the value a plain-`T` method returns +/// after raising a failure on a `panic = "abort"` build. Callback returns are +/// direct-family only. +fn lift_callback_ret( + ret: Option<&Ty>, + user: &UserSig<'_>, +) -> syn::Result<(TokenStream, TokenStream)> { Ok(match ret { - None => TokenStream::new(), + None => (quote!(::std::result::Result::Ok(())), TokenStream::new()), Some(Ty::Enum(name)) => { let et = user .ret_object() .unwrap_or_else(|| rust_type_ident(name).into_token_stream()); - quote! { - match <#et>::__weaveffi_from_i32(__wv_ret) { - ::std::option::Option::Some(__wv_e) => __wv_e, - ::std::option::Option::None => { - ::weaveffi::abi::raise_foreign_error(::weaveffi::abi::ForeignError { + ( + quote! { + <#et>::__weaveffi_from_i32(__wv_ret).ok_or_else(|| { + ::weaveffi::abi::ForeignError { code: ::weaveffi::abi::MARSHAL_ERROR_CODE, message: ::std::string::String::from( "callback interface returned an invalid enum discriminant", ), - }); - <#et>::__weaveffi_placeholder() - } - } - } + } + }) + }, + quote!(<#et>::__weaveffi_placeholder()), + ) + } + Some(ty) if is_copy(ty) => ( + quote!(::std::result::Result::Ok(__wv_ret)), + quote!(__wv_ret), + ), + Some(_) => { + return Err(unsupported( + user.ret_span(), + "callback return", + "non-direct return type", + )) } - Some(ty) if is_copy(ty) => quote!(__wv_ret), - Some(_) => return Err(unsupported("callback return", "non-direct return type")), }) } @@ -137,25 +155,39 @@ fn gen_foreign_method(m: &CallbackMethodBinding, sig: &syn::Signature) -> syn::R let field = ident(&m.name); let mut pre = TokenStream::new(); let mut c_args: Vec = Vec::new(); - let mut cleanup = TokenStream::new(); for pb in &m.params { - let (p, args, free) = lower_callback_arg(pb, &user)?; + let (p, args) = lower_callback_arg(pb, &user)?; pre.extend(p); c_args.extend(args); - cleanup.extend(free); } - let lifted = lift_callback_ret(m.ret.as_ref(), &user)?; + let (lift, fallback) = lift_callback_ret(m.ret.as_ref(), &user)?; + let result = quote! { + ::weaveffi::abi::foreign_status(&__wv_err).and_then(|()| #lift) + }; + let tail = if user.returns_result() { + result + } else { + quote! { + match #result { + ::std::result::Result::Ok(__wv_v) => __wv_v, + ::std::result::Result::Err(__wv_e) => { + ::weaveffi::abi::raise_foreign_error(__wv_e); + #fallback + } + } + } + }; Ok(quote! { - #[allow(unsafe_code, unused_variables, clippy::let_unit_value)] + #[allow(unsafe_code, unused_variables, clippy::let_unit_value, clippy::unit_arg)] #sig { #pre - let mut __wv_err = ::weaveffi::abi::weaveffi_error::default(); + let mut __wv_err = ::weaveffi::abi::FfiError::default(); + // SAFETY: the vtable and `ctx` are live for the life of the + // `ForeignCallback`, and every argument outlives the call. let __wv_ret = unsafe { (self.0.vtable().#field)(self.0.ctx(), #(#c_args,)* &mut __wv_err) }; - #cleanup - ::weaveffi::abi::check_foreign_error(__wv_err); - #lifted + #tail } }) } @@ -174,6 +206,7 @@ fn gen_foreign_method(m: &CallbackMethodBinding, sig: &syn::Signature) -> syn::R pub(crate) fn gen_callback_interface( cb: &CallbackInterfaceBinding, item: &syn::ItemTrait, + prefix: &str, ) -> syn::Result { let vt = ident(&cb.vtable_tag); let trait_ident = ident(&cb.name); @@ -191,7 +224,7 @@ pub(crate) fn gen_callback_interface( }) .ok_or_else(|| { syn::Error::new( - Span::call_site(), + item.span(), format!( "internal error: no source for callback method `{}::{}`", cb.name, m.name @@ -203,9 +236,9 @@ pub(crate) fn gen_callback_interface( let slots = m .abi_params .iter() - .map(|p| slot_type_for(p, &m.params, &user)) + .map(|p| slot_type_for(p, &m.params, &user, prefix)) .collect::>>()?; - let arrow = ret_arrow(&m.abi_ret); + let arrow = ret_arrow(&m.abi_ret, prefix); fields.push(quote!(pub #field: unsafe extern "C" fn(#(#slots),*) #arrow,)); methods.push(gen_foreign_method(m, sig)?); } diff --git a/crates/weaveffi-macros/src/codegen/enums.rs b/crates/weaveffi-macros/src/codegen/enums.rs index 5cd85f8e..223d001c 100644 --- a/crates/weaveffi-macros/src/codegen/enums.rs +++ b/crates/weaveffi-macros/src/codegen/enums.rs @@ -7,9 +7,9 @@ //! `i32` tag followed by the active variant's fields in declaration order, //! so its whole generated surface is one `BufferValue` impl. -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::quote; -use weaveffi_core::model::EnumBinding; +use weaveffi_model::model::EnumBinding; use super::helpers::{ident, rust_type_ident}; use super::marshal::{field_read_expr, field_write_stmt_ref}; @@ -20,14 +20,8 @@ use super::marshal::{field_read_expr, field_write_stmt_ref}; /// /// `__weaveffi_to_i32` takes `&self` so a thunk can lower an enum it holds by /// value or by reference without requiring `Copy` or `Clone`. -pub(crate) fn gen_enum(e: &EnumBinding, item: Option<&syn::ItemEnum>) -> syn::Result { +pub(crate) fn gen_enum(e: &EnumBinding, item: &syn::ItemEnum) -> syn::Result { if e.is_rich() { - let item = item.ok_or_else(|| { - syn::Error::new( - Span::call_site(), - format!("internal error: no source enum for rich enum `{}`", e.name), - ) - })?; return gen_rich_enum(e, item); } let ty = rust_type_ident(&e.name); @@ -42,9 +36,9 @@ pub(crate) fn gen_enum(e: &EnumBinding, item: Option<&syn::ItemEnum>) -> syn::Re quote!(Self::#vident => #value,) }); let first = e.variants.first().map(|v| ident(&v.name)).ok_or_else(|| { - syn::Error::new( - Span::call_site(), - format!("internal error: enum `{}` has no variants", e.name), + syn::Error::new_spanned( + &item.ident, + format!("weaveffi: enum `{}` must have at least one variant", e.name), ) })?; Ok(quote! { @@ -76,6 +70,9 @@ pub(crate) fn gen_enum(e: &EnumBinding, item: Option<&syn::ItemEnum>) -> syn::Re } impl ::weaveffi::abi::BufferValue for #ty { + fn encoded_len(&self) -> usize { + 4 + } fn write_value(&self, __wv_w: &mut ::weaveffi::abi::BufferWriter) { __wv_w.write_i32(self.__weaveffi_to_i32()); } @@ -108,6 +105,20 @@ fn gen_rich_enum(e: &EnumBinding, item: &syn::ItemEnum) -> syn::Result 4,) + } else { + let bindings: Vec = v.fields.iter().map(|f| ident(&f.name)).collect(); + quote! { + Self::#vident { #(#bindings),* } => { + 4 #(+ ::weaveffi::abi::BufferValue::encoded_len(#bindings))* + } + } + } + }); + let write_arms = e.variants.iter().map(|v| { let value = v.value; let vident = ident(&v.name); @@ -150,8 +161,12 @@ fn gen_rich_enum(e: &EnumBinding, item: &syn::ItemEnum) -> syn::Result usize { + match self { + #(#len_arms)* + } + } fn write_value(&self, __wv_w: &mut ::weaveffi::abi::BufferWriter) { match self { #(#write_arms)* @@ -171,5 +186,7 @@ fn gen_rich_enum(e: &EnumBinding, item: &syn::ItemEnum) -> syn::Result, +) -> TokenStream { + if unresolved.is_empty() { + return TokenStream::new(); + } + let mut finder = Finder { + unresolved, + seen: BTreeSet::new(), + out: TokenStream::new(), + }; + for item in items { + finder.exported_item(item); + } + finder.out +} + +struct Finder<'a> { + unresolved: &'a BTreeSet, + seen: BTreeSet, + out: TokenStream, +} + +impl Finder<'_> { + /// Visit the parts of `item` that reach the IR: exported signatures, + /// record and enum fields, and callback-interface methods (never bodies). + fn exported_item(&mut self, item: &syn::Item) { + use weaveffi_model::rust::has_marker; + match item { + syn::Item::Fn(f) if has_marker(&f.attrs, "export") => self.visit_signature(&f.sig), + syn::Item::Struct(s) if has_marker(&s.attrs, "record") => self.visit_fields(&s.fields), + syn::Item::Enum(e) + if has_marker(&e.attrs, "enumeration") || has_marker(&e.attrs, "error") => + { + for v in &e.variants { + self.visit_fields(&v.fields); + } + } + syn::Item::Trait(t) if has_marker(&t.attrs, "callback_interface") => { + for ti in &t.items { + if let syn::TraitItem::Fn(f) = ti { + self.visit_signature(&f.sig); + } + } + } + syn::Item::Impl(i) if i.trait_.is_none() => { + for ii in &i.items { + if let syn::ImplItem::Fn(f) = ii { + if matches!(f.vis, syn::Visibility::Public(_)) { + self.visit_signature(&f.sig); + } + } + } + } + _ => {} + } + } + + fn assert_by_value(&mut self, ty: &dyn ToTokens, span: proc_macro2::Span) { + let tokens = ty.to_token_stream(); + if !self.seen.insert(tokens.to_string()) { + return; + } + self.out.extend(quote_spanned! {span=> + const _: () = { + const fn __weaveffi_assert_by_value< + T: ::weaveffi::abi::ByValue + ?::std::marker::Sized, + >() { + } + __weaveffi_assert_by_value::<#tokens>(); + }; + }); + } + + fn is_unresolved(&self, name: &str) -> bool { + self.unresolved.contains(name) + } +} + +impl<'ast> Visit<'ast> for Finder<'_> { + fn visit_type_path(&mut self, tp: &'ast syn::TypePath) { + if tp.qself.is_none() { + if let Some(last) = tp.path.segments.last() { + if self.is_unresolved(&last.ident.to_string()) { + self.assert_by_value(tp, tp.span()); + return; + } + } + } + syn::visit::visit_type_path(self, tp); + } + + fn visit_type_trait_object(&mut self, obj: &'ast syn::TypeTraitObject) { + let named = obj.bounds.iter().any(|b| match b { + syn::TypeParamBound::Trait(t) => t + .path + .segments + .last() + .is_some_and(|s| self.is_unresolved(&s.ident.to_string())), + _ => false, + }); + if named { + self.assert_by_value(obj, obj.span()); + return; + } + syn::visit::visit_type_trait_object(self, obj); + } + + // Never descend into bodies or default method implementations. + fn visit_block(&mut self, _: &'ast syn::Block) {} +} diff --git a/crates/weaveffi-macros/src/codegen/helpers.rs b/crates/weaveffi-macros/src/codegen/helpers.rs index 3e002b51..f10a53ea 100644 --- a/crates/weaveffi-macros/src/codegen/helpers.rs +++ b/crates/weaveffi-macros/src/codegen/helpers.rs @@ -6,10 +6,8 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use syn::spanned::Spanned as _; use syn::Ident; -use weaveffi_core::abi::{AbiParam, CType, ConstPos}; -use weaveffi_core::model::{ParamBinding, Ty}; - -use super::PREFIX; +use weaveffi_model::abi::{AbiParam, CType, ConstPos}; +use weaveffi_model::model::{ParamBinding, Ty}; /// Make a call-site identifier from a string. pub(crate) fn ident(name: &str) -> Ident { @@ -20,12 +18,13 @@ pub(crate) fn ident(name: &str) -> Ident { /// Render a [`CType`] as the Rust spelling a producer thunk uses. /// -/// This mirrors [`CType::render_rust`] for every slot except opaque object -/// pointers: a struct tag resolves to the producer's *real* Rust type (the -/// `Arc`-allocated object), which is ABI-identical to the header's incomplete -/// tag. Callers that know the producer's written type prefer -/// [`slot_type_for`], which keeps a `super::T` path in scope. -pub(crate) fn ctype_to_rust(ct: &CType) -> TokenStream { +/// This mirrors [`CType::render_rust`] except for the runtime types (the +/// error and cancel-token structs, which are `weaveffi::abi` types) and +/// opaque object pointers: a struct tag resolves to the producer's *real* +/// Rust type (the `Arc`-allocated object), which is ABI-identical to the +/// header's incomplete tag. Callers that know the producer's written type +/// prefer [`slot_type_for`], which keeps a `super::T` path in scope. +pub(crate) fn ctype_to_rust(ct: &CType, prefix: &str) -> TokenStream { match ct { CType::Int8 => quote!(i8), CType::Int16 => quote!(i16), @@ -41,8 +40,8 @@ pub(crate) fn ctype_to_rust(ct: &CType) -> TokenStream { CType::Size => quote!(usize), CType::Char => quote!(::std::os::raw::c_char), CType::Void => quote!(::std::ffi::c_void), - CType::CancelToken => quote!(::weaveffi::abi::weaveffi_cancel_token), - CType::Error => quote!(::weaveffi::abi::weaveffi_error), + CType::CancelToken => quote!(::weaveffi::abi::FfiCancelToken), + CType::Error => quote!(::weaveffi::abi::FfiError), CType::Enum { .. } => quote!(i32), CType::StructTag { name, .. } => { let ty = ident(name); @@ -52,11 +51,11 @@ pub(crate) fn ctype_to_rust(ct: &CType) -> TokenStream { // interface vtable) render `{prefix}_...`, matching `render_rust` so // the slot type lines up with the alias or struct the macro emits. CType::Named(_) | CType::VtableTag { .. } => { - let ty = ident(&ct.render_rust(PREFIX)); + let ty = ident(&ct.render_rust(prefix)); quote!(#ty) } CType::Ptr { konst, pointee } => { - let inner = ctype_to_rust(pointee); + let inner = ctype_to_rust(pointee, prefix); match konst { ConstPos::None => quote!(*mut #inner), ConstPos::West => quote!(*const #inner), @@ -66,26 +65,36 @@ pub(crate) fn ctype_to_rust(ct: &CType) -> TokenStream { } /// Render one ABI slot as `name: ty`. -pub(crate) fn slot_tokens(p: &AbiParam) -> TokenStream { +pub(crate) fn slot_tokens(p: &AbiParam, prefix: &str) -> TokenStream { let n = ident(&p.name); - let t = ctype_to_rust(&p.ty); + let t = ctype_to_rust(&p.ty, prefix); quote!(#n: #t) } /// Render the `-> T` return clause for a lowered symbol (empty for `void`). -pub(crate) fn ret_arrow(ret: &CType) -> TokenStream { +pub(crate) fn ret_arrow(ret: &CType, prefix: &str) -> TokenStream { if matches!(ret, CType::Void) { TokenStream::new() } else { - let t = ctype_to_rust(ret); + let t = ctype_to_rust(ret, prefix); quote!(-> #t) } } -/// The zero/null value a fallible symbol returns on the error path. -pub(crate) fn sentinel(ret: &CType) -> TokenStream { - match ret { - CType::Void => quote!(()), +/// The statement that leaves a thunk early: `return sentinel;`, or a bare +/// `return;` for a `void` symbol (`sentinel` is `None`). +pub(crate) fn early_return(sentinel: Option<&TokenStream>) -> TokenStream { + match sentinel { + Some(s) => quote!(return #s;), + None => quote!(return;), + } +} + +/// The zero/null value a fallible symbol returns on the error path, or +/// `None` for a `void` symbol. +pub(crate) fn sentinel(ret: &CType) -> Option { + Some(match ret { + CType::Void => return None, CType::Ptr { konst: ConstPos::None, .. @@ -94,6 +103,32 @@ pub(crate) fn sentinel(ret: &CType) -> TokenStream { CType::Bool => quote!(false), CType::Float | CType::Double => quote!(0.0), _ => quote!(0), + }) +} + +/// The statement every thunk runs right after the producer's code returns: +/// if a consumer callback failure was deferred during the call (a +/// `panic = "abort"` build), report it through `out_err` and return +/// `sentinel`, dropping the producer's result unlowered. +pub(crate) fn deferred_failure_check(sentinel: Option<&TokenStream>) -> TokenStream { + let ret = early_return(sentinel); + quote! { + if let ::std::option::Option::Some(__wv_f) = ::weaveffi::abi::take_foreign_error() { + unsafe { ::weaveffi::abi::error_set(out_err, __wv_f.code, &__wv_f.message) }; + #ret + } + } +} + +/// The `error_set(out_err, MARSHAL_ERROR_CODE, msg); return sentinel;` tail a +/// synchronous thunk uses to reject an invalid input. +pub(crate) fn reject(msg: &str, sentinel: Option<&TokenStream>) -> TokenStream { + let ret = early_return(sentinel); + quote! { + unsafe { + ::weaveffi::abi::error_set(out_err, ::weaveffi::abi::MARSHAL_ERROR_CODE, #msg) + }; + #ret } } @@ -170,12 +205,50 @@ impl<'a> UserSig<'a> { }) } + /// The parameter's written type as tokens, to anchor a diagnostic on it. + pub(crate) fn param_span(&self, name: &str) -> TokenStream { + match self.param_type(name) { + Some(ty) => ty.to_token_stream(), + None => self.sig.ident.to_token_stream(), + } + } + + /// The written return type as tokens, to anchor a diagnostic on it. + pub(crate) fn ret_span(&self) -> TokenStream { + match &self.sig.output { + syn::ReturnType::Type(_, ty) => ty.to_token_stream(), + syn::ReturnType::Default => self.sig.ident.to_token_stream(), + } + } + /// Whether the parameter is written as a shared reference (`&T`), so the /// call lends the lifted value instead of moving it. pub(crate) fn param_is_ref(&self, name: &str) -> bool { matches!(self.param_type(name), Some(syn::Type::Reference(_))) } + /// Whether the parameter is written `&{elem}` for a bare `elem` (`&str`) + /// or a slice `&[elem]` (`&[u8]`), which the thunk can lend straight from + /// the caller's buffer without copying. + pub(crate) fn param_is_borrowed(&self, name: &str, elem: &str) -> bool { + let Some(syn::Type::Reference(r)) = self.param_type(name) else { + return false; + }; + match r.elem.as_ref() { + syn::Type::Path(p) => p.path.is_ident(elem), + syn::Type::Slice(s) => { + matches!(s.elem.as_ref(), syn::Type::Path(p) if p.path.is_ident(elem)) + } + _ => false, + } + } + + /// Whether the method returns `Result` (a callback + /// method that receives the consumer's failure as a value). + pub(crate) fn returns_result(&self) -> bool { + weaveffi_model::rust::output_is_result(&self.sig.output) + } + /// Whether the parameter's type (under any `&` and `Option`) is an /// `Arc<..>`, meaning the producer wants to retain the object. pub(crate) fn param_wants_arc(&self, name: &str) -> bool { @@ -213,15 +286,10 @@ impl<'a> UserSig<'a> { fn ret_syn(&self) -> Option<&'a syn::Type> { match &self.sig.output { syn::ReturnType::Default => None, - syn::ReturnType::Type(_, ty) => Some(weaveffi_bridge::peel_result(ty)), + syn::ReturnType::Type(_, ty) => Some(weaveffi_model::rust::peel_result(ty)), } } - /// The producer's return type with `Result` peeled, spelled for a thunk. - pub(crate) fn ret_type(&self) -> Option { - self.ret_syn().map(|t| self.spell(t)) - } - /// The producer's spelling of the object type behind an interface return /// (`Result`, `Option`, and `Arc` peeled). pub(crate) fn ret_object(&self) -> Option { @@ -238,6 +306,11 @@ impl<'a> UserSig<'a> { matches!(elem, syn::Type::Path(_)).then(|| self.spell(elem)) } + /// The element type `X` of an `Iter` return, spelled for a thunk. + pub(crate) fn iter_elem_type(&self) -> Option { + self.iter_elem().map(|t| self.spell(t)) + } + /// The element type `X` of an `Iter` return. fn iter_elem(&self) -> Option<&'a syn::Type> { let syn::Type::Path(p) = self.ret_syn()? else { @@ -383,6 +456,7 @@ pub(crate) fn slot_type_for( p: &AbiParam, params: &[ParamBinding], user: &UserSig<'_>, + prefix: &str, ) -> syn::Result { let n = if p.name == "self" { ident("__wv_self") @@ -415,7 +489,7 @@ pub(crate) fn slot_type_for( )); } } - let t = ctype_to_rust(&p.ty); + let t = ctype_to_rust(&p.ty, prefix); Ok(quote!(#n: #t)) } @@ -424,22 +498,47 @@ pub(crate) fn fn_slots( abi_params: &[AbiParam], params: &[ParamBinding], user: &UserSig<'_>, + prefix: &str, ) -> syn::Result> { abi_params .iter() - .map(|p| slot_type_for(p, params, user)) + .map(|p| slot_type_for(p, params, user, prefix)) .collect() } /// Render the `-> T` return clause, spelling an object return with the /// producer's own type. -pub(crate) fn ret_arrow_for(ret: &CType, ret_ty: Option<&Ty>, user: &UserSig<'_>) -> TokenStream { +pub(crate) fn ret_arrow_for( + ret: &CType, + ret_ty: Option<&Ty>, + user: &UserSig<'_>, + prefix: &str, +) -> TokenStream { if ret_ty.is_some_and(|t| t.interface_name().is_some()) { if let Some(obj) = user.ret_object() { return quote!(-> *mut #obj); } } - ret_arrow(ret) + ret_arrow(ret, prefix) +} + +/// The attributes every exported thunk carries. Thunks are `unsafe extern +/// "C"` (they trust the caller's pointers), hidden from the producer's docs, +/// and exempt from the lints generated code can't satisfy. +pub(crate) fn thunk_attrs() -> TokenStream { + quote! { + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow( + unsafe_code, + unused_unsafe, + deprecated, + clippy::missing_safety_doc, + clippy::needless_return, + clippy::let_unit_value, + clippy::unit_arg + )] + } } // ── call targets ───────────────────────────────────────────────────────── @@ -480,10 +579,15 @@ impl CallTarget { /// through `out_err`, and bind `__wv_obj` as a borrow (`&self`) or a /// retained reference (`self: Arc`). Empty for free functions and /// statics. - pub(crate) fn self_preamble(&self, sentinel: &TokenStream, as_arc: bool) -> TokenStream { + pub(crate) fn self_preamble( + &self, + sentinel: Option<&TokenStream>, + as_arc: bool, + ) -> TokenStream { let CallTarget::Method(ty) = self else { return TokenStream::new(); }; + let fail = reject("self is null", sentinel); let lift = if as_arc { quote!(unsafe { ::weaveffi::abi::object_arc::<#ty>(__wv_self) }) } else { @@ -492,14 +596,7 @@ impl CallTarget { quote! { let __wv_obj = match #lift { ::std::option::Option::Some(__wv_o) => __wv_o, - ::std::option::Option::None => { - ::weaveffi::abi::error_set( - out_err, - ::weaveffi::abi::MARSHAL_ERROR_CODE, - "self is null", - ); - return #sentinel; - } + ::std::option::Option::None => { #fail } }; } } @@ -508,35 +605,40 @@ impl CallTarget { /// Wrap a thunk body in `catch_unwind` so a producer panic is reported through /// `out_err` (with the reserved panic code, or the foreign code when the /// payload is a consumer callback's failure) instead of unwinding across the C -/// boundary and aborting the process. On a non-throwing function this is the -/// only way `out_err` can report failure, which consumers interpret per -/// [`weaveffi_core::plan::ErrorStrategy::Trap`]. `sentinel` is the value the -/// thunk returns on the panic path; pass `None` for a void thunk. +/// boundary. On a non-throwing function this is the only way `out_err` can +/// report failure, which consumers interpret per +/// [`weaveffi_model::plan::ErrorStrategy::Trap`]. `sentinel` is the value the +/// thunk returns on the panic path (`None` for a `void` thunk). /// -/// The success arm also drains `take_foreign_error`: on a `panic = "abort"` -/// build a consumer callback failure can't unwind, so `check_foreign_error` -/// records it and the producer runs to completion; the thunk then reports the -/// recorded failure in place of the result. +/// The body runs inside a [`ThunkScope`](weaveffi_abi::ThunkScope), so on a +/// `panic = "abort"` build a consumer failure deferred during the call is +/// recorded for this thunk (and checked by [`deferred_failure_check`] in the +/// body) rather than for some later, unrelated call. pub(crate) fn wrap_unwind(body: TokenStream, sentinel: Option<&TokenStream>) -> TokenStream { let tail = match sentinel { Some(s) => quote!(#s), None => TokenStream::new(), }; quote! { - match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(move || { - #body + let __wv_scope = ::weaveffi::abi::ThunkScope::enter(); + let __wv_out = match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(move || { + #[allow(unused_unsafe)] + unsafe { + #body + } })) { - ::std::result::Result::Ok(__wv_v) => match ::weaveffi::abi::take_foreign_error() { - ::std::option::Option::None => __wv_v, - ::std::option::Option::Some(__wv_foreign) => { - ::weaveffi::abi::error_set(out_err, __wv_foreign.code, &__wv_foreign.message); - #tail - } - }, + ::std::result::Result::Ok(__wv_v) => __wv_v, ::std::result::Result::Err(__wv_panic) => { - ::weaveffi::abi::error_set_panic(out_err, &*__wv_panic); + unsafe { + ::weaveffi::abi::error_store( + out_err, + ::weaveffi::abi::FfiError::from_panic(&*__wv_panic), + ) + }; #tail } - } + }; + ::std::mem::drop(__wv_scope); + __wv_out } } diff --git a/crates/weaveffi-macros/src/codegen/interfaces.rs b/crates/weaveffi-macros/src/codegen/interfaces.rs index 63068547..3da1ce28 100644 --- a/crates/weaveffi-macros/src/codegen/interfaces.rs +++ b/crates/weaveffi-macros/src/codegen/interfaces.rs @@ -9,9 +9,9 @@ use proc_macro2::TokenStream; use quote::quote; -use weaveffi_core::model::InterfaceBinding; +use weaveffi_model::model::InterfaceBinding; -use super::helpers::ident; +use super::helpers::{ident, thunk_attrs}; /// Generate the lifecycle surface for one interface: /// @@ -33,6 +33,7 @@ pub(crate) fn gen_interface_lifecycle(i: &InterfaceBinding) -> TokenStream { thread; wrap interior state in Mutex/RwLock/atomics", i.name ); + let attrs = thunk_attrs(); quote! { const _: () = { #[doc = #assert_msg] @@ -40,15 +41,13 @@ pub(crate) fn gen_interface_lifecycle(i: &InterfaceBinding) -> TokenStream { __wv_assert_send_sync::<#ty>(); }; - #[no_mangle] - #[allow(unsafe_code, clippy::not_unsafe_ptr_arg_deref)] - pub extern "C" fn #clone_sym(ptr: *const #ty) -> *mut #ty { + #attrs + pub unsafe extern "C" fn #clone_sym(ptr: *const #ty) -> *mut #ty { unsafe { ::weaveffi::abi::object_clone(ptr) } } - #[no_mangle] - #[allow(unsafe_code, clippy::not_unsafe_ptr_arg_deref)] - pub extern "C" fn #destroy_sym(ptr: *mut #ty) { + #attrs + pub unsafe extern "C" fn #destroy_sym(ptr: *mut #ty) { unsafe { ::weaveffi::abi::object_destroy(ptr) } } } diff --git a/crates/weaveffi-macros/src/codegen/iterators.rs b/crates/weaveffi-macros/src/codegen/iterators.rs index c4de5e72..f9f7577c 100644 --- a/crates/weaveffi-macros/src/codegen/iterators.rs +++ b/crates/weaveffi-macros/src/codegen/iterators.rs @@ -2,38 +2,47 @@ //! `_destroy` trio. //! //! This is the producer half of the pull contract stated by -//! [`weaveffi_core::plan::IteratorProtocol`]: each `_next` call yields exactly +//! [`weaveffi_model::plan::IteratorProtocol`]: each `_next` call yields exactly //! one element the consumer then owns (and releases per the protocol's //! `elem` pass), and `_destroy` releases the handle exactly once. Errors from //! the launcher and from each `_next` follow the owning function's -//! [`ErrorStrategy`](weaveffi_core::plan::ErrorStrategy). +//! [`ErrorStrategy`](weaveffi_model::plan::ErrorStrategy). use proc_macro2::TokenStream; use quote::quote; -use weaveffi_core::model::{FnBinding, IteratorBinding}; +use weaveffi_model::model::{FnBinding, IteratorBinding}; -use super::helpers::{fn_slots, ident, slot_tokens, wrap_unwind, CallTarget, UserSig}; +use super::helpers::{ + deferred_failure_check, fn_slots, ident, reject, slot_tokens, thunk_attrs, wrap_unwind, + CallTarget, UserSig, +}; use super::marshal::{lift_param, lower_value}; use super::sync::throws; use super::unsupported; /// Generate the launcher / `_next` / `_destroy` trio for a function returning /// `iter`. The producer returns a `weaveffi::Iter` (optionally wrapped in -/// `Result`); the launcher boxes it behind the opaque iterator handle, `_next` -/// pulls one element and lowers it through `out_item`, and `_destroy` drops the -/// box. +/// `Result`); the launcher boxes it behind a +/// [`weaveffi_abi::IterHandle`], `_next` pulls one element under the +/// handle's lock and lowers it through `out_item`, and `_destroy` drops the +/// handle. pub(crate) fn gen_iterator_function( f: &FnBinding, it: &IteratorBinding, user: &UserSig<'_>, target: &CallTarget, + prefix: &str, ) -> syn::Result { - // The handle type is the producer's own `Iter` spelling (with `Result` - // peeled), so the element type, including its map flavor and any - // `super::` path, is exactly what the producer's iterator yields. - let iter_rust = user - .ret_type() - .ok_or_else(|| unsupported(&f.name, "iterator return without a source type"))?; + // The element type is the producer's own `Iter` spelling, so its map + // flavor and any `super::` path are exactly what the iterator yields. + let elem = user.iter_elem_type().ok_or_else(|| { + unsupported( + user.ret_span(), + &f.name, + "iterator return without an element type", + ) + })?; + let handle = quote!(::weaveffi::abi::IterHandle<#elem>); // The object pointee for an element that is an interface (`Arc` or // `Option>`), spelled the producer's way. let elem_object: Option = if it.elem.interface_name().is_some() { @@ -41,123 +50,112 @@ pub(crate) fn gen_iterator_function( } else { None }; + let attrs = thunk_attrs(); - // ── launcher: lift inputs, run the user fn, box the iterator ── + // ── launcher: lift inputs, run the producer's fn, box the iterator ── let launch_sym = ident(&it.launch.symbol); - let launch_params = fn_slots(&it.launch.params, &f.params, user)?; - let launch_sentinel = quote!(::std::ptr::null_mut()); + let launch_params = fn_slots(&it.launch.params, &f.params, user, prefix)?; + let null = quote!(::std::ptr::null_mut()); + let launch_sentinel = Some(&null); - let self_pre = target.self_preamble(&launch_sentinel, user.receiver_is_arc()); + let self_pre = target.self_preamble(launch_sentinel, user.receiver_is_arc()); let mut preamble = TokenStream::new(); let mut call_args: Vec = Vec::new(); for pb in &f.params { - let (pre, arg) = lift_param(pb, user, &launch_sentinel)?; + let (pre, arg) = lift_param(pb, user, launch_sentinel)?; preamble.extend(pre); call_args.push(arg); } let call = target.call(&f.name, &call_args); + let deferred = deferred_failure_check(launch_sentinel); let bind_iter = if throws(f) { quote! { - let __wv_iter = match #call { - ::std::result::Result::Ok(__v) => __v, + let __wv_iter = match __wv_out { + ::std::result::Result::Ok(__wv_v) => __wv_v, ::std::result::Result::Err(__wv_err) => { - ::weaveffi::abi::error_set_with_payload( + ::weaveffi::abi::error_store( out_err, - ::weaveffi::abi::ErrorReport::code(&__wv_err), - &::weaveffi::abi::ErrorReport::message(&__wv_err), - ::weaveffi::abi::ErrorReport::payload(&__wv_err), + ::weaveffi::abi::FfiError::from_report(&__wv_err), ); return ::std::ptr::null_mut(); } }; } } else { - quote!(let __wv_iter = #call;) + quote!(let __wv_iter = __wv_out;) }; let launch_body = wrap_unwind( quote! { #self_pre #preamble + let __wv_out = #call; + #deferred #bind_iter - ::weaveffi::abi::error_set_ok(out_err); - ::std::boxed::Box::into_raw(::std::boxed::Box::new(__wv_iter)) + ::weaveffi::abi::error_clear(out_err); + ::weaveffi::abi::iter_into_raw(__wv_iter) }, - Some(&launch_sentinel), + launch_sentinel, ); let launch = quote! { - #[no_mangle] - #[allow(unsafe_code, deprecated, clippy::not_unsafe_ptr_arg_deref, clippy::missing_safety_doc)] - pub extern "C" fn #launch_sym(#(#launch_params),*) -> *mut #iter_rust { + #attrs + pub unsafe extern "C" fn #launch_sym(#(#launch_params),*) -> *mut #handle { #launch_body } }; // ── next: pull one element, lower it into `out_item`, return 1/0 ── let next_sym = ident(&it.next.symbol); - // The first slot is the opaque `iter` handle (spelled with the real Rust - // type); the rest (`out_item`, any item out-params, `out_err`) lower - // straight from the model, except an object `out_item`, which uses the - // producer's pointee spelling. + // The first slot is the opaque handle (spelled with the real Rust type); + // the rest (`out_item`, any item out-params, `out_err`) lower straight + // from the model, except an object `out_item`, which uses the producer's + // pointee spelling. let rest_params: Vec = it.next.params[1..] .iter() .map(|p| match (&elem_object, p.name.as_str()) { (Some(obj), "out_item") => quote!(out_item: *mut *mut #obj), - _ => slot_tokens(p), + _ => slot_tokens(p, prefix), }) .collect(); - let item_lowered = lower_value(&it.elem, quote!(__wv_item), elem_object.as_ref())?; - let next_sentinel = quote!(0); + let item_lowered = lower_value(&it.elem, quote!(__wv_item), elem_object.as_ref(), user)?; + let zero = quote!(0); + let next_sentinel = Some(&zero); + let null_fail = reject("iterator or out_item is null", next_sentinel); + let deferred = deferred_failure_check(next_sentinel); let next_body = wrap_unwind( quote! { - if iter.is_null() || out_item.is_null() { - ::weaveffi::abi::error_set( - out_err, - ::weaveffi::abi::MARSHAL_ERROR_CODE, - "iterator or out_item is null", - ); - return 0; + if out_item.is_null() { + #null_fail } - let __wv_it = unsafe { &mut *iter }; - match ::std::iter::Iterator::next(__wv_it) { + let __wv_pulled = match ::weaveffi::abi::iter_next(iter) { + ::std::option::Option::Some(__wv_p) => __wv_p, + ::std::option::Option::None => { #null_fail } + }; + #deferred + ::weaveffi::abi::error_clear(out_err); + match __wv_pulled { ::std::option::Option::Some(__wv_item) => { - ::weaveffi::abi::error_set_ok(out_err); - let __wv_slot = #item_lowered; - unsafe { *out_item = __wv_slot }; + *out_item = #item_lowered; 1 } - ::std::option::Option::None => { - ::weaveffi::abi::error_set_ok(out_err); - 0 - } + ::std::option::Option::None => 0, } }, - Some(&next_sentinel), + next_sentinel, ); let next = quote! { - #[no_mangle] - #[allow(unsafe_code, clippy::not_unsafe_ptr_arg_deref, clippy::missing_safety_doc)] - pub extern "C" fn #next_sym(iter: *mut #iter_rust, #(#rest_params),*) -> i32 { + #attrs + pub unsafe extern "C" fn #next_sym(iter: *const #handle, #(#rest_params),*) -> i32 { #next_body } }; - // ── destroy: drop the box; a panicking user `Drop` is swallowed (there is - // no `out_err` slot to report through, and a destructor must not abort), - // and so is a foreign failure a `Drop` deferred by calling back into the - // consumer, which must not leak into the next unrelated thunk. The - // consumer calls this exactly once, per the iterator protocol's + // ── destroy: drop the handle exactly once, per the iterator protocol's // handle-lifecycle clause. ── let destroy_sym = ident(&it.destroy_symbol); let destroy = quote! { - #[no_mangle] - #[allow(unsafe_code, clippy::not_unsafe_ptr_arg_deref)] - pub extern "C" fn #destroy_sym(iter: *mut #iter_rust) { - if !iter.is_null() { - let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| { - unsafe { drop(::std::boxed::Box::from_raw(iter)) } - })); - let _ = ::weaveffi::abi::take_foreign_error(); - } + #attrs + pub unsafe extern "C" fn #destroy_sym(iter: *mut #handle) { + unsafe { ::weaveffi::abi::iter_destroy(iter) } } }; diff --git a/crates/weaveffi-macros/src/codegen/marshal.rs b/crates/weaveffi-macros/src/codegen/marshal.rs index e43edd15..981ecfb9 100644 --- a/crates/weaveffi-macros/src/codegen/marshal.rs +++ b/crates/weaveffi-macros/src/codegen/marshal.rs @@ -14,44 +14,19 @@ //! A callback interface arrives as `(ctx, vtable)` and is lifted into the //! `Arc` the producer takes. The remaining ownership rules here are //! the producer half of the contract stated by -//! [`weaveffi_core::plan::return_free`]. +//! [`weaveffi_model::plan::return_free`]. use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use weaveffi_core::abi::CType; -use weaveffi_core::model::{FieldBinding, ParamBinding, Ty}; +use weaveffi_model::abi::CType; +use weaveffi_model::model::{FieldBinding, ParamBinding, Ty}; -use super::helpers::{ident, is_copy, rust_type_ident, sentinel, UserSig}; +use super::helpers::{ + deferred_failure_check, early_return, ident, is_copy, reject, rust_type_ident, sentinel, + UserSig, +}; use super::unsupported; -/// The expression that reconstructs a borrowed value-buffer slice from a -/// buffered parameter's `(ptr, len)` slot pair. Null yields the empty slice, -/// which decodes only for types whose encoding can be zero bytes (none today: -/// even an empty list is four length bytes), so a null buffer is reported as -/// a decode failure rather than dereferenced. -pub(crate) fn buffer_slice_expr(ptr: &syn::Ident, len: &syn::Ident) -> TokenStream { - quote! { - if #ptr.is_null() { - &[][..] - } else { - unsafe { ::std::slice::from_raw_parts(#ptr, #len) } - } - } -} - -/// The `error_set(out_err, MARSHAL_ERROR_CODE, msg); return sentinel;` tail a -/// synchronous thunk uses to reject an invalid input. -fn reject(msg: &str, sentinel: &TokenStream) -> TokenStream { - quote! { - ::weaveffi::abi::error_set( - out_err, - ::weaveffi::abi::MARSHAL_ERROR_CODE, - #msg, - ); - return #sentinel; - } -} - /// The call argument for a lifted value bound to `name`: lent when the /// producer wrote `&T`, moved otherwise. Deref coercion turns `&String` into /// `&str`, `&Vec` into `&[u8]`, and `&Arc` into `&T` at the call. @@ -64,37 +39,36 @@ fn arg_for(name: &syn::Ident, is_ref: bool) -> TokenStream { } /// Generate the lift preamble and the call-argument expression for one -/// parameter of a synchronous thunk. +/// parameter of a synchronous thunk (or an iterator launcher). The preamble +/// runs inside the thunk's `unsafe` body. pub(crate) fn lift_param( pb: &ParamBinding, user: &UserSig<'_>, - sentinel: &TokenStream, + sentinel: Option<&TokenStream>, ) -> syn::Result<(TokenStream, TokenStream)> { let name = ident(&pb.name); let is_ref = user.param_is_ref(&pb.name); let arg = arg_for(&name, is_ref); - let msg = format!("{} is null or invalid", pb.name); - let fail = reject(&msg, sentinel); + let fail = reject(&format!("{} is null or invalid", pb.name), sentinel); + let ptr = ident(&format!("{}_ptr", pb.name)); + let len = ident(&format!("{}_len", pb.name)); // A buffered parameter is one `(const uint8_t*, size_t)` pair holding the // value serialized in the WeaveFFI buffer format. Decode it into the // owned Rust value the producer's signature names; the concrete type // (including the map flavor `HashMap`/`BTreeMap`) is inferred from the // 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. + // violation, reported with the reserved marshalling code so it can't + // shadow a domain's typed codes. 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); let decode_fail = reject(&format!("{}: malformed value buffer", pb.name), sentinel); let pre = quote! { - let #name = { - let __wv_buf: &[u8] = #slice; - match ::weaveffi::abi::decode_value(__wv_buf) { - ::std::result::Result::Ok(__v) => __v, + let #name = match ::weaveffi::abi::lift_byte_slice(#ptr, #len) { + ::std::option::Option::Some(__wv_buf) => match ::weaveffi::abi::decode_value(__wv_buf) { + ::std::result::Result::Ok(__wv_v) => __wv_v, ::std::result::Result::Err(_) => { #decode_fail } - } + }, + ::std::option::Option::None => { #fail } }; }; return Ok((pre, arg)); @@ -108,29 +82,31 @@ pub(crate) fn lift_param( .unwrap_or_else(|| rust_type_ident(enum_name).into_token_stream()); let pre = quote! { let #name = match <#et>::__weaveffi_from_i32(#name) { - ::std::option::Option::Some(__v) => __v, + ::std::option::Option::Some(__wv_v) => __wv_v, ::std::option::Option::None => { #fail } }; }; (pre, arg) } ty if is_copy(ty) => (TokenStream::new(), arg), - Ty::StringUtf8 => { + // `&str` and `&[u8]` borrow the caller's bytes for the call without + // copying; any other spelling (`String`, `&String`, `Vec`) gets + // an owned copy, lent when written as a reference. + Ty::StringUtf8 | Ty::Bytes => { + let (elem, borrow, copy) = if matches!(pb.ty, Ty::StringUtf8) { + ("str", quote!(lift_str), quote!(lift_string)) + } else { + ("u8", quote!(lift_byte_slice), quote!(lift_bytes)) + }; + let borrowed = user.param_is_borrowed(&pb.name, elem); + let lift = if borrowed { borrow } else { copy }; let pre = quote! { - let #name = match ::weaveffi::abi::c_ptr_to_string(#name) { - ::std::option::Option::Some(__s) => __s, + let #name = match ::weaveffi::abi::#lift(#ptr, #len) { + ::std::option::Option::Some(__wv_v) => __wv_v, ::std::option::Option::None => { #fail } }; }; - (pre, arg) - } - Ty::Bytes => { - let ptr = ident(&format!("{}_ptr", pb.name)); - let len = ident(&format!("{}_len", pb.name)); - ( - quote!(let #name = unsafe { ::weaveffi::abi::lift_bytes(#ptr, #len) };), - arg, - ) + (pre, if borrowed { quote!(#name) } else { arg }) } // An object parameter is borrowed for the call. The producer's own // spelling decides how it is lifted: `&T` borrows through the pointer @@ -140,6 +116,7 @@ pub(crate) fn lift_param( let wants_arc = user.param_wants_arc(&pb.name); if !wants_arc && !is_ref { return Err(unsupported( + user.param_span(&pb.name), &pb.name, "by-value interface parameter (accept `&T` to borrow the object for the \ call, or `Arc` to retain it)", @@ -151,7 +128,7 @@ pub(crate) fn lift_param( quote!(::weaveffi::abi::object_ref(#name)) }; let pre = quote! { - let #name = match unsafe { #lift } { + let #name = match #lift { ::std::option::Option::Some(__wv_o) => __wv_o, ::std::option::Option::None => { #fail } }; @@ -160,16 +137,15 @@ pub(crate) fn lift_param( let arg = if wants_arc { arg } else { quote!(#name) }; (pre, arg) } - // `Interface?` is the one optional that is not buffered: a nullable + // `Interface?` is the one optional that isn't buffered: a nullable // pointer, lifted to `Option<&T>` or `Option>`. Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => { - let wants_arc = user.param_wants_arc(&pb.name); - let lift = if wants_arc { + let lift = if user.param_wants_arc(&pb.name) { quote!(::weaveffi::abi::object_arc(#name)) } else { quote!(::weaveffi::abi::object_ref(#name)) }; - (quote!(let #name = unsafe { #lift };), arg) + (quote!(let #name = #lift;), arg) } // A callback interface is `(ctx, vtable)`; a null vtable is a contract // violation. The `dyn Trait` comes from the producer's `Arc` @@ -178,48 +154,46 @@ pub(crate) fn lift_param( let dyn_ty = user.param_callback(&pb.name)?; let ctx = ident(&format!("{}_ctx", pb.name)); let vtable = ident(&format!("{}_vtable", pb.name)); - let null_msg = format!("{}: null callback vtable", pb.name); - let fail = reject(&null_msg, sentinel); + let fail = reject(&format!("{}: null callback vtable", pb.name), sentinel); let pre = quote! { - let #name = match unsafe { - ::weaveffi::abi::lift_callback::<#dyn_ty>(#ctx, #vtable) - } { + let #name = match ::weaveffi::abi::lift_callback::<#dyn_ty>(#ctx, #vtable) { ::std::option::Option::Some(__wv_cb) => __wv_cb, ::std::option::Option::None => { #fail } }; }; (pre, arg) } - Ty::Iterator(_) => return Err(unsupported(&pb.name, "iterator parameter")), - _ => return Err(unsupported(&pb.name, "parameter type")), + _ => { + return Err(unsupported( + user.param_span(&pb.name), + &pb.name, + "parameter type", + )) + } }) } -/// Lower an owned Rust `value` of IR type `ty` into its C return expression. -/// `out_len` names the trailing length slot for the buffer-returning shapes; -/// `object` is the producer's spelling of the pointee for an object return -/// (see [`UserSig::ret_object`]), which pins the `Arc` the value converts -/// into. +/// Lower an owned Rust `value` of IR type `ty` into its C return expression +/// (evaluated inside the thunk's `unsafe` body). `out_len` names the trailing +/// length slot for the `(ptr, len)` shapes; `object` is the producer's +/// spelling of the pointee for an object return (see +/// [`UserSig::ret_object`]), which pins the `Arc` the value converts into. /// /// Every heap-owning lowering here creates the consumer obligation stated by -/// [`weaveffi_core::plan::return_free`]: strings are released with -/// `{prefix}_free_string`, byte and value buffers with `{prefix}_free_bytes`, -/// and object references with the type's `_destroy` symbol. +/// [`weaveffi_model::plan::return_free`]: strings, bytes, and value buffers +/// are released with `{prefix}_free_bytes`, object references with the +/// type's `_destroy` symbol. pub(crate) fn lower_value( ty: &Ty, value: TokenStream, object: Option<&TokenStream>, + user: &UserSig<'_>, ) -> 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 ty.is_buffered() { return Ok(quote! { - unsafe { - ::weaveffi::abi::lower_bytes( - ::weaveffi::abi::encode_value(&(#value)), - out_len, - ) - } + ::weaveffi::abi::lower_bytes(::weaveffi::abi::encode_value(&(#value)), out_len) }); } // `let __wv_p: *mut T = lower_object(v)` pins `T` so a `Self`/`Arc` @@ -231,25 +205,29 @@ pub(crate) fn lower_value( Ok(match ty { Ty::Enum(_) => quote!((#value).__weaveffi_to_i32()), t if is_copy(t) => value, - Ty::StringUtf8 => quote!(::weaveffi::abi::string_to_c_ptr(&(#value))), - Ty::Bytes => quote!(unsafe { ::weaveffi::abi::lower_bytes(#value, out_len) }), + // `Into` accepts both an owned value and a borrowed `&str`/`&[u8]`. + Ty::StringUtf8 => quote! { + ::weaveffi::abi::lower_string(::std::convert::Into::into(#value), out_len) + }, + Ty::Bytes => quote! { + ::weaveffi::abi::lower_bytes(::std::convert::Into::into(#value), out_len) + }, // A returned object hands the consumer one strong reference, which it - // releases with the type's `_destroy` symbol (`RetPass::Object` in the - // plan). The producer may return `Self`/`T` or `Arc`/`Arc`. + // releases with the type's `_destroy` symbol. The producer may return + // `Self`/`T` or `Arc`/`Arc`. Ty::Interface(_) => typed(quote!(::weaveffi::abi::lower_object(#value))), Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => { typed(quote!(::weaveffi::abi::lower_object_opt(#value))) } - Ty::Iterator(_) => return Err(unsupported("return", "iterator return")), - _ => return Err(unsupported("return", "return type")), + _ => return Err(unsupported(user.ret_span(), "return", "return type")), }) } -/// Assemble the call + error handling + return lowering for a function or -/// constructor whose `call` expression invokes the user's code. +/// Assemble the call, error handling, and return lowering for a callable +/// whose `call` expression invokes the producer's code. /// /// `is_throws` selects the `Result`-matching body; it comes from the plan's -/// [`ErrorStrategy`](weaveffi_core::plan::ErrorStrategy) (`Throws` routes the +/// [`ErrorStrategy`](weaveffi_model::plan::ErrorStrategy) (`Throws` routes the /// producer's `Err` through `out_err` as a typed domain error, carrying the /// matched code's serialized payload fields; `Trap` leaves `out_err` to the /// panic path only). @@ -263,49 +241,38 @@ pub(crate) fn build_call_body( let sentinel = sentinel(ret_ctype); let object = user.ret_object(); let lowered = match ret_ty { - Some(ty) => lower_value(ty, quote!(__wv_ret), object.as_ref())?, - None => quote!(()), - }; - let ok_arm = if ret_ty.is_some() { - quote! {{ ::weaveffi::abi::error_set_ok(out_err); #lowered }} - } else { - quote! {{ ::weaveffi::abi::error_set_ok(out_err); }} + Some(ty) => lower_value(ty, quote!(__wv_ret), object.as_ref(), user)?, + None => TokenStream::new(), }; - - Ok(if is_throws { - // A `Result<(), E>` thunk returns void, so its `Err` arm must stop at the - // `error_set_with_payload` statement; emitting the void sentinel `()` - // would leave a bare trailing unit that trips clippy's `unused_unit`. - let (bind, err_sentinel) = if ret_ty.is_some() { - (quote!(__wv_ret), quote!(#sentinel)) - } else { - (quote!(_), TokenStream::new()) - }; + let deferred = deferred_failure_check(sentinel.as_ref()); + let err_return = early_return(sentinel.as_ref()); + let bind = if is_throws { quote! { - match #call { - ::std::result::Result::Ok(#bind) => #ok_arm, + let __wv_ret = match __wv_out { + ::std::result::Result::Ok(__wv_v) => __wv_v, ::std::result::Result::Err(__wv_err) => { - ::weaveffi::abi::error_set_with_payload( + ::weaveffi::abi::error_store( out_err, - ::weaveffi::abi::ErrorReport::code(&__wv_err), - &::weaveffi::abi::ErrorReport::message(&__wv_err), - ::weaveffi::abi::ErrorReport::payload(&__wv_err), + ::weaveffi::abi::FfiError::from_report(&__wv_err), ); - #err_sentinel + #err_return } - } - } - } else if ret_ty.is_some() { - quote! { - let __wv_ret = #call; - ::weaveffi::abi::error_set_ok(out_err); - #lowered + }; } } else { - quote! { - #call; - ::weaveffi::abi::error_set_ok(out_err); - } + quote!(let __wv_ret = __wv_out;) + }; + let tail = if ret_ty.is_some() { + quote!(#lowered) + } else { + quote!(let () = __wv_ret;) + }; + Ok(quote! { + let __wv_out = #call; + #deferred + #bind + ::weaveffi::abi::error_clear(out_err); + #tail }) } diff --git a/crates/weaveffi-macros/src/codegen/mod.rs b/crates/weaveffi-macros/src/codegen/mod.rs index 434e0636..90112717 100644 --- a/crates/weaveffi-macros/src/codegen/mod.rs +++ b/crates/weaveffi-macros/src/codegen/mod.rs @@ -1,24 +1,26 @@ //! Lower a `#[weaveffi::module]` to its IR and emit the C ABI thunks. //! -//! The flow mirrors the rest of WeaveFFI: parse the annotated module to the IR -//! via [`weaveffi_bridge`], validate/resolve it, build the canonical +//! The flow mirrors the rest of WeaveFFI: parse the annotated module tree to +//! the IR via [`weaveffi_model::rust`], resolve it, build the canonical //! [`BindingModel`], then render each lowered symbol. Signatures come straight //! from the model (so they match the generated header by construction); only -//! the body marshalling - which lifts each ABI slot into a Rust value, calls -//! the user's safe function, and lowers the result - is new here, and every +//! the body marshalling (lift each ABI slot into a Rust value, call the +//! producer's safe function, lower the result) is new here, and every //! `unsafe` operation in it bottoms out in a `weaveffi-abi` helper. //! //! The emission is split by surface: [`sync`] for synchronous callables, //! [`async_fns`] for `async fn` launchers, [`iterators`] for `iter` trios, //! [`records`] and [`enums`] for the generated `BufferValue` serialization //! impls of value types, [`interfaces`] for the object reference-count -//! symbols, and [`callbacks`] for callback-interface vtables and foreign -//! wrappers. [`helpers`] and [`marshal`] hold the shared slot rendering and -//! lift/lower machinery. +//! symbols, [`callbacks`] for callback-interface vtables and foreign +//! wrappers, and [`foreign`] for the compile-time checks on types declared in +//! another module tree. [`helpers`] and [`marshal`] hold the shared slot +//! rendering and lift/lower machinery. mod async_fns; mod callbacks; mod enums; +mod foreign; mod helpers; mod interfaces; mod iterators; @@ -26,23 +28,33 @@ mod marshal; mod records; mod sync; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; 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, CURRENT_SCHEMA_VERSION}; +use quote::{quote, quote_spanned, ToTokens}; +use weaveffi_model::ir::{Api, CURRENT_SCHEMA_VERSION}; +use weaveffi_model::model::{BindingModel, ErrorBinding, ModuleBinding}; +use weaveffi_model::plan::ErrorStrategy; -use self::helpers::{ident, CallTarget}; +pub(crate) use self::helpers::ident; +use self::helpers::CallTarget; use self::sync::gen_function; -/// The single global ABI prefix; matches the CLI's default so the macro and +/// The C symbol prefix of the crate being expanded: its crate name, which is +/// also what the CLI derives for a `.rs` input, so the macro and /// `weaveffi generate` emit identical symbols. -pub(crate) const PREFIX: &str = "weaveffi"; +pub(crate) fn prefix() -> syn::Result { + std::env::var("CARGO_CRATE_NAME").map_err(|_| { + syn::Error::new( + Span::call_site(), + "weaveffi: CARGO_CRATE_NAME is not set; WeaveFFI names every C symbol after the \ + crate, so build the crate with cargo", + ) + }) +} -/// Expand a `#[weaveffi::module]` into the original module plus generated -/// thunks. +/// Expand a top-level `#[weaveffi::module]` into the original module plus +/// generated thunks. pub fn expand_module(item_mod: &syn::ItemMod) -> syn::Result { if item_mod.content.is_none() { return Err(syn::Error::new_spanned( @@ -51,119 +63,204 @@ pub fn expand_module(item_mod: &syn::ItemMod) -> syn::Result { )); } - // 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 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 api = weaveffi_core::ResolvedApi::assume_valid(Api { + // 1. Lower the whole tree to IR through the shared bridge (it recurses + // into nested `#[weaveffi::module]` submodules). The contract checksum + // is computed from this IR, exactly as the CLI computes it from the + // same source. + let module_ir = weaveffi_model::rust::module_from_item_mod(item_mod)?; + let checksum = weaveffi_model::checksum::module_checksum(&module_ir); + let prefix = prefix()?; + + // A module tree is expanded in isolation, so a type declared in a + // *sibling* tree (`orders` using `products::Product`) can't be + // resolved here. `assume_valid` lowers such names as value buffers and + // `unresolved` lists them; `foreign` asserts each one really is a + // record or rich enum, so the thunk agrees with the CLI's header. + // Whole-API rule checks stay with the CLI's `validate`. + let api = weaveffi_model::ResolvedApi::assume_valid(Api { version: CURRENT_SCHEMA_VERSION.to_string(), modules: vec![module_ir], - }); + }) + .with_identity(weaveffi_model::pkg::Identity::named(&prefix)); + let unresolved: BTreeSet = api.unresolved().into_iter().collect(); + check_callback_returns(item_mod, &api)?; - // 2. Build the canonical lowered model. Nested modules are flattened into - // one binding each, keyed here by their path segments (e.g. `["kv", - // "stats"]`) so the recursive rebuild can match each `mod` to its - // binding and emit symbols under the right `weaveffi___` prefix. - let model = BindingModel::build(&api, PREFIX); + // 2. Build the lowered model. Nested modules are flattened into one + // binding each, keyed here by their path segments so the recursive + // rebuild can match each `mod` to its binding. + // The lowering asserts invariants the CLI's validation guarantees; the + // checks above cover the ones a producer can reach, and this turns any + // other violation into a compile error instead of a macro panic. + let model = std::panic::catch_unwind(|| BindingModel::build(&api)).map_err(|payload| { + let why = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .unwrap_or("invalid module"); + syn::Error::new_spanned( + &item_mod.ident, + format!("weaveffi: this module can't be lowered to the C ABI: {why}"), + ) + })?; let by_path: HashMap, &ModuleBinding> = model .modules .iter() .map(|m| (m.segments.clone(), m)) .collect(); - // 3. Rebuild the module tree, injecting each module's thunks into its own - // body and stripping inner `#[weaveffi::module]` markers so nested - // modules expand here (with the correct prefix) instead of re-expanding - // standalone under the wrong one. - rebuild_module(item_mod, &[], &by_path) + // 3. Rebuild the tree, injecting each module's thunks into its own body + // and stripping inner `#[weaveffi::module]` markers so nested modules + // expand here (with the right symbol path) instead of standalone. + let ctx = Expansion { + prefix: &prefix, + by_path: &by_path, + unresolved: &unresolved, + }; + let checksum_sym = ident(&format!("{prefix}_{}_checksum", item_mod.ident)); + let checksum_fn = quote! { + /// The contract checksum of this module tree, which generated + /// bindings compare with the value they were generated against. + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code)] + pub extern "C" fn #checksum_sym() -> u64 { + #checksum + } + }; + ctx.rebuild_module(item_mod, &[], checksum_fn) } -/// Re-emit `item_mod` with its generated thunks appended, recursing into nested -/// `#[weaveffi::module]` submodules. -fn rebuild_module( +/// Reject callback-interface method returns the ABI can't carry (the CLI's +/// validation reports the same rule for IDL input): a method returns nothing +/// or a direct value (a number, `bool`, or C-style enum), optionally wrapped +/// as `Result`. +fn check_callback_returns( item_mod: &syn::ItemMod, - parent_segments: &[String], - by_path: &HashMap, &ModuleBinding>, -) -> syn::Result { + api: &weaveffi_model::ResolvedApi, +) -> syn::Result<()> { + use weaveffi_model::model::Family; + use weaveffi_model::rust::{ + has_marker, output_is_result, return_type_from_syn, returns_foreign_result, + }; let Some((_, items)) = &item_mod.content else { - return Err(syn::Error::new_spanned( - item_mod, - "#[weaveffi::module] requires an inline module body (`mod foo { ... }`)", - )); + return Ok(()); }; - - let mut segments = parent_segments.to_vec(); - segments.push(item_mod.ident.to_string()); - let mb = by_path.get(&segments).ok_or_else(|| { - syn::Error::new_spanned( - &item_mod.ident, - "internal error: module has no lowered binding", - ) - })?; - - let generated = render_symbols(mb, items, &item_mod.ident)?; - - // Pass items through verbatim, except nested `#[weaveffi::module]`s, which - // we expand inline (recursively) with their marker stripped. - let mut body = TokenStream::new(); for item in items { - if let syn::Item::Mod(child) = item { - if weaveffi_bridge::has_marker(&child.attrs, "module") && child.content.is_some() { - body.extend(rebuild_module(child, &segments, by_path)?); - continue; + match item { + syn::Item::Trait(t) if has_marker(&t.attrs, "callback_interface") => { + for ti in &t.items { + let syn::TraitItem::Fn(f) = ti else { + continue; + }; + let syn::ReturnType::Type(_, ret) = &f.sig.output else { + continue; + }; + if output_is_result(&f.sig.output) && !returns_foreign_result(&f.sig.output) { + return Err(syn::Error::new_spanned( + ret, + "weaveffi: a callback interface method can return \ + `Result` (to receive the consumer's \ + failure as a value) but no other `Result`", + )); + } + let direct = return_type_from_syn(&f.sig.output)? + .is_none_or(|r| matches!(api.resolve(&r).family(), Family::Direct)); + if !direct { + return Err(syn::Error::new_spanned( + ret, + "weaveffi: a callback interface method can only return a number, \ + `bool`, or C-style enum (or nothing)", + )); + } + } } + syn::Item::Mod(m) if has_marker(&m.attrs, "module") => check_callback_returns(m, api)?, + _ => {} } - body.extend(quote!(#item)); } + Ok(()) +} - let attrs = item_mod - .attrs - .iter() - .filter(|a| !is_module_marker(a)) - .collect::>(); - let vis = &item_mod.vis; - let mod_token = &item_mod.mod_token; - let name = &item_mod.ident; - Ok(quote! { - #(#attrs)* - #vis #mod_token #name { - #body +/// What every module in one expansion shares. +struct Expansion<'a> { + prefix: &'a str, + by_path: &'a HashMap, &'a ModuleBinding>, + unresolved: &'a BTreeSet, +} - #generated +impl Expansion<'_> { + /// Re-emit `item_mod` with its generated thunks (plus `extra`) appended, + /// recursing into nested `#[weaveffi::module]` submodules. + fn rebuild_module( + &self, + item_mod: &syn::ItemMod, + parent_segments: &[String], + extra: TokenStream, + ) -> syn::Result { + let Some((_, items)) = &item_mod.content else { + return Err(syn::Error::new_spanned( + item_mod, + "#[weaveffi::module] requires an inline module body (`mod foo { ... }`)", + )); + }; + + let mut segments = parent_segments.to_vec(); + segments.push(item_mod.ident.to_string()); + let mb = self.by_path.get(&segments).ok_or_else(|| { + syn::Error::new_spanned( + &item_mod.ident, + "internal error: module has no lowered binding", + ) + })?; + + let mut generated = render_symbols(mb, items, &item_mod.ident, self.prefix)?; + generated.extend(foreign::by_value_assertions(items, self.unresolved)); + + // Pass items through verbatim, except nested `#[weaveffi::module]`s, + // which expand inline (recursively) with their marker stripped. + let mut body = TokenStream::new(); + for item in items { + if let syn::Item::Mod(child) = item { + if weaveffi_model::rust::has_marker(&child.attrs, "module") + && child.content.is_some() + { + body.extend(self.rebuild_module(child, &segments, TokenStream::new())?); + continue; + } + } + body.extend(quote!(#item)); } - }) -} -/// Whether `attr` is the `#[weaveffi::module]` marker (matched by final path -/// segment, mirroring the bridge so detection and stripping stay symmetric). -fn is_module_marker(attr: &syn::Attribute) -> bool { - attr.path() - .segments - .last() - .is_some_and(|s| s.ident == "module") + let attrs = item_mod + .attrs + .iter() + .filter(|a| !weaveffi_model::rust::has_marker(std::slice::from_ref(a), "module")); + let vis = &item_mod.vis; + let mod_token = &item_mod.mod_token; + let name = &item_mod.ident; + Ok(quote! { + #(#attrs)* + #vis #mod_token #name { + #body + + #generated + #extra + } + }) + } } /// Render every C ABI symbol for one lowered module binding. /// /// `items` are the syn items directly in that module's body; they are indexed /// so body marshalling can read reference-ness and `Result` returns from the -/// user's signatures, and record / enum codegen can read the producer's real -/// field and variant types. `mod_ident` is used only for error spans. +/// producer's signatures, and record and enum codegen can read the real +/// field and variant types. `mod_ident` anchors module-level diagnostics. fn render_symbols( mb: &ModuleBinding, items: &[syn::Item], mod_ident: &syn::Ident, + prefix: &str, ) -> syn::Result { let mut fns: HashMap = HashMap::new(); let mut enums: HashMap = HashMap::new(); @@ -204,16 +301,19 @@ fn render_symbols( }; // Mirror the CLI's validation: a callable whose error strategy is - // `Throws` (see `weaveffi_core::plan::ErrorStrategy`) reports typed - // domain errors through `out_err`, so an error domain must be in scope - // for its codes to have a C spelling. + // `Throws` reports typed domain errors through `out_err`, so an error + // domain must be in scope for its codes to have a C spelling. if mb.error.is_none() { if let Some(t) = mb .callables() .find(|f| f.error_strategy() == ErrorStrategy::Throws) { + let at = fns.get(&t.name).map_or_else( + || mod_ident.to_token_stream(), + |f| f.sig.output.to_token_stream(), + ); return Err(syn::Error::new_spanned( - mod_ident, + at, format!( "weaveffi: `{}` returns a Result but no error domain is in scope; declare \ a #[weaveffi::error] enum in this module (or a parent module)", @@ -225,19 +325,23 @@ fn render_symbols( let mut generated = TokenStream::new(); if let Some(eb) = mb.error.as_ref().filter(|e| e.declared_here) { - generated.extend(gen_error_report(eb)); + let item = enums + .get(&eb.name) + .ok_or_else(|| missing("error domain", &eb.name))?; + generated.extend(gen_error_report(eb, item)); } for e in &mb.enums { - generated.extend(enums::gen_enum(e, enums.get(&e.name).copied())?); + let item = enums.get(&e.name).ok_or_else(|| missing("enum", &e.name))?; + generated.extend(enums::gen_enum(e, item)?); } for s in &mb.structs { - generated.extend(records::gen_record(s)?); + generated.extend(records::gen_record(s)); } for c in &mb.callback_interfaces { let item = traits .get(&c.name) .ok_or_else(|| missing("callback interface", &c.name))?; - generated.extend(callbacks::gen_callback_interface(c, item)?); + generated.extend(callbacks::gen_callback_interface(c, item, prefix)?); } for i in &mb.interfaces { let ty = ident(&i.name); @@ -250,7 +354,7 @@ fn render_symbols( let sig = member_sigs .get(&(i.name.clone(), m.name.clone())) .ok_or_else(|| missing("interface member", &m.name))?; - generated.extend(gen_function(m, sig, &target)?); + generated.extend(gen_function(m, sig, &target, prefix)?); } } generated.extend(interfaces::gen_interface_lifecycle(i)); @@ -259,7 +363,7 @@ fn render_symbols( let sfn = fns .get(&f.name) .ok_or_else(|| missing("function", &f.name))?; - generated.extend(gen_function(f, &sfn.sig, &CallTarget::Free)?); + generated.extend(gen_function(f, &sfn.sig, &CallTarget::Free, prefix)?); } Ok(generated) } @@ -274,14 +378,15 @@ fn impl_type_name(item_impl: &syn::ItemImpl) -> Option { } /// Generate the [`ErrorReport`](weaveffi_abi::ErrorReport) implementation for -/// a module's `#[weaveffi::error]` enum, mapping each variant to its declared -/// code and default message, and serializing a payload variant's fields into -/// the error's value-buffer payload. This is what routes `Err(Domain::Case)` -/// from a throwing producer function to the matching C error constant. -fn gen_error_report(eb: &ErrorBinding) -> TokenStream { +/// a module's `#[weaveffi::error]` enum: each variant maps to its declared +/// code, the message is the enum's `Display` output, and a payload variant's +/// fields are serialized into the error's value-buffer payload. This is what +/// routes `Err(Domain::Case)` from a throwing producer function to the +/// matching C error constant. +fn gen_error_report(eb: &ErrorBinding, item: &syn::ItemEnum) -> TokenStream { let ty = ident(&eb.name); // A payload-carrying variant matches with `{ .. }`; a unit variant by name. - let pattern = |c: &weaveffi_core::model::ErrorCodeBinding| { + let pattern = |c: &weaveffi_model::model::ErrorCodeBinding| { let v = ident(&c.name); if c.fields.is_empty() { quote!(Self::#v) @@ -294,11 +399,6 @@ fn gen_error_report(eb: &ErrorBinding) -> TokenStream { let value = c.value; quote!(#pat => #value,) }); - let msg_arms = eb.codes.iter().map(|c| { - let pat = pattern(c); - let msg = &c.message; - quote!(#pat => #msg.to_string(),) - }); let payload_arms: Vec = eb .codes .iter() @@ -306,21 +406,13 @@ fn gen_error_report(eb: &ErrorBinding) -> TokenStream { .map(|c| { let v = ident(&c.name); let bindings: Vec = c.fields.iter().map(|f| ident(&f.name)).collect(); - let writes: Vec = c - .fields - .iter() - .zip(&bindings) - .map(|(f, b)| marshal::field_write_stmt_ref(f, b)) - .collect(); - // The shared field-write helpers name the writer `__wv_w` and - // expect it to be a `&mut BufferWriter`, matching the generated - // `write_value` methods, so bind a reborrowable alias. quote! { Self::#v { #(#bindings),* } => { - let mut __wv_buf = ::weaveffi::abi::BufferWriter::new(); - let __wv_w = &mut __wv_buf; - #(#writes)* - __wv_buf.finish() + let mut __wv_w = ::weaveffi::abi::BufferWriter::with_capacity( + 0 #(+ ::weaveffi::abi::BufferValue::encoded_len(#bindings))* + ); + #(::weaveffi::abi::BufferValue::write_value(#bindings, &mut __wv_w);)* + __wv_w.finish() } } }) @@ -337,6 +429,19 @@ fn gen_error_report(eb: &ErrorBinding) -> TokenStream { } } }; + // Spanned at the enum so a missing `Display` impl is one error that + // points at it and names the requirement. + let user_ty = &item.ident; + let message = quote_spanned! {user_ty.span()=> + fn __weaveffi_error_domains_must_implement_display< + T: ::std::fmt::Display + ?::std::marker::Sized, + >( + e: &T, + ) -> ::std::string::String { + ::std::string::ToString::to_string(e) + } + __weaveffi_error_domains_must_implement_display(self) + }; quote! { impl ::weaveffi::abi::ErrorReport for #ty { fn code(&self) -> i32 { @@ -344,21 +449,21 @@ fn gen_error_report(eb: &ErrorBinding) -> TokenStream { #(#code_arms)* } } - fn message(&self) -> String { - match self { - #(#msg_arms)* - } + fn message(&self) -> ::std::string::String { + #message } #payload_fn } } } -/// Build a spanned "unsupported" error for a type shape the macro cannot -/// marshal yet. -fn unsupported(what: &str, kind: &str) -> syn::Error { - syn::Error::new( - Span::call_site(), - format!("weaveffi: unsupported {kind} for `{what}` (not yet implemented by #[weaveffi::module])"), +/// Build an "unsupported" error for a type shape the macro can't marshal, +/// pointing at `at` (the offending type's tokens). +fn unsupported(at: TokenStream, what: &str, kind: &str) -> syn::Error { + syn::Error::new_spanned( + at, + format!( + "weaveffi: unsupported {kind} for `{what}` (not implemented by #[weaveffi::module])" + ), ) } diff --git a/crates/weaveffi-macros/src/codegen/records.rs b/crates/weaveffi-macros/src/codegen/records.rs index 0c250a7c..af98a00e 100644 --- a/crates/weaveffi-macros/src/codegen/records.rs +++ b/crates/weaveffi-macros/src/codegen/records.rs @@ -9,36 +9,30 @@ use proc_macro2::TokenStream; use quote::quote; -use weaveffi_core::model::StructBinding; +use weaveffi_model::model::StructBinding; use super::helpers::{ident, rust_type_ident}; use super::marshal::{field_read_expr, field_write_stmt}; -/// Generate the `BufferValue` implementation for one record. -pub(crate) fn gen_record(s: &StructBinding) -> syn::Result { +/// Generate the `BufferValue` and `ByValue` implementations for one record. +pub(crate) fn gen_record(s: &StructBinding) -> TokenStream { let rust_ty = rust_type_ident(&s.name); - - let writes: Vec = s - .fields - .iter() - .map(|f| { - let fname = ident(&f.name); - field_write_stmt(f, quote!(self.#fname)) - }) - .collect(); - let reads: Vec = s + let names: Vec = s.fields.iter().map(|f| ident(&f.name)).collect(); + let writes = s .fields .iter() - .map(|f| { - let fname = ident(&f.name); - let read = field_read_expr(f); - quote!(#fname: #read?) - }) - .collect(); + .zip(&names) + .map(|(f, n)| field_write_stmt(f, quote!(self.#n))); + let reads = s.fields.iter().zip(&names).map(|(f, n)| { + let read = field_read_expr(f); + quote!(#n: #read?) + }); - Ok(quote! { - #[allow(unsafe_code)] + quote! { impl ::weaveffi::abi::BufferValue for #rust_ty { + fn encoded_len(&self) -> usize { + 0 #(+ ::weaveffi::abi::BufferValue::encoded_len(&self.#names))* + } fn write_value(&self, __wv_w: &mut ::weaveffi::abi::BufferWriter) { #(#writes)* } @@ -48,5 +42,7 @@ pub(crate) fn gen_record(s: &StructBinding) -> syn::Result { ::std::result::Result::Ok(Self { #(#reads),* }) } } - }) + + impl ::weaveffi::abi::ByValue for #rust_ty {} + } } diff --git a/crates/weaveffi-macros/src/codegen/sync.rs b/crates/weaveffi-macros/src/codegen/sync.rs index 4dc0dc6f..3b3542bf 100644 --- a/crates/weaveffi-macros/src/codegen/sync.rs +++ b/crates/weaveffi-macros/src/codegen/sync.rs @@ -3,17 +3,18 @@ use proc_macro2::TokenStream; use quote::quote; -use weaveffi_core::abi::CType; -use weaveffi_core::model::{AbiFn, CallShape, FnBinding}; -use weaveffi_core::plan::ErrorStrategy; +use weaveffi_model::model::{AbiFn, CallShape, FnBinding}; +use weaveffi_model::plan::ErrorStrategy; use super::async_fns::gen_async_function; -use super::helpers::{fn_slots, ident, ret_arrow_for, sentinel, wrap_unwind, CallTarget, UserSig}; +use super::helpers::{ + fn_slots, ident, ret_arrow_for, sentinel, thunk_attrs, wrap_unwind, CallTarget, UserSig, +}; use super::iterators::gen_iterator_function; use super::marshal::{build_call_body, lift_param}; /// Whether this callable routes typed domain errors through `out_err`, per -/// the plan's error contract ([`weaveffi_core::plan::ErrorStrategy`]). The +/// the plan's error contract ([`weaveffi_model::plan::ErrorStrategy`]). The /// producer declared it by returning a `Result`, so the thunk matches on the /// call's `Ok`/`Err` instead of binding the value directly. pub(crate) fn throws(f: &FnBinding) -> bool { @@ -26,12 +27,13 @@ pub(crate) fn gen_function( f: &FnBinding, sig: &syn::Signature, target: &CallTarget, + prefix: &str, ) -> syn::Result { let user = UserSig::new(sig, target.self_ty()); match &f.shape { - CallShape::Sync(abi) => gen_sync_function(f, abi, &user, target), - CallShape::Iterator(it) => gen_iterator_function(f, it, &user, target), - CallShape::Async(a) => gen_async_function(f, a, &user, target), + CallShape::Sync(abi) => gen_sync_function(f, abi, &user, target, prefix), + CallShape::Iterator(it) => gen_iterator_function(f, it, &user, target, prefix), + CallShape::Async(a) => gen_async_function(f, a, &user, target, prefix), } } @@ -41,34 +43,31 @@ fn gen_sync_function( abi: &AbiFn, user: &UserSig<'_>, target: &CallTarget, + prefix: &str, ) -> syn::Result { let sym = ident(&abi.symbol); - let params = fn_slots(&abi.params, &f.params, user)?; - let arrow = ret_arrow_for(&abi.ret, f.ret.as_ref(), user); + let params = fn_slots(&abi.params, &f.params, user, prefix)?; + let arrow = ret_arrow_for(&abi.ret, f.ret.as_ref(), user, prefix); let sentinel = sentinel(&abi.ret); // Lift each parameter, collecting the preambles and the call arguments. - let self_pre = target.self_preamble(&sentinel, user.receiver_is_arc()); + let self_pre = target.self_preamble(sentinel.as_ref(), user.receiver_is_arc()); let mut preamble = TokenStream::new(); let mut call_args: Vec = Vec::new(); for pb in &f.params { - let (pre, arg) = lift_param(pb, user, &sentinel)?; + let (pre, arg) = lift_param(pb, user, sentinel.as_ref())?; preamble.extend(pre); call_args.push(arg); } let call = target.call(&f.name, &call_args); let body = build_call_body(f.ret.as_ref(), &abi.ret, throws(f), call, user)?; - let is_void = matches!(abi.ret, CType::Void); - let wrapped = wrap_unwind( - quote! { #self_pre #preamble #body }, - (!is_void).then_some(&sentinel), - ); + let wrapped = wrap_unwind(quote! { #self_pre #preamble #body }, sentinel.as_ref()); + let attrs = thunk_attrs(); Ok(quote! { - #[no_mangle] - #[allow(unsafe_code, deprecated, clippy::not_unsafe_ptr_arg_deref, clippy::missing_safety_doc)] - pub extern "C" fn #sym(#(#params),*) #arrow { + #attrs + pub unsafe extern "C" fn #sym(#(#params),*) #arrow { #wrapped } }) diff --git a/crates/weaveffi-macros/src/lib.rs b/crates/weaveffi-macros/src/lib.rs index d05896a0..c8b4fb27 100644 --- a/crates/weaveffi-macros/src/lib.rs +++ b/crates/weaveffi-macros/src/lib.rs @@ -1,12 +1,13 @@ //! Procedural macros that turn safe, annotated Rust into the WeaveFFI C ABI. //! -//! A producer annotates an ordinary Rust module with `#[weaveffi::module]` and -//! tags the items it wants to export. The macro lowers the module to the -//! WeaveFFI IR (through [`weaveffi_bridge`]), builds the canonical -//! [`BindingModel`](weaveffi_core::model::BindingModel), and emits the -//! `#[no_mangle] extern "C"` thunks every generated language binding calls. -//! All of the `unsafe` marshalling lives in the `weaveffi-abi` runtime, so the -//! producer writes only safe Rust. +//! A producer annotates an ordinary Rust module with `#[weaveffi::module]`, +//! tags the items it wants to export, and calls `weaveffi::export_runtime!()` +//! once. The module macro lowers the module tree to the WeaveFFI IR (through +//! [`weaveffi_model::rust`]), builds the canonical +//! [`BindingModel`](weaveffi_model::model::BindingModel), and emits the +//! `extern "C"` thunks every generated language binding calls. All of the +//! `unsafe` marshalling lives in the `weaveffi-abi` runtime, so the producer +//! writes only safe Rust. //! //! ```ignore //! #[weaveffi::module] @@ -21,14 +22,17 @@ //! weaveffi::export_runtime!(); //! ``` //! -//! The same IR the macro lowers is what `weaveffi generate path/to/lib.rs` -//! reads, so the generated bindings and the producer cannot drift. +//! Every C symbol starts with the crate's name (`CARGO_CRATE_NAME`), which +//! is also the prefix `weaveffi generate` derives for a `.rs` input, so the +//! generated bindings and the producer can't drift: they're two views of one +//! parse. //! //! # Attributes //! //! * [`macro@module`] marks an exported namespace (the driver attribute). //! * [`macro@export`] exports a function; [`macro@record`] a by-value struct; -//! [`macro@enumeration`] a `#[repr(i32)]` C-style enum. +//! [`macro@enumeration`] a `#[repr(i32)]` C-style enum or a rich enum with +//! data-carrying variants. //! * [`macro@interface`] declares an opaque, reference-counted object type //! whose `impl` block's `pub fn`s become constructors, methods, and statics. //! * [`macro@error`] declares the module's error domain from an enum with @@ -36,6 +40,8 @@ //! structured payload. //! * [`macro@callback_interface`] declares a trait the consumer implements; //! [`macro@cancellable`] marks an async function as cancellable. +//! * [`export_runtime!`] emits the runtime symbols (memory, errors, cancel +//! tokens, ABI version) once per library. //! //! The item-level attributes are inert markers that [`macro@module`] reads; on //! their own they expand to the item unchanged. @@ -45,12 +51,15 @@ use proc_macro::TokenStream; mod codegen; +mod runtime; /// Mark an inline `mod` as an exported WeaveFFI namespace. /// -/// The macro re-emits the module unchanged and appends the generated C ABI -/// thunks for every tagged item it contains (functions, records, enums). Apply -/// it to a `mod foo { ... }` whose items carry the item-level markers. +/// The macro re-emits the module and appends the generated C ABI thunks for +/// every tagged item it contains, recursing into nested `#[weaveffi::module]` +/// submodules (whose symbols carry the joined module path). A top-level +/// module also exports `{prefix}_{module}_checksum()`, the contract checksum +/// generated bindings verify when they load the library. #[proc_macro_attribute] pub fn module(_attr: TokenStream, item: TokenStream) -> TokenStream { let item_mod = syn::parse_macro_input!(item as syn::ItemMod); @@ -59,6 +68,21 @@ pub fn module(_attr: TokenStream, item: TokenStream) -> TokenStream { .into() } +/// Emit the runtime symbols every WeaveFFI library exports, prefixed with the +/// crate's name: `{prefix}_abi_version`, `{prefix}_error_set`, +/// `{prefix}_error_clear`, `{prefix}_error_free`, `{prefix}_free_bytes`, the +/// four `{prefix}_cancel_token_*` functions, `{prefix}_debug_live`, and on +/// `wasm32` `{prefix}_alloc` and `{prefix}_dealloc`. +/// +/// Invoke it exactly once, at the crate root of the `cdylib`. It takes no +/// arguments. +#[proc_macro] +pub fn export_runtime(input: TokenStream) -> TokenStream { + runtime::expand(input.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + /// Generate `#[doc(hidden)]` no-op marker attributes that [`macro@module`] /// reads. Each expands to the annotated item unchanged. macro_rules! marker_attr { @@ -88,22 +112,27 @@ marker_attr! { interface } marker_attr! { - /// Declare the module's error domain from a unit-variant enum with - /// explicit discriminants. The module macro generates the matching - /// `ErrorReport` implementation. + /// Declare the module's error domain from an enum with explicit + /// discriminants (the stable error codes). The enum must implement + /// `std::fmt::Display`, which supplies the runtime message; each + /// variant's doc comment is the documented default message. error } marker_attr! { - /// Declare a C-style `#[repr(i32)]` enum exported by value. + /// Declare an enum exported by value: a `#[repr(i32)]` C-style enum, or a + /// rich enum whose variants carry named fields. enumeration } marker_attr! { - /// Declare a callback interface: a trait whose `&self` methods the consumer - /// implements. Producers accept one as `Arc`; declare - /// `Send + Sync` as supertraits when it is used from an `async fn`. + /// Declare a callback interface: a trait whose `&self` methods the + /// consumer implements. Producers accept one as `Arc`. A + /// method may return `Result` to receive the + /// consumer's failure as a value. callback_interface } marker_attr! { - /// Mark an async function as accepting a cancellation token. + /// Mark an `async fn` as cancellable: it takes a `weaveffi::CancelToken` + /// as its final parameter, and cancelling the token completes the call + /// with the cancelled code. cancellable } diff --git a/crates/weaveffi-macros/src/runtime.rs b/crates/weaveffi-macros/src/runtime.rs new file mode 100644 index 00000000..617467ba --- /dev/null +++ b/crates/weaveffi-macros/src/runtime.rs @@ -0,0 +1,136 @@ +//! Expansion of `weaveffi::export_runtime!()`: the fixed runtime surface +//! every library exports under its own prefix. +//! +//! The bodies live in `weaveffi-abi`; only the `extern "C"` thunks are +//! emitted here, in the producer's crate, because `#[no_mangle]` symbols in +//! a transitive `rlib` aren't guaranteed to be exported from a `cdylib`. +//! +//! `{prefix}_debug_live` is always exported. The macro can't see which cargo +//! features the producer enabled on its `weaveffi` dependency, and a +//! consumer shouldn't need to know either: without the `leak-check` feature +//! the function exists and reports `0` for every kind. + +use proc_macro2::TokenStream; +use quote::quote; + +use crate::codegen::{ident, prefix}; + +/// Expand `export_runtime!()` (which takes no arguments). +pub(crate) fn expand(input: TokenStream) -> syn::Result { + if !input.is_empty() { + return Err(syn::Error::new_spanned( + input, + "weaveffi::export_runtime!() takes no arguments; the symbol prefix is the crate name", + )); + } + let p = prefix()?; + let sym = |name: &str| ident(&format!("{p}_{name}")); + let abi_version = sym("abi_version"); + let error_set = sym("error_set"); + let error_clear = sym("error_clear"); + let error_free = sym("error_free"); + let free_bytes = sym("free_bytes"); + let token_create = sym("cancel_token_create"); + let token_cancel = sym("cancel_token_cancel"); + let token_is_cancelled = sym("cancel_token_is_cancelled"); + let token_destroy = sym("cancel_token_destroy"); + let debug_live = sym("debug_live"); + let alloc = sym("alloc"); + let dealloc = sym("dealloc"); + Ok(quote! { + // 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. + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code)] + pub extern "C" fn #abi_version() -> u32 { + ::weaveffi::abi::ABI_VERSION + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] + pub unsafe extern "C" fn #error_set( + err: *mut ::weaveffi::abi::FfiError, + code: i32, + message: *const ::std::os::raw::c_char, + ) { + unsafe { ::weaveffi::abi::error_set_c(err, code, message) } + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] + pub unsafe extern "C" fn #error_clear(err: *mut ::weaveffi::abi::FfiError) { + unsafe { ::weaveffi::abi::error_clear(err) } + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] + pub unsafe extern "C" fn #error_free(err: *mut ::weaveffi::abi::FfiError) { + unsafe { ::weaveffi::abi::error_free(err) } + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] + pub unsafe extern "C" fn #free_bytes(ptr: *mut u8, len: usize) { + unsafe { ::weaveffi::abi::free_bytes(ptr, len) } + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code)] + pub extern "C" fn #token_create() -> *mut ::weaveffi::abi::FfiCancelToken { + ::weaveffi::abi::cancel_token_create() + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] + pub unsafe extern "C" fn #token_cancel(token: *mut ::weaveffi::abi::FfiCancelToken) { + unsafe { ::weaveffi::abi::cancel_token_cancel(token) } + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] + pub unsafe extern "C" fn #token_is_cancelled(token: *const ::weaveffi::abi::FfiCancelToken) -> bool { + unsafe { ::weaveffi::abi::cancel_token_is_cancelled(token) } + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] + pub unsafe extern "C" fn #token_destroy(token: *mut ::weaveffi::abi::FfiCancelToken) { + unsafe { ::weaveffi::abi::cancel_token_destroy(token) } + } + + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code)] + pub extern "C" fn #debug_live(kind: i32) -> u64 { + ::weaveffi::abi::debug_live(kind) + } + + // Wasm has no host allocator, so the generated JS glue stages + // input buffers and return slots through these. + #[cfg(target_arch = "wasm32")] + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code)] + pub extern "C" fn #alloc(size: u32) -> *mut u8 { + ::weaveffi::abi::wasm_alloc(size as usize) + } + + #[cfg(target_arch = "wasm32")] + #[doc(hidden)] + #[unsafe(no_mangle)] + #[allow(unsafe_code, unused_unsafe, clippy::missing_safety_doc)] + pub unsafe extern "C" fn #dealloc(ptr: *mut u8, size: u32) { + unsafe { ::weaveffi::abi::wasm_dealloc(ptr, size as usize) } + } + }) +} diff --git a/crates/weaveffi-macros/tests/ui/fail_callback_string_return.stderr b/crates/weaveffi-macros/tests/ui/fail_callback_string_return.stderr index 4bf0fedb..f32ba146 100644 --- a/crates/weaveffi-macros/tests/ui/fail_callback_string_return.stderr +++ b/crates/weaveffi-macros/tests/ui/fail_callback_string_return.stderr @@ -1,7 +1,5 @@ -error: weaveffi: unsupported non-direct return type for `callback return` (not yet implemented by #[weaveffi::module]) - --> tests/ui/fail_callback_string_return.rs:1:1 +error: weaveffi: a callback interface method can only return a number, `bool`, or C-style enum (or nothing) + --> tests/ui/fail_callback_string_return.rs:5:27 | -1 | #[weaveffi::module] - | ^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `weaveffi::module` (in Nightly builds, run with -Z macro-backtrace for more info) +5 | fn name(&self) -> String; + | ^^^^^^ diff --git a/crates/weaveffi-macros/tests/ui/fail_error_without_display.rs b/crates/weaveffi-macros/tests/ui/fail_error_without_display.rs new file mode 100644 index 00000000..f541b34e --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_error_without_display.rs @@ -0,0 +1,15 @@ +#[weaveffi::module] +mod bad { + #[weaveffi::error] + pub enum Oops { + /// Something broke. + Broken = 1, + } + + #[weaveffi::export] + pub fn risky() -> Result { + Err(Oops::Broken) + } +} + +fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_error_without_display.stderr b/crates/weaveffi-macros/tests/ui/fail_error_without_display.stderr new file mode 100644 index 00000000..e361672f --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_error_without_display.stderr @@ -0,0 +1,16 @@ +error[E0277]: `Oops` doesn't implement `std::fmt::Display` + --> tests/ui/fail_error_without_display.rs:4:14 + | +4 | pub enum Oops { + | ^^^^ unsatisfied trait bound + | +help: the trait `std::fmt::Display` is not implemented for `Oops` + --> tests/ui/fail_error_without_display.rs:4:5 + | +4 | pub enum Oops { + | ^^^^^^^^^^^^^ +note: required by a bound in `__weaveffi_error_domains_must_implement_display` + --> tests/ui/fail_error_without_display.rs:4:14 + | +4 | pub enum Oops { + | ^^^^ required by this bound in `__weaveffi_error_domains_must_implement_display` diff --git a/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.stderr b/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.stderr index 13fb6db0..bef87159 100644 --- a/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.stderr +++ b/crates/weaveffi-macros/tests/ui/fail_result_without_error_domain.stderr @@ -1,5 +1,5 @@ error: weaveffi: `risky` returns a Result but no error domain is in scope; declare a #[weaveffi::error] enum in this module (or a parent module) - --> tests/ui/fail_result_without_error_domain.rs:2:5 + --> tests/ui/fail_result_without_error_domain.rs:8:20 | -2 | mod bad { - | ^^^ +8 | pub fn risky() -> Result { + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/weaveffi-macros/tests/ui/fail_sibling_c_enum.rs b/crates/weaveffi-macros/tests/ui/fail_sibling_c_enum.rs new file mode 100644 index 00000000..a83b1586 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_sibling_c_enum.rs @@ -0,0 +1,25 @@ +//! A C-style enum crosses the ABI as an `i32`, but a module tree can't see a +//! sibling tree's declarations, so it would lower the enum as a value buffer +//! and disagree with the generated header. The macro rejects it. + +#[weaveffi::module] +pub mod colors { + #[weaveffi::enumeration] + #[repr(i32)] + pub enum Color { + Red = 0, + Green = 1, + } +} + +#[weaveffi::module] +pub mod paint { + use super::colors::Color; + + #[weaveffi::export] + pub fn mix(color: Color) -> i32 { + color as i32 + } +} + +fn main() {} diff --git a/crates/weaveffi-macros/tests/ui/fail_sibling_c_enum.stderr b/crates/weaveffi-macros/tests/ui/fail_sibling_c_enum.stderr new file mode 100644 index 00000000..232375b6 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/fail_sibling_c_enum.stderr @@ -0,0 +1,18 @@ +error[E0277]: `Color` is declared in a different `#[weaveffi::module]` tree and isn't a record or rich enum + --> tests/ui/fail_sibling_c_enum.rs:20:23 + | +20 | pub fn mix(color: Color) -> i32 { + | ^^^^^ declared outside this module tree + | +help: the trait `ByValue` is not implemented for `Color` + --> tests/ui/fail_sibling_c_enum.rs:9:5 + | + 9 | pub enum Color { + | ^^^^^^^^^^^^^^ + = note: a `#[weaveffi::module]` only sees the declarations inside its own tree, so it can pass a type from another tree only as a value buffer (a record or rich enum) + = note: nest the modules under one `#[weaveffi::module]` root (as inner `mod`s) so the macro can see the declaration +note: required by a bound in `__weaveffi_assert_by_value` + --> tests/ui/fail_sibling_c_enum.rs:20:23 + | +20 | pub fn mix(color: Color) -> i32 { + | ^^^^^ required by this bound in `__weaveffi_assert_by_value` diff --git a/crates/weaveffi-macros/tests/ui/pass_full_surface.rs b/crates/weaveffi-macros/tests/ui/pass_full_surface.rs index 41d2e872..e91b9c4a 100644 --- a/crates/weaveffi-macros/tests/ui/pass_full_surface.rs +++ b/crates/weaveffi-macros/tests/ui/pass_full_surface.rs @@ -17,6 +17,15 @@ mod surface { Detailed { code: i32, note: String } = 2, } + impl std::fmt::Display for Failure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotFound => f.write_str("not found"), + Self::Detailed { code, note } => write!(f, "{code}: {note}"), + } + } + } + #[weaveffi::enumeration] #[repr(i32)] pub enum Mode { @@ -38,6 +47,7 @@ mod surface { fn on_item(&self, item: &Item, widget: Arc); fn should_continue(&self, n: i32) -> bool; fn pick(&self) -> Mode; + fn label(&self, text: &str, data: &[u8]) -> Result; } #[weaveffi::interface] @@ -88,6 +98,16 @@ mod surface { observer.should_continue(1) } + #[weaveffi::export] + pub fn borrow(text: &str, data: &[u8], owned: String) -> String { + format!("{text}{}{owned}", data.len()) + } + + #[weaveffi::export] + pub async fn shout(text: String) -> String { + text.to_uppercase() + } + #[weaveffi::export] pub fn widgets(n: i32) -> Vec> { (0..n).map(|i| Arc::new(Widget::new(i64::from(i)))).collect() diff --git a/crates/weaveffi-macros/tests/ui/pass_sibling_record.rs b/crates/weaveffi-macros/tests/ui/pass_sibling_record.rs new file mode 100644 index 00000000..940eeda3 --- /dev/null +++ b/crates/weaveffi-macros/tests/ui/pass_sibling_record.rs @@ -0,0 +1,35 @@ +//! A record (or rich enum) is the one kind of type a module tree may share +//! with a sibling tree: it crosses as a value buffer either way. +#![deny(unsafe_code)] + +#[weaveffi::module] +pub mod shapes { + #[weaveffi::record] + pub struct Point { + pub x: f64, + pub y: f64, + } + + #[weaveffi::enumeration] + pub enum Shape { + Dot { at: Point }, + Empty, + } +} + +#[weaveffi::module] +pub mod canvas { + use super::shapes::{Point, Shape}; + + #[weaveffi::export] + pub fn origin() -> Point { + Point { x: 0.0, y: 0.0 } + } + + #[weaveffi::export] + pub fn draw(points: Vec, shape: Option) -> i32 { + points.len() as i32 + i32::from(matches!(shape, Some(Shape::Dot { .. }))) + } +} + +fn main() {} diff --git a/crates/weaveffi-model/Cargo.toml b/crates/weaveffi-model/Cargo.toml new file mode 100644 index 00000000..b8647408 --- /dev/null +++ b/crates/weaveffi-model/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "weaveffi-model" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +description = "IR, parsers, Rust extractor, validation, and the resolved binding model for WeaveFFI API definitions" +keywords = ["ffi", "codegen", "bindings", "ir", "parser"] +categories = ["development-tools::ffi"] + +[features] +default = ["idl"] +# The IDL text formats (YAML, JSON, TOML), JSON Schema derivation, and fancy +# miette diagnostics. The proc-macros build without it. +idl = ["dep:serde_yaml", "dep:serde_json", "dep:toml", "dep:schemars", "dep:miette"] + +[dependencies] +serde = { workspace = true } +thiserror = { workspace = true } +heck = { workspace = true } +syn = { workspace = true } +serde_yaml = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +toml = { workspace = true, optional = true } +schemars = { workspace = true, optional = true } +miette = { workspace = true, optional = true } + +[dev-dependencies] +serde_yaml = { workspace = true } +serde_json = { workspace = true } +toml = { workspace = true } + +[lints] +workspace = true diff --git a/crates/weaveffi-core/src/abi/ctype.rs b/crates/weaveffi-model/src/abi/ctype.rs similarity index 96% rename from crates/weaveffi-core/src/abi/ctype.rs rename to crates/weaveffi-model/src/abi/ctype.rs index 7d60cc08..7ae5c9df 100644 --- a/crates/weaveffi-core/src/abi/ctype.rs +++ b/crates/weaveffi-model/src/abi/ctype.rs @@ -144,8 +144,8 @@ impl CType { CType::Void => "std::ffi::c_void".to_string(), // The runtime types come from `weaveffi-abi` and keep their fixed // names regardless of the configured business-symbol prefix. - CType::CancelToken => "weaveffi_cancel_token".to_string(), - CType::Error => "weaveffi_error".to_string(), + CType::CancelToken => "::weaveffi::abi::FfiCancelToken".to_string(), + CType::Error => "::weaveffi::abi::FfiError".to_string(), // A C-style enum is passed/returned as its int discriminant. CType::Enum { .. } => "i32".to_string(), CType::StructTag { module, name } => format!("{prefix}_{module}_{name}"), @@ -258,11 +258,14 @@ mod tests { } #[test] - fn rust_runtime_types_keep_canonical_names_under_custom_prefix() { - assert_eq!(CType::Error.render_rust("acme"), "weaveffi_error"); + fn rust_runtime_types_name_the_runtime_structs() { + assert_eq!( + CType::Error.render_rust("acme"), + "::weaveffi::abi::FfiError" + ); assert_eq!( CType::CancelToken.render_rust("acme"), - "weaveffi_cancel_token" + "::weaveffi::abi::FfiCancelToken" ); } diff --git a/crates/weaveffi-core/src/abi/lower.rs b/crates/weaveffi-model/src/abi/lower.rs similarity index 92% rename from crates/weaveffi-core/src/abi/lower.rs rename to crates/weaveffi-model/src/abi/lower.rs index 0ed4b6d5..a3e30153 100644 --- a/crates/weaveffi-core/src/abi/lower.rs +++ b/crates/weaveffi-model/src/abi/lower.rs @@ -5,8 +5,8 @@ //! //! * [`Family::Direct`] types occupy one C slot by value: scalars, bools, and //! C-style enums. -//! * [`Family::String`] is a `char*`; [`Family::Bytes`] is a `ptr` + `len` -//! pair. +//! * [`Family::String`] and [`Family::Bytes`] are a `const uint8_t*` + +//! `size_t` pair (UTF-8 for strings, never NUL-terminated). //! * [`Family::Buffer`] types (records, rich enums, optionals, lists, and //! maps) cross as one serialized value buffer: a `const uint8_t*` + //! `size_t` pair encoded in the WeaveFFI buffer format. A buffered @@ -124,8 +124,7 @@ fn ptr_len_slots(name: &str) -> Vec { pub fn lower_param(name: &str, ty: &Ty, module: &str) -> Vec { match ty.family() { Family::Direct => vec![AbiParam::new(name, direct_ctype(ty, module))], - Family::String => vec![AbiParam::new(name, CType::const_ptr(CType::Char))], - Family::Bytes | Family::Buffer => ptr_len_slots(name), + Family::String | Family::Bytes | Family::Buffer => ptr_len_slots(name), // An interface parameter borrows the object for the call: the callee // reads through the const pointer and clones if it wants to retain // the object. A nullable one is the same slot with null meaning none. @@ -173,10 +172,10 @@ pub fn lower_return(ty: &Ty, module: &str) -> 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 { + // String, bytes, and buffered returns are producer-allocated byte + // runs: the caller copies or decodes them and then calls + // `{prefix}_free_bytes(ptr, len)`. + Family::String | Family::Bytes | Family::Buffer => AbiReturn { ret: CType::const_ptr(CType::Uint8), out_params: vec![AbiParam::new("out_len", CType::ptr(CType::Size))], }, @@ -198,19 +197,15 @@ pub fn lower_return(ty: &Ty, module: &str) -> AbiReturn { /// The trailing result fields appended to an async callback after the /// `(context, err)` prefix. /// -/// 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. +/// String, bytes, and buffered results are passed as an owned `ptr` + `len` +/// pair (released by the consumer with `{prefix}_free_bytes`); everything +/// else reuses its return slot type by value. pub fn callback_result_params(ty: &Ty, module: &str) -> Vec { match ty.family() { - Family::Buffer => vec![ + Family::String | Family::Bytes | Family::Buffer => vec![ AbiParam::new("result_ptr", CType::const_ptr(CType::Uint8)), AbiParam::new("result_len", CType::Size), ], - Family::Bytes => vec![ - AbiParam::new("result", CType::const_ptr(CType::Uint8)), - AbiParam::new("result_len", CType::Size), - ], _ => vec![AbiParam::new("result", lower_return(ty, module).ret)], } } @@ -231,7 +226,7 @@ mod tests { assert_eq!(render(&lower_param("x", &Ty::I32, "m")), ["int32_t x"]); assert_eq!( render(&lower_param("s", &Ty::StringUtf8, "m")), - ["const char* s"] + ["const uint8_t* s_ptr", "size_t s_len"] ); assert_eq!( render(&lower_param("data", &Ty::Bytes, "m")), @@ -317,7 +312,7 @@ mod tests { ); assert_eq!( render(&callback_result_params(&Ty::Bytes, "m")), - ["const uint8_t* result", "size_t result_len"] + ["const uint8_t* result_ptr", "size_t result_len"] ); assert_eq!( render(&callback_result_params(&Ty::I32, "m")), diff --git a/crates/weaveffi-core/src/abi/mod.rs b/crates/weaveffi-model/src/abi/mod.rs similarity index 97% rename from crates/weaveffi-core/src/abi/mod.rs rename to crates/weaveffi-model/src/abi/mod.rs index 5276b74b..3af9d0dc 100644 --- a/crates/weaveffi-core/src/abi/mod.rs +++ b/crates/weaveffi-model/src/abi/mod.rs @@ -195,7 +195,12 @@ mod tests { assert_eq!(sig.ret, CType::Bool); assert_eq!( render(&sig.params), - ["void* ctx", "const char* text", "weaveffi_error* out_err"] + [ + "void* ctx", + "const uint8_t* text_ptr", + "size_t text_len", + "weaveffi_error* out_err" + ] ); let sig = callback_method_signature(&[], None, "events"); assert_eq!(sig.ret, CType::Void); diff --git a/crates/weaveffi-model/src/checksum.rs b/crates/weaveffi-model/src/checksum.rs new file mode 100644 index 00000000..ac06c4c2 --- /dev/null +++ b/crates/weaveffi-model/src/checksum.rs @@ -0,0 +1,215 @@ +//! Contract checksums: a stable fingerprint of a top-level module's ABI. +//! +//! A producer exports `uint64_t {prefix}_{module}_checksum(void)` for every +//! top-level module, and every generated consumer compares it with the value +//! it was generated against when it loads the library. Both sides compute the +//! value with [`module_checksum`] over the same IR (the `#[weaveffi::module]` +//! macro on the module it expands, the CLI on the parsed IDL or the extracted +//! Rust source), so bindings that are stale relative to the library they load +//! fail loudly instead of misreading memory. +//! +//! The hash covers everything that shapes the C ABI or the value-buffer wire +//! format: every declaration name, every type reference, parameter and field +//! order, function flags (`throws`, `async`, `cancellable`), enum +//! discriminants, and error-code values. It deliberately skips documentation, +//! deprecation notes, and error-code messages, so editing prose never breaks a +//! deployed binding. + +use crate::ir::{ + CallbackInterfaceDef, EnumDef, ErrorDomain, Function, InterfaceDef, Module, StructDef, + StructField, +}; + +/// FNV-1a 64-bit hasher; tiny, dependency-free, and stable across platforms +/// and Rust versions. +struct Fnv(u64); + +impl Fnv { + const OFFSET: u64 = 0xcbf2_9ce4_8422_2325; + const PRIME: u64 = 0x0000_0100_0000_01b3; + + fn bytes(&mut self, bytes: &[u8]) { + for b in bytes { + self.0 ^= u64::from(*b); + self.0 = self.0.wrapping_mul(Self::PRIME); + } + } + + /// Hash a tagged string. The tag and a terminator keep adjacent fields + /// from running together (`"ab" + "c"` never equals `"a" + "bc"`). + fn str(&mut self, tag: u8, s: &str) { + self.bytes(&[tag]); + self.bytes(s.as_bytes()); + self.bytes(&[0xff]); + } + + fn int(&mut self, tag: u8, v: i64) { + self.bytes(&[tag]); + self.bytes(&v.to_le_bytes()); + } + + fn flag(&mut self, tag: u8, v: bool) { + self.bytes(&[tag, u8::from(v)]); + } +} + +/// The contract checksum of one top-level module (including its nested +/// modules). +#[must_use] +pub fn module_checksum(module: &Module) -> u64 { + let mut h = Fnv(Fnv::OFFSET); + hash_module(&mut h, module); + h.0 +} + +fn hash_module(h: &mut Fnv, m: &Module) { + h.str(b'M', &m.name); + for f in &m.functions { + hash_function(h, b'F', f); + } + for i in &m.interfaces { + hash_interface(h, i); + } + for c in &m.callback_interfaces { + hash_callback(h, c); + } + for s in &m.structs { + hash_struct(h, s); + } + for e in &m.enums { + hash_enum(h, e); + } + if let Some(d) = &m.errors { + hash_errors(h, d); + } + for child in &m.modules { + hash_module(h, child); + } + h.bytes(b"}"); +} + +fn hash_function(h: &mut Fnv, tag: u8, f: &Function) { + h.str(tag, &f.name); + for p in &f.params { + h.str(b'p', &p.name); + h.str(b't', &p.ty.to_string()); + } + if let Some(r) = &f.returns { + h.str(b'r', &r.to_string()); + } + h.flag(b'T', f.throws); + h.flag(b'A', f.r#async); + h.flag(b'C', f.cancellable); +} + +fn hash_interface(h: &mut Fnv, i: &InterfaceDef) { + h.str(b'I', &i.name); + for c in &i.constructors { + hash_function(h, b'c', c); + } + for m in &i.methods { + hash_function(h, b'm', m); + } + for s in &i.statics { + hash_function(h, b's', s); + } +} + +fn hash_callback(h: &mut Fnv, c: &CallbackInterfaceDef) { + h.str(b'K', &c.name); + for m in &c.methods { + hash_function(h, b'k', m); + } +} + +fn hash_fields(h: &mut Fnv, fields: &[StructField]) { + for f in fields { + h.str(b'f', &f.name); + h.str(b't', &f.ty.to_string()); + } +} + +fn hash_struct(h: &mut Fnv, s: &StructDef) { + h.str(b'S', &s.name); + hash_fields(h, &s.fields); +} + +fn hash_enum(h: &mut Fnv, e: &EnumDef) { + h.str(b'E', &e.name); + for v in &e.variants { + h.str(b'v', &v.name); + h.int(b'd', i64::from(v.value)); + hash_fields(h, &v.fields); + } +} + +fn hash_errors(h: &mut Fnv, d: &ErrorDomain) { + h.str(b'D', &d.name); + for c in &d.codes { + h.str(b'e', &c.name); + h.int(b'n', i64::from(c.code)); + hash_fields(h, &c.fields); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ir::{Param, TypeRef}; + + fn module() -> Module { + Module { + name: "kv".into(), + doc: Some("docs".into()), + functions: vec![Function { + name: "get".into(), + params: vec![Param { + name: "key".into(), + ty: TypeRef::StringUtf8, + doc: None, + }], + returns: Some(TypeRef::Optional(Box::new(TypeRef::Bytes))), + doc: Some("Fetch a value.".into()), + throws: true, + r#async: false, + cancellable: false, + deprecated: None, + }], + interfaces: vec![], + callback_interfaces: vec![], + structs: vec![], + enums: vec![], + errors: None, + modules: vec![], + } + } + + #[test] + fn docs_do_not_affect_the_checksum() { + let a = module(); + let mut b = module(); + b.doc = None; + b.functions[0].doc = Some("Changed prose.".into()); + b.functions[0].deprecated = Some("old".into()); + assert_eq!(module_checksum(&a), module_checksum(&b)); + } + + #[test] + fn abi_changes_do() { + let a = module(); + let mut b = module(); + b.functions[0].throws = false; + assert_ne!(module_checksum(&a), module_checksum(&b)); + let mut c = module(); + c.functions[0].params[0].ty = TypeRef::Bytes; + assert_ne!(module_checksum(&a), module_checksum(&c)); + } + + #[test] + fn value_is_stable() { + // Pin the algorithm: changing it invalidates every deployed binding, + // so it must be a deliberate, documented ABI change. + assert_eq!(module_checksum(&module()), module_checksum(&module())); + assert_eq!(format!("{:016x}", Fnv(Fnv::OFFSET).0), "cbf29ce484222325"); + } +} diff --git a/crates/weaveffi-core/src/errors.rs b/crates/weaveffi-model/src/errors.rs similarity index 70% rename from crates/weaveffi-core/src/errors.rs rename to crates/weaveffi-model/src/errors.rs index 3223af43..33c5c77a 100644 --- a/crates/weaveffi-core/src/errors.rs +++ b/crates/weaveffi-model/src/errors.rs @@ -7,25 +7,11 @@ //! no target drifts into `KEY_NOT_FOUNDError` (raw SCREAMING_SNAKE with a //! naive `Error` suffix) while another emits `keyNotFound`. //! -//! Backends pick the brand/suffix that matches their ecosystem -//! ([`ERROR_BRAND`] for Swift/Python/TS/C++/Ruby/Go, [`EXCEPTION_BRAND`] for -//! Kotlin/.NET/Dart) and case-convert each code's name through the helpers -//! below. +//! Backends pick the suffix that matches their ecosystem (`Error` or +//! `Exception`) and case-convert each code's name through the helpers below. use heck::ToUpperCamelCase; -/// Canonical brand stem. Always `WeaveFFI` (uppercase `FFI`), never the -/// `heck`-derived `Weaveffi`. -pub const BRAND_STEM: &str = "WeaveFFI"; - -/// Base error type for ecosystems that use the `Error` suffix -/// (Swift, Python, TypeScript/Node, C++, Ruby, Go). -pub const ERROR_BRAND: &str = "WeaveFFIError"; - -/// Base exception type for ecosystems that use the `Exception` suffix -/// (Kotlin, .NET, Dart). -pub const EXCEPTION_BRAND: &str = "WeaveFFIException"; - /// PascalCase form of a raw error code name, with no suffix. /// `KEY_NOT_FOUND` -> `KeyNotFound`. Use for languages whose error variants /// are nested types/cases (Kotlin sealed subclasses, etc.) rather than @@ -49,15 +35,12 @@ pub fn type_name(raw: &str, suffix: &str) -> String { /// Exception-branded type name for an error domain, for targets whose /// idiomatic errors are exceptions rather than `*Error` types. /// A trailing `Error` stem is replaced instead of stacked: -/// `KvError` -> `KvException`; `Failure` -> `FailureException`. +/// `KvError` -> `KvException`; `Failure` -> `FailureException`; a bare +/// `Error` -> `Exception`. pub fn exception_type_name(raw: &str) -> String { let pascal = raw.to_upper_camel_case(); let stem = pascal.strip_suffix("Error").unwrap_or(&pascal); - if stem.is_empty() { - EXCEPTION_BRAND.to_string() - } else { - type_name(stem, "Exception") - } + type_name(stem, "Exception") } #[cfg(test)] @@ -81,13 +64,11 @@ mod tests { assert_eq!(exception_type_name("ContactsError"), "ContactsException"); assert_eq!(exception_type_name("Failure"), "FailureException"); assert_eq!(exception_type_name("KvException"), "KvException"); - assert_eq!(exception_type_name("Error"), "WeaveFFIException"); + assert_eq!(exception_type_name("Error"), "Exception"); } #[test] fn pascal_is_suffix_free() { assert_eq!(pascal("KEY_NOT_FOUND"), "KeyNotFound"); - assert_eq!(BRAND_STEM, "WeaveFFI"); - assert!(ERROR_BRAND.starts_with(BRAND_STEM)); } } diff --git a/crates/weaveffi-ir/src/ir.rs b/crates/weaveffi-model/src/ir.rs similarity index 90% rename from crates/weaveffi-ir/src/ir.rs rename to crates/weaveffi-model/src/ir.rs index 8bd4131c..952981c8 100644 --- a/crates/weaveffi-ir/src/ir.rs +++ b/crates/weaveffi-model/src/ir.rs @@ -11,15 +11,16 @@ //! 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, an interface, or a callback -//! interface is decided by `weaveffi-core`'s validator, which lowers the +//! interface is decided by the [validator](crate::validate), 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. +//! losslessly through this module. //! //! 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. +#[cfg(feature = "idl")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -34,7 +35,7 @@ use serde::{Deserialize, Serialize}; /// /// 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.9.0"; +pub const CURRENT_SCHEMA_VERSION: &str = "0.10.0"; /// Every IR schema version the current tools accept. /// @@ -56,11 +57,15 @@ fn is_false(b: &bool) -> bool { /// This is the value an entire `.yml`, `.json`, or `.toml` IDL file /// deserializes into (see [`crate::parse`]) and the single input the validator /// consumes. It pairs the schema version with the module forest. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] -#[schemars(description = "Top-level WeaveFFI API definition.")] +#[cfg_attr( + feature = "idl", + schemars(description = "Top-level WeaveFFI API definition.") +)] pub struct Api { - /// IR schema version this document targets (for example `0.9.0`). + /// IR schema version this document targets (for example `0.10.0`). /// Validation rejects any value not listed in [`SUPPORTED_VERSIONS`]. pub version: String, /// Top-level modules that make up the API surface. Each is an independent @@ -75,10 +80,14 @@ pub struct Api { /// language's natural grouping construct (a namespace, a submodule, a symbol /// prefix, and so on). They may nest through [`modules`](Self::modules) to /// mirror a package hierarchy. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] -#[schemars( - description = "A WeaveFFI module: a named group of functions, types, callback interfaces, and errors." +#[cfg_attr( + feature = "idl", + schemars( + description = "A WeaveFFI module: a named group of functions, types, callback interfaces, and errors." + ) )] pub struct Module { /// Module name, used as a namespace segment and a symbol-prefix component @@ -124,7 +133,8 @@ pub struct Module { /// describes an interface's constructors, methods, and statics (see /// [`InterfaceDef`]) and a callback interface's methods (see /// [`CallbackInterfaceDef`]). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] pub struct Function { /// Function name, lowered to a per-language symbol (for example @@ -178,10 +188,14 @@ pub struct Function { /// Because the count lives in the producer, an interface value may appear /// anywhere a type can: as a parameter, a return, an iterator element, a record /// field, a list element, or a map value. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] -#[schemars( - description = "An interface: an opaque object type with constructors, methods, and statics." +#[cfg_attr( + feature = "idl", + schemars( + description = "An interface: an opaque object type with constructors, methods, and statics." + ) )] pub struct InterfaceDef { /// Interface type name (for example `Store`). @@ -221,10 +235,14 @@ pub struct InterfaceDef { /// and return either nothing or a value in the direct family (integers, /// floats, `bool`, or a C-style enum). Parameters may use any type other than /// another callback interface or an iterator. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] -#[schemars( - description = "A callback interface: a set of methods the consumer implements and the producer calls." +#[cfg_attr( + feature = "idl", + schemars( + description = "A callback interface: a set of methods the consumer implements and the producer calls." + ) )] pub struct CallbackInterfaceDef { /// Callback interface type name (for example `MessageListener`). @@ -242,7 +260,8 @@ pub struct CallbackInterfaceDef { } /// A single parameter of a [`Function`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] pub struct Param { /// Parameter name as it appears in generated signatures (for example `id`). @@ -323,10 +342,8 @@ pub enum TypeRef { /// # Errors /// /// Returns an error message when `s` is empty or only whitespace, when a map -/// type (`{K:V}`) is missing its `:` separator, or when `s` uses one of the -/// spellings removed in schema 0.9 (`handle`, `handle`, `&str`, `&[u8]`). -/// The same errors propagate up from a malformed inner type of a list, map, -/// optional, or iterator. +/// type (`{K:V}`) is missing its `:` separator. The same errors propagate up +/// from a malformed inner type of a list, map, optional, or iterator. pub fn parse_type_ref(s: &str) -> Result { let s = s.trim(); if s.is_empty() { @@ -361,17 +378,6 @@ pub fn parse_type_ref(s: &str) -> Result { { return parse_type_ref(inner).map(|t| TypeRef::Iterator(Box::new(t))); } - if s == "handle" || s.starts_with("handle<") { - return Err( - "`handle` types were removed in schema 0.9; declare an interface and reference it by name" - .to_string(), - ); - } - if s == "&str" || s == "&[u8]" { - return Err(format!( - "`{s}` was removed in schema 0.9; `string` and `bytes` parameters are always borrowed at the ABI" - )); - } match s { "i8" => Ok(TypeRef::I8), "i16" => Ok(TypeRef::I16), @@ -483,6 +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`, ...), `iter<{T}>`, /// `[{T}]`, `{ {K}: {V} }`, `{name}?`, or any user-defined type name. +#[cfg(feature = "idl")] impl JsonSchema for TypeRef { fn schema_name() -> String { "TypeRef".to_string() @@ -518,10 +525,14 @@ impl JsonSchema for TypeRef { /// A C-style enum lowers across the C ABI by value as an integer, while an /// 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)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] -#[schemars( - description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields." +#[cfg_attr( + feature = "idl", + schemars( + description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields." + ) )] pub struct EnumDef { /// Enum type name (for example `Color`). @@ -550,7 +561,8 @@ impl EnumDef { } /// A single variant of an [`EnumDef`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] pub struct EnumVariant { /// Variant name (for example `Red`). @@ -570,9 +582,13 @@ pub struct EnumVariant { } /// A struct (record) type with named fields. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] -#[schemars(description = "A struct (record) type with named fields.")] +#[cfg_attr( + feature = "idl", + schemars(description = "A struct (record) type with named fields.") +)] pub struct StructDef { /// Struct type name (for example `Contact`). pub name: String, @@ -591,7 +607,8 @@ pub struct StructDef { /// A named field of a [`StructDef`], or the payload of an algebraic /// [`EnumVariant`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] pub struct StructField { /// Field name (for example `email`). @@ -607,7 +624,8 @@ pub struct StructField { /// A module's error domain: the named set of error codes its fallible functions /// can report. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] pub struct ErrorDomain { /// Error domain name, used to name the generated error type (for example @@ -618,7 +636,8 @@ pub struct ErrorDomain { } /// A single named error within an [`ErrorDomain`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "idl", derive(JsonSchema))] #[serde(deny_unknown_fields)] pub struct ErrorCode { /// Error code name, lowered to a variant or constant on the generated error @@ -732,14 +751,6 @@ mod tests { assert!(parse_type_ref("?").is_err()); } - #[test] - fn removed_spellings_are_errors_with_guidance() { - for removed in ["handle", "handle", "&str", "&[u8]", "[handle]"] { - let err = parse_type_ref(removed).unwrap_err(); - assert!(err.contains("0.9"), "{removed}: {err}"); - } - } - #[test] fn walk_visits_nested_types_outermost_first() { let ty = parse_type_ref("{string:[Contact?]}").unwrap(); @@ -760,7 +771,7 @@ mod tests { #[test] fn document_round_trips_through_yaml_and_json() { let yaml = r#" -version: "0.9.0" +version: "0.10.0" modules: - name: contacts doc: Address book @@ -829,19 +840,6 @@ modules: assert_eq!(back2, api); } - #[test] - fn removed_document_keys_are_rejected() { - for yaml in [ - "version: \"0.9.0\"\nmodules:\n - name: m\n callbacks: []\n", - "version: \"0.9.0\"\nmodules:\n - name: m\n listeners: []\n", - "version: \"0.9.0\"\nmodules:\n - name: m\n functions:\n - { name: f, since: \"0.1.0\" }\n", - "version: \"0.9.0\"\nmodules:\n - name: m\n functions:\n - { name: f, params: [{ name: x, type: i32, mutable: true }] }\n", - ] { - let err = serde_yaml::from_str::(yaml).unwrap_err(); - assert!(err.to_string().contains("unknown field"), "{yaml}\n{err}"); - } - } - #[test] fn serialization_omits_defaulted_fields() { let api = Api { @@ -876,6 +874,7 @@ modules: } } + #[cfg(feature = "idl")] #[test] fn json_schema_types_are_strings() { let schema = schemars::schema_for!(Api); diff --git a/crates/weaveffi-model/src/lib.rs b/crates/weaveffi-model/src/lib.rs new file mode 100644 index 00000000..ef85b398 --- /dev/null +++ b/crates/weaveffi-model/src/lib.rs @@ -0,0 +1,44 @@ +//! The WeaveFFI API model: everything between an API definition and code +//! generation. +//! +//! * [`ir`] holds the in-memory IR types that an IDL document or annotated +//! Rust source lowers to, and [`parse`] reads the IDL text formats (YAML, +//! JSON, and TOML). +//! * [`rust`] extracts the IR from annotated Rust source; the proc-macros and +//! the CLI share it. +//! * [`validate`] checks a document and produces the [`ResolvedApi`] every +//! consumer works from. +//! * [`model`], [`abi`], [`plan`], [`errors`], and [`pkg`] build the canonical +//! binding model, the C ABI lowering, the marshalling plan, the error +//! mapping, and the package identity. +//! * [`checksum`] fingerprints each top-level module's contract so a stale +//! binding refuses to load. +//! +//! # Features +//! +//! * `idl` (default): the IDL text formats, JSON Schema derivation for the IR +//! types, and fancy miette diagnostics. Without it the crate still provides +//! the IR types, Rust extraction, validation, and the binding model, which +//! is all the proc-macros need. +#![deny(missing_docs)] +#![warn(clippy::missing_errors_doc)] +#![warn(clippy::missing_panics_doc)] +#![warn(clippy::doc_markdown)] +// A few docs link to IDL-only items (the `parse` module, miette's +// `Diagnostic`); those links only resolve with the `idl` feature. +#![cfg_attr(not(feature = "idl"), allow(rustdoc::broken_intra_doc_links))] + +pub mod abi; +pub mod checksum; +pub mod errors; +pub mod ir; +pub mod model; +#[cfg(feature = "idl")] +pub mod parse; +pub mod pkg; +pub mod plan; +pub mod resolved; +pub mod rust; +pub mod validate; + +pub use resolved::ResolvedApi; diff --git a/crates/weaveffi-core/src/model/mod.rs b/crates/weaveffi-model/src/model/mod.rs similarity index 84% rename from crates/weaveffi-core/src/model/mod.rs rename to crates/weaveffi-model/src/model/mod.rs index a4857301..e3faf644 100644 --- a/crates/weaveffi-core/src/model/mod.rs +++ b/crates/weaveffi-model/src/model/mod.rs @@ -24,11 +24,11 @@ mod ty; pub use ty::{Family, Prim, Ty, WireType}; -use heck::ToUpperCamelCase; -use weaveffi_ir::ir::{ +use crate::ir::{ CallbackInterfaceDef, EnumDef, ErrorDomain, Function, InterfaceDef, Module, StructDef, StructField, TypeRef, }; +use heck::ToUpperCamelCase; use crate::abi::{ async_callback_params, async_input_params, callback_method_signature, context_param, @@ -36,6 +36,49 @@ use crate::abi::{ }; use crate::resolved::ResolvedApi; +/// The runtime symbols every producer exports (and every C header declares), +/// without the `{prefix}_` that begins each one. See the C ABI contract for +/// their signatures. +pub const RUNTIME_SYMBOLS: &[&str] = &[ + "str", + "bytes", + "reader", + "writer", + "abi_version", + "error", + "error_set", + "error_clear", + "error_free", + "free_bytes", + "cancel_token", + "cancel_token_create", + "cancel_token_cancel", + "cancel_token_is_cancelled", + "cancel_token_destroy", + "alloc", + "dealloc", + "debug_live", +]; + +/// Identifier families the generated C value-buffer helper header +/// (`{library}_buffer.h`) declares, without the `{prefix}_` that begins each: +/// a user declaration whose C symbol falls in one of these families would +/// collide with a helper. +pub const RESERVED_SYMBOL_FAMILIES: &[&str] = &[ + "str_", "bytes_", "reader_", "writer_", "list_", "map_", "opt_", +]; + +/// The C ABI revision this model lowers to. Producers export it from +/// `{prefix}_abi_version()` and every generated consumer checks it at load. +pub const ABI_VERSION: u32 = 3; + +/// The symbol of a top-level module's contract checksum function, +/// `{prefix}_{module}_checksum`. +#[must_use] +pub fn checksum_symbol(prefix: &str, module: &str) -> String { + format!("{prefix}_{module}_checksum") +} + /// A single lowered C symbol: its name, ordered ABI parameter slots, and C /// return type. This is what a backend declares to its FFI layer and calls. #[derive(Debug, Clone, PartialEq, Eq)] @@ -366,6 +409,10 @@ pub struct ErrorBinding { pub struct ModuleBinding { /// The module name (its final path segment). pub name: String, + /// The contract checksum ([`crate::checksum::module_checksum`]) of a + /// top-level module; `None` for nested modules, which are covered by + /// their root's checksum. + pub checksum: Option, /// Path segments from the root (e.g. `["outer", "inner"]`). pub segments: Vec, /// Underscore-joined path used as the C symbol segment (e.g. `outer_inner`). @@ -451,11 +498,11 @@ pub struct BindingModel { } impl BindingModel { - /// Build the model from a [`ResolvedApi`], using `prefix` for every C - /// symbol name. `prefix` is the single global ABI prefix (default - /// `"weaveffi"`); passing the same prefix to every backend is what keeps - /// the producer header and all consumers calling identical symbols. - pub fn build(api: &ResolvedApi, prefix: &str) -> Self { + /// Build the model from a [`ResolvedApi`]. Every C symbol name starts + /// with the API identity's prefix, which is the same for the producer and + /// every consumer of one library. + pub fn build(api: &ResolvedApi) -> Self { + let prefix = api.identity().prefix.as_str(); let mut modules = Vec::new(); let lowerer = Lowerer { api, prefix }; for m in &api.modules { @@ -525,6 +572,111 @@ impl BindingModel { .and_then(|m| m.interfaces.iter().find(|i| i.name == bare)) } + /// Every C identifier the library's ABI declares, paired with a + /// human-readable description of the declaration that owns it, in + /// declaration order. Validation rejects an API in which two entries share + /// an identifier. + /// + /// Covers the runtime surface ([`RUNTIME_SYMBOLS`]), each top-level + /// module's checksum function, every callable's symbols (sync entry + /// point, async launcher and completion type, iterator launcher, type, + /// `_next`, and `_destroy`), interface types with their `_clone` and + /// `_destroy`, C-style enum types and constants, error-domain types and + /// code constants, and callback-interface vtable types. + pub fn c_symbols(&self) -> Vec<(String, String)> { + let p = &self.prefix; + let mut out: Vec<(String, String)> = RUNTIME_SYMBOLS + .iter() + .map(|s| (format!("{p}_{s}"), format!("the runtime symbol '{p}_{s}'"))) + .collect(); + for m in self.roots() { + out.push(( + checksum_symbol(p, &m.name), + format!("the checksum of module '{}'", m.name), + )); + } + let callable = |out: &mut Vec<(String, String)>, f: &FnBinding, owner: &str| { + let what = format!("'{owner}.{}'", f.name); + match &f.shape { + CallShape::Sync(abi) => out.push((abi.symbol.clone(), format!("function {what}"))), + CallShape::Async(a) => { + out.push((a.launch.symbol.clone(), format!("async function {what}"))); + out.push(( + a.callback_type.clone(), + format!("the completion type of {what}"), + )); + } + CallShape::Iterator(it) => { + out.push((it.launch.symbol.clone(), format!("function {what}"))); + out.push((it.iter_tag.clone(), format!("the iterator type of {what}"))); + out.push(( + it.next.symbol.clone(), + format!("the iterator step of {what}"), + )); + out.push(( + it.destroy_symbol.clone(), + format!("the iterator destructor of {what}"), + )); + } + } + }; + for m in &self.modules { + let dot = &m.dot_path; + if let Some(e) = m.error.as_ref().filter(|e| e.declared_here) { + out.push((e.c_tag.clone(), format!("error domain '{dot}.{}'", e.name))); + for c in &e.codes { + out.push(( + c.c_const.clone(), + format!("error code '{dot}.{}.{}'", e.name, c.name), + )); + } + } + for e in &m.enums { + out.push((e.c_tag.clone(), format!("enum '{dot}.{}'", e.name))); + if e.rich { + out.push(( + format!("{}_Tag", e.c_tag), + format!("the tag type of enum '{dot}.{}'", e.name), + )); + } + for v in &e.variants { + out.push(( + v.c_const.clone(), + format!("enum variant '{dot}.{}.{}'", e.name, v.name), + )); + } + } + for s in &m.structs { + out.push(( + format!("{p}_{}_{}", m.path, s.name), + format!("record '{dot}.{}'", s.name), + )); + } + for c in &m.callback_interfaces { + out.push(( + c.vtable_tag.clone(), + format!("the vtable of callback interface '{dot}.{}'", c.name), + )); + } + for i in &m.interfaces { + let owner = format!("{dot}.{}", i.name); + out.push((i.c_tag.clone(), format!("interface '{owner}'"))); + out.push((i.clone_symbol.clone(), format!("the clone of '{owner}'"))); + out.push(( + i.destroy_symbol.clone(), + format!("the destructor of '{owner}'"), + )); + for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) { + callable(&mut out, f, &owner); + } + } + for f in &m.functions { + callable(&mut out, f, dot); + } + } + out + } + /// `true` when any callable anywhere in the API is `async`. pub fn has_async(&self) -> bool { self.modules.iter().any(ModuleBinding::has_async) @@ -633,13 +785,11 @@ struct Lowerer<'a> { struct Scope<'a> { /// Underscore-joined C path (`outer_inner`). path: &'a str, - /// Dot-joined qualification path (`outer.inner`). - dot_path: &'a str, } impl Lowerer<'_> { - fn ty(&self, ty: &TypeRef, scope: &Scope<'_>) -> Ty { - self.api.resolve(ty, scope.dot_path) + fn ty(&self, ty: &TypeRef) -> Ty { + self.api.resolve(ty) } /// Recursively lower `module` and its descendants into the flat `out` @@ -658,10 +808,7 @@ impl Lowerer<'_> { segments.push(module.name.clone()); let path = segments.join("_"); let dot_path = segments.join("."); - let scope = Scope { - path: &path, - dot_path: &dot_path, - }; + let scope = Scope { path: &path }; let prefix = self.prefix; let error = match &module.errors { @@ -677,11 +824,7 @@ impl Lowerer<'_> { .iter() .map(|e| self.enum_def(e, &scope)) .collect(); - let structs = module - .structs - .iter() - .map(|s| self.struct_def(s, &scope)) - .collect(); + let structs = module.structs.iter().map(|s| self.struct_def(s)).collect(); let interfaces = module .interfaces .iter() @@ -708,6 +851,9 @@ impl Lowerer<'_> { out.push(ModuleBinding { name: module.name.clone(), + checksum: parent + .is_empty() + .then(|| crate::checksum::module_checksum(module)), segments: segments.clone(), path: path.clone(), dot_path: dot_path.clone(), @@ -742,29 +888,29 @@ impl Lowerer<'_> { message: c.message.clone(), doc: c.doc.clone(), c_const: format!("{c_tag}_{}", c.name), - fields: self.fields(&c.fields, scope), + fields: self.fields(&c.fields), }) .collect(), } } - fn fields(&self, fields: &[StructField], scope: &Scope<'_>) -> Vec { + fn fields(&self, fields: &[StructField]) -> Vec { fields .iter() .map(|f| FieldBinding { name: f.name.clone(), doc: f.doc.clone(), - ty: self.ty(&f.ty, scope), + ty: self.ty(&f.ty), }) .collect() } - fn struct_def(&self, s: &StructDef, scope: &Scope<'_>) -> StructBinding { + fn struct_def(&self, s: &StructDef) -> StructBinding { StructBinding { name: s.name.clone(), doc: s.doc.clone(), deprecated: s.deprecated.clone(), - fields: self.fields(&s.fields, scope), + fields: self.fields(&s.fields), } } @@ -778,7 +924,7 @@ impl Lowerer<'_> { value: v.value, doc: v.doc.clone(), c_const: format!("{c_tag}_{}", v.name), - fields: self.fields(&v.fields, scope), + fields: self.fields(&v.fields), }) .collect(); EnumBinding { @@ -791,10 +937,10 @@ impl Lowerer<'_> { } } - fn params(&self, params: &[weaveffi_ir::ir::Param], scope: &Scope<'_>) -> Vec { + fn params(&self, params: &[crate::ir::Param], scope: &Scope<'_>) -> Vec { params .iter() - .map(|p| ParamBinding::new(&p.name, self.ty(&p.ty, scope), p.doc.clone(), scope.path)) + .map(|p| ParamBinding::new(&p.name, self.ty(&p.ty), p.doc.clone(), scope.path)) .collect() } @@ -809,7 +955,7 @@ impl Lowerer<'_> { .iter() .map(|m| { let params = self.params(&m.params, scope); - let ret = m.returns.as_ref().map(|r| self.ty(r, scope)); + let ret = m.returns.as_ref().map(|r| self.ty(r)); let sig = callback_method_signature(¶ms, ret.as_ref(), scope.path); CallbackMethodBinding { name: m.name.clone(), @@ -896,7 +1042,7 @@ impl Lowerer<'_> { 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)); + let ret = f.returns.as_ref().map(|r| self.ty(r)); // The prefix-stripped spelling used for `CType::Named` cores (which // render as `{prefix}_{core}`), e.g. `kv_Store_scan` from // `weaveffi_kv_Store_scan`. @@ -957,7 +1103,7 @@ impl Lowerer<'_> { )); launch_params.push(context_param()); let launch = AbiFn { - symbol: format!("{c_base}_async"), + symbol: c_base.to_string(), params: with_self(launch_params), ret: CType::Void, }; @@ -1000,7 +1146,7 @@ pub fn iterator_item_ctype(elem: &Ty, module: &str) -> CType { #[cfg(test)] mod tests { use super::*; - use weaveffi_ir::ir::{ + use crate::ir::{ Api, CallbackInterfaceDef, EnumDef, EnumVariant, Function, InterfaceDef, Module, Param, StructDef, StructField, }; @@ -1042,7 +1188,7 @@ mod tests { fn api(modules: Vec) -> ResolvedApi { ResolvedApi::assume_valid(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), + version: crate::ir::CURRENT_SCHEMA_VERSION.into(), modules, }) } @@ -1064,7 +1210,9 @@ mod tests { )], ..module("math") }; - let model = BindingModel::build(&api(vec![m]), "weaveffi"); + let model = BindingModel::build( + &api(vec![m]).with_identity(crate::pkg::Identity::named("weaveffi")), + ); let f = &model.modules[0].functions[0]; assert_eq!(f.c_base, "weaveffi_math_add"); let CallShape::Sync(abi) = &f.shape else { @@ -1077,7 +1225,9 @@ mod tests { ["int32_t a", "int32_t b", "weaveffi_error* out_err"] ); - let model = BindingModel::build(&api(vec![module("net")]), "acme"); + let model = BindingModel::build( + &api(vec![module("net")]).with_identity(crate::pkg::Identity::named("acme")), + ); assert_eq!(model.prefix, "acme"); } @@ -1095,17 +1245,19 @@ mod tests { }], ..module("net") }; - let model = BindingModel::build(&api(vec![m]), "weaveffi"); + let model = BindingModel::build( + &api(vec![m]).with_identity(crate::pkg::Identity::named("weaveffi")), + ); 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.launch.symbol, "weaveffi_net_fetch"); assert_eq!(a.callback_type, "weaveffi_net_fetch_callback"); let names: Vec<&str> = a.launch.params.iter().map(|p| p.name.as_str()).collect(); assert_eq!(names, ["id", "cancel_token", "callback", "context"]); assert_eq!(a.callback_params[0].name, "context"); assert_eq!(a.callback_params[1].name, "err"); - assert_eq!(a.callback_params[2].name, "result"); + assert_eq!(a.callback_params[2].name, "result_ptr"); assert!(model.has_async()); } @@ -1119,7 +1271,9 @@ mod tests { )], ..module("events") }; - let model = BindingModel::build(&api(vec![m]), "weaveffi"); + let model = BindingModel::build( + &api(vec![m]).with_identity(crate::pkg::Identity::named("weaveffi")), + ); let CallShape::Iterator(it) = &model.modules[0].functions[0].shape else { panic!("expected iterator") }; @@ -1132,7 +1286,7 @@ mod tests { ); 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_eq!(it.next.params[1].ty.render_c("weaveffi"), "const uint8_t**"); assert!(model.has_iterators()); } @@ -1194,14 +1348,16 @@ mod tests { )], ..module("ops") }; - let model = BindingModel::build(&api(vec![shared, other]), "weaveffi"); + let model = BindingModel::build( + &api(vec![shared, other]).with_identity(crate::pkg::Identity::named("weaveffi")), + ); let contacts = &model.modules[0]; let s = &contacts.structs[0]; assert_eq!(s.deprecated.as_deref(), Some("use Person")); - assert_eq!(s.fields[1].ty, Ty::Enum("Status".into())); + assert_eq!(s.fields[1].ty, Ty::Enum("contacts.Status".into())); assert_eq!( s.fields[2].ty, - Ty::Optional(Box::new(Ty::Interface("Store".into()))) + Ty::Optional(Box::new(Ty::Interface("contacts.Store".into()))) ); assert_eq!(contacts.enums[0].c_tag, "weaveffi_contacts_Status"); assert_eq!( @@ -1215,11 +1371,11 @@ mod tests { assert_eq!(iface.destroy_symbol, "weaveffi_contacts_Store_destroy"); assert_eq!( iface.constructors[0].ret, - Some(Ty::Interface("Store".into())) + Some(Ty::Interface("contacts.Store".into())) ); let save = &iface.methods[0]; assert!(save.has_self); - assert_eq!(save.params[0].ty, Ty::Record("Contact".into())); + assert_eq!(save.params[0].ty, Ty::Record("contacts.Contact".into())); let CallShape::Sync(abi) = &save.shape else { panic!("expected sync") }; @@ -1282,7 +1438,9 @@ mod tests { )], ..module("events") }; - let model = BindingModel::build(&api(vec![m]), "weaveffi"); + let model = BindingModel::build( + &api(vec![m]).with_identity(crate::pkg::Identity::named("weaveffi")), + ); let mb = &model.modules[0]; let cb = &mb.callback_interfaces[0]; assert_eq!(cb.c_tag, "weaveffi_events_Listener"); @@ -1295,7 +1453,12 @@ mod tests { .collect(); assert_eq!( slots, - ["void* ctx", "const char* text", "weaveffi_error* out_err"] + [ + "void* ctx", + "const uint8_t* text_ptr", + "size_t text_len", + "weaveffi_error* out_err" + ] ); assert_eq!(on_message.abi_ret, CType::Void); assert_eq!(cb.methods[1].abi_ret, CType::Bool); @@ -1303,7 +1466,10 @@ mod tests { assert!(model.has_callback_interfaces()); let f = &mb.functions[0]; - assert_eq!(f.params[0].ty, Ty::CallbackInterface("Listener".into())); + assert_eq!( + f.params[0].ty, + Ty::CallbackInterface("events.Listener".into()) + ); let CallShape::Sync(abi) = &f.shape else { panic!("expected sync") }; @@ -1334,7 +1500,9 @@ mod tests { modules: vec![inner], ..module("outer") }; - let model = BindingModel::build(&api(vec![outer]), "weaveffi"); + let model = BindingModel::build( + &api(vec![outer]).with_identity(crate::pkg::Identity::named("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"); diff --git a/crates/weaveffi-core/src/model/ty.rs b/crates/weaveffi-model/src/model/ty.rs similarity index 98% rename from crates/weaveffi-core/src/model/ty.rs rename to crates/weaveffi-model/src/model/ty.rs index ab8aec64..6a4a7d29 100644 --- a/crates/weaveffi-core/src/model/ty.rs +++ b/crates/weaveffi-model/src/model/ty.rs @@ -1,7 +1,7 @@ //! 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*: +//! The IDL's [`TypeRef`](crate::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, an interface, or a callback //! interface. [`Ty`] is the type *as resolved* by validation: every user @@ -90,8 +90,9 @@ pub enum Ty { pub enum Family { /// One C slot passed by value: scalars, bools, and C-style enums. Direct, - /// One `char*` slot. Borrowed for a call as a parameter; producer-owned - /// and released with `{prefix}_free_string` as a return. + /// A `(ptr, len)` pair of UTF-8 bytes, never NUL-terminated. Borrowed for + /// a call as a parameter; producer-owned and released with + /// `{prefix}_free_bytes` as a return. String, /// A `(ptr, len)` raw byte pair. Borrowed as a parameter; producer-owned /// and released with `{prefix}_free_bytes` as a return. diff --git a/crates/weaveffi-ir/src/parse.rs b/crates/weaveffi-model/src/parse.rs similarity index 97% rename from crates/weaveffi-ir/src/parse.rs rename to crates/weaveffi-model/src/parse.rs index eceaa20d..465aceaf 100644 --- a/crates/weaveffi-ir/src/parse.rs +++ b/crates/weaveffi-model/src/parse.rs @@ -212,7 +212,7 @@ mod tests { #[test] fn every_format_parses_the_same_document() { let yaml = r#" -version: "0.9.0" +version: "0.10.0" modules: - name: math functions: @@ -226,7 +226,7 @@ modules: doc: "Adds two numbers" "#; let json = r#"{ - "version": "0.9.0", + "version": "0.10.0", "modules": [{ "name": "math", "functions": [{ @@ -241,7 +241,7 @@ modules: }] }"#; let toml_str = r#" -version = "0.9.0" +version = "0.10.0" [[modules]] name = "math" @@ -285,7 +285,7 @@ type = "i32" #[test] fn parse_errors_carry_spans() { - let yaml = "version: \"0.9.0\"\nmodules:\n - name: [oops\n"; + let yaml = "version: \"0.10.0\"\nmodules:\n - name: [oops\n"; match parse_api_str(yaml, "yaml").unwrap_err() { ParseError::Yaml { line, span, .. } => { assert!(line > 0); @@ -305,7 +305,7 @@ type = "i32" #[test] fn unknown_type_syntax_is_a_parse_error() { - let yaml = "version: \"0.9.0\"\nmodules:\n - name: m\n functions:\n - name: f\n params: [{ name: x, type: \"{string}\" }]\n"; + let yaml = "version: \"0.10.0\"\nmodules:\n - name: m\n functions:\n - name: f\n params: [{ name: x, type: \"{string}\" }]\n"; let err = parse_api_str(yaml, "yaml").unwrap_err(); assert!(err.to_string().contains("map type missing"), "{err}"); } diff --git a/crates/weaveffi-model/src/pkg.rs b/crates/weaveffi-model/src/pkg.rs new file mode 100644 index 00000000..54a7c8c4 --- /dev/null +++ b/crates/weaveffi-model/src/pkg.rs @@ -0,0 +1,293 @@ +//! Package identity: the one source of every name a generated package uses. +//! +//! The `[package]` table of a project's `weaveffi.toml` ([`Package`]) declares +//! what the library is called and how it is published. The CLI resolves it +//! once into an [`Identity`] and attaches it to the [`ResolvedApi`], so the C +//! symbol prefix, the native library name, and every ecosystem's package, +//! module, and namespace name derive from the same values. Nothing a +//! generator emits is named after WeaveFFI itself, so any number of +//! WeaveFFI-built libraries can coexist in one process and one application. +//! +//! [`ResolvedApi`]: crate::ResolvedApi + +use serde::{Deserialize, Serialize}; + +/// Fallback package version when the project omits `package.version`. +pub const DEFAULT_VERSION: &str = "0.1.0"; + +/// Fallback package name for an API that was never given an identity (unit +/// tests and other in-memory uses). The CLI always derives a real one. +pub const DEFAULT_NAME: &str = "api"; + +/// The `[package]` table of `weaveffi.toml`. +/// +/// Every field is optional in the file; the CLI fills the gaps from the +/// producer's `Cargo.toml` (for a Rust producer) or the input file name (for +/// an IDL) before calling [`Identity::new`]. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Package { + /// Distribution name used by every ecosystem (npm, PyPI, NuGet, and so + /// on) unless a target's own configuration overrides it. + 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, + /// C symbol prefix for an IDL-defined library. Defaults to the snake-case + /// package name. A Rust producer's prefix is always its crate name, so + /// setting this for a `.rs` input is an error. + pub c_prefix: Option, + /// Base name of the native library (`lib{library}.so`, `{library}.dll`) + /// for an IDL-defined library. Defaults to the snake-case package name. + /// A Rust producer's library is always its crate's library name. + pub library: Option, +} + +/// The resolved identity of the library being bound. +/// +/// Every field is final: generators read names from here (and from the +/// target's own configuration overrides) and never invent their own. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct Identity { + /// Distribution name as published (may contain `-` or `.`). + pub name: String, + /// C symbol prefix: a lower-snake C identifier that begins every exported + /// symbol and every C type name (`{prefix}_error`, `{prefix}_kv_get`). + pub prefix: String, + /// Native library base name: the producer is `lib{library}.so`, + /// `lib{library}.dylib`, or `{library}.dll`. + pub library: String, + /// Semantic version. + pub version: String, + /// Short description, if declared. + pub description: Option, + /// License identifier, if declared. + pub license: Option, + /// Package authors. + pub authors: Vec, + /// Project homepage URL, if declared. + pub homepage: Option, + /// Source repository URL, if declared. + pub repository: Option, +} + +impl Default for Identity { + fn default() -> Self { + Self::named(DEFAULT_NAME) + } +} + +impl Identity { + /// An identity with only a name: the prefix and library are the + /// snake-case name and every metadata field is unset. + #[must_use] + pub fn named(name: &str) -> Self { + Self::new(name, &Package::default()) + } + + /// Resolve `package` (whose own `name`, if set, wins over `name`) into a + /// complete identity. The prefix and library default to the snake-case + /// form of the resolved name. + #[must_use] + pub fn new(name: &str, package: &Package) -> Self { + let name = non_empty(package.name.as_ref()).unwrap_or_else(|| name.trim().to_string()); + let name = if name.is_empty() { + DEFAULT_NAME.to_string() + } else { + name + }; + let snake = c_ident(&name); + Self { + prefix: non_empty(package.c_prefix.as_ref()) + .map(|p| c_ident(&p)) + .unwrap_or_else(|| snake.clone()), + library: non_empty(package.library.as_ref()).unwrap_or(snake), + version: non_empty(package.version.as_ref()) + .unwrap_or_else(|| DEFAULT_VERSION.to_string()), + description: non_empty(package.description.as_ref()), + license: non_empty(package.license.as_ref()), + authors: package.authors.clone(), + homepage: non_empty(package.homepage.as_ref()), + repository: non_empty(package.repository.as_ref()), + name, + } + } + + /// The uppercase prefix used for C preprocessor macros (`KVSTORE_API`, + /// `KVSTORE_ABI_VERSION`). + #[must_use] + pub fn macro_prefix(&self) -> String { + self.prefix.to_ascii_uppercase() + } + + /// The name as a lower-snake identifier (a Python import package, a Ruby + /// `require` path, a Dart package). `"my-kv.store"` becomes + /// `"my_kv_store"`. + #[must_use] + pub fn snake_name(&self) -> String { + c_ident(&self.name) + } + + /// The name as an UpperCamelCase identifier (a Swift module, a .NET + /// namespace, a Ruby module). `"my-kv.store"` becomes `"MyKvStore"`. + #[must_use] + pub fn pascal_name(&self) -> String { + pascal_ident(&self.name) + } + + /// The description, or a generated one when unset. + #[must_use] + pub fn description_or_default(&self) -> String { + self.description + .clone() + .unwrap_or_else(|| format!("Native bindings for {}", self.name)) + } + + /// The environment variable a generated loader consults for an explicit + /// library path (`KVSTORE_LIBRARY`). + #[must_use] + pub fn library_env_var(&self) -> String { + format!("{}_LIBRARY", self.macro_prefix()) + } + + /// Platform file names for the native library: `(macOS, Linux, Windows)`. + #[must_use] + pub fn library_files(&self) -> (String, String, String) { + let l = &self.library; + ( + format!("lib{l}.dylib"), + format!("lib{l}.so"), + format!("{l}.dll"), + ) + } +} + +/// UpperCamelCase identifier form of an arbitrary name. Word boundaries are +/// runs of non-alphanumerics; existing camel humps are preserved. +/// `"my-kv.store"` becomes `"MyKvStore"`. +#[must_use] +pub fn pascal_ident(name: &str) -> String { + let mut out = String::with_capacity(name.len()); + let mut start_word = true; + for ch in name.chars() { + if ch.is_ascii_alphanumeric() { + if start_word { + out.push(ch.to_ascii_uppercase()); + } else { + out.push(ch); + } + start_word = false; + } else { + start_word = true; + } + } + if out.starts_with(|c: char| c.is_ascii_digit()) { + out.insert(0, 'N'); + } + if out.is_empty() { + pascal_ident(DEFAULT_NAME) + } else { + out + } +} + +/// Lower-snake C identifier form of an arbitrary name: non-alphanumerics +/// collapse to `_`, and a leading digit gets a `lib_` prefix. +/// `"my-kv.store"` becomes `"my_kv_store"`. +#[must_use] +pub fn c_ident(name: &str) -> String { + let mut out = String::with_capacity(name.len()); + let mut prev_us = false; + for ch in name.chars() { + if ch.is_ascii_alphanumeric() { + out.push(ch.to_ascii_lowercase()); + prev_us = false; + } else if !prev_us && !out.is_empty() { + out.push('_'); + prev_us = true; + } + } + let trimmed = out.trim_end_matches('_'); + if trimmed.is_empty() { + DEFAULT_NAME.to_string() + } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) { + format!("lib_{trimmed}") + } else { + trimmed.to_string() + } +} + +/// The file stem of a path-like input name: `"path/to/kv.store.yml"` becomes +/// `"kv"`. +#[must_use] +pub fn name_from_basename(basename: &str) -> String { + let file = basename.rsplit(['/', '\\']).next().unwrap_or(basename); + file.split('.').next().unwrap_or(file).to_string() +} + +fn non_empty(s: Option<&String>) -> Option { + s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn name_drives_prefix_and_library() { + let id = Identity::named("my-kv.store"); + assert_eq!(id.name, "my-kv.store"); + assert_eq!(id.prefix, "my_kv_store"); + assert_eq!(id.library, "my_kv_store"); + assert_eq!(id.macro_prefix(), "MY_KV_STORE"); + assert_eq!(id.pascal_name(), "MyKvStore"); + assert_eq!(id.library_env_var(), "MY_KV_STORE_LIBRARY"); + assert_eq!(id.version, DEFAULT_VERSION); + } + + #[test] + fn package_table_wins() { + let pkg = Package { + name: Some("kvstore".into()), + version: Some("1.2.0".into()), + c_prefix: Some("kv".into()), + library: Some("kvstore_core".into()), + ..Package::default() + }; + let id = Identity::new("ignored", &pkg); + assert_eq!(id.name, "kvstore"); + assert_eq!(id.prefix, "kv"); + assert_eq!(id.library, "kvstore_core"); + assert_eq!(id.version, "1.2.0"); + assert_eq!(id.description_or_default(), "Native bindings for kvstore"); + } + + #[test] + fn identifiers_sanitize() { + assert_eq!(c_ident("Kvstore"), "kvstore"); + assert_eq!(c_ident("--"), DEFAULT_NAME); + assert_eq!(c_ident("3d-engine"), "lib_3d_engine"); + assert_eq!(pascal_ident("kvstore"), "Kvstore"); + assert_eq!(pascal_ident("3d"), "N3d"); + assert_eq!(name_from_basename("path/to/contacts.yml"), "contacts"); + } + + #[test] + 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()); + } +} diff --git a/crates/weaveffi-core/src/plan.rs b/crates/weaveffi-model/src/plan.rs similarity index 80% rename from crates/weaveffi-core/src/plan.rs rename to crates/weaveffi-model/src/plan.rs index b59bf126..f21b9c19 100644 --- a/crates/weaveffi-core/src/plan.rs +++ b/crates/weaveffi-model/src/plan.rs @@ -13,9 +13,8 @@ //! * **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** ([`RetPass::free`], [`Free`]): after copying a returned -//! value into a native one, exactly which runtime release call does the -//! wrapper owe, if any? +//! * **Ownership** ([`RetPass::frees_bytes`]): after copying a returned value +//! into a native one, does the wrapper owe `{prefix}_free_bytes`? //! * **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). @@ -49,15 +48,17 @@ pub enum ErrorStrategy { /// `error` value, ...), decodes any payload fields from the error's /// `payload_ptr`/`payload_len` buffer, and surfaces it through the /// target's normal error channel so callers can catch and match on it. - /// A *negative* code (a runtime trap: panic, marshalling failure, - /// foreign callback failure) is still a programming error and follows - /// [`Trap`](Self::Trap) even on a throwing function. + /// A *negative* code (generic `-1`, panic `-2`, marshalling failure + /// `-3`, foreign callback failure `-4`, cancelled `-5`) surfaces through + /// the same channel as the target's runtime error type, distinct from + /// every domain error type, and `-5` as the language's cancellation error + /// where it has one. Throws, /// The function does not throw: the only way `out_err` reports failure /// is a producer bug or a runtime trap (a caught panic, code `-2`; a /// consumer callback that raised, code `-4`). The wrapper surfaces it - /// through the target's *programming-error* idiom (a Python - /// `WeaveFFIError`, a Go `panic`, a Swift `fatalError`, a C# exception). + /// through the target's *programming-error* idiom (an unchecked exception + /// where the language has them, a Go `panic`, a Swift `fatalError`). /// It must never be silently ignored, and it must never be dressed up as /// a typed domain error. Trap, @@ -89,12 +90,14 @@ pub enum ArgPass<'a> { /// The single ABI slot. slot: &'a AbiParam, }, - /// One `char*` slot. The wrapper encodes UTF-8 plus a NUL terminator - /// and keeps the encoding alive for the duration of the call; the - /// producer copies what it needs. + /// A borrowed `(ptr, len)` pair of UTF-8 bytes, not NUL-terminated. The + /// wrapper encodes the string and keeps the encoding alive for the + /// duration of the call; the producer copies what it needs. String { - /// The single `char*` slot. - slot: &'a AbiParam, + /// The `const uint8_t*` slot. + ptr: &'a AbiParam, + /// The `size_t` length slot. + len: &'a AbiParam, }, /// A borrowed `(ptr, len)` byte pair. The wrapper pins its native byte /// storage for the call; the producer copies what it needs. @@ -164,7 +167,10 @@ impl ParamBinding { }; match self.ty.family() { Family::Direct => ArgPass::Direct { slot: single() }, - Family::String => ArgPass::String { slot: single() }, + Family::String => { + let (ptr, len) = pair(); + ArgPass::String { ptr, len } + } Family::Bytes => { let (ptr, len) = pair(); ArgPass::Bytes { ptr, len } @@ -186,19 +192,6 @@ impl ParamBinding { } } -/// 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 value produced by the producer crosses back to the wrapper: the /// receiving contract, including the decode step and the release obligation. /// @@ -210,8 +203,9 @@ pub enum RetPass { /// By-value return (scalar, bool, C-style enum): use directly, nothing to /// free. Direct, - /// Owned `const char*`: copy into the native string, then release with - /// `{prefix}_free_string`. + /// Owned `(const uint8_t*, out_len)` UTF-8: decode into the native string, + /// then release with + /// `{prefix}_free_bytes`. String, /// Owned `(const uint8_t*, out_len)` raw bytes: copy, then release with /// `{prefix}_free_bytes`. @@ -237,15 +231,14 @@ 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, - } + /// Whether the wrapper owes `{prefix}_free_bytes(ptr, len)` after copying + /// or decoding a *returned* value: true for strings, bytes, and value + /// buffers. Adopted objects owe their `destroy` symbol instead (see + /// [`RetPass::Object`]). Callback-method *arguments* reuse this plan but + /// are borrowed for the call, so a trampoline never frees them. + #[must_use] + pub fn frees_bytes(&self) -> bool { + matches!(self, RetPass::String | RetPass::Bytes | RetPass::Buffer) } } @@ -259,7 +252,7 @@ impl RetPass { /// a value-passing plan (backends dispatch on /// [`CallShape`](crate::model::CallShape) before consulting this), and on a /// callback interface, which validation never admits as a return. -pub fn ret_pass(ty: Option<&Ty>, module: &str, prefix: &str) -> RetPass { +pub fn ret_pass(ty: Option<&Ty>, prefix: &str) -> RetPass { let Some(ty) = ty else { return RetPass::Void; }; @@ -273,8 +266,8 @@ pub fn ret_pass(ty: Option<&Ty>, module: &str, prefix: &str) -> RetPass { .interface_name() .expect("object family names an interface"); RetPass::Object { - destroy_symbol: destroy_symbol(iface, module, prefix), - clone_symbol: clone_symbol(iface, module, prefix), + destroy_symbol: destroy_symbol(iface, prefix), + clone_symbol: clone_symbol(iface, prefix), nullable, } } @@ -283,28 +276,17 @@ pub fn ret_pass(ty: Option<&Ty>, module: &str, prefix: &str) -> RetPass { } } -/// The per-element release owed for one iterator element of type `ty`. An -/// object element is adopted (see [`IteratorProtocol::elem`]) and owes -/// [`Free::None`] here. -pub fn elem_free(ty: &Ty) -> Free { - 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`. -pub fn destroy_symbol(name: &str, current_module: &str, prefix: &str) -> String { - let (module, name) = split_qualified(name, current_module); +pub fn destroy_symbol(name: &str, prefix: &str) -> String { + let (module, name) = split_qualified(name, ""); format!("{prefix}_{module}_{name}_destroy") } /// The `{prefix}_{module}_{Name}_clone` symbol for a (possibly /// dot-qualified) interface name referenced from `current_module`. -pub fn clone_symbol(name: &str, current_module: &str, prefix: &str) -> String { - let (module, name) = split_qualified(name, current_module); +pub fn clone_symbol(name: &str, prefix: &str) -> String { + let (module, name) = split_qualified(name, ""); format!("{prefix}_{module}_{name}_clone") } @@ -337,9 +319,6 @@ pub struct IteratorProtocol<'a> { pub binding: &'a IteratorBinding, /// How each element written by `next` is received. pub elem: RetPass, - /// The release owed for each element copied out of a `next` slot - /// (`elem.free()`, kept for convenience). - pub elem_free: Free, /// How `out_err` reports from the launcher and each `next` call are /// interpreted. pub error: ErrorStrategy, @@ -348,16 +327,10 @@ pub struct IteratorProtocol<'a> { impl IteratorBinding { /// Build the full pull contract for this iterator, resolving the element /// plan against the declaring `module` and `prefix`. - pub fn protocol<'a>( - &'a self, - f: &FnBinding, - module: &str, - prefix: &str, - ) -> IteratorProtocol<'a> { - let elem = ret_pass(Some(&self.elem), module, prefix); + pub fn protocol<'a>(&'a self, f: &FnBinding, prefix: &str) -> IteratorProtocol<'a> { + let elem = ret_pass(Some(&self.elem), prefix); IteratorProtocol { binding: self, - elem_free: elem.free(), elem, error: f.error_strategy(), } @@ -375,7 +348,7 @@ impl IteratorBinding { /// JS `Promise`, a Swift continuation, a C# `TaskCompletionSource`, a Go /// channel) exactly once and then releases the registration. /// 2. **Owned results.** Everything passed to the callback is owned by the -/// consumer. String results are released with `{prefix}_free_string`, +/// consumer. String results are released with `{prefix}_free_bytes`, /// byte and buffered-value results with `{prefix}_free_bytes`, and /// interface-object results transfer one strong reference (the wrapper /// adopts the pointer). This is what lets runtimes that defer callback @@ -400,7 +373,7 @@ pub struct AsyncProtocol<'a> { /// `callback`/`context`. pub cancellable: bool, /// How the callback's result slots are received, including the release - /// owed ([`RetPass::free`]) or the destroy symbol an adopted object owes. + /// owed ([`RetPass::frees_bytes`]) or the destroy symbol an adopted object owes. pub result: RetPass, /// How the callback's `err` slot is interpreted. pub error: ErrorStrategy, @@ -409,11 +382,11 @@ pub struct AsyncProtocol<'a> { impl AsyncBinding { /// Build the full completion contract for this async function, resolving /// the result plan against the declaring `module` and `prefix`. - pub fn protocol<'a>(&'a self, f: &FnBinding, module: &str, prefix: &str) -> AsyncProtocol<'a> { + pub fn protocol<'a>(&'a self, f: &FnBinding, prefix: &str) -> AsyncProtocol<'a> { AsyncProtocol { binding: self, cancellable: f.cancellable, - result: ret_pass(f.ret.as_ref(), module, prefix), + result: ret_pass(f.ret.as_ref(), prefix), error: f.error_strategy(), } } @@ -461,7 +434,7 @@ impl CallbackInterfaceBinding { /// Build the full contract for this callback interface, resolving each /// parameter's receiving plan against the declaring `module` and /// `prefix`. - pub fn protocol<'a>(&'a self, module: &str, prefix: &str) -> CallbackProtocol<'a> { + pub fn protocol<'a>(&'a self, prefix: &str) -> CallbackProtocol<'a> { CallbackProtocol { binding: self, method_args: self @@ -470,7 +443,7 @@ impl CallbackInterfaceBinding { .map(|m| { m.params .iter() - .map(|p| ret_pass(Some(&p.ty), module, prefix)) + .map(|p| ret_pass(Some(&p.ty), prefix)) .collect() }) .collect(), @@ -491,7 +464,7 @@ mod tests { )); assert!(matches!( pb("s", Ty::StringUtf8).arg_pass(), - ArgPass::String { slot } if slot.name == "s" + ArgPass::String { ptr, .. } if ptr.name == "s_ptr" )); assert!(matches!( pb("data", Ty::Bytes).arg_pass(), @@ -528,13 +501,10 @@ 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(&Ty::StringUtf8), "m", "weaveffi"), - RetPass::String - ); - assert_eq!(ret_pass(Some(&Ty::Bytes), "m", "weaveffi"), RetPass::Bytes); + assert_eq!(ret_pass(None, "weaveffi"), RetPass::Void); + assert_eq!(ret_pass(Some(&Ty::I64), "weaveffi"), RetPass::Direct); + assert_eq!(ret_pass(Some(&Ty::StringUtf8), "weaveffi"), RetPass::String); + assert_eq!(ret_pass(Some(&Ty::Bytes), "weaveffi"), RetPass::Bytes); for ty in [ Ty::Record("Contact".into()), Ty::RichEnum("Shape".into()), @@ -542,14 +512,10 @@ mod tests { Ty::List(Box::new(Ty::Interface("Store".into()))), Ty::Optional(Box::new(Ty::I64)), ] { - assert_eq!( - ret_pass(Some(&ty), "m", "weaveffi"), - RetPass::Buffer, - "{ty}" - ); + assert_eq!(ret_pass(Some(&ty), "weaveffi"), RetPass::Buffer, "{ty}"); } assert_eq!( - ret_pass(Some(&Ty::Interface("kv.Store".into())), "m", "weaveffi"), + ret_pass(Some(&Ty::Interface("kv.Store".into())), "weaveffi"), RetPass::Object { destroy_symbol: "weaveffi_kv_Store_destroy".into(), clone_symbol: "weaveffi_kv_Store_clone".into(), @@ -558,8 +524,7 @@ mod tests { ); assert_eq!( ret_pass( - Some(&Ty::Optional(Box::new(Ty::Interface("Store".into())))), - "kv", + Some(&Ty::Optional(Box::new(Ty::Interface("kv.Store".into())))), "weaveffi" ), RetPass::Object { @@ -568,18 +533,8 @@ 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(&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); - assert_eq!(elem_free(&Ty::Interface("Store".into())), Free::None); + assert!(RetPass::String.frees_bytes()); + assert!(RetPass::Buffer.frees_bytes()); + assert!(!RetPass::Direct.frees_bytes()); } } diff --git a/crates/weaveffi-core/src/resolved.rs b/crates/weaveffi-model/src/resolved.rs similarity index 65% rename from crates/weaveffi-core/src/resolved.rs rename to crates/weaveffi-model/src/resolved.rs index ff020117..c6aa4bef 100644 --- a/crates/weaveffi-core/src/resolved.rs +++ b/crates/weaveffi-model/src/resolved.rs @@ -18,10 +18,10 @@ use std::collections::BTreeMap; -use weaveffi_ir::ir::{Api, Module, TypeRef}; +use crate::ir::{Api, Module, TypeRef}; use crate::model::Ty; -use crate::pkg::Package; +use crate::pkg::Identity; /// What kind of declaration a bare type name refers to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -50,50 +50,45 @@ pub struct TypeDecl { /// 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. +/// The checked way to obtain one is +/// [`validate_api`](crate::validate::validate_api). [`assume_valid`](Self::assume_valid) +/// exists for the `#[weaveffi::module]` proc-macro, which expands one module +/// tree in isolation, and for tests that build well-formed trees by hand. /// /// `ResolvedApi` dereferences to [`Api`] for read access; there is no mutable -/// access, so the index can never go stale. -/// -/// 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. +/// access, so the index can never go stale. It also carries the library's +/// [`Identity`], which the CLI attaches with [`with_identity`](Self::with_identity) +/// and every generator reads names from. #[derive(Debug, Clone, PartialEq)] pub struct ResolvedApi { api: Api, types: BTreeMap, - package: Option, + identity: Identity, } impl ResolvedApi { - /// Attach the project's package identity. Generators read it through - /// [`pkg::resolve`](crate::pkg::resolve). + /// Attach the library's resolved identity. #[must_use] - pub fn with_package(mut self, package: Package) -> Self { - self.package = Some(package); + pub fn with_identity(mut self, identity: Identity) -> Self { + self.identity = identity; self } - /// The project's package identity, when the CLI attached one. - pub fn package(&self) -> Option<&Package> { - self.package.as_ref() + /// The library's identity: its name, C symbol prefix, native library + /// name, and package metadata. + pub fn identity(&self) -> &Identity { + &self.identity } - /// Wrap an [`Api`] the caller asserts satisfies every validation rule, - /// indexing its declarations for [`resolve`](Self::resolve). + /// Wrap a document without running validation. /// - /// 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. + /// Type names that resolve to no declaration become [`Ty::Record`] + /// references named exactly as written; [`unresolved`](Self::unresolved) + /// lists them. The proc-macro relies on this to expand one module tree + /// while a sibling tree declares some of the types it mentions, and + /// asserts at compile time that each such type really crosses as a value + /// buffer. + #[doc(hidden)] pub fn assume_valid(api: Api) -> Self { let mut types = BTreeMap::new(); for module in &api.modules { @@ -102,29 +97,51 @@ impl ResolvedApi { Self { api, types, - package: None, + identity: Identity::default(), } } - /// Borrow the underlying [`Api`] document. + /// The underlying document. pub fn api(&self) -> &Api { &self.api } - /// Look up where a bare type name is declared. + /// Look up the declaration a bare type name refers to. pub fn declaration(&self, name: &str) -> Option<&TypeDecl> { - self.types.get(name) + self.types.get(bare(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. - pub fn resolve(&self, ty: &TypeRef, module_path: &str) -> Ty { + /// Every type name the document references that no declaration in it + /// provides, deduplicated and sorted. Always empty for an API that + /// passed validation. + pub fn unresolved(&self) -> Vec { + let mut out = std::collections::BTreeSet::new(); + let mut visit = |ty: &TypeRef| { + ty.walk(&mut |t| { + if let TypeRef::Named(n) = t { + if self.declaration(n).is_none() { + out.insert(n.clone()); + } + } + }); + }; + for_each_type_ref(&self.api.modules, &mut visit); + out.into_iter().collect() + } + + /// The absolute, dot-joined name of the type a reference denotes + /// (`kv.Store`, `shapes.geo.Point`). Unknown names come back as written. + pub fn qualified_name(&self, name: &str) -> String { + match self.declaration(name) { + Some(decl) => format!("{}.{}", decl.module_path, bare(name)), + None => name.to_string(), + } + } + + /// Map a written type reference to its resolved type. User types carry + /// their kind and absolute dot-joined name, so two references to one + /// declaration compare equal wherever they are written. + pub fn resolve(&self, ty: &TypeRef) -> Ty { match ty { TypeRef::I8 => Ty::I8, TypeRef::I16 => Ty::I16, @@ -140,8 +157,8 @@ impl ResolvedApi { TypeRef::StringUtf8 => Ty::StringUtf8, TypeRef::Bytes => Ty::Bytes, TypeRef::Named(name) => { - let qualified = self.qualify(name, module_path); - match self.types.get(bare(name)).map(|d| d.kind) { + let qualified = self.qualified_name(name); + match self.declaration(name).map(|d| d.kind) { Some(TypeKind::Enum) => Ty::Enum(qualified), Some(TypeKind::RichEnum) => Ty::RichEnum(qualified), Some(TypeKind::Interface) => Ty::Interface(qualified), @@ -149,38 +166,63 @@ impl ResolvedApi { 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))), + TypeRef::Optional(inner) => Ty::Optional(Box::new(self.resolve(inner))), + TypeRef::List(inner) => Ty::List(Box::new(self.resolve(inner))), + TypeRef::Map(k, v) => Ty::Map(Box::new(self.resolve(k)), Box::new(self.resolve(v))), + TypeRef::Iterator(inner) => Ty::Iterator(Box::new(self.resolve(inner))), } } +} - /// 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(), +/// Call `f` on every type reference written anywhere in `modules`. +pub(crate) fn for_each_type_ref(modules: &[Module], f: &mut dyn FnMut(&TypeRef)) { + fn fns(fs: &[crate::ir::Function], f: &mut dyn FnMut(&TypeRef)) { + for func in fs { + for p in &func.params { + f(&p.ty); + } + if let Some(r) = &func.returns { + f(r); + } } } + for m in modules { + fns(&m.functions, f); + for i in &m.interfaces { + fns(&i.constructors, f); + fns(&i.methods, f); + fns(&i.statics, f); + } + for c in &m.callback_interfaces { + fns(&c.methods, f); + } + for s in &m.structs { + for field in &s.fields { + f(&field.ty); + } + } + for e in &m.enums { + for v in &e.variants { + for field in &v.fields { + f(&field.ty); + } + } + } + if let Some(d) = &m.errors { + for c in &d.codes { + for field in &c.fields { + f(&field.ty); + } + } + } + for_each_type_ref(&m.modules, f); + } } -/// 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, and callback 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() @@ -232,7 +274,7 @@ impl AsRef for ResolvedApi { #[cfg(test)] mod tests { use super::*; - use weaveffi_ir::ir::{ + use crate::ir::{ CallbackInterfaceDef, EnumDef, EnumVariant, Function, InterfaceDef, StructDef, StructField, CURRENT_SCHEMA_VERSION, }; @@ -325,32 +367,32 @@ mod tests { fn kinds_and_qualification() { let api = api(); assert_eq!( - api.resolve(&TypeRef::Named("Status".into()), "shared"), - Ty::Enum("Status".into()) + api.resolve(&TypeRef::Named("Status".into())), + Ty::Enum("shared.Status".into()) ); assert_eq!( - api.resolve(&TypeRef::Named("Status".into()), "orders"), + api.resolve(&TypeRef::Named("Status".into())), Ty::Enum("shared.Status".into()) ); assert_eq!( - api.resolve(&TypeRef::Named("Shape".into()), "orders"), + api.resolve(&TypeRef::Named("Shape".into())), Ty::RichEnum("shared.Shape".into()) ); assert_eq!( - api.resolve(&TypeRef::Named("Store".into()), "shared"), + api.resolve(&TypeRef::Named("Store".into())), Ty::Interface("shared.inner.Store".into()) ); assert_eq!( - api.resolve(&TypeRef::Named("Store".into()), "shared.inner"), - Ty::Interface("Store".into()) + api.resolve(&TypeRef::Named("Store".into())), + Ty::Interface("shared.inner.Store".into()) ); // Already-qualified spellings normalize to the owner's path. assert_eq!( - api.resolve(&TypeRef::Named("shared.Point".into()), "orders"), + api.resolve(&TypeRef::Named("shared.Point".into())), Ty::Record("shared.Point".into()) ); assert_eq!( - api.resolve(&TypeRef::Named("Watcher".into()), "orders"), + api.resolve(&TypeRef::Named("Watcher".into())), Ty::CallbackInterface("shared.inner.Watcher".into()) ); assert_eq!( @@ -367,7 +409,7 @@ mod tests { Box::new(TypeRef::Optional(Box::new(TypeRef::Named("Point".into())))), ))); assert_eq!( - api.resolve(&ty, "orders"), + api.resolve(&ty), Ty::List(Box::new(Ty::Map( Box::new(Ty::StringUtf8), Box::new(Ty::Optional(Box::new(Ty::Record("shared.Point".into())))) @@ -379,9 +421,10 @@ mod tests { fn unknown_names_fall_back_to_records() { let api = api(); assert_eq!( - api.resolve(&TypeRef::Named("Elsewhere".into()), "orders"), + api.resolve(&TypeRef::Named("Elsewhere".into())), Ty::Record("Elsewhere".into()) ); + assert!(api.unresolved().is_empty()); assert!(api.declaration("Elsewhere").is_none()); assert_eq!(api.declaration("Point").unwrap().kind, TypeKind::Record); } diff --git a/crates/weaveffi-bridge/src/lib.rs b/crates/weaveffi-model/src/rust.rs similarity index 95% rename from crates/weaveffi-bridge/src/lib.rs rename to crates/weaveffi-model/src/rust.rs index e728258c..15259eb3 100644 --- a/crates/weaveffi-bridge/src/lib.rs +++ b/crates/weaveffi-model/src/rust.rs @@ -1,6 +1,6 @@ //! Extract a WeaveFFI [`Api`] from annotated Rust source. //! -//! This crate is the single Rust-to-IR bridge shared by two callers: +//! This module is the single Rust-to-IR bridge shared by two callers: //! //! * the `#[weaveffi::module]` proc-macro, which lowers a module to its IR to //! build the C ABI scaffolding at compile time; and @@ -41,13 +41,11 @@ //! names the callback interface `Trait`, and any other named path is a record, //! enum, or interface resolved later. -#![deny(missing_docs)] - -use syn::spanned::Spanned; -use weaveffi_ir::ir::{ +use crate::ir::{ Api, CallbackInterfaceDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, Module, Param, StructDef, StructField, TypeRef, CURRENT_SCHEMA_VERSION, }; +use syn::spanned::Spanned; /// Match a WeaveFFI marker attribute by its final path segment. /// @@ -587,13 +585,43 @@ fn members_from_impl(item_impl: &syn::ItemImpl, iface: &mut InterfaceDef) -> syn Ok(()) } +/// Whether a return type is `Result`: a callback method's +/// way of receiving the consumer's failure as a value. The `Result` is a +/// producer-side convention (the IDL method still returns `T` and doesn't +/// throw), so it's peeled without marking the method `throws`. +pub fn returns_foreign_result(output: &syn::ReturnType) -> bool { + let syn::ReturnType::Type(_, ty) = output else { + return false; + }; + let syn::Type::Path(p) = ty.as_ref() else { + return false; + }; + let Some(seg) = p.path.segments.last() else { + return false; + }; + if seg.ident != "Result" { + return false; + } + let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { + return false; + }; + match args.args.iter().nth(1) { + Some(syn::GenericArgument::Type(err)) => { + type_path_ident(err).as_deref() == Some("ForeignError") + } + _ => false, + } +} + /// Extract a `#[weaveffi::callback_interface]` trait into a /// [`CallbackInterfaceDef`]. /// /// Every trait method is a callback method the consumer implements. Methods -/// must take `&self`; the validator additionally rejects `async`, `Result` -/// returns, and non-direct return types, so those are mapped faithfully here -/// and reported with the IDL-level diagnostics. +/// must take `&self`. A method may return `Result` +/// to receive the consumer's failure as a value; that maps to a plain `T` +/// return. The validator rejects `async`, other `Result` returns, and +/// non-direct return types, so those are mapped faithfully here and reported +/// with the IDL-level diagnostics. fn extract_callback_interface(item: &syn::ItemTrait) -> syn::Result { let mut methods = Vec::new(); for trait_item in &item.items { @@ -617,7 +645,11 @@ fn extract_callback_interface(item: &syn::ItemTrait) -> syn::Result Result; + fn ping(&self) -> Result<(), ForeignError>; + fn strict(&self) -> Result; + } + } + "#, + ); + let methods = &m.callback_interfaces[0].methods; + assert_eq!(methods[0].returns, Some(TypeRef::I32)); + assert!(!methods[0].throws); + assert_eq!(methods[1].returns, None); + assert!(!methods[1].throws); + assert!(methods[2].throws, "other error types still throw"); + } + #[test] fn callback_interface_methods_need_self() { let err = api_from_src( diff --git a/crates/weaveffi-core/src/validate.rs b/crates/weaveffi-model/src/validate.rs similarity index 65% rename from crates/weaveffi-core/src/validate.rs rename to crates/weaveffi-model/src/validate.rs index 15b1a97e..4b9b04f1 100644 --- a/crates/weaveffi-core/src/validate.rs +++ b/crates/weaveffi-model/src/validate.rs @@ -7,15 +7,16 @@ //! with several problems reports them all in one run rather than one per //! invocation. +use crate::ir::{Api, SUPPORTED_VERSIONS}; +#[cfg(feature = "idl")] use miette::Diagnostic; use std::collections::BTreeSet; -use weaveffi_ir::ir::{Api, SUPPORTED_VERSIONS}; use crate::resolved::ResolvedApi; mod diagnostic; mod rules; -#[cfg(test)] +#[cfg(all(test, feature = "idl"))] mod tests; mod warnings; @@ -28,27 +29,40 @@ pub use warnings::{collect_warnings, ValidationWarning}; /// the names needed to render an actionable diagnostic, and the /// `#[error]`/`#[diagnostic]` attributes supply the message and help text /// shown to the user. -#[derive(Debug, thiserror::Error, Diagnostic)] +#[derive(Debug, thiserror::Error)] +#[cfg_attr(feature = "idl", derive(Diagnostic))] pub enum ValidationError { /// A module is missing its required `name` field. #[error("module has no name")] - #[diagnostic(help("every module must have a non-empty 'name' field"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("every module must have a non-empty 'name' field")) + )] NoModuleName, /// Two modules share the same name. #[error("duplicate module name: {0}")] - #[diagnostic(help( - "module names must be unique within an API definition; rename or merge the duplicate" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "module names must be unique within an API definition; rename or merge the duplicate" + )) + )] DuplicateModuleName(String), /// A module name is not a valid identifier; the second field explains why. #[error("invalid module name '{0}': {1}")] - #[diagnostic(help( - "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word" + )) + )] InvalidModuleName(String, &'static str), /// Two functions in the same module share a name. #[error("duplicate function name in module '{module}': {function}")] - #[diagnostic(help("function names must be unique within a module; rename the duplicate"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("function names must be unique within a module; rename the duplicate")) + )] DuplicateFunctionName { /// Module that contains the colliding functions. module: String, @@ -57,7 +71,12 @@ pub enum ValidationError { }, /// Two parameters of one function share a name. #[error("duplicate param name in function '{function}' of module '{module}': {param}")] - #[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "parameter names must be unique within a function; rename the duplicate" + )) + )] DuplicateParamName { /// Module that contains the function. module: String, @@ -68,19 +87,28 @@ pub enum ValidationError { }, /// A name matches a reserved keyword in one of the target languages. #[error("reserved keyword used: {0}")] - #[diagnostic(help("choose a different name that is not a language reserved word"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("choose a different name that is not a language reserved word")) + )] ReservedKeyword(String), /// An identifier is malformed; the second field explains why. #[error("invalid identifier '{0}': {1}")] - #[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))] + #[cfg_attr(feature = "idl", diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters")))] InvalidIdentifier(String, &'static str), /// An error domain in the named module is missing its `name` field. #[error("error domain missing name in module '{0}'")] - #[diagnostic(help("add a non-empty 'name' field to the error domain"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("add a non-empty 'name' field to the error domain")) + )] ErrorDomainMissingName(String), /// Two error codes in the same module share a name. #[error("duplicate error code name in module '{module}': {name}")] - #[diagnostic(help("error code names must be unique within a module; rename the duplicate"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("error code names must be unique within a module; rename the duplicate")) + )] DuplicateErrorName { /// Module that contains the error domain. module: String, @@ -89,9 +117,12 @@ pub enum ValidationError { }, /// Two error codes in the same module share a numeric value. #[error("duplicate error numeric code in module '{module}': {code}")] - #[diagnostic(help( - "numeric error codes must be unique within a module; assign a different value" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "numeric error codes must be unique within a module; assign a different value" + )) + )] DuplicateErrorCode { /// Module that contains the error domain. module: String, @@ -101,10 +132,13 @@ pub enum ValidationError { /// An error code uses a reserved value: `0` means success and the /// negative range belongs to the runtime's trap codes. #[error("invalid error code in module '{module}' for '{name}': must be a positive integer")] - #[diagnostic(help( - "0 means success and negative codes are reserved for the runtime (-1 generic, \ - -2 panic, -3 marshalling failure, -4 foreign callback error); use a positive integer" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "0 means success and negative codes are reserved for the runtime (-1 generic, \ + -2 panic, -3 marshalling failure, -4 foreign callback error, -5 cancelled); use a positive integer" + )) + )] InvalidErrorCode { /// Module that contains the error domain. module: String, @@ -113,39 +147,65 @@ pub enum ValidationError { }, /// A function name collides with an error domain name in the same module. #[error("function name collides with error domain name in module '{module}': {name}")] - #[diagnostic(help( - "function and error domain names share a namespace; rename one to avoid the collision" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "function and error domain names share a namespace; rename one to avoid the collision" + )) + )] NameCollisionWithErrorDomain { /// Module where the collision occurs. module: String, /// Name shared by the function and the error domain. name: String, }, - /// Two callables in one module lower to the same C symbol. + /// Two declarations lower to the same C identifier. /// - /// Free functions claim `{prefix}_{module}_{name}`; interface members - /// claim `{prefix}_{module}_{Interface}_{member}` plus an implicit - /// `_destroy`. A free function named `Store_get` and a `get` method on an - /// interface `Store` would collide. - #[error("C symbol collision in module '{module}': two declarations lower to '..._{symbol}'")] - #[diagnostic(help( - "free functions and interface members share the module's C symbol namespace \ - (interface members are prefixed with the interface name); rename one of the \ - colliding declarations" - ))] - AbiSymbolCollision { - /// Module whose symbol namespace has the collision. - module: String, - /// The colliding symbol suffix (without the `{prefix}_{module}_` part). + /// Every function, type, constant, and runtime name in the C ABI starts + /// with the library prefix and is flattened from its module path, so + /// `m.x_y`, `m.x.y`, and `m_x.y` all become `{prefix}_m_x_y`, and an + /// async `foo`'s completion type `{prefix}_m_foo_callback` collides with + /// a function named `foo_callback`. + #[error("C symbol collision: {first} and {second} both lower to '{symbol}'")] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "every C identifier is the library prefix plus the underscore-joined module path \ + plus the declaration name; rename one of the colliding declarations" + )) + )] + SymbolCollision { + /// The colliding C identifier. symbol: String, + /// The declaration that claimed the identifier first. + first: String, + /// The declaration that collided with it. + second: String, + }, + /// A synchronous callable declares `cancellable: true`. + #[error("function '{module}::{function}' is cancellable but not async")] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "only async functions take a cancel token; add `async: true` or remove \ + `cancellable: true`" + )) + )] + CancellableNotAsync { + /// Module that contains the function. + module: String, + /// The offending function. + function: String, }, /// A function declares `throws: true` but no error domain is in scope. #[error("function '{module}::{function}' declares throws but no error domain is in scope")] - #[diagnostic(help( - "a throwing function reports codes from its module's error domain; declare an \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "a throwing function reports codes from its module's error domain; declare an \ `errors:` block on this module (or an ancestor module), or remove `throws: true`" - ))] + )) + )] ThrowsWithoutErrorDomain { /// Module that contains the function. module: String, @@ -160,10 +220,13 @@ pub enum ValidationError { /// resolve by bare name, so two types called `Config` would collide in /// generated code and make references ambiguous. #[error("duplicate type name '{name}' (declared in '{first}' and '{second}')")] - #[diagnostic(help( - "struct, enum, interface, callback interface, and error domain names must be unique \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "struct, enum, interface, callback interface, and error domain names must be unique \ across the whole API; rename one of the declarations" - ))] + )) + )] DuplicateTypeName { /// The colliding bare type name. name: String, @@ -179,10 +242,13 @@ pub enum ValidationError { /// constant per code, so `NotFound` in two domains would collide in /// generated code. #[error("duplicate error code name '{name}' (declared in '{first}' and '{second}')")] - #[diagnostic(help( - "error code names must be unique across the whole API; qualify one of them \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "error code names must be unique across the whole API; qualify one of them \ (e.g. 'OrderNotFound')" - ))] + )) + )] DuplicateErrorCodeName { /// The colliding code name. name: String, @@ -193,7 +259,10 @@ pub enum ValidationError { }, /// Two structs in the same module share a name. #[error("duplicate struct name in module '{module}': {name}")] - #[diagnostic(help("struct names must be unique within a module; rename the duplicate"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("struct names must be unique within a module; rename the duplicate")) + )] DuplicateStructName { /// Module that contains the structs. module: String, @@ -202,7 +271,10 @@ pub enum ValidationError { }, /// Two fields of one struct share a name. #[error("duplicate field name in struct '{struct_name}': {field}")] - #[diagnostic(help("field names must be unique within a struct; rename the duplicate"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("field names must be unique within a struct; rename the duplicate")) + )] DuplicateStructField { /// Struct that contains the colliding fields. struct_name: String, @@ -211,7 +283,12 @@ pub enum ValidationError { }, /// A struct declares no fields. #[error("empty struct in module '{module}': {name}")] - #[diagnostic(help("structs must have at least one field; add a field or remove the struct"))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "structs must have at least one field; add a field or remove the struct" + )) + )] EmptyStruct { /// Module that contains the struct. module: String, @@ -220,7 +297,10 @@ pub enum ValidationError { }, /// Two enums in the same module share a name. #[error("duplicate enum name in module '{module}': {name}")] - #[diagnostic(help("enum names must be unique within a module; rename the duplicate"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("enum names must be unique within a module; rename the duplicate")) + )] DuplicateEnumName { /// Module that contains the enums. module: String, @@ -229,7 +309,12 @@ pub enum ValidationError { }, /// An enum declares no variants. #[error("empty enum in module '{module}': {name}")] - #[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "enums must have at least one variant; add a variant or remove the enum" + )) + )] EmptyEnum { /// Module that contains the enum. module: String, @@ -238,7 +323,10 @@ pub enum ValidationError { }, /// Two variants of one enum share a name. #[error("duplicate enum variant in enum '{enum_name}': {variant}")] - #[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("variant names must be unique within an enum; rename the duplicate")) + )] DuplicateEnumVariant { /// Enum that contains the colliding variants. enum_name: String, @@ -247,9 +335,12 @@ pub enum ValidationError { }, /// Two associated fields of one rich enum variant share a name. #[error("duplicate field '{field}' in variant '{variant}' of enum '{enum_name}'")] - #[diagnostic(help( - "associated field names must be unique within an enum variant; rename the duplicate" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "associated field names must be unique within an enum variant; rename the duplicate" + )) + )] DuplicateEnumVariantField { /// Enum that contains the variant. enum_name: String, @@ -260,9 +351,12 @@ pub enum ValidationError { }, /// Two variants of one enum share a numeric discriminant. #[error("duplicate enum value in enum '{enum_name}': {value}")] - #[diagnostic(help( - "variant numeric values must be unique within an enum; assign a different value" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "variant numeric values must be unique within an enum; assign a different value" + )) + )] DuplicateEnumValue { /// Enum that contains the variants. enum_name: String, @@ -271,7 +365,10 @@ pub enum ValidationError { }, /// Two interfaces in the same module share a name. #[error("duplicate interface name in module '{module}': {name}")] - #[diagnostic(help("interface names must be unique within a module; rename the duplicate"))] + #[cfg_attr( + feature = "idl", + diagnostic(help("interface names must be unique within a module; rename the duplicate")) + )] DuplicateInterfaceName { /// Module that contains the interfaces. module: String, @@ -281,10 +378,13 @@ pub enum ValidationError { /// Two members (constructors, methods, or statics) of one interface share /// a name. #[error("duplicate member name in interface '{interface}': {name}")] - #[diagnostic(help( - "constructor, method, and static names share one namespace per interface; \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "constructor, method, and static names share one namespace per interface; \ rename the duplicate" - ))] + )) + )] DuplicateInterfaceMember { /// Interface that contains the colliding members. interface: String, @@ -293,10 +393,13 @@ pub enum ValidationError { }, /// An interface declares no members at all. #[error("empty interface in module '{module}': {name}")] - #[diagnostic(help( - "interfaces must declare at least one constructor, method, or static; \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "interfaces must declare at least one constructor, method, or static; \ add a member or remove the interface" - ))] + )) + )] EmptyInterface { /// Module that contains the interface. module: String, @@ -305,10 +408,13 @@ pub enum ValidationError { }, /// An interface constructor declares an explicit return type. #[error("constructor '{constructor}' of interface '{interface}' declares a return type")] - #[diagnostic(help( - "a constructor implicitly returns a new instance of its interface; remove the \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "a constructor implicitly returns a new instance of its interface; remove the \ `return` field" - ))] + )) + )] ConstructorHasReturn { /// Interface that declares the constructor. interface: String, @@ -317,10 +423,13 @@ pub enum ValidationError { }, /// An interface constructor is marked `async`. #[error("constructor '{constructor}' of interface '{interface}' cannot be async")] - #[diagnostic(help( - "constructors are synchronous; expose an async static factory returning the \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "constructors are synchronous; expose an async static factory returning the \ interface instead" - ))] + )) + )] AsyncConstructor { /// Interface that declares the constructor. interface: String, @@ -329,10 +438,13 @@ pub enum ValidationError { }, /// An interface reference appears as a map key. #[error("interface type '{name}' is not valid in {location}")] - #[diagnostic(help( - "interface objects may appear anywhere except as map keys; key a map by a scalar, \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "interface objects may appear anywhere except as map keys; key a map by a scalar, \ string, or C-style enum instead" - ))] + )) + )] InterfaceInInvalidPosition { /// The referenced interface name. name: String, @@ -342,33 +454,39 @@ pub enum ValidationError { /// A type reference names a struct, enum, interface, or callback interface /// that doesn't exist. #[error("unknown type reference: {name}")] - #[diagnostic(help( - "define a struct, enum, interface, or callback interface with this name, or check \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "define a struct, enum, interface, or callback interface with this name, or check \ for typos" - ))] + )) + )] UnknownTypeRef { /// Unresolved type name. name: String, }, /// A map uses a key type the C ABI can't represent. - #[error("invalid map key type: {key_type}; only scalars, strings, and C-style enums are allowed as map keys")] - #[diagnostic(help("map keys must be integers (i8 through u64), f32, f64, bool, string, or a C-style enum; structs, rich enums, interfaces, optionals, lists, and maps cannot be keys"))] + #[error("invalid map key type: {key_type}; only integers, bools, strings, and C-style enums are allowed as map keys")] + #[cfg_attr(feature = "idl", diagnostic(help("map keys must be integers (i8 through u64), bool, string, or a C-style enum; structs, rich enums, interfaces, optionals, lists, and maps cannot be keys")))] InvalidMapKey { /// Rejected key type, rendered as it appears in the IDL. key_type: String, }, /// An iterator type appears somewhere other than a function return. #[error("iterator type is only valid as a function return type, found in {location}")] - #[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))] + #[cfg_attr(feature = "idl", diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields")))] IteratorInInvalidPosition { /// Position where the iterator type appeared. location: String, }, /// Two callback interfaces in the same module share a name. #[error("duplicate callback interface name in module '{module}': {name}")] - #[diagnostic(help( - "callback interface names must be unique within a module; rename the duplicate" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "callback interface names must be unique within a module; rename the duplicate" + )) + )] DuplicateCallbackInterfaceName { /// Module that contains the callback interfaces. module: String, @@ -377,10 +495,13 @@ pub enum ValidationError { }, /// A callback interface declares no methods. #[error("empty callback interface in module '{module}': {name}")] - #[diagnostic(help( - "callback interfaces must declare at least one method; add a method or remove the \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "callback interfaces must declare at least one method; add a method or remove the \ callback interface" - ))] + )) + )] EmptyCallbackInterface { /// Module that contains the callback interface. module: String, @@ -389,9 +510,12 @@ pub enum ValidationError { }, /// Two methods of one callback interface share a name. #[error("duplicate method name in callback interface '{interface}': {name}")] - #[diagnostic(help( - "method names must be unique within a callback interface; rename the duplicate" - ))] + #[cfg_attr( + feature = "idl", + diagnostic(help( + "method names must be unique within a callback interface; rename the duplicate" + )) + )] DuplicateCallbackMethod { /// Callback interface that contains the colliding methods. interface: String, @@ -401,11 +525,14 @@ pub enum ValidationError { /// A callback-interface method uses a flag or return type the callback /// ABI can't carry. The `reason` names the offending feature. #[error("method '{method}' of callback interface '{interface}' {reason}")] - #[diagnostic(help( - "callback-interface methods are synchronous, never throw, and return either nothing \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "callback-interface methods are synchronous, never throw, and return either nothing \ or a direct value (a scalar, bool, or C-style enum): the consumer implements them, \ so no producer-owned allocation or error payload can flow back" - ))] + )) + )] InvalidCallbackMethod { /// Callback interface that declares the method. interface: String, @@ -417,11 +544,14 @@ pub enum ValidationError { /// A callback-interface type appears in a position other than a callable /// parameter. #[error("callback interface '{name}' is not valid in {location}")] - #[diagnostic(help( - "a callback interface is a consumer-implemented vtable: it may be passed as a \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "a callback interface is a consumer-implemented vtable: it may be passed as a \ parameter of a function, constructor, static, or method, but it can't be returned, \ nested inside another type, or passed to another callback-interface method" - ))] + )) + )] CallbackInterfaceInInvalidPosition { /// The referenced callback interface name. name: String, @@ -430,10 +560,13 @@ pub enum ValidationError { }, /// An async function tries to return an iterator, which has no async ABI. #[error("async function '{module}::{function}' cannot return an iterator")] - #[diagnostic(help( - "the callback-completed async ABI has no streaming protocol; return a list ([T]) \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "the callback-completed async ABI has no streaming protocol; return a list ([T]) \ from the async function, or make the function synchronous and return iter" - ))] + )) + )] AsyncIteratorReturn { /// Module that contains the function. module: String, @@ -442,10 +575,13 @@ pub enum ValidationError { }, /// The document declares a schema version this build doesn't support. #[error("unsupported schema version '{version}'; supported versions: {supported}")] - #[diagnostic(help( - "set the version field to the current schema version and update the \ + #[cfg_attr( + feature = "idl", + diagnostic(help( + "set the version field to the current schema version and update the \ document to match the current schema (see docs/src/reference/idl.md)" - ))] + )) + )] UnsupportedSchemaVersion { /// Version requested by the document. version: String, @@ -486,6 +622,7 @@ impl std::fmt::Display for ValidationDiagnostics { impl std::error::Error for ValidationDiagnostics {} +#[cfg(feature = "idl")] impl Diagnostic for ValidationDiagnostics { fn code<'a>(&'a self) -> Option> { self.first().code() @@ -567,5 +704,43 @@ fn validate_api_inner(api: &Api) -> Vec { } rules::check_global_type_names(&api.modules, &mut errors); rules::check_global_error_code_names(&api.modules, &mut errors); + if errors.is_empty() { + check_symbol_collisions(api, &mut errors); + } errors } + +/// Build the binding model (any prefix works: collisions are invariant under +/// a shared prefix) and reject any C identifier claimed twice. +fn check_symbol_collisions(api: &Api, errors: &mut Vec) { + let resolved = ResolvedApi::assume_valid(api.clone()); + let model = crate::model::BindingModel::build(&resolved); + let mut seen: std::collections::BTreeMap = std::collections::BTreeMap::new(); + let runtime = crate::model::RUNTIME_SYMBOLS.len(); + for (i, (symbol, origin)) in model.c_symbols().into_iter().enumerate() { + let family = symbol + .strip_prefix(&format!("{}_", model.prefix)) + .and_then(|rest| { + crate::model::RESERVED_SYMBOL_FAMILIES + .iter() + .find(|f| rest.starts_with(*f)) + }); + if let (Some(family), true) = (family, i >= runtime) { + errors.push(ValidationError::SymbolCollision { + symbol: symbol.clone(), + first: format!("the C value-buffer helpers ('{}_{family}*')", model.prefix), + second: origin.clone(), + }); + } + match seen.get(&symbol) { + Some(first) => errors.push(ValidationError::SymbolCollision { + symbol, + first: first.clone(), + second: origin, + }), + None => { + seen.insert(symbol, origin); + } + } + } +} diff --git a/crates/weaveffi-core/src/validate/diagnostic.rs b/crates/weaveffi-model/src/validate/diagnostic.rs similarity index 86% rename from crates/weaveffi-core/src/validate/diagnostic.rs rename to crates/weaveffi-model/src/validate/diagnostic.rs index a0559826..aff280d1 100644 --- a/crates/weaveffi-core/src/validate/diagnostic.rs +++ b/crates/weaveffi-model/src/validate/diagnostic.rs @@ -1,12 +1,17 @@ //! [`ValidationDiagnostic`]: a [`ValidationError`] plus an optional source -//! snippet and best-effort span for fancy [`miette`] rendering. +//! snippet and best-effort span for fancy miette rendering. +//! +//! The source snippet, the span, and the miette `Diagnostic` impl are only +//! available with the `idl` feature; without it the wrapper carries the +//! error alone. use super::ValidationError; +#[cfg(feature = "idl")] use miette::{Diagnostic, NamedSource, SourceSpan}; /// Diagnostic wrapper that attaches an optional source code snippet and a /// best-effort byte range to a [`ValidationError`] for fancy rendering via -/// [`miette`]. The wrapper delegates `help()` and `code()` to the inner error +/// miette. The wrapper delegates `help()` and `code()` to the inner error /// while exposing its own `source_code` and `labels` so the renderer can /// underline the offending identifier in the input. #[derive(Debug)] @@ -15,9 +20,11 @@ pub struct ValidationDiagnostic { pub error: ValidationError, /// Named source snippet (filename plus contents), when an on-disk IDL was /// supplied. `None` for in-memory APIs. + #[cfg(feature = "idl")] pub src: Option>, /// Best-effort byte range of the offending identifier within `src`, used /// to underline it. `None` when no span could be located. + #[cfg(feature = "idl")] pub span: Option, } @@ -33,6 +40,7 @@ impl std::error::Error for ValidationDiagnostic { } } +#[cfg(feature = "idl")] impl Diagnostic for ValidationDiagnostic { fn code<'a>(&'a self) -> Option> { self.error.code() @@ -76,6 +84,7 @@ impl ValidationDiagnostic { /// a [`SourceSpan`] for fancy rendering. If no span can be computed the /// label is omitted and miette still produces a nicer message + help /// section than plain `Display`. + #[cfg(feature = "idl")] pub fn new(error: ValidationError, source: Option<(&str, &str)>) -> Self { let (src, span) = match source { Some((filename, contents)) => { @@ -86,8 +95,18 @@ impl ValidationDiagnostic { }; Self { error, src, span } } + + /// Build a [`ValidationDiagnostic`] from a [`ValidationError`]. Without + /// the `idl` feature there's no miette rendering, so the optional + /// `(filename, contents)` source is accepted for signature parity and + /// ignored. + #[cfg(not(feature = "idl"))] + pub fn new(error: ValidationError, _source: Option<(&str, &str)>) -> Self { + Self { error } + } } +#[cfg(feature = "idl")] fn find_offending_span(err: &ValidationError, src: &str) -> Option { let needle: &str = match err { ValidationError::DuplicateModuleName(n) => Some(n.as_str()), @@ -122,7 +141,9 @@ fn find_offending_span(err: &ValidationError, src: &str) -> Option { ValidationError::DuplicateTypeName { name, .. } => Some(name.as_str()), ValidationError::DuplicateErrorCodeName { name, .. } => Some(name.as_str()), ValidationError::ThrowsWithoutErrorDomain { function, .. } => Some(function.as_str()), - ValidationError::AbiSymbolCollision { symbol, .. } => Some(symbol.as_str()), + ValidationError::CancellableNotAsync { function, .. } => { + Some(function.rsplit('.').next().unwrap_or(function)) + } _ => None, }?; let quoted = format!("\"{needle}\""); diff --git a/crates/weaveffi-core/src/validate/rules.rs b/crates/weaveffi-model/src/validate/rules.rs similarity index 93% rename from crates/weaveffi-core/src/validate/rules.rs rename to crates/weaveffi-model/src/validate/rules.rs index eec74d16..2fbffd74 100644 --- a/crates/weaveffi-core/src/validate/rules.rs +++ b/crates/weaveffi-model/src/validate/rules.rs @@ -7,10 +7,10 @@ //! document. use super::ValidationError; -use std::collections::{BTreeMap, BTreeSet}; -use weaveffi_ir::ir::{ +use crate::ir::{ CallbackInterfaceDef, ErrorDomain, Function, InterfaceDef, Module, Param, StructField, TypeRef, }; +use std::collections::{BTreeMap, BTreeSet}; const RESERVED: &[&str] = &[ "if", "else", "for", "while", "loop", "match", "type", "return", "async", "await", "break", @@ -47,9 +47,9 @@ fn check_identifier(name: &str, errors: &mut Vec) -> bool { /// positional rules consult this index to learn what a name is. #[derive(Default)] pub(super) struct TypeIndex { - /// Bare names of every struct, enum, interface, and callback interface - /// anywhere in the API. - pub all: BTreeSet, + /// Bare name of every struct, enum, interface, and callback interface + /// anywhere in the API, mapped to its declaring module's dot path. + pub all: BTreeMap, /// Bare names of every interface anywhere in the API. pub interfaces: BTreeSet, /// Bare names of every callback interface anywhere in the API. @@ -61,30 +61,40 @@ pub(super) struct TypeIndex { impl TypeIndex { pub(super) fn build(modules: &[Module]) -> Self { let mut index = Self::default(); - index.walk(modules); + index.walk(modules, ""); index } - fn walk(&mut self, modules: &[Module]) { + fn walk(&mut self, modules: &[Module], parent: &str) { for m in modules { + let path = if parent.is_empty() { + m.name.clone() + } else { + format!("{parent}.{}", m.name) + }; + let mut declare = |name: &str| { + self.all + .entry(name.to_string()) + .or_insert_with(|| path.clone()); + }; for s in &m.structs { - self.all.insert(s.name.clone()); + declare(&s.name); } for e in &m.enums { - self.all.insert(e.name.clone()); + declare(&e.name); if !e.is_rich() { self.plain_enums.insert(e.name.clone()); } } for i in &m.interfaces { - self.all.insert(i.name.clone()); + declare(&i.name); self.interfaces.insert(i.name.clone()); } for cb in &m.callback_interfaces { - self.all.insert(cb.name.clone()); + declare(&cb.name); self.callback_interfaces.insert(cb.name.clone()); } - self.walk(&m.modules); + self.walk(&m.modules, &path); } } @@ -98,16 +108,22 @@ impl TypeIndex { self.callback_interfaces.contains(bare(name)) } - /// Does a declaration with this (bare or dot-qualified) name exist - /// anywhere in the API? + /// Does a declaration with this name exist? A bare name may refer to a + /// declaration in any module; a dot-qualified name must spell the + /// declaring module's path exactly. fn exists(&self, name: &str) -> bool { - self.all.contains(bare(name)) + match (self.all.get(bare(name)), name.rsplit_once('.')) { + (None, _) => false, + (Some(_), None) => true, + (Some(path), Some((qualifier, _))) => path == qualifier, + } } /// 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 objects are - /// rejected. + /// its native dictionary idiom with exact equality, so only integers, + /// bools, strings, and C-style enums qualify; floats (NaN and signed zero + /// break key equality in several languages), composites, optionals, + /// bytes, and objects are rejected. fn is_map_key(&self, ty: &TypeRef) -> bool { match ty { TypeRef::I8 @@ -118,8 +134,6 @@ impl TypeIndex { | TypeRef::U32 | TypeRef::I64 | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 | TypeRef::Bool | TypeRef::StringUtf8 => true, TypeRef::Named(name) => self.plain_enums.contains(bare(name)), @@ -262,19 +276,6 @@ pub(super) fn validate_module( let has_domain = ancestor_has_domain || module.errors.is_some(); - // Every C symbol suffix a module-level callable claims: free functions, - // interface members, and the implicit clone/destroy pair. Two entries - // with the same suffix would produce two identical C symbols. - let mut symbol_suffixes: BTreeSet = BTreeSet::new(); - let mut claim_symbol = |suffix: String, errors: &mut Vec| { - if !symbol_suffixes.insert(suffix.clone()) { - errors.push(ValidationError::AbiSymbolCollision { - module: module.name.clone(), - symbol: suffix, - }); - } - }; - let mut function_names = BTreeSet::new(); for f in &module.functions { if !function_names.insert(f.name.clone()) { @@ -283,7 +284,6 @@ pub(super) fn validate_module( function: f.name.clone(), }); } - claim_symbol(f.name.clone(), errors); validate_function(&module.name, &f.name, f, has_domain, errors); } @@ -368,9 +368,7 @@ pub(super) fn validate_module( name: i.name.clone(), }); } - claim_symbol(format!("{}_clone", i.name), errors); - claim_symbol(format!("{}_destroy", i.name), errors); - validate_interface(module, i, has_domain, &mut claim_symbol, errors); + validate_interface(module, i, has_domain, errors); } let mut callback_names = BTreeSet::new(); @@ -451,12 +449,11 @@ pub(super) fn validate_module( /// Validate an interface's shape: unique member names across constructors, /// methods, and statics; constructor restrictions; per-member signature -/// rules; and C symbol claims for every member. +/// rules. C symbol collisions are checked API-wide once the model is built. fn validate_interface( module: &Module, iface: &InterfaceDef, has_domain: bool, - claim_symbol: &mut impl FnMut(String, &mut Vec), errors: &mut Vec, ) { if iface.constructors.is_empty() && iface.methods.is_empty() && iface.statics.is_empty() { @@ -473,7 +470,6 @@ fn validate_interface( name: f.name.clone(), }); } - claim_symbol(format!("{}_{}", iface.name, f.name), errors); let display = format!("{}.{}", iface.name, f.name); validate_function(&module.name, &display, f, has_domain, errors); }; @@ -590,6 +586,13 @@ fn validate_function( ) { check_identifier(&f.name, errors); + if f.cancellable && !f.r#async { + errors.push(ValidationError::CancellableNotAsync { + module: module_name.to_string(), + function: display_name.to_string(), + }); + } + if f.throws && !has_domain { errors.push(ValidationError::ThrowsWithoutErrorDomain { module: module_name.to_string(), diff --git a/crates/weaveffi-core/src/validate/tests.rs b/crates/weaveffi-model/src/validate/tests.rs similarity index 91% rename from crates/weaveffi-core/src/validate/tests.rs rename to crates/weaveffi-model/src/validate/tests.rs index 9465e972..6f5ab4b2 100644 --- a/crates/weaveffi-core/src/validate/tests.rs +++ b/crates/weaveffi-model/src/validate/tests.rs @@ -6,8 +6,8 @@ //! snippets are exercised by the CLI tests against on-disk files. use super::*; -use weaveffi_ir::ir::CURRENT_SCHEMA_VERSION; -use weaveffi_ir::parse::parse_api_str; +use crate::ir::CURRENT_SCHEMA_VERSION; +use crate::parse::parse_api_str; /// Parse a YAML body (without the `version:` line) and validate it. fn check(body: &str) -> Result { @@ -212,7 +212,71 @@ modules: "#, ), ( - "AbiSymbolCollision", + "SymbolCollision", + r#" +modules: + - name: m + functions: + - { name: foo, async: true, params: [{ name: a, type: i32 }] } + - { name: foo_callback, params: [] } +"#, + ), + ( + "SymbolCollision", + r#" +modules: + - name: m + functions: [{ name: x_y, params: [] }] + modules: + - name: x + functions: [{ name: y, params: [] }] +"#, + ), + ( + "SymbolCollision", + r#" +modules: + - name: m + functions: [{ name: x_y, params: [] }] + - name: m_x + functions: [{ name: y, params: [] }] +"#, + ), + ( + "SymbolCollision", + r#" +modules: + - name: list + functions: [{ name: names, params: [] }] +"#, + ), + ( + "CancellableNotAsync", + r#" +modules: + - name: m + functions: [{ name: f, cancellable: true, params: [] }] +"#, + ), + ( + "InvalidMapKey", + r#" +modules: + - name: m + functions: [{ name: f, params: [{ name: x, type: "{f64:i32}" }] }] +"#, + ), + ( + "UnknownTypeRef", + r#" +modules: + - name: m + structs: [{ name: P, fields: [{ name: v, type: i32 }] }] + functions: [{ name: f, params: [{ name: p, type: nowhere.P }] }] +"#, + ), + ( + "SymbolCollision", r#" modules: - name: m @@ -223,7 +287,7 @@ modules: "#, ), ( - "AbiSymbolCollision", + "SymbolCollision", r#" modules: - name: m @@ -234,7 +298,7 @@ modules: "#, ), ( - "AbiSymbolCollision", + "SymbolCollision", r#" modules: - name: m @@ -622,7 +686,7 @@ fn diagnostics_render_every_message_and_related() { #[test] fn source_spans_underline_the_offending_identifier() { - let src = "version: \"0.9.0\"\nmodules:\n - name: \"dup\"\n - name: \"dup\"\n"; + let src = "version: \"0.10.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(); @@ -650,28 +714,28 @@ modules: "#, ) .unwrap(); + use crate::ir::TypeRef; use crate::model::Ty; - use weaveffi_ir::ir::TypeRef; let f = &api.modules[1].functions[0]; assert_eq!( - api.resolve(&f.params[0].ty, "orders"), + api.resolve(&f.params[0].ty), Ty::Interface("shared.Store".into()) ); assert_eq!( - api.resolve(&f.params[1].ty, "orders"), + api.resolve(&f.params[1].ty), Ty::Enum("shared.Status".into()) ); assert_eq!( - api.resolve(&f.params[2].ty, "orders"), + api.resolve(&f.params[2].ty), Ty::CallbackInterface("shared.Watcher".into()) ); assert_eq!( - api.resolve(f.returns.as_ref().unwrap(), "orders"), + api.resolve(f.returns.as_ref().unwrap()), Ty::List(Box::new(Ty::Record("shared.Point".into()))) ); assert_eq!( - api.resolve(&TypeRef::Named("Point".into()), "shared"), - Ty::Record("Point".into()) + api.resolve(&TypeRef::Named("Point".into())), + Ty::Record("shared.Point".into()) ); } diff --git a/crates/weaveffi-core/src/validate/warnings.rs b/crates/weaveffi-model/src/validate/warnings.rs similarity index 99% rename from crates/weaveffi-core/src/validate/warnings.rs rename to crates/weaveffi-model/src/validate/warnings.rs index 3a8072d0..93a1e7e6 100644 --- a/crates/weaveffi-core/src/validate/warnings.rs +++ b/crates/weaveffi-model/src/validate/warnings.rs @@ -6,7 +6,7 @@ //! modules, async functions with no result) that the caller can surface and //! the user can choose to ignore. -use weaveffi_ir::ir::{Api, Module, TypeRef}; +use crate::ir::{Api, Module, TypeRef}; /// Nesting deeper than this (`[[[[i32]]]]`) is flagged. const MAX_NESTING: usize = 3; diff --git a/crates/weaveffi-ir/tests/regen_schema.rs b/crates/weaveffi-model/tests/regen_schema.rs similarity index 76% rename from crates/weaveffi-ir/tests/regen_schema.rs rename to crates/weaveffi-model/tests/regen_schema.rs index 560648f1..c0a525ed 100644 --- a/crates/weaveffi-ir/tests/regen_schema.rs +++ b/crates/weaveffi-model/tests/regen_schema.rs @@ -1,13 +1,14 @@ //! Dev helper: regenerate the checked-in `weaveffi.schema.json` from the IR //! types, exactly as `weaveffi schema --format json-schema` renders it. //! -//! Ignored by default; run with `cargo test -p weaveffi-ir --test +//! Ignored by default; run with `cargo test -p weaveffi-model --test //! regen_schema -- --ignored` after changing the IR. +#![cfg(feature = "idl")] #[test] #[ignore = "dev helper, rewrites weaveffi.schema.json at the repo root"] fn regen_schema_json() { - let schema = schemars::schema_for!(weaveffi_ir::ir::Api); + let schema = schemars::schema_for!(weaveffi_model::ir::Api); let json = serde_json::to_string_pretty(&schema).unwrap(); let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../weaveffi.schema.json"); std::fs::write(path, format!("{json}\n")).unwrap(); diff --git a/crates/weaveffi/Cargo.toml b/crates/weaveffi/Cargo.toml index d6955efc..c50c64cb 100644 --- a/crates/weaveffi/Cargo.toml +++ b/crates/weaveffi/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "weaveffi" -version = "0.23.0" +version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true @@ -10,12 +11,20 @@ keywords = ["ffi", "abi", "bindings", "codegen", "proc-macro"] categories = ["development-tools::ffi"] [dependencies] -weaveffi-abi = { path = "../weaveffi-abi", version = "0.23.0" } -weaveffi-macros = { path = "../weaveffi-macros", version = "0.23.0" } +weaveffi-abi = { workspace = true } +weaveffi-macros = { workspace = true } + +[features] +# Count live objects, callbacks, iterators, cancel tokens, and returned +# allocations for `{prefix}_debug_live`. Every sample enables it. +leak-check = ["weaveffi-abi/leak-check"] [dev-dependencies] -# Only for the ABI-revision lockstep test; the facade never links core at runtime. -weaveffi-core = { path = "../weaveffi-core", version = "0.23.0" } +# Only for the ABI-revision lockstep test; the facade never links the +# generators at runtime. +weaveffi-gen = { workspace = true } +# The leak-counter test needs counting on. +weaveffi-abi = { workspace = true, features = ["leak-check"] } [lints] workspace = true diff --git a/crates/weaveffi/src/lib.rs b/crates/weaveffi/src/lib.rs index a1ec3eff..63375156 100644 --- a/crates/weaveffi/src/lib.rs +++ b/crates/weaveffi/src/lib.rs @@ -3,13 +3,28 @@ //! This is the single crate a Rust producer depends on. Annotate an ordinary //! module with [`macro@module`], tag the items you want to export, and call //! [`export_runtime!`] once. The [`macro@module`] expansion emits the -//! `#[no_mangle] extern "C"` thunks that the generated language bindings call, -//! marshalling every argument and result through the audited [`abi`] runtime so -//! you never write `unsafe` glue by hand. +//! `extern "C"` thunks that the generated language bindings call, marshalling +//! every argument and result through the audited [`abi`] runtime so you never +//! write `unsafe` glue by hand. Every C symbol starts with the crate's name +//! (`calculator_math_add` below). //! //! ```ignore //! #[weaveffi::module] -//! pub mod calculator { +//! pub mod math { +//! /// The module's error domain; `Display` supplies the runtime message. +//! #[weaveffi::error] +//! #[derive(Debug)] +//! pub enum MathError { +//! /// Division by zero. +//! DivisionByZero = 1, +//! } +//! +//! impl std::fmt::Display for MathError { +//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +//! f.write_str("division by zero") +//! } +//! } +//! //! /// Add two integers. //! #[weaveffi::export] //! pub fn add(a: i32, b: i32) -> i32 { @@ -18,15 +33,12 @@ //! //! /// Divide, reporting division by zero through the ABI's error channel. //! #[weaveffi::export] -//! pub fn div(a: i32, b: i32) -> Result { -//! if b == 0 { -//! return Err("division by zero".to_string()); -//! } -//! Ok(a / b) +//! pub fn div(a: i32, b: i32) -> Result { +//! a.checked_div(b).ok_or(MathError::DivisionByZero) //! } //! } //! -//! // Expose the fixed runtime surface (memory/error/cancel helpers) once. +//! // Export the runtime symbols (memory, errors, cancel tokens) once. //! weaveffi::export_runtime!(); //! ``` //! @@ -50,8 +62,16 @@ //! * [`macro@cancellable`] - mark an `async fn` as accepting a cancel token. //! * [`set_spawner`] - install the executor async exports run on (Tokio, for //! example); the default drives each future on its own thread. -//! * [`abi`] - the C ABI runtime: the error struct, memory helpers, the -//! marshalling converters the expansion calls, and [`export_runtime!`]. +//! * [`export_runtime!`] - export the runtime symbols (memory, errors, cancel +//! tokens, ABI version) under the crate's prefix, once per library. +//! * [`abi`] - the C ABI runtime: the error struct, memory helpers, and the +//! marshalling converters the expansion calls. +//! +//! # Features +//! +//! * `leak-check` counts live objects, callbacks, iterators, cancel tokens, +//! and returned allocations, reported by `{prefix}_debug_live` so a test +//! harness can assert a consumer released everything. Off by default. #![deny(missing_docs)] @@ -62,27 +82,31 @@ /// crate; the generated thunks reference these items as `::weaveffi::abi::*`. pub use weaveffi_abi as abi; -pub use weaveffi_abi::export_runtime; - /// An owned, lazily-pulled iterator returned by a producer function whose IDL /// return type is `iter`. Construct one from any iterator with /// [`Iter::new`](weaveffi_abi::Iter::new); the [`macro@module`] expansion turns /// it into the opaque iterator handle the generated bindings consume. pub use weaveffi_abi::Iter; -/// A `Send` view of a foreign cancellation token, accepted as the final -/// parameter of a `#[weaveffi::cancellable]` `async fn`. Poll -/// [`is_cancelled`](weaveffi_abi::CancelToken::is_cancelled) at safe points and -/// return early when it reports cancellation; the [`macro@module`] expansion -/// supplies the token from the async launcher's `cancel_token` slot. +/// The producer's handle on a consumer's cancel token, accepted as the final +/// parameter of a `#[weaveffi::cancellable]` `async fn`. When the consumer +/// cancels, the runtime drops the function's future and completes the call +/// with the cancelled code; poll +/// [`is_cancelled`](weaveffi_abi::CancelToken::is_cancelled) only for +/// cooperative cleanup (work on other threads, say). pub use weaveffi_abi::CancelToken; +/// A consumer's callback-interface implementation failed. A callback trait +/// method declared to return `Result` receives the failure +/// as an `Err` instead of unwinding. +pub use weaveffi_abi::ForeignError; + /// Maps a producer error onto the ABI's `(code, message)` pair. A fallible /// `#[weaveffi::export]` function reports `Err(e)` through its trailing /// `out_err` slot using this trait: `String` and `&str` errors get the /// generic code `-1` out of the box, while a `#[weaveffi::error]` enum (or a /// manual [`ErrorReport`] impl) surfaces the named codes of an IDL error -/// domain. +/// domain, with its `Display` output as the message. pub use weaveffi_abi::ErrorReport; /// Install the process-wide executor that exported `async fn`s run on. Call it @@ -99,5 +123,6 @@ pub use weaveffi_abi::Spawner; pub use weaveffi_abi::BoxFuture; pub use weaveffi_macros::{ - callback_interface, cancellable, enumeration, error, export, interface, module, record, + callback_interface, cancellable, enumeration, error, export, export_runtime, interface, module, + record, }; diff --git a/crates/weaveffi/tests/abi_version.rs b/crates/weaveffi/tests/abi_version.rs index 34851362..e11bac5c 100644 --- a/crates/weaveffi/tests/abi_version.rs +++ b/crates/weaveffi/tests/abi_version.rs @@ -1,5 +1,5 @@ //! The ABI revision is declared twice on purpose: once in `weaveffi-abi` -//! (which producers link) and once in `weaveffi-core` (which generators +//! (which producers link) and once in `weaveffi-gen` (which generators //! read). The runtime crate must not depend on the generator stack, so //! this test is what keeps the two numbers equal. @@ -9,15 +9,10 @@ weaveffi::export_runtime!(); #[test] fn runtime_and_generator_abi_revisions_agree() { - assert_eq!(weaveffi::abi::ABI_VERSION, weaveffi_core::cabi::ABI_VERSION); + assert_eq!(weaveffi::abi::ABI_VERSION, weaveffi_gen::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")); + assert_eq!(abi_version_abi_version(), weaveffi::abi::ABI_VERSION); } diff --git a/crates/weaveffi/tests/dropped_future.rs b/crates/weaveffi/tests/dropped_future.rs new file mode 100644 index 00000000..b2edef65 --- /dev/null +++ b/crates/weaveffi/tests/dropped_future.rs @@ -0,0 +1,47 @@ +//! An executor that drops an async call's future without finishing it must +//! still get exactly one completion, with the cancelled code. This runs in +//! its own test binary because it installs a process-wide spawner. + +#![allow(unsafe_code)] + +use std::os::raw::c_void; +use std::sync::mpsc; +use std::time::Duration; + +use weaveffi::abi::{self, FfiError}; + +#[weaveffi::module] +pub mod work { + /// Would answer 42, if the executor ever ran it. + #[weaveffi::export] + pub async fn answer() -> i32 { + 42 + } +} + +extern "C" fn done(ctx: *mut c_void, err: *mut FfiError, result: i32) { + let tx = unsafe { &*ctx.cast::>() }; + let code = if err.is_null() { + 0 + } else { + let code = unsafe { (*err).code }; + unsafe { abi::error_free(err) }; + code + }; + tx.send((code, result)).unwrap(); +} + +#[test] +fn a_dropped_future_completes_with_the_cancelled_code_once() { + weaveffi::set_spawner(drop::).unwrap(); + + let (tx, rx) = mpsc::channel::<(i32, i32)>(); + let ctx: *mut c_void = Box::into_raw(Box::new(tx)).cast(); + unsafe { work::dropped_future_work_answer(done, ctx) }; + assert_eq!( + rx.recv_timeout(Duration::from_secs(5)).unwrap(), + (abi::CANCELLED_ERROR_CODE, 0) + ); + assert!(rx.recv_timeout(Duration::from_millis(100)).is_err()); + drop(unsafe { Box::from_raw(ctx.cast::>()) }); +} diff --git a/crates/weaveffi/tests/export_runtime.rs b/crates/weaveffi/tests/export_runtime.rs new file mode 100644 index 00000000..60f310bd --- /dev/null +++ b/crates/weaveffi/tests/export_runtime.rs @@ -0,0 +1,79 @@ +//! The `weaveffi::export_runtime!()` surface: every runtime symbol exists +//! under the crate's own prefix (this test crate is `export_runtime`), with +//! the C signature the generated header declares, and behaves as specified. + +#![allow(unsafe_code)] + +use std::ffi::CString; +use std::os::raw::c_char; +use std::ptr; + +use weaveffi::abi::{self, FfiCancelToken, FfiError}; + +weaveffi::export_runtime!(); + +#[test] +fn every_runtime_symbol_has_its_c_signature() { + let _: extern "C" fn() -> u32 = export_runtime_abi_version; + let _: unsafe extern "C" fn(*mut FfiError, i32, *const c_char) = export_runtime_error_set; + let _: unsafe extern "C" fn(*mut FfiError) = export_runtime_error_clear; + let _: unsafe extern "C" fn(*mut FfiError) = export_runtime_error_free; + let _: unsafe extern "C" fn(*mut u8, usize) = export_runtime_free_bytes; + let _: extern "C" fn() -> *mut FfiCancelToken = export_runtime_cancel_token_create; + let _: unsafe extern "C" fn(*mut FfiCancelToken) = export_runtime_cancel_token_cancel; + let _: unsafe extern "C" fn(*const FfiCancelToken) -> bool = + export_runtime_cancel_token_is_cancelled; + let _: unsafe extern "C" fn(*mut FfiCancelToken) = export_runtime_cancel_token_destroy; + let _: extern "C" fn(i32) -> u64 = export_runtime_debug_live; + assert_eq!(export_runtime_abi_version(), 3); +} + +#[test] +fn error_set_copies_and_clear_frees() { + let mut err = FfiError::default(); + let msg = CString::new("consumer failure").unwrap(); + unsafe { export_runtime_error_set(&mut err, abi::FOREIGN_ERROR_CODE, msg.as_ptr()) }; + drop(msg); + assert_eq!(err.code, abi::FOREIGN_ERROR_CODE); + assert_eq!(unsafe { err.message_str() }, Some("consumer failure")); + unsafe { export_runtime_error_clear(&mut err) }; + assert_eq!(err.code, 0); + assert!(err.message.is_null()); + unsafe { + export_runtime_error_set(ptr::null_mut(), 1, ptr::null()); + export_runtime_error_clear(ptr::null_mut()); + export_runtime_error_free(ptr::null_mut()); + } +} + +#[test] +fn free_bytes_releases_returned_runs() { + let (p, len) = abi::bytes_into_raw(vec![1, 2, 3]); + unsafe { + export_runtime_free_bytes(p.cast_mut(), len); + export_runtime_free_bytes(ptr::null_mut(), 0); + } +} + +#[test] +fn cancel_tokens_round_trip() { + let tok = export_runtime_cancel_token_create(); + assert!(!tok.is_null()); + unsafe { + assert!(!export_runtime_cancel_token_is_cancelled(tok)); + export_runtime_cancel_token_cancel(tok); + export_runtime_cancel_token_cancel(tok); + assert!(export_runtime_cancel_token_is_cancelled(tok)); + export_runtime_cancel_token_destroy(tok); + + export_runtime_cancel_token_cancel(ptr::null_mut()); + assert!(!export_runtime_cancel_token_is_cancelled(ptr::null())); + export_runtime_cancel_token_destroy(ptr::null_mut()); + } +} + +#[test] +fn unknown_leak_kinds_read_zero() { + assert_eq!(export_runtime_debug_live(-1), 0); + assert_eq!(export_runtime_debug_live(99), 0); +} diff --git a/crates/weaveffi/tests/leak.rs b/crates/weaveffi/tests/leak.rs new file mode 100644 index 00000000..3b469a56 --- /dev/null +++ b/crates/weaveffi/tests/leak.rs @@ -0,0 +1,177 @@ +//! The `leak-check` counters behind `{prefix}_debug_live` (this test crate is +//! `leak`): every resource handed to a consumer is counted while it's live, +//! and every count returns to zero once the consumer releases everything. +//! +//! The counters are process-wide, so this binary holds a single test. + +#![allow(unsafe_code)] + +use std::os::raw::c_void; +use std::sync::mpsc; +use std::time::Duration; + +use weaveffi::abi::{self, leak, FfiError}; + +#[weaveffi::module] +pub mod pool { + use std::sync::Arc; + + /// A pooled object. + #[weaveffi::interface] + pub struct Slot { + /// The slot number. + pub n: i32, + } + + impl Slot { + /// Create a slot. + pub fn new(n: i32) -> Self { + Self { n } + } + } + + /// A record holding an object token. + #[weaveffi::record] + pub struct Holder { + /// The held slot. + pub slot: Arc, + } + + /// Notified of each slot. + #[weaveffi::callback_interface] + pub trait Watcher: Send + Sync { + /// See a slot number. + fn seen(&self, n: i32); + } + + /// Wrap a slot in a record (an object token inside a value buffer). + #[weaveffi::export] + pub fn hold(slot: Arc) -> Holder { + Holder { slot } + } + + /// Unwrap a record, returning its slot. + #[weaveffi::export] + pub fn release(holder: Holder) -> Arc { + holder.slot + } + + /// Describe a slot (a returned string allocation). + #[weaveffi::export] + pub fn describe(slot: &Slot) -> String { + format!("slot {}", slot.n) + } + + /// Call the watcher once. + #[weaveffi::export] + pub fn notify(watcher: Arc, n: i32) { + watcher.seen(n); + } + + /// Count up lazily. + #[weaveffi::export] + pub fn count(n: i32) -> weaveffi::Iter { + weaveffi::Iter::new(0..n) + } + + /// Wait for cancellation. + #[weaveffi::export] + #[weaveffi::cancellable] + pub async fn park(cancel: weaveffi::CancelToken) { + let _keep = cancel; + std::future::pending::<()>().await; + } +} + +weaveffi::export_runtime!(); + +fn live() -> [u64; 5] { + [ + leak_debug_live(leak::OBJECTS), + leak_debug_live(leak::CALLBACKS), + leak_debug_live(leak::ITERATORS), + leak_debug_live(leak::TOKENS), + leak_debug_live(leak::ALLOCATIONS), + ] +} + +unsafe extern "C" fn seen(ctx: *mut c_void, _n: i32, _err: *mut FfiError) { + let _ = ctx; +} + +unsafe extern "C" fn free(ctx: *mut c_void) { + drop(unsafe { Box::from_raw(ctx.cast::()) }); +} + +static VTABLE: pool::leak_pool_Watcher_vtable = pool::leak_pool_Watcher_vtable { seen, free }; + +extern "C" fn parked(ctx: *mut c_void, err: *mut FfiError) { + let tx = unsafe { &*ctx.cast::>() }; + let code = unsafe { (*err).code }; + unsafe { abi::error_free(err) }; + tx.send(code).unwrap(); +} + +#[test] +fn every_counter_returns_to_zero() { + const { assert!(leak::ENABLED) }; + assert_eq!(live(), [0; 5]); + let mut err = FfiError::default(); + + unsafe { + // Objects: a returned pointer, a clone, and a token in a buffer. + let slot = pool::leak_pool_Slot_new(1, &mut err); + let twin = pool::leak_pool_Slot_clone(slot); + assert_eq!(live()[0], 2); + let mut len = 0usize; + let held = pool::leak_pool_hold(slot, &mut len, &mut err); + assert_eq!(live()[0], 3, "the token carries a reference"); + assert_eq!(live()[4], 1, "the returned buffer is an allocation"); + let bytes = std::slice::from_raw_parts(held, len).to_vec(); + leak_free_bytes(held.cast_mut(), len); + let back = pool::leak_pool_release(bytes.as_ptr(), bytes.len(), &mut err); + assert_eq!(live()[0], 3, "the token was adopted, the return handed out"); + + // Allocations: a returned string. + let text = pool::leak_pool_describe(back, &mut len, &mut err); + assert_eq!(live()[4], 1); + leak_free_bytes(text.cast_mut(), len); + + pool::leak_pool_Slot_destroy(back); + pool::leak_pool_Slot_destroy(twin); + pool::leak_pool_Slot_destroy(slot); + + // Callbacks: held for the call, released after. + let ctx = Box::into_raw(Box::new(0u8)).cast(); + pool::leak_pool_notify(ctx, &VTABLE, 3, &mut err); + + // Iterators: live until destroyed. + let it = pool::leak_pool_count(2, &mut err); + assert_eq!(live()[2], 1); + pool::leak_pool_CountIterator_destroy(it); + + // Tokens: the consumer's reference plus the in-flight call's. + let (tx, rx) = mpsc::channel::(); + let ctx: *mut c_void = Box::into_raw(Box::new(tx)).cast(); + let token = leak_cancel_token_create(); + pool::leak_pool_park(token, parked, ctx); + assert!(live()[3] >= 1); + leak_cancel_token_cancel(token); + leak_cancel_token_destroy(token); + assert_eq!( + rx.recv_timeout(Duration::from_secs(5)).unwrap(), + abi::CANCELLED_ERROR_CODE + ); + drop(Box::from_raw(ctx.cast::>())); + } + + // The completion fires before the task's last locals drop, so give the + // executor thread a moment to finish unwinding its frame. + for _ in 0..100 { + if live() == [0; 5] { + break; + } + std::thread::sleep(Duration::from_millis(10)); + } + assert_eq!(live(), [0; 5]); +} diff --git a/crates/weaveffi/tests/runtime.rs b/crates/weaveffi/tests/runtime.rs index f4beaf4e..974774a9 100644 --- a/crates/weaveffi/tests/runtime.rs +++ b/crates/weaveffi/tests/runtime.rs @@ -1,17 +1,22 @@ //! End-to-end runtime tests for the `#[weaveffi::module]` expansion. //! //! Each test defines a module with the macro, then calls the generated -//! `#[no_mangle] extern "C"` thunks directly (by their Rust path) and checks -//! that arguments lift, results lower, and errors flow through `out_err` the -//! way the C ABI promises. This is the executable proof that the generated glue -//! matches the calling convention every language binding expects. +//! `extern "C"` thunks directly (by their Rust path) and checks that +//! arguments lift, results lower, and errors flow through `out_err` the way +//! the C ABI promises. This is the executable proof that the generated glue +//! matches the calling convention every language binding expects. This test +//! crate is named `runtime`, so that's the prefix of every symbol. #![allow(unsafe_code)] -#![allow(clippy::not_unsafe_ptr_arg_deref)] -use std::os::raw::c_char; +use std::os::raw::c_void; +use std::sync::mpsc; use std::sync::Arc; -use weaveffi::abi::{self, c_ptr_to_string, free_string, string_to_c_ptr, weaveffi_error}; +use std::time::Duration; + +use weaveffi::abi::{self, FfiError}; + +const WAIT: Duration = Duration::from_secs(5); #[weaveffi::module] pub mod demo { @@ -23,6 +28,14 @@ pub mod demo { DivisionByZero = 100, } + impl std::fmt::Display for DemoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DivisionByZero => f.write_str("cannot divide by zero"), + } + } + } + /// A C-style enum that crosses the ABI as its `i32` discriminant. #[weaveffi::enumeration] #[repr(i32)] @@ -71,12 +84,25 @@ pub mod demo { format!("hi {name}") } - /// Borrow a string slice and report its length. + /// Borrow a string slice and report its length in characters. #[weaveffi::export] pub fn str_len(text: &str) -> i32 { text.chars().count() as i32 } + /// The address of a borrowed string's first byte, proving the thunk lent + /// the caller's bytes instead of copying them. + #[weaveffi::export] + pub fn str_addr(text: &str) -> u64 { + text.as_ptr() as u64 + } + + /// The address of a borrowed byte slice's first byte. + #[weaveffi::export] + pub fn bytes_addr(data: &[u8]) -> u64 { + data.as_ptr() as u64 + } + /// Return an optional string depending on the flag. #[weaveffi::export] pub fn maybe_name(present: bool) -> Option { @@ -117,6 +143,12 @@ pub mod demo { pub fn point_x(p: Point) -> i32 { p.x } + + /// Echo a color (C-style enum in and out). + #[weaveffi::export] + pub fn echo_color(c: Color) -> Color { + c + } } #[weaveffi::module] @@ -140,21 +172,20 @@ pub mod warehouse { #[weaveffi::module] pub mod dispatch { - // A struct declared in a *sibling* top-level module. The macro expands each - // module in isolation, so this exercises cross-module type resolution: the - // thunk must accept/return the producer's real `Crate` type as an opaque - // pointer without the per-module expansion rejecting it as unknown. + // A record declared in a *sibling* top-level module. The macro expands + // each tree in isolation, lowers the unresolved name as a value buffer, + // and asserts at compile time that `Crate` really is a by-value type. use super::warehouse::Crate; - /// Read a sibling-module record's id (struct parameter by value). + /// Read a sibling-module record's id (record parameter by value). #[weaveffi::export] pub fn crate_id(item: Crate) -> i64 { item.id } - /// Return a relabeled copy (sibling-module struct in and out). + /// Return a relabeled copy (sibling-module record in and out). #[weaveffi::export] - pub fn relabel(item: Crate, label: String) -> Crate { + pub fn relabel(item: Crate, label: String) -> super::warehouse::Crate { Crate { id: item.id, label } } } @@ -170,7 +201,7 @@ pub mod deferred { #[weaveffi::export] pub fn sync_then_fail(fail: bool) -> i32 { if fail { - weaveffi::abi::defer_foreign_error(weaveffi::abi::ForeignError { + weaveffi::abi::defer_foreign_error(weaveffi::ForeignError { code: weaveffi::abi::FOREIGN_ERROR_CODE, message: "consumer said no".to_string(), }); @@ -182,7 +213,7 @@ pub mod deferred { #[weaveffi::export] pub async fn later_then_fail(fail: bool) -> i32 { if fail { - weaveffi::abi::defer_foreign_error(weaveffi::abi::ForeignError { + weaveffi::abi::defer_foreign_error(weaveffi::ForeignError { code: weaveffi::abi::FOREIGN_ERROR_CODE, message: "consumer said no, later".to_string(), }); @@ -308,6 +339,9 @@ pub mod bus { fn classify(&self, weight: i32) -> Priority; /// Inspect a shared object without retaining it. fn on_ticker(&self, ticker: Arc, alt: Option>) -> bool; + /// Weigh a message; a consumer failure comes back as an `Err` + /// instead of unwinding. + fn weigh(&self, weight: i32) -> Result; } /// A shared object handed to subscribers. @@ -354,7 +388,7 @@ pub mod bus { /// A subscriber failure unwinds through this frame like a panic, so /// the subscriber list is snapshotted and the lock released before /// any callback runs (the same discipline a panic-safe producer uses). - pub fn publish(&self, text: String, weight: i32) -> i64 { + pub fn publish(&self, text: &str, weight: i32) -> i64 { let env = Envelope { seq: 1, topic: Some("t".to_string()), @@ -365,13 +399,13 @@ pub mod bus { .unwrap_or_else(PoisonError::into_inner) .clone(); subs.iter() - .map(|s| s.on_message(text.clone(), weight, &env)) + .map(|s| s.on_message(text.to_string(), weight, &env)) .sum() } /// Publish asynchronously. pub async fn publish_later(&self, text: String, weight: i32) -> i64 { - self.publish(text, weight) + self.publish(&text, weight) } /// Drop every subscriber (the consumer's `free` must run). @@ -396,6 +430,16 @@ pub mod bus { subscriber.on_ticker(ticker.clone(), None) && subscriber.on_ticker(ticker.clone(), Some(ticker)) } + + /// Weigh through the subscriber and describe the outcome, handling a + /// consumer failure as an ordinary value. + #[weaveffi::export] + pub fn weigh_or_explain(subscriber: Arc, weight: i32) -> String { + match subscriber.weigh(weight) { + Ok(v) => format!("ok {v}"), + Err(e) => format!("err {}: {}", e.code, e.message), + } + } } #[weaveffi::module] @@ -408,6 +452,12 @@ pub mod tasks { Overflow = 1, } + impl std::fmt::Display for TaskError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("arithmetic overflow") + } + } + /// The by-value result an async task completes with. #[weaveffi::record] #[derive(Clone)] @@ -427,15 +477,35 @@ pub mod tasks { } } + /// Echo a string asynchronously (a `(result_ptr, result_len)` result). + #[weaveffi::export] + pub async fn shout(text: &str) -> String { + text.to_uppercase() + } + /// Add two integers asynchronously, failing on overflow. #[weaveffi::export] pub async fn checked_add(a: i32, b: i32) -> Result { a.checked_add(b).ok_or(TaskError::Overflow) } + + /// Never finish on its own: only cancellation completes this call. + #[weaveffi::export] + #[weaveffi::cancellable] + pub async fn wait_forever(cancel: weaveffi::CancelToken) -> i32 { + let _keep = cancel; + std::future::pending::<()>().await; + 0 + } } -fn ok_err() -> weaveffi_error { - weaveffi_error::default() +fn ok_err() -> FfiError { + FfiError::default() +} + +/// The message in `err`, or `""`. +fn message(err: &FfiError) -> String { + unsafe { err.message_str() }.unwrap_or_default().to_string() } /// Decode a buffered return `(ptr, out_len)` into an owned value and release @@ -447,65 +517,134 @@ fn decode_ret(ptr: *const u8, len: usize) -> T { ); let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; let value = abi::decode_value(bytes).expect("well-formed value buffer"); - abi::free_bytes(ptr as *mut u8, len); + unsafe { abi::free_bytes(ptr.cast_mut(), len) }; value } +/// Copy a returned `(ptr, len)` string and release it. +fn take_string(ptr: *const u8, len: usize) -> String { + let s = unsafe { abi::lift_string(ptr, len) }.expect("valid UTF-8"); + unsafe { abi::free_bytes(ptr.cast_mut(), len) }; + s +} + +/// Copy a consumer-owned async error and release it. +fn take_async_err(err: *mut FfiError) -> (i32, String) { + if err.is_null() { + return (0, String::new()); + } + let out = unsafe { ((*err).code, message(&*err)) }; + unsafe { abi::error_free(err) }; + out +} + #[test] fn scalar_call_sets_ok() { let mut err = ok_err(); - let r = demo::weaveffi_demo_add(2, 40, &mut err); + let r = unsafe { demo::runtime_demo_add(2, 40, &mut err) }; assert_eq!(r, 42); assert_eq!(err.code, 0); assert!(err.message.is_null()); } #[test] -fn fallible_ok_and_err_paths() { +fn fallible_ok_and_err_paths_use_display_for_the_message() { let mut err = ok_err(); - assert_eq!(demo::weaveffi_demo_checked_div(10, 2, &mut err), 5); + assert_eq!( + unsafe { demo::runtime_demo_checked_div(10, 2, &mut err) }, + 5 + ); assert_eq!(err.code, 0); - let r = demo::weaveffi_demo_checked_div(1, 0, &mut err); + let r = unsafe { demo::runtime_demo_checked_div(1, 0, &mut err) }; assert_eq!(r, 0, "error path returns the zero sentinel"); assert_eq!( err.code, 100, "domain code from the #[weaveffi::error] enum" ); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "division by zero"); - abi::error_clear(&mut err); + assert_eq!(message(&err), "cannot divide by zero", "the Display output"); + + // The next successful call resets the slot. + assert_eq!(unsafe { demo::runtime_demo_add(1, 1, &mut err) }, 2); + assert_eq!(err.code, 0); + assert!(err.message.is_null()); } #[test] fn owned_string_roundtrip() { let mut err = ok_err(); - let input = string_to_c_ptr("alice"); - let out = demo::weaveffi_demo_greet(input, &mut err); + let input = "alice"; + let mut out_len = 0usize; + let out = + unsafe { demo::runtime_demo_greet(input.as_ptr(), input.len(), &mut out_len, &mut err) }; assert_eq!(err.code, 0); - assert_eq!(c_ptr_to_string(out).unwrap(), "hi alice"); - free_string(out); - free_string(input); + assert_eq!(take_string(out, out_len), "hi alice"); +} + +#[test] +fn interior_nul_in_a_returned_string_round_trips() { + let mut err = ok_err(); + let input = "a\0b"; + let mut out_len = 0usize; + let out = + unsafe { demo::runtime_demo_greet(input.as_ptr(), input.len(), &mut out_len, &mut err) }; + assert_eq!(out_len, 6); + assert_eq!(take_string(out, out_len), "hi a\0b"); } #[test] -fn borrowed_str_param() { +fn borrowed_str_param_is_not_copied() { let mut err = ok_err(); - let input = string_to_c_ptr("héllo"); - assert_eq!(demo::weaveffi_demo_str_len(input, &mut err), 5); - free_string(input); + // A slice of a larger buffer: not NUL-terminated, and borrowed in place. + let backing = "héllo world"; + let text = &backing[..6]; + assert_eq!( + unsafe { demo::runtime_demo_str_len(text.as_ptr(), text.len(), &mut err) }, + 5 + ); + let addr = unsafe { demo::runtime_demo_str_addr(text.as_ptr(), text.len(), &mut err) }; + assert_eq!( + addr, + text.as_ptr() as u64, + "the thunk lent the caller's bytes" + ); + + let data = [1u8, 2, 3]; + let addr = unsafe { demo::runtime_demo_bytes_addr(data.as_ptr(), data.len(), &mut err) }; + assert_eq!(addr, data.as_ptr() as u64); + + // An empty string may be passed as (NULL, 0). + assert_eq!( + unsafe { demo::runtime_demo_str_len(std::ptr::null(), 0, &mut err) }, + 0 + ); + assert_eq!(err.code, 0); +} + +#[test] +fn invalid_string_params_are_marshalling_errors() { + let mut err = ok_err(); + let bad = [0xFFu8, 0xFE]; + let r = unsafe { demo::runtime_demo_str_len(bad.as_ptr(), bad.len(), &mut err) }; + assert_eq!(r, 0); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + + let r = unsafe { demo::runtime_demo_str_len(std::ptr::null(), 3, &mut err) }; + assert_eq!(r, 0); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); } #[test] fn optional_string_return_is_buffered() { let mut err = ok_err(); let mut out_len: usize = 0; - let some = demo::weaveffi_demo_maybe_name(true, &mut out_len, &mut err); + let some = unsafe { demo::runtime_demo_maybe_name(true, &mut out_len, &mut err) }; assert_eq!( decode_ret::>(some, out_len), Some("present".to_string()) ); - let none = demo::weaveffi_demo_maybe_name(false, &mut out_len, &mut err); + let none = unsafe { demo::runtime_demo_maybe_name(false, &mut out_len, &mut err) }; assert_eq!(decode_ret::>(none, out_len), None); } @@ -513,7 +652,7 @@ fn optional_string_return_is_buffered() { fn scalar_list_param_is_buffered() { let mut err = ok_err(); let xs = abi::encode_value(&vec![3i32, 4, 5]); - let total = demo::weaveffi_demo_sum(xs.as_ptr(), xs.len(), &mut err); + let total = unsafe { demo::runtime_demo_sum(xs.as_ptr(), xs.len(), &mut err) }; assert_eq!(total, 12); } @@ -521,21 +660,21 @@ fn scalar_list_param_is_buffered() { fn string_list_param_is_buffered() { let mut err = ok_err(); let parts = abi::encode_value(&vec!["a".to_string(), "b".to_string(), "c".to_string()]); - let out = demo::weaveffi_demo_join(parts.as_ptr(), parts.len(), &mut err); - assert_eq!(c_ptr_to_string(out).unwrap(), "a,b,c"); - free_string(out); + let mut out_len = 0usize; + let out = + unsafe { demo::runtime_demo_join(parts.as_ptr(), parts.len(), &mut out_len, &mut err) }; + assert_eq!(take_string(out, out_len), "a,b,c"); } #[test] fn malformed_buffer_param_reports_error() { let mut err = ok_err(); - // A truncated encoding (length prefix with no elements) must be rejected - // through `out_err`, never decoded partially. + // A truncated encoding (count with no elements) must be rejected through + // `out_err`, never decoded partially. let bad = [9u8, 0, 0, 0]; - let total = demo::weaveffi_demo_sum(bad.as_ptr(), bad.len(), &mut err); + let total = unsafe { demo::runtime_demo_sum(bad.as_ptr(), bad.len(), &mut err) }; assert_eq!(total, 0, "error path returns the zero sentinel"); - assert_ne!(err.code, 0); - abi::error_clear(&mut err); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); } #[test] @@ -543,11 +682,20 @@ fn byte_buffer_param() { let mut err = ok_err(); let data = [1u8, 2, 3, 4, 5]; assert_eq!( - demo::weaveffi_demo_byte_count(data.as_ptr(), data.len(), &mut err), + unsafe { demo::runtime_demo_byte_count(data.as_ptr(), data.len(), &mut err) }, 5 ); } +#[test] +fn c_style_enum_in_and_out() { + let mut err = ok_err(); + assert_eq!(unsafe { demo::runtime_demo_echo_color(2, &mut err) }, 2); + assert_eq!(err.code, 0); + assert_eq!(unsafe { demo::runtime_demo_echo_color(9, &mut err) }, 0); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); +} + #[test] fn record_buffer_round_trip() { // A record is a value type: its whole generated surface is the @@ -560,6 +708,7 @@ fn record_buffer_round_trip() { color: demo::Color::Blue, }; let bytes = abi::encode_value(&original); + assert_eq!(bytes.len(), abi::BufferValue::encoded_len(&original)); let back: demo::Point = abi::decode_value(&bytes).expect("round-trip"); assert_eq!(back.x, 7); assert_eq!(back.label, "corner"); @@ -577,7 +726,7 @@ fn record_param_is_buffered() { color: demo::Color::Red, }; let bytes = abi::encode_value(&p); - let x = demo::weaveffi_demo_point_x(bytes.as_ptr(), bytes.len(), &mut err); + let x = unsafe { demo::runtime_demo_point_x(bytes.as_ptr(), bytes.len(), &mut err) }; assert_eq!(err.code, 0); assert_eq!(x, 41); } @@ -586,7 +735,7 @@ fn record_param_is_buffered() { fn struct_return_is_buffered() { let mut err = ok_err(); let mut out_len: usize = 0; - let ptr = demo::weaveffi_demo_make_point(99, &mut out_len, &mut err); + let ptr = unsafe { demo::runtime_demo_make_point(99, &mut out_len, &mut err) }; assert_eq!(err.code, 0); let p: demo::Point = decode_ret(ptr, out_len); assert_eq!(p.x, 99); @@ -596,39 +745,61 @@ fn struct_return_is_buffered() { } #[test] -fn cross_module_struct_param_and_return() { +fn sibling_module_record_param_and_return() { let mut err = ok_err(); - let label = string_to_c_ptr("widget"); + let label = "widget"; let mut out_len: usize = 0; - let ptr = warehouse::weaveffi_warehouse_make_crate(7, label, &mut out_len, &mut err); + let ptr = unsafe { + warehouse::runtime_warehouse_make_crate( + 7, + label.as_ptr(), + label.len(), + &mut out_len, + &mut err, + ) + }; assert_eq!(err.code, 0); - free_string(label); let c: warehouse::Crate = decode_ret(ptr, out_len); assert_eq!(c.id, 7); assert_eq!(c.label, "widget"); // `dispatch::crate_id` accepts `warehouse::Crate` as a value buffer. let bytes = abi::encode_value(&c); - let id = dispatch::weaveffi_dispatch_crate_id(bytes.as_ptr(), bytes.len(), &mut err); + let id = unsafe { dispatch::runtime_dispatch_crate_id(bytes.as_ptr(), bytes.len(), &mut err) }; assert_eq!(id, 7); assert_eq!(err.code, 0); - // `dispatch::relabel` returns a fresh `warehouse::Crate` buffer decoded - // with the same impl (same Rust type, same wire format). - let new_label = string_to_c_ptr("gadget"); - let ptr2 = dispatch::weaveffi_dispatch_relabel( - bytes.as_ptr(), - bytes.len(), - new_label, - &mut out_len, - &mut err, - ); - free_string(new_label); + let new_label = "gadget"; + let ptr2 = unsafe { + dispatch::runtime_dispatch_relabel( + bytes.as_ptr(), + bytes.len(), + new_label.as_ptr(), + new_label.len(), + &mut out_len, + &mut err, + ) + }; let c2: warehouse::Crate = decode_ret(ptr2, out_len); assert_eq!(c2.id, 7); assert_eq!(c2.label, "gadget"); } +#[test] +fn every_top_level_module_exports_a_checksum() { + let sums = [ + demo::runtime_demo_checksum(), + warehouse::runtime_warehouse_checksum(), + dispatch::runtime_dispatch_checksum(), + outer::runtime_outer_checksum(), + ]; + for (i, a) in sums.iter().enumerate() { + for b in &sums[i + 1..] { + assert_ne!(a, b, "different modules hash differently"); + } + } +} + #[test] fn map_param_and_return_are_buffered() { use std::collections::BTreeMap; @@ -639,44 +810,35 @@ fn map_param_and_return_are_buffered() { let bytes = abi::encode_value(&scores); let mut out_len: usize = 0; - let ptr = - maps::weaveffi_maps_double_scores(bytes.as_ptr(), bytes.len(), &mut out_len, &mut err); + let ptr = unsafe { + maps::runtime_maps_double_scores(bytes.as_ptr(), bytes.len(), &mut out_len, &mut err) + }; assert_eq!(err.code, 0); let doubled: BTreeMap = decode_ret(ptr, out_len); assert_eq!(doubled.get("a"), Some(&4)); assert_eq!(doubled.get("b"), Some(&2)); -} -#[test] -fn map_param_scalar_return() { - use std::collections::BTreeMap; - let mut err = ok_err(); - let mut scores = BTreeMap::new(); - scores.insert("a".to_string(), 10i32); - scores.insert("b".to_string(), 32i32); - let bytes = abi::encode_value(&scores); - let total = maps::weaveffi_maps_total(bytes.as_ptr(), bytes.len(), &mut err); - assert_eq!(err.code, 0); - assert_eq!(total, 42); + let total = unsafe { maps::runtime_maps_total(bytes.as_ptr(), bytes.len(), &mut err) }; + assert_eq!(total, 3); } #[test] fn widget_optional_field_round_trips() { - let with_note = build::Widget { - name: "bolt".to_string(), - qty: 7, - note: Some("aisle 4".to_string()), - }; - let back: build::Widget = abi::decode_value(&abi::encode_value(&with_note)).unwrap(); - assert_eq!(back, with_note); - - let without_note = build::Widget { - name: "nut".to_string(), - qty: 1, - note: None, - }; - let back: build::Widget = abi::decode_value(&abi::encode_value(&without_note)).unwrap(); - assert_eq!(back, without_note); + for w in [ + build::Widget { + name: "bolt".to_string(), + qty: 7, + note: Some("aisle 4".to_string()), + }, + build::Widget { + name: "nut".to_string(), + qty: 1, + note: None, + }, + ] { + let back: build::Widget = abi::decode_value(&abi::encode_value(&w)).unwrap(); + assert_eq!(back, w); + } } #[test] @@ -707,56 +869,94 @@ fn rich_enum_encodes_tag_then_fields() { fn rich_enum_param_is_buffered() { let mut err = ok_err(); let circle = abi::encode_value(&geom::Shape::Circle { radius: 2.5 }); - let d = geom::weaveffi_geom_describe(circle.as_ptr(), circle.len(), &mut err); + let mut out_len = 0usize; + let d = unsafe { + geom::runtime_geom_describe(circle.as_ptr(), circle.len(), &mut out_len, &mut err) + }; assert_eq!(err.code, 0); - assert_eq!(c_ptr_to_string(d).unwrap(), "circle(2.5)"); - free_string(d); + assert_eq!(take_string(d, out_len), "circle(2.5)"); } #[test] fn iterator_string_elements() { let mut err = ok_err(); - let iter = stream::weaveffi_stream_greetings(3, &mut err); + let iter = unsafe { stream::runtime_stream_greetings(3, &mut err) }; assert_eq!(err.code, 0); assert!(!iter.is_null()); let mut got = Vec::new(); loop { - let mut item: *const c_char = std::ptr::null(); - let has = stream::weaveffi_stream_GreetingsIterator_next(iter, &mut item, &mut err); + let mut item: *const u8 = std::ptr::null(); + let mut len = 0usize; + let has = unsafe { + stream::runtime_stream_GreetingsIterator_next(iter, &mut item, &mut len, &mut err) + }; assert_eq!(err.code, 0); if has == 0 { break; } - got.push(c_ptr_to_string(item).unwrap()); - free_string(item); + got.push(take_string(item, len)); } - stream::weaveffi_stream_GreetingsIterator_destroy(iter); + unsafe { stream::runtime_stream_GreetingsIterator_destroy(iter) }; assert_eq!(got, vec!["hi 0", "hi 1", "hi 2"]); } #[test] fn iterator_scalar_elements() { let mut err = ok_err(); - let iter = stream::weaveffi_stream_squares(4, &mut err); + let iter = unsafe { stream::runtime_stream_squares(4, &mut err) }; let mut got = Vec::new(); loop { let mut item: i32 = 0; - if stream::weaveffi_stream_SquaresIterator_next(iter, &mut item, &mut err) == 0 { + if unsafe { stream::runtime_stream_SquaresIterator_next(iter, &mut item, &mut err) } == 0 { break; } got.push(item); } - stream::weaveffi_stream_SquaresIterator_destroy(iter); + unsafe { stream::runtime_stream_SquaresIterator_destroy(iter) }; assert_eq!(got, vec![0, 1, 4, 9]); } +#[test] +fn concurrent_iterator_next_yields_each_element_once() { + const N: i32 = 2000; + let mut err = ok_err(); + let iter = unsafe { stream::runtime_stream_squares(N, &mut err) }; + let addr = iter as usize; + let threads: Vec<_> = (0..8) + .map(|_| { + std::thread::spawn(move || { + let iter = addr as *mut abi::IterHandle; + let mut err = FfiError::default(); + let mut got = Vec::new(); + loop { + let mut item = 0i32; + let has = unsafe { + stream::runtime_stream_SquaresIterator_next(iter, &mut item, &mut err) + }; + assert_eq!(err.code, 0); + if has == 0 { + break got; + } + got.push(item); + } + }) + }) + .collect(); + let mut all: Vec = threads + .into_iter() + .flat_map(|t| t.join().unwrap()) + .collect(); + all.sort_unstable(); + assert_eq!(all, (0..N).map(|i| i * i).collect::>()); + unsafe { stream::runtime_stream_SquaresIterator_destroy(iter) }; +} + /// A consumer-side `Subscriber` implementation: the context is a heap-allocated /// `SubState`, the vtable is a process-wide static, exactly as a generated /// binding would do it. mod consumer_subscriber { use super::*; - use std::os::raw::c_void; use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; pub struct SubState { @@ -768,73 +968,74 @@ mod consumer_subscriber { unsafe extern "C" fn on_message( ctx: *mut c_void, - text: *const c_char, + text_ptr: *const u8, + text_len: usize, weight: i32, envelope_ptr: *const u8, envelope_len: usize, - out_err: *mut weaveffi_error, + out_err: *mut FfiError, ) -> i64 { - let state = &*(ctx as *const SubState); - let text = c_ptr_to_string(text).unwrap(); + let state = unsafe { &*(ctx as *const SubState) }; + let text = unsafe { abi::lift_str(text_ptr, text_len) }.unwrap(); let env: bus::Envelope = - abi::decode_value(std::slice::from_raw_parts(envelope_ptr, envelope_len)).unwrap(); + abi::decode_value(unsafe { std::slice::from_raw_parts(envelope_ptr, envelope_len) }) + .unwrap(); *state.last_topic.lock().unwrap() = env.topic.clone(); if weight == state.fail_at { - abi::error_set( - out_err, - abi::FOREIGN_ERROR_CODE, - &format!("subscriber rejected {text}"), - ); + let msg = std::ffi::CString::new(format!("subscriber rejected {text}")).unwrap(); + // Consumers report through the exported `{prefix}_error_set`. + unsafe { super::runtime_error_set(out_err, abi::FOREIGN_ERROR_CODE, msg.as_ptr()) }; return 0; } - abi::error_set_ok(out_err); state.total.fetch_add(weight as i64, Ordering::Relaxed) + weight as i64 } - unsafe extern "C" fn classify( - _ctx: *mut c_void, - weight: i32, - out_err: *mut weaveffi_error, - ) -> i32 { - abi::error_set_ok(out_err); - if weight > 5 { - 1 - } else { - 0 - } + unsafe extern "C" fn classify(_ctx: *mut c_void, weight: i32, _err: *mut FfiError) -> i32 { + i32::from(weight > 5) } unsafe extern "C" fn on_ticker( _ctx: *mut c_void, ticker: *mut bus::Ticker, alt: *mut bus::Ticker, - out_err: *mut weaveffi_error, + _err: *mut FfiError, ) -> bool { - abi::error_set_ok(out_err); // Object arguments transfer one strong reference: the consumer adopts // each non-null pointer and owes exactly one `_destroy`. - let mut err = weaveffi_error::default(); - let v = bus::weaveffi_bus_Ticker_value(ticker, &mut err); - bus::weaveffi_bus_Ticker_destroy(ticker); - let alt_ok = if alt.is_null() { - true - } else { - let same = bus::weaveffi_bus_Ticker_value(alt, &mut err) == v; - bus::weaveffi_bus_Ticker_destroy(alt); - same - }; - v == 42 && alt_ok + let mut err = FfiError::default(); + unsafe { + let v = bus::runtime_bus_Ticker_value(ticker, &mut err); + bus::runtime_bus_Ticker_destroy(ticker); + let alt_ok = if alt.is_null() { + true + } else { + let same = bus::runtime_bus_Ticker_value(alt, &mut err) == v; + bus::runtime_bus_Ticker_destroy(alt); + same + }; + v == 42 && alt_ok + } + } + + unsafe extern "C" fn weigh(ctx: *mut c_void, weight: i32, out_err: *mut FfiError) -> i64 { + let state = unsafe { &*(ctx as *const SubState) }; + if weight == state.fail_at { + unsafe { abi::error_set(out_err, abi::FOREIGN_ERROR_CODE, "too heavy") }; + return 0; + } + i64::from(weight) * 10 } unsafe extern "C" fn free(ctx: *mut c_void) { - let state = Box::from_raw(ctx as *mut SubState); + let state = unsafe { Box::from_raw(ctx as *mut SubState) }; state.freed.fetch_add(1, Ordering::SeqCst); } - pub static VTABLE: bus::weaveffi_bus_Subscriber_vtable = bus::weaveffi_bus_Subscriber_vtable { + pub static VTABLE: bus::runtime_bus_Subscriber_vtable = bus::runtime_bus_Subscriber_vtable { on_message, classify, on_ticker, + weigh, free, }; @@ -848,267 +1049,331 @@ mod consumer_subscriber { } } +weaveffi::export_runtime!(); + #[test] fn callback_interface_sync_paths() { use consumer_subscriber::{new_ctx, VTABLE}; - use std::sync::atomic::AtomicUsize; + use std::sync::atomic::{AtomicUsize, Ordering}; let freed = Arc::new(AtomicUsize::new(0)); - use std::sync::atomic::Ordering; - let mut err = ok_err(); // Direct-family return through a non-retained callback: `free` runs as soon // as the thunk drops its `Arc`. let ctx = new_ctx(-1, &freed); assert_eq!( - bus::weaveffi_bus_classify_once(ctx, &VTABLE, 9, &mut err), + unsafe { bus::runtime_bus_classify_once(ctx, &VTABLE, 9, &mut err) }, 1 ); assert_eq!(err.code, 0); assert_eq!(freed.load(Ordering::SeqCst), 1); - // Objects flow producer -> consumer as borrowed pointers the consumer may - // clone; a `&Arc` spelling lends the lifted callback. + // Objects flow producer -> consumer as owned references; a + // `&Arc` spelling lends the lifted callback. let ctx = new_ctx(-1, &freed); - assert!(bus::weaveffi_bus_tick(ctx, &VTABLE, 42, &mut err)); + assert!(unsafe { bus::runtime_bus_tick(ctx, &VTABLE, 42, &mut err) }); assert_eq!(err.code, 0); assert_eq!(freed.load(Ordering::SeqCst), 2); // A null vtable is a marshalling error, not a crash. - let r = bus::weaveffi_bus_classify_once(std::ptr::null_mut(), std::ptr::null(), 1, &mut err); + let r = unsafe { + bus::runtime_bus_classify_once(std::ptr::null_mut(), std::ptr::null(), 1, &mut err) + }; assert_eq!(r, 0); assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); - abi::error_clear(&mut err); } #[test] -fn callback_interface_retained_and_foreign_error() { +fn callback_result_methods_return_failures_as_values() { use consumer_subscriber::{new_ctx, VTABLE}; - use std::sync::atomic::AtomicUsize; + use std::sync::atomic::{AtomicUsize, Ordering}; let freed = Arc::new(AtomicUsize::new(0)); - use std::sync::atomic::Ordering; + let mut err = ok_err(); + let mut len = 0usize; + + let ptr = unsafe { + bus::runtime_bus_weigh_or_explain(new_ctx(3, &freed), &VTABLE, 2, &mut len, &mut err) + }; + assert_eq!(err.code, 0); + assert_eq!(take_string(ptr, len), "ok 20"); + + // The consumer fails, the producer sees an `Err`, and the call itself + // succeeds: nothing unwound. + let ptr = unsafe { + bus::runtime_bus_weigh_or_explain(new_ctx(3, &freed), &VTABLE, 3, &mut len, &mut err) + }; + assert_eq!(err.code, 0); + assert_eq!(take_string(ptr, len), "err -4: too heavy"); + assert_eq!(freed.load(Ordering::SeqCst), 2); +} +#[test] +fn callback_interface_retained_and_foreign_error() { + use consumer_subscriber::{new_ctx, VTABLE}; + use std::sync::atomic::{AtomicUsize, Ordering}; + let freed = Arc::new(AtomicUsize::new(0)); let mut err = ok_err(); - let b = bus::weaveffi_bus_Bus_new(&mut err); + let b = unsafe { bus::runtime_bus_Bus_new(&mut err) }; assert!(!b.is_null()); - bus::weaveffi_bus_Bus_subscribe(b, new_ctx(7, &freed), &VTABLE, &mut err); - bus::weaveffi_bus_Bus_subscribe(b, new_ctx(-1, &freed), &VTABLE, &mut err); + unsafe { + bus::runtime_bus_Bus_subscribe(b, new_ctx(7, &freed), &VTABLE, &mut err); + bus::runtime_bus_Bus_subscribe(b, new_ctx(-1, &freed), &VTABLE, &mut err); + } assert_eq!(err.code, 0); assert_eq!(freed.load(Ordering::SeqCst), 0, "retained by the bus"); - let text = string_to_c_ptr("hi"); - assert_eq!(bus::weaveffi_bus_Bus_publish(b, text, 3, &mut err), 6); + let text = "hi"; + let publish = |weight: i32, err: &mut FfiError| unsafe { + bus::runtime_bus_Bus_publish(b, text.as_ptr(), text.len(), weight, err) + }; + assert_eq!(publish(3, &mut err), 6); assert_eq!(err.code, 0); - assert_eq!(bus::weaveffi_bus_Bus_publish(b, text, 5, &mut err), 16); + assert_eq!(publish(5, &mut err), 16); // The first subscriber fails on weight 7: the producer call is aborted and // the consumer's own message comes back with FOREIGN_ERROR_CODE. - let r = bus::weaveffi_bus_Bus_publish(b, text, 7, &mut err); - assert_eq!(r, 0); + assert_eq!(publish(7, &mut err), 0); assert_eq!(err.code, abi::FOREIGN_ERROR_CODE); - assert_eq!( - c_ptr_to_string(err.message).unwrap(), - "subscriber rejected hi" - ); - abi::error_clear(&mut err); + assert_eq!(message(&err), "subscriber rejected hi"); // The bus is still usable afterwards. - assert_eq!(bus::weaveffi_bus_Bus_publish(b, text, 1, &mut err), 18); + assert_eq!(publish(1, &mut err), 18); assert_eq!(err.code, 0); - bus::weaveffi_bus_Bus_clear(b, &mut err); + unsafe { bus::runtime_bus_Bus_clear(b, &mut err) }; assert_eq!(freed.load(Ordering::SeqCst), 2, "free runs once each"); - free_string(text); - bus::weaveffi_bus_Bus_destroy(b); + unsafe { bus::runtime_bus_Bus_destroy(b) }; +} + +type Completion = mpsc::Sender<(i32, String, T)>; + +/// Box a sender as an async `context`; reclaim it with [`drop_ctx`]. +fn new_ctx(tx: Completion) -> *mut c_void { + Box::into_raw(Box::new(tx)).cast() +} + +fn drop_ctx(ctx: *mut c_void) { + drop(unsafe { Box::from_raw(ctx.cast::>()) }); +} + +fn send(ctx: *mut c_void, err: *mut FfiError, value: T) { + let tx = unsafe { &*ctx.cast::>() }; + let (code, msg) = take_async_err(err); + tx.send((code, msg, value)).unwrap(); +} + +extern "C" fn on_i64(ctx: *mut c_void, err: *mut FfiError, result: i64) { + send(ctx, err, result); +} + +extern "C" fn on_i32(ctx: *mut c_void, err: *mut FfiError, result: i32) { + send(ctx, err, result); +} + +extern "C" fn on_string(ctx: *mut c_void, err: *mut FfiError, ptr: *const u8, len: usize) { + let value = if ptr.is_null() { + String::new() + } else { + take_string(ptr, len) + }; + send(ctx, err, value); } #[test] fn callback_interface_from_async_method() { - use consumer_subscriber::{new_ctx, VTABLE}; - use std::os::raw::c_void; - use std::sync::mpsc; - use std::time::Duration; - - type Msg = (i32, String, i64); - extern "C" fn cb(ctx: *mut c_void, err: *mut weaveffi_error, result: i64) { - let tx = unsafe { &*(ctx as *const mpsc::Sender) }; - let (code, msg) = if err.is_null() { - (0, String::new()) - } else { - let e = unsafe { &*err }; - let out = (e.code, c_ptr_to_string(e.message).unwrap_or_default()); - abi::error_free(err); - out - }; - tx.send((code, msg, result)).unwrap(); - } + use consumer_subscriber::{new_ctx as sub_ctx, VTABLE}; let mut err = ok_err(); - let b = bus::weaveffi_bus_Bus_new(&mut err); + let b = unsafe { bus::runtime_bus_Bus_new(&mut err) }; let freed = Arc::new(std::sync::atomic::AtomicUsize::new(0)); - bus::weaveffi_bus_Bus_subscribe(b, new_ctx(4, &freed), &VTABLE, &mut err); + unsafe { bus::runtime_bus_Bus_subscribe(b, sub_ctx(4, &freed), &VTABLE, &mut err) }; - let (tx, rx) = mpsc::channel::(); - let tx_ptr = Box::into_raw(Box::new(tx)); - let text = string_to_c_ptr("async"); + let (tx, rx) = mpsc::channel(); + let ctx = new_ctx::(tx); + let text = "async"; - bus::weaveffi_bus_Bus_publish_later_async(b, text, 2, cb, tx_ptr as *mut c_void); - let (code, _, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - assert_eq!(code, 0); - assert_eq!(result, 2); + unsafe { bus::runtime_bus_Bus_publish_later(b, text.as_ptr(), text.len(), 2, on_i64, ctx) }; + assert_eq!(rx.recv_timeout(WAIT).unwrap(), (0, String::new(), 2)); // A foreign failure inside the future is delivered through the callback. - bus::weaveffi_bus_Bus_publish_later_async(b, text, 4, cb, tx_ptr as *mut c_void); - let (code, msg, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); + unsafe { bus::runtime_bus_Bus_publish_later(b, text.as_ptr(), text.len(), 4, on_i64, ctx) }; + let (code, msg, result) = rx.recv_timeout(WAIT).unwrap(); assert_eq!(code, abi::FOREIGN_ERROR_CODE); assert_eq!(msg, "subscriber rejected async"); assert_eq!(result, 0); // The receiver was retained across the spawn: releasing the consumer's // reference while a call is in flight is safe. - bus::weaveffi_bus_Bus_publish_later_async(b, text, 1, cb, tx_ptr as *mut c_void); - bus::weaveffi_bus_Bus_destroy(b); - let (code, _, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - assert_eq!(code, 0); - assert_eq!(result, 3); - - free_string(text); - unsafe { drop(Box::from_raw(tx_ptr)) }; + unsafe { + bus::runtime_bus_Bus_publish_later(b, text.as_ptr(), text.len(), 1, on_i64, ctx); + bus::runtime_bus_Bus_destroy(b); + } + assert_eq!(rx.recv_timeout(WAIT).unwrap(), (0, String::new(), 3)); + drop_ctx::(ctx); } #[test] fn async_struct_result_completes_via_callback() { - use std::os::raw::c_void; - use std::sync::mpsc; - use std::time::Duration; - - type Msg = (bool, i64, String); + type Msg = (i32, String, Option<(i64, String)>); // The buffered result is owned by the consumer: decode it, then release // the producer allocation with `free_bytes`. - extern "C" fn cb( - ctx: *mut c_void, - err: *mut weaveffi_error, - result_ptr: *const u8, - result_len: usize, - ) { - let tx = unsafe { &*(ctx as *const mpsc::Sender) }; - let had_err = !err.is_null() && unsafe { (*err).code } != 0; - if had_err { - abi::error_free(err); - } - let payload = if result_ptr.is_null() { - (had_err, 0, String::new()) - } else { - let bytes = unsafe { std::slice::from_raw_parts(result_ptr, result_len) }; - let r: tasks::TaskResult = abi::decode_value(bytes).expect("well-formed result"); - abi::free_bytes(result_ptr as *mut u8, result_len); - (had_err, r.id, r.value) - }; - tx.send(payload).unwrap(); + extern "C" fn cb(ctx: *mut c_void, err: *mut FfiError, ptr: *const u8, len: usize) { + let tx = unsafe { &*ctx.cast::>() }; + let (code, msg) = take_async_err(err); + let value = (!ptr.is_null()).then(|| { + let r: tasks::TaskResult = decode_ret(ptr, len); + (r.id, r.value) + }); + tx.send((code, msg, value)).unwrap(); } let (tx, rx) = mpsc::channel::(); - let tx_ptr = Box::into_raw(Box::new(tx)); - let name = string_to_c_ptr("alpha"); - tasks::weaveffi_tasks_run_task_async(name, cb, tx_ptr as *mut c_void); - let (had_err, id, value) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - free_string(name); - unsafe { drop(Box::from_raw(tx_ptr)) }; - - assert!(!had_err); - assert_eq!(id, 7); - assert_eq!(value, "done: alpha"); + let ctx: *mut c_void = Box::into_raw(Box::new(tx)).cast(); + let name = "alpha"; + unsafe { tasks::runtime_tasks_run_task(name.as_ptr(), name.len(), cb, ctx) }; + let (code, _, value) = rx.recv_timeout(WAIT).unwrap(); + drop(unsafe { Box::from_raw(ctx.cast::>()) }); + assert_eq!(code, 0); + assert_eq!(value, Some((7, "done: alpha".to_string()))); +} + +#[test] +fn async_string_result_is_ptr_and_len() { + let (tx, rx) = mpsc::channel(); + let ctx = new_ctx::(tx); + let text = "quiet\0please"; + unsafe { tasks::runtime_tasks_shout(text.as_ptr(), text.len(), on_string, ctx) }; + assert_eq!( + rx.recv_timeout(WAIT).unwrap(), + (0, String::new(), "QUIET\0PLEASE".into()) + ); + drop_ctx::(ctx); } #[test] fn async_result_ok_and_err_paths() { - use std::os::raw::c_void; - use std::sync::mpsc; - use std::time::Duration; - - type Msg = (bool, i32); - extern "C" fn cb(ctx: *mut c_void, err: *mut weaveffi_error, result: i32) { - let tx = unsafe { &*(ctx as *const mpsc::Sender) }; - let had_err = !err.is_null() && unsafe { (*err).code } != 0; - if had_err { - // The reported error is heap-boxed and owned by the consumer. - abi::error_free(err); - } - tx.send((had_err, result)).unwrap(); - } + let (tx, rx) = mpsc::channel(); + let ctx = new_ctx::(tx); - let (tx, rx) = mpsc::channel::(); - let tx_ptr = Box::into_raw(Box::new(tx)); + unsafe { tasks::runtime_tasks_checked_add(2, 3, on_i32, ctx) }; + assert_eq!(rx.recv_timeout(WAIT).unwrap(), (0, String::new(), 5)); - tasks::weaveffi_tasks_checked_add_async(2, 3, cb, tx_ptr as *mut c_void); - let (had_err, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - assert!(!had_err); - assert_eq!(result, 5); + unsafe { tasks::runtime_tasks_checked_add(i32::MAX, 1, on_i32, ctx) }; + let (code, msg, result) = rx.recv_timeout(WAIT).unwrap(); + assert_eq!((code, msg.as_str(), result), (1, "arithmetic overflow", 0)); - tasks::weaveffi_tasks_checked_add_async(i32::MAX, 1, cb, tx_ptr as *mut c_void); - let (had_err, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - assert!(had_err); - assert_eq!(result, 0); + drop_ctx::(ctx); +} - unsafe { drop(Box::from_raw(tx_ptr)) }; +#[test] +fn cancelling_completes_with_the_cancelled_code_exactly_once() { + let (tx, rx) = mpsc::channel(); + let ctx = new_ctx::(tx); + let token = runtime_cancel_token_create(); + unsafe { tasks::runtime_tasks_wait_forever(token, on_i32, ctx) }; + assert!( + rx.recv_timeout(Duration::from_millis(50)).is_err(), + "not complete until cancelled" + ); + unsafe { + runtime_cancel_token_cancel(token); + assert!(runtime_cancel_token_is_cancelled(token)); + runtime_cancel_token_destroy(token); + } + let (code, msg, result) = rx.recv_timeout(WAIT).unwrap(); + assert_eq!( + (code, msg.as_str(), result), + (abi::CANCELLED_ERROR_CODE, "cancelled", 0) + ); + assert!(rx.recv_timeout(Duration::from_millis(100)).is_err()); + drop_ctx::(ctx); +} + +#[test] +fn cancel_then_destroy_races_the_launch_safely() { + const RUNS: usize = 200; + let (tx, rx) = mpsc::channel(); + let ctx = new_ctx::(tx); + for i in 0..RUNS { + let token = runtime_cancel_token_create(); + if i % 2 == 0 { + // The launcher takes its own reference before returning, so the + // consumer may then cancel and destroy its reference from another + // thread while the spawned future starts running. (Destroying the + // token before handing it to a launcher would break the contract: + // the launcher would adopt a freed token.) + unsafe { tasks::runtime_tasks_wait_forever(token, on_i32, ctx) }; + let raw = token as usize; + let canceller = std::thread::spawn(move || unsafe { + runtime_cancel_token_cancel(raw as *mut abi::FfiCancelToken); + runtime_cancel_token_destroy(raw as *mut abi::FfiCancelToken); + }); + canceller.join().unwrap(); + } else { + // Cancel and destroy before the spawned future is ever polled. + unsafe { + tasks::runtime_tasks_wait_forever(token, on_i32, ctx); + runtime_cancel_token_cancel(token); + runtime_cancel_token_destroy(token); + } + } + } + for _ in 0..RUNS { + let (code, _, _) = rx.recv_timeout(WAIT).unwrap(); + assert_eq!(code, abi::CANCELLED_ERROR_CODE); + } + assert!( + rx.recv_timeout(Duration::from_millis(100)).is_err(), + "each call completed exactly once" + ); + drop_ctx::(ctx); } #[test] fn deferred_foreign_error_replaces_a_sync_result() { let mut err = ok_err(); assert_eq!( - deferred::weaveffi_deferred_sync_then_fail(false, &mut err), + unsafe { deferred::runtime_deferred_sync_then_fail(false, &mut err) }, 77 ); assert_eq!(err.code, 0); - let r = deferred::weaveffi_deferred_sync_then_fail(true, &mut err); + let r = unsafe { deferred::runtime_deferred_sync_then_fail(true, &mut err) }; assert_eq!(r, 0, "the producer's value is discarded for the sentinel"); assert_eq!(err.code, abi::FOREIGN_ERROR_CODE); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "consumer said no"); - abi::error_clear(&mut err); + assert_eq!(message(&err), "consumer said no"); - assert!( - abi::take_foreign_error().is_none(), - "the thunk drained the recorded failure" - ); assert_eq!( - deferred::weaveffi_deferred_sync_then_fail(false, &mut err), + unsafe { deferred::runtime_deferred_sync_then_fail(false, &mut err) }, 77 ); assert_eq!(err.code, 0, "a later call on the same thread is unaffected"); } #[test] -fn deferred_foreign_error_fires_the_async_callback_once() { - use std::os::raw::c_void; - use std::sync::mpsc; - use std::time::Duration; - - type Msg = (i32, String, i32); - extern "C" fn cb(ctx: *mut c_void, err: *mut weaveffi_error, result: i32) { - let tx = unsafe { &*(ctx as *const mpsc::Sender) }; - let (code, message) = if err.is_null() { - (0, String::new()) - } else { - let e = unsafe { &*err }; - let out = (e.code, c_ptr_to_string(e.message).unwrap_or_default()); - abi::error_free(err); - out - }; - tx.send((code, message, result)).unwrap(); - } +fn deferring_outside_any_call_is_dropped() { + // No thunk is running on this thread, so the failure goes to stderr + // instead of leaking into the next call. + abi::defer_foreign_error(weaveffi::ForeignError { + code: abi::FOREIGN_ERROR_CODE, + message: "stray".into(), + }); + let mut err = ok_err(); + assert_eq!(unsafe { demo::runtime_demo_add(1, 2, &mut err) }, 3); + assert_eq!(err.code, 0); +} - let (tx, rx) = mpsc::channel::(); - let tx_ptr = Box::into_raw(Box::new(tx)); +#[test] +fn deferred_foreign_error_fires_the_async_callback_once() { + let (tx, rx) = mpsc::channel(); + let ctx = new_ctx::(tx); - deferred::weaveffi_deferred_later_then_fail_async(false, cb, tx_ptr as *mut c_void); - let (code, _, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - assert_eq!((code, result), (0, 88)); + unsafe { deferred::runtime_deferred_later_then_fail(false, on_i32, ctx) }; + assert_eq!(rx.recv_timeout(WAIT).unwrap(), (0, String::new(), 88)); - deferred::weaveffi_deferred_later_then_fail_async(true, cb, tx_ptr as *mut c_void); - let (code, message, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); + unsafe { deferred::runtime_deferred_later_then_fail(true, on_i32, ctx) }; + let (code, message, result) = rx.recv_timeout(WAIT).unwrap(); assert_eq!(code, abi::FOREIGN_ERROR_CODE); assert_eq!(message, "consumer said no, later"); assert_eq!(result, 0); @@ -1116,13 +1381,12 @@ fn deferred_foreign_error_fires_the_async_callback_once() { rx.recv_timeout(Duration::from_millis(200)).is_err(), "the completion callback fires exactly once" ); - - unsafe { drop(Box::from_raw(tx_ptr)) }; + drop_ctx::(ctx); } -/// Exercises nested-module codegen: the inner module's symbols must carry the -/// joined `outer_inner` path, and a nested function may reference an interface -/// declared in its parent module via `super::` (the `kvstore` `stats` pattern). +/// Exercises nested-module codegen: the inner module's symbols carry the +/// joined `outer_inner` path, and a nested function may reference an +/// interface declared in its parent module via `super::`. #[weaveffi::module] pub mod outer { use std::sync::Arc; @@ -1141,13 +1405,24 @@ pub mod outer { } } + /// A C-style enum declared in the parent module. + #[weaveffi::enumeration] + #[repr(i32)] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Level { + /// Low. + Low = 1, + /// High. + High = 2, + } + /// Return the same session (an `Arc`-typed parameter and return). #[weaveffi::export] pub fn share(session: Arc) -> Arc { session } - /// The nested sub-module: its symbols use the `outer_inner` prefix. + /// The nested sub-module: its symbols use the `outer_inner` path. #[weaveffi::module] pub mod inner { use std::sync::Arc; @@ -1185,103 +1460,119 @@ pub mod outer { pub fn session_of(report: Report) -> Option> { report.session } + + /// Raise a parent-module C-style enum (by value in and out). + #[weaveffi::export] + pub fn raise(level: super::Level) -> super::Level { + let _ = level; + super::Level::High + } } } #[test] fn nested_module_symbols_and_parent_type_reference() { let mut err = ok_err(); - let session = outer::weaveffi_outer_Session_open(7, &mut err); + let session = unsafe { outer::runtime_outer_Session_open(7, &mut err) }; assert_eq!(err.code, 0); assert!(!session.is_null()); - // The nested function is reachable at `outer::inner::*` and its symbol - // carries the joined module path; its record return is a value buffer. let mut out_len: usize = 0; - let ptr = outer::inner::weaveffi_outer_inner_summarize( - session, - std::ptr::null(), - &mut out_len, - &mut err, - ); + let ptr = unsafe { + outer::inner::runtime_outer_inner_summarize( + session, + std::ptr::null(), + &mut out_len, + &mut err, + ) + }; assert_eq!(err.code, 0); let report: outer::inner::Report = decode_ret(ptr, out_len); assert_eq!(report.score, 70); assert!(report.session.is_none()); - let ptr = - outer::inner::weaveffi_outer_inner_summarize(session, session, &mut out_len, &mut err); + let ptr = unsafe { + outer::inner::runtime_outer_inner_summarize(session, session, &mut out_len, &mut err) + }; let report: outer::inner::Report = decode_ret(ptr, out_len); assert_eq!(report.score, 77); - outer::weaveffi_outer_Session_destroy(session); + assert_eq!( + unsafe { outer::inner::runtime_outer_inner_raise(1, &mut err) }, + 2 + ); + unsafe { outer::runtime_outer_Session_destroy(session) }; } #[test] fn object_reference_counting() { let mut err = ok_err(); - let s = outer::weaveffi_outer_Session_open(3, &mut err); + unsafe { + let s = outer::runtime_outer_Session_open(3, &mut err); - // `share` retains through `Arc` in and hands back a new strong - // reference out; the pointer identity is the same allocation. - let again = outer::weaveffi_outer_share(s, &mut err); - assert_eq!(err.code, 0); - assert_eq!(again, s, "the same object, one more reference"); - let third = outer::weaveffi_outer_Session_clone(s); - assert_eq!(third, s); - - outer::weaveffi_outer_Session_destroy(s); - outer::weaveffi_outer_Session_destroy(again); - // Still alive through `third`. - let mut out_len: usize = 0; - let ptr = outer::inner::weaveffi_outer_inner_summarize( - third, - std::ptr::null(), - &mut out_len, - &mut err, - ); - assert_eq!(err.code, 0); - let report: outer::inner::Report = decode_ret(ptr, out_len); - assert_eq!(report.score, 30); - outer::weaveffi_outer_Session_destroy(third); - outer::weaveffi_outer_Session_destroy(std::ptr::null_mut()); - assert!(outer::weaveffi_outer_Session_clone(std::ptr::null()).is_null()); + // `share` retains through `Arc` in and hands back a new + // strong reference; the pointer identity is the same allocation. + let again = outer::runtime_outer_share(s, &mut err); + assert_eq!(err.code, 0); + assert_eq!(again, s, "the same object, one more reference"); + let third = outer::runtime_outer_Session_clone(s); + assert_eq!(third, s); + + outer::runtime_outer_Session_destroy(s); + outer::runtime_outer_Session_destroy(again); + // Still alive through `third`. + let mut out_len: usize = 0; + let ptr = outer::inner::runtime_outer_inner_summarize( + third, + std::ptr::null(), + &mut out_len, + &mut err, + ); + assert_eq!(err.code, 0); + let report: outer::inner::Report = decode_ret(ptr, out_len); + assert_eq!(report.score, 30); + outer::runtime_outer_Session_destroy(third); + outer::runtime_outer_Session_destroy(std::ptr::null_mut()); + assert!(outer::runtime_outer_Session_clone(std::ptr::null()).is_null()); + } } #[test] fn objects_inside_value_buffers_carry_a_reference() { let mut err = ok_err(); - let s = outer::weaveffi_outer_Session_open(5, &mut err); - - // `attach` retains the session inside the returned record: the buffer's - // object token is one strong reference the consumer adopts on decode. - let mut out_len: usize = 0; - let ptr = outer::inner::weaveffi_outer_inner_attach(s, &mut out_len, &mut err); - assert_eq!(err.code, 0); - let bytes = unsafe { std::slice::from_raw_parts(ptr, out_len) }.to_vec(); - abi::free_bytes(ptr as *mut u8, out_len); - // The consumer owns `s` and the token in `bytes`: two references. - outer::weaveffi_outer_Session_destroy(s); - - // Sending the buffer back transfers the token's reference to the producer, - // which returns it as the optional object result. - let back = outer::inner::weaveffi_outer_inner_session_of(bytes.as_ptr(), bytes.len(), &mut err); - assert_eq!(err.code, 0); - assert_eq!(back, s, "same allocation, still alive"); - outer::weaveffi_outer_Session_destroy(back); + unsafe { + let s = outer::runtime_outer_Session_open(5, &mut err); - // A record with no object decodes to a null optional object return. - let none = abi::encode_value(&outer::inner::Report { - score: 0, - session: None, - }); - let back = outer::inner::weaveffi_outer_inner_session_of(none.as_ptr(), none.len(), &mut err); - assert!(back.is_null()); - assert_eq!(err.code, 0); + // `attach` retains the session inside the returned record: the + // buffer's object token is one strong reference the consumer adopts. + let mut out_len: usize = 0; + let ptr = outer::inner::runtime_outer_inner_attach(s, &mut out_len, &mut err); + assert_eq!(err.code, 0); + let bytes = std::slice::from_raw_parts(ptr, out_len).to_vec(); + abi::free_bytes(ptr.cast_mut(), out_len); + outer::runtime_outer_Session_destroy(s); + + // Sending the buffer back transfers the token's reference to the + // producer, which returns it as the optional object result. + let back = + outer::inner::runtime_outer_inner_session_of(bytes.as_ptr(), bytes.len(), &mut err); + assert_eq!(err.code, 0); + assert_eq!(back, s, "same allocation, still alive"); + outer::runtime_outer_Session_destroy(back); + + let none = abi::encode_value(&outer::inner::Report { + score: 0, + session: None, + }); + let back = + outer::inner::runtime_outer_inner_session_of(none.as_ptr(), none.len(), &mut err); + assert!(back.is_null()); + assert_eq!(err.code, 0); + } } -/// A producer module whose fallible function surfaces an IDL error domain's -/// named codes through [`weaveffi::ErrorReport`]. +/// A producer module whose fallible function reports through a hand-written +/// [`weaveffi::ErrorReport`] type rather than the domain enum itself. #[weaveffi::module] pub mod vault { use weaveffi::ErrorReport; @@ -1296,10 +1587,18 @@ pub mod vault { Sealed = 2002, } - /// The producer's internal failure type. It carries payloads (which the - /// declared domain cannot), so it maps itself onto the domain's codes with - /// a hand-written `ErrorReport` and dynamic messages. It deliberately does - /// not implement `Display`, which would collide with the blanket impl. + impl std::fmt::Display for VaultError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::NotFound => "entry not found", + Self::Sealed => "vault sealed", + }) + } + } + + /// The producer's internal failure type: it carries data the declared + /// domain doesn't, so it maps itself onto the domain's codes with a + /// hand-written `ErrorReport` and dynamic messages. pub enum VaultFailure { /// No entry exists for the key. NotFound, @@ -1336,31 +1635,24 @@ pub mod vault { #[test] fn fallible_with_domain_error_codes() { let mut err = ok_err(); - assert_eq!(vault::weaveffi_vault_fetch(21, &mut err), 42); + assert_eq!(unsafe { vault::runtime_vault_fetch(21, &mut err) }, 42); assert_eq!(err.code, 0); - // `Err` carries the producer-chosen code and message verbatim. - let r = vault::weaveffi_vault_fetch(0, &mut err); + let r = unsafe { vault::runtime_vault_fetch(0, &mut err) }; assert_eq!(r, 0, "error path returns the zero sentinel"); assert_eq!(err.code, 2001); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "entry not found"); - abi::error_clear(&mut err); + assert_eq!(message(&err), "entry not found"); - let r = vault::weaveffi_vault_fetch(-1, &mut err); + let r = unsafe { vault::runtime_vault_fetch(-1, &mut err) }; assert_eq!(r, 0); assert_eq!(err.code, 2002); - assert_eq!( - c_ptr_to_string(err.message).unwrap(), - "vault sealed: negative key" - ); - abi::error_clear(&mut err); + assert_eq!(message(&err), "vault sealed: negative key"); } /// A producer module that exports a `#[deprecated]` function. The generated -/// thunk must still *call* the deprecated function, so it has to carry an -/// `#[allow(deprecated)]` of its own; otherwise the workspace's `-D warnings` -/// policy would reject the expansion. This module compiling at all is the -/// proof. +/// thunk must still *call* the deprecated function, so it carries an +/// `#[allow(deprecated)]` of its own; this module compiling under the +/// workspace's `-D warnings` policy is the proof. #[weaveffi::module] pub mod legacy { /// The modern entry point. @@ -1392,6 +1684,12 @@ pub mod counters { OutOfRange = 1, } + impl std::fmt::Display for CounterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("step must be positive") + } + } + /// A monotonic counter, exported as an interface. #[weaveffi::interface] pub struct Counter { @@ -1474,7 +1772,7 @@ pub mod counters { self.value() } - /// Return a fresh counter asynchronously (async object result). + /// Return the same counter asynchronously (async object result). pub async fn snapshot_later(self: Arc) -> Arc { self } @@ -1506,232 +1804,218 @@ pub mod counters { #[test] fn interface_constructor_methods_destroy() { let mut err = ok_err(); + unsafe { + let c = counters::runtime_counters_Counter_new(10, &mut err); + assert_eq!(err.code, 0); + assert!(!c.is_null()); + + assert_eq!( + counters::runtime_counters_Counter_increment(c, &mut err), + 11 + ); + assert_eq!( + counters::runtime_counters_Counter_increment(c, &mut err), + 12 + ); + assert_eq!(counters::runtime_counters_Counter_value(c, &mut err), 12); + assert_eq!(err.code, 0); - let c = counters::weaveffi_counters_Counter_new(10, &mut err); - assert_eq!(err.code, 0); - assert!(!c.is_null()); - - assert_eq!( - counters::weaveffi_counters_Counter_increment(c, &mut err), - 11 - ); - assert_eq!( - counters::weaveffi_counters_Counter_increment(c, &mut err), - 12 - ); - assert_eq!(counters::weaveffi_counters_Counter_value(c, &mut err), 12); - assert_eq!(err.code, 0); - - let prefix = string_to_c_ptr("n="); - let label = counters::weaveffi_counters_Counter_label(c, prefix, &mut err); - assert_eq!(c_ptr_to_string(label).unwrap(), "n=12"); - free_string(label); - free_string(prefix); + let prefix = "n="; + let mut len = 0usize; + let label = counters::runtime_counters_Counter_label( + c, + prefix.as_ptr(), + prefix.len(), + &mut len, + &mut err, + ); + assert_eq!(take_string(label, len), "n=12"); - counters::weaveffi_counters_Counter_destroy(c); + counters::runtime_counters_Counter_destroy(c); + } } #[test] fn interface_fallible_constructor() { let mut err = ok_err(); - - let ok = counters::weaveffi_counters_Counter_with_step(0, 5, &mut err); - assert_eq!(err.code, 0); - assert!(!ok.is_null()); - assert_eq!( - counters::weaveffi_counters_Counter_increment(ok, &mut err), - 5 - ); - counters::weaveffi_counters_Counter_destroy(ok); - - let bad = counters::weaveffi_counters_Counter_with_step(0, 0, &mut err); - assert!(bad.is_null()); - assert_eq!(err.code, 1, "domain code from the #[weaveffi::error] enum"); - assert_eq!( - c_ptr_to_string(err.message).unwrap(), - "start value out of range" - ); - abi::error_clear(&mut err); + unsafe { + let ok = counters::runtime_counters_Counter_with_step(0, 5, &mut err); + assert_eq!(err.code, 0); + assert!(!ok.is_null()); + assert_eq!( + counters::runtime_counters_Counter_increment(ok, &mut err), + 5 + ); + counters::runtime_counters_Counter_destroy(ok); + + let bad = counters::runtime_counters_Counter_with_step(0, 0, &mut err); + assert!(bad.is_null()); + assert_eq!(err.code, 1, "domain code from the #[weaveffi::error] enum"); + assert_eq!(message(&err), "step must be positive"); + } } #[test] fn interface_returning_method_and_static() { let mut err = ok_err(); - assert_eq!( - counters::weaveffi_counters_Counter_default_start(&mut err), - 0 - ); - - let c = counters::weaveffi_counters_Counter_new(3, &mut err); - let snap = counters::weaveffi_counters_Counter_snapshot(c, &mut err); - assert!(!snap.is_null()); - counters::weaveffi_counters_Counter_increment(c, &mut err); - assert_eq!(counters::weaveffi_counters_Counter_value(c, &mut err), 4); - assert_eq!( - counters::weaveffi_counters_Counter_value(snap, &mut err), - 3, - "the snapshot is an independent object" - ); - counters::weaveffi_counters_Counter_destroy(snap); - counters::weaveffi_counters_Counter_destroy(c); + unsafe { + assert_eq!( + counters::runtime_counters_Counter_default_start(&mut err), + 0 + ); + + let c = counters::runtime_counters_Counter_new(3, &mut err); + let snap = counters::runtime_counters_Counter_snapshot(c, &mut err); + assert!(!snap.is_null()); + counters::runtime_counters_Counter_increment(c, &mut err); + assert_eq!(counters::runtime_counters_Counter_value(c, &mut err), 4); + assert_eq!( + counters::runtime_counters_Counter_value(snap, &mut err), + 3, + "the snapshot is an independent object" + ); + counters::runtime_counters_Counter_destroy(snap); + counters::runtime_counters_Counter_destroy(c); + } } #[test] fn interface_as_free_function_parameter() { let mut err = ok_err(); - let c = counters::weaveffi_counters_Counter_new(21, &mut err); - assert_eq!(counters::weaveffi_counters_read_twice(c, &mut err), 42); - counters::weaveffi_counters_Counter_destroy(c); + unsafe { + let c = counters::runtime_counters_Counter_new(21, &mut err); + assert_eq!(counters::runtime_counters_read_twice(c, &mut err), 42); + counters::runtime_counters_Counter_destroy(c); + } } #[test] fn arc_self_receiver_and_optional_objects() { let mut err = ok_err(); - let c = counters::weaveffi_counters_Counter_new(10, &mut err); - let shared = counters::weaveffi_counters_Counter_share(c, &mut err); - assert_eq!(err.code, 0); - assert_eq!(shared, c, "`self: Arc` returns the same object"); - counters::weaveffi_counters_Counter_destroy(shared); + unsafe { + let c = counters::runtime_counters_Counter_new(10, &mut err); + let shared = counters::runtime_counters_Counter_share(c, &mut err); + assert_eq!(err.code, 0); + assert_eq!(shared, c, "`self: Arc` returns the same object"); + counters::runtime_counters_Counter_destroy(shared); - let other = counters::weaveffi_counters_Counter_new(20, &mut err); - let bigger = counters::weaveffi_counters_Counter_larger(c, other, 0, &mut err); - assert_eq!(err.code, 0); - assert_eq!( - counters::weaveffi_counters_Counter_value(bigger, &mut err), - 20 - ); - counters::weaveffi_counters_Counter_destroy(bigger); + let other = counters::runtime_counters_Counter_new(20, &mut err); + let bigger = counters::runtime_counters_Counter_larger(c, other, 0, &mut err); + assert_eq!(err.code, 0); + assert_eq!( + counters::runtime_counters_Counter_value(bigger, &mut err), + 20 + ); + counters::runtime_counters_Counter_destroy(bigger); - let mine = counters::weaveffi_counters_Counter_larger(c, std::ptr::null(), 0, &mut err); - assert_eq!( - counters::weaveffi_counters_Counter_value(mine, &mut err), - 10 - ); - counters::weaveffi_counters_Counter_destroy(mine); + let mine = counters::runtime_counters_Counter_larger(c, std::ptr::null(), 0, &mut err); + assert_eq!(counters::runtime_counters_Counter_value(mine, &mut err), 10); + counters::runtime_counters_Counter_destroy(mine); - let none = counters::weaveffi_counters_Counter_larger(c, other, 100, &mut err); - assert!(none.is_null()); - assert_eq!(err.code, 0, "a null optional object return is not an error"); + let none = counters::runtime_counters_Counter_larger(c, other, 100, &mut err); + assert!(none.is_null()); + assert_eq!(err.code, 0, "a null optional object return is not an error"); - counters::weaveffi_counters_Counter_destroy(other); - counters::weaveffi_counters_Counter_destroy(c); + counters::runtime_counters_Counter_destroy(other); + counters::runtime_counters_Counter_destroy(c); + } } #[test] fn iterator_of_objects() { let mut err = ok_err(); - let c = counters::weaveffi_counters_Counter_new(5, &mut err); - let iter = counters::weaveffi_counters_Counter_fan_out(c, 3, &mut err); - assert_eq!(err.code, 0); let mut values = Vec::new(); - loop { - let mut item: *mut counters::Counter = std::ptr::null_mut(); - if counters::weaveffi_counters_Counter_FanOutIterator_next(iter, &mut item, &mut err) == 0 { - break; + unsafe { + let c = counters::runtime_counters_Counter_new(5, &mut err); + let iter = counters::runtime_counters_Counter_fan_out(c, 3, &mut err); + assert_eq!(err.code, 0); + loop { + let mut item: *mut counters::Counter = std::ptr::null_mut(); + if counters::runtime_counters_Counter_FanOutIterator_next(iter, &mut item, &mut err) + == 0 + { + break; + } + values.push(counters::runtime_counters_Counter_value(item, &mut err)); + counters::runtime_counters_Counter_destroy(item); } - values.push(counters::weaveffi_counters_Counter_value(item, &mut err)); - counters::weaveffi_counters_Counter_destroy(item); + counters::runtime_counters_Counter_FanOutIterator_destroy(iter); + counters::runtime_counters_Counter_destroy(c); } - counters::weaveffi_counters_Counter_FanOutIterator_destroy(iter); - counters::weaveffi_counters_Counter_destroy(c); assert_eq!(values, vec![5, 6, 7]); } #[test] fn async_methods_retain_the_receiver() { - use std::os::raw::c_void; - use std::sync::mpsc; - use std::time::Duration; - - extern "C" fn on_value(ctx: *mut c_void, err: *mut weaveffi_error, result: i64) { - let tx = unsafe { &*(ctx as *const mpsc::Sender) }; - assert!(err.is_null()); - tx.send(result).unwrap(); - } - extern "C" fn on_obj( - ctx: *mut c_void, - err: *mut weaveffi_error, - result: *mut counters::Counter, - ) { - let tx = unsafe { &*(ctx as *const mpsc::Sender) }; - assert!(err.is_null()); - let mut e = weaveffi_error::default(); - let v = counters::weaveffi_counters_Counter_value(result, &mut e); - counters::weaveffi_counters_Counter_destroy(result); - tx.send(v).unwrap(); + extern "C" fn on_obj(ctx: *mut c_void, err: *mut FfiError, result: *mut counters::Counter) { + let mut e = FfiError::default(); + let v = unsafe { + let v = counters::runtime_counters_Counter_value(result, &mut e); + counters::runtime_counters_Counter_destroy(result); + v + }; + send(ctx, err, v); } let mut err = ok_err(); - let (tx, rx) = mpsc::channel::(); - let tx_ptr = Box::into_raw(Box::new(tx)); - - let c = counters::weaveffi_counters_Counter_new(8, &mut err); - counters::weaveffi_counters_Counter_value_later_async(c, on_value, tx_ptr as *mut c_void); - counters::weaveffi_counters_Counter_snapshot_later_async(c, on_obj, tx_ptr as *mut c_void); - // Releasing the consumer's reference while calls are in flight is safe: - // each launcher retained its own. - counters::weaveffi_counters_Counter_destroy(c); + let (tx, rx) = mpsc::channel(); + let ctx = new_ctx::(tx); + + unsafe { + let c = counters::runtime_counters_Counter_new(8, &mut err); + counters::runtime_counters_Counter_value_later(c, on_i64, ctx); + counters::runtime_counters_Counter_snapshot_later(c, on_obj, ctx); + // Releasing the consumer's reference while calls are in flight is + // safe: each launcher retained its own. + counters::runtime_counters_Counter_destroy(c); + } let mut got = vec![ - rx.recv_timeout(Duration::from_secs(5)).unwrap(), - rx.recv_timeout(Duration::from_secs(5)).unwrap(), + rx.recv_timeout(WAIT).unwrap().2, + rx.recv_timeout(WAIT).unwrap().2, ]; got.sort_unstable(); assert_eq!(got, vec![8, 8]); // A null receiver still completes (with a marshalling error). - extern "C" fn on_null(ctx: *mut c_void, err: *mut weaveffi_error, result: i64) { - let tx = unsafe { &*(ctx as *const mpsc::Sender) }; - assert!(!err.is_null()); - assert_eq!(unsafe { (*err).code }, abi::MARSHAL_ERROR_CODE); - abi::error_free(err); - tx.send(result).unwrap(); - } - counters::weaveffi_counters_Counter_value_later_async( - std::ptr::null(), - on_null, - tx_ptr as *mut c_void, - ); - assert_eq!(rx.recv_timeout(Duration::from_secs(5)).unwrap(), 0); - unsafe { drop(Box::from_raw(tx_ptr)) }; + unsafe { counters::runtime_counters_Counter_value_later(std::ptr::null(), on_i64, ctx) }; + let (code, _, result) = rx.recv_timeout(WAIT).unwrap(); + assert_eq!((code, result), (abi::MARSHAL_ERROR_CODE, 0)); + drop_ctx::(ctx); } #[test] fn interface_null_self_reports_error() { let mut err = ok_err(); - let r = counters::weaveffi_counters_Counter_value(std::ptr::null(), &mut err); + let r = unsafe { counters::runtime_counters_Counter_value(std::ptr::null(), &mut err) }; assert_eq!(r, 0); - assert_ne!(err.code, 0); - abi::error_clear(&mut err); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); } #[test] fn producer_panic_reports_panic_code() { let mut err = ok_err(); - let c = counters::weaveffi_counters_Counter_new(0, &mut err); + unsafe { + let c = counters::runtime_counters_Counter_new(0, &mut err); - counters::weaveffi_counters_Counter_explode(c, &mut err); - assert_eq!(err.code, abi::PANIC_ERROR_CODE); - assert!(c_ptr_to_string(err.message) - .unwrap() - .contains("counter exploded")); - abi::error_clear(&mut err); + counters::runtime_counters_Counter_explode(c, &mut err); + assert_eq!(err.code, abi::PANIC_ERROR_CODE); + assert!(message(&err).contains("counter exploded")); - // The object is still usable and the error slot resets on the next call. - assert_eq!(counters::weaveffi_counters_Counter_value(c, &mut err), 0); - assert_eq!(err.code, 0); - counters::weaveffi_counters_Counter_destroy(c); + // The object is still usable and the error slot resets on the next call. + assert_eq!(counters::runtime_counters_Counter_value(c, &mut err), 0); + assert_eq!(err.code, 0); + counters::runtime_counters_Counter_destroy(c); + } } #[test] fn deprecated_export_thunk_compiles_and_runs() { let mut err = ok_err(); - assert_eq!(legacy::weaveffi_legacy_add_one(41, &mut err), 42); + assert_eq!(unsafe { legacy::runtime_legacy_add_one(41, &mut err) }, 42); assert_eq!(err.code, 0); - // Calling the deprecated thunk would warn at this site, but the generated - // thunk's own `#[allow(deprecated)]` keeps the macro expansion clean. #[allow(deprecated)] - let bumped = legacy::weaveffi_legacy_bump(41, &mut err); + let bumped = unsafe { legacy::runtime_legacy_bump(41, &mut err) }; assert_eq!(bumped, 42); assert_eq!(err.code, 0); } @@ -1757,6 +2041,15 @@ pub mod quota { Unavailable = 3002, } + impl std::fmt::Display for QuotaError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Exceeded { limit, used } => write!(f, "used {used} of {limit}"), + Self::Unavailable => f.write_str("quota service unavailable"), + } + } + } + /// Consume `amount` units against a limit of 100. #[weaveffi::export] pub fn consume(amount: i64) -> Result { @@ -1776,28 +2069,27 @@ fn error_payload_fields_cross_the_abi() { let mut err = ok_err(); // Success leaves the payload slots empty. - assert_eq!(quota::weaveffi_quota_consume(30, &mut err), 70); + assert_eq!(unsafe { quota::runtime_quota_consume(30, &mut err) }, 70); assert_eq!(err.code, 0); assert!(err.payload_ptr.is_null()); // A payload-carrying variant serializes its fields in declaration order. - let r = quota::weaveffi_quota_consume(250, &mut err); + let r = unsafe { quota::runtime_quota_consume(250, &mut err) }; assert_eq!(r, 0, "error path returns the zero sentinel"); assert_eq!(err.code, 3001); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "quota exceeded"); + assert_eq!(message(&err), "used 250 of 100"); assert!(!err.payload_ptr.is_null()); let payload = unsafe { std::slice::from_raw_parts(err.payload_ptr, err.payload_len) }; let mut reader = abi::BufferReader::new(payload); assert_eq!(reader.read_i64().unwrap(), 100, "limit field"); assert_eq!(reader.read_i64().unwrap(), 250, "used field"); reader.expect_end().unwrap(); - abi::error_clear(&mut err); + unsafe { runtime_error_clear(&mut err) }; assert!(err.payload_ptr.is_null(), "clear releases the payload"); // A unit variant reports code and message with no payload. - let r = quota::weaveffi_quota_consume(-1, &mut err); + let r = unsafe { quota::runtime_quota_consume(-1, &mut err) }; assert_eq!(r, 0); assert_eq!(err.code, 3002); assert!(err.payload_ptr.is_null()); - abi::error_clear(&mut err); } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index e56db413..52aaf55b 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,46 +1,51 @@ # Summary -- [Introduction](intro.md) +[Introduction](intro.md) + - [Getting Started](getting-started.md) + +# Guides + +- [The Rust Producer Macro](guides/producer-macro.md) +- [Errors and Memory](guides/errors-and-memory.md) +- [Async and Cancellation](guides/async.md) +- [Project Configuration](guides/config.md) +- [Packaging](guides/packaging.md) +- [Extracting an IDL from Rust](guides/extract.md) + +# Reference + +- [IDL Schema](reference/idl.md) +- [C ABI Contract](reference/abi.md) +- [Value Buffers](reference/value-buffers.md) +- [Naming](reference/naming.md) + +# Generators + +- [Capability Matrix](generators/README.md) +- [C](generators/c.md) +- [C++](generators/cpp.md) +- [Swift](generators/swift.md) +- [Kotlin](generators/kotlin.md) +- [Node.js](generators/node.md) +- [WebAssembly](generators/wasm.md) +- [Python](generators/python.md) +- [.NET](generators/dotnet.md) +- [Dart](generators/dart.md) +- [Go](generators/go.md) +- [Ruby](generators/ruby.md) + +# Project + - [Architecture](architecture.md) +- [Samples](samples.md) - [Comparison](comparison.md) - [FAQ](faq.md) - [Stability and Versioning](stability.md) -- [Performance](performance.md) - [Roadmap](roadmap.md) -- [Samples](samples.md) -- [Reference](reference/README.md) - - [IDL Schema](reference/idl.md) - - [C ABI Contract](reference/abi.md) - - [Value Buffer Protocol](reference/value-buffers.md) - - [Memory & Error Model](reference/memory-error.md) - - [Naming and Package Conventions](reference/naming.md) -- [Generators](generators/README.md) - - [Kotlin](generators/kotlin.md) - - [C](generators/c.md) - - [Node](generators/node.md) - - [Swift](generators/swift.md) - - [Wasm](generators/wasm.md) - - [Python](generators/python.md) - - [.NET](generators/dotnet.md) - - [C++](generators/cpp.md) - - [Dart](generators/dart.md) - - [Go](generators/go.md) - - [Ruby](generators/ruby.md) -- [API](api/README.md) - - [Rust API (cargo doc)](api/rust.md) - - [Doc Comment Style](api/doc-style.md) -- [Guides](guides/README.md) - - [The Rust Producer Macro](guides/producer-macro.md) - - [Memory Ownership](guides/memory.md) - - [Error Handling](guides/errors.md) - - [Async Functions](guides/async.md) - - [Annotated Rust Extraction](guides/extract.md) - - [Project Configuration](guides/config.md) - - [Packaging and Distribution](guides/packaging.md) -- [Tutorials](tutorials/README.md) - - [Calculator](tutorials/calculator.md) - - [Swift iOS](tutorials/swift.md) - - [Kotlin](tutorials/kotlin.md) - - [Python](tutorials/python.md) - - [Node.js](tutorials/node.md) + +# API + +- [Rust Crates](api/README.md) +- [Rust API Map](api/rust.md) +- [Doc Comment Style](api/doc-style.md) diff --git a/docs/src/api/README.md b/docs/src/api/README.md index dc21bea4..8b91ae88 100644 --- a/docs/src/api/README.md +++ b/docs/src/api/README.md @@ -1,25 +1,28 @@ -# API +# Rust Crates -Reference documentation for the WeaveFFI Rust crates. +WeaveFFI is published as six crates that share one version: -- [Rust API](rust.md): a map of the public surface of the `weaveffi` facade - (the attributes, `export_runtime!`, `Iter`, `CancelToken`, `ErrorReport`, - and the spawner) and of the `weaveffi-abi` runtime it re-exports (the - error struct and reserved codes, reference-counted objects, callback - vtables, the value-buffer codec, cancel tokens, and the async spawner), - with a note on which items a producer calls and which exist for the macro - expansion. -- [Doc Comment Style](doc-style.md): the conventions and lints behind the - doc comments themselves. +| Crate | Use it when | +|-------|-------------| +| [`weaveffi`](https://docs.rs/weaveffi) | You're writing a Rust producer. It's the only dependency a producer needs. | +| [`weaveffi-abi`](https://docs.rs/weaveffi-abi) | You need the runtime directly; producers reach it as `weaveffi::abi`. | +| [`weaveffi-macros`](https://docs.rs/weaveffi-macros) | Never directly; `weaveffi` re-exports its macros. | +| [`weaveffi-model`](https://docs.rs/weaveffi-model) | You're building a tool on the IR: parsing, validating, or extracting an API, or reading the binding model. | +| [`weaveffi-gen`](https://docs.rs/weaveffi-gen) | You're driving or extending the generators from Rust. | +| [`weaveffi-cli`](https://crates.io/crates/weaveffi-cli) | You want the `weaveffi` command (`cargo install weaveffi-cli`). | -API docs are generated from source via `cargo doc`: +- [Rust API Map](rust.md) lists the items a producer uses from `weaveffi` + and `weaveffi-abi`, and which exist only for the macro expansion. +- [Doc Comment Style](doc-style.md) is the convention for the doc comments + themselves. + +The full API docs are published at +[weaveffi.com/api/rust/weaveffi/](https://weaveffi.com/api/rust/weaveffi/) +and build locally with: ```bash cargo doc --workspace --all-features --no-deps --open ``` -When the documentation site is deployed, API docs are available under the -[API section](https://weavefoundry.github.io/weaveffi/api/rust/weaveffi/). - -Every public item in the library crates is documented; this is enforced in -CI by `#![deny(missing_docs)]` and the Clippy doc lints. +Every public item in the library crates is documented, enforced by +`#![deny(missing_docs)]` and the Clippy doc lints. diff --git a/docs/src/api/doc-style.md b/docs/src/api/doc-style.md index 5c9c7091..365cf2af 100644 --- a/docs/src/api/doc-style.md +++ b/docs/src/api/doc-style.md @@ -14,7 +14,7 @@ read consistently and the doc lints stay green in CI. `# Safety` sections. These are the Rust analog of "what can go wrong," and the matching Clippy lints require them. - Link other items with intra-doc links: `` [`BindingModel`] `` or - `` [`Api`](weaveffi_ir::ir::Api) ``. + `` [`Api`](weaveffi_model::ir::Api) ``. - Wrap code-like identifiers in backticks. Product and tool names (WeaveFFI, SwiftPM, CMake) are allow-listed in `clippy.toml` instead. - Comments explain why, not what. @@ -58,8 +58,8 @@ Google-style docstring. /// # Examples /// /// ```no_run -/// use weaveffi_core::codegen::generate; -/// # use weaveffi_ir::ir::Api; +/// use my_crate::generate; +/// # use weaveffi_model::ir::Api; /// # fn demo(api: Api) -> anyhow::Result<()> { /// generate(&api, "./generated", &["c", "swift"])?; /// # Ok(()) @@ -124,7 +124,7 @@ flags undocumented `pub` fields and variants, not just the type itself. ```rust /// Error struct passed across the C ABI boundary. #[repr(C)] -pub struct weaveffi_error { +pub struct FfiError { /// Status code. `0` means success; any non-zero value indicates failure. pub code: i32, /// Owned, NUL-terminated UTF-8 message, or null when `code` is `0`. @@ -183,7 +183,7 @@ is the Rust analog of the docs site's autorefs: ```rust /// Renders from the shared [`BindingModel`], never re-deriving lowering. /// -/// See [`Api`](weaveffi_ir::ir::Api) for the input model and +/// See [`Api`](weaveffi_model::ir::Api) for the input model and /// [`LanguageBackend`](crate::backend::LanguageBackend) for the trait every /// generator implements. ``` diff --git a/docs/src/api/rust.md b/docs/src/api/rust.md index 0b25332a..e588d38f 100644 --- a/docs/src/api/rust.md +++ b/docs/src/api/rust.md @@ -1,221 +1,57 @@ -# Rust API (cargo doc) +# Rust API Map -This page is a map of the public Rust API a producer sees: the `weaveffi` -facade crate you depend on, and the `weaveffi-abi` runtime it re-exports as -`weaveffi::abi`. Every item below has a doc comment; `cargo doc` renders the -full signatures and the safety contracts, and this page tells you which items -you'll actually reach for and which exist for the macro expansion. +A map of the public API a Rust producer sees: the `weaveffi` facade and the +`weaveffi-abi` runtime it re-exports as `weaveffi::abi`. `cargo doc` has the +full signatures and safety contracts; this page says which items you'll +reach for. ```toml [dependencies] -weaveffi = "0.22" +weaveffi = "0.23" ``` -The supporting crates are published separately and are useful when you need -the lower layers directly: - -| Crate | What it is | -|-------|------------| -| [`weaveffi`][weaveffi-crate] | The producer facade: the attribute macros, `export_runtime!`, the handful of runtime types a producer names, and `abi`. Depend on this. | -| [`weaveffi-abi`][abi-crate] | The C ABI runtime: `weaveffi_error`, the reserved error codes, memory helpers, reference-counted objects, callback-interface vtables, the value-buffer codec, cancel tokens, and the async spawner. The macro generates code against it; you reach it as `weaveffi::abi`. | -| [`weaveffi-macros`][macros-crate] | The proc-macro implementation behind `weaveffi`'s attributes. You rarely depend on it directly. | -| [`weaveffi-ir`][ir-crate] | The IR types (`Api`, `Module`, `TypeRef`, ...) and the IDL parser, for tools that consume the IDL. | - ## The `weaveffi` facade -The facade is deliberately small. Its whole public surface, in the order -`cargo doc -p weaveffi --no-deps` lists it: - -### Attribute macros - -| Attribute | Applies to | What it does | -|-----------|------------|--------------| -| `#[weaveffi::module]` | `pub mod` | The driver. Reads every tagged item in the module and emits the `#[no_mangle] extern "C"` thunks, the vtable structs, and the `ErrorReport` implementations. Exactly one per module you export. | -| `#[weaveffi::export]` | `pub fn` / `pub async fn` | Exports a free function. `async fn` becomes an asynchronous export; a `Result`-returning fn becomes a `throws` function. | -| `#[weaveffi::record]` | `pub struct` | A by-value record, serialized across the ABI in the value-buffer format. | -| `#[weaveffi::enumeration]` | `pub enum` | A `#[repr(i32)]` C-style enum, or a rich enum with data-carrying variants. | -| `#[weaveffi::interface]` | `pub struct` (+ its `impl`) | An opaque, reference-counted object type. Must be `Send + Sync`. Constructors return `Self`, `Arc`, or the type name; methods take `&self` or `self: Arc`. | -| `#[weaveffi::callback_interface]` | `pub trait` | A consumer-implemented interface. Producer functions accept it as `Arc`. | -| `#[weaveffi::error]` | `pub enum` | The module's error domain: positive discriminants are the codes, doc comments are the default messages, named fields are the payload. Implements `ErrorReport`. | -| `#[weaveffi::cancellable]` | `pub async fn` | Marks an async export as accepting a `CancelToken` as its final parameter. | - -The [Producer Macro](../guides/producer-macro.md) guide covers each one with -compiling examples and the diagnostics the macro emits for what it rejects. - -### Macros - | Item | Purpose | |------|---------| -| `weaveffi::export_runtime!()` | Emits the fixed runtime symbols every producer cdylib must export: `weaveffi_abi_version`, `weaveffi_free_string`, `weaveffi_free_bytes`, `weaveffi_error_set`, `weaveffi_error_clear`, `weaveffi_error_free`, and the four `weaveffi_cancel_token_*` functions, plus `weaveffi_alloc` / `weaveffi_dealloc` on `wasm32`. Call it exactly once, at crate root, after the module. | - -### Types and functions - -| Item | Re-exported from | Purpose | -|------|------------------|---------| -| `weaveffi::Iter` | `weaveffi_abi::Iter` | The return type of a function whose IDL return is `iter`. Build one with `Iter::new(any_into_iterator)`; the macro boxes it behind an opaque handle that the consumer pulls one element at a time. Requires the iterator to be `Send + 'static`. | -| `weaveffi::CancelToken` | `weaveffi_abi::CancelToken` | The final parameter of a `#[weaveffi::cancellable]` async fn. Poll `is_cancelled()` at safe points and return early. `Send + Sync`; a null token reads as never cancelled. | -| `weaveffi::ErrorReport` | `weaveffi_abi::ErrorReport` | The trait a `Result`'s error type must implement to cross the boundary: `code()` (defaults to `-1`), `message()`, and `payload()` (defaults to empty). Implemented for `String`, `&str`, and `Box`; the `#[weaveffi::error]` expansion implements it for your domain enum. Implement it by hand for an error type the macro doesn't own (a `thiserror` enum, for example). | -| `weaveffi::set_spawner` | `weaveffi_abi::set_spawner` | Installs the process-wide executor async exports run on. Call once before the first async launch; returns `Err(SpawnerAlreadySet)` on a second call. | -| `weaveffi::Spawner` | `weaveffi_abi::Spawner` | The hook `set_spawner` takes: `fn spawn(&self, fut: BoxFuture)`. Blanket-implemented for any `Fn(BoxFuture) + Send + Sync + 'static`, so a closure forwarding to `tokio::runtime::Handle::spawn` is enough. | -| `weaveffi::BoxFuture` | `weaveffi_abi::BoxFuture` | `Pin + Send + 'static>>`, the type-erased future a spawner receives. Already wrapped so a panic inside it is caught and reported through the completion callback. | -| `weaveffi::abi` | `weaveffi_abi` | The whole runtime crate, for the rare case a producer needs a helper directly (tests that call the generated thunks, for instance). | - -A typical producer uses three of these by name (`Iter`, `CancelToken`, and -`set_spawner`) and never spells `abi::` outside its tests. The -[Async](../guides/async.md) guide has a compiling Tokio spawner example. +| `#[weaveffi::module]` | Marks an exported module and emits its thunks and checksum. | +| `#[weaveffi::export]` | Exports a function (`async fn` is async; a `Result` return throws). | +| `#[weaveffi::record]` | A by-value struct. | +| `#[weaveffi::enumeration]` | A `#[repr(i32)]` C-style enum or a rich enum. | +| `#[weaveffi::interface]` | A reference-counted object type, with its inherent `impl`. | +| `#[weaveffi::callback_interface]` | A trait the consumer implements; accepted as `Arc`. | +| `#[weaveffi::error]` | The module's error domain; requires `Display`. | +| `#[weaveffi::cancellable]` | An async function taking a `CancelToken`. | +| `weaveffi::export_runtime!()` | Exports the runtime symbols under the crate's prefix; call once. | +| `Iter` | The return type for `iter`; build with `Iter::new(iterator)`. | +| `CancelToken` | A cancellable function's last parameter; `is_cancelled()` for cooperative cleanup. | +| `ForeignError` | A consumer callback's failure (`code`, `message`), returned by `Result`-returning callback methods. | +| `ErrorReport` | Maps an error type to `(code, message, payload)`; implemented for `String`, `&str`, boxed errors, and every `#[weaveffi::error]` enum. | +| `set_spawner`, `Spawner`, `BoxFuture` | Install the executor async functions run on. | +| `abi` | The `weaveffi-abi` crate. | + +The `leak-check` feature enables the runtime's leak counters. The +[producer macro guide](../guides/producer-macro.md) covers every attribute. ## The `weaveffi-abi` runtime -The runtime implements the contract described in the -[ABI reference](../reference/abi.md). Its root module holds the C-facing -types and the helpers every thunk needs; five submodules hold the object, -callback, buffer, conversion, and spawner machinery. Everything in the -submodules is also re-exported at the root, so `weaveffi::abi::lower_object` -and `weaveffi::abi::object::lower_object` are the same function. - -Items marked **(macro)** are called by generated code; you can read them to -understand the expansion but shouldn't need to call them. Items marked -**(tests)** are what a producer's unit tests use to exercise the generated -thunks directly, as every `samples/*/src/lib.rs` does. - -### Version - -| Item | Notes | -|------|-------| -| `ABI_VERSION: u32` | `2`. Bumps only on an incompatible change to the runtime surface; independent of the crate version and the IDL schema. | -| `abi_version() -> u32` | The body of the exported `weaveffi_abi_version` thunk. Generated consumers compare it against the revision they were generated for at load time. | - -### The error struct and reserved codes - -| Item | Notes | -|------|-------| -| `weaveffi_error` | `#[repr(C)] { code: i32, message: *const c_char, payload_ptr: *const u8, payload_len: usize }`. `Default` is the OK state. Never copy one that owns a message. | -| `GENERIC_ERROR_CODE` | `-1`: an untyped producer error (`Result` and any `ErrorReport` that keeps the default `code`). | -| `PANIC_ERROR_CODE` | `-2`: the producer panicked inside a thunk's `catch_unwind`. | -| `MARSHAL_ERROR_CODE` | `-3`: an argument couldn't be lifted (null pointer, invalid UTF-8, bad discriminant, malformed buffer). | -| `FOREIGN_ERROR_CODE` | `-4`: a consumer callback-interface implementation failed. | -| `error_set_ok(out_err)` **(macro)** | Sets `code = 0` and frees any prior message and payload. | -| `error_set(out_err, code, &str)` **(macro)** | Copies the message into a fresh allocation. | -| `error_set_c(out_err, code, *const c_char)` | The body of the exported `weaveffi_error_set`, which consumer trampolines call to report a callback failure. Copies the borrowed C string. | -| `error_set_with_payload(out_err, code, &str, Vec)` **(macro)** | `error_set` plus an owned payload buffer. | -| `error_set_panic(out_err, &dyn Any)` **(macro)** | The `catch_unwind` arm: a `ForeignError` payload is reported with its own code and message, anything else as `-2` with `producer panicked: ...`. | -| `error_clear(err)` **(tests)** | The body of `weaveffi_error_clear`: frees the message and payload, zeroes the fields. | -| `error_free(err)` | The body of `weaveffi_error_free`: `error_clear` plus freeing the heap box an async completion callback delivered. | -| `panic_message(&dyn Any) -> String` | Best-effort extraction of a panic payload's text. | -| `result_to_out_err(Result, out_err) -> Option` **(macro)** | Writes `Err` through `ErrorReport` and returns `None`; `Ok` sets the slot to OK. | -| `ErrorReport` | See the facade table above. | - -### Strings and bytes - -| Item | Notes | -|------|-------| -| `string_to_c_ptr(impl AsRef) -> *const c_char` **(macro)** | Allocates an owned C string; interior NULs are stripped. Freed by `free_string`. | -| `c_ptr_to_string(*const c_char) -> Option` **(tests)** | Copies a borrowed C string; `None` for null or invalid UTF-8. | -| `unsafe c_ptr_to_str<'a>(*const c_char) -> Option<&'a str>` **(macro)** | The zero-copy lift for a `&str` parameter. | -| `free_string(*const c_char)` | The body of `weaveffi_free_string`. Null is a no-op. | -| `free_bytes(*mut u8, len)` | The body of `weaveffi_free_bytes`. Null is a no-op. | -| `convert::lift_bytes`, `convert::lift_byte_slice`, `convert::lower_bytes` **(macro)** | Lift a `(ptr, len)` pair into an owned `Vec` or a borrowed `&[u8]`; lower a `Vec` into a `(ptr, len)` pair the consumer frees with `weaveffi_free_bytes`. | -| `wasm_alloc(size)`, `wasm_dealloc(ptr, size)` | `wasm32` only: the linear-memory allocator behind `weaveffi_alloc` / `weaveffi_dealloc`, which the generated JS glue uses to stage inputs and return slots. | - -### Reference-counted objects (`abi::object`) - -An interface value crosses the ABI as an `Arc` turned into a raw pointer. -Every helper here is **(macro)**; they exist so each `unsafe` -pointer-to-`Arc` conversion has one audited home. The -[Memory](../guides/memory.md) guide explains the ownership rules they -implement. - -| Item | Notes | -|------|-------| -| `lower_object(impl Into>) -> *mut T` | Hands one strong reference to the consumer; accepts an owned `T` or an existing `Arc`. | -| `lower_object_opt(Option>>) -> *mut T` | Same, with `None` as null. | -| `unsafe object_ref<'a>(*const T) -> Option<&'a T>` | Borrows an object parameter for the call without touching the count. | -| `unsafe object_arc(*const T) -> Option>` | Takes a new strong reference so the producer can keep the object past the call. | -| `unsafe object_clone(*const T) -> *mut T` | The body of every `{tag}_clone` symbol. | -| `unsafe object_destroy(*mut T)` | The body of every `{tag}_destroy` symbol: releases one reference, swallowing a panicking `Drop` and discarding any deferred foreign error. | -| `object_to_token(&Arc) -> u64` | The `u64` an interface value takes inside a value buffer; carries one strong reference. | -| `unsafe object_from_token(u64) -> Option>` | Adopts that reference. Adopting the same token twice double-frees. | - -### Callback interfaces (`abi::callback`) - -A callback-interface parameter arrives as a `(ctx, vtable)` pair. The -expansion wraps it in a `ForeignCallback`, implements your trait on top, and -hands you an `Arc`. - -| Item | Notes | -|------|-------| -| `Vtable` (trait) **(macro)** | Implemented by every generated `#[repr(C)]` vtable struct; exposes the trailing `free(ctx)` entry. | -| `CallbackInterface` (trait) **(macro)** | Implemented for `dyn Trait` of every callback interface; ties the trait object to its vtable type and builds the `Arc` from a `ForeignCallback`. | -| `unsafe lift_callback(ctx, vtable) -> Option>` **(macro)** | Lifts the parameter slots. A null vtable is a marshalling failure. | -| `ForeignCallback` **(macro)** | Owns the consumer's `ctx` and vtable pointer; `Send + Sync` because the ABI obliges consumers to make every entry callable from any thread. Calls `free(ctx)` exactly once when the last `Arc` drops. | -| `ForeignError { code, message }` | The payload a failed consumer call travels as. Implements `Display` and `Error`. | -| `check_foreign_error(weaveffi_error)` **(macro)** | Called after every vtable call. A zero code is a no-op; otherwise it copies the message, clears the error, and calls `raise_foreign_error`. A positive code is replaced by `-4` so a consumer bug can't impersonate a domain code. | -| `raise_foreign_error(ForeignError)` **(macro)** | On a `panic = "unwind"` build, `resume_unwind`s with the payload (the panic hook doesn't fire). On a `panic = "abort"` build, defers it. | -| `defer_foreign_error(ForeignError)` | Records the failure in a thread-local slot, keeping the first if one is pending. Public so tests can exercise the abort-build route. | -| `take_foreign_error() -> Option` **(macro)** | Every thunk calls this after the producer returns and reports a recorded failure instead of the result. | - -The [Errors](../guides/errors.md) guide walks through what a consumer sees at -the other end. - -### Value buffers (`abi::buffer`) - -Records, rich enums, optionals, lists, maps, and error payloads cross the ABI -serialized in the format specified in the -[value-buffer reference](../reference/value-buffers.md). The codec is public so -a producer can round-trip a value in a test, or implement `BufferValue` for a -type the macro doesn't generate. - -| Item | Notes | -|------|-------| -| `BufferValue` (trait) | `write_value(&self, &mut BufferWriter)` and `read_value(&mut BufferReader) -> Result`. Implemented for every scalar, `String`, `Arc` (as an object token), `Option`, `Vec` (so `Vec` encodes as `bytes` does), `BTreeMap`, and `HashMap`; the `#[weaveffi::record]` and rich-enum expansions implement it for your types. | -| `BufferWriter` | Growable little-endian writer: `new()`, `write_bool` through `write_f64`, `write_len`, `write_string`, `write_bytes`, `write_option_flag`, `finish() -> Vec`. | -| `BufferReader<'a>` | Validating reader over a borrowed slice: the matching `read_*` methods, `remaining()`, and `expect_end()`, which rejects trailing bytes. Every read returns `Result<_, BufferDecodeError>`. | -| `BufferDecodeError` | What a truncated buffer, hostile length prefix, invalid `bool` byte, or non-UTF-8 string produces; the thunk reports it as `-3`. | -| `encode_value(&T) -> Vec` | Serialize a whole value. | -| `decode_value(&[u8]) -> Result` | Deserialize a whole value, requiring the buffer to be consumed exactly. | - -### Cancel tokens - -| Item | Notes | -|------|-------| -| `weaveffi_cancel_token` | `#[repr(C)]` opaque struct holding one atomic flag. Consumers create, cancel, and destroy it through the exported `weaveffi_cancel_token_*` symbols. | -| `cancel_token_create()`, `cancel_token_cancel(ptr)`, `cancel_token_is_cancelled(ptr) -> bool`, `cancel_token_destroy(ptr)` | The bodies of those four symbols. Null is a safe no-op everywhere (`is_cancelled` reads `false`). | -| `CancelToken` | See the facade table above. `CancelToken::from_raw` is `#[doc(hidden)]`: the macro builds the token, you only read it. | - -### Iterators - -| Item | Notes | -|------|-------| -| `Iter` | See the facade table above. Implements `Iterator`; the macro's `_next` thunk calls `next()` and `_destroy` drops it. | - -### The async spawner (`abi::spawn`) - -| Item | Notes | -|------|-------| -| `Spawner`, `BoxFuture`, `set_spawner` | See the facade table above. | -| `SpawnerAlreadySet` | The error `set_spawner` returns on a second call; the first spawner wins. | -| `spawn(impl Future + Send + 'static)` **(macro)** | What every generated launcher calls: routes to the installed spawner, or to the default. | -| `block_on(F) -> F::Output` | The dependency-free executor behind the default spawner: polls on the current thread, parking between wakeups. No reactor, so futures that need Tokio's I/O driver need a Tokio spawner instead. | -| `CatchUnwind` **(macro)** | The future adapter every launcher awaits. Resolves to `Err(payload)` instead of unwinding when the inner future panics, and to `Err(ForeignError)` when a deferred foreign failure is pending, so the completion callback always fires exactly once. | - -The default spawner drives each future on a fresh thread with `block_on`; on -`wasm32`, which has no threads, the future is driven inline before the -launcher returns. The [Async](../guides/async.md) guide covers the -consequences for producers. - -## Browsing the docs - -Generate and view the Rust API docs locally: - -```bash -cargo doc -p weaveffi -p weaveffi-abi --no-deps --open -``` - -Add `--workspace --all-features` to include the generators, the IR, and the -CLI crates. When the documentation site is deployed, API docs are available at -[weavefoundry.github.io/weaveffi/api/rust/weaveffi/](https://weavefoundry.github.io/weaveffi/api/rust/weaveffi/). - -[weaveffi-crate]: https://docs.rs/weaveffi -[abi-crate]: https://docs.rs/weaveffi-abi -[macros-crate]: https://docs.rs/weaveffi-macros -[ir-crate]: https://docs.rs/weaveffi-ir +Items marked **(macro)** are called by generated thunks; read them to +understand the expansion, but you shouldn't need to call them. Everything is +re-exported at the crate root. + +| Module | Items | +|--------|-------| +| root | `ABI_VERSION` (`3`). | +| `error` | `FfiError`, the `#[repr(C)]` error struct (the C `{prefix}_error`); the codes `GENERIC_ERROR_CODE` through `CANCELLED_ERROR_CODE` (`-1` to `-5`); `ErrorReport`; `panic_message`. **(macro)** `error_set`, `error_store`, `error_clear`, `error_free`, `boxed_error`. | +| `convert` | `(ptr, len)` conversions: `lift_str`, `lift_string`, `lift_byte_slice`, `lift_bytes` read borrowed parameters; `lower_string`, `lower_bytes`, `bytes_into_raw` hand out allocations; `free_bytes` releases them. Useful in tests that call thunks directly. | +| `object` | **(macro)** `lower_object`, `object_ref`, `object_arc`, `object_clone`, `object_destroy`, and the buffer token pair `object_to_token` / `object_from_token`. | +| `callback` | `ForeignError`; `raise_foreign_error`, which propagates a callback failure from producer code; `foreign_status`; `ThunkScope`. **(macro)** `ForeignCallback`, `Vtable`, `CallbackInterface`, `lift_callback`, `defer_foreign_error`, `take_foreign_error`. | +| `cancel` | `CancelToken`. **(macro)** `Cancellable`, `FfiCancelToken`, and the `cancel_token_*` bodies. | +| `iter` | `Iter`. **(macro)** `IterHandle`, `iter_into_raw`, `iter_next`, `iter_destroy`. | +| `buffer` | The value-buffer codec: `BufferValue`, `BufferWriter`, `BufferReader`, `BufferDecodeError`, `encode_value`, `decode_value`, and the `ByValue` marker that records and rich enums implement. | +| `spawn` | `set_spawner`, `Spawner`, `SpawnerAlreadySet`, `BoxFuture`, and `block_on` (the default executor). **(macro)** `run_async`, `spawn`, `CatchUnwind`. | +| `leak` | `debug_live` and the `ENABLED` flag; counters are live only with `leak-check`. | + +The [C ABI contract](../reference/abi.md) is the specification these items +implement, and [Async and Cancellation](../guides/async.md) covers the +spawner in practice. diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 156cd19b..363b7281 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -1,876 +1,250 @@ # Architecture -This page is the canonical reference for how WeaveFFI works internally. -It is the document new generator authors and contributors should read -before making non-trivial changes; all other documentation is consumer- -or library-author-facing. +This page is for contributors: how the workspace is organized, how a +definition becomes bindings, how to add a generator, and how changes are +tested. The [C ABI contract](reference/abi.md) is the specification +everything here implements. -## High-level pipeline +## The pipeline -Every `weaveffi generate` invocation flows through the same stages, in -this order: +Every generating command (`generate`, `diff`, `package`) runs the same +stages: ```text -Input: annotated Rust (.rs) or an IDL (YAML/JSON/TOML) - │ - ▼ -Parse ── weaveffi-ir::parse (IDL) | weaveffi-bridge (.rs): builds an `Api` IR - │ (`Module`, `Function`, `StructDef`, `EnumDef`, `InterfaceDef`, - │ `CallbackInterfaceDef`, `ErrorDomain`, `TypeRef`) - ▼ -Validate ── weaveffi-core::validate: rejects errors, collects warnings, - │ indexes every user-type declaration, and wraps the untouched - │ document in a `ResolvedApi` proof type; `ResolvedApi::resolve` - │ turns each written `TypeRef` into a `Ty` with `Family` / `WireType` - ▼ -Configure ── weaveffi-cli `ProjectConfig`: loads the nearest weaveffi.toml - │ ([package], [global], [generators.]) and attaches the - │ package identity to the `ResolvedApi` - ▼ -Model ── weaveffi-core::model::BindingModel::build(api, prefix): every - │ callable gets its C symbol and `AbiSig` (ordered ABI slots from - │ `abi::lower_param` / `lower_return`), every interface its - │ `_clone`/`_destroy` symbols, every callback interface its vtable - ▼ -Plan ── weaveffi-core::plan: `ArgPass`, `RetPass`, `ErrorStrategy`, - │ `IteratorProtocol`, `AsyncProtocol`, `CallbackProtocol`: the - │ language-neutral calling contracts derived from the model - ▼ -Generate ── weaveffi-core::codegen::Orchestrator: checks each target's - │ `capabilities(config)` against the features the API uses, then - │ dispatches every selected target in parallel via rayon; each - │ `LanguageBackend` renders the model and plan in its own syntax - ▼ -Output ── Each target writes its files under {out_dir}/{target}/ - and updates {out_dir}/.weaveffi-cache/{target}.hash +annotated Rust (.rs) IDL (.yml / .json / .toml) + weaveffi_model::rust weaveffi_model::parse + \ / + +--------> Api (the IR) <------+ + | + v + validate weaveffi_model::validate: every rule, including the + C symbol table; reports all violations at once + | + v + resolve ResolvedApi: the untouched document plus a type index; + every reference resolves to a Ty with an absolute path. + The CLI attaches the Identity from weaveffi.toml / Cargo.toml + | + v + model BindingModel::build: per-module bindings with every C + symbol and lowered ABI signature computed once + | + v + plan weaveffi_model::plan: ArgPass, RetPass, ErrorStrategy, + IteratorProtocol, AsyncProtocol, CallbackProtocol + | + v + render Target::render: each generator returns its files in + memory (pure; no I/O) + | + v + orchestrate Orchestrator: capability gate, cache records, writes + changed files, removes stale ones, runs hooks ``` -`validate` stops after the second stage; `generate`, `diff`, and `package` -run all of them. The `#[weaveffi::module]` proc-macro runs the same -pipeline up to the plan stage inside the producer's build and renders the -`extern "C"` thunks instead of a consumer wrapper. +`validate` stops after resolution; `diff` renders and compares without +writing; `package` renders package layouts instead of source trees. -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. -Because the CLI and the macro share one extraction, the IDL the CLI derives -and the symbols the macro emits are two views of one parse and cannot drift. -See [The Rust Producer Macro](guides/producer-macro.md). +The **producer macro** runs the front half of the same pipeline inside the +producer's build. `#[weaveffi::module]` lowers its module tree with +`weaveffi_model::rust::module_from_item_mod` (the extractor the CLI uses for +`.rs` inputs), computes the module's checksum with +`weaveffi_model::checksum::module_checksum`, builds a `BindingModel` with an +identity named after `CARGO_CRATE_NAME`, and emits `extern "C"` thunks +instead of rendering a binding. Because both sides share the extractor, the +model, and the checksum function, the symbols and checksums a producer +exports are exactly those its generated bindings expect. -## Crate layout - -The workspace is structured as a small set of stable, focused crates. -The dependency graph is acyclic and shallow: +## Crates ```text -Consumer side (the `weaveffi` binary): - -weaveffi-cli ──┬──► weaveffi-gen-c ──┐ - ├──► weaveffi-gen-cpp ──┤ - ├──► weaveffi-gen-swift ──┤ - ├──► weaveffi-gen-kotlin ──┤ - ├──► weaveffi-gen-node ──┤ - ├──► weaveffi-gen-wasm ──┼──► weaveffi-core ──► weaveffi-ir - ├──► weaveffi-gen-python ──┤ - ├──► weaveffi-gen-dotnet ──┤ - ├──► weaveffi-gen-dart ──┤ - ├──► weaveffi-gen-go ──┤ - ├──► weaveffi-gen-ruby ──┘ - ├──► weaveffi-core - └──► weaveffi-bridge ──► weaveffi-ir (lowers annotated .rs to IR) - -Producer side (a Rust cdylib depends on these, never on the CLI): - -weaveffi ──┬──► weaveffi-macros ──► weaveffi-bridge, weaveffi-core, weaveffi-ir - │ (build-time only: the proc-macro) - └──► weaveffi-abi (the C ABI runtime, re-exported as `weaveffi::abi`; - linked into every cdylib that exposes the ABI) - -weaveffi-fuzz ──► weaveffi-ir, weaveffi-core (workspace-private; unpublished) -``` - -`weaveffi-abi` depends on no other workspace crate, and `weaveffi-core` -depends only on `weaveffi-ir`, so a producer's runtime never pulls the -generators in and a generator never pulls the runtime in. The two meet only -through the ABI revision number (`weaveffi_abi::ABI_VERSION` and -`weaveffi_core::cabi::ABI_VERSION`, kept equal by a test). - -| 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`. The document model only: no package or generator configuration. | -| `weaveffi-abi` | The C ABI runtime (revision `ABI_VERSION = 2`): the `weaveffi_error` struct and the `-1`..`-4` runtime codes, `weaveffi_error_set`/`_clear`/`_free`, `weaveffi_free_string`/`_bytes`, cancel tokens, and the `export_runtime!` macro that emits them as `#[no_mangle]` symbols. Plus the modules the macro-generated thunks call: `buffer` (the value-buffer reader and writer), `convert` (the `lift_*`/`lower_*` converters), `object` (reference-counted interface objects: `lower_object`, `object_clone`, `object_destroy`, `object_arc`, `object_from_token`), `callback` (consumer vtables: the `Vtable` trait, `ForeignCallback`, `check_foreign_error`), and `spawn` (the pluggable `Spawner` behind `set_spawner`, with `CatchUnwind` turning a panicking future into a `-2` completion). | -| `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 and its marker attributes (`export`, `record`, `enumeration`, `interface`, `callback_interface`, `error`, `cancellable`). 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 focused submodules under `src/codegen/` (`sync`, `async_fns`, `iterators`, `records`, `enums`, `interfaces`, `callbacks`, `marshal`, `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` | 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 `capabilities` feature gate, 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 `errors` naming policy for generated error types, 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`, and any `samples/*` producer that dereferences a raw -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 -a macro-based producer needs no crate-level opt-in. The workspace-wide -`unsafe_code = deny` lint forbids it everywhere else. - -### CLI internals - -`weaveffi-cli` is split so that `main.rs` holds only argument parsing and -command dispatch; each subcommand and shared concern lives in its own -module: - -| Module | Responsibility | -| ------------- | --------------------------------------------------------------------- | -| `main.rs` | `clap` definitions and top-level dispatch into `commands/`. | -| `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`, `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. | - -#### The `cli_targets!` registry - -The 11 language targets used to be spelled out a dozen times (config -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`: - -```rust -cli_targets! { - "c" => c: CConfig via CGenerator, - "cpp" => cpp: CppConfig via CppGenerator, - "swift" => swift: SwiftConfig via SwiftGenerator, strip, - // … one line per target … - "ruby" => ruby: RubyConfig via RubyGenerator, strip, -} -``` - -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 }`: 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, callback_interfaces, - structs, enums, errors, modules }`: modules can nest. -- `Function { name, params, returns, doc, throws, async, cancellable, - deprecated }`. The same shape describes an interface's constructors, - methods, and statics and a callback interface's methods. -- `InterfaceDef { name, doc, deprecated, constructors, methods, statics }`: - a reference-counted object type; every interface also receives implicit - `_clone` and `_destroy` symbols. -- `CallbackInterfaceDef { name, doc, deprecated, methods }`: a method set - the consumer implements and the producer calls through a vtable. -- `TypeRef` enumerates every type reference a document can *write*: the - thirteen primitives (`I8` through `U64`, `F32`, `F64`, `Bool`, - `StringUtf8`, `Bytes`), one user-type spelling (`Named(String)` for any - record, enum, interface, or callback interface by bare or dot-qualified - name), and the four composite shapes (`Optional`, `List`, `Map`, - `Iterator`). Schema 0.9.0 removed `Handle`, `TypedHandle`, - `BorrowedStr`, and `BorrowedBytes` along with `Param.mutable` and - `Function.since`; the parser rejects each with a message naming its - replacement. - -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`, `CallbackInterface`). 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` (scalars, `bool`, C-style - enums, one slot by value), `String`, `Bytes`, `Buffer` (records, rich - enums, optionals, lists, maps, as one serialized `ptr`/`len` pair), - `Object { nullable }` (an interface or `Interface?`, one pointer slot), - `Callback` (a callback interface, a `ctx`/`vtable` pair), and - `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`, `Object`, `Enum`, - `User`, `Optional`, `List`, `Map`); every backend's codec emitter - dispatches on it. `Object` is the wire shape of an interface stored in - a record, list, map, or optional: a `u64` token carrying one strong - reference (see the [Value Buffer Protocol](reference/value-buffers.md)). - Callback interfaces and iterators have no wire shape because validation - never admits them inside a buffer. - -Every IR type derives `Debug`, `Clone`, `PartialEq`, `Eq`, `Serialize`, -and `Deserialize`. - -`TypeRef` (de)serializes as a string with custom syntax (`i32`, `Name`, -`[T]`, `{K:V}`, `T?`, `iter`). The parser is -`weaveffi_ir::ir::parse_type_ref`; both human-written IDL and the -JSON Schema export rely on it. - -### Schema versioning - -`CURRENT_SCHEMA_VERSION` (currently `"0.9.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 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. - -## Validation - -`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 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: - -- Identifier well-formedness (`is_valid_identifier`). -- Reserved keyword rejection (`if`, `else`, `for`, `while`, `loop`, - `match`, `type`, `return`, `async`, `await`, `break`, `continue`, - `fn`, `struct`, `enum`, `mod`, `use`). -- Uniqueness of module/function/parameter/struct/enum/field/variant/ - interface/callback-interface names within their respective scopes, plus - API-wide uniqueness of bare type names (structs, enums, interfaces, - callback interfaces, and error domains share one global namespace). -- Structs must have at least one field; enums at least one variant; - interfaces at least one member; callback interfaces at least one method. -- Enum discriminant uniqueness within an enum. -- 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. - Interface types are valid in every position except as a map key. Free - functions and interface members (including the implicit `_clone` and - `_destroy`) share the module's C symbol namespace. -- Callback-interface methods are synchronous, never `throws`, and return - nothing or a `Direct` value; a callback interface type is valid only as - a bare top-level parameter of a function, constructor, static, or - method. -- `throws: true` requires an error domain in scope (the module or an - ancestor). -- Iterator types are valid only as the outermost return of a synchronous - callable. -- Map keys must be a scalar, `bool`, `string`, or C-style enum type. -- Error domain name must not collide with a function name in the same - module; codes must be positive (`0` means success and negative codes - are reserved for the runtime) and unique within the domain; code names - must be unique across every domain in the API. - -The [IDL Reference](reference/idl.md#error-catalog) lists every -`ValidationError` variant with the message and suggestion the CLI prints. - -Warnings emitted today: - -- `LargeEnumVariantCount` (>100 variants). -- `DeepNesting` (composite types nested deeper than 3 levels). -- `EmptyModuleDoc` (no `doc:` on any function in the module). -- `AsyncVoidFunction` (async without a return type). -- `DeprecatedFunction` (informational). - -Interfaces (in every position, including inside records, collections, -iterator elements, and async returns), callback interfaces, typed error -domains with structured payloads, async and cancellable functions, -iterators (`iter`), nested modules, and cross-module type references -are all **first-class**. They pass validation and every generator handles -them. Do not re-add validator rejections for these features. - -Per-target capability gating still exists as a mechanism. -`weaveffi_core::capabilities::Feature` names the gated features -(`AsyncFunctions`, `CallbackInterfaces`, `Iterators`), each generator -answers `LanguageBackend::capabilities(&self, config)` with a -`TargetCapabilities`, and the orchestrator runs `capabilities::check` -against `used_features(api)` before dispatch, failing generation (and -listing the offending IDL definitions) when a selected target cannot -deliver a used feature. The config is passed because a backend can have -modes with different ceilings: the Wasm generator's Emscripten mode -reports no async or callback support, and its `allow_unsupported = true` -flag (read through `LanguageBackend::allows_unsupported`) downgrades the -failure to a loud warning and makes the backend emit explicit throwing -stubs in place of the unsupported entry points (omitted from the -TypeScript declarations) because Emscripten modules do not portably expose -the trampoline machinery. Every other shipped target declares full -support. Capability failures and mode gaps must stay loud: never skip a -definition silently. - -## 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 - -`weaveffi_core::codegen::Orchestrator` coordinates the generator stage: - -1. If `--force` is set, every cache entry under - `{out_dir}/.weaveffi-cache/{target}.hash` is invalidated. -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 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-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). - -## `LanguageBackend` and the `Target` 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: +weaveffi-cli ──► weaveffi-gen ──► weaveffi-model + ▲ +weaveffi ──┬──► weaveffi-macros ───────┘ (proc macro, build time only) + └──► weaveffi-abi (runtime linked into every producer) -```rust,ignore -pub trait Target: Send + Sync { - fn name(&self) -> &'static str; // the `--target` token - fn capabilities(&self) -> TargetCapabilities; // the backend's answer for its bound config - fn allows_unsupported(&self) -> bool; - 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 -} +weaveffi-fuzz ──► weaveffi-model (unpublished) ``` -`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: +`weaveffi-abi` depends on no other workspace crate, so a producer never +links the generators, and the generators never link the runtime. The ABI +revision is therefore declared twice, in `weaveffi_abi::ABI_VERSION` and in +the model (re-exported as `weaveffi_gen::cabi::ABI_VERSION`), and +`crates/weaveffi/tests/abi_version.rs` keeps them equal. + +| Crate | Owns | +|-------|------| +| `weaveffi-model` | Everything between a definition and code generation: the IR (`ir`), IDL parsing (`parse`, behind the default `idl` feature), Rust extraction (`rust`), validation and diagnostics (`validate`), the resolved view (`resolved`), package identity (`pkg`), the binding model with the symbol table (`model`), C ABI lowering (`abi`), the marshalling plan (`plan`), error naming (`errors`), and contract checksums (`checksum`). The macro uses it without the `idl` feature. | +| `weaveffi-gen` | The backend framework and all eleven generators: `backend` (the `LanguageBackend` trait), `codegen` (the object-safe `Target`, `ConfiguredBackend`, the `Orchestrator`, and the `CodeWriter` toolkit), `cabi` (the shared C declaration renderer), `cache` (generation records), `capabilities` (the feature gate), `lang` (keyword tables and escaping), `manifest` (JSON and XML escaping for package manifests), `package` and `platform` (packaging), and `targets::{c, cpp, swift, kotlin, node, wasm, python, dotnet, dart, go, ruby}`. | +| `weaveffi-cli` | The `weaveffi` binary: argument parsing (`main.rs`), `weaveffi.toml` and the target registry (`config.rs`), one module per subcommand under `commands/`, identity resolution, and extraction (`extract.rs`). | +| `weaveffi-abi` | The runtime: the error struct and codes, `(ptr, len)` conversions, reference-counted objects, cancel tokens, callback vtables and foreign errors, iterators, the value-buffer codec, the async spawner and `run_async`, and leak counters. | +| `weaveffi-macros` | `#[weaveffi::module]`, the marker attributes, and `export_runtime!`. Emission is split by concern under `src/codegen/` (`sync`, `async_fns`, `iterators`, `records`, `enums`, `interfaces`, `callbacks`, `foreign`, `marshal`). | +| `weaveffi` | The producer facade: re-exports the macros, the few runtime types a producer names, and `weaveffi_abi` as `weaveffi::abi`. | +| `weaveffi-fuzz` | `cargo-fuzz` targets for the parsers, `parse_type_ref`, and the validator. | + +The workspace denies `unsafe_code`; `weaveffi-abi` opts in, and the thunks +the macro emits carry a scoped allowance, so a macro-based producer needs no +`unsafe` of its own. + +## Key invariants + +- **Generators consume the model only.** Symbol names, ABI signatures, type + families, and wire shapes come from `BindingModel`, `Ty::family()`, and + `Ty::wire()`; a generator never re-derives a symbol, splits a qualified + name, or reads the raw IR. Names come from the `Identity` on the + `ResolvedApi`. +- **Absolute types.** Every resolved `Ty` names a user type by its absolute + path (`kv.Store`), so equality works across modules; + `weaveffi_gen::utils::local_type_name` gives the display name. +- **One symbol table.** `BindingModel::c_symbols` lists every C identifier + with the declaration that owns it, and validation rejects duplicates. +- **Fixed runtime code is real source.** A target's codec, error base + types, and loader live as source files under `targets//runtime/`, + included with `include_str!` and filled in with `{{PLACEHOLDER}}` + substitution, so they read and lint as normal code. +- **Determinism.** No hash-map iteration reaches output; output is + byte-identical across runs and platforms. +- **No branding.** Nothing generated is named after WeaveFFI except the + generated-file header and runtime-version comments. + +## The binding model and the plan + +`BindingModel::build` walks a `ResolvedApi` once and produces one +`ModuleBinding` per module (nested modules flattened, each with its +segments, its underscore `path`, its dotted path, and, for a top-level +module, its checksum). Every function, interface member, and callback method +carries: + +- its resolved parameter and return `Ty`s, the idiomatic shape a generator + renders; +- its lowered `AbiFn`, the native shape: the C symbol and the ordered slots + from `weaveffi_model::abi` (`lower_param`, `lower_return`, the async and + callback signature helpers), whose C rendering is canonical; +- its `CallShape`: `Sync`, `Async` (launcher, completion type, result + slots, cancellability), or `Iterator` (launcher, iterator type, `_next`, + `_destroy`). + +Two classifications on `Ty` drive every generator's dispatch, so no target +re-derives them. `Ty::family()` is the slot family from the +[ABI contract](reference/abi.md#families-and-slots) (`Direct`, `String`, +`Bytes`, `Buffer`, or `Object` with nullability). `Ty::wire()` is the shape +inside a value buffer (`Prim`, `Object`, `Enum`, `User`, `Optional`, `List`, +`Map`), which codec emitters match on. + +`weaveffi_model::plan` states the calling contracts once, derived from the +family: how each argument is passed (`ArgPass`), what a wrapper does with a +result and which runtime release it owes (`RetPass`, `Free`), whether a +non-zero error code is a typed domain error or a trap (`ErrorStrategy`), and +the iterator, async, and callback protocols. A generator renders these in its +own syntax; it doesn't decide them. + +## Backends and the orchestrator + +A generator implements `weaveffi_gen::backend::LanguageBackend`: ```rust,ignore pub trait LanguageBackend: Send + Sync { type Config: Serialize + Default + Clone + Send + Sync; fn name(&self) -> &'static str; - - /// Required: the gated features (async, callback interfaces, iterators) - /// this backend implements under `config`. Modes with different - /// ceilings (Wasm's Emscripten mode) answer differently per config. fn capabilities(&self, config: &Self::Config) -> TargetCapabilities; - /// Whether `config` opts into generating despite unsupported features - /// (the backend must then emit explicit throwing stubs). Default `false`. - fn allows_unsupported(&self, config: &Self::Config) -> bool { false } - - /// C ABI symbol prefix; the driver builds the `BindingModel` with it. - fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str { "weaveffi" } - - /// The other required hook: assemble every output file. Rendering is - /// pure; the driver performs the actual writes. fn files(&self, api: &ResolvedApi, model: &BindingModel, out_dir: &Utf8Path, config: &Self::Config) -> Vec; - - /// Canonical per-module walk (error → enums → structs → callback - /// interfaces → interfaces → functions) with call-shape dispatch. - /// Single-pass backends override the `render_*` hooks and call this; - /// multi-pass backends build their layout in `files` directly. - fn emit_members(&self, out: &mut String, module: &ModuleBinding, config: &Self::Config) { /* … */ } - // render_error / render_enum / render_struct / render_callback_interface / - // render_interface / render_function: all default to no-op. + fn package(/* ... */) -> Option> { None } + // Optional per-entity hooks (render_error, render_enum, render_struct, + // render_callback_interface, render_interface, render_function) and + // emit_members, which walks a module in canonical order. } ``` -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, 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 -C++ order declarations by dependency, Swift splits types from the -namespaced module body, and Kotlin, Node, and Wasm each render -parallel files (Kotlin + JNI C, addon C + JS + `.d.ts`, JS + `.d.ts`) in -their own passes. - -Generators emit code into a `String`; there is no template-engine layer -(an early Tera prototype intended for user template overrides was removed -in 0.4.0 because nothing read from it). Indentation and block nesting are -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`/ - `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, - 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`: 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)). -- `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 (objects encode - as `u64` tokens carrying one strong reference, records and rich enums - share one user-codec shape). -- `capabilities`: the `Feature` alphabet, `TargetCapabilities`, and the - `used_features`/`check` pair the orchestrator runs before dispatch. -- `errors`: the naming policy for generated error types (`WeaveFFIError` - versus `WeaveFFIException` brands, `type_name`, `exception_type_name`, - `pascal`), so no two targets disagree on how `KEY_NOT_FOUND` is spelled; - see [Naming and Package Conventions](reference/naming.md#error-domains-and-codes). -- `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 - named `class` emits valid code in every target. -- `manifest`: JSON and XML escaping plus an insertion-ordered - `JsonObject` builder for package manifests (`package.json`, `.nuspec`, - …), so user-supplied names and descriptions can't corrupt quoting. - -### The `CodeWriter` emission toolkit - -`weaveffi_core::codegen::CodeWriter` is a small, deterministic, -language-agnostic builder that owns indentation and block scoping, so a -generator describes the *shape* of its output instead of threading -`\n` and indent strings through every `push_str`. It is the preferred -way to render any indented, line-oriented body. - -```rust,ignore -let mut w = CodeWriter::four_space(); // or two_space() / tabs() -w.line("class Greeter:"); -w.scope(|w| { // one deeper indent level - w.line("def greet(self, name):"); - w.scope(|w| { - w.line("return f\"Hello, {name}\""); - }); -}); -let src = w.finish(); // owns the assembled String -``` - -Design points that keep output stable and migrations safe: - -- **One indent authority.** `line` writes `indent + text + "\n"`; - `scope`/`block` push and pop a level around a closure; `indent`/ - `dedent` adjust it manually. Blank lines (`blank`) never carry trailing - whitespace, preserving the determinism contract. -- **`with_depth(n)`** seeds the starting indent so a writer can render a - fragment that will be spliced into an already-indented context. -- **`raw`** appends pre-formatted text verbatim (no re-indentation), - which is how existing helpers (e.g. `emit_doc`) and large literal - blocks compose into a writer without a rewrite. This makes adoption - incremental: a backend can move one function at a time onto - `CodeWriter` while the snapshot suite proves the output is unchanged - byte-for-byte. - -The Python backend (`weaveffi-gen-python`) is the reference adopter: its -return marshalling, getters, enums, callback interfaces, and the central -function renderer are built with `CodeWriter`. Remaining generators are -being migrated onto it incrementally, each move guarded by the snapshot -corpus. - -The signatures above use `Result` from `anyhow` and IR types from -`weaveffi_ir`; consult those crates for the precise import set. - -Implementation notes: - -- Implement `name()` (the `--target` flag value, e.g. `"swift"`), the - associated `Config` type, `capabilities()`, and `files()`; override - `prefix()` when the config carries a configurable `c_prefix` and - `allows_unsupported()` when a mode of the target can't deliver every - feature. -- Return every emitted file from `files()`; `--dry-run` and - `weaveffi diff` read the derived `output_files`, so there is no separate - list to keep in sync. -- All paths are joined under `out_dir`; do not write outside the passed - directory or you will break the per-generator cache. -- Generators run in parallel; share no mutable state across calls. - -## C ABI naming convention - -Every emitted C symbol follows `{c_prefix}_{module_path}_{function}` -(default `c_prefix = "weaveffi"`), interface members follow -`{c_prefix}_{module_path}_{Interface}_{member}` with the implicit -`_clone` and `_destroy` beside them, and callback interfaces contribute a -`{c_prefix}_{module_path}_{Name}_vtable` type and no symbols. 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_set`, -`{c_prefix}_error_clear`, `{c_prefix}_error_free`, -`{c_prefix}_free_string`, `{c_prefix}_free_bytes`, -`{c_prefix}_abi_version`, the cancel-token functions). 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!`). The full naming policy, including the per-language -re-casing, is in [Naming and Package Conventions](reference/naming.md#generated-identifiers). - -### ABI revision - -`weaveffi_abi::ABI_VERSION` (currently `2`; 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 and runtime codes, the value-buffer encoding including object -tokens, the object `_clone`/`_destroy` and callback vtable conventions, -and the set and signatures of the `weaveffi_*` runtime symbols. The -normative text is the [C ABI Contract](reference/abi.md). -`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). - -## The ABI lowering model - -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; the `Buffer` -[`Family`](#the-ir)) becomes the same borrowed -`{name}_ptr` / `{name}_len` pair holding its serialized -[value buffer](reference/value-buffers.md); an interface becomes one -borrowed `const {tag}*` parameter or one owned `{tag}*` return; a callback -interface becomes `void* {name}_ctx, const {tag}_vtable* {name}_vtable`; -bytes and buffered returns append a `size_t* out_len` pointer; and every -synchronous symbol other than `_clone` and `_destroy` ends with a trailing -`{prefix}_error* out_err`. - -That calling convention is defined **once**, in -[`weaveffi_core::abi`][abi-source], rather than re-derived inside each -generator: - -- `CType`: a prefix-agnostic algebra of C types (`Int32`, `Size`, - `Ptr { pointee, const_pos }`, `StructTag { module, name }`, …) with a - single `render_c(prefix)` method that produces canonical C spelling. -- `lower_param(name, ty, module)`: expands one IDL parameter into its - ordered `AbiParam` slots. -- `lower_return(ty, module)`: the return `CType` plus any trailing - `out_*` `AbiParam`s. -- `sync_signature`, `async_input_params`, `async_callback_params`, and - `callback_method_signature`: assemble a whole `AbiSig` for a synchronous - symbol, an `_async` launcher, its completion callback, and a - callback-interface vtable entry, adding the fixed slots - (`error_out_param`, `context_param`, `ctx_param`, `cancel_token_param`) - in their canonical positions. - -The C and C++ generators render these slots straight to C -declarations, so their headers *are* the model by construction. The -declarative consumer generators (Python, Ruby, .NET) call the same -`lower_*` functions and map each `CType` onto their own FFI vocabulary -(`ctypes.c_*`, Ruby FFI symbols, P/Invoke `IntPtr`/`UIntPtr`). This is -what guarantees the producer header and every consumer agree on the -parameter arity and order of a symbol: the class of drift that -previously hid in a dozen hand-written copies of the lowering. - -A few conventions are genuinely language-specific and stay local to -their generator rather than leaking into the shared model: - -- **Iterator returns.** The C ABI returns an opaque iterator handle - (`{prefix}_{module}_{Iter}*`) while other backends model the same - slot differently, so `lower_return` refuses an `Iterator` and each - caller lowers it explicitly. -- **`byref` out-params.** ctypes (Python) and P/Invoke (.NET) express a - bytes or buffered return's `out_len` with an extra pointer level or - the C# `out` keyword; those renderings stay in the respective - generator. - -Imperative generators (Go cgo, Node, Dart, Swift) build their FFI -signatures inline with marshalling code and dispatch on `Ty::family()` -for the shape of each slot. The Kotlin (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; -the snapshot suite will show every generator the edit touches. - -## The marshalling plan - -The ABI lowering model answers *which symbols exist and what their C -signatures are*. `weaveffi_core::plan` is a distinct layer one level -up, sitting between that lowering and the syntax backends: a -language-neutral statement of the calling contracts every backend -renders, the questions the eleven generators used to answer -independently (and inconsistently): - -- **Passing in.** `ArgPass` (`ParamBinding::arg_pass()`): how one - parameter crosses the boundary (`Direct`, `String`, `Bytes`, `Buffer`, - `Object` borrowed for the call, or `Callback` as a `ctx`/`vtable` pair), - so marshalling code dispatches on the plan instead of re-deriving the - shape with local matches on `Ty`. -- **Passing out.** `RetPass` (`plan::ret_pass`): the receiving contract - for a sync return, an async result, an iterator element, or a - callback-method argument (`Void`, `Direct`, `String`, `Bytes`, `Buffer`, - or `Object { destroy_symbol, clone_symbol, nullable }`). `Object` names - both lifecycle symbols because a wrapper that adopts one strong - reference eventually owes `destroy_symbol` and calls `clone_symbol` - whenever it needs a second reference, for example to write the object - into a value buffer. -- **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 - trap on? `FnBinding::error_strategy()` answers it once. See the - [Error Handling guide](guides/errors.md#throws-versus-trap). -- **Ownership.** `Free` (`None` | `String` | `Bytes`, via - `RetPass::free()` and `plan::elem_free`): after copying a returned - value, or one list/map/iterator element, into a native one, exactly - which runtime release call does the wrapper owe? Adopted objects report - `Free::None` here and owe their `_destroy` instead. -- **Iterators.** `IteratorProtocol` (`IteratorBinding::protocol`): the - `iter` pull contract, including the requirement that wrappers stay - lazy (one producer `next` call per consumer step, never a hidden - drain into a list), the per-element `RetPass`, and the - destroy-exactly-once handle lifecycle. -- **Async.** `AsyncProtocol` (`AsyncBinding::protocol`): the - completion-callback contract: the callback fires exactly once from an - arbitrary producer thread, and everything it receives is owned by the - consumer: result buffers are released through the runtime free - symbols, a heap-boxed error through `{prefix}_error_free`, and - owned-object results are adopted. -- **Callback interfaces.** `CallbackProtocol` - (`CallbackInterfaceBinding::protocol`): the vtable layout (one entry per - method in declaration order plus the trailing `free`), the `RetPass` for - each method argument as received inside the consumer's trampoline - (strings and buffers borrowed for the dispatch, objects adopted), the - direct-only return, and the foreign-error rule (an exception becomes - `weaveffi_error_set(out_err, -4, …)`, never an unwind through C). - -Generators are thin syntax backends over this shared plan: a backend -that renders these plans in its own syntax cannot drift from the others -on semantics; only the spelling differs. The producer side consumes the -same contracts: each `weaveffi-macros` codegen submodule states which -plan clause it implements (the generated `_async` launchers hand the -callback owned results and a heap-boxed error, iterator thunks yield one -element per `_next`, `interfaces` emits `_clone`/`_destroy` over -`weaveffi_abi::object`, `callbacks` wraps a vtable in a -`weaveffi_abi::callback::ForeignCallback` and checks `out_err` after every -dispatch, and error dispatch follows `ErrorStrategy`), so the emitted glue -and every consumer wrapper agree by construction. When a generator needs a free/destroy or throws/trap -decision, it should consume the plan rather than re-derive the fact -with a local match. - -## Generator crate layout - -Every `weaveffi-gen-*` crate follows one internal layout so a reader who -knows one backend can navigate the other ten. `lib.rs` stays small: the -crate docs, the config struct, the generator type, and the -`LanguageBackend` wiring. The rendering lives in focused modules, named -consistently across the crates (a backend omits a module its target -doesn't need): - -| Module | Responsibility | -| ------------- | ----------------------------------------------------------------- | -| `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 `Ty::wire()` | -| `runtime.rs` | The fixed runtime prelude the generated code relies on | -| `calls.rs` | Sync/async/iterator wrappers, callback trampolines, marshalling | -| `entities.rs` | Enums, records, interfaces, callback interfaces, typed error domains | -| `package.rs` | Ecosystem manifests and READMEs | -| `tests.rs` | The crate's test module, via `#[cfg(test)] mod tests;` | - -The Go backend (`weaveffi-gen-go`) is the reference layout. - -## Determinism - -> Regenerating with the same WeaveFFI version on the same IDL produces -> byte-identical output. - -The contract is enforced by determinism tests in the snapshot suite. -Internally, every `HashMap` iteration that contributes to generated -output has been replaced with `BTreeMap` or an explicit sort, and the -`serde_json`-backed cache key uses canonical ordering. - -If you need to iterate a map inside a generator, use `BTreeMap` or -collect to a `Vec` and `sort_by_key`. Never rely on `HashMap` -iteration order for output; CI snapshot tests will fail -non-deterministically on different platforms or insta orderings. - -## The test pyramid - -Each layer catches a different class of regression, and a change to the -ABI or the IR usually has to pass through all of them: +`ConfiguredBackend::new(backend, config)` builds the `BindingModel` and +erases the backend to `dyn Target`, whose `render` returns `OutputFile`s. +Single-pass backends override the per-entity hooks and call `emit_members`; +backends with several parallel files (C++, Swift, Kotlin, Node.js, Wasm) +build their layout in `files`. + +The `Orchestrator` runs the selected targets: + +1. **Capability gate.** Each target declares the features it implements for + its config; an API using anything else fails with every offending + declaration listed (or warns, for targets configured to emit throwing + stubs). +2. **Freshness.** For each target it hashes the canonical IR, the identity, + the target name, its serialized config, and the CLI version, and compares + with `{out}/.weaveffi-cache/{target}.json`. A target is fresh when the + hash matches and every recorded file is on disk unchanged. +3. **Render and write.** Stale targets render in parallel (rayon). Each file + is written only if its contents differ, and files recorded by the + previous run but no longer produced are deleted. The `pre_generate` and + `post_generate` hooks run around the writes, only when something is + stale. + +`weaveffi diff` uses `render` directly and compares in memory. + +## The CLI + +`config.rs` holds `ProjectConfig` (the `[project]`, `[package]`, `[global]`, +and `[generators.*]` tables, with discovery and path resolution) and the +`cli_targets!` registry: one line per target that expands to the typed +`[generators.]` field, the `--target` name, and the orchestrator +registration. `commands/mod.rs` has the shared front half every generating +command runs (locate the project, resolve the identity, load and validate, +attach the identity), and each subcommand has its own module: `init`, +`generate`, `validate`, `diff`, `package`. + +## Adding a generator + +1. Add `crates/weaveffi-gen/src/targets//` following an existing + target's layout: `mod.rs` (config, generator, `LanguageBackend` impl), + `types.rs`, `codec.rs`, `calls.rs`, `entities.rs`, `package.rs`, a + `runtime/` directory of fixed source, and `tests.rs`. +2. Implement `LanguageBackend`. Declare honest capabilities; take every name + from the model and the identity; load `{library}` and honor + `{PREFIX}_LIBRARY`; check the ABI revision and every module checksum at + load; surface cancellation idiomatically; keep objects alive across calls. +3. Register it with one line in `cli_targets!` in + `crates/weaveffi-cli/src/config.rs`. +4. Add it to `snapshot_tests!` in `crates/weaveffi-cli/tests/snapshots.rs` + and accept the new snapshots. +5. Add `scripts/fixtures/.sh`, which compiles or type-checks a + generated tree with the language's toolchain, and a CI matrix entry. +6. Add consumers under `conformance//` with a `run.sh` that declares + one lane per sample, and add the language to `conformance/run.sh`. +7. Write `docs/src/generators/.md`, add it to `SUMMARY.md`, and add a + row to the [capability matrix](generators/README.md). + +## Testing + +Each layer catches a different class of regression; an ABI or IR change +usually has to pass all of them. | Layer | Where | What it pins | |-------|-------|--------------| -| Unit tests | `#[cfg(test)]` modules in every crate; `tests.rs` in each generator | Parsing, validation rules, `Ty` classification, ABI lowering, per-generator naming and codec helpers | -| Property tests | `crates/weaveffi-abi/tests/buffer_proptest.rs` (`proptest`) | The value-buffer codec laws: `decode(encode(v)) == v` for every `BufferValue`, and encodings are self-delimiting (concatenations split back exactly; trailing bytes are an error) | -| Compile-fail tests | `crates/weaveffi-macros/tests/ui.rs` (`trybuild`) | The proc-macro's contract: `ui/pass_*.rs` must expand and compile, `ui/fail_*.rs` must be rejected with the pinned `.stderr` (a `&mut` parameter, a callback method returning `String`, an interface that isn't `Sync`, a `Result` without an error domain, and so on) | -| Runtime tests | `crates/weaveffi-abi/tests/export_runtime.rs`, `crates/weaveffi/tests/abi_version.rs` | `export_runtime!` exports every symbol in `ABI_RUNTIME_SYMBOLS`, and the two `ABI_VERSION` constants agree | -| Snapshots | `crates/weaveffi-cli/tests/snapshots.rs` (`insta`) | Byte-exact output of all eleven generators over the five-fixture corpus (below) | -| Determinism | `crates/weaveffi-cli/tests/determinism.rs` | Two runs over the kitchen-sink fixture are byte-identical | -| CLI and output audits | `crates/weaveffi-cli/tests/cli_*.rs`, `extract_roundtrip.rs`, `no_silent_stubs.rs`, `safety_audit.rs`, `docs_examples.rs` | Exit codes and diagnostics of every subcommand, including `diff --check`; `extract` round-trips an IDL unchanged; no `unimplemented!`/`todo!` or stub markers in generator sources or their output; each target's disposal idiom really calls `_destroy` and frees strings; the README and Getting Started IDL snippets validate and generate, and every `SUMMARY.md` link resolves | -| Conformance | `conformance/run.sh` | Real consumers in all eleven languages against the eight `samples/*` producers (`contacts`, `events`, `kvstore`, `shapes`, `async-demo`, `codec`, `inventory`, and the header-only `calculator`), each compiled and run to exit 0 | - -`weaveffi diff --check` is also how downstream projects gate their own -CI: it regenerates into a temporary directory, compares with the committed -output, and exits non-zero (with a distinct exit code for "would change") -without writing anything. - -The conformance matrix is the only layer that executes generated code -against a live producer, so it is where ownership bugs surface: every -language runs the `events` lane (a callback interface, an object handed -back through a callback, iterators), the `kvstore` lane (objects inside -records, lists, and optionals; async; an eviction callback interface), the -`async-demo` lane, the `codec` lane (every wire shape round-tripped, -including object tokens inside buffers), and `shapes` (rich enums); -`contacts` covers the original struct/enum/optional surface, and the -`inventory` lanes cover nested modules with cross-module references and -per-module error domains. - -## Snapshot tests - -`crates/weaveffi-cli/tests/snapshots.rs` runs every generator across a -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, optional parameters, interfaces in every legal position -including inside records, lists, maps, and iterators, callback interfaces -carrying composite payloads and objects, 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: +| Unit tests | `#[cfg(test)]` modules; `tests.rs` in each target | Parsing, validation rules, `Ty` classification, lowering, naming helpers | +| Validation tests | `crates/weaveffi-model/src/validate/tests.rs` | Every `ValidationError` with its source span | +| Property tests | `crates/weaveffi-abi/tests/buffer_proptest.rs` | Codec laws: round trips, self-delimiting encodings, rejected trailing bytes | +| Runtime tests | `crates/weaveffi/tests/` | Exported runtime symbols, cancellation and dropped futures, foreign-error routing, leak counters, the ABI revision lockstep | +| Macro tests | `crates/weaveffi-macros/tests/ui/` (`trybuild`) | `pass_*.rs` compile; `fail_*.rs` fail with the pinned `.stderr` | +| Snapshots | `crates/weaveffi-cli/tests/snapshots.rs` (`insta`) | Byte-exact output of every target for every fixture in `tests/fixtures/` | +| CLI tests | `crates/weaveffi-cli/tests/cli/` | Every subcommand's behavior and exit codes, extraction round trips, determinism, no stub markers in output | +| Fixture compile checks | `scripts/check-fixtures.sh ` | Every fixture's generated tree compiles or type-checks with the target's toolchain | +| Conformance | `conformance/run.sh` | Real consumers in every language against every sample, with leak counters at zero | +| Fuzzing | `crates/weaveffi-fuzz` | Parsers and the validator never panic | + +The fixtures are `kitchen_sink` (every feature), `edge_cases` (reserved +words, deep nesting, objects in every legal position), `nested_modules` +(cross-module references and name clashes), `docs_everywhere` (doc-comment +emission), and `shapes` (rich enums). Snapshots redact the CLI version, so a +version bump doesn't touch them. + +Day to day: ```bash -cargo install cargo-insta --locked -cargo test -p weaveffi-cli --test snapshots -cargo insta review +just check # fmt, clippy (with the doc lints), rustdoc +just test # every test; fails on snapshot drift +just snapshots # accept reviewed snapshot changes +just fixtures swift # fixture compile check for one target +ONLY=python just conformance # conformance lanes for one language ``` -Press `a` to accept, `r` to reject, `s` to skip. Commit accepted -`.snap` files in the same commit as the code change that produced -them; never commit `.snap.new`. CI rejects pending snapshots. - -The harness redacts the WeaveFFI version in each file's generated-by -prelude to `[VERSION]` before snapshotting (and separately asserts the -real prelude is present), so a routine version bump does not invalidate -every snapshot in the corpus. - -## Adding a new generator - -A condensed checklist (the long version lives in -[`CONTRIBUTING.md`][contributing]): - -1. Create `crates/weaveffi-gen-/` mirroring the layout of - `weaveffi-gen-c`. Add it to `members` in the root `Cargo.toml` and - depend on `weaveffi-core` and `weaveffi-ir`. -2. Implement `weaveffi_core::backend::LanguageBackend`: define the - associated `Config` type, then `name`, `capabilities` (declare every - feature you implement; the orchestrator fails loudly for the rest), - `prefix` (if the config carries 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`. 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 `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 - into `conformance/run.sh`. -7. Add `scripts/publish-crates.sh` to the dependency-ordered publish - list (only when the crate is ready to be released). - -## Where to read next - -- [IDL Reference](reference/idl.md): the type system and validation - rules from a user's perspective. -- [C ABI Contract](reference/abi.md) and - [Memory and Error Model](reference/memory-error.md): the normative ABI - and the ownership summary every generator must honor. -- [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 - every generator must enforce. -- [Async Functions](guides/async.md): the per-target async invariants - every async-capable generator implements. - -[ir-source]: https://github.com/weavefoundry/weaveffi/blob/main/crates/weaveffi-ir/src/ir.rs -[abi-source]: https://github.com/weavefoundry/weaveffi/blob/main/crates/weaveffi-core/src/abi/mod.rs -[insta]: https://insta.rs/ -[contributing]: https://github.com/weavefoundry/weaveffi/blob/main/CONTRIBUTING.md +See [CONTRIBUTING.md](https://github.com/weavefoundry/weaveffi/blob/main/CONTRIBUTING.md) +for the full workflow. diff --git a/docs/src/comparison.md b/docs/src/comparison.md index 8c31a9a3..55bd604b 100644 --- a/docs/src/comparison.md +++ b/docs/src/comparison.md @@ -8,25 +8,27 @@ hand-written glue, the single-language bridges **swift-bridge**, **napi-rs**, and **wasm-bindgen**, and the C/C++-first generators **SWIG** and **autocxx**. > All comparisons reflect the public state of each project at the time of -> writing (ABI revision 2, schema `0.9.0`). If something here is out of date, -> please open a PR. +> writing (WeaveFFI ABI revision 3, schema `0.10.0`). If something here is out +> of date, please open a PR. ## The short version WeaveFFI's distinguishing bet is *one language-neutral IDL, one C ABI, eleven generators*. The producer can be Rust (with `#[weaveffi::module]` writing the ABI for you) or anything else that can export C symbols, and every generated -package is standalone: consumers never install WeaveFFI. Since ABI revision 2 -the object model is on par with the Rust-first tools: interface objects are -reference counted (`Arc`), can be shared between wrappers and nested inside -records, lists, maps, optionals, iterators, and async results, and callback -interfaces let the producer call consumer code through a vtable. +package is standalone: consumers never install WeaveFFI. The object model is +on par with the Rust-first tools: interface objects are reference counted +(`Arc`), can be shared between wrappers and nested inside records, lists, +maps, optionals, iterators, and async results, and callback interfaces let +the producer call consumer code through a vtable. Every symbol and package is +named after the library, never after WeaveFFI, so several WeaveFFI-built +libraries coexist in one process. The honest flip side is that WeaveFFI is pre-1.0 and its type system is deliberately smaller than UniFFI's: no user-defined generics or trait objects -with generics, callback methods return only scalars (no strings, buffers, or -objects yet), no async callback methods, and no multi-file IDL imports. The -[roadmap](roadmap.md) lists what's planned. +with generics, callback methods return only direct values (no strings, +buffers, or objects yet), no async callback methods, and no multi-file IDL +imports. The [roadmap](roadmap.md) lists what's planned. ## At a glance @@ -39,9 +41,9 @@ objects yet), no async callback methods, and no multi-file IDL imports. The | **Type system** | | | | | | | | | | | Records / enums / optionals / lists / maps | ✓ (value buffers) | ✓ | ✓ (structs, enums, `Option`, slices; no maps) | manual | ✓ (transparent structs and enums, `Option`, `Vec`) | ✓ (serde objects) | ✓ (`serde-wasm-bindgen` or classes) | ✓ | ✓ | | Objects with methods | ✓ reference counted, shareable, nestable | ✓ (`Arc`) | ✓ (opaques, borrowed or owned) | manual | ✓ (opaque types) | ✓ (`#[napi]` classes) | ✓ (classes) | ✓ (classes) | ✓ (C++ classes) | -| Callback interfaces | ✓ (vtable; sync, scalar returns) | ✓ (foreign traits; any return, `throws`, async) | partial (Kotlin, C, C++; input-only) | manual fn pointers | partial (closures Rust to Swift) | ✓ (`ThreadsafeFunction`) | ✓ (closures) | partial (directors) | partial | +| Callback interfaces | ✓ (vtable; sync, direct returns, failures as `Result`) | ✓ (foreign traits; any return, `throws`, async) | partial (Kotlin, C, C++; input-only) | manual fn pointers | partial (closures Rust to Swift) | ✓ (`ThreadsafeFunction`) | ✓ (closures) | partial (directors) | partial | | Typed error domains | ✓ (per-module codes, opt-in `throws`, payload fields) | ✓ (error enums) | ✓ (`Result`) | manual | ✓ (`Result`) | ✓ (JS `Error`) | ✓ (`Result`) | ✗ | ✗ | -| Async functions | ✓ (callback ABI, pluggable spawner, cancel tokens) | ✓ (poll-based, foreign executors) | ✗ | manual | ✓ (both directions) | ✓ (Tokio) | ✓ (`Promise`) | ✗ | ✗ | +| Async functions | ✓ (callback ABI, pluggable spawner, native cancellation on every target) | ✓ (poll-based, foreign executors) | ✗ | manual | ✓ (both directions) | ✓ (Tokio) | ✓ (`Promise`) | ✗ | ✗ | | Iterators | ✓ (`iter`, lazy on every target) | ✗ (materialize or use a trait) | partial (`DiplomatWrite`) | manual | ✗ | ✗ | ✓ (JS iterators) | partial | ✗ | | Generics / trait objects | ✗ (fixed set of built-in shapes) | partial (traits, no generics) | partial (traits on some backends) | ✗ | partial | ✗ | ✗ | ✓ (templates via `%template`) | ✓ | | Multi-file / multi-crate definitions | ✗ (one document per API) | ✓ (external types across crates) | ✓ (one bridge crate, many modules) | n/a | ✗ | n/a | n/a | ✓ (`%include`) | ✓ | @@ -49,7 +51,7 @@ objects yet), no async callback methods, and no multi-file IDL imports. The | Standalone CLI | ✓ (`cargo install weaveffi-cli`) | `uniffi-bindgen` (build.rs or CLI) | `diplomat-tool` | ✓ | `swift-bridge-cli` | `napi` CLI (npm) | `wasm-bindgen-cli` | system package | cargo build | | Publishable per-ecosystem packages | ✓ (`weaveffi package` for every target) | partial | partial | n/a | ✓ (SwiftPM) | ✓ (npm) | ✓ (npm via wasm-pack) | ✗ | n/a | | Schema-checked IDL with JSON Schema | ✓ | ✗ | n/a | n/a | n/a | n/a | n/a | ✗ | n/a | -| Load-time ABI version check | ✓ (`weaveffi_abi_version`) | ✓ (checksums) | ✗ | ✗ | ✗ | ✓ (N-API version) | ✗ | ✗ | n/a | +| Load-time ABI check | ✓ (ABI revision plus per-module contract checksums) | ✓ (checksums) | ✗ | ✗ | ✗ | ✓ (N-API version) | ✗ | ✗ | n/a | | Generated-output drift check in CI | ✓ (`weaveffi diff --check`) | build-time | build-time | ✓ | build-time | build-time | build-time | ✗ | build-time | | Maturity | pre-1.0 | shipping in Firefox and Mozilla products since 2020 | shipping in ICU4X | 1.0+, widely deployed | 0.1.x, active | 2.x+, widely deployed | 0.2.x, ubiquitous | 30+ years | pre-1.0 | | License | MIT OR Apache-2.0 | MPL-2.0 | MIT OR Apache-2.0 | MPL-2.0 | MIT OR Apache-2.0 | MIT | MIT OR Apache-2.0 | GPL-3.0 (generated code exempt) | MIT OR Apache-2.0 | @@ -101,15 +103,16 @@ is ahead of WeaveFFI today. generated header. - **WeaveFFI's Wasm target is single-threaded.** The default `wasm32-unknown-unknown` build has no threads, so async functions resolve - inline and callback-interface methods fire only while a call into the - module is on the stack; a producer that calls back from a spawned thread - can't run there. wasm-bindgen's `wasm-bindgen-futures` integrates with the + inline (which leaves little for an `AbortSignal` to cancel) and + callback-interface methods fire only while a call into the module is on the + stack; a producer that calls back from a spawned thread can't run there. wasm-bindgen's `wasm-bindgen-futures` integrates with the JS event loop natively, and Emscripten-based toolchains can use `pthread`s. In WeaveFFI's Emscripten compatibility mode, async functions and callback interfaces are not available at all. -- **No formal stability guarantee yet.** WeaveFFI is pre-1.0; schema `0.9.0` - and ABI revision 2 removed constructs without compatibility shims (see the - [migration guide](stability.md#migrating-from-schema-080--abi-1-to-090--abi-2)). +- **No formal stability guarantee yet.** WeaveFFI is pre-1.0; schema + `0.10.0` and ABI revision 3 renamed every symbol and changed string + passing without compatibility shims (see the + [migration guide](stability.md#migrating-to-schema-010-and-abi-3)). UniFFI, cbindgen, napi-rs, wasm-bindgen, and SWIG offer stronger compatibility commitments today. @@ -144,11 +147,13 @@ WeaveFFI is the right pick when you want: Async functions become `async/await` in Swift, `Promise`s in Node and Wasm, `suspend fun` in Kotlin, `async def` in Python, `Task` in C#, and `Future` in Dart, all from the same `async: true` flag; Rust producers - can plug their own executor with `weaveffi::set_spawner`. + can plug their own executor with `weaveffi::set_spawner`. Cancelling the + awaiting task, coroutine, or promise cancels the native call. 7. **A CI-first CLI.** `validate`, `diff --check`, `extract`, `schema`, and `package` are designed to drop into pipelines, every generator's output is byte-for-byte deterministic, and generated consumers refuse to load a - producer built for a different ABI revision. + producer built for a different ABI revision or from a different API + definition (per-module contract checksums). ## When to choose something else diff --git a/docs/src/faq.md b/docs/src/faq.md index fbd90a24..b4826dc9 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -1,346 +1,129 @@ # FAQ -The questions we hear most about WeaveFFI. For broader context see the -[introduction](intro.md), the [comparison page](comparison.md), the -[C ABI contract](reference/abi.md), and the -[per-target generator docs](generators/README.md). - -## 1. Why not UniFFI? - -[UniFFI](https://mozilla.github.io/uniffi-rs/) is excellent, ships in -production at Mozilla, and is the right choice if you only need Swift, -Kotlin, and Python. We built WeaveFFI because we needed: - -- **More targets out of the box.** WeaveFFI ships first-class generators - for C, C++, Swift, Kotlin, Node.js, Wasm, Python, .NET, Dart, Go, and - Ruby, eleven in total, all consuming one C ABI. UniFFI's first-party - language list is shorter and the rest live as external bindgens of varying - maturity. -- **A standalone CLI workflow.** WeaveFFI is a single binary - (`cargo install weaveffi-cli`) with `generate`, `validate`, `diff`, - `extract`, and `package` subcommands designed to drop into CI. UniFFI is a - build-script integration first. -- **A non-Rust-only story.** WeaveFFI's IR is language-agnostic: any - backend that can expose a stable C ABI (Rust, C, C++, Zig, ...) can be - driven from the same IDL and implement the same generated header. UniFFI - is Rust-first and its scaffolding ABI is private. -- **A YAML/JSON/TOML IDL with a JSON Schema.** WeaveFFI ships - `weaveffi.schema.json` for editor autocompletion. UniFFI's UDL is - custom-syntax and its proc-macro path is Rust-only. - -Since ABI revision 2 the object model is comparable: WeaveFFI interfaces are -reference-counted `Arc` objects that can be shared and nested inside -records and collections, and callback interfaces are traits the consumer -implements. UniFFI is still ahead on callback methods that return strings, -records, or objects, on async callback methods, and on cross-crate type -imports (see the [roadmap](roadmap.md)). If your matrix is only -Swift+Kotlin+Python and you want maximum maturity today, UniFFI is the safer -pick. See the [comparison page](comparison.md) for the full table. - -## 2. Can I use it with C++ codebases? - -Two distinct cases: - -- **Generating C++ bindings for consumers.** Yes, `--target cpp` - emits a header-only RAII C++ API (`weaveffi.hpp`) with - `std::optional`, `std::vector`, `std::unordered_map`, exception-based - errors, move semantics, and a `CMakeLists.txt`. See the - [C++ generator docs](generators/cpp.md). -- **Wrapping an existing C++ library.** WeaveFFI does not parse C++ - headers; you describe the surface area you want to expose in the - IDL and the C++ implementation provides the stable C ABI symbols. - If you want to start from C++ headers and auto-generate, look at - [autocxx](https://github.com/google/autocxx) or - [SWIG](https://www.swig.org/). - -## 3. Does it support generics? - -Yes, with a curated set of built-in generic shapes rather than open -user-defined generics: - -- `iter`: lazy streaming sequences with a `_next` / `_destroy` ABI. -- `[T]`: homogeneous lists. -- `{K:V}`: homogeneous maps. -- `T?`: optionals. - -All four compose with each other and with every other type, including -interface objects: `[Store]`, `{string:Store}`, `Store?`, and `iter` -are all valid, and records may carry any of them as fields. Lists, maps, -optionals, records, and rich enums cross the C ABI serialized as -[value buffers](reference/value-buffers.md); objects inside them travel as -tokens that carry one strong reference. - -We deliberately do **not** support arbitrary user-defined generics (for -example `Result` parameterized at the IDL level) or traits -with generic methods. Cross-language generic monomorphization is a rabbit -hole; the built-in shapes cover the large majority of real-world FFI surface -area without requiring every target generator to implement type-erasure -logic. Trait-object interfaces (one declared method set, several producer -implementations) are on the [roadmap](roadmap.md). - -## 4. What's the runtime overhead? - -WeaveFFI itself adds **no runtime** beyond the small `weaveffi-abi` -crate (error helpers, string and byte-buffer allocators, the value-buffer -codec, the object reference-count shims, the callback vtable shims, cancel -tokens, and the async spawner hook). Per-call overhead is the cost of: - -1. Marshalling arguments across the C ABI (`string` to `const char*`, - `bytes` to `ptr + len`, and so on). String and byte parameters are - borrowed views for the duration of the call, so the producer copies only - what it keeps. -2. The single `extern "C"` function call. -3. Marshalling the return value back. - -For primitive arguments and return types, this is roughly the cost of -a normal function call plus an out-pointer write for the error. Passing an -object is one pointer; the producer clones the `Arc` only if it retains it. -For larger structs, lists, and maps, it's dominated by the encode/decode of -the value buffer, which is a single allocation per value on each side. - -Async functions add a callback indirection (the C ABI is callback-based) -plus whatever executor drives the future. The default spawner runs each -future on a dedicated thread; producers that already have a runtime plug it -in with `weaveffi::set_spawner` (see question 13). - -## 5. How are errors propagated? - -At the C ABI, generated functions take a trailing `weaveffi_error* -out_err` parameter. On success the runtime sets `code = 0` and -`message = NULL`. On failure it sets a non-zero code and a -heap-allocated UTF-8 message that the caller frees via -`weaveffi_error_clear`. Negative codes are reserved for the runtime: -`-1` generic, `-2` producer panic, `-3` marshalling failure, and `-4` -a consumer callback-interface implementation raised. - -Above the ABI, error surfacing is opt-in per function: a module -declares an error domain (`errors:` in the IDL) with named, stable -codes, and functions marked `throws: true` surface those codes as the -domain's typed error in each language. Taking a `KvError` domain as -the example: - -- **C**: direct `weaveffi_error` struct, plus an enum constant per - code (`weaveffi_kv_KvError_KeyNotFound`). -- **C++**: per-domain exception types (`KvError` + per-code - subclasses). -- **Swift**: `throws` with a domain enum conforming to `Error` - (`catch KvError.keyNotFound`). -- **Kotlin**: domain exceptions (`KvException` extends - `WeaveFFIException`, one nested class per code). -- **Node.js / TypeScript**: domain error classes (`KvError` extends - `WeaveFFIError`); async functions reject the promise with them. -- **Wasm/JS**: the same domain error classes. -- **Python**: a domain exception hierarchy (`KvError` extends - `WeaveFFIError`, `KeyNotFound` extends `KvError`). -- **.NET**: thrown `KvException` (extends `WeaveFFIException`). -- **Dart**: thrown `KvException` (extends `WeaveFFIException`). -- **Go**: a second `error` return carrying a typed error struct - matched via `errors.As` and code constants - (`KvErrorKeyNotFound`). -- **Ruby**: domain exception classes (`KvError` with nested - per-code classes). - -Non-throwing functions (the default) return plain values; a non-zero -code on one of them only ever reports a producer bug, so the wrapper -panics or traps instead of surfacing an error type. See the -[Error Handling guide](guides/errors.md). - -## 6. Can I customize the generated code? - -Yes, via two escape hatches in increasing order of power: - -1. **Project config** (`[generators.]` tables in the - `weaveffi.toml` next to your definition). Controls Swift module - names, the Kotlin package, C prefix, C++ namespace, Dart/Go/Ruby package - names, module-prefix stripping (`strip_module_prefix`), Wasm's - Emscripten mode, 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. - -If you need to change the C ABI shape itself, that's a generator -contribution. See [`CONTRIBUTING.md`](https://github.com/weavefoundry/weaveffi/blob/main/CONTRIBUTING.md#adding-a-new-generator). - -## 7. Does it work with Flutter? - -Yes, `--target dart` emits `dart:ffi` bindings plus a `pubspec.yaml` -that's drop-in compatible with both Flutter and pure Dart projects. -You ship the generated package alongside the `cdylib` for each -platform Flutter targets (iOS framework, Android `.so` per ABI, macOS -`.dylib`, Linux `.so`, Windows `.dll`). - -The generated Dart code uses the standard `package:ffi` helpers, so -it works on every Flutter platform that supports `dart:ffi` (i.e. -everything except Web today; for the browser, use `--target wasm` -and load the bindings via JS interop). See the -[Dart generator docs](generators/dart.md). - -## 8. Is it Windows-friendly? - -Yes, WeaveFFI itself builds and runs on Windows (the CLI is plain -Rust, no platform-specific dependencies). Generated outputs target -Windows correctly: - -- **C / C++**: emitted headers are compiler-agnostic (MSVC, clang, - gcc), and every prototype carries a portable `WEAVEFFI_API` - visibility macro. Consumers resolve it to `__declspec(dllimport)`; - a C/C++/Zig backend that implements the header builds its library - with `WEAVEFFI_BUILD` defined to export the symbols via - `__declspec(dllexport)` (see the - [C generator docs](generators/c.md#symbol-visibility)). -- **.NET**: P/Invoke uses `DllImport` with the right calling - conventions and looks up `weaveffi.dll`. -- **Node.js**: the N-API addon builds with `node-gyp` on Windows. -- **Python**: `ctypes` loads `weaveffi.dll`. -- **Dart**: looks up `weaveffi.dll` via `Platform.isWindows`. -- **Go / Ruby**: load the appropriate Windows shared library. - -CI builds and tests the workspace on Windows on every PR, and a -dedicated Windows job generates every target's bindings and verifies -the output. The full conformance harness (running the generated -bindings in all eleven languages) is exercised on Linux. If you hit a -Windows-specific issue, please open an issue. - -## 9. How do I distribute the cdylib? - -You build a platform-specific shared library per target triple and -ship it alongside the generated package. `weaveffi package` does the -assembly: point it at prebuilt libraries with `--binaries ` (or let it -cross-compile a Rust producer with `--build `) and it lays each -target's package out the way its ecosystem expects, including -`jniLibs//` for Kotlin and the `.wasm` binary inside an npm package for -Wasm (which needs a `wasm32` build). Three common layouts: - -- **Per-platform npm/PyPI/gem packages.** Publish one package per - `(os, arch)` and let the generated loader pick the right binary at - runtime. WeaveFFI generates the TypeScript/Python/Ruby loader, you supply - the binaries. -- **`xcframework` for Swift.** Bundle iOS device, iOS simulator, - and macOS slices into a single `.xcframework` that SwiftPM can - consume. The generated `Package.swift` references it as a - `.binaryTarget`. -- **`.aar` for Android.** Package the JNI shim plus per-ABI `.so` files - (`android-arm64`, `android-x64`) into an Android Archive that Gradle - resolves like any other dependency. The generated `build.gradle.kts` is - compatible with this layout, and the same wrapper also runs on the desktop - JVM. - -The name, version, and metadata stamped into every generated manifest -(`package.json`, `pyproject.toml`, `*.gemspec`, `*.csproj`, `pubspec.yaml`, -`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 "weaveffi publish" command; you use each ecosystem's normal -publish flow on the output of `weaveffi package`. See the -[Packaging and Distribution guide](guides/packaging.md) and the -[generator-specific docs](generators/README.md) for the recommended build -matrix per language. - -## 10. What's the licensing? - -WeaveFFI is dual-licensed under -[MIT](https://github.com/weavefoundry/weaveffi/blob/main/LICENSE-MIT) **OR** -[Apache-2.0](https://github.com/weavefoundry/weaveffi/blob/main/LICENSE-APACHE) -at your option, the same dual-license used by the Rust project itself. - -You can use WeaveFFI in commercial, closed-source, or open-source -projects without restriction. Generated code carries no license header -of its own; it's yours to license however you like. Contributions -to the WeaveFFI repo are accepted under the same MIT-or-Apache-2.0 -dual license; see [`CONTRIBUTING.md`](https://github.com/weavefoundry/weaveffi/blob/main/CONTRIBUTING.md#license). - -## 11. Who owns an object, and when is it freed? - -The producer does. Every interface object is reference counted on the native -side (an `Arc` in a Rust producer), and the C ABI exposes two symbols per -interface: `{tag}_clone`, which returns a new strong reference to the same -object, and `{tag}_destroy`, which releases one. The object is dropped when -the last reference goes away, wherever that reference lives. - -Each generated wrapper holds exactly one strong reference and releases it -through the language's natural disposal hook: `deinit` in Swift, the -destructor in C++ (copying a wrapper calls `_clone`), `close()` or a `using` -declaration in Node.js and Wasm, `close()` or a `with` block in Python, -`Dispose()` in .NET, `close()` (`AutoCloseable`) in Kotlin, `dispose()` in -Dart, `Close()` in Go, and `close` in Ruby. Every managed target also -registers a garbage-collector backstop (`Cleaner`, `FinalizationRegistry`, -`__del__`, a finalizer, `NativeFinalizer`, `runtime.SetFinalizer`, -`FFI::AutoPointer`) so a wrapper that's never closed still releases its -reference eventually. Closing a wrapper twice is a no-op; using one after -closing it throws. - -Because the count lives in the producer, ownership is uniform in every -position: an object returned from a call, produced by an iterator, delivered -by an async completion, or passed to a callback-interface method is one -reference the consumer adopts. An object passed *to* the producer as a -parameter is borrowed for the call, and the producer clones it if it keeps -it. An object inside a record, list, map, or optional travels as a token -carrying one reference, so two wrappers (or a wrapper and a producer-side -`Arc`) can point at the same object and neither can free it out from under -the other. See [Memory Ownership](guides/memory.md) and the -[C ABI contract](reference/abi.md#objects-interfaces). - -## 12. How do callback interfaces work? - -A callback interface is a set of methods the consumer implements and the -producer calls. In the IDL it's a `callback_interfaces:` entry with -`methods:`; in Rust it's - -```rust -#[weaveffi::callback_interface] -pub trait EvictionListener: Send + Sync { - fn on_evict(&self, entry: &Entry, reason: EvictionReason) -> bool; -} -``` - -accepted by any function, constructor, static, or method as -`Arc`. Each target exposes it as the natural thing to -implement: a Swift `protocol`, a Kotlin, Go, or C# interface, a Python `ABC`, -a Dart `abstract class`, a C++ class with virtual methods passed as a -`std::shared_ptr`, a TypeScript `interface` in Node.js and Wasm, and a Ruby -module. At the C ABI it's a `void* ctx` plus a static vtable of function -pointers with a trailing `free(ctx)`; the producer may call the methods any -number of times from any thread and calls `free` exactly once when it drops -its last reference. There is no unregister call to forget. - -Methods are synchronous, never `throws` or `async`, and return nothing, a -scalar, `bool`, or a C-style enum; parameters may be anything except another -callback interface or an iterator, and an object parameter hands the consumer -a reference it adopts. If the consumer's implementation raises, the wrapper -reports it through the method's `out_err` slot with `FOREIGN_ERROR_CODE` -(`-4`); the producer aborts the call it was making (in Rust, the call -unwinds) and the original caller sees that code and message instead of a -crash. Producers should therefore not hold a `Mutex` guard across a callback -call. Richer return types and async callback methods are on the -[roadmap](roadmap.md). The `events` and `kvstore` [samples](samples.md) show -both patterns. - -## 13. Which executor runs my async functions? - -Whichever you install. An `async` export lowers to a launcher that returns -immediately and a completion callback that fires exactly once, from a -producer thread. Something has to drive the future in between, and the -`weaveffi-abi` runtime has a pluggable `Spawner` hook for that. The default -spawner needs no runtime: it detaches a thread per future and blocks on it, -which is fine for CPU-bound work and for futures woken from other threads. - -A producer whose futures need a reactor (Tokio's I/O or timers, for example) -calls `weaveffi::set_spawner` once at startup: - -```rust -let handle = tokio::runtime::Handle::current(); -weaveffi::set_spawner(move |fut| { - handle.spawn(fut); -}) -.expect("spawner installed once"); -``` - -The first call wins; a second returns `SpawnerAlreadySet`. Every future handed -to a spawner is already wrapped so a panic inside it is caught and reported -through the completion callback as `PANIC_ERROR_CODE`, so a spawner never -sees an unwinding future. On `wasm32`, which has no threads, the default -spawner drives the future inline before the launcher returns. Consumers don't -see any of this: they get `async/await`, `Promise`, `suspend`, `Task`, or -`Future` as usual. See the [Async Functions guide](guides/async.md). +## Why not UniFFI? + +[UniFFI](https://mozilla.github.io/uniffi-rs/) is excellent and ships in +production at Mozilla; if you need only Swift, Kotlin, and Python and want +maximum maturity, it's the safer pick. WeaveFFI exists for a different set of +needs: eleven first-party targets from one definition, a producer that can be +written in any language with a C ABI (the generated header is a public +contract, not private scaffolding), a standalone CLI built for CI, and a +schema-checked YAML, JSON, or TOML IDL. UniFFI is still ahead on callback +methods that return rich types, async callback methods, and cross-crate type +imports. See the [comparison](comparison.md) for the full table. + +## Can I use it with a C++ codebase? + +To *consume* a library from C++, `--target cpp` emits a header-only RAII API +with `std::optional`, `std::vector`, `std::future`, exceptions, and a +`CMakeLists.txt`; see [C++](generators/cpp.md). To *expose* an existing C++ +library, describe the surface in an IDL and implement the generated C header +in C++; WeaveFFI doesn't parse C++ headers. For automatic wrapping of C++ +headers, look at [autocxx](https://github.com/google/autocxx) or +[SWIG](https://www.swig.org/). + +## Does it support generics? + +Through built-in shapes rather than user-defined generics: `T?`, `[T]`, +`{K:V}`, and `iter`, which compose with each other and with every other +type, including objects (`[Store]`, `{string:Store}`, `iter`). +Arbitrary generics would push type erasure into every generator; +trait-object interfaces are on the [roadmap](roadmap.md). + +## What's the runtime overhead? + +A call costs the marshalling of its arguments, one `extern "C"` call, and the +marshalling of its result. Scalars pass by value. Strings and bytes pass as +borrowed `(ptr, len)` views, so the producer copies only what it keeps. +Objects pass as one pointer. Records and collections are encoded into one +value buffer per value, with single-copy fast paths for byte and numeric +lists. Async calls add a completion callback and whatever executor drives +the future. The runtime itself is the small `weaveffi-abi` crate; the +`leak-check` counters cost an atomic update per counted operation and are off +by default. + +## How are errors propagated? + +Every fallible C call takes a `{prefix}_error*` out-parameter; async calls +receive one in their completion. Positive codes are your declared error +domain, negative codes are the runtime's (generic, panic, marshalling, +callback failure, cancelled). A function marked `throws: true` (a `Result` in +Rust) surfaces domain codes as typed errors in every language; a function +without it traps on failure, since a failure there is a bug. See +[Errors and Memory](guides/errors-and-memory.md). + +## Can two WeaveFFI libraries live in one process? + +Yes. Every symbol, type, macro, package, and environment variable is named +after the library's own prefix and name (`kvstore_error`, +`KVSTORE_LIBRARY`), and none after WeaveFFI, so two libraries never +collide, whether linked statically or loaded dynamically. Each carries its +own copy of the small runtime. + +## What happens if the bindings and the library drift apart? + +They refuse to load. Every generated consumer compares the library's ABI +revision and each top-level module's contract checksum with the values it +was generated against, and raises the language's load error naming the +module on a mismatch. The checksum covers names, types, order, and flags, but +not documentation, so editing doc comments never breaks a deployed binding. +In CI, `weaveffi diff --check` catches stale committed bindings before they +ship. + +## Can I customize the generated code? + +Through `weaveffi.toml`: package metadata, per-target names (the Swift +module, the Kotlin package, the Go module path), target-specific options, and +`pre_generate`/`post_generate` hooks for formatters. See +[Project Configuration](guides/config.md). Changing the C ABI itself is a +generator contribution; see the [architecture guide](architecture.md). + +## Does it work with Flutter? + +The Dart target emits `dart:ffi` bindings with a `pubspec.yaml` usable from +Flutter and plain Dart on every platform that supports `dart:ffi`. You ship +the native library for each platform yourself; Flutter's native-assets build +isn't generated yet. For the web, use the WebAssembly target. See +[Dart](generators/dart.md). + +## Is it Windows-friendly? + +The CLI is plain Rust and runs on Windows, and the workspace's tests run on +Windows in CI. Headers carry a `{PREFIX}_API` macro that resolves to +`__declspec(dllimport)` for consumers and `dllexport` when a C or C++ +producer defines `{PREFIX}_BUILD`, and every loader knows the `{library}.dll` +naming. The conformance harness runs on Linux and macOS today; Windows lanes +are on the [roadmap](roadmap.md#windows-conformance-lanes-planned). + +## How do I distribute the native library? + +Build it per platform and let `weaveffi package` bundle it into each +ecosystem's package layout (npm platform packages, platform wheels, NuGet +`runtimes/`, Android `jniLibs/`, and so on), then publish with each +ecosystem's own tools. Every generated manifest takes its name and version +from `[package]`. See [Packaging](guides/packaging.md). + +## Who owns an object, and when is it freed? + +The producer reference-counts it. Each wrapper holds one strong reference and +releases it through the language's disposal idiom (`close()`, `Dispose()`, +`deinit`, a destructor), with a garbage-collector backstop; the object is +freed when the last reference anywhere goes. Objects returned, yielded, +delivered, or passed to a callback are references the receiver adopts; +objects passed to the producer are borrowed. See +[Errors and Memory](guides/errors-and-memory.md#objects). + +## Which executor runs my async functions, and how does cancellation work? + +The one you install with `weaveffi::set_spawner`, such as a Tokio handle; +by default each future runs on its own thread. Consumers see their native +async idiom, and cancelling it (a Swift `Task`, a Kotlin coroutine, an +`AbortSignal`, a `CancellationToken`, a Go `context`) cancels the native +call: the runtime drops the future and completes with the cancelled code, +which surfaces as the language's cancellation error. See +[Async and Cancellation](guides/async.md). + +## What's the license? + +MIT OR Apache-2.0, at your option. Generated code carries no license header +of its own and is yours to license as you like. diff --git a/docs/src/generators/README.md b/docs/src/generators/README.md index e82832cc..fa59cbf4 100644 --- a/docs/src/generators/README.md +++ b/docs/src/generators/README.md @@ -1,69 +1,84 @@ # Generators -This section contains language-specific generators and guidance for using the -artifacts they produce. Choose a target below to explore the details. +WeaveFFI ships eleven generators, one per target. Every generator implements +the whole IDL surface of schema 0.10.0 on ABI revision 3: records, C-style +and rich enums, reference-counted objects in every position, callback +interfaces, optionals, lists, maps, typed error domains, nested modules, +async functions with cancellation, and lazy iterators. Each one names its +output from the library's [identity](../reference/naming.md), loads +`{library}` (overridable with `{PREFIX}_LIBRARY`), checks the ABI revision +and every module checksum at load time, and produces a tree under +`{out}/{target}/` that builds with the language's normal tooling. -## Feature support matrix +A generator that can't support a feature in some mode (Wasm's Emscripten +mode, for example) declares it, and `weaveffi generate` fails with the +offending declarations listed instead of silently skipping them. -Every generator implements the full IDL surface of schema 0.9.0 on ABI -revision 2: records, plain and rich enums, reference-counted interface -objects (including nullable objects and objects inside records, lists, -optionals, iterators, and async results), callback interfaces, -optionals, lists, maps, typed error domains with opt-in `throws`, and -nested modules, plus the call shapes below. A generator that cannot -support a feature declares it in its `TargetCapabilities`, and -`weaveffi generate` fails loudly when an IDL uses a feature the -selected target cannot deliver (no silent skips). +## Capability matrix -| Target | Object disposal | Callback interface | Async functions | Iterators (`iter`) | `weaveffi package` bundles | -|--------|-----------------|--------------------|-----------------|-----------------------|----------------------------| -| [C](c.md) | raw `_clone`/`_destroy` symbols | `ctx` + vtable struct with `free` | raw completion-callback ABI | raw `_next`/`_destroy` | every supplied slice (CMake selects desktop) | -| [C++](cpp.md) | copyable RAII class (copy = `_clone`) | abstract class via `std::shared_ptr` | `std::future` | move-only input range | desktop slices | -| [Swift](swift.md) | `final class`, `deinit` releases | protocol | `async throws` | `Sequence` | desktop slices (XCFramework) | -| [Kotlin](kotlin.md) | `AutoCloseable` `close()` + `Cleaner` backstop | `interface` via JNI shim | `suspend fun` | `Iterator` | `jniLibs//` + desktop `resources/natives/` | -| [Node.js](node.md) | `close()`/`Symbol.dispose` + `FinalizationRegistry` | plain object (TS `interface`) | `Promise` | `IterableIterator` | desktop slices as `optionalDependencies` | -| [Wasm](wasm.md) | `close()`/`Symbol.dispose` + `FinalizationRegistry` | plain object via table trampolines | `Promise` (settles inline) | `IterableIterator` | the `wasm32` `.wasm` | -| [Python](python.md) | `close()` + `__del__`, context manager | abstract base class | `async def` | iterator | desktop slices (platform wheel) | -| [.NET](dotnet.md) | `IDisposable` + finalizer | C# `interface` | `Task` | `IEnumerable` (single pass) | desktop slices under `runtimes//native/` | -| [Dart](dart.md) | `dispose()` + `NativeFinalizer` | abstract class | `Future` | `Iterable` (single pass) | desktop slices | -| [Go](go.md) | `Close()` + `runtime.SetFinalizer` | Go `interface` | blocking bridge | `iter.Seq`/`iter.Seq2` | desktop slices (relocatable cgo preamble) | -| [Ruby](ruby.md) | `close` + `FFI::AutoPointer` | duck-typed object | blocking bridge | `Enumerator` | desktop slices (platform gems) | +| Target | Package / module | Object release | Async | Cancellation | Callback interface | `iter` | Minimum toolchain | +|--------|------------------|----------------|-------|--------------|--------------------|-----------|-------------------| +| [C](c.md) | header `{library}.h` | `_destroy` | launcher plus callback | `{prefix}_cancel_token*` | `ctx` plus vtable struct | `_next`/`_destroy` | C11 | +| [C++](cpp.md) | header `{library}.hpp`, namespace `{prefix}` | RAII destructor; copies clone | `std::future` | RAII `CancelToken` | abstract class | input range | C++17 | +| [Swift](swift.md) | module `PascalCase(name)` | `deinit` | `async` / `async throws` | `Task` cancellation | protocol | `Sequence` | Swift 5.7 | +| [Kotlin](kotlin.md) | package `{prefix}` | `close()`, cleaner backstop | `suspend fun` | coroutine cancellation | interface | `Iterator` | Kotlin 1.9; Android API 21 or a JVM | +| [Node.js](node.md) | npm `{name}` | `close()`, `Symbol.dispose`, finalizer | `Promise` | `AbortSignal` | object implementing an interface | `IterableIterator` | Node.js 18 | +| [WebAssembly](wasm.md) | npm `{name}` | `close()`, `Symbol.dispose`, finalizer | `Promise` | `AbortSignal` | object implementing an interface | `IterableIterator` | Node.js 18 or a current browser | +| [Python](python.md) | dist `{name}`, import `{prefix}` | `close()`, `with`, finalizer | awaitable | task cancellation | abstract base class | iterator | Python 3.9 | +| [.NET](dotnet.md) | namespace `PascalCase(name)` | `Dispose()` (`SafeHandle`) | `Task` | `CancellationToken` | interface | `IEnumerable` | .NET 8 | +| [Dart](dart.md) | package `{prefix}` | `dispose()`, `NativeFinalizer` | `Future` | cancel-token object | abstract class | `Iterable` | Dart 3.1 | +| [Go](go.md) | module `{name}`, package `{prefix}` | `Close()`, finalizer | blocking call | `context.Context` | interface | `iter.Seq` / `iter.Seq2` | Go 1.23 | +| [Ruby](ruby.md) | gem `{name}`, module `PascalCase(name)` | `close`, `FFI::AutoPointer` | blocking call | `cancel:` keyword | duck-typed object | `Enumerator` | Ruby with the `ffi` gem 1.15 | -Notes: +The language pages are authoritative for exact type names, signatures, and +toolchain versions. Notes that apply across targets: -- **Objects are reference counted.** Every wrapper owns one strong - reference to the producer's `Arc`; disposal drops it, and copying a - wrapper or placing it in a record, list, or optional mints a new - reference through the `_clone` symbol. Use after disposal is a - consumer error surfaced in the language's idiom (an exception with - `MARSHAL_ERROR_CODE` `-3`, `ObjectDisposedException`, `StateError`, a - Go panic, and so on); each page documents its own. -- **Callback implementations own the objects they receive.** An object - argument to a callback method is adopted by the implementation, which - disposes it. A callback implementation that throws surfaces to the - caller of the producer function as `FOREIGN_ERROR_CODE` `-4` through - the language's error type; a non-throwing callable follows the - language's trap idiom. -- **Iterators are lazy.** Every target wraps the C ABI's - handle/`_next`/`_destroy` triple in its native lazy idiom, pulling one - element per consumer step and destroying the handle exactly once. C - exposes the raw symbols directly. -- **Go and Ruby async** wrappers block the calling thread until the - producer's completion callback fires (a channel receive in Go, a - `Queue#pop` in Ruby). Run them from a goroutine or Ruby thread for - concurrency; the native producer still runs off-thread. -- **Thread affinity of callbacks differs.** Dart's `isolateLocal` - callables require the producer to call back synchronously on the - calling thread; Node blocks the producer thread while the JS thread - runs the method and can deadlock if a synchronous call from JS waits - on a worker's callback; Kotlin attaches producer threads to the JVM; - Ruby and Python rely on their FFI's GVL/GIL dispatch; Wasm is - single-threaded, so callbacks fire only while a call into the module - is on the stack ([details](wasm.md#callback-interfaces)). In - [Emscripten mode](wasm.md#emscripten-mode) callback interfaces and - async functions are unsupported and, when allowed, become explicit - throwing stubs rather than silent no-ops. -- **64-bit integers.** Node and Wasm use `bigint`; Dart and Kotlin carry - `u64` in a signed `int`/`Long` bit pattern; the other targets have - native 64-bit types. `NaN`, infinities, and `-0` cross every boundary - unchanged. +- **Objects.** Every wrapper owns one strong reference and keeps itself + alive for the duration of each native call, so it can't be released + mid-call. Disposing twice is a no-op; using a disposed wrapper raises the + target's usage error. +- **Callbacks.** Callback methods may be invoked from any producer thread. + An implementation that throws reaches the original caller as runtime code + `-4`; an object passed to a callback is adopted by the implementation. +- **Async.** A cancelled call raises the language's cancellation error. Go + and Ruby block the calling goroutine or thread while the native work runs + elsewhere. See [Async and Cancellation](../guides/async.md). +- **64-bit integers.** Node.js and WebAssembly use `bigint`; Kotlin and Dart + carry `u64` in a signed 64-bit type. `NaN`, infinities, and `-0` cross + unchanged everywhere. + +## Fixture compile checks + +Snapshot tests pin the generated text; the fixture check proves it's valid +code. `scripts/check-fixtures.sh ` generates every fixture in +`crates/weaveffi-cli/tests/fixtures/` (`kitchen_sink`, `edge_cases`, +`nested_modules`, `docs_everywhere`, `shapes`) together with the C header, +then runs `scripts/fixtures/.sh`, which compiles or type-checks the +output with the target's own toolchain: `cc` and `c++` with `-Werror` for C, +`clang++ -fsyntax-only` for C++, `swiftc -typecheck`, `kotlinc`, `tsc +--strict` and `node --check`, `py_compile` plus a type checker on the stubs, +`dotnet build -warnaserror`, `dart analyze --fatal-infos`, `go vet` and `go +build`, and `ruby -wc`. CI runs one job per target. + +## Conformance lanes + +`conformance/run.sh` is the behavioral gate. It builds the six sample +producers with leak counting enabled, runs `weaveffi generate` on each +sample's `src/lib.rs`, and then runs `conformance//run.sh` for every +language. Each lane (`python-kvstore`, `go-events`, and so on) compiles and +runs one consumer against one sample under a timeout; the consumer binds +only through the generated package, asserts concrete results, and checks +that every `{prefix}_debug_live` counter is zero at exit after forcing its +garbage collector. The C lanes also implement a producer by hand +(`c-producer-exports`) and check each checksum against the header. + +```bash +bash conformance/run.sh # every lane +ONLY=python bash conformance/run.sh # one language +ONLY=c,go-kvstore SKIP_GEN=1 bash conformance/run.sh +``` + +`SKIP` excludes lanes, `SKIP_GEN=1` reuses previously generated bindings, +and `LANE_TIMEOUT` sets the per-lane limit in seconds (default 300). A +missing toolchain fails its lanes; skip them explicitly. CI runs every +language on Linux and macOS. diff --git a/docs/src/generators/c.md b/docs/src/generators/c.md index f137662c..8730c09e 100644 --- a/docs/src/generators/c.md +++ b/docs/src/generators/c.md @@ -1,788 +1,262 @@ # C -## Overview - -The C target emits the canonical C header and a thin reference C file -that every other WeaveFFI target ultimately speaks to. All cross-language -bindings sit on top of these symbols, so the C output is also the easiest -way to inspect what the IDL compiles to. The header is a rendering of the -[C ABI contract](../reference/abi.md), revision 2: reference-counted -objects, consumer-implemented callback-interface vtables, value buffers, -async launchers, and iterators. +The C target emits the canonical header for a library's +[C ABI](../reference/abi.md) (revision 3). Every other target binds to the +symbols it declares, and a C or C++ producer implementing an IDL by hand +implements exactly those symbols. Alongside it, the target emits a helper +header with C structs and codecs for the API's value buffers, so a C +consumer never encodes the wire format by hand. ## What gets generated -| File | Purpose | -|------|---------| -| `generated/c/weaveffi.h` | Public header: enums, object types, vtables, function prototypes, error/memory helpers, and the value-buffer convention comment | -| `generated/c/weaveffi.c` | Default `weaveffi_alloc`/`weaveffi_dealloc` implementations (used by the Wasm JS glue); producers that ship their own allocator can omit it | - -## Type mapping - -| IDL type | C parameter type | C return type | -|--------------|-----------------------------------------|------------------------------------| -| `i32` | `int32_t` | `int32_t` | -| `u32` | `uint32_t` | `uint32_t` | -| `i64` | `int64_t` | `int64_t` | -| `u64` | `uint64_t` | `uint64_t` | -| `i8` | `int8_t` | `int8_t` | -| `i16` | `int16_t` | `int16_t` | -| `u8` | `uint8_t` | `uint8_t` | -| `u16` | `uint16_t` | `uint16_t` | -| `f32` | `float` | `float` | -| `f64` | `double` | `double` | -| `bool` | `bool` | `bool` | -| `string` | `const char*` (NUL-terminated UTF-8, borrowed) | `const char*` (owned; `weaveffi_free_string`) | -| `bytes` | `const uint8_t* ptr, size_t len` (borrowed) | `const uint8_t*` + `size_t* out_len` (owned; `weaveffi_free_bytes`) | -| `Struct` | `const uint8_t* {name}_ptr, size_t {name}_len` (value buffer, borrowed) | `const uint8_t*` + `size_t* out_len` (value buffer, owned) | -| `Interface` | `const weaveffi_m_I*` (borrowed) | `weaveffi_m_I*` (one strong reference, adopted) | -| `Interface?` | `const weaveffi_m_I*` (null is "none") | `weaveffi_m_I*` (null is "none") | -| `CallbackInterface` | `void* {name}_ctx, const weaveffi_m_CI_vtable* {name}_vtable` | not allowed | -| `Enum` (plain) | `weaveffi_m_E` | `weaveffi_m_E` | -| `Enum` (rich) | value buffer, like `Struct` | value buffer, like `Struct` | -| `T?` (other) | value buffer | value buffer | -| `[T]` | value buffer | value buffer | -| `{K:V}` | value buffer | value buffer | -| `iter` | n/a | opaque iterator pointer (see [Iterators](#iterators)) | - -Every buffered type (structs, rich enums, optionals, lists, maps) is one -serialized `(ptr, len)` pair in the -[value-buffer format](../reference/value-buffers.md): borrowed when passed -in, producer-allocated and freed with `weaveffi_free_bytes` when returned. -An object inside a buffer is a `u64` token that carries one strong -reference (see [Objects inside buffers](#objects-inside-buffers)). - -C ABI symbol naming follows a strict convention: - -| Kind | Pattern | Example | -|-------------------|---------------------------------------------------|-----------------------------------------------| -| Function | `weaveffi_{module}_{function}` | `weaveffi_events_route_once` | -| Enum type | `weaveffi_{module}_{Enum}` | `weaveffi_kv_EntryKind` | -| Enum variant | `weaveffi_{module}_{Enum}_{Variant}` | `weaveffi_kv_EntryKind_Volatile` | -| Interface type | `weaveffi_{module}_{Interface}` | `weaveffi_kv_Store` | -| Interface member | `weaveffi_{module}_{Interface}_{member}` | `weaveffi_kv_Store_open` | -| Interface clone | `weaveffi_{module}_{Interface}_clone` | `weaveffi_kv_Store_clone` | -| Interface destroy | `weaveffi_{module}_{Interface}_destroy` | `weaveffi_kv_Store_destroy` | -| Callback vtable | `weaveffi_{module}_{CallbackInterface}_vtable` | `weaveffi_events_Subscriber_vtable` | -| Error enum | `weaveffi_{module}_{Domain}` | `weaveffi_kv_KvError` | -| Error constant | `weaveffi_{module}_{Domain}_{Code}` | `weaveffi_kv_KvError_KeyNotFound` | -| Async callback | `weaveffi_{module}_{function}_callback` | `weaveffi_kv_Store_compact_callback` | -| Async launcher | `weaveffi_{module}_{function}_async` | `weaveffi_kv_Store_compact_async` | -| Iterator type | `weaveffi_{module}_{Function}Iterator` | `weaveffi_kv_Store_ListKeysIterator` | -| Iterator next | `weaveffi_{module}_{Function}Iterator_next` | `weaveffi_kv_Store_ListKeysIterator_next` | -| Iterator destroy | `weaveffi_{module}_{Function}Iterator_destroy` | `weaveffi_kv_Store_ListKeysIterator_destroy` | - -`{Function}` is the function name converted to PascalCase (`list_keys` -becomes `ListKeys`). An iterator returned by an interface method nests -under the interface (`weaveffi_kv_Store_ListKeysIterator`), and so do -async launchers (`weaveffi_kv_Store_compact_async`). Nested modules join -their path with underscores (`weaveffi_kv_stats_get_stats`). - -When the IDL sets `c_prefix`, every symbol, including the runtime helpers, -is rewritten with the new prefix. - -## Example IDL and generated code - -```yaml -version: "0.9.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: name, type: string } - - { name: email, type: "string?" } - - { name: age, type: i32 } - - functions: - - name: create_contact - params: - - { name: first_name, type: string } - - { name: last_name, type: string } - return: Contact - - - name: find_contact - params: - - { name: id, type: "i32?" } - return: "Contact?" - - - name: list_contacts - params: [] - return: "[Contact]" - - - name: count_contacts - params: [] - return: i32 -``` - -The header opens with an include guard, standard headers, an `extern "C"` -block, the ABI revision, and the shared error/memory helpers (taken from the -`kvstore` sample's output): - -```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 2u -WEAVEFFI_API uint32_t weaveffi_abi_version(void); - -/* 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. Positive codes are - the module's declared error codes; negative codes are runtime traps: - -1 generic, -2 producer panic, -3 marshalling failure, -4 a callback - interface implementation raised. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -/* Fill `err` with `code` and a producer-owned copy of `message`. Callback - interface trampolines call this to report a failure in the consumer's - implementation (code -4) without allocating with a foreign allocator. */ -WEAVEFFI_API void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -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); -``` - -A comment block near the top of the header states the value-buffer -convention: a buffered parameter named `v` expands to a borrowed -`const uint8_t* v_ptr, size_t v_len`, and a buffered return is a -producer-allocated buffer returned as `const uint8_t*` with a trailing -`size_t* out_len`, decoded by the caller and released with -`weaveffi_free_bytes`. - -In the real output each prototype is prefixed with a `WEAVEFFI_API` -visibility macro (and deprecated functions with `WEAVEFFI_DEPRECATED`), -omitted from the shorter snippets on this page. See -[Symbol visibility](#symbol-visibility) for what it does and when you need -it. +For the `kvstore` sample (library `kvstore`, prefix `kvstore`): -Structs generate no C symbols of their own: a struct crosses the ABI as a -serialized value buffer, so a function that takes or returns one simply -carries the buffer slots. The consumer packs and unpacks the bytes per the -[value-buffer encoding](../reference/value-buffers.md): - -```c -/* create_contact returns a Contact: a buffered return. */ -const uint8_t* weaveffi_contacts_create_contact( - const char* first_name, - const char* last_name, - size_t* out_len, - weaveffi_error* out_err); +```text +c/ +├── kvstore.h # the ABI: runtime, types, and function prototypes +└── kvstore_buffer.h # C structs and static inline codecs for value buffers ``` -Enums turn into typed `enum` declarations with prefixed variants: +The ABI header is `{library}.h`, with include guard `{PREFIX}_H` and the +visibility macro `{PREFIX}_API`. The helper is `{library}_buffer.h`; it's +emitted only when some type crosses the ABI as a value buffer, and it can be +turned off: -```c -typedef enum { - weaveffi_contacts_ContactType_Personal = 0, - weaveffi_contacts_ContactType_Work = 1, - weaveffi_contacts_ContactType_Other = 2 -} weaveffi_contacts_ContactType; +```toml +[generators.c] +buffer_helpers = false ``` -Optionals and lists are buffered too, so they use the same two-slot shape -(an optional encodes a flag byte, a list a `u32` count, then the elements): - -```c -/* find_contact takes an i32? and returns a Contact?: both buffered. */ -const uint8_t* weaveffi_contacts_find_contact( - const uint8_t* id_ptr, size_t id_len, - size_t* out_len, - weaveffi_error* out_err); - -/* list_contacts returns [Contact]: one buffer holding every record. */ -const uint8_t* weaveffi_contacts_list_contacts( - size_t* out_len, - weaveffi_error* out_err); -``` - -Every synchronous function takes a trailing `weaveffi_error* out_err`. On -failure `out_err->code` is non-zero and `out_err->message` points at a -producer-allocated string the consumer must clear: - -```c -weaveffi_error err = {0}; -int32_t total = weaveffi_contacts_count_contacts(&err); -if (err.code != 0) { - fprintf(stderr, "Error %d: %s\n", err.code, err.message); - weaveffi_error_clear(&err); - return 1; -} -``` - -## Objects (interfaces) - -An `interfaces:` entry lowers to a forward-declared opaque struct, one -prototype per member, and a `_clone`/`_destroy` pair. Constructors are -statics returning a pointer, methods take a leading `const {tag}* self` -argument before their declared parameters, and statics take no `self`. From -the `kvstore` sample's `Store`: - -```c -typedef struct weaveffi_kv_Store weaveffi_kv_Store; - -/* Constructor: returns one strong reference the caller adopts. */ -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_open(const char* path, weaveffi_error* out_err); +`weaveffi package` lays the same headers out under `include/`, adds the +prebuilt library for each platform under `lib//`, and writes a +`CMakeLists.txt` that exposes it as the imported target `kvstore::kvstore`. -/* Static: no self slot. */ -WEAVEFFI_API int64_t weaveffi_kv_Store_default_capacity(weaveffi_error* out_err); +## Build and load -/* Methods: a leading self slot, borrowed for the call. */ -WEAVEFFI_API bool weaveffi_kv_Store_delete(const weaveffi_kv_Store* self, const char* key, weaveffi_error* out_err); -WEAVEFFI_API int64_t weaveffi_kv_Store_count(const weaveffi_kv_Store* self, weaveffi_error* out_err); +Both headers are plain C11 that also compiles as C++. Point the compiler at +the `c/` directory and link the producer's library: -/** Returns a new strong reference to the same object (the pointer value is unchanged). Null is a no-op returning null. */ -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_clone(const weaveffi_kv_Store* self); -/** Releases one strong reference; the object is dropped when the last reference is released. Null is a no-op. */ -WEAVEFFI_API void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self); -``` - -Objects are reference counted by the producer, so the C consumer deals in -strong references rather than owned allocations: - -- Every pointer a constructor, function, iterator step, or async callback - hands you is one strong reference. Release it with `_destroy` exactly - once; the object is dropped when the last reference anywhere goes away. -- `_clone` mints another strong reference to the same object and returns - the same pointer value. Use it whenever you want to store a pointer in - two places, or hand one to a value buffer, and pair each clone with its - own `_destroy`. -- An interface parameter (`const weaveffi_kv_Store* self`, or `store` on - `weaveffi_kv_stats_get_stats`) is borrowed for the duration of the call; - the producer clones it if it needs to retain it. -- Both `_clone` and `_destroy` accept null as a no-op, and a null `self` - on any method is rejected with `MARSHAL_ERROR_CODE` (`-3`) before the - producer runs. - -There is no wrapper type in C, so there's no use-after-close guard: a -pointer that has been passed to `_destroy` is dangling, and it's the -caller's job to null it. The `conformance/c/kvstore.c` consumer shows the -idiom: - -```c -weaveffi_kv_Store* cloned = weaveffi_kv_Store_clone(store); -assert(cloned == store); -weaveffi_kv_Store_destroy(store); -assert(count(cloned) == 2 && "clone outlives the destroyed original"); -store = cloned; -``` - -### Nullable objects - -`Interface?` doesn't use a value buffer: it stays a nullable pointer in -both directions, where null means "none". From `Store.larger(other: -Store?) -> Store?`: - -```c -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_larger(const weaveffi_kv_Store* self, const weaveffi_kv_Store* other, weaveffi_error* out_err); +```sh +cc -std=c11 -I generated/c app.c -L target/release -lkvstore -o app ``` -A non-null return is one strong reference to release, even when the -producer returned one of its inputs back to you (`larger` returns either -`self` or `other`, with a fresh reference either way). +C links the library at build time, so the platform loader finds it at run +time (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`, `PATH`, or an rpath). The +`{PREFIX}_LIBRARY` override (`KVSTORE_LIBRARY`) applies to targets that load +the library dynamically; a C program that wants it calls `dlopen` itself. -### Objects inside buffers - -Inside a value buffer (a record field, a list element, an optional, a map -value, an async result) an object is a `u64` pointer token that carries one -strong reference; whoever decodes the buffer adopts it. That has two -practical consequences: - -- **Reading.** Each token you decode is yours to `_destroy`. Decode a - buffer exactly once; decoding it twice would adopt the same reference - twice. -- **Writing.** Never write a pointer you still hold. Write the result of - `_clone`, since the producer's reader takes that reference: +Every C identifier starts with the prefix, so any number of WeaveFFI +libraries link into one program. Before its first call, a consumer should +check the contract it was generated against: ```c -// Encode a StoreInfo. Object fields are written as fresh strong references -// (`_clone`), since the buffer's reader adopts them. -static void write_store_info(wv_writer* w, const char* label, - weaveffi_kv_Store* store, weaveffi_kv_Store* mirror, - int64_t n) { - wv_put_str(w, label); - wv_put_obj(w, weaveffi_kv_Store_clone(store)); - wv_put_bool(w, mirror != NULL); - if (mirror) wv_put_obj(w, weaveffi_kv_Store_clone(mirror)); - wv_put_i64(w, n); +if (kvstore_abi_version() != KVSTORE_ABI_VERSION || + kvstore_kv_checksum() != KVSTORE_KV_CHECKSUM) { + fprintf(stderr, "kvstore: library does not match kvstore.h (module kv)\n"); + exit(1); } ``` -The `kvstore` sample exercises every position: `describe` returns a -`StoreInfo` record whose `store` field is the receiver itself, `open_many` -returns `[Store]`, and `total_count` takes `[Store]` plus `StoreInfo?`. -Iterator elements and async results that are objects follow the same -rule as returns: one strong reference per element, adopted by the -consumer. - -## Callback interfaces - -A `callback_interfaces:` entry is a set of methods the consumer implements -and the producer calls. It lowers to a vtable `typedef struct` and no -exported symbols. From the `events` sample's `Subscriber`: +There's one `{PREFIX}_{ROOT}_CHECKSUM` constant and one +`{prefix}_{root}_checksum()` function per top-level module. -```c -typedef struct weaveffi_events_Subscriber_vtable { - /** Decide how the bus should treat `topic` for this subscriber. */ - weaveffi_events_Delivery (*route)(void* ctx, const char* topic, weaveffi_error* out_err); - /** - * Receive an accepted message. Returns the subscriber's running count - * of received messages. - */ - int64_t (*on_message)(void* ctx, const uint8_t* message_ptr, size_t message_len, weaveffi_error* out_err); - /** - * Receive the bus itself (an object handed through a callback). The - * consumer adopts the reference and may keep or drop it. - */ - void (*on_attached)(void* ctx, weaveffi_events_EventBus* bus, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_events_Subscriber_vtable; - -WEAVEFFI_API int64_t weaveffi_events_EventBus_subscribe(const weaveffi_events_EventBus* self, void* subscriber_ctx, const weaveffi_events_Subscriber_vtable* subscriber_vtable, weaveffi_error* out_err); -WEAVEFFI_API weaveffi_events_Delivery weaveffi_events_route_once(void* subscriber_ctx, const weaveffi_events_Subscriber_vtable* subscriber_vtable, const char* topic, weaveffi_error* out_err); -``` +## Type mapping -A callback-interface parameter named `subscriber` lowers to two slots, -`void* subscriber_ctx` and `const {tag}_vtable* subscriber_vtable`. The -consumer supplies a heap context per implementation and one static vtable -per callback interface: +| IDL type | Parameter | Return | In `{library}_buffer.h` | +|---|---|---|---| +| `i8` to `u64`, `f32`, `f64` | `int8_t` to `uint64_t`, `float`, `double` | same | same | +| `bool` | `bool` | `bool` | `bool` | +| C-style enum | `{p}_{path}_{Enum}` | same | same | +| `string` | `const uint8_t* s_ptr, size_t s_len` | `const uint8_t*` + `size_t* out_len` | `{p}_str` | +| `bytes` | `const uint8_t* b_ptr, size_t b_len` | `const uint8_t*` + `size_t* out_len` | `{p}_bytes` | +| record, rich enum | value buffer `(ptr, len)` | value buffer + `out_len` | struct `{p}_{path}_{Name}` | +| `[T]`, `{K:V}` | value buffer | value buffer | structs `{p}_list_{T}`, `{p}_map_{K}_{V}` | +| `T?` | value buffer | value buffer | `T*` (`NULL` is absent) | +| interface `I` | `const {p}_{path}_I*` (borrowed) | `{p}_{path}_I*` (owned) | `{p}_{path}_I*` | +| `I?` | `const {p}_{path}_I*` (`NULL` is absent) | `{p}_{path}_I*` | `{p}_{path}_I*` | +| callback interface | `void* x_ctx, const {p}_{path}_{Cb}_vtable* x_vtable` | not allowed | not allowed | +| `iter` | not allowed | iterator handle | not applicable | + +Strings are UTF-8 without a NUL terminator and may contain NUL bytes. A +parameter's pointer may be `NULL` when its length is `0`; a `NULL` pointer +with a nonzero length, or invalid UTF-8, fails the call with code `-3`. +Every returned string, byte string, and value buffer is owned by the +consumer and released with `{p}_free_bytes(ptr, len)`; `NULL` with length +`0` is the empty value. + +## Value buffers and `{library}_buffer.h` + +Records, rich enums, lists, maps, and optionals cross as one serialized +[value buffer](../reference/value-buffers.md). The helper header renders +each record as a struct (from the `kitchen_sink` fixture): ```c -typedef struct { - const char* fail_topic; - int64_t received; - weaveffi_events_EventBus* bus; // kept reference (when keep_bus) - // ... -} sub_ctx; - -static weaveffi_events_Delivery sub_route(void* ctx, const char* topic, - weaveffi_error* out_err) { - sub_ctx* s = (sub_ctx*)ctx; - if (s->fail_topic && strcmp(topic, s->fail_topic) == 0) { - weaveffi_error_set(out_err, -4, "subscriber rejected topic"); - return weaveffi_events_Delivery_Skip; - } - return weaveffi_events_Delivery_Accept; -} - -// The bus arrives as one strong reference the consumer adopts: it is usable -// right here, and it is ours to keep or release. -static void sub_on_attached(void* ctx, weaveffi_events_EventBus* bus, - weaveffi_error* out_err) { - (void)out_err; - sub_ctx* s = (sub_ctx*)ctx; - if (s->keep_bus) { - s->bus = bus; - } else { - weaveffi_events_EventBus_destroy(bus); - } -} - -static void sub_free(void* ctx) { - sub_ctx* s = (sub_ctx*)ctx; - weaveffi_events_EventBus_destroy(s->bus); // null is a no-op - free(s); -} - -static const weaveffi_events_Subscriber_vtable SUB_VTABLE = { - sub_route, - sub_on_message, - sub_on_attached, - sub_free, +struct kitchen_sink_kitchen_Item { + /** Stable identifier */ + int64_t id; + /** Display name */ + kitchen_sink_str name; + /** Initial count */ + int32_t count; + /** Whether the item is enabled */ + bool enabled; + /** Scaling ratio */ + double ratio; + uint32_t nick; + kitchen_sink_bytes payload; + kitchen_sink_list_string tags; + kitchen_sink_map_string_string attrs; + int64_t* parent; + /** Cross-module struct reference */ + kitchen_sink_shared_Token token; + kitchen_sink_kitchen_Priority priority; + /** An optional object inside a record */ + kitchen_sink_kitchen_Gadget* gadget; }; - -sub_ctx* a = calloc(1, sizeof *a); -weaveffi_events_EventBus_subscribe(bus, a, &SUB_VTABLE, &err); ``` -The contract, in full: - -- **Lifetime.** The producer may call any method any number of times, from - any thread, until it calls `free(ctx)` exactly once; after that it never - touches `ctx` again. The producer holds the pair behind its own reference - count, so `free` runs when its last clone drops: for `route_once`, before - the call returns; for `subscribe`, when the bus is cleared or destroyed. -- **Argument ownership.** Strings, bytes, and value buffers passed to a - method (`topic`, `message_ptr`/`message_len`) are borrowed for the call; - copy what you need. An object passed to a method (`bus` above) is one - strong reference the implementation owns: keep it or `_destroy` it. The - vtable slot is `weaveffi_events_EventBus*`, not `const`, to make that - visible. -- **Returns.** Methods return `void` or a Direct-family value (a scalar, - `bool`, or a C-style enum), so nothing is allocated on the way back. -- **Failure.** To report that the implementation failed, call - `weaveffi_error_set(out_err, -4, "message")` and return any value. The - producer copies the message, aborts the current call, and the original - caller observes `FOREIGN_ERROR_CODE` (`-4`) with your text in - `out_err->message`, exactly as it would observe a producer panic. Never - write `out_err->message` yourself; the producer frees it with its own - allocator. -- **Reentrancy.** A method may call back into the producer (the - `on_attached` above calls `subscriber_count` on the bus it was handed). - The samples are written to never hold a lock across a callback; treat - your own state the same way, since the method runs on whatever thread - the producer is on (an `_async` method may call from a worker thread). - -## Error codes - -Every synchronous symbol writes its trailing `out_err`; async and -iterator paths deliver errors as described below. Positive codes are the -module's declared domain codes. The negative range is reserved: - -| Code | Meaning | Where it comes from | -|------|---------|---------------------| -| `-1` | `GENERIC_ERROR_CODE` | an untyped producer error (`Result`) | -| `-2` | `PANIC_ERROR_CODE` | the producer panicked, synchronously or inside a spawned async future; `message` carries the panic text | -| `-3` | `MARSHAL_ERROR_CODE` | an argument could not be lifted: a null `string` or `self` pointer, non-UTF-8 text, an out-of-range enum discriminant, a malformed value buffer | -| `-4` | `FOREIGN_ERROR_CODE` | one of your callback-interface methods called `weaveffi_error_set`; `message` carries your text | - -C is the raw ABI, so throwing and non-throwing callables look identical: -every prototype carries `out_err`, and the consumer checks `err.code` after -each call. What a module's error domain adds is a typed enum naming the -positive codes its `throws: true` callables can report, so consumers match -on names instead of magic numbers. From the `kvstore` sample: +A rich enum is a `tag` (its `{p}_{path}_{Enum}_Tag` constant) plus a union +`as` with one struct per variant that has fields. Each of these types, and +each optional shape, gets four `static inline` functions: ```c -/** Error codes reported by throwing functions in the `kv` module tree. */ -typedef enum { - /** key not found */ - weaveffi_kv_KvError_KeyNotFound = 1001, - /** entry expired */ - weaveffi_kv_KvError_Expired = 1002, - /** store has reached capacity */ - weaveffi_kv_KvError_StoreFull = 1003, - /** I/O failure */ - weaveffi_kv_KvError_IoError = 1004 -} weaveffi_kv_KvError; +static inline void kitchen_sink_kitchen_Item_write(kitchen_sink_writer* w, const kitchen_sink_kitchen_Item* v); +static inline void kitchen_sink_kitchen_Item_read(kitchen_sink_reader* r, kitchen_sink_kitchen_Item* out); +static inline bool kitchen_sink_kitchen_Item_decode(const uint8_t* ptr, size_t len, kitchen_sink_kitchen_Item* out); +static inline void kitchen_sink_kitchen_Item_free(kitchen_sink_kitchen_Item* v); ``` -A callable declared with `throws: true` can set any of these codes; a -callable without `throws` can only fail with the reserved negative codes. -The higher-level targets trap on a negative code from a non-throwing -function; in C the check is yours. See the -[Error Handling guide](../guides/errors.md) for the full picture. - -## 64-bit integers and floats - -`i64`/`u64` cross as `int64_t`/`uint64_t` by value, so the full range is -available with no conversion; the `codec` sample's `roundtrip_u64` returns -`UINT64_MAX` unchanged. `f32`/`f64` cross as `float`/`double`, and NaN, -the infinities, and negative zero survive the trip bit for bit; inside a -value buffer they are written as their IEEE 754 little-endian bytes. - -## Symbol visibility - -Every function prototype is tagged with a `WEAVEFFI_API` macro that the -header defines near the top: +To pass a value, write it into a zeroed `{p}_writer` and pass `ptr` and +`len`. To read a returned buffer, decode it, release the buffer, and free +the decoded value when you're done: ```c -#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 +size_t len = 0; +const uint8_t* buf = kitchen_sink_kitchen_maybe_item(7, &len, &err); +kitchen_sink_kitchen_Item* item = NULL; +if (kitchen_sink_opt_kitchen_Item_decode(buf, len, &item)) { + /* item is NULL when the producer returned none */ +} +kitchen_sink_free_bytes((uint8_t*)buf, len); +kitchen_sink_opt_kitchen_Item_free(&item); ``` -This covers the two ways the header is used: - -- **Consuming** a prebuilt library (the common case) needs nothing extra. - On Windows the prototypes resolve to `__declspec(dllimport)`; everywhere - else the macro is harmless. -- **Implementing** the header (a C, C++, or Zig backend that supplies the - symbols instead of calling them) relies on the macro to stay exportable. - Under hidden default visibility (`-fvisibility=hidden`, the release-build - norm and the MSVC default) an untagged definition is local and ships no - usable symbol. On GCC and Clang the macro applies `visibility("default")`, - so your definitions export with no extra flags. +An optional's `_write` takes the nullable pointer itself. A value you build +for writing may borrow your own memory (`{p}_str_of("text")` borrows a C +string); only values produced by `_read` or `_decode` are passed to `_free`. +`_decode` rejects malformed or trailing bytes, frees what it built, and +returns `false`. -When you implement the header on Windows, compile your library with -`WEAVEFFI_BUILD` defined so the macro switches to `__declspec(dllexport)`: +## Objects and lifetime -```sh -cc -DWEAVEFFI_BUILD -shared mylib.c -o mylib.dll -``` +An interface is an opaque, reference-counted pointer. Parameters borrow it +for the call; a returned pointer carries one strong reference that the +consumer releases with `{p}_{path}_{I}_destroy`. `_clone` returns a new +reference to the same object, and both functions treat `NULL` as a no-op. +Inside a value buffer an object is a token carrying one reference: the +helper's `_write` stores a fresh `_clone`, `_read` adopts the token, and +`_free` destroys it. -Deprecated functions carry a companion `WEAVEFFI_DEPRECATED("...")` macro -that expands to `__declspec(deprecated(...))` on MSVC and -`__attribute__((deprecated(...)))` on GCC and Clang. +## Errors -When the IDL sets `c_prefix`, both macros follow it: a `c_prefix` of `acme` -yields `ACME_API`, `ACME_BUILD`, and `ACME_DEPRECATED`, so two -WeaveFFI-generated libraries can coexist in one translation unit without -colliding. +Every synchronous function takes a trailing `{p}_error* out_err`. On +failure `code` is nonzero and `message` is NUL-terminated UTF-8; release +both with `{p}_error_clear`. Positive codes are the module's error domain +(`kvstore_kv_KvError_KeyNotFound`); negative codes are runtime traps: `-1` +generic, `-2` panic, `-3` marshalling failure, `-4` a callback +implementation failed, and `-5` cancelled. An error code with fields carries +them in `payload_ptr`/`payload_len`, which the helper decodes with +`{code constant}_payload_decode`. -## Rich (algebraic) enums +## Async and cancellation -An enum whose variants declare `fields` is a *rich* (algebraic) enum, a sum -type with associated data. Unlike a plain C-style enum (a bare `int32_t` -discriminant), a rich enum crosses the ABI as a serialized value buffer: an -`i32` tag (the variant's declared discriminant) followed by the active -variant's fields in declaration order. No per-enum C symbols are generated; -the consumer packs and unpacks the bytes. From the `codec` sample (`Shape` -= `Empty | Circle{radius} | Rect{width,height} | Labeled{label,count} | -Nested{inner,note}`), a function taking and returning a `Shape` looks like -any other buffered call: +An async function's symbol is its launcher. It returns at once and later +calls the completion exactly once, from any thread: ```c -/** Return the argument unchanged. */ -WEAVEFFI_API const uint8_t* weaveffi_codec_roundtrip_shape(const uint8_t* value_ptr, size_t value_len, size_t* out_len, weaveffi_error* out_err); -``` - -To build a `Circle{radius: 2.0}`, encode the tag and the payload -little-endian per the -[value-buffer encoding](../reference/value-buffers.md); to read a result, -decode the leading `i32` tag and then the matching variant's fields: - -```c -weaveffi_error err = {0}; - -/* Encode Circle (tag 1) with radius 2.0. */ -uint8_t shape[12]; -int32_t tag = 1; -double radius = 2.0; -memcpy(shape, &tag, 4); -memcpy(shape + 4, &radius, 8); - -size_t out_len = 0; -const uint8_t* back = weaveffi_codec_roundtrip_shape(shape, sizeof shape, &out_len, &err); -/* decode the returned tag and fields from `back` ... */ -weaveffi_free_bytes((uint8_t*)back, out_len); -``` - -The consumer owns every buffer a function returns; release each one with -`weaveffi_free_bytes`. Buffers passed in stay owned by the caller. - -## Build instructions - -The runnable consumers use the sample crates and their conformance -programs; `conformance/c/wvbuf.h` is a small header-only value-buffer -writer and reader you can copy into your own project. - -macOS: - -```bash -cargo build -p events -weaveffi generate samples/events/src/lib.rs -o generated - -cc -I generated/c -I conformance/c conformance/c/events.c -L target/debug -levents -o c_events -DYLD_LIBRARY_PATH=target/debug ./c_events -``` - -Linux: - -```bash -cargo build -p events -weaveffi generate samples/events/src/lib.rs -o generated - -cc -I generated/c -I conformance/c conformance/c/events.c -L target/debug -levents -o c_events -LD_LIBRARY_PATH=target/debug ./c_events -``` - -Windows: - -```powershell -cargo build -p events -weaveffi generate samples\events\src\lib.rs -o generated -cl /I generated\c /I conformance\c conformance\c\events.c /link events.lib -.\events.exe +typedef void (*kitchen_sink_kitchen_do_cancellable_callback)(void* context, kitchen_sink_error* err, const uint8_t* result_ptr, size_t result_len); +KITCHEN_SINK_API void kitchen_sink_kitchen_do_cancellable(const uint8_t* input_ptr, size_t input_len, kitchen_sink_cancel_token* cancel_token, kitchen_sink_kitchen_do_cancellable_callback callback, void* context); ``` -See `conformance/c/` for end-to-end consumers of every sample. +`err` is `NULL` on success; otherwise it's heap-allocated and the consumer +releases it with `{p}_error_free`. A string, bytes, or buffer result is +owned by the consumer (release it with `{p}_free_bytes`), and an object +result carries one reference. Inputs are copied before the launcher +returns. -## Packaging +A `cancellable` function takes a `{p}_cancel_token*` (or `NULL`). Create one +with `{p}_cancel_token_create`, cancel it with `{p}_cancel_token_cancel`, +and release your reference with `{p}_cancel_token_destroy` whenever you +like: the call holds its own reference. Cancelling completes a pending call +with code `-5`, even if it was cancelled before launch. -`weaveffi package --target c` emits the header under `c/include/`, one -prebuilt library per platform under `c/lib//`, and a -`CMakeLists.txt` that picks the library matching the host and exposes it as -an imported target; `add_subdirectory` and link. Every library you pass -with `--binary` is copied, but the generated `CMakeLists.txt` only knows how -to select the desktop platforms (`darwin-arm64`, `darwin-x64`, `linux-x64`, -`linux-arm64`, `windows-x64`); Android and `wasm32` slices, if bundled, need -your own build logic to pick them up. See -[Packaging and Distribution](../guides/packaging.md). - -## Memory and ownership - -Rust always owns memory it allocates. Strings and byte buffers returned -across the boundary must be freed by the consumer with the matching -helper: - -```c -const char* text = weaveffi_codec_roundtrip_string("héllo", &err); -printf("%s\n", text); -weaveffi_free_string(text); - -size_t len; -const uint8_t* data = weaveffi_codec_roundtrip_bytes(in, in_len, &len, &err); -weaveffi_free_bytes((uint8_t*)data, len); -``` - -Returned value buffers (structs, rich enums, optionals, lists, maps) follow -the bytes rule: decode, then free once with `weaveffi_free_bytes(ptr, -out_len)`, and `_destroy` every object token you adopted from it. For -top-level objects, release each strong reference with `_destroy`. Borrowed -parameters (`const T*` objects, `string`/`bytes` inputs, buffered `(ptr, -len)` pairs) remain owned by the caller for the duration of the call only. - -## Async support +## Callback interfaces -Async functions (`async: true`) get no synchronous prototype. Each one -emits a per-function callback typedef, `(void* context, weaveffi_error* -err, )`, and a launcher with the `_async` suffix. From the -`events` sample's `EventBus.publish_later`: +A callback interface is a vtable the consumer fills in, passed with a +context pointer: ```c -typedef void (*weaveffi_events_EventBus_publish_later_callback)(void* context, weaveffi_error* err, int64_t result); - -/** Publish from a producer thread, resolving with the accepted count. */ -WEAVEFFI_API void weaveffi_events_EventBus_publish_later_async(const weaveffi_events_EventBus* self, const char* topic, const char* text, weaveffi_events_EventBus_publish_later_callback callback, void* context); +typedef struct kitchen_sink_kitchen_ReadyListener_vtable { + /** Fires when an item is ready */ + void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); + /** Receives the item itself and says whether to keep listening */ + bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); + void (*free)(void* ctx); +} kitchen_sink_kitchen_ReadyListener_vtable; ``` -The launcher returns immediately; the producer runs the future on its -configured spawner (`weaveffi::set_spawner`; the default drives each future -on a dedicated thread) and invokes the callback exactly once, from an -arbitrary producer thread, with either a result or a populated error. The -launcher clones `self` for the duration of the call, so destroying your -own reference while the call is pending is safe. - -Ownership inside the callback follows the async contract: everything the -callback receives is owned by the consumer. Copy or decode result buffers -(strings, bytes, and the serialized value buffers of buffered results), -then release them with `weaveffi_free_string` or `weaveffi_free_bytes`. An -object result is one strong reference to `_destroy`. A non-null `err` is -heap-boxed: copy its code, message, and payload, then release it exactly -once with `weaveffi_error_free` (null is a safe no-op, so the -`conformance/c/events.c` consumer calls it unconditionally): - -```c -static void on_publish_later(void* context, weaveffi_error* err, int64_t result) { - g_later_err = err ? err->code : 0; - weaveffi_error_free(err); - g_later_result = result; - atomic_store(&g_later_done, 1); -} -``` +The producer may call any entry from any thread, so the context must be +thread-safe. String and buffer arguments are borrowed for the call; an +object argument carries one reference the method adopts. A method reports +failure with `{p}_error_set(out_err, -4, "message")`. The producer calls +`free(ctx)` exactly once, when it drops its last reference. -A panic inside the spawned future is caught (`weaveffi::abi::CatchUnwind`) -and reported through `err` with `PANIC_ERROR_CODE`, so the callback still -fires exactly once; a callback-interface failure inside the future arrives -as `FOREIGN_ERROR_CODE` the same way. +## Iterators -For `cancellable: true` functions the launcher gains a -`weaveffi_cancel_token*` slot before the callback, and the runtime -provides the token lifecycle. From the `kvstore` sample's `Store.compact`: +A function returning `iter` returns an iterator handle. `_next` writes +one item and returns `1`, or returns `0` when it's done; `_destroy` +releases the handle, even mid-stream: ```c -typedef void (*weaveffi_kv_Store_compact_callback)(void* context, weaveffi_error* err, int64_t result); - -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); - -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); +KITCHEN_SINK_API int32_t kitchen_sink_kitchen_StreamItemsIterator_next(kitchen_sink_kitchen_StreamItemsIterator* iter, const uint8_t** out_item, size_t* out_len, kitchen_sink_error* out_err); +KITCHEN_SINK_API void kitchen_sink_kitchen_StreamItemsIterator_destroy(kitchen_sink_kitchen_StreamItemsIterator* iter); ``` -See [Async functions](../guides/async.md) for the full pattern. +String, bytes, and buffer items are owned (release each with +`{p}_free_bytes`); object items carry one reference. -## Iterators +## Implementing an IDL in C -Functions returning `iter` produce an opaque iterator pointer plus -`_next`/`_destroy` functions instead of a materialized list. From the -`events` sample (`EventBus.messages` returns `iter`): +A hand-written producer defines every function the header declares, +including the runtime and one checksum per top-level module. For the +calculator sample that's: ```c -typedef struct weaveffi_events_EventBus_MessagesIterator weaveffi_events_EventBus_MessagesIterator; - -WEAVEFFI_API weaveffi_events_EventBus_MessagesIterator* weaveffi_events_EventBus_messages(const weaveffi_events_EventBus* self, weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_events_EventBus_MessagesIterator_next(weaveffi_events_EventBus_MessagesIterator* iter, const char** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_events_EventBus_MessagesIterator_destroy(weaveffi_events_EventBus_MessagesIterator* iter); +uint32_t calculator_abi_version(void) { return CALCULATOR_ABI_VERSION; } +uint64_t calculator_calculator_checksum(void) { return CALCULATOR_CALCULATOR_CHECKSUM; } + +void calculator_error_set(calculator_error* err, int32_t code, const char* message); +void calculator_error_clear(calculator_error* err); +void calculator_error_free(calculator_error* err); +void calculator_free_bytes(uint8_t* ptr, size_t len); +calculator_cancel_token* calculator_cancel_token_create(void); +void calculator_cancel_token_cancel(calculator_cancel_token* token); +bool calculator_cancel_token_is_cancelled(const calculator_cancel_token* token); +void calculator_cancel_token_destroy(calculator_cancel_token* token); +uint64_t calculator_debug_live(int32_t kind); /* may return 0 */ ``` -`_next` writes the next element into the out-param and returns `1`, or -returns `0` when exhausted (leaving `*out_item` untouched). Failures are -reported through `out_err`, so check it after the loop ends. Element -ownership follows the usual return rules; each `next` hands over an element -the consumer now owns, so here each `const char*` must be freed with -`weaveffi_free_string`. Call `_destroy` exactly once when done, even if -iteration stopped early: - -```c -weaveffi_error err = {0}; -weaveffi_events_EventBus_MessagesIterator* it = weaveffi_events_EventBus_messages(bus, &err); -const char* item = NULL; -while (weaveffi_events_EventBus_MessagesIterator_next(it, &item, &err) != 0) { - printf("%s\n", item); - weaveffi_free_string(item); -} -if (err.code != 0) { /* a failing step ended the loop */ } -weaveffi_events_EventBus_MessagesIterator_destroy(it); -``` - -An iterator over a buffered element type writes each element as a -producer-allocated value buffer through `const uint8_t** out_item` plus a -`size_t* out_len`; decode it, then free it with `weaveffi_free_bytes` per -element. An iterator over objects writes a `{tag}**` slot holding one -strong reference per element, which you `_destroy` when done. - -The higher-level targets wrap exactly these three symbols in their native -lazy idioms; only the C surface exposes them raw. +`{p}_alloc` and `{p}_dealloc` are needed only on wasm32. Every prototype +carries `{PREFIX}_API`, so the definitions stay exported under +`-fvisibility=hidden`; on Windows, define `{PREFIX}_BUILD` while building +the producer. `conformance/c/producer.c` is a complete example. ## Known limitations -- C has no wrapper type, so nothing stops you from using a pointer after - `_destroy` or decoding a buffer with object tokens twice. Both are - undefined behavior; the other targets guard against them. -- There is no generated value-buffer codec for C. You write the encoder and - decoder yourself (or start from `conformance/c/wvbuf.h`). -- Async completion runs on a producer thread with no marshalling back to - the caller; you synchronize (the conformance consumers use C11 atomics). -- Callback-interface methods run on the producer's thread and the vtable - entries must be thread-safe; nothing serializes them for you. - -## Troubleshooting - -- **`undefined reference to weaveffi_*`**: make sure the linker sees the - cdylib (`-L target/debug -l`). The header alone is not - enough. -- **Crashes inside `weaveffi_free_string`**: the pointer wasn't - producer-allocated. Only free pointers returned from a generated - function, iterator step, or async callback. -- **`err.code == -3` on a method call**: the `self` pointer was null, a - `string` parameter was null or not UTF-8, or a value buffer you encoded - is malformed. Check the buffer layout against the - [protocol](../reference/value-buffers.md). -- **`err.code == -4` from a function you didn't expect to fail**: one of - your vtable methods reported a foreign error. The message is yours. -- **`weaveffi.c` looks nearly empty**: that file only carries the default - `weaveffi_alloc`/`weaveffi_dealloc` implementations for Wasm producers. - All declarations live in `weaveffi.h`. +- The C target emits declarations and helpers only; there's no runtime + loader, so `{PREFIX}_LIBRARY` isn't consulted. +- The helper's decoders don't validate UTF-8 in decoded strings; the + producer has already done so. +- Composite helper names are derived from the type (`{p}_list_kv_Entry`, + `{p}_map_string_i64`), so a module or function named like one of them + can collide with it. diff --git a/docs/src/generators/cpp.md b/docs/src/generators/cpp.md index b5fb004f..89de16bd 100644 --- a/docs/src/generators/cpp.md +++ b/docs/src/generators/cpp.md @@ -1,671 +1,229 @@ # C++ -## Overview - -The C++ target emits a header-only library `weaveffi.hpp` that wraps the -C ABI in idiomatic C++17. Structs become plain value structs, rich enums -become `std::variant`-backed sum types, interfaces become copyable RAII -classes holding one strong reference to a reference-counted producer -object, callback interfaces become abstract classes you subclass and pass -as `std::shared_ptr`, error domains map to typed exception hierarchies, -async functions return `std::future`, and `iter` returns a lazy range. -The header inlines the same `extern "C"` declarations the C target emits, -so it's self-contained. A `CMakeLists.txt` is included so the generated -directory can be dropped into any CMake build. - -The generated surface follows ABI revision 2 (`WEAVEFFI_ABI_VERSION`). -Call `weaveffi::check_abi_version()` once at startup; it throws -`WeaveFFIError` if the loaded library was built for a different revision. +The C++ target emits a header-only C++17 library over the C ABI. Records +become value structs, rich enums become `std::variant`-backed sum types, +interfaces become copyable RAII classes, callback interfaces become abstract +classes you subclass, error domains become exception hierarchies, async +functions return `std::future`, and `iter` returns a lazy range. ## What gets generated -| File | Purpose | -|------|---------| -| `generated/cpp/weaveffi.hpp` | Header-only bindings: extern "C" declarations, RAII wrappers, abstract callback classes, enum classes, inline function wrappers | -| `generated/cpp/CMakeLists.txt` | INTERFACE library target (`weaveffi_cpp`) | -| `generated/cpp/README.md` | Build instructions | - -## Type mapping - -| IDL type | C++ type | Passed as parameter | -|--------------|--------------------------------------|-----------------------------| -| `i32` | `int32_t` | `int32_t` | -| `u32` | `uint32_t` | `uint32_t` | -| `i64` | `int64_t` | `int64_t` | -| `u64` | `uint64_t` | `uint64_t` | -| `i8` | `int8_t` | `int8_t` | -| `i16` | `int16_t` | `int16_t` | -| `u8` | `uint8_t` | `uint8_t` | -| `u16` | `uint16_t` | `uint16_t` | -| `f32` | `float` | `float` | -| `f64` | `double` | `double` | -| `bool` | `bool` | `bool` | -| `string` | `std::string` | `const std::string&` | -| `bytes` | `std::vector` | `const std::vector&` | -| `StructName` | `StructName` (value struct) | `const StructName&` | -| `InterfaceName` | `InterfaceName` (RAII class) | `const InterfaceName&` (borrowed) | -| `InterfaceName?` | `std::optional` | `const std::optional&` | -| `CallbackName` | abstract class `CallbackName` | `std::shared_ptr` | -| `EnumName` (plain) | `EnumName` (`enum class`) | `EnumName` | -| `EnumName` (rich) | `EnumName` (`std::variant`-backed sum type) | `const EnumName&` | -| `T?` | `std::optional` | `const std::optional&` | -| `[T]` | `std::vector` | `const std::vector&` | -| `{K: V}` | `std::unordered_map` | `const std::unordered_map&` | -| `iter` | generated lazy range class (return only; see [Iterators](#iterators)) | n/a | - -64-bit integers are native `int64_t`/`uint64_t`, and `float`/`double` -cross the ABI as IEEE values, so NaN, the infinities, and `-0.0` round-trip -bit-for-bit (the `codec` sample's `roundtrip_u64` and `roundtrip_f64` -exercise this). - -## Example IDL → generated code - -```yaml -version: "0.9.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: name, type: string } - - { name: email, type: "string?" } - - { name: age, type: i32 } - - { name: contact_type, type: ContactType } - - functions: - - name: create_contact - params: - - { name: name, type: string } - - { name: email, type: "string?" } - - { name: age, type: i32 } - return: Contact - - - name: find_contact - params: - - { name: id, type: i32 } - return: "Contact?" - - - name: list_contacts - params: [] - return: "[Contact]" - - - name: count_contacts - params: [] - return: i32 - - - name: fetch_contact - async: true - params: - - { name: id, type: i32 } - return: Contact -``` - -Enums become `enum class`: - -```cpp -enum class ContactType : int32_t { - Personal = 0, - Work = 1, - Other = 2 -}; +```text +cpp/ +├── {library}.h the C header (identical to the c target's) +├── {library}.hpp the wrapper; includes {library}.h +├── CMakeLists.txt INTERFACE target {library}::cpp +└── README.md ``` -Structs become plain value structs with typed members: - -```cpp -struct Contact { - std::string name; - std::optional email; - int32_t age; - ContactType contact_type; -}; -``` +For the kvstore sample that's `kvstore.h`, `kvstore.hpp`, `CMakeLists.txt`, +and `README.md`. Everything in the wrapper lives in `namespace {prefix}` +(`kvstore`), and each IDL module's free functions live in a nested namespace +named after the module path (`kvstore::kv::stats::get_stats`). Types sit at +the namespace root, since type names are unique across an API. -There are no C symbols behind a struct. A `Contact` crosses the ABI -serialized in the [value-buffer format](../reference/value-buffers.md) as a -single `(const uint8_t*, size_t)` pair; the header carries a small private -buffer reader and writer in the `detail` namespace plus one generated pack -and unpack routine per type. +| `[cpp]` key | Default | Meaning | +|---|---|---| +| `namespace` | the C prefix | Namespace holding every declaration | +| `header_name` | `{library}.hpp` | Wrapper header file name | +| `standard` | `17` | C++ standard the CMake target requires | -Free functions live in a nested namespace per module inside the outer -`weaveffi` namespace (configurable via `namespace`), keeping their -snake_case IDL names with no module prefix, and throw on failure: +## Build and link -```cpp -namespace weaveffi { -namespace contacts { - -inline Contact create_contact( - const std::string& name, - const std::optional& email, - int32_t age) -{ - weaveffi_error err{}; - // Optionals are buffered: pack the argument into a value buffer. - std::vector email_buf = /* generated pack routine */; - size_t out_len = 0; - const uint8_t* raw = weaveffi_contacts_create_contact( - name.c_str(), - email_buf.data(), email_buf.size(), - age, &out_len, &err); - detail::check(err); - Contact ret = /* generated unpack routine over (raw, out_len) */; - weaveffi_free_bytes(raw, out_len); - return ret; -} +Add the generated directory to a CMake build and link the INTERFACE target: -} // namespace contacts -} // namespace weaveffi +```cmake +add_subdirectory(path/to/cpp) +target_link_libraries(app PRIVATE kvstore::cpp) ``` -Call it as `weaveffi::contacts::create_contact(...)`. Nested IDL modules -nest namespaces the same way (`weaveffi::kv::stats::get_stats`). - -## Objects (interfaces) - -An `interfaces:` entry becomes a copyable RAII class that holds exactly one -strong reference to a reference-counted object owned by the producer. -Constructors become static factories (or a real C++ constructor when the -IDL constructor is named `new`, as `EventBus()` in the `events` sample), -methods are instance members, and statics are static members. From the -`kvstore` sample's `Store`: +The target adds the directory to the include path, requires C++17, and links +the producer library. Set the `KVSTORE_LIBRARY` CMake cache variable or +environment variable (in general `{PREFIX}_LIBRARY`) to a full path to link +that file; otherwise the target links `kvstore` by name, which resolves to a +CMake target of that name or `libkvstore` on the linker search path. Without +CMake, add `cpp/` to the include path and link `-lkvstore`. -```cpp -class Store { - weaveffi_kv_Store* handle_; +Both headers can be included in one translation unit, in either order. -public: - /** Adopts one strong reference to a producer object. */ - explicit Store(weaveffi_kv_Store* h) : handle_(h) {} - - /** Releases this wrapper's reference; the object is dropped with its last one. */ - ~Store() { - if (handle_) weaveffi_kv_Store_destroy(handle_); - } - - /** Copies share the object: the copy takes a new strong reference. */ - Store(const Store& other) : handle_(weaveffi_kv_Store_clone(other.handle_)) {} - - Store(Store&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - - /** The wrapped pointer, borrowed: this wrapper keeps its reference. */ - const weaveffi_kv_Store* handle() const { return handle_; } - - /** A new strong reference the caller owns (for example to write into a value buffer). */ - weaveffi_kv_Store* clone_handle() const { return weaveffi_kv_Store_clone(handle_); } - - static Store open(const std::string& path); - bool delete_(const std::string& key) const; - int64_t count() const; - StoreListKeysIterator list_keys(const std::optional& prefix) const; - std::future compact(weaveffi_cancel_token* cancel_token = nullptr) const; - static int64_t default_capacity(); -}; -``` +### The library check -- **Disposal is RAII.** The destructor calls `_destroy` once, releasing - this wrapper's reference; the producer drops the object when the last - reference (from any wrapper, any record field, or the producer's own - retention) goes away. There is no public `destroy()` or `close()`. -- **Copies mint a new strong reference.** The copy constructor and copy - assignment call `_clone`, so `Store copy = store;` yields a second wrapper - over the *same* object (`copy.handle() == store.handle()`); a mutation - through one is visible through the other, and each destructor releases - its own reference. Moves transfer the pointer and leave the source empty. -- **Use after move.** A moved-from wrapper has `handle() == nullptr`. Its - destructor is a no-op, but calling a method on it passes a null object to - the producer, which rejects the call with `MARSHAL_ERROR_CODE` (-3) as a - `WeaveFFIError`. Don't reuse a moved-from wrapper. -- `handle()` borrows the raw pointer for the duration of a call; - `clone_handle()` returns a fresh strong reference you must eventually - pass to `_destroy` (or adopt into another wrapper). - -Method names keep their snake_case IDL spelling; a name that collides with -a C++ keyword gains a trailing underscore (`delete` → `delete_`). -Deprecated members carry `[[deprecated("...")]]`. - -### Objects as parameters, returns, and inside values - -A top-level object parameter is passed as `const Store&` and borrowed for -the call. A returned object is adopted into a new wrapper. `Store?` maps to -`std::optional`: a disengaged optional passes a null pointer, and a -null return becomes `std::nullopt`: +`check_library()` verifies that the linked library implements C ABI revision +3 and the exact contract of every top-level module, and throws `LoadError` +naming the first mismatch: ```cpp -inline std::optional Store::larger(const std::optional& other) const { - weaveffi_error err{}; - auto result = weaveffi_kv_Store_larger(handle_, other.has_value() ? other->handle() : nullptr, &err); - detail::check(err); - if (!result) return std::nullopt; - return Store(result); -} +inline void check_library() { + static const std::string failure = [] { + uint32_t abi = kitchen_sink_abi_version(); + if (abi != KITCHEN_SINK_ABI_VERSION) { + return "kitchen_sink: C ABI revision mismatch (header " + std::to_string(KITCHEN_SINK_ABI_VERSION) + + ", library " + std::to_string(abi) + ")"; + } + if (kitchen_sink_shared_checksum() != 0x42c4ce2c8d0af052ull) { + return std::string("kitchen_sink: module 'shared' does not match the linked library (contract checksum mismatch); regenerate the bindings for this build"); + } + ... ``` -Objects inside records, lists, map values, optionals, and rich-enum -payloads are held by value (`Store store; std::optional mirror;` in -`StoreInfo`). On the wire they're `u64` tokens: the generated pack routine -calls `clone_handle()` for each one, so the producer receives its own -reference and your wrapper stays valid, and the unpack routine adopts the -token into a wrapper: +A C++ binary links its library rather than loading it, and an exception +thrown during static initialization can't be caught, so the check runs on +first use instead: every free function, constructor, and static member calls +`check_library()` before its first native call. The result is cached, so the +cost after the first call is one guard-variable read. Call it at startup to +fail early. -```cpp -inline void write_StoreInfo(BufferWriter& w, const StoreInfo& v) { - w.write_string(v.label); - w.write_u64(static_cast(reinterpret_cast(v.store.clone_handle()))); - w.write_option_flag(v.mirror.has_value()); - if (v.mirror.has_value()) { - w.write_u64(static_cast(reinterpret_cast((*v.mirror).clone_handle()))); - } - w.write_i64(v.count); -} +## Type mapping -inline StoreInfo read_StoreInfo(BufferReader& r) { - std::string f_label = r.read_string(); - Store f_store = Store(reinterpret_cast(static_cast(r.read_u64()))); - // ... -} -``` +| IDL type | C++ type | Parameter | +|---|---|---| +| `i8` ... `u64`, `f32`, `f64`, `bool` | `int8_t` ... `uint64_t`, `float`, `double`, `bool` | by value | +| `string` | `std::string` | `std::string_view` | +| `bytes` | `std::vector` | `const std::vector&` | +| C-style enum | `enum class E : int32_t` | by value | +| record | `struct R` | `const R&` | +| rich enum | `struct E` with `std::variant<...> value` | `const E&` | +| interface | RAII class | `const I&` (borrowed) | +| `I?` | `std::optional` | `const std::optional&` | +| callback interface | abstract class | `std::shared_ptr` | +| `T?`, `[T]`, `{K: V}` | `std::optional`, `std::vector`, `std::unordered_map` | by const reference | +| `iter` | lazy range class (returns only) | n/a | -`Store::open_many` returns `std::vector` (one adopted wrapper per -element), and `Store::total_count(const std::vector&, const std::optional&)` -clones every object it encodes. Copying a record that contains an object -copies the wrapper, which clones the reference. +Strings cross the ABI as a UTF-8 pointer and a length, so interior NULs +survive in both directions. A returned string is copied into a `std::string` +and the producer's allocation is released with `{prefix}_free_bytes`. +Records, rich enums, optionals, lists, and maps cross as one value buffer, +encoded and decoded by generated routines in `detail`. -## Typed errors +## Objects and lifetime -`WeaveFFIError` extends `std::runtime_error` and carries the raw `code()`. -A module's error domain generates a typed hierarchy: one class named after -the domain, plus one subclass per declared code, each named in PascalCase -with exactly one `Error` suffix. From the `kvstore` sample's `KvError` -domain: +An interface class holds one strong reference to a reference-counted +producer object: ```cpp -class KvError : public WeaveFFIError { -public: - KvError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; +class Gadget { + kitchen_sink_kitchen_Gadget* handle_; -/** key not found */ -class KeyNotFoundError : public KvError { public: - KeyNotFoundError(const std::string& msg) : KvError(1001, msg) {} -}; -``` + /** The C type this class wraps. */ + using raw_type = kitchen_sink_kitchen_Gadget; -A callable declared with `throws: true` routes its failure through a -per-domain checker (`detail::check_kv`) that throws the most specific -subclass, so you can catch a single code, the domain, or the generic base: - -```cpp -try { - auto entry = store.get("missing"); -} catch (const weaveffi::KeyNotFoundError& e) { - std::cerr << "Not found: " << e.what() << '\n'; -} catch (const weaveffi::KvError& e) { - std::cerr << "kv error " << e.code() << ": " << e.what() << '\n'; -} + /** Adopts one strong reference to a producer object: `Gadget(adopt, raw)`. */ + explicit Gadget(adopt_t, kitchen_sink_kitchen_Gadget* h) noexcept : handle_(h) {} ``` -An error code that declares payload `fields:` exposes them as typed members -on its subclass, decoded from the error's payload buffer before -`weaveffi_error_clear` releases it. An unknown positive code on the typed -path falls back to the domain class itself (`KvError`). - -### Runtime error codes +- The destructor releases the reference with `_destroy`; copying takes a new + one with `_clone`, so copies share the object; moving transfers it. +- The constructor named `new` becomes an `explicit` C++ constructor; any + other constructor becomes a static factory. Nothing converts implicitly + into an object, and adopting a raw pointer requires the `adopt` tag. +- A parameter borrows the caller's reference for the call. A returned + object, an async result, an iterator element, and an object argument + handed to a callback are adopted. +- An object inside a record, list, map, or optional crosses as a token + minted with `_clone`, so a record can be copied and destroyed freely. +- `handle()` borrows the C pointer; `clone_handle()` returns a new reference + the caller owns. -Domain codes are validated positive-only, so a negative runtime code always -surfaces as the generic `WeaveFFIError`, never a typed domain exception. -That's also why no wrapper is `noexcept`: a callable without `throws` has -the same C++ signature and still checks `out_err` through `detail::check`. +The C++ object model makes lifetimes the caller's responsibility: a wrapper +must outlive the calls made through it, and destroying a wrapper on one +thread while another thread calls through it is a data race, as for any C++ +object. A moved-from wrapper holds null. -The header doesn't name these codes; the names below are the ABI's (see -[ABI](../reference/abi.md)). +## Errors -| Code | ABI name | When | -|------|----------|------| -| -1 | `GENERIC_ERROR_CODE` | The producer reported a failure with no domain code (also what `check_abi_version()` throws) | -| -2 | `PANIC_ERROR_CODE` | The Rust producer panicked inside an export or a spawned async future | -| -3 | `MARSHAL_ERROR_CODE` | A null object pointer or a malformed value buffer or string was rejected at the boundary | -| -4 | `FOREIGN_ERROR_CODE` | A callback-interface implementation threw | +Every failure is an exception derived from `{namespace}::Error`, which +carries `code()`: -Sync callables throw from the call; async callables reject the future so -`.get()` rethrows; iterator steps throw from `next()` (after releasing the -producer iterator). - -## Callback interfaces +- Each module that declares an error domain gets a domain class + (`KvError : Error`) and one subclass per code (`KeyNotFoundError`). A code + with payload fields exposes them as members. +- A throwing function maps a positive code to its typed exception. Negative + codes are runtime traps and always surface as the generic `Error`: -1 + generic, -2 producer panic, -3 marshalling failure, and -4 a callback + implementation that threw. +- -5 surfaces as `Cancelled`. +- `LoadError` reports a library that doesn't match the headers. -A `callback_interfaces:` entry becomes an abstract class with one pure -virtual method per IDL method. Subclass it and pass a -`std::shared_ptr` to any function that accepts the interface. From -the `events` sample: +Wrappers are never `noexcept`, since even a non-throwing function can +surface a trap. -```cpp -class Subscriber { -public: - virtual ~Subscriber() = default; +## Async and cancellation - virtual Delivery route(const std::string& topic) = 0; - virtual int64_t on_message(const Message& message) = 0; - virtual void on_attached(EventBus bus) = 0; -}; -``` +An async function returns `std::future`, settled from a producer thread. +A `cancellable` function also takes a trailing `const CancelToken&`: ```cpp -class RecordingSubscriber : public Subscriber { - std::optional kept_bus_; - -public: - Delivery route(const std::string& topic) override { - return topic == "quiet" ? Delivery::Skip : Delivery::Accept; - } - int64_t on_message(const Message& message) override { - std::cout << message.topic << ": " << message.text << '\n'; - return 1; - } - void on_attached(EventBus bus) override { - kept_bus_ = std::move(bus); // or let it drop to release the reference - } -}; - -weaveffi::EventBus bus; -bus.subscribe(std::make_shared()); +inline std::future do_cancellable(std::string_view input, const CancelToken& cancel_token = CancelToken::none()) { ``` -Under the hood the wrapper boxes your `shared_ptr` on the heap as the -vtable `ctx`, hands the producer the one process-wide static vtable for the -interface, and the vtable's `free` deletes the box when the producer drops -its last reference (which runs your destructor): +`CancelToken` is a move-only RAII wrapper over the native token. Pass one +token to any number of calls and call `cancel()` from any thread; every call +still in flight completes with `Cancelled`. Each call takes its own native +reference, so the token can be destroyed at any time without affecting the +calls. Omitting the argument passes `CancelToken::none()`, which never +cancels. ```cpp -inline int64_t EventBus::subscribe(std::shared_ptr subscriber) const { - if (!subscriber) throw std::invalid_argument("subscriber: null callback interface"); - auto* subscriber_ctx = new std::shared_ptr(std::move(subscriber)); - // ... weaveffi_events_EventBus_subscribe(handle_, subscriber_ctx, &detail::Subscriber_vtable(), &err) -} -``` - -- **Argument ownership.** Strings and buffered values (`const Message&`) - are decoded into temporaries that live for the duration of the call; - copy them if you need them afterward. An object argument - (`EventBus bus`) is passed *by value* and is owned by your - implementation: keep it (move it into a member) or let it go out of - scope to release the reference. -- **Lifetime.** The producer may keep the implementation as long as it - likes (`subscribe` retains it until `clear_subscribers`); a function that - only uses it for the call (`route_once`) frees it before returning. - Passing a null `shared_ptr` throws `std::invalid_argument` before - anything crosses the ABI. -- **Exceptions.** Trampolines never let an exception unwind through the C - frame. Anything derived from `std::exception` is reported with - `weaveffi_error_set(out_err, -4, e.what())`; anything else gets a - generic message. The producer aborts the call that triggered the - callback, and the caller sees `WeaveFFIError` with `code() == -4` - carrying your `what()`, for a `throws` and a non-`throws` callable alike, - and through the future of an async callable. The implementation stays - attached. -- **Threads.** Methods run on whichever thread the producer calls from: the - calling thread for a synchronous call, a producer worker for an async - one (`publish_later` in the sample). Synchronize shared state yourself; - the wrapper doesn't marshal calls onto any particular thread. - -## Rich (algebraic) enums - -An enum whose variants declare `fields` is a *rich* (algebraic) enum, a sum -type with associated data. Plain C-style enums stay `enum class`; a rich enum -instead becomes a `std::variant`-backed sum type: one plain payload struct per -variant carrying that variant's fields, aggregated into a variant type named -after the enum. From the `shapes` sample, the surface follows this shape: - -```cpp -namespace weaveffi { - -struct ShapeEmpty {}; -struct ShapeCircle { double radius; }; -struct ShapeRectangle { float width; float height; }; -struct ShapeLabeled { std::string label; uint8_t count; }; - -using Shape = std::variant; - -} // namespace weaveffi -``` - -Values are plain data. Build the payload struct you need and inspect results -with `std::holds_alternative`, `std::get`, or `std::visit`: - -```cpp -weaveffi::Shape shape = weaveffi::ShapeCircle{2.0}; - -if (auto* c = std::get_if(&shape)) { - std::cout << "radius = " << c->radius << '\n'; +kvstore::CancelToken token; +std::future pending = store.compact(token); +token.cancel(); +try { + pending.get(); +} catch (const kvstore::Cancelled&) { + // The runtime dropped the work. } - -std::cout << weaveffi::shapes::describe(shape) << '\n'; -weaveffi::Shape bigger = weaveffi::shapes::scale(shape, 3.0); ``` -On the wire a rich enum is a value buffer holding an `i32` tag followed by -the active variant's fields; there are no per-variant C constructors, tag -readers, or destroy symbols. The wrappers pack and unpack the buffer, so no -manual free is required. A variant payload may hold an object; it follows -the token rules above. - -## Build instructions - -The generated `CMakeLists.txt` defines an INTERFACE library (the -project version mirrors `package.version` from the IDL): - -```cmake -cmake_minimum_required(VERSION 3.14) -project(weaveffi_cpp VERSION 1.0.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) -``` +`std::future` has no cancellation of its own, so destroying a future doesn't +cancel the call; cancel its token instead. -Consume it from your project: - -```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) -``` +## Callback interfaces -Then `#include "weaveffi.hpp"` and link against the Rust shared library -(`libweaveffi.dylib`, `libweaveffi.so`, or `weaveffi.dll`). The header -needs `-std=c++17` (or `cxx_std_17`); `std::optional`, `std::variant`, and -`std::future` come from the standard library, so no other dependency is -required. - -## Packaging - -`weaveffi package --target cpp` emits the header under `cpp/include/`, -one prebuilt library per desktop platform under `cpp/lib//`, and -a `CMakeLists.txt` that selects the library matching the host and links it -into the `weaveffi_cpp` interface target. Only desktop slices are bundled -(`darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `windows-x64`); -Android and `wasm32` binaries are skipped. See -[Packaging and Distribution](../guides/packaging.md). - -## Memory and ownership - -- Interface wrappers own one strong reference. The destructor releases it; - copies clone it; moves transfer it. Nothing else to do. -- Structs, rich enums, optionals, lists, and maps are plain C++ values. - They cross the ABI serialized in a single value buffer: parameters are - packed by the wrapper and borrowed by the callee for the duration of - the call, and returns are unpacked into the C++ value and the - producer's buffer is released with `weaveffi_free_bytes`. Object fields - inside those values are cloned on the way in and adopted on the way out. -- Returned strings are copied into `std::string` and the raw pointer is - freed via `weaveffi_free_string` before returning. -- Callback-interface implementations are owned jointly by your - `shared_ptr` and the producer's box; the producer's `free` releases its - share. - -## Async support - -Async IDL functions return `std::future`. The wrapper allocates a -heap-owned `std::promise`, hands the C ABI a callback that resolves -(or rejects) the promise, and returns the corresponding future. From the -`kvstore` sample's cancellable `Store::compact`: +A callback interface is an abstract class with one pure virtual method per +IDL method: ```cpp -inline std::future Store::compact(weaveffi_cancel_token* cancel_token) const { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_kv_Store_compact_async(handle_, cancel_token, [](void* context, weaveffi_error* err, int64_t result) { - auto* p = static_cast*>(context); - try { - 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)); - } else { - p->set_value(result); - } - } catch (...) { - p->set_exception(std::current_exception()); - } - weaveffi_error_free(err); - delete p; - }, static_cast(promise_ptr)); - return future; -} -``` - -Use it with `.get()` (blocking) or compose with your event loop. The -completion lambda runs exactly once, on an arbitrary producer thread; it -settles the promise and then deletes it. Everything passed to the callback -is owned by the consumer: strings, bytes, and buffered results are copied -into C++ values and released with `weaveffi_free_string` or -`weaveffi_free_bytes`, a heap-boxed error is released with -`weaveffi_error_free`, and an object result is adopted into a wrapper -(`std::future`). An async callable with `throws: true` rejects with -the module's typed domain exception; one without `throws` rejects with the -generic `WeaveFFIError` for runtime codes (a panic in the future is -2, a -callback that threw is -4). - -When the IDL marks the callable `cancellable: true`, the wrapper gains -a trailing `weaveffi_cancel_token*` parameter defaulting to `nullptr`: +class ReadyListener { +public: + virtual ~ReadyListener() = default; -```cpp -weaveffi_cancel_token* token = weaveffi_cancel_token_create(); -auto fut = store.compact(token); -weaveffi_cancel_token_cancel(token); // from any thread -// fut.get() throws (typed KvError) if the operation was cancelled -weaveffi_cancel_token_destroy(token); + /** Fires when an item is ready */ + virtual void on_ready(int32_t code, std::string_view msg) = 0; ``` -C++ is one of the few targets (C, C++, Kotlin) that expose the cancel -token; see [Async functions](../guides/async.md). +Pass an implementation as `std::shared_ptr`. The wrapper boxes the pointer +as the vtable context, and the producer's last release deletes the box, +which runs your destructor if nothing else holds the implementation. The +producer may call methods from any thread, so implementations must be +thread-safe. String arguments are views valid for the call. An exception +thrown by a method is reported to the producer with code -4 and its +`what()` message; it never unwinds through the C frame. Whether the +producer method returns `T` or `Result`, the caller sees +the generic `Error` with code -4. ## Iterators -`iter` return values surface as a generated move-only RAII range -class with `begin()`/`end()`, so results stream in constant memory: -nothing is drained up front, and each iteration step pulls exactly one -element from the producer through `_next`. From the `kvstore` sample -(`Store::list_keys` returns `iter`, trimmed): - -```cpp -class StoreListKeysIterator { - weaveffi_kv_Store_ListKeysIterator* handle_; - -public: - ~StoreListKeysIterator() { - if (handle_) weaveffi_kv_Store_ListKeysIterator_destroy(handle_); - } - StoreListKeysIterator(const StoreListKeysIterator&) = delete; - StoreListKeysIterator(StoreListKeysIterator&& other) noexcept; - - /** Pulls the next element, or `std::nullopt` once exhausted. */ - 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; - } - - struct sentinel {}; - class iterator { /* input_iterator_tag; compares against sentinel */ }; - iterator begin() { return iterator(this); } - sentinel end() const { return sentinel{}; } -}; -``` - -The range is single-pass: `begin()` returns an input iterator that -compares against a sentinel, so a plain range-`for` works: +An `iter` function returns a move-only range class: ```cpp for (const std::string& key : store.list_keys(std::nullopt)) { - std::cout << key << '\n'; + // One producer call per step. } ``` -Each pulled string is copied into `std::string` and its native allocation -freed with `weaveffi_free_string`; a buffered element arrives as a value -buffer that is unpacked and released with `weaveffi_free_bytes`; an object -element is adopted into a wrapper. The producer iterator is destroyed -exactly once: eagerly when `next()` reports exhaustion (or an error), or -from the range's destructor when iteration is abandoned early. - -Errors from the launcher and from each `next` follow the function's error -strategy: `Store::list_keys` checks both through `detail::check_kv`, so a -failing step throws the typed `KvError` subclass after releasing the -iterator, while a non-throwing function throws the generic `WeaveFFIError` -only for runtime codes. +Each step pulls one element. The range releases the producer iterator when +it's exhausted or destroyed, so abandoning a loop early leaks nothing. +`next()` returns `std::optional` for manual iteration. ## Known limitations -- A callback interface must be passed as `std::shared_ptr`; there is no - overload for a raw reference or a lambda. Wrap a lambda in a small - subclass if you need one. -- Trampolines only capture `what()` from exceptions derived from - `std::exception`; other thrown types are reported with a generic - message. -- Exceptions thrown by callback methods that return `void` and belong to a - non-`throws` callable still abort the call with -4; C++ has no separate - trap path, so the caller simply sees `WeaveFFIError`. -- The lazy range is single-pass and move-only; copy the elements into a - `std::vector` if you need random access or a second pass. -- `std::future` has no cancellation or continuation support of its own; - cancellation goes through the raw `weaveffi_cancel_token*`. - -## Troubleshooting - -- **`undefined reference to weaveffi_*`**: link against the Rust - cdylib. The header alone is not enough. -- **`WeaveFFIError` with code -3 on a method call**: the wrapper's handle is - null. You're calling through a moved-from wrapper or one constructed from - a null pointer. -- **Double-free crashes**: every wrapper releases exactly one reference. A - double free means a raw pointer from `handle()` was adopted by a second - wrapper without `clone_handle()`, or a `clone_handle()` result was - destroyed twice. -- **Exceptions not caught across DLL boundaries on MSVC**: build the - consumer and the dynamically loaded library with the same - `_HAS_EXCEPTIONS` setting and CRT. -- **`std::optional` is missing**: the header requires C++17. Add - `target_compile_features(... cxx_std_17)` to your CMake target. +- The ranges are single-pass input ranges; copy them into a `std::vector` + for random access. +- A callback interface must be a `std::shared_ptr` to a subclass; there's + no lambda overload. +- Exceptions not derived from `std::exception` are reported to the + producer with a generic message. +- The library check runs on first use, not at load time (see above). +- The C ABI's callback methods can't return strings or buffered values, so + neither can the abstract classes. diff --git a/docs/src/generators/dart.md b/docs/src/generators/dart.md index 1fe8cdf3..5be90018 100644 --- a/docs/src/generators/dart.md +++ b/docs/src/generators/dart.md @@ -1,850 +1,209 @@ # Dart -## Overview - -The Dart target produces a pure-Dart FFI package that wraps the C ABI -(revision 2) using [`dart:ffi`](https://dart.dev/interop/c-interop). It -opens the shared library with `DynamicLibrary.open` and resolves each -symbol via `lookupFunction`. There's no native compilation step or -`ffigen` run required; the generated `.dart` file is ready to import. - -Records and rich enums are plain Dart classes (a sealed hierarchy for a -rich enum) that cross the ABI as value buffers. Interfaces are -reference-counted wrapper classes with `dispose()` and a -`NativeFinalizer` backstop. Callback interfaces are abstract classes the -consumer implements, bound through `NativeCallable.isolateLocal` -trampolines. Async functions return `Future`, and `iter` returns -are lazy `Iterable`s backed by `sync*` generators. +The Dart target generates a standalone Dart package that calls the C ABI +through `dart:ffi`. It works in Dart command-line apps and in Flutter apps on +the desktop, Android, and iOS. ## What gets generated -| File | Purpose | -|------|---------| -| `dart/lib/weaveffi.dart` | `dart:ffi` bindings: loader, ABI check, typedefs, lookups, wrappers, record/enum classes | -| `dart/pubspec.yaml` | Package metadata and `package:ffi` dependency | -| `dart/README.md` | Basic usage instructions | - -The library verifies the producer's ABI revision when it's first -loaded, throwing a `StateError` on mismatch: - -```dart -const int _abiVersion = 2; +```text +dart/ + pubspec.yaml package `{prefix}`, depends on `ffi` + README.md + lib/{prefix}.dart the whole binding: runtime, types, and functions ``` -## Type mapping +The package name is the library's C prefix (`kvstore` for the kvstore +sample), so consumers write `import 'package:kvstore/kvstore.dart';`. Set +`package_name` under `[generators.dart]` to choose another name; the library +file follows it. With `strip_module_prefix = false`, module-level functions +keep their module path (`kvStatsGetStats` instead of `getStats`). -| IDL type | Dart type | Native FFI type | -|--------------|---------------------|------------------------| -| `i8`, `i16`, `i32`, `i64` | `int` | `Int8`, `Int16`, `Int32`, `Int64` | -| `u8`, `u16`, `u32`, `u64` | `int` | `Uint8`, `Uint16`, `Uint32`, `Uint64` | -| `f32`, `f64` | `double` | `Float`, `Double` | -| `bool` | `bool` | `Int32` | -| `string` | `String` | `Pointer` | -| `bytes` | `List` | `Pointer` + `Size` | -| `StructName` | `StructName` (plain class) | value buffer (`Pointer` + `Size`) | -| `EnumName` (plain) | `EnumName` (enhanced enum) | `Int32` | -| `EnumName` (rich) | `EnumName` (sealed class hierarchy) | value buffer | -| `InterfaceName` | `InterfaceName` (wrapper class) | `Pointer` | -| `InterfaceName?` | `InterfaceName?` | `Pointer` (`nullptr` for `null`) | -| `CallbackName` | `CallbackName` (abstract class to extend) | `Pointer` ctx + vtable pointer | -| `T?` | `T?` | value buffer | -| `[T]` | `List` | value buffer | -| `{K: V}` | `Map` | value buffer | -| `iter` | `Iterable` (lazy) | `Pointer` | - -Buffered types cross the boundary serialized in the -[value-buffer format](../reference/value-buffers.md); the library -carries a private `_BufferWriter`/`_BufferReader` pair plus one -`_pack*`/`_unpack*` routine per record and rich enum. Objects nested -inside a buffered value travel as object tokens (see -[Objects](#objects-interfaces)). Booleans cross as `Int32` (`0`/`1`) -and the wrapper converts both ways. - -### 64-bit integers and floats - -Dart's `int` is a signed 64-bit integer, so `i64` round-trips exactly. -`u64` is carried as its two's-complement bit pattern: `u64::MAX` arrives -as `-1` and `2^63` as `-9223372036854775808`, both across `Uint64` FFI -slots and inside value buffers (`writeUint64`/`readUint64`). Apply -`.toUnsigned(64)` or `BigInt.from(v).toUnsigned(64)` when you need the -arithmetic value. `f32`/`f64` map to `double`; the `codec` conformance -consumer verifies NaN, both infinities, and `-0.0` survive a round trip -bit-for-bit. - -## Example IDL and generated code - -```yaml -version: "0.9.0" -modules: - - name: contacts - enums: - - name: ContactType - doc: Type of contact - variants: - - { name: Personal, value: 0 } - - { name: Work, value: 1 } - - { name: Other, value: 2 } - - structs: - - name: Contact - doc: A contact record - fields: - - { name: name, type: string } - - { name: email, type: "string?" } - - { name: age, type: i32 } - - functions: - - name: create_contact - params: - - { name: name, type: string } - - { name: email, type: "string?" } - - { name: contact_type, type: ContactType } - return: Contact - - - name: find_contact - params: - - { name: id, type: i32 } - return: "Contact?" -``` - -The loader auto-detects the platform: - -```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(); -``` +## Install, build, and load -Enums become Dart enhanced enums: +Add the generated package as a dependency, for example with a `path:` +dependency, and run `dart pub get`: -```dart -/// Type of contact -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); -} +```yaml +dependencies: + kvstore: + path: generated/dart ``` -Structs are plain Dart value classes: one final typed field per IDL -field plus a constructor with named arguments. They declare no C symbols -(no destroy, no getters) and there's nothing to dispose: +The bindings open the native library on first use: `libkvstore.dylib` on +macOS, `libkvstore.so` on Linux and Android, and `kvstore.dll` on Windows, +found through the platform's normal search. Set `KVSTORE_LIBRARY` (the +identity's `{PREFIX}_LIBRARY` variable) to a full path to load a specific +build. On iOS, and on macOS when the file isn't found, the bindings look the +symbols up in the running executable, which is how a statically linked or +embedded library is found. `weaveffi package` bundles prebuilt desktop +libraries under `native//` and tries those first. -```dart -/// A contact record -class Contact { - final String name; - final String? email; - final int age; - - Contact({required this.name, this.email, required this.age}); -} -``` +Before any other call, the bindings check the library: `kvstore_abi_version()` +must report 3, and every top-level module's checksum function must match the +value the bindings were generated against. A mismatch throws a `StateError` +that names the module and asks you to regenerate the bindings. -Each function emits a native typedef, Dart typedef, lookup, and -top-level wrapper. Buffered parameters are packed and staged in native -memory the producer borrows for the call; buffered returns are copied, -freed, and decoded. From the `kvstore` sample's cross-module `getStats`: +The package needs Dart 3.12 or newer and the `ffi` package. Flutter native +assets (build hooks that compile the library) aren't generated; ship the +library with your app as you would any other native dependency. -```dart -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'); - -/// 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 decoded = _unpackStats(reader); - reader.expectEnd(); - return decoded; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} -``` - -Wrapper names are lowerCamelCase with the IDL module prefix stripped by -default (a `kv.open_store` function would surface as `openStore`, not -`kvOpenStore`); the C symbols keep their full names. Set -`strip_module_prefix: false` in the Dart generator config (or under -`[global]`) to keep module-prefixed wrapper names. +## Type mapping -## Typed errors +| IDL type | Dart type | Crosses the C ABI as | +|---|---|---| +| `i8` to `i64`, `u8` to `u64` | `int` | by value (`u64` as its two's-complement bit pattern) | +| `f32`, `f64` | `double` | by value | +| `bool` | `bool` | C `bool` | +| `string` | `String` | UTF-8 `(ptr, len)`, so interior NULs survive | +| `bytes` | `List` (a `Uint8List` when returned) | `(ptr, len)` | +| Record | a class with final fields and named constructor arguments | value buffer | +| Plain enum | an enhanced `enum` with `value` and `fromValue` | `int32_t` | +| Rich enum | a `sealed` class with one subclass per variant | value buffer | +| Interface | a `final class` wrapper | object pointer | +| `T?` | `T?` | value buffer (`Interface?` is a nullable pointer) | +| `[T]`, `{K:V}` | `List`, `Map` | value buffer | +| `iter` | `Iterable` | native iterator | +| Callback interface | an `abstract class` you implement | `ctx` plus vtable | -The package defines `WeaveFFIException` with `code` and `message` fields -and the four runtime trap codes as constants: +Wrapper functions and members use lowerCamelCase; types use UpperCamelCase. +A name that collides with a Dart keyword or a type the bindings use gains a +trailing `_` (`class_`, `String_`). -```dart -class WeaveFFIException implements Exception { - /// The producer reported an untyped error. - static const int genericCode = -1; - /// The producer panicked; [message] carries the panic text. - static const int panicCode = -2; - /// An argument could not be lifted by the producer. - static const int marshalCode = -3; - /// A Dart callback-interface implementation threw; [message] carries the - /// exception's text. - static const int foreignCode = -4; - final int code; - final String message; - WeaveFFIException(this.code, this.message); - @override - String toString() => '$runtimeType($code): $message'; -} -``` +## Objects and lifetime -A module's error domain adds an exception subclass named by replacing -the trailing `Error` stem with `Exception` (`KvError` becomes -`KvException`) plus one subclass per code, and a mapper that falls back -to `WeaveFFIException` for codes outside the domain. From the `kvstore` -sample: +Each wrapper holds one strong reference. Call `dispose()` when you're done; +an undisposed wrapper is released by a `NativeFinalizer` when it's +collected. Disposing twice is harmless, and using a disposed wrapper throws a +`StateError`. ```dart -/// Typed error domain `KvError` declared by module `kv`. -class KvException extends WeaveFFIException { - KvException(super.code, super.message); -} - -/// key not found -class KeyNotFoundException extends KvException { - KeyNotFoundException([String message = 'key not found']) : super(1001, message); -} - -// ExpiredException, StoreFullException, IoException follow the same shape. +final store = kv.Store.open('/tmp/data'); // factory constructor +store.put('key', [1, 2, 3], kv.EntryKind.persistent, null); +final copy = store.share(); // a second reference +store.dispose(); +copy.count(); // still valid +copy.dispose(); ``` -Only callables marked `throws: true` in the IDL check their error slot -with `_checkKvException` (their doc comments read -`Throws [KvException] on domain errors.`); catching -`KeyNotFoundException` or `KvException` works as usual. A callable -without `throws` uses the generic `_checkError`, which throws -`WeaveFFIException` if the producer misbehaves. Both copy the message -out and release the slot with `weaveffi_error_clear` before throwing. - -An error code that declares payload `fields:` carries them serialized in -the error's payload buffer; the mapper decodes them into typed fields on -the exception before the buffer is released. - -### Runtime error codes - -| Code | Constant | Meaning | Where it surfaces | -|------|----------|---------|-------------------| -| `-1` | `genericCode` | The producer reported an error without a declared code. | Thrown as `WeaveFFIException`. | -| `-2` | `panicCode` | The Rust implementation panicked; the export macros and the async spawner catch the unwind. | Thrown as `WeaveFFIException`, or fails the awaited `Future`. | -| `-3` | `marshalCode` | Malformed input at the boundary (invalid UTF-8, a truncated value buffer, a bad enum discriminant). | Thrown as `WeaveFFIException`. | -| `-4` | `foreignCode` | A callback-interface method implemented in Dart threw. | Thrown as `WeaveFFIException` from the producer call that invoked the callback (see [Callback interfaces](#callback-interfaces)). | - -There's no non-throwing call path in Dart: a non-throwing callable whose -error slot comes back non-zero still throws `WeaveFFIException`. Using a -disposed wrapper is reported separately as `StateError`. - -## Objects (interfaces) +Every call borrows the wrapper's pointer for its duration. If a callback +disposes a wrapper that a call in progress is using, the release waits until +that call returns, so the native object never disappears mid-call. Wrappers +are `Finalizable`, so Dart keeps them alive until the end of any call that +uses them. An object written into a value buffer (a record field, a list +element) is encoded as a fresh reference, so the wrapper stays usable. -An `interfaces:` entry becomes a class that adopts one strong reference -to a reference-counted producer object. From the `kvstore` sample -(trimmed): - -```dart -final _weaveffiKvStoreDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('weaveffi_kv_Store_destroy')); - -class Store implements Finalizable { - final Pointer _ptr; - bool _disposed = false; - - /// Adopts one strong reference to the native object. - Store._(this._ptr) { - _weaveffiKvStoreDestroyFinalizer.attach(this, _ptr, detach: this); - } - - /// The borrowed native pointer for the duration of a call. - Pointer get _handle { - if (_disposed) throw StateError('Store used after dispose()'); - return _ptr; - } - - /// Mints a second strong reference (the interface's `_clone` symbol) for - /// an object token written into a value buffer. - Pointer _cloneRef() => _weaveffiKvStoreClone(_handle); - - /// Releases this instance's native reference. Safe to call more than - /// once; the native object is dropped when its last reference (this - /// one, any other wrapper's, or the producer's) is released. - void dispose() { - if (_disposed) return; - _disposed = true; - _weaveffiKvStoreDestroyFinalizer.detach(this); - _weaveffiKvStoreDestroy(_ptr); - } - - /// 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); - } - } -} -``` +## Errors -- **Construction.** A constructor named `new` renders as an unnamed - `factory` (the `events` sample's `EventBus()`); other constructors - become named factories (`Store.open(path)`). Methods are - lowerCamelCase instance methods, statics are static methods, and - deprecated members carry `@Deprecated`. -- **Disposal.** `dispose()` releases this wrapper's reference through - the `_destroy` symbol. It's idempotent, and a `NativeFinalizer` - attached at construction destroys the reference at GC time if the - wrapper was never disposed (disposal detaches it first, so the destroy - runs exactly once). The producer object itself is dropped only when - the last reference anywhere is released. -- **Use after dispose.** Every call reads the private `_handle` getter, - which throws `StateError('Store used after dispose()')` on a disposed - wrapper, whether it's the receiver, a parameter, or a field of a record - being packed. -- **Copies mint new references.** Methods returning the receiver or an - existing object (`share()`, `fork()`) return a fresh strong reference - adopted into a new wrapper; disposing one never affects another. +Every failure is a `NativeException` with a `code` and a `message`. A +module's error domain becomes a subclass, with one subclass per code: ```dart -final store = Store.open('/tmp/cache.kv'); try { - store.put('alpha', [1], EntryKind.persistent, null); - print('${store.count()} / ${Store.defaultCapacity()}'); - final reclaimed = await store.compact(); -} finally { - store.dispose(); + store.get('missing'); +} on kv.KeyNotFoundException catch (e) { + print('${e.code}: ${e.message}'); // 1001: key not found +} on kv.KvException { + // any other KvError code } ``` -### Nullable objects, and objects inside values +Only functions declared `throws` raise domain exceptions. The runtime codes +are constants on `NativeException`: `genericCode` (-1), `panicCode` (-2), +`marshalCode` (-3), `foreignCode` (-4, a callback implementation threw), and +`cancelledCode` (-5, raised as `CancelledException`). A domain code with +fields carries them as typed properties on its exception. -An `Interface?` parameter passes `nullptr` for `null`, and an -`Interface?` return maps `nullptr` to `null`: +## Async and cancellation -```dart -Store? larger(Store? other) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStoreLarger(_handle, other?._handle ?? nullptr, err); - _checkError(err); - return result == nullptr ? null : Store._(result); - } finally { - calloc.free(err); - } -} -``` - -Objects inside records, lists, maps, and optionals travel as 8-byte -object tokens in the value buffer. Writing a token mints a new strong -reference with `_cloneRef()`; reading one adopts the reference into a -fresh wrapper. From the `StoreInfo` record (`store: Store`, -`mirror: Store?`): - -```dart -void _packStoreInfo(_BufferWriter w, StoreInfo v) { - w.writeString(v.label); - w.writeUint64(v.store._cloneRef().address); - final t0 = v.mirror; - if (t0 == null) { - w.writeOptionFlag(false); - } else { - w.writeOptionFlag(true); - w.writeUint64(t0._cloneRef().address); - } - w.writeInt64(v.count); -} - -StoreInfo _unpackStoreInfo(_BufferReader r) { - return StoreInfo( - label: r.readString(), - store: Store._(Pointer.fromAddress(r.readUint64())), - mirror: (r.readOptionFlag() ? Store._(Pointer.fromAddress(r.readUint64())) : null), - count: r.readInt64(), - ); -} -``` - -Lists of objects work the same way in both directions -(`Store.openMany(paths)` returns `List`, -`Store.totalCount(stores, extra)` takes one); each wrapper in a returned -list owns its own reference and must be disposed (or left to the -finalizer) individually. Iterators over objects adopt one reference per -step, and async functions returning an object adopt the pointer inside -the completion callback before completing the `Future`. +An async function returns a `Future`. The producer completes it from a +worker thread through a `NativeCallable.listener`, and the result is +decoded on the event loop. -## Rich (algebraic) enums - -A rich (algebraic) enum is a sum type whose variants carry associated -data. A plain C-style enum surfaces as a Dart `enum` and crosses as an -`Int32`; a rich enum instead becomes an idiomatic sealed class -hierarchy: a sealed base class plus one subclass per variant named -`{Enum}{Variant}`, each carrying that variant's fields. Rich enums -declare no C symbols; values cross the ABI serialized in value buffers -as an `i32` tag followed by the active variant's fields. - -For a `Shape` enum with variants `Empty`, `Circle { radius: f64 }`, -`Rectangle { width: f32, height: f32 }`, and `Labeled { label: string, -count: u8 }`, the generator emits: +A cancellable function takes an optional named `CancelToken`: ```dart -/// An algebraic shape (sum type with associated data) -sealed class Shape { - const Shape(); -} - -/// The empty shape -class ShapeEmpty extends Shape {} - -/// A circle with a radius -class ShapeCircle extends Shape { - /// Radius in points - final double radius; - - ShapeCircle({required this.radius}); -} - -/// A labeled shape with a small count -class ShapeLabeled extends Shape { - final String label; - final int count; - - ShapeLabeled({required this.label, required this.count}); -} -``` - -Construct variants directly and match on the sealed hierarchy with an -exhaustive `switch`: - -```dart -final circle = ShapeCircle(radius: 2.0); -final labeled = ShapeLabeled(label: 'unit', count: 3); - -switch (circle) { - case ShapeCircle(:final radius): - print(radius); // 2.0 - case _: - break; +final token = kv.CancelToken(); +final pending = store.compact(cancelToken: token); +token.cancel(); +try { + await pending; +} on kv.CancelledException { + // completed with code -5 without waiting for the work } - -print(describe(circle)); // packs the buffer via the C ABI -final bigger = scale(circle, 3.0); // returns a new Shape value ``` -Values are plain Dart data: there's no native handle and nothing to -dispose. The generated `_packShape`/`_unpackShape` helpers write and -read the tagged wire format; variant fields of interface type follow the -object token rules above. - -## Callback interfaces +One token can serve any number of calls, a token that's already cancelled +cancels a call at launch, and cancelling after completion does nothing. The +bindings create a native token for each call, cancel it when the +`CancelToken` is cancelled, and destroy it when the call completes. -A `callback_interfaces:` entry becomes an abstract class the consumer -extends (or implements) and passes wherever the API takes that type. -From the `kvstore` sample: +## Callbacks -```dart -/// Object arguments are owned by the implementation: call `dispose()` on -/// them (or let the finalizer run) when done. A thrown exception is reported -/// to the producer, which aborts the call it was making; the original Dart -/// caller then observes a [WeaveFFIException] with [WeaveFFIException.foreignCode] -/// carrying the exception's text. -/// -/// Dart limitation: methods are bound with `NativeCallable.isolateLocal`, so -/// the producer may only invoke them on the thread of the isolate that -/// passed the callback. That holds when the producer calls them -/// synchronously during a call from Dart. A producer that invokes a method -/// from another thread (an async task, a background worker) is unsupported: -/// a `NativeCallable.listener` cannot return a value or read borrowed -/// arguments after the native frame returns. -abstract class EvictionListener { - /// An entry left the store. Returns whether the listener wants to keep - /// receiving notifications; `false` detaches it. - bool onEvict(Entry entry, EvictionReason reason); -} -``` +Implement the generated abstract class and pass an instance. The bindings +keep it in a table until the producer releases it, so you don't need to hold +another reference. ```dart -class Auditor extends wv.EvictionListener { +class Logger extends kv.EvictionListener { @override - bool onEvict(wv.Entry entry, wv.EvictionReason reason) { - print('${entry.key}: $reason'); + bool onEvict(kv.Entry entry, kv.EvictionReason reason) { + print('evicted ${entry.key}'); return true; } } -store.setEvictionListener(Auditor()); +store.setEvictionListener(Logger()); ``` -Behind the class is one process-wide static vtable per callback -interface, allocated once and never freed, whose method slots are -`NativeCallable.isolateLocal` trampolines and whose `free` slot is a -`NativeCallable.listener`. Passing an implementation parks it in a -handle table under an integer key; the key (widened to a pointer) -crosses as `ctx`, so the producer never holds a Dart object: +How a method runs depends on its return type: -```dart -final Map _callbackTable = {}; -int _nextCallbackKey = 1; - -Pointer _registerCallback(Object impl) { - final key = _nextCallbackKey++; - _callbackTable[key] = impl; - return Pointer.fromAddress(key); -} +- A method that returns a value is a `NativeCallable.isolateLocal` + trampoline. It runs synchronously, so the producer must call it on the + isolate's thread while a call from Dart is in progress. If it throws, the + producer's call fails with `foreignCode` (-4) and the exception's text. +- A void method is a `NativeCallable.isolateGroupBound` forwarder. The + producer may call it from any thread; the forwarder copies its arguments + and returns at once, and the method runs later on the event loop, in the + zone that passed the instance. An exception there is an uncaught error of + that zone. -// Vtable entry `weaveffi_kv_EvictionListener_vtable.on_evict`. -bool _evictionListenerVtOnEvict(Pointer ctx, Pointer entryPtr, int entryLen, int reason, Pointer<_WeaveFFIError> outErr) { - try { - final entryData = _copyNativeBytes(entryPtr, entryLen); - final entryReader = _BufferReader(entryData); - final entryValue = _unpackEntry(entryReader); - entryReader.expectEnd(); - final impl = _callbackFor(ctx) as EvictionListener; - return impl.onEvict(entryValue, EvictionReason.fromValue(reason)); - } catch (e) { - _foreignError(outErr, e); - return false; - } -} - -void _evictionListenerVtFree(Pointer ctx) { - _callbackTable.remove(ctx.address); -} - -final Pointer<_EvictionListenerVtableStruct> _EvictionListenerVtable = () { - final vt = calloc<_EvictionListenerVtableStruct>(); - vt.ref.onEvict = _pinCallable(NativeCallable<_NativeEvictionListenerVtOnEvict>.isolateLocal( - _evictionListenerVtOnEvict, exceptionalReturn: false)); - vt.ref.free = _pinCallable(NativeCallable<_NativeEvictionListenerVtFree>.listener( - _evictionListenerVtFree)); - return vt; -}(); -``` - -- **Lifetime.** The implementation stays in `_callbackTable` (and - therefore alive) exactly as long as the producer may call it; the - vtable's `free` removes it when the producer drops its last reference. - A producer that retains the implementation (a store's eviction - listener) keeps it alive across calls; one that doesn't (the `events` - sample's `routeOnce`) frees it before returning. `free` is a - `.listener` callable, so it may safely arrive from any thread; the - removal is posted to the isolate's event loop. -- **Argument ownership.** Borrowed strings and buffers are copied into - Dart values before the method runs, so the implementation may keep - them. An object passed to a callback method is owned by the - implementation: the trampoline adopts it into a new wrapper - (`impl.onAttached(EventBus._(bus))` in the `events` sample), and the - implementation should `dispose()` it when done (or let the finalizer - run). -- **Return values.** A method's return value is converted back to its C - representation (`bool` as `Bool`, a plain enum as its `value`, a - record as a value buffer the producer frees). -- **Exceptions.** An exception escaping a method never unwinds through - the C frame. `_foreignError` writes `foreignCode` (-4) with - `error.toString()` into the producer's error slot, and the trampoline - returns the `exceptionalReturn` default; the producer aborts the call - in progress, and the original Dart caller sees `WeaveFFIException` - with `code == -4`. For a callable marked `throws`, the domain mapper - falls through to `WeaveFFIException` (so `on KvException` doesn't - catch it but `on WeaveFFIException` does). The isolate is never taken - down. -- **Threads (the important caveat).** Method trampolines are - `NativeCallable.isolateLocal`, which can only run on the thread of the - isolate that created them. That's exactly right when the producer - calls back synchronously during a call from Dart (the `kvstore` - eviction listener fires inside `delete()`/`get()`). A producer that - invokes a callback method from another thread (an async task, a - background worker, a timer) is unsupported and will crash the - process; `NativeCallable.listener` can't be used instead because it - returns before the method runs, so it could neither return a value - nor read borrowed arguments. Keep such producers on the C, C++, - Swift, Kotlin, Node, .NET, Go, Python, or Ruby targets. -- **Isolate lifetime.** The vtable callables have `keepIsolateAlive` - cleared, so an idle isolate can still exit while a producer holds a - callback implementation. - -## Build instructions - -Standalone Dart: - -1. Generate the bindings: - - ```bash - weaveffi generate api.yaml -o generated --target dart - ``` - -2. Build the Rust shared library: - - ```bash - cargo build --release -p your_library - ``` - -3. Make the cdylib findable at runtime. `WEAVEFFI_LIBRARY` may point at - an exact file; otherwise: - - - macOS: `DYLD_LIBRARY_PATH=$PWD/../../target/release dart run example/main.dart` - - Linux: `LD_LIBRARY_PATH=$PWD/../../target/release dart run example/main.dart` - - Windows: place `weaveffi.dll` next to the script or add its - directory to `PATH`. - -Flutter: - -1. Generate the bindings as above. -2. Cross-compile the Rust cdylib for every Flutter target you support - (`aarch64-apple-ios`, `aarch64-linux-android`, `x86_64-apple-darwin`, - etc.). -3. Reference the generated package from your app's `pubspec.yaml`: - - ```yaml - dependencies: - weaveffi: - path: ../generated/dart - ``` - -4. Bundle the cdylib per platform: - - - iOS / macOS: ship a Framework or use a `podspec`. - - Android: place `.so` files under `android/src/main/jniLibs/{abi}/`. - - Linux / Windows: place next to the executable or on the library - search path. - -## Packaging - -`weaveffi package --target dart` emits the Dart package under `dart/` -with a loader that tries the bundled libraries first, and copies each -supplied desktop binary to `dart/native//` (`macos-arm64`, -`macos-x64`, `linux-x64`, `linux-arm64`, `windows-x64`). The packaged -loader still honours `WEAVEFFI_LIBRARY` first, then probes every -bundled slice for the running OS, then the bare system library name. Android and -`wasm32` binaries have no slot in the loader and are skipped; bundle -them through the Flutter mechanisms above. See -[Packaging](../guides/packaging.md) for the shared workflow. - -## Memory and ownership - -- **Strings:** Dart `String` values are converted with `toNativeUtf8()` - and freed in a `finally` block. Returned UTF-8 pointers are copied - with `toDartString()` and then released with `weaveffi_free_string`. -- **Bytes:** copied into native memory for the call and freed after; - returned buffers are copied into Dart lists, then the producer's - allocation is released with `weaveffi_free_bytes`. -- **Buffered values (records, rich enums, optionals, lists, maps):** - parameters are packed into a value buffer that the producer borrows - for the duration of the call; returns are copied into Dart memory, - released with `weaveffi_free_bytes`, and decoded with the generated - `_unpack*` helper. Object tokens written into a buffer are fresh - strong references the producer owns; tokens read out are adopted into - wrappers. -- **Interfaces:** one strong reference per wrapper, released by - `dispose()` with a `NativeFinalizer` backstop. -- **Callback implementations:** held in `_callbackTable` until the - producer calls the vtable's `free`. -- **Iterators:** each yielded element is copied (or decoded from a value - buffer that's then freed), and the iterator handle is destroyed - exactly once; see [Iterators](#iterators). - -## Async support - -Functions marked `async: true` return a `Future` backed by the -`_async`-suffixed C launcher. The completion callback is a -`NativeCallable.listener`, which may be invoked from any native thread: -the event is posted to the owning isolate's event loop, where it -completes the `Completer`. From the `kvstore` sample's `Store.compact`: - -```dart -/// 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; -} -``` - -The callable is closed in the callback's `finally` (or immediately if -the launch itself throws), so each native trampoline is freed exactly -once; input buffers (a `publishLater(topic, text)` call's string -arguments, say) are released in a `whenComplete` once the future -settles. The `dart:async` import is only emitted when the IDL contains -at least one async function. - -Result ownership follows the async contract: the callback owns the -string, bytes, and buffered results it receives (records, rich enums, -optionals, lists, and maps arrive as a `(resultPtr, resultLen)` pair), -so the callback body copies them into Dart values and then releases them -with `_weaveffiFreeString` or `_weaveffiFreeBytes`. Consumer ownership -is what makes `NativeCallable.listener` safe here: the listener defers -the callback body to the isolate's event loop, past the native -callback's return, so a producer-freed buffer would already dangle by -the time it ran. A reported error is heap-boxed and released with -`_weaveffiErrorFree` after its fields are copied. An owned interface -result transfers ownership too: the wrapper adopts the pointer, and its -`dispose()` owns the eventual destroy. - -For a callable marked `throws: true`, the completion callback maps an -error through the domain mapper (`_mapKvException` above), so the future -fails with the typed exception; a non-throwing async callable can only -fail with `WeaveFFIException` on a producer bug. A panic inside the -spawned future surfaces as `panicCode` (-2). Async interface methods -follow the same pattern as instance methods returning `Future`; the -receiver's `_handle` is read (throwing `StateError` if disposed) before -the launcher runs. - -For functions marked `cancellable: true` the C launcher gains a -`weaveffi_cancel_token*` parameter. The Dart wrapper passes `nullptr` -for it and doesn't expose the token; only the C and C++ targets surface -cancellation tokens. +Object arguments are owned by the implementation; call `dispose()` on them +when you're done. ## Iterators -`iter` returns surface as `Iterable` backed by a `sync*` -generator, so they are fully lazy: nothing runs until the consumer -starts iterating, and each element pulls exactly one native `next` call. -Iterating the returned `Iterable` again launches a fresh native -iterator. From the `kvstore` sample's `Store.listKeys` (argument -packing elided): +An `iter` return is a lazy `Iterable`: each step pulls one element from +the native iterator, and iterating again starts a new one. The native +iterator is destroyed when iteration finishes or fails, or by a finalizer +when an iteration is abandoned (`first`, `take`, a `break`) and collected. ```dart -Iterable listKeys(String? prefix) sync* { - // ... pack the `String?` prefix into a staged value buffer ... - 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); - } +for (final key in store.listKeys('user:')) { + print(key); } ``` -Each yielded string is copied with `toDartString()` and its producer -allocation released with `weaveffi_free_string`; a buffered element is -decoded from its value buffer and released with `weaveffi_free_bytes` -instead; an object element is adopted into a new wrapper. The handle -lifecycle covers early abandonment: the `finally` block runs when the -loop exhausts, when a step fails, or when the consumer stops iterating -(Dart closes the suspended `sync*` frame on `break`). If an iteration is -abandoned without ever resuming the frame, the `_IteratorLifetime` -anchor's `NativeFinalizer` destroys the handle at GC time; an eagerly -destroyed handle detaches first, so the destroy runs exactly once either -way. - -Errors from the launcher and from each `next` follow the function's -error strategy: the throwing `listKeys` checks each step with -`_checkKvException` and throws the typed `KvException` subclasses from -the step that failed; a non-throwing iterator throws `WeaveFFIException` -only for producer bugs. Because the generator is lazy, the launch error -is thrown on the first `moveNext()`, not when the method returns. +## Performance notes + +Synchronous calls share one error slot and one length slot per isolate, and +stage arguments in a single `Arena` per call. When the API declares no +callback interfaces, synchronous calls are bound as leaf calls (`isLeaf: +true`), which skips the VM's safepoint transition; a leaf call blocks this +isolate group's garbage collector until it returns, so it suits fast +functions. ## Known limitations -- Callback-interface methods only work when the producer calls them - synchronously on the thread of the isolate that passed the - implementation; invocation from another producer thread is - unsupported. -- `u64` values above `2^63 - 1` arrive as negative `int`s (two's - complement); convert with `toUnsigned(64)` if needed. -- Async cancellation doesn't propagate: `cancellable: true` tokens are - not exposed, and an abandoned `Future` leaves the native operation - running. -- `Iterable` results relaunch the native iterator on each - re-iteration; the producer sees a fresh call. -- The plain `generate` output relies on the dynamic loader (or - `WEAVEFFI_LIBRARY`) to find the library; only `weaveffi package` - bundles desktop slices, and mobile slices go through Flutter. - -## Troubleshooting - -- **`Invalid argument(s): Failed to load dynamic library`**: the cdylib - is not on the search path. Set `WEAVEFFI_LIBRARY`, - `DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH`, or copy the library next to - your executable. -- **`StateError: WeaveFFI ABI mismatch`**: the library was built by a - different `weaveffi` release than the bindings. Regenerate the bindings - and rebuild the library together. -- **`StateError: Store used after dispose()`**: a disposed wrapper was - used as a receiver, a parameter, or a record field. Keep the wrapper - alive for as long as the object is in use. -- **`WeaveFFIException(-4): ...`**: a callback-interface method you - implemented threw; the message is the exception's `toString()`. -- **Crash or "Cannot invoke native callback outside an isolate"**: the - producer called a callback-interface method from a thread other than - the isolate's. That producer can't be used from Dart; see the - threading caveat above. -- **`UnsupportedError: Unsupported platform`**: the loader maps to - `darwin`, `linux`, and `windows`. Other platforms (Android, iOS) use - the Flutter integration where the framework opens the library. -- **`MissingPluginException` in Flutter**: that error is unrelated to - WeaveFFI; double-check that you depend on the generated package and - haven't shadowed it with a different `weaveffi` dependency. +- A value-returning callback method invoked from a thread other than the + isolate's aborts the process. That's a Dart VM rule for + `NativeCallable.isolateLocal`, and pure Dart can't detect the thread first: + an any-thread entry point can't re-enter the isolate to run the method. + Producers must call value-returning methods synchronously during a call + from Dart. +- Void callback methods are always delivered asynchronously, even when the + producer calls them on the isolate's thread, so their exceptions can't fail + the producer's call. +- `NativeCallable.isolateGroupBound` is marked experimental in the Dart SDK. +- Library loading uses `DynamicLibrary`; Flutter's native-assets build hooks + aren't generated. diff --git a/docs/src/generators/dotnet.md b/docs/src/generators/dotnet.md index 89328e35..f08b4a65 100644 --- a/docs/src/generators/dotnet.md +++ b/docs/src/generators/dotnet.md @@ -1,948 +1,220 @@ # .NET -## Overview - -The .NET target emits a C# class library that wraps the C ABI (revision -2) through -[P/Invoke](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke). -Records and rich enums are plain C# value classes packed and unpacked -from value buffers; interfaces are reference-counted objects wrapped in -`IDisposable` classes with a finalizer backstop; callback interfaces are -C# `interface`s the consumer implements, adapted to the producer's -vtable by `[UnmanagedCallersOnly]` trampolines; async functions return -`Task`; `iter` returns are lazily streamed `IEnumerable`; error -domains become managed exception types; and the project targets -`net8.0`. +The .NET target emits a standalone C# class library over the C ABI +(revision 3). Native calls go through source-generated +[`[LibraryImport]`](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation) +declarations whose slots are all blittable, so the bindings need no runtime +marshalling and are trim- and AOT-friendly. The project targets `net8.0` +and builds with any newer SDK. ## What gets generated -| File | Purpose | -|------|---------| -| `generated/dotnet/WeaveFFI.cs` | C# bindings: P/Invoke declarations, wrapper classes, enums, exceptions | -| `generated/dotnet/WeaveFFI.csproj` | SDK-style project (`net8.0`, `AllowUnsafeBlocks`) | -| `generated/dotnet/WeaveFFI.nuspec` | NuGet package metadata | -| `generated/dotnet/README.md` | Build and pack instructions | +For a package named `kvstore`: -File names and the C# namespace follow the IDL `package.name` (a package -named `kvstore` produces `Kvstore.cs` inside `namespace Kvstore`); -`WeaveFFI` is the default and `namespace` under `[generators.dotnet]` -overrides it. - -The `NativeMethods` class checks the producer's ABI revision in its -static constructor, so a mismatched library fails before the first real -P/Invoke instead of misreading the error struct or a value buffer: - -```csharp -internal static class NativeMethods -{ - private const string LibName = "weaveffi"; - - // The ABI revision these bindings were generated against. - internal const uint AbiVersion = 2; - - 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(); -} -``` - -## Type mapping - -| IDL type | C# type | P/Invoke type | -|--------------|----------------------------|---------------| -| `i8`, `i16`, `i32`, `i64` | `sbyte`, `short`, `int`, `long` | same | -| `u8`, `u16`, `u32`, `u64` | `byte`, `ushort`, `uint`, `ulong` | same | -| `f32`, `f64` | `float`, `double` | same | -| `bool` | `bool` | `int` | -| `string` | `string` | `IntPtr` | -| `bytes` | `byte[]` | `IntPtr` + `UIntPtr` | -| `StructName` | `StructName` (sealed value class) | value buffer (`IntPtr` + length) | -| `EnumName` (plain) | `EnumName` | `int` | -| `EnumName` (rich) | `EnumName` (closed class hierarchy) | value buffer (`IntPtr` + length) | -| `InterfaceName` | `InterfaceName` (`IDisposable` class) | `IntPtr` | -| `InterfaceName?` | `InterfaceName?` | `IntPtr` (`IntPtr.Zero` for `null`) | -| `CallbackName` | `ICallbackName` (C# interface) | `IntPtr` ctx + `IntPtr` vtable | -| `T?` | `T?` (nullable) | value buffer | -| `[T]` | `T[]` | value buffer | -| `{K: V}` | `Dictionary` | value buffer | -| `iter` | `IEnumerable` (lazy, single use) | `IntPtr` | - -Buffered types cross the boundary serialized in the -[value-buffer format](../reference/value-buffers.md). The generated -file carries an internal `WeaveFFIBufferWriter`/`WeaveFFIBufferReader` -pair plus one `WriteTo`/`ReadFrom` pair per record and rich enum. -Objects nested inside a buffered value travel as `u64` object tokens -(see [Objects](#objects-interfaces)). - -### 64-bit integers and floats - -`i64` and `u64` are native `long` and `ulong`, both across P/Invoke and -inside value buffers (`WriteI64`/`WriteU64`), so the full range -round-trips exactly. `f32`/`f64` are `float`/`double`; the `codec` -conformance consumer verifies NaN, both infinities, and `-0.0` survive -a round trip bit-for-bit. - -## Example IDL and generated code - -```yaml -version: "0.9.0" -modules: - - name: contacts - enums: - - name: ContactType - doc: Type of contact - variants: - - { name: Personal, value: 0 } - - { name: Work, value: 1 } - - { name: Other, value: 2 } - - structs: - - name: Contact - doc: A contact record - fields: - - { name: name, type: string } - - { name: email, type: "string?" } - - { name: age, type: i32 } - - { name: contact_type, type: ContactType } - - functions: - - name: create_contact - params: - - { name: name, type: string } - - { name: email, type: "string?" } - - { name: age, type: i32 } - return: Contact - - - name: list_contacts - params: [] - return: "[Contact]" +```text +dotnet/ + Kvstore.cs generated API: types, wrappers, native imports + Runtime.cs fixed runtime: exceptions, loader, codec, async and iterator helpers + Kvstore.csproj SDK-style project with the NuGet metadata + README.md ``` -Enums become C# enums with explicit values: +The namespace, assembly name, and NuGet id are the package name in +PascalCase (`my-kv` becomes `MyKv`). Set `namespace` to override all three: -```csharp -/// Type of contact -public enum ContactType -{ - Personal = 0, - Work = 1, - Other = 2, -} +```toml +[generators.dotnet] +namespace = "Acme.Storage" +strip_module_prefix = true # the default; false keeps `kv_get`-style names ``` -Structs are sealed value classes: one get-only PascalCase property per -field plus a positional constructor. They own no native resources, so -there's no handle, no `Dispose`, and no getter symbols: - -```csharp -/// A contact record -public sealed class Contact -{ - public string Name { get; } - public string? Email { get; } - public int Age { get; } - public ContactType ContactType { get; } - - public Contact(string name, string? email, int age, ContactType contactType) - { - Name = name; - Email = email; - Age = age; - ContactType = contactType; - } +Each module becomes a static class named by its full path (`kv` is `Kv`, +`kv.stats` is `KvStats`); records, enums, interfaces, and exceptions sit +directly in the namespace. - internal void WriteTo(WeaveFFIBufferWriter writer) { /* generated */ } - internal static Contact ReadFrom(WeaveFFIBufferReader reader) { /* generated */ } -} -``` +## Build and load -Functions live as static methods on a class named after the module. -Method names are PascalCase with the module prefix stripped -(`Contacts.CreateContact`, not `ContactsCreateContact`); set -`strip_module_prefix: false` in the .NET generator config (or under -`[global]`) to keep prefixed names. Nested IDL modules flatten into a -single class with a concatenated name (a `stats` module nested under -`kv` becomes `KvStats` with `KvStats.GetStats`). Buffered parameters are -packed into a pinned `byte[]` the producer borrows for the call, and -buffered returns are copied out, freed, and decoded. From the `kvstore` -sample: +Reference the generated project from yours, or pack it: -```csharp -public static class KvStats -{ - 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 decodedReader = new WeaveFFIBufferReader(resultBuf); - var decoded = Stats.ReadFrom(decodedReader); - decodedReader.ExpectEnd(); - return decoded; - } -} +```bash +dotnet add reference generated/dotnet/Kvstore.csproj +dotnet pack generated/dotnet/Kvstore.csproj -c Release ``` -## Typed errors +The bindings load the native library by its base name (`kvstore`), so .NET +probes `libkvstore.dylib`, `libkvstore.so`, or `kvstore.dll` next to the +app and on the platform's library path. Set `KVSTORE_LIBRARY` (the +`{PREFIX}_LIBRARY` variable) to a full path to load a specific file. The +override is a `DllImportResolver` on the generated assembly. -The library defines `WeaveFFIException` with a `Code` property and the -four runtime trap codes as constants: +`weaveffi package` additionally bundles prebuilt libraries under +`runtimes//native/`, where NuGet resolves them at restore time. -```csharp -/// Raised for any WeaveFFI failure that isn't a typed domain -/// error: producer bugs, panics, marshalling failures, and exceptions -/// thrown by a callback-interface implementation. -public class WeaveFFIException : Exception -{ - /// An untyped producer error. - public const int GenericErrorCode = -1; - /// The producer panicked; the message carries the panic text. - public const int PanicErrorCode = -2; - /// An argument couldn't be lifted by the producer. - public const int MarshalErrorCode = -3; - /// A callback-interface implementation threw; the message - /// carries the original exception's message. - public const int ForeignErrorCode = -4; - - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } -} -``` - -A module's error domain adds a derived exception named by replacing the -trailing `Error` stem with `Exception` (`KvError` becomes -`KvException`), carrying one `const int` per code and a `FromCode` -factory. From the `kvstore` sample: +The first native call runs `NativeMethods`' static constructor, which +installs the resolver, checks `{prefix}_abi_version()` against 3, and +compares every top-level module's contract checksum with the value the +bindings were generated from: ```csharp -/// Typed exception for the KvError error domain (module kv). -public class KvException : WeaveFFIException +static partial void VerifyChecksums() { - /// key not found - public const int KeyNotFound = 1001; - /// entry expired - public const int Expired = 1002; - /// store has reached capacity - public const int StoreFull = 1003; - /// 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); - // ... Expired, StoreFull, IoError ... - default: - return new WeaveFFIException(code, message); - } - } + VerifyChecksum("shared", &kitchen_sink_shared_checksum, 0x42c4ce2c8d0af052UL); + VerifyChecksum("kitchen", &kitchen_sink_kitchen_checksum, 0xd83da75b24b545ffUL); } ``` -Only callables marked `throws: true` in the IDL surface the typed -exception: their wrappers check the error slot with -`WeaveFFIError.CheckKv`, which throws `KvException` for domain codes and -plain `WeaveFFIException` for anything else (producer panics, -marshalling failures, a throwing callback), and their doc comments carry -an `` tag. A callable without `throws` -uses the generic `WeaveFFIError.Check`, which throws `WeaveFFIException` -if the producer misbehaves. Both copy the message (and payload) out of -the slot and release it with `weaveffi_error_clear` before throwing. +A mismatch throws `InvalidOperationException` naming the module (surfaced +as the `InnerException` of a `TypeInitializationException`), so stale +bindings fail before any call can misread memory. -An error code that declares payload `fields:` carries them serialized in -the error's payload buffer; `FromCode` decodes them and exposes each -field through the exception's `Data` dictionary. - -```csharp -try -{ - store.Delete("missing"); -} -catch (KvException e) when (e.Code == KvException.KeyNotFound) -{ - // specific code -} -``` - -### Runtime error codes - -| Code | Constant | Meaning | Where it surfaces | -|------|----------|---------|-------------------| -| `-1` | `GenericErrorCode` | The producer reported an error without a declared code. | Thrown as `WeaveFFIException`. | -| `-2` | `PanicErrorCode` | The Rust implementation panicked; the export macros and the async spawner catch the unwind. | Thrown as `WeaveFFIException`, or faults the awaited `Task`. | -| `-3` | `MarshalErrorCode` | Malformed input at the boundary (invalid UTF-8, a truncated value buffer, a bad enum discriminant). | Thrown as `WeaveFFIException`. | -| `-4` | `ForeignErrorCode` | A callback-interface method implemented in C# threw. | Thrown as `WeaveFFIException` from the producer call that invoked the callback (see [Callback interfaces](#callback-interfaces)). | - -There's no non-throwing call path in C#: a non-throwing callable whose -error slot comes back non-zero still throws `WeaveFFIException`, so a -producer bug or a throwing callback never goes unnoticed. Misuse of a -disposed wrapper is reported separately as `ObjectDisposedException`. - -## Objects (interfaces) - -An `interfaces:` entry becomes a class implementing `IDisposable` that -owns one strong reference to a reference-counted producer object. From -the `kvstore` sample (trimmed): - -```csharp -public class Store : IDisposable -{ - private IntPtr _handle; - private int _released; - - /// Adopts one strong reference to a native object. - internal Store(WeaveFFIHandle handle) - { - _handle = handle.Value; - } - - /// The borrowed native pointer for the duration of a call. - /// The wrapper was disposed. - internal IntPtr Handle - { - get - { - var h = _handle; - if (h == IntPtr.Zero) - { - throw new ObjectDisposedException(nameof(Store)); - } - return h; - } - } - - /// Mints a second strong reference the caller owns (for - /// example to write this object into a value buffer). - internal IntPtr CloneHandle() - { - return NativeMethods.weaveffi_kv_Store_clone(Handle); - } +## Type mapping - /// 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(new WeaveFFIHandle(result)); - } - finally - { - Marshal.FreeCoTaskMem(pathPtr); - } - } +| IDL type | C# type | Crosses the ABI as | +|---|---|---| +| `i8` ... `u64`, `f32`, `f64` | `sbyte` ... `ulong`, `float`, `double` | by value | +| `bool` | `bool` | one byte | +| `string` | `string` | UTF-8 `(ptr, len)`; interior NULs survive | +| `bytes` | `byte[]` (parameters also take `ReadOnlySpan`) | `(ptr, len)` | +| C-style enum | `enum` with the IDL discriminants | `int32_t` | +| record | `sealed class` with get-only properties | value buffer | +| rich enum | `abstract class` with one nested sealed class per variant | value buffer | +| `T?` | `T?` | value buffer (`Iface?` is a nullable pointer) | +| `[T]` | `T[]` | value buffer | +| `{K: V}` | `Dictionary` | value buffer | +| interface | `sealed class : IDisposable` | object pointer | +| callback interface | `interface I{Name}` | `ctx` plus a static vtable | +| `iter` | `IEnumerable` | iterator handle | - /// Releases this wrapper's reference. The native object is - /// dropped when the producer releases its last reference; other - /// wrappers to the same object stay valid. - public void Dispose() - { - Release(); - GC.SuppressFinalize(this); - } +Strings are encoded to UTF-8 and pinned for the call; returned strings, +bytes, and buffers are copied and released with `{prefix}_free_bytes`. - ~Store() - { - Release(); - } +## Objects and lifetime - private void Release() - { - if (System.Threading.Interlocked.Exchange(ref _released, 1) != 0) - { - return; - } - var h = _handle; - _handle = IntPtr.Zero; - if (h != IntPtr.Zero) - { - NativeMethods.weaveffi_kv_Store_destroy(h); - } - } -} -``` +An interface wrapper holds one strong reference in a nested `SafeHandle` +subclass whose `ReleaseHandle` calls `_destroy`. Every import takes that +handle, not a raw pointer, so the interop stub adds a reference for the +duration of the call: a wrapper that's disposed on another thread or +collected mid-call can't free the object under it. `Dispose` releases the +reference (an in-flight call finishes first); the `SafeHandle`'s finalizer +covers wrappers that are never disposed. Calling a disposed wrapper, or +passing one as an argument, throws `ObjectDisposedException`. -- **Construction.** A constructor named `new` becomes a public C# - constructor (the `events` sample's `new EventBus()`); any other - constructor is a static factory (`Store.Open(path)`). Methods are - PascalCase instance methods, statics are static methods, and - deprecated members carry `[Obsolete]`. The adopting constructor takes - a distinct `WeaveFFIHandle` struct rather than a bare `IntPtr`, so it - never competes with a public constructor taking an integer. -- **Disposal.** `Dispose()` releases this wrapper's reference through - the `_destroy` symbol. It's idempotent (an `Interlocked.Exchange` - guard), and the finalizer is a backstop for wrappers that were never - disposed. Use `using`. The producer object itself is dropped only when - the last reference anywhere is released. -- **Use after dispose.** Every call reads the internal `Handle` - property, which throws `ObjectDisposedException` once the wrapper has - been released, whether the wrapper is the receiver, a parameter, or a - field of a record being packed. -- **Copies mint new references.** Methods that return the receiver or an - existing object (`Share()`, `Fork()`) return a fresh strong reference - adopted into a new wrapper; disposing one wrapper never affects - another pointing at the same object. - -### Nullable objects, and objects inside values - -An `Interface?` parameter passes `IntPtr.Zero` for `null`, and an -`Interface?` return maps `IntPtr.Zero` to `null`: +Returned objects, object fields of decoded records, and objects handed to +a callback each arrive as a new wrapper that owns its reference. Two +wrappers are equal (`Equals`, `GetHashCode`) when they reference the same +native object. Writing an +object into a value buffer clones its reference first, so the producer +can keep it. -```csharp -public Store? Larger(Store? other) -{ - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kv_Store_larger(Handle, other?.Handle ?? IntPtr.Zero, ref err); - WeaveFFIError.Check(err); - return result == IntPtr.Zero ? null : new Store(new WeaveFFIHandle(result)); -} -``` +## Errors -Objects inside records, lists, dictionaries, and optionals travel as -`u64` object tokens in the value buffer. Writing a token mints a new -strong reference with `CloneHandle()`; reading one adopts the reference -into a fresh wrapper. From the `StoreInfo` record (`store: Store`, -`mirror: Store?`): +Every failure derives from `NativeException`, whose `Code` is the native +error code. Runtime codes are constants on it: `GenericErrorCode` (-1), +`PanicErrorCode` (-2), `MarshalErrorCode` (-3), and `ForeignErrorCode` +(-4). Each error domain becomes a typed subclass with one constant per +code; payload fields land in `Exception.Data` keyed by field name: ```csharp -internal void WriteTo(WeaveFFIBufferWriter writer) +/// Poke the gadget a number of times, failing on a negative count +/// The call reported a KitchenErrorsError code. +public int Poke(int times) { - writer.WriteString(Label); - writer.WriteObject(Store.CloneHandle()); - if (Mirror != null) - { - writer.WriteOptionFlag(true); - writer.WriteObject(Mirror!.CloneHandle()); - } - else - { - writer.WriteOptionFlag(false); - } - writer.WriteI64(Count); -} - -internal static StoreInfo ReadFrom(WeaveFFIBufferReader reader) -{ - var fLabel = reader.ReadString(); - var fStore = new Store(new WeaveFFIHandle(reader.ReadObject())); - Store? fMirror = null; - if (reader.ReadOptionFlag()) - { - var fMirrorValue = new Store(new WeaveFFIHandle(reader.ReadObject())); - fMirror = fMirrorValue; - } - var fCount = reader.ReadI64(); - return new StoreInfo(fLabel, fStore, fMirror, fCount); + var ffiErr = default(FfiError); + var ffiResult = NativeMethods.kitchen_sink_kitchen_Gadget_poke(Handle, times, &ffiErr); + if (ffiErr.Code != 0) throw Ffi.TakeError(&ffiErr, global::KitchenSink.KitchenErrorsException.FromError); + return ffiResult; } ``` -`ReadObject` rejects a zero token as a malformed buffer. Lists of -objects work the same way in both directions (`Store.OpenMany(paths)` -returns `Store[]`, `Store.TotalCount(stores, extra)` takes one); each -wrapper in a returned array owns its own reference and must be disposed -individually. Iterators over objects adopt one reference per step, and -async functions returning an object adopt the pointer inside the -completion callback before completing the `Task`. - -## Rich (algebraic) enums +The cancelled code (-5) surfaces as `OperationCanceledException`. If the +API declares a type named `NativeException`, the base class takes the +namespace as a prefix (`KvstoreNativeException`). -A *rich* (algebraic) enum, a sum type whose variants carry associated -data, becomes a closed class hierarchy: an abstract base class with a -private constructor plus one nested sealed class per variant, each -shaped like a record (typed get-only properties and a positional -constructor). Rich enums own no native resources and declare no C -symbols. (A plain C-style enum with no payloads stays a normal C# `enum` -backed by `int`; see above.) +## Async and cancellation -For the `shapes` module's `Shape` enum (`Empty`, `Circle { radius: f64 }`, -`Rectangle { width: f32, height: f32 }`, and -`Labeled { label: string, count: u8 }`), the generator emits (abridged): +An async function returns `Task` or `Task`. The launcher receives an +`[UnmanagedCallersOnly]` completion and a `GCHandle` to the pending call; +the completion runs on a producer thread, decodes the result, releases +everything the call owned, and completes the task (continuations run +asynchronously). A `cancellable` function also takes a +`CancellationToken`: ```csharp -/// An algebraic shape (sum type with associated data) -public abstract class Shape +public static Task DoCancellable(string input, CancellationToken cancellationToken = default) { - private Shape() - { - } - - /// The empty shape - public sealed class Empty : Shape + if (cancellationToken.IsCancellationRequested) { + return Task.FromCanceled(cancellationToken); } - - /// A circle with a radius - public sealed class Circle : Shape + var inputBytes = Ffi.Utf8(input); + var ffiCall = new FfiCall(cancellationToken); + try { - public double Radius { get; } - - public Circle(double radius) + fixed (byte* inputPtr = inputBytes) { - Radius = radius; + NativeMethods.kitchen_sink_kitchen_do_cancellable(inputPtr, (nuint)inputBytes.Length, ffiCall.CancelToken(), &CompleteDoCancellable, ffiCall.Context); } } - - /// A labeled shape with a small count - public sealed class Labeled : Shape + catch { - public string Label { get; } - public byte Count { get; } - - public Labeled(string label, byte count) - { - Label = label; - Count = count; - } + ffiCall.Abandon(); + throw; } - - internal void WriteTo(WeaveFFIBufferWriter writer) { /* generated */ } - internal static Shape ReadFrom(WeaveFFIBufferReader reader) { /* generated */ } -} -``` - -On the wire a `Shape` is a value buffer holding the `i32` variant tag -followed by the active variant's fields in declaration order. Construct -variants with `new`, and match on the nested classes with pattern -matching: - -```csharp -var c = new Shape.Circle(2.0); -if (c is Shape.Circle circle) -{ - Console.WriteLine(circle.Radius); // 2 + return ffiCall.Task; } -var bigger = Shapes.Scale(c, 3.0); // returns a new Shape -Console.WriteLine(Shapes.Describe(bigger)); ``` -Values are plain managed data: there's nothing to dispose and no native -handle to track. Variant fields of interface type follow the object -token rules above. +`CancelToken()` creates the native token and registers the +`CancellationToken` to cancel it. When the producer completes with -5, the +task becomes canceled, so `await` throws `TaskCanceledException`. The +consumer's token reference is destroyed after completion, and a +registration racing the completion never touches a destroyed token. -## Callback interfaces +## Callbacks -A `callback_interfaces:` entry becomes a C# `interface` (prefixed with -`I`) the consumer implements and passes wherever the API takes that -type. From the `kvstore` sample: +A callback interface becomes a C# interface the consumer implements: ```csharp -/// Implement this interface and pass an instance wherever the -/// native library expects it. The library may call any method from any -/// thread until it releases its last reference to the instance. Object -/// arguments are owned by the implementation, which should dispose them -/// when done. An exception thrown by a method is reported to the native -/// caller as . -public interface IEvictionListener +public interface IReadyListener { - bool OnEvict(Entry entry, EvictionReason reason); + /// Fires when an item is ready + void OnReady(int code, string msg); + /// Receives the item itself and says whether to keep listening + bool OnItem(Item item, Gadget gadget); } ``` -```csharp -sealed class Auditor : IEvictionListener -{ - public bool OnEvict(Entry entry, EvictionReason reason) - { - Console.WriteLine($"{entry.Key}: {reason}"); - return true; - } -} - -store.SetEvictionListener(new Auditor()); -``` - -Behind the interface is one process-wide static vtable per callback -interface, allocated once with `Marshal.AllocHGlobal` and never freed, -whose slots are unmanaged function pointers to `[UnmanagedCallersOnly]` -trampolines. Passing an implementation allocates a `GCHandle` for it and -hands the producer the handle's `IntPtr` as `ctx`: - -```csharp -internal static unsafe class WeaveFFIVtable_kv_EvictionListener -{ - internal static readonly IntPtr Pointer = Allocate(); - - private static IntPtr Allocate() - { - var layout = new Layout - { - on_evict = (IntPtr)(delegate* unmanaged[Cdecl])&OnEvictTrampoline, - free = (IntPtr)(delegate* unmanaged[Cdecl])&FreeTrampoline, - }; - var mem = Marshal.AllocHGlobal(Marshal.SizeOf()); - Marshal.StructureToPtr(layout, mem, false); - return mem; - } - - private static IEvictionListener Target(IntPtr ctx) - { - return (IEvictionListener)GCHandle.FromIntPtr(ctx).Target!; - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static byte OnEvictTrampoline(IntPtr ctx, IntPtr entry_ptr, UIntPtr entry_len, int reason, IntPtr out_err) - { - try - { - var impl = Target(ctx); - var arg0Buf = new byte[(int)entry_len]; - if (entry_ptr != IntPtr.Zero && (int)entry_len > 0) Marshal.Copy(entry_ptr, arg0Buf, 0, (int)entry_len); - var arg0Reader = new WeaveFFIBufferReader(arg0Buf); - var arg0 = Entry.ReadFrom(arg0Reader); - arg0Reader.ExpectEnd(); - return (byte)(impl.OnEvict(arg0, (EvictionReason)reason) ? 1 : 0); - } - catch (Exception ex) - { - NativeMethods.weaveffi_error_set(out_err, WeaveFFIException.ForeignErrorCode, ex.Message); - return default; - } - } - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] - private static void FreeTrampoline(IntPtr ctx) - { - GCHandle.FromIntPtr(ctx).Free(); - } -} -``` - -```csharp -public void SetEvictionListener(IEvictionListener listener) -{ - var err = new WeaveFFIError(); - var listenerCtx = GCHandle.ToIntPtr(GCHandle.Alloc(listener)); - NativeMethods.weaveffi_kv_Store_set_eviction_listener(Handle, listenerCtx, WeaveFFIVtable_kv_EvictionListener.Pointer, ref err); - WeaveFFIError.Check(err); -} -``` - -- **Lifetime.** The `GCHandle` keeps the implementation alive exactly as - long as the producer may call it; the vtable's `free` trampoline frees - the handle when the producer drops its last reference. A producer that - retains the implementation (a store's eviction listener) keeps it - alive across calls; one that doesn't (the `events` sample's - `Events.RouteOnce`) frees it before returning. Passing the same - object twice allocates two handles. -- **Argument ownership.** Borrowed strings and buffers are copied into - managed memory before the method runs, so the implementation may keep - them. An object passed to a callback method is owned by the - implementation: the trampoline adopts it into a new wrapper - (`impl.OnAttached(new EventBus(new WeaveFFIHandle(bus)))` in the - `events` sample), and the implementation should dispose it when done. -- **Return values.** A method's return value is converted back to its C - representation (`bool` to `0`/`1`, a plain enum to its `int`, a record - to a value buffer the producer frees). -- **Exceptions.** An exception escaping a method never crosses the - unmanaged frame. The trampoline writes `ForeignErrorCode` (-4) with - `ex.Message` into the producer's error slot and returns a default - value; the producer aborts the call in progress, and the original - caller gets `WeaveFFIException` with `Code == -4`. For a callable - marked `throws`, `FromCode` falls through to `WeaveFFIException` - because -4 is outside the domain, so `catch (KvException)` doesn't - catch it but `catch (WeaveFFIException)` does. The runtime is never - taken down. -- **Threads.** The producer may call a method from any thread; - `[UnmanagedCallersOnly]` entry points are safe to enter from native - threads the runtime has never seen. The method runs on that thread, - not on any captured `SynchronizationContext`, so post to your UI - thread or dispatcher yourself. A callback that blocks waiting for the - thread that made the producer call will deadlock if the producer - invoked it synchronously from that call. - -## Build instructions - -1. Generate the bindings: - - ```bash - weaveffi generate api.yaml -o generated/ --target dotnet - ``` - -2. Build: - - ```bash - cd generated/dotnet - dotnet build - ``` - -3. Pack as NuGet: - - ```bash - dotnet pack -c Release - ``` - - The resulting `.nupkg` lives in `bin/Release/`. For a package that - carries the native library, use `weaveffi package` (below). - -4. Make the cdylib findable at runtime: place it next to the built DLL, - set `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH`, or register a - `DllImportResolver` for the generated assembly that returns the - absolute path (the conformance consumers do this from a - `WEAVEFFI_LIBRARY` variable). - -5. Use the bindings: - - ```csharp - using var store = Store.Open("/tmp/cache.kv"); - store.Put("alpha", new byte[] { 1 }, EntryKind.Persistent, null); - Console.WriteLine($"{store.Count()} / {Store.DefaultCapacity()}"); - foreach (var key in store.ListKeys(null)) - { - Console.WriteLine(key); - } - long reclaimed = await store.Compact(); - ``` - -## Packaging - -`weaveffi package --target dotnet` emits a single NuGet project under -`dotnet/` whose `.csproj` packs `runtimes/**` as content, and copies -each supplied desktop binary to `runtimes//native/`, the layout -NuGet resolves automatically at restore time. The generated `LibName` -constant is rebound from `weaveffi` to the bundled library's base name. - -| Platform | RID | -|----------|-----| -| `macos-arm64` | `osx-arm64` | -| `macos-x64` | `osx-x64` | -| `linux-x64` | `linux-x64` | -| `linux-arm64` | `linux-arm64` | -| `windows-x64` | `win-x64` | - -Android and `wasm32` binaries have no RID slot and are skipped. See -[Packaging](../guides/packaging.md) for the shared workflow. - -## Memory and ownership - -- Each interface class implements `IDisposable`; use `using` for - deterministic cleanup. The finalizer is a safety net only and runs on - a non-deterministic schedule. Records and rich enums are plain managed - values with nothing to dispose. -- Returned strings are copied into managed memory and the raw pointer is - freed via `weaveffi_free_string` immediately. -- Strings passed as parameters are marshalled with - `Marshal.StringToCoTaskMemUTF8` and freed in a `finally` block. -- Buffered values (records, rich enums, optionals, arrays, and - dictionaries) cross as one value buffer: parameters are packed into a - pinned `byte[]` that the producer borrows for the call; returns are - copied into managed memory, released with `weaveffi_free_bytes`, and - decoded. Object tokens written into a buffer are fresh strong - references the producer owns; tokens read out are adopted into - wrappers. -- Callback implementations are pinned by a `GCHandle` until the producer - calls the vtable's `free`. - -## Async support - -Async IDL functions are exposed as `async Task` methods (named like -every other wrapper: no extra `Async` suffix is appended). The wrapper -wires the C ABI completion callback into a `TaskCompletionSource` and -keeps the callback delegate alive with a `GCHandle` while the call is in -flight. From the `kvstore` sample's `Store.Compact`: - -```csharp -/// 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; -} -``` - -- The `GCHandle` prevents the GC from collecting the delegate (and the - native thunk the producer will call) before completion. It's freed - exactly once: in the callback's `finally`, or on the `catch` path if - the native call itself throws synchronously. -- The completion callback runs on the producer's native thread; - `RunContinuationsAsynchronously` keeps awaiting code from running - inline on that thread. -- For a callable marked `throws: true`, an error faults the task with - the domain exception via its `FromCode` factory; otherwise a failure - can only be a producer bug and faults the task with - `WeaveFFIException`. A panic inside the spawned future surfaces as - `PanicErrorCode` (-2). -- Result ownership follows the async contract: string, bytes, and - buffered results (arriving as a `(result, resultLen)` pair) are owned - by the consumer, so the callback copies or decodes them into managed - values and then releases them with `weaveffi_free_string` or - `weaveffi_free_bytes`. A reported error is heap-boxed and released - with `weaveffi_error_free` after its fields are copied. An owned - interface result transfers ownership too: the callback adopts the - pointer into a new wrapper. - -Async interface methods follow the same pattern as instance methods: -`await store.Compact()` returns `Task`, and the receiver's -`Handle` is read (throwing `ObjectDisposedException` if needed) before -the launcher runs. - -For functions marked `cancellable: true` the wrapper passes -`IntPtr.Zero` for the C ABI's cancel-token slot; no `CancellationToken` -parameter is exposed. Only the C and C++ targets expose cancellation -tokens. +Passing an implementation pins it in a `GCHandle` (the `ctx`) and hands the +producer one process-wide vtable of `[UnmanagedCallersOnly]` trampolines; +the vtable's `free` entry releases the handle when the producer drops its +last reference. Trampolines run on whatever thread the producer calls +from. String, bytes, and buffer arguments are copied; object arguments are +adopted. An exception thrown by the implementation never unwinds into +native code: the trampoline reports it with `{prefix}_error_set(out_err, +-4, message)`, and the call that triggered it throws `NativeException` with +`ForeignErrorCode` and the original message. That holds whether the +producer's trait method returns `T` or `Result`. ## Iterators -Functions returning `iter` return a lazy, single-use `IEnumerable` -(`WeaveFFIOnceEnumerable`) backed by a C# iterator method that pulls -one item through the C `_next` function per step. From the `kvstore` -sample's `Store.ListKeys`: - -```csharp -public IEnumerable ListKeys(string? prefix) -{ - // ... pack the `string?` prefix into a pinned value buffer ... - var iter = NativeMethods.weaveffi_kv_Store_list_keys(Handle, prefixPin.AddrOfPinnedObject(), (UIntPtr)prefixBuf.Length, ref err); - WeaveFFIError.CheckKv(err); - return new WeaveFFIOnceEnumerable(EnumerateListKeys(iter)); -} - -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); - } -} -``` - -- The native handle is destroyed in the `finally` block exactly once: - when enumeration completes, when a step throws, or when the enumerator - is disposed early (a `foreach` disposes it automatically, including on - `break`). -- The sequence can be enumerated only once; a second `GetEnumerator()` - throws `InvalidOperationException`. Materialise with `.ToList()` if - you need to iterate twice. -- Each yielded element is owned by the consumer: strings are copied and - freed, buffered elements are copied, freed, and decoded, and an object - element is adopted into a new wrapper (or `null` for an absent - `Interface?` element). -- A throwing function checks the launch and each step with the domain - checker (`ListKeys` throws `KvException` from the failing step); a - non-throwing one throws `WeaveFFIException` only for producer bugs. +An `iter` function launches eagerly, so launch errors throw at the call, +and returns a single-use `IEnumerable` that pulls one item per +`MoveNext`. The native iterator is a `SafeHandle`, destroyed when +enumeration ends, when the enumerator is disposed (`foreach` does this on +early exit), or by its finalizer. Enumerating the same sequence twice +throws `InvalidOperationException`. ## Known limitations -- Async cancellation doesn't propagate: no `CancellationToken` is - accepted, and an abandoned `Task` leaves the native operation running. -- Callback methods run on whatever thread the producer uses; nothing - marshals them to a `SynchronizationContext`. -- `iter` sequences are single-pass. -- The plain `generate` output relies on the default `DllImport` probing - for a library named `weaveffi`; only `weaveffi package` bundles the - native library under `runtimes//native/`. -- Function pointers and `[UnmanagedCallersOnly]` require .NET 5+ and - `AllowUnsafeBlocks`; the generated project targets `net8.0`. - -## Troubleshooting - -- **`DllNotFoundException: Unable to load DLL 'weaveffi'`**: the runtime - cannot find the shared library. Place it in the application directory, - set `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH`, register a - `DllImportResolver`, or ship it via `weaveffi package`. -- **`TypeInitializationException` wrapping "WeaveFFI ABI mismatch"**: - the library was built by a different `weaveffi` release than the - bindings. Regenerate the bindings and rebuild the library together. -- **`ObjectDisposedException`**: a disposed wrapper was used as a - receiver, a parameter, or a record field. Keep the wrapper alive for - as long as the object is in use; disposing one wrapper doesn't affect - others pointing at the same object. -- **`WeaveFFIException` with `Code == -4`**: a callback-interface method - you implemented threw; the message is the original exception's - message. Catch inside the method if the producer call should succeed - anyway. -- **`InvalidOperationException: this sequence can be enumerated only - once`**: an `iter` result was enumerated twice; call the function - again or materialise the first pass. -- **Strings returned with garbage characters**: make sure your binding - is targeting `UTF8` (`Marshal.PtrToStringUTF8`, - `StringToCoTaskMemUTF8`); the generated helpers do this for you. +- Every module's types share one namespace, so two modules declaring the + same type name collide. +- A module class whose name equals the namespace (an `events` module in + the `Events` package) must be reached as `Events.Events` or through + `using static`. +- Only one `DllImportResolver` can exist per assembly. If you compile the + generated sources into an assembly that already has one, the + `{PREFIX}_LIBRARY` override is skipped and default probing applies. +- Async functions that aren't `cancellable` take no `CancellationToken`. +- Object references cloned into a value buffer leak if encoding a later + field throws before the call (for example a disposed wrapper in the same + list). diff --git a/docs/src/generators/go.md b/docs/src/generators/go.md index 1a8431ac..cb2366b4 100644 --- a/docs/src/generators/go.md +++ b/docs/src/generators/go.md @@ -1,873 +1,222 @@ # Go -## Overview - -The Go target produces idiomatic Go bindings that use cgo to call the C -ABI (revision 2). The generator emits one Go source file (`weaveffi.go`) -plus a `go.mod` so the result can be imported by any Go module. -Functions marked `throws: true` return `(value, error)` to match Go -conventions; all other wrappers return plain values and panic on the -runtime trap codes. Records and rich enums are plain Go value types -packed and unpacked from value buffers. Interfaces are reference-counted -objects behind pointer wrappers with `Close()` and a finalizer backstop. -Callback interfaces are Go `interface` types the consumer implements, -crossing as a `cgo.Handle` plus one static vtable of exported -trampolines. Async functions block the calling goroutine on a channel, -and `iter` returns produce standard-library `iter.Seq`/`iter.Seq2` -sequences, so the generated module requires Go 1.23 or later (the -emitted `go.mod` declares `go 1.23`). - -## What gets generated - -| File | Purpose | -|------|---------| -| `go/weaveffi.go` | cgo bindings: preamble, ABI check, codecs, type wrappers, function wrappers | -| `go/go.mod` | Go module descriptor (configurable module path) | -| `go/README.md` | Prerequisites and build instructions | - -The package checks the producer's ABI revision in `init()` and panics -on mismatch, so a stale library fails at load time: +The Go target emits a self-contained cgo module that binds the library's C +ABI. Records and rich enums are plain Go values, interfaces are +reference-counted wrappers with `Close`, async functions take a +`context.Context`, and `iter` returns standard-library `iter.Seq` +sequences, so the module requires Go 1.23 or later. -```go -// The ABI revision these bindings were generated against. -const wvABIVersion uint32 = 2 - -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)) - } -} -``` - -## Type mapping +## What's generated -| IDL type | Go type | C type (cgo) | -|--------------|---------------|----------------------------| -| `i8`, `i16`, `i32`, `i64` | `int8`, `int16`, `int32`, `int64` | `C.int8_t` ... `C.int64_t` | -| `u8`, `u16`, `u32`, `u64` | `uint8`, `uint16`, `uint32`, `uint64` | `C.uint8_t` ... `C.uint64_t` | -| `f32`, `f64` | `float32`, `float64` | `C.float`, `C.double` | -| `bool` | `bool` | `C._Bool` | -| `string` | `string` | `*C.char` (via `C.CString`/`C.GoString`) | -| `bytes` | `[]byte` | `*C.uint8_t` + `C.size_t` | -| `Struct` | `StructName` (plain struct) | value buffer (`*C.uint8_t` + `C.size_t`) | -| `Enum` (plain) | `EnumName` (`int32` alias) | `C.weaveffi_mod_Enum` | -| `Enum` (rich) | `EnumName` (sealed interface + variant structs) | value buffer | -| `Interface` | `*InterfaceName` | `*C.weaveffi_mod_Interface` | -| `Interface?` | `*InterfaceName` (nil-able) | `*C.weaveffi_mod_Interface` (NULL for `nil`) | -| `CallbackInterface` | `CallbackName` (Go `interface`) | `void*` ctx + `const vtable*` | -| `T?` | `*T` | value buffer | -| `[T]` | `[]T` | value buffer | -| `{K: V}` | `map[K]V` | value buffer | -| `iter` | `iter.Seq[T]`, or `iter.Seq2[T, error]` when the function throws | opaque iterator pointer + `_next`/`_destroy` | - -Buffered types cross the boundary serialized in the -[value-buffer format](../reference/value-buffers.md); the package -carries a private `wvWriter`/`wvReader` pair plus one `wvPack*` and one -`wvUnpack*` routine per record and rich enum. Objects nested inside a -buffered value travel as `u64` object tokens (see -[Objects](#objects-interfaces)). Booleans map to `C._Bool`, matching -cgo's representation of `_Bool`. - -### 64-bit integers and floats - -`i64` and `u64` are native `int64` and `uint64`, both across cgo and -inside value buffers (`writeI64`/`writeU64`), so the full range -round-trips exactly. `f32`/`f64` are `float32`/`float64`, packed with -`math.Float64bits`; the `codec` conformance consumer verifies NaN, both -infinities, and `-0.0` survive a round trip bit-for-bit. - -## Example IDL and generated code - -```yaml -version: "0.9.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: email, type: "string?" } - - { name: contact_type, type: ContactType } - - functions: - - name: create_contact - params: - - { name: first_name, type: string } - - { name: email, type: "string?" } - - { name: contact_type, type: ContactType } - return: Contact - - - name: list_contacts - params: [] - return: "[Contact]" - - - name: count_contacts - params: [] - return: i32 -``` - -The generated `weaveffi.go` opens with the cgo preamble. The preamble -also declares the exported trampolines and static vtables for every -callback interface and async function in the IDL (from the `kvstore` -sample): +For a library whose identity is `kvstore`: -```go -package kvstore - -/* -#cgo LDFLAGS: -lkvstore -#include "weaveffi.h" -#include -static void* wvHandlePtr(uintptr_t h) { return (void*)h; } -extern bool goWv_weaveffi_kv_EvictionListener_on_evict(void* ctx, uint8_t* entry_ptr, size_t entry_len, weaveffi_kv_EvictionReason reason, weaveffi_error* out_err); -extern void goWv_weaveffi_kv_EvictionListener_free(void* ctx); -static const weaveffi_kv_EvictionListener_vtable wvVtable_weaveffi_kv_EvictionListener = { - (bool (*)(void*, const uint8_t*, size_t, weaveffi_kv_EvictionReason, weaveffi_error*))goWv_weaveffi_kv_EvictionListener_on_evict, - goWv_weaveffi_kv_EvictionListener_free, -}; -static const weaveffi_kv_EvictionListener_vtable* wvVtablePtr_weaveffi_kv_EvictionListener(void) { return &wvVtable_weaveffi_kv_EvictionListener; } -extern void goWv_weaveffi_kv_Store_compact_callback(void* context, weaveffi_error* err, int64_t result); -*/ -import "C" +```text +go/ + go.mod module path (the package name, or `module_path`) + README.md + kvstore.h a copy of the C header + bindings.go the API: types, wrappers, trampolines, load-time checks + runtime.go errors, string and object helpers, the async bridge + codec.go the value-buffer writer and reader (only when needed) ``` -Enums become typed integer aliases: - -```go -type ContactType int32 - -const ( - ContactTypePersonal ContactType = 0 - ContactTypeWork ContactType = 1 - ContactTypeOther ContactType = 2 -) -``` +Every name comes from the package identity: -Structs become plain Go structs with exported, typed fields: - -```go -// Contact is a plain value; there's no native handle and no Close. -type Contact struct { - Id int64 - FirstName string - Email *string - ContactType ContactType -} -``` - -Function wrappers are PascalCase with the IDL module prefix stripped -(`CreateContact`, not `ContactsCreateContact`); set -`strip_module_prefix: false` in the Go generator config (or under -`[global]`) to keep prefixed names. A function marked `throws: true` -returns `(value, error)`; a buffered return is copied into Go memory, -released with `weaveffi_free_bytes`, and decoded. From the `kvstore` -sample's cross-module `GetStats`: - -```go -func GetStats(store *Store) (Stats, error) { - defer runtime.KeepAlive(store) - 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 -} -``` - -A function without `throws` returns a plain value; its error slot is -checked by `wvTrap`, which panics, because a non-zero code there can -only be a runtime trap: - -```go -// 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, a marshalling failure, or a callback-interface -// implementation that panicked. The panic value is a *WeaveFFIError, so a -// recover() site can still inspect the code. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - panic(wvBrandError(wvTakeError(cErr))) - } -} -``` - -The Go module path follows the `[package]` name in `weaveffi.toml` -(falling back to `weaveffi`); override it directly with: +| Name | Default | Override | +|------|---------|----------| +| Module path | the package `name` | `[generators.go] module_path` | +| Package name | the C prefix | none | +| Linked library | `-l{library}` | none | +| Header | `{library}.h` | none | ```toml [generators.go] -module_path = "github.com/myorg/mylib" +module_path = "github.com/example/kvstore" +strip_module_prefix = true # the default ``` -## Typed errors - -The package defines a generic `WeaveFFIError` struct with `Code` and -`Message` fields: - -```go -// WeaveFFIError reports a failure crossing the C boundary that no typed -// error domain claims: an unknown code, a marshalling failure, a -// producer panic (code -2), or a Go callback-interface implementation -// that panicked (code -4). -type WeaveFFIError struct { - // Code is the numeric ABI error code. - Code int32 - // Message is the human-readable error message. - Message string -} -``` +With `strip_module_prefix`, module `kv`'s `delete` surfaces as `Delete`. +All modules render into one Go package, so a function whose stripped name +would clash with a type, a constant, or another module's function keeps its +module prefix (`directory.card` next to a `Card` record becomes +`DirectoryCard`). -A module's error domain adds a typed error struct named after the -domain, package-level code constants, and a mapper that falls back to -`*WeaveFFIError` for codes outside the domain. From the `kvstore` -sample: - -```go -// 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 key not found - KvErrorKeyNotFound int32 = 1001 - // KvErrorExpired entry expired - KvErrorExpired int32 = 1002 - // KvErrorStoreFull store has reached capacity - KvErrorStoreFull int32 = 1003 - // KvErrorIoError: I/O failure - KvErrorIoError int32 = 1004 -) -``` +## Build and load -A callable marked `throws: true` returns `(value, error)` and maps a -non-zero error slot through the domain mapper (`wvMapKv`); match it with -`errors.As` and compare the code constants: +The bindings link with `#cgo LDFLAGS: -l{library}` and include the bundled +header, so a consumer only supplies the library's directory: -```go -_, err := store.Delete("missing") -var kvErr *KvError -if errors.As(err, &kvErr) && kvErr.Code == KvErrorKeyNotFound { - // specific code -} +```sh +export CGO_LDFLAGS="-L/path/to/lib" +export LD_LIBRARY_PATH="/path/to/lib" # DYLD_LIBRARY_PATH on macOS +go build ./... ``` -A callable without `throws` returns a plain value and checks its slot -with `wvTrap`, which panics with a `*WeaveFFIError`. - -An error code that declares payload `fields:` carries them serialized in -the error's payload buffer; the mapper decodes them into typed fields on -the error value before `weaveffi_error_clear` releases the buffer. - -### Runtime error codes +Depend on the module with `require` (plus `replace` for a local checkout) +and import it by its module path. The library is bound when the program is +linked and loaded by the platform's dynamic loader, so the +`{PREFIX}_LIBRARY` override other targets honor doesn't apply; use the +loader's search path or an rpath instead. -| Code | Meaning | Where it surfaces | -|------|---------|-------------------| -| `-1` | The producer reported an error without a declared code. | `*WeaveFFIError` returned from a throwing call, or the panic value of a non-throwing one. | -| `-2` | The Rust implementation panicked; the export macros and the async spawner catch the unwind. | Same as above (a blocking async call returns it as its `error`). | -| `-3` | Malformed input at the boundary (invalid UTF-8, a truncated value buffer, a bad enum discriminant). | Same as above. | -| `-4` | A callback-interface method implemented in Go panicked. | `*WeaveFFIError` with `Code == -4` returned from the throwing producer call that invoked the callback, or the panic value of a non-throwing one (see [Callback interfaces](#callback-interfaces)). | - -The panic value from `wvTrap` is a `*WeaveFFIError`, so a `recover()` -site can still inspect the code. Using a closed wrapper is a separate -panic with a plain string message. - -## Objects (interfaces) - -An `interfaces:` entry becomes a struct holding the typed C pointer to a -reference-counted producer object. Constructors become package-level -factory functions combining the constructor and type names (`open` -becomes `OpenStore`, `new` becomes `NewEventBus`), methods hang off the -wrapper, statics become package-level functions prefixed by the type -name (`StoreDefaultCapacity`, `StoreOpenMany`), and `Close()` releases -the reference. From the `kvstore` sample (trimmed): +At package initialization, `init` checks the library's ABI revision and the +contract checksum of every top-level module, and panics with the stale +module's name on a mismatch: ```go -// Each wrapper holds one strong reference; Close releases it, and a -// finalizer releases it if the wrapper is garbage collected first. -type Store struct { - ptr *C.weaveffi_kv_Store -} - -// wvAdoptStore adopts one owned strong reference into a new wrapper. A null -// pointer adopts to nil. -func wvAdoptStore(ptr *C.weaveffi_kv_Store) *Store { - if ptr == nil { - return nil - } - s := &Store{ptr: ptr} - runtime.SetFinalizer(s, (*Store).Close) - return s -} - -// wvTokenStore clones o's reference into a value-buffer object token. The -// wrapper keeps its own reference; the token carries the new one. -func wvTokenStore(o *Store) uint64 { - if o == nil || o.ptr == nil { - panic("weaveffi: nil or closed Store cannot be encoded in a non-optional position") - } - return uint64(uintptr(unsafe.Pointer(C.weaveffi_kv_Store_clone(o.ptr)))) -} - -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 wvAdoptStore(result), nil +func init() { + wvCheckABI() + wvCheckModule("shared", 0x42c4ce2c8d0af052, uint64(C.kitchen_sink_shared_checksum())) + wvCheckModule("kitchen", 0xd83da75b24b545ff, uint64(C.kitchen_sink_kitchen_checksum())) } +``` -// Share: A second reference to this same store (the returned pointer equals -// the receiver's; both must eventually be destroyed). -func (s *Store) Share() *Store { - if s.ptr == nil { - panic("weaveffi: Store used after Close") - } - defer runtime.KeepAlive(s) - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_share(s.ptr, &cErr) - wvTrap(&cErr) - return wvAdoptStore(result) -} +`weaveffi package` bundles a prebuilt library per platform under +`lib//` and adds `${SRCDIR}`-relative search and rpath lines per +GOOS and GOARCH, so no `CGO_LDFLAGS` is needed. -func (s *Store) Close() { - if s.ptr != nil { - C.weaveffi_kv_Store_destroy(s.ptr) - s.ptr = nil - runtime.SetFinalizer(s, nil) - } -} -``` +## Type mapping -- **Construction.** Every constructor is a package-level function; a - throwing one returns `(*T, error)`, a non-throwing one returns `*T` - (`NewEventBus()` in the `events` sample). Deprecated members carry a - standard `// Deprecated:` comment that `go vet` and editors - understand. -- **Disposal.** `Close()` releases this wrapper's reference through the - `_destroy` symbol and clears the finalizer. It's idempotent. A - finalizer set at adoption releases the reference if the wrapper is - garbage collected first, but Go finalizers run on a non-deterministic - schedule, so pair every wrapper with `defer store.Close()`. The - producer object itself is dropped only when the last reference - anywhere is released. -- **Use after close.** Every method checks `s.ptr == nil` first and - panics with `"weaveffi: Store used after Close"`. Encoding a closed - (or nil) wrapper into a non-optional position of a value buffer panics - from `wvTokenStore`. -- **Copies mint new references.** Methods returning the receiver or an - existing object (`Share()`, `Fork()`) return a fresh strong reference - adopted into a new wrapper; closing one never affects another. - `runtime.KeepAlive` keeps the receiver (and object parameters) - reachable until the C call returns, so the finalizer can't fire - mid-call. +| IDL type | Go type | Crosses the ABI as | +|----------|---------|--------------------| +| `i8` to `i64`, `u8` to `u64` | `int8` to `int64`, `uint8` to `uint64` | the C integer | +| `f32`, `f64` | `float32`, `float64` | `float`, `double` | +| `bool` | `bool` | C `bool` | +| `string` | `string` | borrowed `(ptr, len)` UTF-8; returns are copied and freed | +| `bytes` | `[]byte` | borrowed `(ptr, len)`; returns are copied and freed | +| C-style enum | `type E int32` plus `E{Variant}` constants | `int32_t` | +| rich enum | sealed `interface` plus one struct per variant | value buffer | +| record | plain struct with exported fields | value buffer | +| `T?` | `*T` (nil-able types such as slices stay as they are) | value buffer | +| `[T]`, `{K: V}` | `[]T`, `map[K]V` | value buffer | +| interface | `*Iface` | object pointer | +| `Iface?` | `*Iface` (nil is none) | object pointer or NULL | +| callback interface | a Go `interface` you implement | `cgo.Handle` plus a static vtable | +| `iter` | `iter.Seq[T]` or `iter.Seq2[T, error]` | iterator handle | -```go -store, err := OpenStore("/tmp/cache.kv") -if err != nil { - return err -} -defer store.Close() -ok, err := store.Put("alpha", []byte{1}, EntryKindPersistent, nil) -fmt.Println(store.Count(), StoreDefaultCapacity()) -``` +Strings pass a view of Go memory with no copy and no NUL terminator, so +they may contain `\x00`. Lengths are never narrowed to `C.int`. -### Nullable objects, and objects inside values +## Objects and lifetime -An `Interface?` parameter is a nil-able pointer that crosses as NULL -when nil, and an `Interface?` return adopts a NULL pointer to nil -(`wvAdoptStore` handles both): +Each wrapper owns one strong reference. `Close() error` releases it (so a +wrapper satisfies `io.Closer`), and a finalizer releases it if you forget: ```go -func (s *Store) Larger(other *Store) *Store { - if s.ptr == nil { - panic("weaveffi: Store used after Close") - } - defer runtime.KeepAlive(s) - defer runtime.KeepAlive(other) - var cOther *C.weaveffi_kv_Store - if other != nil { - cOther = other.ptr - } - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_larger(s.ptr, cOther, &cErr) - wvTrap(&cErr) - return wvAdoptStore(result) +func (s *Gadget) Close() error { + runtime.SetFinalizer(s, nil) + s.ref.close() + return nil } ``` -Objects inside records, lists, maps, and optionals travel as `u64` -object tokens in the value buffer. Writing a token mints a new strong -reference with the `_clone` symbol (`wvTokenStore`); reading one adopts -the reference into a fresh wrapper (`wvUntokenStore`). From the -`StoreInfo` record (`Store *Store`, `Mirror *Store`): +Every call borrows the native pointer for its duration: ```go -func wvPackStoreInfo(w *wvWriter, v StoreInfo) { - w.writeString(v.Label) - w.writeU64(wvTokenStore(v.Store)) - if v.Mirror == nil { - w.writeOptionFlag(false) - } else { - w.writeOptionFlag(true) - w.writeU64(wvTokenStore(v.Mirror)) - } - w.writeI64(v.Count) -} - -func wvUnpackStoreInfo(r *wvReader) StoreInfo { - var v StoreInfo - v.Label = r.readString() - v.Store = wvUntokenStore(r.readU64()) - if r.readOptionFlag() { - var oMirror0 *Store - oMirror0 = wvUntokenStore(r.readU64()) - v.Mirror = oMirror0 +func (s *Gadget) Poke(times int32) (int32, error) { + cSelf := s.native() + defer s.ref.release() + var cErr C.kitchen_sink_error + cRet := C.kitchen_sink_kitchen_Gadget_poke(cSelf, C.int32_t(times), &cErr) + if cErr.code != 0 { + return 0, wvMapKitchen(wvTakeError(&cErr)) } - v.Count = r.readI64() - return v + return int32(cRet), nil } ``` -Lists of objects work the same way in both directions -(`StoreOpenMany(paths)` returns `([]*Store, error)`, -`StoreTotalCount(stores, extra)` takes `[]*Store`); each wrapper in a -returned slice owns its own reference and should be closed individually. -Iterators over objects adopt one reference per step, and a blocking -async call returning an object adopts the pointer inside the completion -trampoline. +`Close` is idempotent and goroutine-safe. When it races a call in flight +(even one that closes the wrapper from inside a callback), the reference is +released as soon as that call returns, never mid-call. Using a wrapper after +`Close` panics. An object inside a record or list crosses as a token that +carries a fresh reference, so the wrapper you passed stays valid. -## Rich (algebraic) enums +## Errors -A *rich* (algebraic) enum, a sum type whose variants carry associated -data, crosses the C ABI as a serialized value buffer, exactly like a -struct: an `i32` tag (the declared discriminant, or declaration order) -followed by the active variant's fields in order. The Go surface is a -sealed interface plus one struct per variant, with no `Close()`. (A -plain C-style enum with no payloads stays a typed `int32` alias with -`const` values; see above.) From the `codec` sample: +A function that declares `throws: true` returns `(T, error)`. Domain codes +map to the module's error type, which `errors.As` selects: ```go -// Shape is a sealed sum type: exactly one of its variant structs is the -// value at a time. -type Shape interface { - isShape() -} - -// ShapeEmpty: No payload. -type ShapeEmpty struct{} - -func (ShapeEmpty) isShape() {} - -// ShapeCircle: One `f64`. -type ShapeCircle struct { - // Radius: Radius. - Radius float64 +_, err := store.Get("missing") +var kerr *kvstore.KvError +if errors.As(err, &kerr) && kerr.Code == kvstore.KvErrorKeyNotFound { + // ... } - -func (ShapeCircle) isShape() {} - -// ShapeLabeled: A string and an `i32`. -type ShapeLabeled struct { - // Label: Label text. - Label string - // Count: Repeat count. - Count int32 -} - -func (ShapeLabeled) isShape() {} ``` -Construct a variant as a struct literal and discriminate with a type -switch: - -```go -var c Shape = ShapeCircle{Radius: 2.0} -switch v := c.(type) { -case ShapeCircle: - fmt.Println(v.Radius) -case ShapeLabeled: - fmt.Println(v.Label, v.Count) -} -``` +Codes with payload fields attach a `{Type}{Code}Payload` struct as +`Payload`. Anything a domain doesn't claim is the package's generic +`*Error`, which keeps the code: `-1` generic, `-2` producer panic, `-3` +marshalling failure, and `-4` a callback implementation that panicked. A +function without `throws` has a plain signature and panics with `*Error` +instead, since only a bug can make it fail. -Free functions that take or return the enum pack it into a value buffer -on the way in and unpack the returned buffer on the way out, releasing -the producer's bytes with `weaveffi_free_bytes`. Variant fields of -interface type follow the object token rules above. +## Async and cancellation -## Callback interfaces - -A `callback_interfaces:` entry becomes a Go `interface` the consumer -implements and passes wherever the API takes that type. From the -`kvstore` sample: +Async functions take a `context.Context` first and always return an error. +They block the calling goroutine on a channel that a completion trampoline +fills from the producer's thread. A `cancellable` function creates a +native cancel token, cancels it when the context is done, and returns +`ctx.Err()` for the cancelled completion (code `-5`): ```go -// Implement this interface in Go and pass the value to native functions -// that accept it. -// -// The native library may call any method from any thread until it releases -// the implementation. A panic in a method is reported to the native caller -// as a foreign error (code -4) instead of crashing the process. -type EvictionListener interface { - // OnEvict: An entry left the store. Returns whether the listener wants to keep - // receiving notifications; `false` detaches it. - OnEvict(entry Entry, reason EvictionReason) bool +ctx, cancel := context.WithTimeout(context.Background(), time.Second) +defer cancel() +n, err := store.Compact(ctx) +if errors.Is(err, context.DeadlineExceeded) { + // The native call was cancelled. } ``` -```go -type auditor struct{} - -func (auditor) OnEvict(entry kvstore.Entry, reason kvstore.EvictionReason) bool { - fmt.Println(entry.Key, reason) - return true -} +Any other async function returns `ctx.Err()` as soon as the context is done +and discards the native result when it arrives. An already-cancelled +context never launches the call. -store.SetEvictionListener(auditor{}) -``` +## Callbacks -cgo forbids passing Go pointers to C, so the implementation never -crosses the boundary directly. Passing one wraps it in a `cgo.Handle` -whose integer value crosses as `ctx` (widened to `void*` in C by the -preamble's `wvHandlePtr`, which keeps `go vet` quiet), together with the -address of one static vtable per interface whose slots are `//export`ed -Go trampolines: +A callback interface is a Go interface. Passing an implementation stores it +in a `cgo.Handle` and hands the producer one static vtable of `//export` +trampolines; the producer's `free` entry deletes the handle. The producer +may call methods from any thread. A panic inside a method is recovered and +reported to the producer as code `-4` with the panic's text, whether the +producer's method returns a plain value or a `Result`, and the caller sees +it as `*Error`. ```go -//export goWv_weaveffi_kv_EvictionListener_on_evict -func goWv_weaveffi_kv_EvictionListener_on_evict(ctx unsafe.Pointer, entry_ptr *C.uint8_t, entry_len C.size_t, reason C.weaveffi_kv_EvictionReason, out_err *C.weaveffi_error) (ret C._Bool) { - defer func() { - if r := recover(); r != nil { - wvForeignError(out_err, r) - } - }() - impl := cgo.Handle(uintptr(ctx)).Value().(EvictionListener) - rArg0 := &wvReader{buf: wvBorrowBuffer(entry_ptr, entry_len)} - var arg0 Entry - arg0 = wvUnpackEntry(rArg0) - rArg0.expectEnd() - arg1 := EvictionReason(reason) - ret = boolToC(impl.OnEvict(arg0, arg1)) - return -} - -//export goWv_weaveffi_kv_EvictionListener_free -func goWv_weaveffi_kv_EvictionListener_free(ctx unsafe.Pointer) { - cgo.Handle(uintptr(ctx)).Delete() +type ReadyListener interface { + // OnReady: Fires when an item is ready + OnReady(code int32, msg string) + // OnItem: Receives the item itself and says whether to keep listening + OnItem(item Item, gadget *Gadget) bool } ``` -```go -func (s *Store) SetEvictionListener(listener EvictionListener) { - if s.ptr == nil { - panic("weaveffi: Store used after Close") - } - defer runtime.KeepAlive(s) - hListener := cgo.NewHandle(listener) - var cErr C.weaveffi_error - C.weaveffi_kv_Store_set_eviction_listener(s.ptr, C.wvHandlePtr(C.uintptr_t(hListener)), C.wvVtablePtr_weaveffi_kv_EvictionListener(), &cErr) - wvTrap(&cErr) -} -``` - -- **Lifetime.** The `cgo.Handle` keeps the implementation alive exactly - as long as the producer may call it; the vtable's `free` trampoline - deletes the handle when the producer drops its last reference. A - producer that retains the implementation (a store's eviction listener) - keeps it alive across calls; one that doesn't (the `events` sample's - `RouteOnce`) frees it before returning. Passing the same value twice - creates two handles. -- **Argument ownership.** Borrowed strings and buffers are copied into - Go memory before the method runs (`wvBorrowBuffer`, `C.GoString`), so - the implementation may keep them. An object passed to a callback - method is owned by the implementation: the trampoline adopts it into a - new wrapper (`impl.OnAttached(wvAdoptEventBus(bus))` in the `events` - sample), and the implementation should `Close()` it when done (or let - the finalizer run). -- **Return values.** A method's return value is converted back to its C - representation (`bool` via `boolToC`, a plain enum to its C enum, a - record to a value buffer the producer frees). -- **Panics.** A panic escaping a method never unwinds through the C - frame. The deferred `recover()` hands the value to `wvForeignError`, - which writes code -4 with `fmt.Sprint(recovered)` into the producer's - error slot, and the trampoline returns its zero value; the producer - aborts the call in progress. The original caller then sees - `*WeaveFFIError` with `Code == -4` as the returned `error` (throwing - callable) or as the `wvTrap` panic value (non-throwing callable, the - trap idiom). The process is never taken down. -- **Threads.** The producer may call a method from any thread; cgo - callbacks run on whatever OS thread the producer fires them from, with - the Go runtime attaching a goroutine to it. The `kvstore` eviction - listener fires synchronously inside `Delete()`/`Get()`; the `events` - sample's `PublishLater` calls subscribers from the producer's worker - thread. Don't block in a callback waiting on the goroutine that made - the producer call if the producer invoked it synchronously from that - call. - -## Build instructions - -1. Generate the bindings: - - ```bash - weaveffi generate api.yaml -o generated --target go - ``` - -2. Build the Rust shared library: - - ```bash - cargo build --release -p your_library - ``` - -3. Point cgo at the header and library. The Go package `#include`s the - header emitted by the C target, so generate that too: - - ```bash - weaveffi generate api.yaml -o generated --target c - export CGO_CFLAGS="-I$PWD/generated/c" - export CGO_LDFLAGS="-L$PWD/target/release -lweaveffi" - ``` - -4. Build and run a Go consumer: - - ```bash - cd generated/go - go build ./... - ``` - -cgo requires a C compiler (`gcc` or `clang`) on the host; on Windows use -a MinGW-w64 toolchain or the MSVC build provided by `go env`. Go 1.23 or -later is required for the `iter` package. - -## Packaging - -`weaveffi package --target go` emits the Go module under `go/` with a -self-contained, relocatable cgo preamble and copies each supplied -desktop binary to `go/lib//` (`macos-arm64`, `macos-x64`, -`linux-x64`, `linux-arm64`, `windows-x64`). The single -`#cgo LDFLAGS: -l` line is expanded into a `${SRCDIR}`-relative -include path for the packaged C header plus one `-L` (and, except on -Windows, `-Wl,-rpath`) directive per `GOOS,GOARCH` pair, so -`go build` selects the matching slice with no environment setup: - -```go -#cgo CFLAGS: -I${SRCDIR}/../c/include -#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/lib/macos-arm64 -Wl,-rpath,${SRCDIR}/lib/macos-arm64 -#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/lib/linux-x64 -Wl,-rpath,${SRCDIR}/lib/linux-x64 -#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/lib/windows-x64 -#cgo LDFLAGS: -lkvstore -``` - -Android and `wasm32` binaries are skipped. See -[Packaging](../guides/packaging.md) for the shared workflow. - -## Memory and ownership - -- **Strings in:** `C.CString` allocates a copy in C memory; the - generated wrapper pairs every `CString` with a `defer C.free(...)`. -- **Strings out:** `C.GoString` copies the C string into Go-owned - memory, then the wrapper calls `weaveffi_free_string` to release the - Rust allocation. -- **Bytes:** input slices are passed by pointer for the duration of the - call (no copy); returned bytes are copied with `C.GoBytes` and then - `weaveffi_free_bytes` is called. -- **Buffered values (structs, rich enums, optionals, lists, maps):** - parameters are packed into a Go-owned `[]byte` that the producer - borrows for the duration of the call; returns are copied into Go - memory with `wvCopyBuffer`, released with `weaveffi_free_bytes`, and - decoded. Object tokens written into a buffer are fresh strong - references the producer owns; tokens read out are adopted into - wrappers. -- **Interfaces:** one strong reference per wrapper, released by - `Close()` with a `runtime.SetFinalizer` backstop. Always - `defer s.Close()`. -- **Callback implementations:** held by a `cgo.Handle` until the - producer calls the vtable's `free`. - -## Async support - -Functions marked `async: true` are exposed through `_async`-suffixed C -launchers that take a completion callback plus `void* context`. Go has -no ambient async runtime, so the generated wrapper turns that into a -blocking call built on a channel: it makes a buffered channel, wraps it -in a `cgo.Handle`, launches the C call with an exported trampoline and -the handle as context, then receives from the channel. The generated -doc comment states that the call blocks. From the `kvstore` sample: - -```go -// Blocks the calling goroutine until the async producer completes. -func (s *Store) Compact() (int64, error) { - if s.ptr == nil { - panic("weaveffi: Store used after Close") - } - defer runtime.KeepAlive(s) - ch := make(chan wvOutcomeKvStoreCompact, 1) - h := cgo.NewHandle(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)), C.wvHandlePtr(C.uintptr_t(h))) - outcome := <-ch - if outcome.err != nil { - return 0, outcome.err - } - return outcome.val, nil -} -``` +## Iterators -The completion callback fires exactly once, on a producer thread. The -trampoline takes the channel out of the handle (one-shot), converts the -C error or result inside the callback, and sends a single `wvOutcome…` -value: +An `iter` return becomes a lazy sequence: each `range` launches the +native iterator, pulls one element per step, and destroys the iterator when +the loop ends, including on an early `break`. A throwing function yields +its error as a final `(zero, err)` pair of an `iter.Seq2[T, error]`. ```go -//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) { - h := cgo.Handle(uintptr(context)) - ch := h.Value().(chan wvOutcomeKvStoreCompact) - h.Delete() - if err != nil && err.code != 0 { - ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeBoxedError(err))} - return - } - ch <- wvOutcomeKvStoreCompact{val: int64(result)} +for g := range kitchen_sink.StreamGadgets() { + fmt.Println(g.Describe()) + g.Close() } ``` -Result buffers such as strings and buffered values are owned by the -consumer, so the trampoline copies or decodes them into Go memory and -then releases them with the runtime free symbols; a heap-boxed error is -read and released by `wvTakeBoxedError` (`weaveffi_error_free`); an -owned interface result is adopted into a wrapper instead. - -For a callable marked `throws: true`, the trampoline maps the error -through the domain mapper, so the returned `error` is the typed one -(`*KvError` from `store.Compact()`); a non-throwing async callable -returns a plain value and panics via `wvTrap` on a trap code. A panic -inside the spawned future surfaces as code -2. The native producer -already runs on its own thread, so the wrapper simply blocks the calling -goroutine; callers that want concurrency run the call from a goroutine -of their own. - -For functions marked `cancellable: true` the C launcher gains a -`weaveffi_cancel_token*` parameter. The Go wrapper passes `nil` for it -and doesn't expose the token; only the C and C++ targets surface -cancellation tokens. - -## Iterators +## Leak checks -`iter` returns map to the standard library's range-over-function -sequences (Go 1.23+): a non-throwing function returns `iter.Seq[T]` and -a throwing one returns `iter.Seq2[T, error]`. Nothing is drained: the -producer iterator is launched when the consumer starts ranging, and each -consumer step issues exactly one producer `next` call. From the -`kvstore` sample's throwing `ListKeys`: +`DebugLive(kind)` reports the producer's live objects (0), callbacks (1), +iterators (2), cancel tokens (3), and returned allocations (4). It's always +0 unless the library was built with the `leak-check` feature. -```go -// 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) { - if s.ptr == nil { - panic("weaveffi: Store used after Close") - } - defer runtime.KeepAlive(s) - // ... pack the *string prefix into a value buffer ... - 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 - } - } - } -} -``` +## Limitations -Consume it with `for key, err := range store.ListKeys(nil)`, checking -`err` on each step. Each yielded element is copied into Go memory and -its Rust allocation released per element (strings via -`weaveffi_free_string`; buffered elements are decoded and released with -`weaveffi_free_bytes`; object elements are adopted into wrappers). The -deferred `_destroy` call runs exactly once, whether the consumer -exhausts the sequence or breaks out of the `for range` loop early. -Ranging over the same returned sequence again launches a fresh producer -iterator. - -In a non-throwing sequence (the `events` sample's `Messages()`), a -reported error can only be a producer bug or a panicking callback, so -`wvTrap` panics instead of yielding it. - -## Known limitations - -- Async functions block the calling goroutine; there's no channel- or - context-based variant, and `cancellable: true` tokens are not exposed. -- Callback methods run on the producer's thread as cgo callbacks; there - is no marshalling to a particular goroutine. -- Non-throwing wrappers panic on the runtime trap codes (including a - panicking callback, code -4) rather than returning an `error`. -- The generated module needs Go 1.23+ (`iter`, `range` over functions) - and a C toolchain for cgo. -- The plain `generate` output expects `weaveffi.h` from the C target and - the library on `CGO_CFLAGS`/`CGO_LDFLAGS`; only `weaveffi package` - produces a relocatable module. - -## Troubleshooting - -- **`undefined reference to weaveffi_*`**: `CGO_LDFLAGS` is missing the - `-l` flag or `-L` directory. Recheck the environment exports. -- **`could not determine kind of name` in cgo**: ensure `CGO_CFLAGS` - points at the directory containing `weaveffi.h` (generate the C - target alongside Go). -- **`panic: WeaveFFI ABI mismatch`** at startup: the library was built - by a different `weaveffi` release than the bindings. Regenerate the - bindings and rebuild the library together. -- **`panic: weaveffi: Store used after Close`**: a closed wrapper was - used. Keep the wrapper alive for as long as the object is in use; - closing one wrapper doesn't affect others pointing at the same object. -- **`weaveffi: ... (code -4)`** returned or panicked: a - callback-interface method you implemented panicked; the message is - the panic value. Recover inside the method if the producer call should - succeed anyway. -- **`go: cannot find module providing package weaveffi`**: change the - generator config so `go.mod` declares the module path you actually - import, e.g. `github.com/myorg/mylib`. +- All modules share one Go package, so two modules can't declare types + with the same name. +- The library is bound at link time, so there's no runtime library path + override. +- Callback methods can only return direct values (scalars, `bool`, and + C-style enums). +- A non-cancellable async call abandoned by its context keeps running in + the producer until it finishes. diff --git a/docs/src/generators/kotlin.md b/docs/src/generators/kotlin.md index 53d3277e..9acca90d 100644 --- a/docs/src/generators/kotlin.md +++ b/docs/src/generators/kotlin.md @@ -1,812 +1,194 @@ # Kotlin -## Overview - -The Kotlin target produces a Gradle library module (`kotlin/`, Kotlin DSL) -that combines a Kotlin wrapper, a JNI C shim, and a CMake build for the -shim. Android is the primary runtime (the module is an Android library and -the shim builds through the NDK's CMake), and the same sources run on a -desktop JVM when the shim is built against a JDK. The wrapper exposes -idiomatic Kotlin: data classes and sealed classes for values, -`AutoCloseable` wrappers for reference-counted objects, Kotlin `interface`s -for callback interfaces, `suspend fun`s for async callables, and lazy -`Iterator`s. The surface follows ABI revision 2; `JNI_OnLoad` checks the -producer's `weaveffi_abi_version()` and refuses to load a mismatched -library with `UnsatisfiedLinkError`. +The Kotlin target emits a Gradle library module: Kotlin sources over a JNI +shim that calls the producer's C ABI (revision 3). The default flavor is an +Android library whose shim builds through the NDK's CMake; the `jvm` flavor +is a plain Kotlin/JVM library for desktop and server JVMs. Records become +data classes, interfaces `AutoCloseable` wrappers, async functions `suspend` +functions, callback interfaces Kotlin `interface`s, and each IDL module an +`object`. ## What gets generated -| File | Purpose | -|------|---------| -| `generated/kotlin/settings.gradle.kts` | Gradle settings naming the module after the package | -| `generated/kotlin/build.gradle.kts` | `com.android.library` plugin, NDK/CMake config, `kotlinx-coroutines-core` dependency | -| `generated/kotlin/src/main/kotlin/com/weaveffi/WeaveFFI.kt` | Kotlin wrapper: enums, data classes, object wrappers, callback interfaces, iterators, buffer codec | -| `generated/kotlin/src/main/cpp/weaveffi_jni.c` | JNI shim that calls the C ABI, throws Kotlin exceptions, and hosts the callback vtables | -| `generated/kotlin/src/main/cpp/CMakeLists.txt` | CMake build for the JNI shared library | - -The generated `build.gradle.kts`: - -```kotlin -plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") version "1.9.22" -} - -android { - namespace = "com.weaveffi" - compileSdk = 34 - defaultConfig { - // java.lang.ref.Cleaner, which backs every wrapper's disposal, is API 33. - minSdk = 33 - externalNativeBuild { - cmake { - cppFlags("") - } - } - } - externalNativeBuild { - cmake { - path = file("src/main/cpp/CMakeLists.txt") - } - } -} - -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") -} -``` +For a library whose C prefix and library name are `kitchen_sink`: + +```text +kotlin/ + settings.gradle.kts repositories and the root project name + build.gradle.kts Android library (or Kotlin/JVM) plus maven-publish + consumer-rules.pro R8 keep rules (Android flavor only) + src/main/cpp/ + CMakeLists.txt builds libkitchen_sink_jni, linked to libkitchen_sink + kitchen_sink.h the C header the shim compiles against + kitchen_sink_jni.c the JNI shim + src/main/kotlin/kitchen_sink/ + Kitchen.kt, Shared.kt one file per top-level module + JniBridge.kt internal: one native per C symbol + Runtime.kt FfiException, NativeIterator, loader, lifetime + Buffers.kt value-buffer codec (when the API needs it) + Async.kt coroutine bridge (when the API has async) +``` + +The package defaults to the identity's C prefix (`kitchen_sink`), the +Gradle group to the package, and the root project and artifact ID to the +`[package]` name. Nothing outside the generated-file headers carries WeaveFFI +branding. ## Configuration -| Key | Default | Meaning | -|-----|---------|---------| -| `[generators.kotlin] package` | `"com.weaveffi"` | JVM package for the wrapper and the `namespace` in `build.gradle.kts` | -| `[generators.kotlin] strip_module_prefix` | `true` | Strip the IR module prefix from function names (`createContact` rather than `contactsCreateContact`) | -| `[generators.kotlin] prefix` | `"weaveffi"` | C ABI symbol prefix; normally set once via `[global] c_prefix` | - -See [Configuration](../guides/config.md) for the full table. - -## Type mapping - -| IDL type | Kotlin type (wrapper) | JNI lowering | Notes | -|----------------|------------------------|-----------------------|-------| -| `i8` | `Byte` | `jbyte` | | -| `i16` | `Short` | `jshort` | | -| `i32` | `Int` | `jint` | | -| `i64` | `Long` | `jlong` | | -| `u8` | `Byte` | `jbyte` | Same bit pattern; values above 127 read negative | -| `u16` | `Short` | `jshort` | Same bit pattern; values above 32767 read negative | -| `u32` | `Long` | `jlong` | Widened; always non-negative | -| `u64` | `Long` | `jlong` | Same bit pattern; values above `2^63 - 1` read negative (`toULong()` recovers them) | -| `f32` | `Float` | `jfloat` | Raw bits; NaN, infinities, `-0.0` preserved | -| `f64` | `Double` | `jdouble` | Raw bits; NaN, infinities, `-0.0` preserved | -| `bool` | `Boolean` | `jboolean` | | -| `string` | `String` | `jstring` | Modified-UTF-8-safe conversion in the shim | -| `bytes` | `ByteArray` | `jbyteArray` | | -| `StructName` | `StructName` (data class) | `jbyteArray` (value buffer) | | -| `InterfaceName` | `InterfaceName` (`AutoCloseable` class) | `jlong` handle | See [Objects](#objects-interfaces) | -| `InterfaceName?` | `InterfaceName?` | `Long?` | Null pointer | -| `CallbackName` | `interface CallbackName` | `jobject` pinned by a global ref | See [Callback interfaces](#callback-interfaces) | -| `EnumName` (plain) | `EnumName` (`enum class`) | `jint` | | -| `EnumName` (rich) | `EnumName` (sealed class) | `jbyteArray` (value buffer) | | -| `T?` | `T?` | `jbyteArray` (value buffer) | | -| `[T]` | `List` | `jbyteArray` (value buffer) | | -| `{K: V}` | `Map` | `jbyteArray` (value buffer) | | -| `iter` | `Iterator` (lazy wrapper class) | `jlong` iterator handle | | - -The JVM has no unsigned primitives, so the wrapper carries `u8`, `u16`, -and `u64` as the same-width signed type with the identical bit pattern -(the `codec` sample's `roundtripU64(value: Long): Long` round-trips values -above `2^63` as negative `Long`s). Convert with `toUByte()`, `toUShort()`, -and `toULong()` when you need the unsigned view. Floats are written to and -read from value buffers with `toRawBits()`/`fromBits()`, so NaN payloads, -the infinities, and `-0.0` survive. - -## Example IDL → generated code - -```yaml -version: "0.9.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: name, type: string } - - { name: age, type: i32 } - - functions: - - name: get_contact - params: - - { name: id, type: i32 } - return: Contact - - - name: find_by_type - params: - - { name: contact_type, type: ContactType } - return: "[Contact]" -``` - -Free functions live on the `WeaveFFI` companion object. Function names -are lowerCamelCase with the module prefix stripped by default. 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 crosses JNI as a -`ByteArray` copy of its value buffer, encoded and decoded by the -`WeaveBufferWriter`/`WeaveBufferReader` codec in the same file: - -```kotlin -package com.weaveffi - -class WeaveFFI { - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - @JvmStatic private external fun getContactJni(id: Int): ByteArray - @JvmStatic fun getContact(id: Int): Contact = weaveDecode(getContactJni(id)) { r -> unpackContact(r) } - @JvmStatic private external fun findByTypeJni(contactType: Int): ByteArray - @JvmStatic fun findByType(contactType: ContactType): List = - weaveDecode(findByTypeJni(contactType.value)) { r -> r.readList { unpackContact(r) } } - } -} +```toml +[generators.kotlin] +package = "com.example.kitchen" # default: the C prefix +flavor = "jvm" # "android" (default) or "jvm" +strip_module_prefix = true # default ``` -Enums become Kotlin `enum class` with a `fromValue` factory: +## Building and loading -```kotlin -enum class ContactType(val value: Int) { - Personal(0), - Work(1), - Other(2); +The tree builds with Gradle as-is: `settings.gradle.kts` declares +`google()`, `mavenCentral()`, and the Gradle plugin portal, and +`./gradlew publishToMavenLocal` publishes the module. - companion object { - fun fromValue(value: Int): ContactType = entries.first { it.value == value } - } -} -``` - -Structs are plain Kotlin data classes with one typed property per field -(field names keep their IDL spelling). They own no native resources: no -handle, no `AutoCloseable`, no per-struct JNI symbols. A `Contact` crosses -the boundary serialized in the -[value-buffer format](../reference/value-buffers.md): - -```kotlin -/** A contact record */ -data class Contact( - val name: String, - val age: Int, -) - -internal fun packContact(w: WeaveBufferWriter, v: Contact) { - w.writeString(v.name) - w.writeI32(v.age) -} - -internal fun unpackContact(r: WeaveBufferReader): Contact = Contact( - r.readString(), - r.readI32(), -) -``` +- **Android.** Put the producer library for each ABI in + `src/main/jniLibs//libkitchen_sink.so`. The NDK build links + `libkitchen_sink_jni.so` against it and the AAR packages both. `minSdk` is + 21. +- **JVM.** Build the shim with CMake, pointing it at the producer library, + and put the result on `java.library.path`: -The JNI shim (`weaveffi_jni.c`) bridges each `external fun` into the C -ABI. Strings are converted with a standard-UTF-8 helper pair -(`weaveffi_jni_string_to_utf8`/`weaveffi_jni_utf8_to_string`), so -supplementary characters survive; returned value buffers -are copied into a `byte[]` and released with `weaveffi_free_bytes`: - -```c -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_getContactJni(JNIEnv* env, jclass clazz, jint id) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_contacts_get_contact((int32_t)id, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); - weaveffi_free_bytes(rv, out_len); - return out; -} -``` - -The CMake file builds the shim against the generated C header: - -```cmake -cmake_minimum_required(VERSION 3.22) -project(weaveffi_jni C) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) +```bash +cmake -S src/main/cpp -B build -DKITCHEN_SINK_LIBRARY_DIR=/path/to/lib/dir +cmake --build build ``` -## Typed errors +At first use the bindings load the producer (`System.loadLibrary("kitchen_sink")`) +and then the shim (`System.loadLibrary("kitchen_sink_jni")`). Set +`KITCHEN_SINK_LIBRARY` to an absolute path to load the producer from there +instead. `JNI_OnLoad` checks that the producer implements C ABI revision 3 +and that every top-level module's contract checksum matches the bindings; +otherwise loading fails with `UnsatisfiedLinkError` naming the module. -Every generated file carries the generic -`open class WeaveFFIException(val code: Int, message: String)`. A module's -[error domain](../guides/errors.md) adds a sealed exception hierarchy -named after the domain with the trailing `Error` stem replaced by -`Exception` (`KvError` becomes `KvException`), one nested class per code, -and a `fromCode` mapper. From the `kvstore` sample: - -```kotlin -/** Generic WeaveFFI failure: panics, marshalling errors, callback implementation failures (code -4), 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) { - class KeyNotFound(message: String = "key not found") : KvException(1001, message) - class Expired(message: String = "entry expired") : KvException(1002, message) - class StoreFull(message: String = "store has reached capacity") : KvException(1003, message) - class IoError(message: String = "I/O failure") : KvException(1004, message) - - companion object { - @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) - } - } -} -``` - -A callable with `throws: true` throws the matching subclass from its JNI -shim (a per-domain thrower resolves `com/weaveffi/KvException$KeyNotFound` -and friends by code); catch the specific class, the sealed domain, or the -generic base: +## Type mapping -```kotlin -try { - store.put("alpha", byteArrayOf(1), EntryKind.Volatile, null) -} catch (e: KvException.StoreFull) { - // typed case -} catch (e: KvException) { - // any kv domain error -} catch (e: WeaveFFIException) { - // runtime code (negative) or unknown code -} -``` +| IDL type | Kotlin type | Crosses JNI as | +|---|---|---| +| `i8`, `u8` | `Byte` | `jbyte` | +| `i16`, `u16` | `Short` | `jshort` | +| `i32` | `Int` | `jint` | +| `u32`, `i64`, `u64` | `Long` | `jlong` | +| `f32`, `f64` | `Float`, `Double` | `jfloat`, `jdouble` | +| `bool` | `Boolean` | `jboolean` | +| `string` | `String` | UTF-8 `ByteArray` (pointer plus length) | +| `bytes` | `ByteArray` | `ByteArray` | +| enum | `enum class` with `value` | `jint` | +| record | `data class` | value buffer | +| rich enum | `sealed class` | value buffer | +| `T?`, `[T]`, `{K:V}` | `T?`, `List`, `Map` | value buffer | +| interface, `Iface?` | wrapper class, nullable wrapper | address (`0` is none) | +| callback interface | Kotlin `interface` | the implementing object | +| `iter` | `NativeIterator` | iterator address | -A callable without `throws` keeps a plain signature; the shim still checks -the error slot and throws the generic `WeaveFFIException` for a runtime -code. An error code that declares payload `fields:` carries them in the -error's payload buffer; the shim passes the bytes up and `fromCode` decodes -them into properties on the typed exception. +Unsigned integers ride in the signed type of the same width (`u64` max is +`-1L`). Strings cross as UTF-8 bytes with an explicit length, so interior +NULs and supplementary characters round-trip exactly. -### Runtime error codes +## Naming -Negative codes are never domain codes and always arrive as the generic -`WeaveFFIException`: +Free functions live in one `object` per module, with nested modules as +nested objects, so two modules can each declare a `get`. User types live at +the top level of the package. Two rules keep the generated names valid: -| Code | ABI name | When | -|------|----------|------| -| -1 | `GENERIC_ERROR_CODE` | The producer reported a failure with no domain code | -| -2 | `PANIC_ERROR_CODE` | The Rust producer panicked inside an export or a spawned async future; also used by the Kotlin buffer reader for a malformed value buffer | -| -3 | `MARSHAL_ERROR_CODE` | A null object or a malformed value buffer or string was rejected at the boundary | -| -4 | `FOREIGN_ERROR_CODE` | A callback-interface implementation threw | +- A user type named like a Kotlin keyword, a type Kotlin or Java imports by + default (`Unit`, `String`, `Result`, ...), or a runtime declaration + (`FfiException`, `NativeIterator`, ...) gains a trailing underscore: + `Unit` becomes `Unit_`. +- A module object whose name equals a type's gains a `Module` suffix: a + `kv.stats` module beside a `Stats` record is `Kv.StatsModule`. -They surface as a thrown exception from a sync call, as the exception a -`suspend fun` resumes with, or from the `hasNext()`/`next()` step of an -iterator. Kotlin has no separate trap path: a non-throwing callable throws -`WeaveFFIException` just like a throwing one. +Parameters and methods are lowerCamelCase; record fields keep their IDL +spelling. Keywords gain a trailing underscore (`class_`). -## Objects (interfaces) +## Objects and lifetime -An `interfaces:` entry becomes a class that holds one strong native -reference and implements `AutoCloseable`. Constructors become companion -factories (a constructor named `new` becomes `operator fun invoke`, so -`EventBus()` reads like a real constructor), methods are instance -functions, and statics are companion functions. From the `kvstore` -sample's `Store`: +An interface wrapper owns one strong reference: ```kotlin -class Store private constructor(handle: Long) : AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - - /** The native pointer, borrowed for one call; throws after [close]. */ - internal val handle: Long get() = ref.get() - - /** A new strong reference, written into value buffers as an object token. */ - internal fun cloneHandle(): Long = nativeClone(ref.get()) +class Gadget private constructor(address: Long) : AutoCloseable { + internal val handle: NativeHandle = NativeCleaner.register(this, NativeHandle(address, JniBridge::kitchen_Gadget_destroy)) - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - /** Adopts one strong native reference (a constructor return, a `_clone`, or a buffer token) into a wrapper that owes its `_destroy`. */ - internal fun fromHandle(handle: Long): Store = Store(handle) - @JvmStatic private external fun nativeOpen(path: String): Long - @JvmStatic private external fun nativeDelete(selfHandle: Long, key: String): Boolean - @JvmStatic private external fun nativeClone(handle: Long): Long - @JvmStatic private external fun nativeDestroy(handle: Long) - - fun open(path: String): Store = Store.fromHandle(nativeOpen(path)) - fun defaultCapacity(): Long = nativeDefaultCapacity() - } - - fun delete(key: String): Boolean = nativeDelete(handle, key) - fun count(): Long = nativeCount(handle) - fun listKeys(prefix: String?): Iterator = KvStoreListKeysIterator(nativeListKeys(handle, /* ... */)) - suspend fun compact(): Long = suspendCancellableCoroutine { cont -> /* ... */ } - - @Deprecated("use put() with explicit kind") - fun legacyPut(key: String, value: ByteArray): Boolean = nativeLegacyPut(handle, key, value) - - /** Releases this wrapper's native reference; safe to call more than once. */ - override fun close() { - cleanable.clean() + /** Render the gadget as a human-readable string */ + fun describe(): String = handle.borrow { _self -> + decodeUtf8(JniBridge.kitchen_Gadget_describe(_self)) } -} ``` -```kotlin -Store.open("/tmp/cache.kv").use { store -> - store.put("alpha", byteArrayOf(1), EntryKind.Volatile, null) - println(store.count()) -} -``` +Every call borrows the receiver and every object argument for its whole +duration. `close()` (or `use { }`) releases the reference; if a call is in +flight, the release waits until the call returns, and if the wrapper becomes +unreachable mid-call, its handle is still borrowed, so the object is never +freed under native code. Using a closed wrapper throws +`IllegalStateException`; closing twice is safe. Wrappers that are never +closed are released by a daemon thread draining a `PhantomReference` queue, +which works on every Android API level. -- **Disposal is `close()` plus a `Cleaner` backstop.** `close()` releases - the wrapper's reference with `_destroy` exactly once; calling it again - is a no-op. A wrapper that becomes unreachable without being closed is - released by the process-wide `java.lang.ref.Cleaner` (which is why - `minSdk` is 33). Prefer `use { }` or an explicit `close()`; the cleaner - is a safety net, not deterministic cleanup. -- **Use after close throws.** Every native call borrows the pointer - through `ref.get()`, which throws `IllegalStateException("WeaveFFI - object used after close()")` once the reference has been released. -- **Clones mint a new strong reference.** `cloneHandle()` calls `_clone` - whenever the wrapper must hand the producer a reference it will own; - each wrapper adopted from a return or a buffer token owes its own - `_destroy`. Two wrappers over the same producer object (the sample's - `share()`, or the bus handed to `onAttached`) are independent: closing - one leaves the other usable. - -### Objects as parameters, returns, and inside values - -A top-level object parameter is borrowed for the call (the JNI external -takes `handle`); a returned object is adopted with `fromHandle`. `Store?` -is a `Long?` at the JNI layer, null for absent: +Objects inside records and collections cross as new strong references: the +encoder clones each one (and releases the clones again if encoding fails), +and the decoder adopts each one into a new wrapper. -```kotlin -fun larger(other: Store?): Store? = nativeLarger(handle, other?.handle)?.let { Store.fromHandle(it) } -``` +## Errors -Objects inside records, lists, map values, optionals, and rich-enum -payloads are ordinary properties (`val store: Store, val mirror: Store?` -in `StoreInfo`). On the wire they're `u64` tokens: the pack routine writes -a fresh `cloneHandle()` for each one, and the unpack routine adopts the -token: +Every failure is an `FfiException` with a `code`. Each error domain becomes +a sealed subclass with one class per code: ```kotlin -internal fun packStoreInfo(w: WeaveBufferWriter, v: StoreInfo) { - w.writeString(v.label) - w.writeI64(v.store.cloneHandle()) - w.writeOptional(v.mirror) { v0 -> w.writeI64(v0.cloneHandle()) } - w.writeI64(v.count) -} - -internal fun unpackStoreInfo(r: WeaveBufferReader): StoreInfo = StoreInfo( - r.readString(), - Store.fromHandle(r.readObject()), - r.readOptional { Store.fromHandle(r.readObject()) }, - r.readI64(), -) +sealed class KitchenErrorsException(code: Int, message: String) : FfiException(code, message) { + class NotFound(message: String = "Item not found") : KitchenErrorsException(1, message) ``` -`Store.openMany(paths)` returns `List` (one adopted wrapper per -element, each of which you should close), and -`Store.totalCount(stores, extra)` clones each object it encodes. An async -callable returning an object resumes with the raw handle, which the -suspend wrapper adopts; an `iter` adopts one per step. +Runtime failures use the generic class: `-2` producer panic, `-3` +marshalling failure (including a malformed value buffer), and `-4` a +callback implementation that threw. -## Callback interfaces +## Async and cancellation -A `callback_interfaces:` entry becomes a Kotlin `interface` with one -function per IDL method. Implement it in any class and pass the instance -wherever the API expects the interface. From the `events` sample: +Async functions are `suspend` functions. A cancellable one creates a native +cancel token for the call, and cancelling the awaiting coroutine (directly, +by its scope, or through `withTimeout`) cancels the token, so the producer +drops the pending work: ```kotlin -interface Subscriber { - /** Decide how the bus should treat `topic` for this subscriber. */ - fun route(topic: String): Delivery - /** Receive an accepted message. Returns the subscriber's running count of received messages. */ - fun onMessage(message: Message): Long - /** Receive the bus itself (an object handed through a callback). The consumer adopts the reference and may keep or drop it. */ - fun onAttached(bus: EventBus) -} - -/** JNI dispatch shims for [Subscriber]; the native vtable trampolines call these, nothing else does. */ -internal object SubscriberJni { - @JvmStatic fun route(impl: Subscriber, topic: String): Int = impl.route(topic).value - @JvmStatic fun onMessage(impl: Subscriber, message: ByteArray): Long = impl.onMessage(weaveDecode(message) { r -> unpackMessage(r) }) - @JvmStatic fun onAttached(impl: Subscriber, bus: Long) = impl.onAttached(EventBus.fromHandle(bus)) +suspend fun doCancellable(input: String): String = awaitNative(true, 0, { decodeUtf8(it as ByteArray) }) { _token, _done -> + JniBridge.kitchen_do_cancellable(encodeUtf8(input), _token, _done) } ``` -```kotlin -class LoggingSubscriber : Subscriber { - var keptBus: EventBus? = null - - override fun route(topic: String) = if (topic == "quiet") Delivery.Skip else Delivery.Accept - override fun onMessage(message: Message): Long { - println("${message.topic}: ${message.text}") - return 1 - } - override fun onAttached(bus: EventBus) { - keptBus = bus // or bus.close() to release it now - } -} - -val bus = EventBus() -bus.subscribe(LoggingSubscriber()) -``` - -In the shim, the implementation is pinned with `NewGlobalRef` and that -reference is the vtable `ctx`. `JNI_OnLoad` caches the `SubscriberJni` -class and its static method ids, and one process-wide static vtable of C -trampolines dispatches every call: - -```c -static weaveffi_events_Delivery weaveffi_events_Subscriber_jni_route(void* ctx, const char* topic, weaveffi_error* out_err) { - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env == NULL) { - weaveffi_error_set(out_err, -4, "WeaveFFI: could not attach the calling thread to the JavaVM"); - return 0; - } - if ((*env)->PushLocalFrame(env, 16) != 0) { /* ... -4 ... */ } - jstring _a0 = weaveffi_jni_utf8_to_string(env, topic); - jint _rv = (*env)->CallStaticIntMethod(env, weaveffi_events_Subscriber_jni_cls, weaveffi_events_Subscriber_jni_mid_route, (jobject)ctx, _a0); - if ((*env)->ExceptionCheck(env)) { - weaveffi_jni_report_foreign(env, out_err); - _rv = (jint)0; - } - (*env)->PopLocalFrame(env, NULL); - weaveffi_jni_detach(attached); - return (weaveffi_events_Delivery)_rv; -} - -static void weaveffi_events_Subscriber_jni_free(void* ctx) { - if (ctx == NULL) { return; } - JNIEnv* env = NULL; - int attached = weaveffi_jni_attach(&env); - if (env != NULL) { (*env)->DeleteGlobalRef(env, (jobject)ctx); } - weaveffi_jni_detach(attached); -} -``` - -- **Argument ownership.** Strings and buffered values are copied into - Kotlin objects before your method runs. An object argument - (`bus: EventBus`) arrives as an adopted wrapper your implementation - owns: keep it, or `close()` it to release the reference. If you neither - keep nor close it, the `Cleaner` releases it eventually. -- **Lifetime.** The global ref keeps your implementation reachable for as - long as the producer holds it (`subscribe` retains it until - `clearSubscribers()` or the bus is dropped); the vtable's `free` deletes - the ref, after which the object is collectable. A function that only - uses the interface for the call (`WeaveFFI.routeOnce`) frees it before - returning. -- **Exceptions.** A Kotlin exception thrown by a callback method never - unwinds through the native frame. The trampoline clears it, calls - `Throwable.toString()` for the message, and reports - `weaveffi_error_set(out_err, -4, ...)`. The producer aborts the call - that triggered the callback, and the original caller (sync, `suspend`, - or iterator step) sees `WeaveFFIException` with `code == -4` and a - message like `java.lang.IllegalStateException: ...`. The implementation - stays attached. `setCallbackExceptionHandler` doesn't apply here; it - only covers exceptions thrown while an async result is delivered. -- **Threads.** The producer may call from any thread. The trampoline - attaches a non-JVM thread to the `JavaVM` for the duration of the call - and detaches it afterward, so a callback invoked from a producer worker - (`publishLater` in the sample) runs on that worker with a fresh - `JNIEnv`. Nothing hops to the main thread; use - `withContext(Dispatchers.Main)` or a `Handler` yourself. - -## Rich (algebraic) enums - -A *rich* (algebraic) enum, a sum type whose variants carry associated -data, becomes an idiomatic Kotlin sealed class: one subclass (or object, -for a unit variant) per variant carrying that variant's fields. Rich -enums own no native resources and declare no JNI symbols. (A plain -C-style enum with no payloads stays a Kotlin `enum class` backed by an -`Int`; see above.) - -For the `shapes` module's `Shape` enum (`Empty`, `Circle { radius: f64 }`, -`Rectangle { width: f32, height: f32 }`, and -`Labeled { label: string, count: u8 }`), the surface follows this shape: - -```kotlin -/** An algebraic shape (sum type with associated data) */ -sealed class Shape { - /** The empty shape */ - object Empty : Shape() - /** A circle with a radius */ - data class Circle(val radius: Double) : Shape() - /** An axis-aligned rectangle */ - data class Rectangle(val width: Float, val height: Float) : Shape() - /** A labeled shape with a small count */ - data class Labeled(val label: String, val count: Byte) : Shape() -} -``` - -On the wire a `Shape` is a value buffer holding the `i32` variant tag -followed by the active variant's fields; the wrapper layer packs and -decodes the `ByteArray` at the JNI boundary. Construct variants directly -and branch with an exhaustive `when`: - -```kotlin -val c = Shape.Circle(2.0) -when (c) { - is Shape.Circle -> println(c.radius) // 2.0 - else -> {} -} -val bigger = WeaveFFI.scale(c, 3.0) // returns a new Shape value -println(WeaveFFI.describe(bigger)) -``` - -Values are plain Kotlin data: there's no handle and nothing to close. A -payload that holds an object follows the token rules above. - -## Build instructions - -### Android - -1. Install Android Studio plus the NDK. -2. Cross-compile the Rust cdylib for every Android ABI you support: - - ```bash - rustup target add aarch64-linux-android x86_64-linux-android - export ANDROID_NDK_HOME=/path/to/ndk - cargo ndk -t arm64-v8a -t x86_64 build --release -p your_library - ``` - -3. Place each `libyour_library.so` under `src/main/jniLibs//`, open - `generated/kotlin` in Android Studio, sync Gradle, and build the AAR - (`./gradlew :kvstore:assemble`). The NDK's CMake builds the JNI shim - (`libweaveffi.so`) against the generated C header in the sibling `c/` - output, so generate both targets. -4. Add the resulting AAR as a dependency in your app module. - -### Desktop JVM - -Compile the shim against a JDK and link it to the producer cdylib, then -compile `WeaveFFI.kt` with your sources against `kotlinx-coroutines-core` -and run with the shim on `java.library.path`. This is what -`conformance/run.sh` does: - -```bash -cc -shared -fPIC generated/kotlin/src/main/cpp/weaveffi_jni.c \ - -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -Igenerated/c \ - -Ltarget/debug -lkvstore -Wl,-rpath,target/debug \ - -o build/libweaveffi.dylib # .so on Linux - -kotlinc generated/kotlin/src/main/kotlin/com/weaveffi/WeaveFFI.kt Main.kt \ - -cp kotlinx-coroutines-core-jvm.jar -include-runtime -d build/app.jar +The coroutine resumes with `CancellationException` when it's cancelled or +when the producer completes the call with code `-5`. Completions arrive on +producer threads; the token is destroyed once the call completes. The +coroutines runtime (`kotlinx-coroutines-core`) is a dependency only when the +API has async functions. -java -Djava.library.path=build -cp build/app.jar:kotlinx-coroutines-core-jvm.jar Main -``` +## Callbacks -`WeaveNativeLibrary` calls `System.loadLibrary("weaveffi")` once, from the -first companion `init` that runs. - -## Packaging - -`weaveffi package --target kotlin` assembles an AAR-style module that is -self-contained: the shim compiles against a bundled copy of the C header -(`src/main/cpp/include/.h`), and the prebuilt producer libraries -land in two places: - -- Android slices under `src/main/jniLibs//lib.so` - (`arm64-v8a`, `x86_64`), which the packaged `CMakeLists.txt` links as an - imported target per `ANDROID_ABI` and Gradle bundles through - `sourceSets { getByName("main") { jniLibs.srcDirs("src/main/jniLibs") } }`. -- Desktop slices under `src/main/resources/natives//` - (`darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, - `windows-x64`). Build the shim once per platform with - `cmake -S src/main/cpp -B build -DWEAVEFFI_PLATFORM_ID=darwin-arm64 && - cmake --build build`; the packaged loader picks `natives/-/` - from `os.name` and `os.arch`, extracts the producer library and the shim - from the classpath, and falls back to `System.loadLibrary` when the - resources are absent. - -`wasm32` binaries are skipped. Kotlin is the one target besides Wasm that -bundles a non-desktop platform. See -[Packaging and Distribution](../guides/packaging.md). - -## Memory and ownership - -- Object wrappers own one strong reference, released by `close()` or the - `Cleaner`. Structs and rich enums are plain Kotlin values. -- Strings returned from JNI are fresh Java strings; the shim frees the - Rust pointer with `weaveffi_free_string` before returning. String - parameters are converted to heap UTF-8 for the call and freed afterward. -- Byte arrays returned from JNI are copied with `SetByteArrayRegion`, then - the Rust buffer is freed with `weaveffi_free_bytes`. -- Buffered values cross JNI as `ByteArray` copies of their value buffers: - the shim copies a returned buffer and releases the original; a buffered - parameter is encoded by the Kotlin layer and borrowed by the producer - for the call (`GetByteArrayElements` / `ReleaseByteArrayElements`). - Object fields inside are cloned on the way in and adopted on the way out. -- Callback-interface implementations are pinned by a JNI global ref until - the producer's `free` runs. - -## Async support - -Async IDL functions (`async: true`) are exposed as `suspend fun`s built on -`suspendCancellableCoroutine`. The public wrapper passes a -`WeaveContinuation` (a small class with `onSuccess`/`onError` methods) to -a private external launcher. From the `kvstore` sample: - -```kotlin -@JvmStatic private external fun nativeCompactAsync(selfHandle: Long, cancelToken: Long, callback: Any) - -suspend fun compact(): Long = suspendCancellableCoroutine { cont -> - nativeCompactAsync(handle, 0L, WeaveContinuation(cont) { code, message, payload -> KvException.fromCode(code, message, payload) }) -} - -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)) } -} -``` - -The JNI launcher allocates a per-call context holding a `NewGlobalRef` to -the continuation and hands the C ABI a completion callback. That callback -attaches the producer's thread to the JVM if needed, calls -`onSuccess`/`onError`, deletes the global ref, frees the context exactly -once, and detaches the thread if it attached it: - -```c -static void weaveffi_events_EventBus_publish_later_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 = weaveffi_jni_attach(&env); - if (env == NULL) { free(ctx); return; } - if (err != NULL && err->code != 0) { - /* copy code, message, payload; weaveffi_error_free(err); call onError(int, String, byte[]) */ - } else { - /* box the result; call onSuccess(Object) */ - } - weaveffi_jni_handle_uncaught(env); - (*env)->DeleteGlobalRef(env, ctx->callback); - free(ctx); - weaveffi_jni_detach(attached); -} -``` - -The completion callback fires exactly once, on a producer thread. Result -buffers passed to it are owned by the consumer, so the shim copies them -into Java objects and releases them with `weaveffi_free_string` or -`weaveffi_free_bytes`; a reported error is heap-boxed and released with -`weaveffi_error_free`; an object result is resumed as a raw handle that -the suspend wrapper adopts. A `throws: true` callable resumes with the -typed domain exception (or `WeaveFFIException` for a runtime code); one -without `throws` resumes with `WeaveFFIException`. - -An exception thrown by the resumed coroutine itself (not by a callback -interface) has no Kotlin caller on that thread. The glue routes it through -`weaveffi_jni_handle_uncaught`, which delivers it to the handler installed -with `WeaveFFI.setCallbackExceptionHandler` or, with no handler, logs it -with `ExceptionDescribe` and drops it. - -For callables marked `cancellable: true`, the private launcher carries a -`cancelToken: Long` and the shim casts it to `weaveffi_cancel_token*`, -but the public suspend wrapper passes `0L` (no token); coroutine -cancellation isn't wired to the native token. +A callback interface is a plain Kotlin `interface`. Passing an +implementation pins it with a JNI global reference until the producer +releases it. Producer threads may call it from anywhere: the shim attaches +a thread to the JVM once (as a daemon) and detaches it when the thread +exits. A method that throws reports `-4` with the exception's `toString()` +to the producer, which surfaces it to the original caller as an +`FfiException`; nothing unwinds through native frames. ## Iterators -`iter` returns surface as `Iterator`, backed by a generated -per-function wrapper class that is fully lazy: the external launcher -returns the raw iterator handle as a `Long`, and each `hasNext()` -lookahead issues exactly one `nativeNext` call, which maps to one producer -`_next`. From the `kvstore` sample (`Store.listKeys` returns -`iter`): - -```kotlin -class KvStoreListKeysIterator internal constructor(handle: Long) : Iterator, AutoCloseable { - private val ref = WeaveNativeRef(handle) { h -> nativeDestroy(h) } - private val cleanable = weaveCleaner.register(this, ref) - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (ref.peek() == 0L) return false - val slot = nativeNext(ref.get()) - 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 - } - - /** Releases the native iterator; safe to call more than once. */ - override fun close() { - cleanable.clean() - } - - companion object { - init { WeaveNativeLibrary.ensureLoaded() } - - @JvmStatic private external fun nativeNext(handle: Long): Array? - @JvmStatic private external fun nativeDestroy(handle: Long) - } -} -``` - -The `nativeNext` shim pulls one element and returns it in a one-slot -`Object[]`; `null` means the stream is exhausted. A string element is -freed with `weaveffi_free_string` right after it's copied; a buffered -element is copied into a `ByteArray`, released with `weaveffi_free_bytes`, -and decoded in `next()`; an object element is adopted into a wrapper. The -native handle is destroyed exactly once: eagerly when `hasNext()` sees -exhaustion, from `close()` when you abandon iteration early (the class is -`AutoCloseable`), or by the `Cleaner` otherwise. A closed iterator reports -`hasNext() == false`. - -Use it as a plain `Iterator` or lift it into a `Sequence`: - -```kotlin -for (key in store.listKeys(prefix = null)) println(key) -val texts = bus.messages().asSequence().toList() -``` - -Errors from the launcher and from each `next` follow the function's error -strategy: `listKeys` throws the typed `KvException` subclass from the step -that failed, while a non-throwing function throws the generic -`WeaveFFIException`. +An `iter` return is a `NativeIterator`, which is both an `Iterator` +and `AutoCloseable`. Each `hasNext()` pulls at most one element. The native +iterator is released when the sequence is exhausted, on `close()`, or by the +cleaner. ## Known limitations -- `minSdk` is 33 because disposal relies on `java.lang.ref.Cleaner`. -- Unsigned integers are carried as signed bit patterns (`u8` → `Byte`, - `u16` → `Short`, `u64` → `Long`); only `u32` is widened. -- Coroutine cancellation isn't propagated to the native cancel token. -- The generated `build.gradle.kts` pins the Kotlin Android plugin and - `kotlinx-coroutines-core` versions; adjust them to your project. -- The Kotlin module compiles the generated `WeaveFFI.kt` as one file; the - conformance consumers compile it in the same module so `internal` - members are reachable, which isn't required for normal use. -- Callback methods run on whichever thread the producer uses; nothing is - marshalled to the main thread. - -## Troubleshooting - -- **`UnsatisfiedLinkError: Couldn't find libweaveffi.so`**: the JNI shim - wasn't built or isn't on `java.library.path` (desktop) or inside the AAR - (Android). On Android the producer cdylib must also sit under - `src/main/jniLibs//`. -- **`UnsatisfiedLinkError: ... different C ABI revision`**: `JNI_OnLoad` - found a producer library built for another ABI revision. Rebuild the - producer with the same WeaveFFI version. -- **`IllegalStateException: WeaveFFI object used after close()`**: a - wrapper was used after `close()` (or after `use { }` returned). Adopt a - second wrapper (`share()`, or the one a callback hands you) if you need - a longer-lived reference. -- **`WeaveFFIException` with code -4**: a callback interface method threw. - The message carries the throwable's `toString()`; fix the implementation - or catch the exception at the call site. -- **R8/ProGuard removes `WeaveFFI` symbols**: keep the wrapper package with - `-keep class com.weaveffi.** { *; }`; the shim resolves `SubscriberJni` - and the exception classes by name. +- Gradle builds aren't exercised by the conformance suite, which compiles + the same sources with `kotlinc` and the shim with CMake. +- On Windows the shim never detaches producer threads it attached. +- Interior NULs in an exception message are cut at the first NUL. +- Unsigned integers have no `UInt`/`ULong` surface. diff --git a/docs/src/generators/node.md b/docs/src/generators/node.md index 8ac34ef6..6873efd6 100644 --- a/docs/src/generators/node.md +++ b/docs/src/generators/node.md @@ -1,671 +1,196 @@ # Node.js -## Overview - -The Node.js target produces a CommonJS loader, TypeScript type -definitions, and the complete N-API addon C source (plus a -`binding.gyp`) that bridges JS to the C ABI. The loader honors a -`WEAVEFFI_ADDON` environment override, then prefers the node-gyp build -output (`./build/Release/weaveffi.node`), and falls back to a prebuilt -binary placed next to it as `index.node`. On top of the raw native -bindings it layers the idiomatic wrappers: error classes, object classes -with `close()` and `Symbol.dispose`, plain-object records and tagged-union -rich enums, duck-typed callback interfaces, `Promise`-returning async -functions, lazy iterators, and camelCased function wrappers. The surface -follows ABI revision 2, and 64-bit integers are `bigint` everywhere. +The Node.js target generates an npm package: an ES module with TypeScript +declarations, and a small N-API addon that `npm install` compiles with +node-gyp and that calls the library's C ABI. The JavaScript API is shared +with the [WebAssembly target](wasm.md); only the transport underneath +differs, so code written against one runs against the other. ## What gets generated -| File | Purpose | -|------|---------| -| `generated/node/index.js` | CommonJS loader: tries `./build/Release/weaveffi.node`, falls back to `./index.node`; idiomatic wrappers | -| `generated/node/types.d.ts` | TypeScript declarations for the public surface | -| `generated/node/weaveffi_addon.c` | N-API addon source: marshaling, promises, callback vtables, threadsafe functions | -| `generated/node/binding.gyp` | node-gyp build file (includes `../c`, links `-lweaveffi`) | -| `generated/node/package.json` | npm package metadata (`main`, `types`, `gypfile`, install script) | +For a library named `kvstore` (the snapshot fixtures use `kitchen_sink`): -## Type mapping - -| IDL type | TypeScript type | Notes | -|---------------|----------------------|-------| -| `i8`, `i16`, `i32` | `number` | | -| `u8`, `u16`, `u32` | `number` | | -| `i64` | `bigint` | Full 64-bit range; never rounded | -| `u64` | `bigint` | Full 64-bit range; never rounded | -| `f32`, `f64` | `number` | IEEE values; NaN, infinities, `-0` preserved | -| `bool` | `boolean` | | -| `string` | `string` | | -| `bytes` | `Buffer` | | -| `StructName` | `StructName` (a plain object interface) | Value buffer | -| `InterfaceName` | `class InterfaceName` | One strong reference per instance; see [Objects](#objects-interfaces) | -| `InterfaceName?` | `InterfaceName \| null` | | -| `CallbackName` | `interface CallbackName` | Any object with the right methods; see [Callback interfaces](#callback-interfaces) | -| `EnumName` (plain, C-style) | `enum EnumName` | A frozen object with forward and reverse mappings at runtime | -| `EnumName` (rich / algebraic) | discriminated union type (e.g. `Shape`) | Value buffer | -| `T?` | `T \| null` | Value buffer | -| `[T]` | `T[]` | Value buffer | -| `{K: V}` | `Record` | Value buffer | -| `iter` | `IterableIterator` (lazy) | | - -`i64` and `u64` are `bigint` in every position: parameters, returns, -record fields, and callback arguments and results. Passing a `number` -where a `bigint` is expected is a type error in the addon; the `codec` -sample declares `roundtripI64(value: bigint): bigint` and -`roundtripU64(value: bigint): bigint`, and the `kvstore` sample's -`Store.count()` returns `1n`, not `1`. Floats cross as raw IEEE values -(`writeDoubleLE`/`readDoubleLE` in the value-buffer codec), so NaN, the -infinities, and `-0` round-trip. - -## Example IDL → generated code - -```yaml -version: "0.9.0" -modules: - - name: contacts - enums: - - name: Color - variants: - - { name: Red, value: 0 } - - { name: Green, value: 1 } - - { name: Blue, value: 2 } - - structs: - - name: Contact - fields: - - { name: name, type: string } - - { name: email, type: "string?" } - - { name: tags, type: "[string]" } - - functions: - - name: get_contact - params: - - { name: id, type: i32 } - return: "Contact?" - - - name: list_contacts - params: [] - return: "[Contact]" - - - name: set_favorite_color - params: - - { name: contact_id, type: i32 } - - { name: color, type: "Color?" } -``` - -Structs become TypeScript interfaces backed by plain JS objects (the -loader packs and unpacks them from value buffers with generated private -codec functions; there are no per-struct native symbols), and enums -become explicit numeric TypeScript enums: - -```typescript -export interface Contact { - name: string; - email: string | null; - tags: string[]; -} - -export enum Color { - Red = 0, - Green = 1, - Blue = 2, -} +```text +node/ +├── package.json ES module manifest; `npm install` runs `node-gyp rebuild` +├── binding.gyp builds the addon and links the library +├── kvstore_node.c the N-API addon +├── kvstore.h a copy of the C header the addon compiles against +├── index.js the API: one namespace export per top-level module +├── index.d.ts TypeScript declarations +├── runtime.js the shared runtime (codec, errors, wrappers) +└── README.md ``` -Functions are exported flat in lowerCamelCase with the module prefix -stripped by default (`strip_module_prefix = false` in `[generators.node]` -restores `_`-prefixed names); parameters are camelCased too, while -record field names keep their IDL spelling (`created_at`). Optional -return and parameter types use `| null`, arrays use `T[]`: - -```typescript -export function getContact(id: number): Contact | null -export function listContacts(): Contact[] -export function setFavoriteColor(contactId: number, color: Color | null): void -``` - -## Typed errors - -Every generated `index.js` exports `WeaveFFIError` (extending `Error` -with a numeric `code` and the raw `errorMessage`). A module's error -domain adds a class named after the domain plus one subclass per code, -each carrying its stable `CODE`. From the `kvstore` sample: +The package name defaults to the library identity's name; set +`package_name` under `[generators.node]` in `weaveffi.toml` to override it. +The addon is named `{library}_node.node`. -```js -class WeaveFFIError extends Error { - constructor(code, message) { - super('(' + code + ') ' + (message || '')); - this.name = 'WeaveFFIError'; - this.code = code; - this.errorMessage = message || ''; - } -} - -class KvError extends WeaveFFIError { /* ... */ } - -class KeyNotFoundError extends KvError { - constructor(message) { - super(1001, message || 'key not found'); - this.name = 'KeyNotFoundError'; - } -} -KeyNotFoundError.CODE = 1001; -// ExpiredError, StoreFullError, IoError follow the same shape. - -const __kvErrorCodes = Object.freeze({ 1001: KeyNotFoundError, 1002: ExpiredError, 1003: StoreFullError, 1004: IoError }); -function __kvErrorFrom(code, message, payload) { - const _cls = __kvErrorCodes[code]; - return _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); -} -``` +## Install, build, and load -A callable with `throws: true` rebrands any native failure through the -domain's code map, so consumers catch the typed class: +The generated directory is a complete package. Install it like any other, +pointing the build at the native library with the `{PREFIX}_LIBRARY` +environment variable (the library file or its directory) or the matching +npm option: -```js -try { - store.put('alpha', Buffer.from('1'), EntryKind.Volatile, null); -} catch (e) { - if (e instanceof StoreFullError) { - // typed case; e.code === 1003 - } else if (e instanceof KvError) { - // any kv domain error - } else if (e instanceof WeaveFFIError) { - // runtime code (negative) - } -} +```sh +KVSTORE_LIBRARY=/opt/kvstore/lib/libkvstore.so npm install ./node +npm install ./node --kvstore-library=/opt/kvstore/lib ``` -A callable without `throws` has the same JS signature and rebrands -through `__generic`, so every failure is a plain `WeaveFFIError`. Unknown -codes on the typed path fall back to `WeaveFFIError` as well. - -### Runtime error codes - -| Code | ABI name | When | -|------|----------|------| -| -1 | `GENERIC_ERROR_CODE` | The producer reported a failure with no domain code | -| -2 | `PANIC_ERROR_CODE` | The Rust producer panicked inside an export or a spawned async future; also used by the loader for a malformed value buffer | -| -3 | `MARSHAL_ERROR_CODE` | A null object or a malformed value at the boundary; also thrown by the loader for use after `close()`, a wrong wrapper class, or a null callback implementation | -| -4 | `FOREIGN_ERROR_CODE` | A callback-interface implementation threw, is missing a method, or returned a value of the wrong type | - -They surface as a thrown `WeaveFFIError` from a sync call, as a rejected -`Promise` from an async one, and from the `next()` step of an iterator. -JavaScript has no trap path: a non-throwing callable throws the generic -class the same way. - -## Objects (interfaces) - -An `interfaces:` entry becomes a JS class holding one strong reference to -a reference-counted native object. A constructor named `new` becomes the -class constructor (`new EventBus()` in the `events` sample); other -constructors become static factories; methods are instance methods and -statics are static methods, all camelCased. From the `kvstore` sample's -`Store` (trimmed from `index.js`): +Without either, node-gyp looks for the library in the package directory. +The addon links `{library}` and carries an rpath to its own directory and to +the directory it was linked against, so a library copied next to the addon +also loads. The setting applies when the addon is built; at run time the +operating system's loader finds the library as usual. ```js -// Borrow a live object wrapper's native handle for one call. The wrapper -// keeps its own reference; the producer clones if it retains the object. -function __borrow(o, cls) { - if (!(o instanceof cls)) { - throw new WeaveFFIError(-3, 'expected an instance of ' + cls.name); - } - if (!o._handle) { - throw new WeaveFFIError(-3, cls.name + ' used after close()'); - } - return o._handle; -} +import { kv, KvstoreError } from 'kvstore'; -class Store { - static open(path) { - const _r = __invoke(addon.Store_open, [path], __kvErrorFrom); - return Store._adopt(_r); - } - put(key, value, kind, ttlSeconds) { - return __invoke(addon.Store_put, [__borrow(this, Store), key, value, kind, __encode((w, v) => __wOpt(w, v, (w, v) => w.i64(v)), ttlSeconds)], __kvErrorFrom); - } - count() { - return __invoke(addon.Store_count, [__borrow(this, Store)], __generic); - } - compact() { - return __invokeAsync(addon.Store_compact, [__borrow(this, Store)], __kvErrorFrom); - } - static defaultCapacity() { - return __invoke(addon.Store_default_capacity, [], __generic); - } - close() { - if (this._handle) { - Store._cleanup.unregister(this); - addon.Store__destroy(this._handle); - this._handle = 0n; - } - } - _cloneHandle() { - return addon.Store__clone(__borrow(this, Store)); - } -} -Store._adopt = 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); } -}); -if (typeof Symbol.dispose === 'symbol') { - Store.prototype[Symbol.dispose] = Store.prototype.close; -} +const store = kv.Store.open('/tmp/data'); +store.put('greeting', new TextEncoder().encode('hi'), kv.EntryKind.Volatile, null); +store.close(); ``` -The typed declarations mirror the class, with `@throws` and -`@deprecated` JSDoc tags: - -```typescript -export class Store { - /** @throws {KvError} */ - static open(path: string): Store; - /** @throws {KvError} */ - put(key: string, value: Buffer, kind: EntryKind, ttlSeconds: bigint | null): boolean; - count(): bigint; - /** @throws {KvError} */ - compact(): Promise; - static defaultCapacity(): bigint; - /** - * Release this wrapper's reference to the native object. Safe to call - * more than once; a wrapper that is never closed is released when it is - * garbage collected. Using the wrapper after `close()` throws. - */ - close(): void; - /** Alias of `close()` for `using` declarations. */ - [Symbol.dispose](): void; -} -``` +Importing the package loads the addon and checks the contract before any +other call: the library must implement ABI revision 3, and each top-level +module's checksum must match the one the bindings were generated from. +Otherwise the import fails with an error naming the mismatched module. -- **Disposal is `close()` plus a `FinalizationRegistry` backstop.** - `close()` releases the wrapper's reference exactly once and is - idempotent; `Symbol.dispose` aliases it so `using store = Store.open(...)` - works where the runtime supports explicit resource management. A - wrapper collected unclosed is released by the registry, so `_destroy` - runs exactly once either way. -- **Use after close throws** `WeaveFFIError` with code -3 (`Store used - after close()`), from `__borrow`, before anything reaches the addon. -- **Clones mint a new strong reference.** `_cloneHandle()` calls - `_clone` whenever the wrapper must hand the producer a reference it - will own; every adopted wrapper (a return, an async result, an iterated - element, a buffer token) owes its own `close()`. Two wrappers over the - same object (`share()`, or the bus handed to `onAttached`) are - independent: closing one leaves the other usable. - -### Objects as parameters, returns, and inside values - -A top-level object parameter is borrowed (`__borrow`); a returned object -is adopted. `Store?` is `Store | null` both ways: +`weaveffi package` produces the esbuild-style layout instead: the main +package lists one `{package}-{os}-{cpu}` package per platform in +`optionalDependencies`, each gated by npm `os` and `cpu` and carrying the +prebuilt library, and the addon links the one npm installed. -```js -larger(other) { - const _r = __invoke(addon.Store_larger, [__borrow(this, Store), (other == null ? null : __borrow(other, Store))], __generic); - return (_r == null ? null : Store._adopt(_r)); -} -``` +## Modules and names -Objects inside records, lists, map values, optionals, and rich-enum -payloads are ordinary properties (`store: Store; mirror: Store | null` in -`StoreInfo`). On the wire they're `u64` tokens: the pack function writes a -fresh `_cloneHandle()` per object and the unpack function adopts: +Each top-level IDL module is a frozen namespace object, and nested modules +are nested namespaces, so equal names in different modules never collide: ```js -function __packStoreInfo(w, v) { - w.str(v.label); - w.u64(v.store._cloneHandle()); - __wOpt(w, v.mirror, (w, v) => w.u64(v._cloneHandle())); - w.i64(v.count); -} -function __unpackStoreInfo(r) { - return { - label: r.str(), - store: Store._adopt(r.u64()), - mirror: __rOpt(r, (r) => Store._adopt(r.u64())), - count: r.i64(), - }; -} +export const kitchen = Object.freeze({ + KitchenErrorsError: kitchen$KitchenErrorsError, + NotFoundError: kitchen$NotFoundError, + InvalidInputError: kitchen$InvalidInputError, + Priority: kitchen$Priority, + Gadget: kitchen$Gadget, + boolId: kitchen$boolId, ``` -`Store.openMany(paths)` returns `Store[]` (one adopted wrapper each); -`Store.totalCount(stores, extra)` clones every object it encodes. An -async callable returning an object resolves with an adopted wrapper, and -an `iter` adopts one per step. - -## Callback interfaces +Functions and methods are camelCase. A name that is a JavaScript reserved +word gains a trailing underscore (`delete` becomes `delete_`), as does an +instance method named `close` or `constructor` and a static named `name` or +`length`. Record fields keep their IDL spelling. -A `callback_interfaces:` entry becomes a TypeScript `interface`. Any -object with the right methods satisfies it: a class instance, an object -literal, or a module. From the `events` sample: +## Type mapping -```typescript -export interface Subscriber { - route(topic: string): Delivery; - onMessage(message: Message): bigint; - onAttached(bus: EventBus): void; -} -``` +| IDL type | TypeScript type | Notes | +|----------|-----------------|-------| +| `i8`, `i16`, `i32`, `u8`, `u16`, `u32` | `number` | | +| `i64`, `u64` | `bigint` | Integral numbers are accepted as arguments; out-of-range values throw a `RangeError` | +| `f32`, `f64` | `number` | NaN, the infinities, and `-0` round-trip | +| `bool` | `boolean` | | +| `string` | `string` | UTF-8, passed as pointer and length, so interior NULs survive | +| `bytes` | `Uint8Array` | | +| record | `interface` | A plain object; fields keep their IDL names | +| C-style enum | `enum` | A frozen object with forward and reverse mappings | +| rich enum | tagged union | `{ tag: 'Circle', radius: 2.5 }` | +| interface | `class` | See [Objects](#objects-and-lifetime) | +| callback interface | `interface` | Any object with the methods | +| `T?` | `T \| null` | `undefined` is accepted as `null` | +| `[T]` | `T[]` | | +| `{K: V}` | `Record` | Keys are strings at run time; a `Map` is accepted too | +| `iter` | `IterableIterator` | Lazy | + +Arguments are type-checked: a value of the wrong type throws a `TypeError` +before the native call. + +## Objects and lifetime + +An interface is a class whose instances each own one strong reference to the +native object. `close()` releases it, and so does `[Symbol.dispose]()` (for +`using` declarations); both are idempotent, and a wrapper that is garbage +collected unclosed is released by a `FinalizationRegistry`. Every call lends +the wrapper to the native side for its duration, so a `close()` that races +the call (from a callback, or while an async method is pending) waits for it +to return. Using a closed wrapper throws a code -3 error. ```js -const subscriber = { - route(topic) { return topic === 'quiet' ? Delivery.Skip : Delivery.Accept; }, - onMessage(message) { console.log(message.topic, message.text); return 1n; }, - onAttached(bus) { this.bus = bus; }, // or bus.close() to release it now -}; -const bus = new wv.EventBus(); -bus.subscribe(subscriber); + poke(times) { + const $self = $lend(this, kitchen$Gadget); + try { + return $raw.kitchen_sink_kitchen_Gadget_poke($self, times); + } catch ($e) { + throw $from$kitchen$KitchenErrors($e); + } finally { + $unlend(this); + } + } ``` -The loader wraps your object in a small adapter that converts arguments -(decoding buffered values, adopting objects) and maps camelCase method -names to the ABI's snake_case: +The synchronous constructor named `new` is the class constructor; other +constructors are static factories, and a class without one can't be +constructed directly. Objects returned, awaited, iterated, or decoded from a +record are new wrappers the caller owns. Two wrappers may share one native +object. -```js -function __adaptSubscriber(impl) { - if (impl === null || impl === undefined) { - throw new WeaveFFIError(-3, 'Subscriber implementation must be an object'); - } - return { - route(topic) { - return impl.route(topic); - }, - on_message(message) { - return impl.onMessage(__decode(__unpackMessage, message)); - }, - on_attached(bus) { - return impl.onAttached(EventBus._adopt(bus)); - }, - }; -} -``` +## Errors -The addon keeps the adapter behind a `napi_ref`, installs one static -vtable per interface, and gives each implementation its own -`napi_threadsafe_function`. A trampoline calls the implementation -directly when the producer is on the JS thread, and otherwise hops the -call to the JS thread and blocks until it has run: - -```c -static weaveffi_events_Delivery weaveffi_events_Subscriber_route_tramp(void* ctx, const char* topic, weaveffi_error* out_err) { - weaveffi_events_Subscriber_route_frame f; - memset(&f, 0, sizeof f); - f.hdr.out_err = out_err; - f.topic = topic; - weaveffi_napi_cb_ctx* c = (weaveffi_napi_cb_ctx*)ctx; - if (weaveffi_napi_on_js_thread()) { - weaveffi_events_Subscriber_route_invoke(c->env, c, &f); - } else { - weaveffi_napi_cb_req req; - req.ctx = c; - req.method = 0; - req.frame = &f; - weaveffi_napi_cb_hop(&req); // napi_call_threadsafe_function + wait on a condvar - } - return f.result; -} -``` +Every error extends the package's root class, `{Package}Error` (here +`KitchenSinkError`), which carries the ABI `code`: -- **Argument ownership.** Strings and buffered values are decoded into - fresh JS values. An object argument (`bus`) is adopted into a wrapper - your implementation owns: keep it, or `close()` it; the finalizer - backstops it otherwise. -- **Lifetime.** The `napi_ref` keeps the implementation alive for as long - as the producer holds it; the vtable's `free` releases the reference and - the threadsafe function (hopping to the JS thread if needed). The - threadsafe function is unref'd, so a live implementation doesn't by - itself keep the event loop alive. -- **Errors.** A thrown exception, a missing method, or a return value of - the wrong type (for example a `number` where a `bigint` is required) - never unwinds through the C frame. The addon clears the pending - exception, takes its `message`, and reports - `weaveffi_error_set(out_err, -4, ...)`. The producer aborts the call - that triggered the callback and the caller sees `WeaveFFIError` with - `code === -4`, thrown from a sync call or rejecting the `Promise` of an - async one. The implementation stays attached. -- **Threads.** Callback methods always run on the JS thread. When the - producer calls from one of its own threads (the `events` sample's - `publishLater`), that thread blocks until the JS thread has run the - method, which requires the event loop to be free. A synchronous call - from JS that waits on a producer worker which in turn needs a callback - will deadlock, since the JS thread is busy in the synchronous call. Use - async callables for any path that invokes callbacks from a worker. - -## Rich (algebraic) enums - -A *rich* (algebraic) enum is a sum type whose variants carry associated -data. A plain C-style enum stays a numeric TypeScript `enum`, but a rich -enum crosses the ABI as a serialized value buffer (`i32` tag, then the -active variant's fields) and surfaces in JS as a plain **tagged-union -object** with a string `tag` property naming the active variant. No -native handles, no classes, no destructors. - -Take a `Shape` enum with variants `Empty`, `Circle { radius: f64 }`, -`Rectangle { width: f32, height: f32 }`, and -`Labeled { label: string, count: u8 }`. The generated `types.d.ts` -declares a discriminated union: - -```typescript -export type Shape = - | { tag: 'Empty' } - | { tag: 'Circle'; radius: number } - | { tag: 'Rectangle'; width: number; height: number } - | { tag: 'Labeled'; label: string; count: number }; -``` +- Each error domain is a class (`kitchen.KitchenErrorsError`), and each of + its codes a subclass (`kitchen.NotFoundError`) with a static `CODE` and the + IDL message as its default message. Payload fields become properties. +- A cancelled call rejects with `CancelledError` (code -5). +- Runtime failures throw the root class itself: -1 generic, -2 producer + panic, -3 marshalling failure, -4 a callback implementation failed. -The loader carries one private pack and one unpack function per rich -enum: packing switches on the string `tag`, writes the `i32` -discriminant, then the variant's fields; unpacking reads the tag and -builds the matching object. Consumers construct and match variants as -ordinary JS values: +Only functions declared `throws` map codes onto their module's domain. -```js -const { describe, scale } = require('./index.js'); - -const circle = { tag: 'Circle', radius: 2.0 }; -console.log(describe(circle)); -const bigger = scale(circle, 3.0); -if (bigger.tag === 'Circle') { - console.log(bigger.radius); // 6 -} -``` +## Async and cancellation -There is nothing to release: values are copied across the boundary, and -the loader frees each returned native buffer immediately after decoding -it. A variant payload that holds an object follows the token rules above. - -## Build instructions - -The generated addon is self-contained: run `npm install` (the install -script runs `node-gyp rebuild` on the generated `binding.gyp`) inside -`generated/node/` with the generated C headers at `../c` and the -producer cdylib on the linker path: - -```bash -cargo build -p kvstore -weaveffi generate samples/kvstore/src/lib.rs -o generated --target c,node - -cd generated/node -npm install # builds build/Release/weaveffi.node -DYLD_LIBRARY_PATH=../../target/debug node -e " - const kv = require('./index.js'); - const store = kv.Store.open('/tmp/cache.kv'); - console.log(store.count()); - store.close(); -" -``` +An async function returns a `Promise`. The completion may arrive on any +producer thread; the addon settles the promise on the JavaScript thread. A +cancellable function takes an optional last argument with an `AbortSignal`: -(Use `LD_LIBRARY_PATH` on Linux.) Copying a prebuilt platform binary in -as `index.node` also works, and the `WEAVEFFI_ADDON` env var can point -the loader at any built addon (the `conformance/node/` consumers use it; -see `conformance/run.sh`). - -## Packaging - -`weaveffi package --target node` emits the main npm package (`index.js`, -`types.d.ts`, `weaveffi_addon.c`, `binding.gyp`, `package.json`) plus one -platform package per desktop slice under `node/npm/--/` -(`darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, -`win32-x64`), each gated by npm `os`/`cpu` fields and bundling its -prebuilt producer library. The main package lists them as -`optionalDependencies`, so npm installs only the one matching the host. -Android and `wasm32` binaries have no npm tokens and are skipped. See -[Packaging and Distribution](../guides/packaging.md). - -## Memory and ownership - -- The N-API addon is responsible for all conversions between JS values - and C ABI types. Strings and byte buffers are copied into JS-managed - storage, so consumers never need to think about freeing memory. -- Buffered values (structs, rich enums, optionals, lists, maps) are - returned as plain JS values: the loader decodes the returned value - buffer, which the addon has already freed with the native free. Object - fields inside are cloned on the way in and adopted on the way out. -- Object wrappers own one strong reference; release it with `close()` - (or `using`); the `FinalizationRegistry` backstops forgotten wrappers - at GC time. -- `iter` returns are lazy JS iterables; see - [Iterators](#iterators). The native handle is released on - exhaustion, on early exit, or by a finalizer as a backstop. -- Errors from the C ABI are converted into JavaScript `Error` instances - by the addon, then rebranded into the typed error classes by the - loader before bubbling up to the caller. - -## Async support - -Async IDL functions return a `Promise`: - -```typescript -compact(): Promise; +```ts + export function doCancellable(input: string, options?: { signal?: AbortSignal }): Promise; ``` -The addon creates the promise with `napi_create_promise` and calls the -C ABI `_async` entry point, which runs the work on a native producer -thread. The promise is never settled from that thread: the completion -callback only stashes the result (or error) and posts it through a -`napi_threadsafe_function` whose settle callback runs on the JS event -loop and calls `napi_resolve_deferred` / `napi_reject_deferred` there: - -```c -static void weaveffi_events_EventBus_publish_later_napi_cb(void* context, weaveffi_error* err, int64_t result) { - weaveffi_events_EventBus_publish_later_napi_actx* ctx = (weaveffi_events_EventBus_publish_later_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"); - /* ... copy the payload buffer the same way ... */ - } else { - ctx->result = result; - } - weaveffi_error_free(err); - napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); -} -``` +Aborting the signal cancels the native token, and the call rejects with +`CancelledError`. An already-aborted signal cancels the call before it +starts. -The completion callback fires exactly once, on the producer thread. -Result buffers passed to it (strings, byte buffers, and value buffers) -are owned by the consumer, so the callback deep-copies them and then -releases them with `weaveffi_free_string` or `weaveffi_free_bytes`; a -reported error is heap-boxed and released with `weaveffi_error_free` -after its fields are copied. An object result is adopted into a wrapper -by the loader when the promise resolves. +## Callback interfaces -Rejected promises carry a numeric `code`; the loader rebrands the -rejection into the module's typed error class when the callable declares -`throws: true` (`Store.compact()` rejects with `KvError` subclasses), and -into the generic `WeaveFFIError` otherwise. A pending async call keeps -the event loop alive until it settles. +Any object with the interface's methods implements it; the methods are +camelCase. Arguments arrive converted (records decoded, objects as wrappers +the implementation owns), and the return value is checked against the IDL +type. A method that throws, or returns a value of the wrong type, fails the +native call with code -4 and the exception's message; nothing unwinds +through native code. -For functions marked `cancellable: true` the addon passes `NULL` for -the C ABI's cancel-token slot; the token is not surfaced to JS and -there is no `AbortSignal` parameter. +The producer may call back from any thread. A call made on the JavaScript +thread runs directly; one made on another thread runs on the JavaScript +thread while the producer thread waits. ## Iterators -`iter` returns are lazy: the addon hands back an opaque external -wrapping the native iterator handle, and the loader wraps it in a -shared `WeaveFFIIterator` class implementing the JS iterator protocol. -Nothing is drained up front; each `next()` issues exactly one native -`_iterNext` call: - -```js -class WeaveFFIIterator { - 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; - } -} - -listKeys(prefix) { - const _it = __invoke(addon.Store_list_keys, [__borrow(this, Store), __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); -} -``` - -The TypeScript declaration is `IterableIterator`, so a plain -`for...of` loop (or `[...store.listKeys(null)]`) works, and `break`ing -out of the loop triggers `return()`, which destroys the native iterator -early. The addon's `_iterNext` binding pulls one element, copies it into -a JS value (freeing the native string or buffer), and destroys the -iterator eagerly when the producer reports exhaustion; the external's -N-API finalizer backstops abandoned iterators at GC time. Buffered -elements are decoded per step; object elements are adopted through -`_wrapElem`. - -Errors from the launcher and each `next` step follow the function's -error strategy: `Store.listKeys` (throws) rebrands a failing step -through `__kvErrorFrom` into the typed `KvError` subclasses, while a -non-throwing function throws the generic `WeaveFFIError`. +An `iter` result is a lazy iterator: each `next()` makes one native call. +The native iterator is destroyed when it's exhausted, when it fails, on +`return()` (which `for...of` calls on `break`), on `close()`, or when the +iterator is garbage collected. ## Known limitations -- Callback methods run only on the JS thread. A producer thread that - invokes one blocks until the event loop services it, so a synchronous - call that waits on such a thread deadlocks; prefer async callables for - callback-heavy paths. -- `i64`/`u64` require `bigint`; a `number` in those positions is rejected - at the boundary. -- Cancellation tokens aren't exposed; there is no `AbortSignal` support. -- `Symbol.dispose` is only wired when the runtime defines it. -- The loader is CommonJS; import it from ESM with `createRequire` or a - default import. - -## Troubleshooting - -- **`Error: Cannot find module './index.node'`**: no addon binary was - found at either loader path. Run `npm install` in `generated/node/` - to build the generated addon with node-gyp, or copy a prebuilt - binary in as `index.node`. -- **`dlopen: ... image not found`**: the addon links against the - Rust cdylib at runtime; set `DYLD_LIBRARY_PATH` / - `LD_LIBRARY_PATH` or copy the cdylib next to `index.node`. -- **`WeaveFFIError: (-3) Store used after close()`**: the wrapper was - closed (or disposed by `using`) before the call. Adopt a second wrapper - if you need a longer-lived reference. -- **`WeaveFFIError: (-4) ...`**: your callback implementation threw, is - missing a method, or returned the wrong type; check that `bigint` - results are returned as `1n`, not `1`. -- **Process hangs during a sync call**: a callback is being invoked from a - producer thread while the JS thread is blocked in that call. Switch the - call to an async callable. -- **TypeScript complains about missing types**: point `tsconfig`'s - `paths` at `generated/node/types.d.ts` or include the generated - package in `compilerOptions.types`. +- A synchronous call whose producer blocks on a callback from another thread + deadlocks, because the JavaScript thread is busy with the call. +- A callback implementation that holds a wrapper of the object retaining it + forms a cycle the garbage collector can't see; close the wrapper or + release the callback explicitly. +- `{PREFIX}_LIBRARY` selects the library at build time, not at run time. +- Windows builds link `{library}.lib`, and the DLL must be on the loader's + search path. +- The package is ESM only, and its declarations need TypeScript 5.2 or + later (for `Symbol.dispose`). diff --git a/docs/src/generators/python.md b/docs/src/generators/python.md index a6bd26e1..552d9129 100644 --- a/docs/src/generators/python.md +++ b/docs/src/generators/python.md @@ -1,906 +1,216 @@ # Python -## Overview - -The Python target produces pure-Python `ctypes` bindings, `.pyi` type -stubs, and packaging files over the C ABI (revision 2). Calls go -through Python's built-in `ctypes` module, so there's no compilation -step, no native extension, and no third-party runtime dependency. The -generated package works on any Python 3.7+ interpreter that can -`dlopen` the shared library. +The Python target produces a pip-installable package of pure-Python +`ctypes` bindings over the C ABI (revision 3), plus `.pyi` type stubs. There's +no compilation step, no native extension, and no third-party runtime +dependency; the package works on any Python 3.9+ interpreter that can load +the producer library. Records and rich enums are dataclasses that cross the boundary as value -buffers; interfaces are reference-counted wrapper classes with `close()` -and a `__del__` backstop; callback interfaces are abstract base classes -the consumer subclasses; async functions surface as `async def` wrappers -integrated with asyncio; and `iter` returns are lazy Python -iterators. - -The trade-off is that `ctypes` calls are slower than compiled extensions -(`cffi`, `pybind11`, PyO3). For typical FFI workloads the overhead is -negligible compared to the work done inside the Rust library. +buffers. Interfaces are reference-counted wrapper classes. Callback +interfaces are abstract base classes the consumer subclasses. Async +functions are coroutines whose cancellation cancels the native call, and +`iter` returns are lazy Python iterators. ## What gets generated -| File | Purpose | -|------|---------| -| `python//__init__.py` | Re-exports the public API from `weaveffi.py` | -| `python//weaveffi.py` | ctypes bindings: library loader, ABI check, codecs, wrappers, classes | -| `python//weaveffi.pyi` | Type stub for IDE autocompletion and `mypy` | -| `python/pyproject.toml` | PEP 621 project metadata | -| `python/setup.py` | Fallback setuptools script | -| `python/README.md` | Basic usage instructions | - -The package directory follows the IDL `package.name` (a package named -`kvstore` produces `python/kvstore/...`); `weaveffi` is the default and -`package_name` under `[generators.python]` overrides it. - -The module verifies the producer's ABI revision at import time, so a -stale library fails fast instead of misreading the error struct or a -value buffer later: - -```python -_ABI_VERSION = 2 - - -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}" - ) -``` - -## Type mapping - -| IDL type | Python type hint | ctypes type | -|--------------|----------------------|------------------------------------| -| `i8`, `i16`, `i32`, `i64` | `int` | `c_int8`, `c_int16`, `c_int32`, `c_int64` | -| `u8`, `u16`, `u32`, `u64` | `int` | `c_uint8`, `c_uint16`, `c_uint32`, `c_uint64` | -| `f32`, `f64` | `float` | `c_float`, `c_double` | -| `bool` | `bool` | `c_int32` | -| `string` | `str` | `c_char_p` | -| `bytes` | `bytes` | `POINTER(c_uint8)` + `c_size_t` | -| `Struct` | `"StructName"` (a `@dataclass`) | value buffer: `POINTER(c_uint8)` + `c_size_t` | -| `Enum` (plain) | `"EnumName"` (`IntEnum`) | `c_int32` | -| `Enum` (rich) | `"EnumName"` (variant dataclasses) | value buffer, like `Struct` | -| `Interface` | `"InterfaceName"` (wrapper class) | `c_void_p` | -| `Interface?` | `Optional["InterfaceName"]` | `c_void_p` (NULL for `None`) | -| `CallbackInterface` | `"CallbackName"` (subclass of the generated ABC) | `c_void_p` ctx + `POINTER(Vtable)` | -| `T?` | `Optional[T]` | value buffer | -| `[T]` | `List[T]` | value buffer | -| `{K: V}` | `Dict[K, V]` | value buffer | -| `iter` | `Iterator[T]` (lazy) | opaque `c_void_p` iterator handle | - -Buffered types (structs, rich enums, optionals, lists, maps) cross the -boundary serialized in the -[value-buffer format](../reference/value-buffers.md); the generated -module ships a private `_BufferWriter`/`_BufferReader` pair plus one -pack and one unpack function per record and rich enum. Objects nested -inside a buffered value travel as object tokens (see -[Objects](#objects-interfaces)). - -Booleans cross the boundary as `c_int32` (`0`/`1`) because C has no -standard fixed-width boolean type across ABIs. - -### 64-bit integers and floats - -Python's `int` is arbitrary precision, so `i64` and `u64` round-trip -exactly with no wrapper type: the `ctypes` slots are `c_int64` and -`c_uint64`, and the value-buffer codec packs them with `struct` as -` 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() -``` - -Functions become snake_case Python functions with full type hints; -ctypes `argtypes`/`restype` are set up at the call site. A buffered -argument is packed with the generated codec; a buffered return is -decoded from the producer's buffer, which the wrapper then frees. From -the `kvstore` sample's `get_stats` (docstring trimmed): - -```python -def get_stats(store: "Store") -> "Stats": - _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(_borrow(store), ctypes.byref(_out_len), ctypes.byref(_err)) - _check_kv_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Stats(_data) -``` - -Enums become `IntEnum` subclasses: - -```python -class ContactType(IntEnum): - """Type of contact""" - Personal = 0 - Work = 1 - Other = 2 -``` - -Structs become plain `@dataclass` value classes. There are no C symbols -per struct: construction, equality, and repr come from the dataclass, -and instances cross the boundary serialized in value buffers by -generated private codec functions (`_pack_Contact` / `_unpack_Contact`): - -```python -@dataclass -class Contact: - """A contact record""" - - name: str - email: Optional[str] - age: int -``` - -The accompanying `.pyi` stub mirrors the public surface for IDE/mypy: - -```python -class ContactType(IntEnum): - Personal: int - Work: int - Other: int - -class Contact: - name: str - email: Optional[str] - age: int - def __init__(self, name: str, email: Optional[str], age: int) -> None: ... - -def create_contact(name: str, email: Optional[str], contact_type: "ContactType") -> "Contact": ... -``` - -Wrapper names drop the IDL module prefix by default and stay -snake_case, so `create_contact` in module `contacts` is exported as -plain `create_contact` (the C symbol keeps its full -`weaveffi_contacts_create_contact` name). Set -`strip_module_prefix: false` in the Python generator config (or under -`[global]`) to restore module-prefixed wrapper names like -`contacts_create_contact`. - -## Typed errors - -Every generated module defines `WeaveFFIError(Exception)` with `code` -and `message` attributes and the four runtime trap codes as class -constants: - -```python -class WeaveFFIError(Exception): - """An error reported through the C ABI. - - Positive codes are a module's declared error codes (raised as the - module's typed subclasses); negative codes are runtime traps: a generic - producer error (-1), a producer panic (-2), a marshalling failure (-3), - or an exception raised by a callback-interface implementation (-4). - """ - - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - FOREIGN_ERROR_CODE = -4 - - def __init__(self, code: int, message: str) -> None: - self.code = code - self.message = message - super().__init__(f"({code}) {message}") -``` - -A module that declares an error domain also gets a domain base class -and one subclass per code, each pinning its stable `CODE`; from the -`kvstore` sample: - -```python -class KvError(WeaveFFIError): - """Base exception for the `kv` module's error domain.""" - - -class KeyNotFound(KvError): - """key not found""" - - CODE = 1 - - def __init__(self, message: str = "key not found") -> None: - super().__init__(1, message) -``` - -Only callables marked `throws: true` in the IDL raise these typed -errors: their wrappers check the error slot with `_check_kv_error`, -which maps the code through `_kv_error_from` and raises `KeyNotFound`, -`IoError`, or (for codes outside the domain, such as a producer panic) -a plain `WeaveFFIError`. Their docstrings carry a `Raises` section -naming the domain. A callable without `throws` uses the generic -`_check_error`, which raises `WeaveFFIError` only if the producer -misbehaves. The error slot's message and payload are copied and then -released with `weaveffi_error_clear` before the exception is raised. +For the `kvstore` sample (package name `kvstore`, C prefix `kvstore`): -```python -try: - store.put("k", b"", EntryKind.Persistent, None) -except StoreFull: - ... # specific code -except KvError as e: - print(e.code, e.message) # any domain error -``` - -### Runtime error codes - -| Code | Constant | Meaning | Where it surfaces | -|------|----------|---------|-------------------| -| `-1` | `GENERIC_ERROR_CODE` | The producer reported an error without a declared code, or the wrapper itself detected misuse (a `None` object pointer, a wrapper used after `close()`). | Raised as `WeaveFFIError` from the call. | -| `-2` | `PANIC_ERROR_CODE` | The Rust implementation panicked; the export macros and the async spawner catch the unwind. | Raised as `WeaveFFIError`, or set as an awaited future's exception. | -| `-3` | `MARSHAL_ERROR_CODE` | Malformed input at the boundary (invalid UTF-8, a truncated value buffer, a bad enum discriminant). | Raised as `WeaveFFIError`. | -| `-4` | `FOREIGN_ERROR_CODE` | A callback-interface method implemented in Python raised. | Raised as `WeaveFFIError` from the producer call that invoked the callback (see [Callback interfaces](#callback-interfaces)). | - -Python has no non-raising call path: a non-throwing callable whose -error slot comes back non-zero still raises `WeaveFFIError`, so a -producer bug or a raising callback never goes unnoticed. - -## Objects (interfaces) - -An `interfaces:` entry becomes a Python class wrapping one strong -reference to a reference-counted producer object. Every interface -exports a `_clone` and `_destroy` pair on the C -ABI, bound once at module scope: - -```python -_lib.weaveffi_kv_Store_clone.argtypes = [ctypes.c_void_p] -_lib.weaveffi_kv_Store_clone.restype = ctypes.c_void_p -_lib.weaveffi_kv_Store_destroy.argtypes = [ctypes.c_void_p] -_lib.weaveffi_kv_Store_destroy.restype = None -``` - -The wrapper class (from the `kvstore` sample, trimmed): - -```python -class Store: - @classmethod - def _from_ptr(cls, ptr) -> "Store": - """Adopt one strong reference the producer handed over.""" - _obj = cls.__new__(cls) - _obj._ptr = ptr - return _obj - - def __init__(self) -> None: - raise TypeError("Store cannot be instantiated directly") - - def _clone_ptr(self): - """A new strong reference to the same object (a raw pointer the - receiver must eventually release), leaving this wrapper's own - reference untouched.""" - return _lib.weaveffi_kv_Store_clone(_borrow(self)) - - def close(self) -> None: - """Release this wrapper's reference. The object itself is dropped - when its last reference (here or in the producer) is released. - Idempotent; the wrapper is unusable afterwards.""" - _p = getattr(self, "_ptr", None) - self._ptr = None - if _p is not None: - _lib.weaveffi_kv_Store_destroy(_p) - - def __enter__(self) -> "Store": - return self - - def __exit__(self, *exc) -> bool: - self.close() - return False - - def __del__(self) -> None: - self.close() - - @classmethod - def open(cls, path: str) -> "Store": ... +```text +python/ +├── pyproject.toml # PEP 621 metadata, built with setuptools +├── README.md +└── kvstore/ # the import package, named after the C prefix + ├── __init__.py # re-exports the public API + ├── kvstore.py # runtime, prototypes, and wrappers + ├── kvstore.pyi # type stub + └── py.typed # PEP 561 marker ``` -- **Construction.** A constructor named `new` renders as `__init__` - (the `events` sample's `EventBus()`); any other constructor becomes a - `@classmethod` factory (`Store.open("/tmp/cache.kv")`). An interface - without a `new` constructor cannot be instantiated directly; its - `__init__` raises `TypeError`. Methods are instance methods, statics - are `@staticmethod`s, and deprecated members emit `DeprecationWarning` - at call time. -- **Disposal.** `close()` releases the wrapper's reference through the - `_destroy` symbol; it's idempotent, and the wrapper is a context - manager (`with Store.open(path) as store:`). `__del__` is a backstop - that closes an unclosed wrapper when the garbage collector reaches it. - The producer object itself is dropped only when the last reference - anywhere (any Python wrapper, the producer's own clones) is released. -- **Use after close.** `_borrow(obj)` is the only way a wrapper's - pointer reaches the producer. On a closed wrapper it raises - `WeaveFFIError(-1, "Store used after close()")`, whether the wrapper - is the receiver, a parameter, or a field inside a record or list being - packed: - - ```python - def _borrow(obj): - """The raw pointer of a live object wrapper, lent to the producer for the - duration of one call. Raises if the wrapper was already closed.""" - _p = obj._ptr - if _p is None: - raise WeaveFFIError(-1, f"{type(obj).__name__} used after close()") - return _p - ``` - -- **Copies mint new references.** A method that returns the receiver or - another existing object (`share()`, `fork()`) hands back a fresh - strong reference, adopted by `_from_ptr` into a new wrapper; closing - one wrapper never affects the other. The same holds for every object - read out of a value buffer. - -### Nullable objects, and objects inside values - -An `Interface?` parameter is passed as NULL for `None` and an -`Interface?` return maps a NULL pointer to `None`: - -```python -def larger(self, other: Optional["Store"]) -> Optional["Store"]: - _fn = _lib.weaveffi_kv_Store_larger - _fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(_borrow(self), (_borrow(other) if other is not None else None), ctypes.byref(_err)) - _check_error(_err) - if _result is None: - return None - return Store._from_ptr(_result) -``` - -Objects inside records, lists, maps, and optionals travel as 8-byte -object tokens in the value buffer. Writing a token mints a new strong -reference with `_clone_ptr()`, so the value can be dropped by the -producer without touching the wrapper's own reference; reading a token -adopts the reference into a fresh wrapper. From the `StoreInfo` record -(`store: Store`, `mirror: Store?`): - -```python -def _write_StoreInfo(_w: _BufferWriter, value: "StoreInfo") -> None: - _w.write_string(value.label) - _w.write_object(value.store) - if value.mirror is None: - _w.write_option_flag(False) - else: - _w.write_option_flag(True) - _w.write_object(value.mirror) - _w.write_i64(value.count) - - -def _read_StoreInfo(_r: _BufferReader) -> "StoreInfo": - return StoreInfo( - label=_r.read_string(), - store=Store._from_ptr(_r.read_object()), - mirror=(Store._from_ptr(_r.read_object()) if _r.read_option_flag() else None), - count=_r.read_i64(), - ) -``` - -`_BufferWriter.write_object` checks the wrapper is live immediately but -defers the actual clone to `finish()`, so an encoding that raises -part-way (a closed wrapper, a field of the wrong type) leaks no strong -references. `Store.open_many(paths) -> List[Store]` and -`Store.total_count(stores, extra)` in the `kvstore` sample show lists of -objects in both directions; each wrapper in a returned list owns its own -reference and must be closed (or left to `__del__`) individually. - -Iterators over objects (`iter`) adopt one reference per -`__next__`; async functions returning an object adopt the pointer inside -the completion callback and hand the wrapper to the awaiting coroutine. - -## Rich (algebraic) enums - -A rich (algebraic) enum is a sum type whose variants carry associated -data. Unlike a plain C-style `Enum`, which crosses the boundary as a -bare `ctypes.c_int32` discriminant, a rich enum crosses as a serialized -value buffer (`i32` tag, then the active variant's fields), and the -generator emits an idiomatic Python sum type with no FFI symbols -involved: a base class holding a nested `Tag` `IntEnum` and a `tag` -property, plus one `@dataclass` subclass per variant. - -Given a `Shape` enum with variants `Empty`, `Circle { radius: f64 }`, -`Rectangle { width: f32, height: f32 }`, and `Labeled { label: string, -count: u8 }`: +The distribution name is the package identity's `name` and the import +package is its C prefix, so a crate named `async-demo` installs as +`async-demo` and imports as `async_demo`. Both are configurable: -```python -class Shape: - """An algebraic shape (sum type with associated data)""" - - class Tag(IntEnum): - Empty = 0 - Circle = 1 - Rectangle = 2 - Labeled = 3 - - @property - def tag(self) -> "Shape.Tag": - """The discriminant of this value's active variant.""" - return type(self).TAG - - -@dataclass -class ShapeCircle(Shape): - """A circle with a radius""" - - TAG = Shape.Tag.Circle - - radius: float +```toml +[generators.python] +package_name = "kvstore-python" # distribution name (default: package name) +import_name = "kvstore_py" # import package (default: C prefix) +strip_module_prefix = true # `get_stats`, not `kv_stats_get_stats` ``` -Each variant class is also aliased under the base class -(`Shape.Circle` is `ShapeCircle`), so consumers construct variants -directly and discriminate with `isinstance` or the `tag` property: +## Install and load -```python -from weaveffi import Shape, describe, scale - -circle = Shape.Circle(2.0) -labeled = Shape.Labeled("unit", 3) - -if isinstance(circle, Shape.Circle): - print(circle.radius) # 2.0 -print(labeled.count) # 3 - -print(describe(circle)) # render via the C ABI -bigger = scale(circle, 3.0) # returns a brand-new Shape value +```bash +weaveffi generate src/lib.rs -o generated --target python +pip install generated/python ``` -Wrappers pack a `Shape` argument with the generated `_pack_Shape` codec -and unpack a returned buffer with `_unpack_Shape`, freeing the returned -buffer with `weaveffi_free_bytes`. Values are plain Python objects with -no native handle and no destructor. Variant fields of interface type -follow the object-token rules above. The `.pyi` stub mirrors the class -hierarchy (nested `Tag`, variant subclasses with typed fields) for IDE -and `mypy` support. +At import, the module loads the producer library from the first of: -## Callback interfaces +1. the path in `{PREFIX}_LIBRARY` (for example `KVSTORE_LIBRARY`), +2. a library bundled inside the package directory (what `weaveffi package` + produces), and +3. the system loader's search path, by platform file name + (`libkvstore.dylib`, `libkvstore.so`, or `kvstore.dll`). -A `callback_interfaces:` entry becomes an abstract base class. The -consumer subclasses it, implements every abstract method, and passes -an instance wherever the API takes that type. From the `kvstore` -sample: +It then checks `kvstore_abi_version()` against revision 3 and every +top-level module's contract checksum (`kvstore_kv_checksum()`) against the +value the bindings were generated with. A mismatch raises `ImportError` +naming the module, so a stale binding never misreads a buffer at runtime. +Every C prototype is bound once at this point, in a module-level table: ```python -class EvictionListener(abc.ABC): - """ - Consumer-implemented callback interface. Subclass it, implement every abstract method, and pass an instance wherever the API takes a `EvictionListener`; the producer may call the methods from any thread until it releases the instance. An exception raised by a method is reported to the producer as WeaveFFIError.FOREIGN_ERROR_CODE (-4) and aborts the call that was in progress. - """ - - @abc.abstractmethod - def on_evict(self, entry: "Entry", reason: "EvictionReason") -> bool: - """ - An entry left the store. Returns whether the listener wants to keep - receiving notifications; `false` detaches it. - """ +_c_kitchen_Gadget_describe = _bind("kitchen_sink_kitchen_Gadget_describe", ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_ErrorStruct)) ``` -```python -class Auditor(kvstore.EvictionListener): - def on_evict(self, entry, reason): - print(entry.key, reason) - return True +On macOS, the system `python3` strips `DYLD_LIBRARY_PATH`, so prefer +`{PREFIX}_LIBRARY` there. -store.set_eviction_listener(Auditor()) -``` - -Under the hood there's exactly one process-wide C vtable per callback -interface, built from `ctypes.CFUNCTYPE` trampolines held at module -scope so their function pointers stay valid for the process lifetime. -Passing an implementation registers it in the module-level `_cb_impls` -dict and hands the producer the integer key as its `ctx`, so the -producer never holds a raw reference to a Python object: +## Type mapping -```python -def _EvictionListener_on_evict_trampoline(ctx, entry_ptr, entry_len, reason, out_err): +| IDL type | Python type | Crosses the ABI as | +|---|---|---| +| `i8` to `u64` | `int` | `c_int8` to `c_uint64` | +| `f32`, `f64` | `float` | `c_float`, `c_double` | +| `bool` | `bool` | `c_bool` | +| `string` | `str` | UTF-8 bytes plus length (`c_char_p`, `c_size_t`) | +| `bytes` | `bytes` | pointer plus length | +| C-style enum | `IntEnum` subclass | `c_int32` | +| record | `@dataclass` | value buffer | +| rich enum | base class plus one `@dataclass` per variant | value buffer | +| `T?` | `Optional[T]` | value buffer | +| `[T]`, `{K:V}` | `List[T]`, `Dict[K, V]` | value buffer | +| interface | wrapper class | `c_void_p` | +| interface `?` | `Optional[...]` | `c_void_p`, NULL for `None` | +| callback interface | subclass of the generated ABC | context key plus vtable | +| `iter` | `Iterator[T]` | iterator handle | + +Strings and bytes pass without per-byte loops: a `str` is encoded once and +its bytes are lent to the call, so interior NUL characters survive. Returned +strings, bytes, and buffers are copied with `ctypes.string_at` and released +with `{prefix}_free_bytes`. Lists of a fixed-width number type are packed +and unpacked with one `struct` call. + +All modules share one Python namespace. Nested modules' declarations are +exported next to their parents', so names must be unique across modules +(set `strip_module_prefix = false` to prefix function names). + +## Objects and lifetime + +An interface wrapper holds one strong reference and releases it exactly +once: from `close()`, the `with` statement, or the `__del__` backstop. +`close()` is idempotent, and a closed wrapper raises +`Error(-1, "... used after close()")`. Each call lends the pointer for its +duration, so `close()` from another thread while a call is in flight defers +the release until the call returns: + +```python + def describe(self) -> str: + """Render the gadget as a human-readable string""" + _err = _ErrorStruct() + _out_len = ctypes.c_size_t() + _self_p = self._acquire() + try: + _ret = _c_kitchen_Gadget_describe(_self_p, ctypes.byref(_out_len), ctypes.byref(_err)) + finally: + self._release() + _check_error(_err) + return _take_str(_ret, _out_len.value) +``` + +A constructor named `new` becomes `__init__`; other constructors are +`@classmethod` factories. Objects returned by the producer, decoded from a +buffer, or passed to a callback are adopted into new wrappers. Writing an +object into a value buffer (a record field or a list element) mints a new +reference only after the whole argument list has encoded, so a failed +encoding leaks nothing. + +## Errors + +Every exception derives from the package's root `Error` (named +`{PascalName}Error` if the API declares its own `Error` type), which +carries `code` and `message`. A module's error domain becomes a subclass +(`KvError`) with one subclass per code (`KeyNotFound`), each also reachable +as `KvError.KeyNotFound` and carrying its stable `CODE`. Payload fields +decode onto the exception as attributes. + +Negative codes are runtime traps with constants on `Error`: +`GENERIC_ERROR_CODE` (-1), `PANIC_ERROR_CODE` (-2), `MARSHAL_ERROR_CODE` +(-3), and `FOREIGN_ERROR_CODE` (-4, a callback implementation raised). A +cancelled async call (-5) raises `asyncio.CancelledError` instead. + +## Async and cancellation + +An async function is an `async def` that must run on an `asyncio` event +loop. The launcher receives a static completion trampoline and an integer +key; the trampoline takes ownership of the result on the producer's +thread and settles the future through `call_soon_threadsafe`. + +For a `cancellable` function, each call owns a native cancel token. +Cancelling the awaiting task (directly, through `asyncio.wait_for`, or by +cancelling a `gather`) cancels the token, waits for the producer's +cancelled completion, and raises `asyncio.CancelledError`: + +```python +async def do_cancellable(input: str) -> str: + """Cancellable async operation""" + _input_b = input.encode("utf-8") + _call, _future = _async_begin() + _token = _cancel_token_create() try: - _impl = _cb_impls[ctx] - _ret = _impl.on_evict(_unpack_Entry(ctypes.string_at(entry_ptr, entry_len) if entry_ptr else b""), EvictionReason(reason)) - return 1 if _ret else 0 - except Exception as _exc: - # Never unwind through the C frame: report the failure and return a - # default; the producer aborts its call with FOREIGN_ERROR_CODE. - _lib.weaveffi_error_set(out_err, -4, str(_exc).encode("utf-8", "replace")) - return 0 - - -def _EvictionListener_vtable_free_trampoline(ctx): - # The producer's last reference is gone; it never touches `ctx` again. - _cb_impls.pop(ctx, None) -``` - -```python -def set_eviction_listener(self, listener: "EvictionListener") -> None: - _fn = _lib.weaveffi_kv_Store_set_eviction_listener - _fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_EvictionListenerVtable), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = None - _listener_ctx = _cb_register(listener) - _err = _WeaveFFIErrorStruct() - _fn(_borrow(self), _listener_ctx, ctypes.byref(_EvictionListener_vtable), ctypes.byref(_err)) - _check_error(_err) + _c_kitchen_do_cancellable(_input_b, len(_input_b), _token, _kitchen_do_cancellable_completion, _call) + except BaseException: + _async_abandon(_call) + _cancel_token_destroy(_token) + raise + return await _async_wait_cancellable(_future, _token) ``` -- **Lifetime.** The implementation stays registered (and therefore - alive) exactly as long as the producer may call it: the entry is - removed when the producer invokes the vtable's `free(ctx)`. A producer - that retains the implementation (a store's eviction listener) keeps it - alive across calls; one that doesn't (the `events` sample's - `route_once`) frees it before the call returns. The same Python object - may be passed more than once; each pass registers a new `ctx`. -- **Argument ownership.** Borrowed strings and buffers are copied before - the method runs (`_unpack_Entry(ctypes.string_at(...))`), so the - implementation may keep them. An object passed to a callback method is - owned by the implementation: the trampoline adopts it with - `_from_ptr`, so the method receives a full wrapper it may store, use - after the callback returns, and must eventually `close()` (or leave to - `__del__`). The `events` sample's `on_attached(bus)` demonstrates this; - the received `EventBus` stays usable independently of the caller's - wrapper. -- **Return values.** A method's return value is converted back to its C - representation (`bool` to `0`/`1`, a plain enum to its discriminant, a - record to a value buffer the producer frees). -- **Exceptions.** An exception escaping a method never unwinds through - the C frame. The trampoline writes `FOREIGN_ERROR_CODE` (-4) with - `str(exc)` as the message into the producer's error slot and returns a - default value; the producer aborts the call that was in progress, and - the original caller sees `WeaveFFIError` with `code == -4`. For a - callable marked `throws`, the domain mapper passes -4 through - unchanged (it's outside the domain), so `except KvError` doesn't catch - it but `except WeaveFFIError` does. The interpreter is never taken - down. -- **Threads.** The producer may call a method from any thread. `ctypes` - acquires the GIL before entering a `CFUNCTYPE` trampoline, so a - callback running on a producer thread is an ordinary Python call, but - it runs on that thread: don't touch an asyncio loop or UI toolkit from - it directly; use `loop.call_soon_threadsafe` or a queue. A blocking - producer call that waits for a callback dispatched on another thread - works only because the calling thread releases the GIL while inside - `ctypes`; a callback that itself blocks on the calling thread will - deadlock. - -## Build instructions - -1. Generate the bindings: - - ```bash - weaveffi generate weaveffi.yaml -o generated --target python - ``` - -2. Build the Rust shared library: - - ```bash - cargo build --release -p your_library - ``` - -3. Install the package (editable install for development): - - ```bash - cd generated/python - pip install -e . - ``` - -4. Make the shared library findable at runtime. `WEAVEFFI_LIBRARY` may - point at an exact file; otherwise the loader asks the dynamic linker - for `libweaveffi.dylib` / `libweaveffi.so` / `weaveffi.dll`: - - - Any OS: `export WEAVEFFI_LIBRARY=$PWD/../../target/release/libweaveffi.dylib` - - macOS: `export DYLD_LIBRARY_PATH=$PWD/../../target/release` - - Linux: `export LD_LIBRARY_PATH=$PWD/../../target/release` - - Windows: place `weaveffi.dll` next to your script or add its - directory to `PATH`. - -5. Use the bindings: - - ```python - import asyncio - from kvstore import EntryKind, Store - - with Store.open("/tmp/cache.kv") as store: - store.put("alpha", b"\x01", EntryKind.Persistent, None) - print(store.count(), Store.default_capacity()) - for key in store.list_keys(None): - print(key) - reclaimed = asyncio.run(store.compact()) - ``` +Cancelling the task awaiting a function that isn't `cancellable` stops the +wait only; the native call runs to completion and its result is dropped. -## Packaging - -`weaveffi package --target python` assembles one wheel source tree per -supplied desktop binary under `python//`, each containing -the generated package, the native library copied next to `weaveffi.py`, -and a `setup.py` that ships it as package data and forces a -platform-tagged (non-pure) wheel. The packaged loader still honours -`WEAVEFFI_LIBRARY` first, then opens the bundled library if it exists, -then falls back to the system path. Only platforms with a wheel -platform tag are emitted: - -| Platform | Wheel tag | -|----------|-----------| -| `macos-arm64` | `macosx_11_0_arm64` | -| `macos-x64` | `macosx_10_12_x86_64` | -| `linux-x64` | `manylinux2014_x86_64` | -| `linux-arm64` | `manylinux2014_aarch64` | -| `windows-x64` | `win_amd64` | - -Android and `wasm32` binaries are skipped. Build each tree with -`python -m build --wheel` and tag the result with the platform tag -listed in its `README.md`. See [Packaging](../guides/packaging.md) for the -shared workflow. - -## Memory and ownership - -- **Strings in:** Python `str` is encoded to UTF-8 by `_string_to_bytes` - before crossing the boundary. ctypes manages the lifetime of the - temporary buffer. -- **Strings out:** owned `const char*` returns come back as raw - addresses; `_take_string` copies the text and immediately calls - `weaveffi_free_string` on the producer's buffer. `_bytes_to_string` - handles borrowed strings, such as callback-method parameters, which - the wrapper must not free. -- **Bytes:** copied in via a ctypes array, copied out via - `ctypes.string_at`; the wrapper then releases the producer's buffer - with `weaveffi_free_bytes`. -- **Buffered values out (structs, rich enums, optionals, lists, maps):** - the wrapper decodes the returned value buffer into plain Python values - (dataclasses, `dict`s, `list`s, `None`) with `_take_buffer`, which - copies then releases the one buffer with `weaveffi_free_bytes`. - Nothing is freed per element; object tokens are adopted into wrappers. -- **Buffered values in:** the wrapper packs the Python value with the - generated codec into a `bytes` object it owns for the duration of the - call; the producer never frees it. Object tokens inside it are fresh - strong references the producer owns. -- **Interfaces:** one strong reference per wrapper, released by `close()` - (or the `with` statement) with `__del__` as the backstop. -- **Callback implementations:** pinned in `_cb_impls` until the producer - calls the vtable's `free`. - -## Async support - -Async IDL functions (`async: true`) are exposed as `async def` wrappers -that integrate directly with asyncio; no worker thread blocks waiting -for the result. The wrapper creates a future on the running loop, builds -a `ctypes.CFUNCTYPE` completion callback, calls the `_async`-suffixed C -launcher (which returns immediately), and awaits the future. From the -`kvstore` sample's `Store.compact`: +## Callbacks -```python -async def compact(self) -> int: - _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 -``` +A callback interface is an `abc.ABC` with one abstract method per IDL +method. Passing an instance registers it in a handle table under an integer +key, which the producer receives as `ctx` along with the address of the +interface's one static vtable. The producer's `free(ctx)` removes the +entry, so an implementation lives exactly as long as the producer holds it. -The completion callback fires exactly once, on an arbitrary producer -thread. Result buffers passed to it (strings, bytes, and the serialized -value buffers of buffered results) are owned by the consumer, so the -wrapper copies or decodes them inside the callback and releases them -with `weaveffi_free_string` or `weaveffi_free_bytes`. A reported error -is heap-boxed and released with `weaveffi_error_free` after its fields -are copied. An object result is adopted into a wrapper with `_from_ptr` -inside the callback. Conversion happens on the producer thread; the -wrapper then hops back to the event loop with -`loop.call_soon_threadsafe` to resolve the future, since asyncio -futures must not be touched from foreign threads. - -When the callable is marked `throws: true`, an error reported through -the callback is mapped through the domain mapper (here `_kv_error_from`) -and set as the future's exception, so `await` raises the typed error. -For a non-throwing callable a non-zero code can only be a producer bug -or a raising callback; the wrapper raises the generic `WeaveFFIError` -rather than swallowing it. A panic inside the spawned future surfaces -as `PANIC_ERROR_CODE` (-2). - -Each callback trampoline is pinned in the module-level `_async_pending` -dict until completion, so the GC cannot collect an object the producer -still holds, even if the awaiting coroutine is cancelled. A cancelled -future is never resolved, but the native operation itself keeps -running. - -For functions marked `cancellable: true` the C launcher takes an extra -cancel-token parameter; the Python wrapper always passes `None` (NULL) -for it, as in the `compact` example above. The token is not exposed, so -cancelling the awaiting asyncio task does not stop the native operation. -Cancellation tokens are currently surfaced only by the C and C++ -targets. +Trampolines run on whichever thread the producer calls from; `ctypes` +acquires the GIL first. An exception raised by an implementation is +reported through `{prefix}_error_set(out_err, -4, message)` and never +unwinds through C; the producer's call then fails with +`FOREIGN_ERROR_CODE`. A producer method declared to return +`Result` receives the failure as an `Err` value. ## Iterators -Functions returning `iter` receive an opaque iterator handle from the -C ABI and wrap it in a generated lazy iterator class. The wrapper -returns immediately; nothing is drained, and each consumer step issues -exactly one producer `next` call. The signature is annotated -`Iterator[T]`, so `for` loops, `list(...)`, and `next(...)` all work. -From the `kvstore` sample's `Store.list_keys`: +An `iter` return is a lazy iterator: each `next()` makes one native +`next` call through the pre-bound function, and the handle is released on +exhaustion, `close()`, or garbage collection. ```python -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 __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 __del__(self): - self._destroy() +for key in store.list_keys(None): + print(key) ``` -The native handle is destroyed exactly once: eagerly on exhaustion, via -`close()` when iteration is abandoned early (a second `close()` is a -no-op), or from `__del__` as a garbage-collection backstop. `_destroy` -nulls the stored pointer, so a double destroy is impossible. - -Each yielded element is owned by the consumer: strings are copied and -freed with `_take_string`, buffered elements are decoded and freed with -`_take_buffer`, and an object element is adopted into a wrapper with -`_from_ptr` (or `None` for a NULL `Interface?` element). +## Packaging -Errors from the launcher and from each `next` follow the function's -error strategy. A throwing iterator such as `list_keys` checks each step -with `_check_kv_error` and raises the typed domain error (`KeyNotFound`, -`IoError`, ...) from the step that failed; a non-throwing iterator -raises the generic `WeaveFFIError` only for producer bugs. +`weaveffi package --target python` writes one wheel source tree per +desktop platform under `python//`, with the producer library +bundled inside the import package and a `setup.py` that forces a +platform-tagged wheel. Build each tree with `python -m build --wheel` and +tag it with the platform tag listed in its `README.md`. Android and +`wasm32` binaries have no wheel platform and are skipped. ## Known limitations -- `ctypes` dispatch is slower than a compiled extension; hot loops - crossing the boundary per element will feel it. -- Async cancellation doesn't propagate: cancelling the awaiting task - leaves the native operation running, and `cancellable: true` tokens - are not exposed. -- Callback methods run on whatever thread the producer uses, under the - GIL; long-running callbacks stall the producer thread, and a callback - that blocks waiting for the calling thread deadlocks. -- Interface wrappers without a `new` constructor can't be instantiated - from Python; use the generated factories. -- The plain `generate` output relies on the dynamic linker (or - `WEAVEFFI_LIBRARY`) to find the library; only `weaveffi package` - bundles it. - -## Troubleshooting - -- **`OSError: cannot find ...`**: the loader could not locate the shared - library. Set `WEAVEFFI_LIBRARY`, `DYLD_LIBRARY_PATH` / - `LD_LIBRARY_PATH`, or copy the library next to your script. -- **`ImportError: WeaveFFI ABI mismatch`**: the library was built by a - different `weaveffi` release than the bindings. Regenerate the bindings - and rebuild the library together. -- **`WeaveFFIError: (-1) Store used after close()`**: a closed wrapper - was passed to the producer (as the receiver, a parameter, or a field - of a record or list). Keep the wrapper open for as long as it's in - use. -- **`WeaveFFIError: (-4) ...`**: a callback-interface method you - implemented raised; the message is `str(exc)`. Catch the exception - inside the method if the producer call should succeed anyway. -- **`WeaveFFIError: ...` with a positive code from a non-throwing - call**: the Rust side returned a domain error the IDL didn't declare - as `throws`; inspect `.code` / `.message`. -- **`TypeError: Store cannot be instantiated directly`**: the interface - has no `new` constructor; call its `@classmethod` factory instead. -- **`RuntimeError: no running event loop`**: `async def` wrappers need a - running loop; call them with `asyncio.run(...)` or from inside a - coroutine. +- `ctypes` dispatch costs more per call than a compiled extension; hot + loops that cross the boundary per element will feel it. +- All modules share one namespace (see "Type mapping"). +- A long-running callback stalls the producer thread that called it, and a + callback that waits for the thread blocked in the calling Python code + deadlocks. +- The stubs type `iter` returns as `Iterator[T]`, so a type checker + doesn't know about their `close()` method. diff --git a/docs/src/generators/ruby.md b/docs/src/generators/ruby.md index 7113522b..44835608 100644 --- a/docs/src/generators/ruby.md +++ b/docs/src/generators/ruby.md @@ -1,887 +1,230 @@ # Ruby -## Overview - -The Ruby target produces pure-Ruby FFI bindings using the -[ffi](https://github.com/ffi/ffi) gem to call the C ABI (revision 2) -directly. There's no native extension to compile; `gem install ffi` is -the only prerequisite. The generator emits a single `.rb` file plus a -`gemspec` ready for `gem build` and `gem install`. - -Interfaces become reference-counted wrapper classes (`close` plus a GC -finalizer backstop, `dup`/`clone` for a second reference), records and -rich enums become value classes packed into value buffers (with -interfaces inside them carried as object tokens), callback interfaces -become duck-typed Ruby modules backed by one static vtable of pinned -`FFI::Function` trampolines per interface, async functions block on a -`Queue`, and `iter` returns are lazy `Enumerator`s. - -The trade-off is that FFI gem calls are slower than a hand-written C -extension. For typical FFI workloads the overhead is negligible compared -to the work done inside the Rust library. +The Ruby target generates a pure-Ruby gem that binds the C ABI (revision 3) +through the [ffi](https://github.com/ffi/ffi) gem. There's no native +extension to compile: the gem depends on `ffi ~> 1.15` and loads the +producer's shared library at `require` time. Ruby 2.7 or newer is required. ## What gets generated -| File | Purpose | -|------|---------| -| `ruby/lib/weaveffi.rb` | FFI bindings: library loader, ABI check, `attach_function` declarations, codecs, wrapper classes | -| `ruby/weaveffi.gemspec` | Gem specification with `ffi ~> 1.15` dependency | -| `ruby/README.md` | Prerequisites and usage instructions | +For a package named `kvstore` (prefix `kvstore`, library `kvstore`): -The file names follow the gem name (IDL `package.name`): a package named -`kvstore` produces `lib/kvstore.rb`, `kvstore.gemspec`, and -`module Kvstore`; `weaveffi` is the default. The module verifies the -producer's ABI revision at `require` time: - -```ruby - ABI_VERSION = 2 - 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 -``` - -## Type mapping - -| IDL type | Ruby type | FFI type | -|--------------|--------------------|--------------------------------| -| `i8`, `i16`, `i32`, `i64` | `Integer` | `:int8`, `:int16`, `:int32`, `:int64` | -| `u8`, `u16`, `u32`, `u64` | `Integer` | `:uint8`, `:uint16`, `:uint32`, `:uint64` | -| `f32`, `f64` | `Float` | `:float`, `:double` | -| `bool` | `true`/`false` | `:int32` (0/1 conversion) | -| `string` | `String` | `:string` (param) / `:pointer` (return) | -| `bytes` | `String` (binary) | `:pointer` + `:size_t` | -| `Struct` | `StructName` (plain class) | value buffer (`:pointer` + `:size_t`) | -| `Enum` (plain) | `Integer` (constants in a module) | `:int32` | -| `Enum` (rich) | `EnumName` (nested variant classes) | value buffer | -| `Interface` | `InterfaceName` (wrapper class) | `:pointer` | -| `Interface?` | `InterfaceName` or `nil` | `:pointer` (NULL for `nil`) | -| `CallbackInterface` | any object responding to the methods (include the module) | `:pointer` ctx + vtable `:pointer` | -| `T?` | `T` or `nil` | value buffer | -| `[T]` | `Array` | value buffer | -| `{K: V}` | `Hash` | value buffer | -| `iter` | `Enumerator` (lazy) | `:pointer` iterator handle | - -Buffered types cross the boundary serialized in the -[value-buffer format](../reference/value-buffers.md); the module carries -private `WvBufferWriter`/`WvBufferReader` classes plus one -`_wv_write_*`/`_wv_read_*` pair per record and rich enum. Objects -nested inside a buffered value travel as object tokens (see -[Objects](#objects-interfaces)). Booleans cross as `:int32` (`0`/`1`); -the wrapper converts both directions. - -### 64-bit integers and floats - -Ruby's `Integer` is arbitrary precision, so `i64` and `u64` round-trip -exactly through the `:int64`/`:uint64` FFI types and the value-buffer -codec's `write_i64`/`write_u64` (`String#pack` with `q<`/`Q<`). -`f32`/`f64` map to `Float`; the `codec` conformance consumer verifies -NaN, both infinities, and `-0.0` survive a round trip bit-for-bit. - -## Example IDL and generated code - -```yaml -version: "0.9.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: email, type: "string?" } - - { name: contact_type, type: ContactType } - - functions: - - name: create_contact - params: - - { name: first_name, type: string } - - { name: email, type: "string?" } - - { name: contact_type, type: ContactType } - return: Contact - - - name: list_contacts - params: [] - return: "[Contact]" -``` - -The generated module extends `FFI::Library` and selects the right shared -library at load time: - -```ruby -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/ then ffi_lib 'libweaveffi.dylib' - when /mswin|mingw/ then ffi_lib 'weaveffi.dll' - else ffi_lib 'libweaveffi.so' - end - end -end -``` - -Enums become Ruby modules with constants: - -```ruby -module ContactType - PERSONAL = 0 - WORK = 1 - OTHER = 2 -end -``` - -Structs become plain Ruby value classes: one `attr_reader` per field, a -keyword-argument initializer, and structural equality. They hold no -native pointer and declare no C symbols: - -```ruby -# A contact record -class Contact - attr_reader :id - attr_reader :first_name - attr_reader :email - attr_reader :contact_type - - def initialize(id:, first_name:, email:, contact_type:) - @id = id - @first_name = first_name - @email = email - @contact_type = contact_type - end - - # Structural equality over every field. - def ==(other) - # ... - end -end -``` - -Functions are snake_case class methods on the module, with the IDL -module prefix stripped by default (a `kv.open_store` function surfaces -as `open_store`, not `kv_open_store`; the `attach_function` bindings -keep the full C symbol names). Set `strip_module_prefix: false` in the -Ruby generator config (or under `[global]`) to keep prefixed names. A -buffered return is copied out, freed, and decoded; from the `kvstore` -sample's cross-module `get_stats`: - -```ruby - 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 -``` - -## Typed errors - -The shared error machinery: an `ErrorStruct` mirroring the C error -slot, the four runtime trap codes as module constants, and a generic -`Error`: - -```ruby - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - # The runtime-reserved error codes. Positive codes belong to a module's - # declared error domain; these negative codes are programming errors the - # wrappers raise as a plain Error regardless of the function's `throws`. - GENERIC_ERROR_CODE = -1 - PANIC_ERROR_CODE = -2 - MARSHAL_ERROR_CODE = -3 - # A callback-interface implementation raised; the message carries the - # exception text. - FOREIGN_ERROR_CODE = -4 - - class Error < StandardError - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end +```text +ruby/ + kvstore.gemspec gem `kvstore`, depends on ffi ~> 1.15 + README.md + lib/ + kvstore.rb require 'kvstore': module Kvstore, the bindings + kvstore/ + runtime.rb loader, ABI check, errors, codec, object base ``` -A module's error domain adds a base class extending `Error` with one -nested class per code, each pinning its stable `CODE`, plus a mapper -that falls back to the generic `Error` for codes outside the domain. -From the `kvstore` sample: - -```ruby -# Base error for the `kv` module's error domain. -class KvError < Error - # key not found - class KeyNotFound < KvError - CODE = 1001 - - def initialize(message = 'key not found') - super(1001, message) - end - end +Names come from the package identity: - # Expired, StoreFull, IoError follow the same shape. -end +| Name | Default | Override (`[generators.ruby]`) | +|------|---------|--------------------------------| +| Gem | the package name (`async-demo`) | `gem_name` | +| Ruby module | the package name in PascalCase (`AsyncDemo`) | `module_name` | +| `require` path | the C prefix (`async_demo`) | none | -# Builds the KvError subclass matching `code`, 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 -``` +`strip_module_prefix` (default `true`) controls whether a function in +module `contacts` is spelled `create_contact` or `contacts_create_contact`. +Every IDL module, nested ones included, lands in the one Ruby module. -Only callables marked `throws: true` in the IDL raise the typed -classes: their wrappers call `check_kv_error!`, so you can rescue -`Kvstore::KvError::KeyNotFound` for one code or `Kvstore::KvError` for -the whole domain. A callable without `throws` uses the generic -`check_error!`, which raises `Error` if the producer misbehaves. Both -copy the message (and payload) out and release the slot with -`weaveffi_error_clear` before raising. +`lib/{prefix}/runtime.rb` is fixed code shipped with the generator (the +source lives in `targets/ruby/runtime/runtime.rb`); only the module name, +the C prefix, and the library names are substituted into it. -An error code that declares payload `fields:` carries them serialized in -the error's payload buffer (`payload_ptr`/`payload_len` on -`ErrorStruct`); the domain factory decodes them into attributes on the -raised error object before the buffer is released. +## Install, build, and load -```ruby -begin - store.delete('missing') -rescue Kvstore::KvError::KeyNotFound - # specific code -rescue Kvstore::KvError => e - puts "kv error #{e.code}: #{e.message}" -end +```bash +weaveffi generate samples/kvstore/src/lib.rs -o generated --target ruby +cargo build --release -p kvstore +cd generated/ruby +gem build kvstore.gemspec +gem install kvstore-1.0.0.gem ``` -### Runtime error codes - -| Code | Constant | Meaning | Where it surfaces | -|------|----------|---------|-------------------| -| `-1` | `GENERIC_ERROR_CODE` | The producer reported an error without a declared code, or the wrapper itself detected misuse (a NULL object pointer, a wrapper used after `close`). | Raised as `Error`. | -| `-2` | `PANIC_ERROR_CODE` | The Rust implementation panicked; the export macros and the async spawner catch the unwind. | Raised as `Error` (a blocking async call re-raises it on the caller's thread). | -| `-3` | `MARSHAL_ERROR_CODE` | Malformed input at the boundary (invalid UTF-8, a truncated value buffer, a bad enum discriminant). | Raised as `Error`. | -| `-4` | `FOREIGN_ERROR_CODE` | A callback-interface method implemented in Ruby raised. | Raised as `Error` from the producer call that invoked the callback (see [Callback interfaces](#callback-interfaces)). | +The loader opens the library in this order: -There's no non-raising call path in Ruby: a non-throwing callable whose -error slot comes back non-zero still raises `Error`. +1. the path in `{PREFIX}_LIBRARY` (`KVSTORE_LIBRARY=/path/to/libkvstore.dylib`), +2. a copy bundled at `lib/native/` (platform gems from `weaveffi package`), +3. `libkvstore.dylib`, `libkvstore.so`, or `kvstore.dll` on the system + search path. -## Objects (interfaces) - -An `interfaces:` entry becomes a class holding an `FFI::AutoPointer` -subclass that owns one strong reference to a reference-counted producer -object; the `AutoPointer`'s `release` hook calls the `_destroy` symbol -when Ruby garbage-collects it. From the `kvstore` sample (trimmed): +At `require` time the bindings then check that the library reports ABI +revision 3 and that every top-level module's contract checksum matches +the one the bindings were generated from. A mismatch raises `LoadError` +naming the module: ```ruby - # Owns one strong reference to a Store; releases it exactly once. - class StorePtr < FFI::AutoPointer - def self.release(ptr) - Kvstore.weaveffi_kv_Store_destroy(ptr) - end - end - - class Store - # Adopts one strong reference 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 - - # The borrowed object pointer passed to producer calls. - def handle - raise Error.new(-1, 'Store used after close') if @handle.nil? - @handle - end - - # Whether close has released this wrapper's reference. - def closed? - @handle.nil? - end - - # Releases this wrapper's reference now rather than at GC time. - # Idempotent; the object itself is dropped when the last reference - # anywhere (another wrapper, a record field, the producer) goes. - def close - return if @handle.nil? - @handle.free - @handle = nil - end - - # Mints a new strong reference the caller owns (used when this - # object is written into a value buffer). - def _wv_clone_ptr - Kvstore.weaveffi_kv_Store_clone(handle) - end - - # dup and clone produce an independent wrapper with its own reference. - def initialize_copy(other) - super - @handle = StorePtr.new(Kvstore.weaveffi_kv_Store_clone(other.handle)) - end - - def self.open(path) - err = ErrorStruct.new - result = Kvstore.weaveffi_kv_Store_open(path, err) - Kvstore.check_kv_error!(err) - raise Error.new(-1, 'null pointer') if result.null? - _from_ptr(result) - end - end +module KitchenSink + # Contract checksums of the top-level modules these bindings were + # generated from; a library built from a different API fails to load. + _wv_check_contract!( + 'shared' => [:kitchen_sink_shared_checksum, 0x42c4ce2c8d0af052], + 'kitchen' => [:kitchen_sink_kitchen_checksum, 0xd83da75b24b545ff], + ) ``` -- **Construction.** A constructor named `new` becomes an ordinary - `initialize`, so `EventBus.new` works as usual (the `events` sample); - any other constructor is a class method (`Store.open(path)`). Methods - are snake_case instance methods, statics are class methods - (`Store.default_capacity`), and deprecated members print a - `[DEPRECATED]` warning at call time. -- **Disposal.** `close` releases this wrapper's reference through the - `_destroy` symbol now; it's idempotent, and `closed?` reports the - state. If you never call `close`, the `FFI::AutoPointer` releases the - reference when the wrapper is garbage-collected. The producer object - itself is dropped only when the last reference anywhere is released. -- **Use after close.** Every call goes through `handle`, which raises - `Error.new(-1, 'Store used after close')` on a closed wrapper, whether - it's the receiver, a parameter, or a field of a record being packed. -- **Copies mint new references.** `dup` and `clone` are overridden via - `initialize_copy` to mint a new strong reference, so the copy is an - independent wrapper; closing one never affects the other. Methods that - return the receiver or an existing object (`share`, `fork`) likewise - return a fresh reference adopted into a new wrapper. +`weaveffi package --target ruby` writes one platform gem tree per desktop +binary under `ruby//`, with the library at `lib/native/` and +`s.platform` set (`arm64-darwin`, `x86_64-darwin`, `x86_64-linux`, +`aarch64-linux`, `x64-mingw-ucrt`). Android and `wasm32` binaries are +skipped. -```ruby -store = Kvstore::Store.open('/tmp/cache.kv') -begin - store.put('alpha', "\x01".b, Kvstore::EntryKind::PERSISTENT, nil) - puts "#{store.count} / #{Kvstore::Store.default_capacity}" - reclaimed = store.compact -ensure - store.close -end -``` - -### Nullable objects, and objects inside values - -An `Interface?` parameter passes `nil` as NULL (`other&.handle`), and an -`Interface?` return maps a NULL pointer to `nil`: - -```ruby - def larger(other) - err = ErrorStruct.new - result = Kvstore.weaveffi_kv_Store_larger(handle, other&.handle, err) - Kvstore.check_error!(err) - return nil if result.null? - Store._from_ptr(result) - end -``` - -Objects inside records, arrays, hashes, and optionals travel as 8-byte -object tokens in the value buffer. Writing a token mints a new strong -reference with `_wv_clone_ptr`; reading one adopts the reference into a -fresh wrapper. From the `StoreInfo` record (`store: Store`, -`mirror: Store?`): - -```ruby - def self._wv_write_store_info(w, v) - w.write_string(v.label) - w.write_u64(v.store._wv_clone_ptr.address) - if v.mirror.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_u64(v.mirror._wv_clone_ptr.address) - end - w.write_i64(v.count) - end - - def self._wv_read_store_info(r) - _wv_label = r.read_string - _wv_store = Store._from_ptr(r.read_object_token) - if r.read_flag - _wv_mirror = Store._from_ptr(r.read_object_token) - else - _wv_mirror = nil - end - _wv_count = r.read_i64 - StoreInfo.new(label: _wv_label, store: _wv_store, mirror: _wv_mirror, count: _wv_count) - end -``` - -Arrays of objects work the same way in both directions -(`Store.open_many(paths)` returns an `Array` of `Store`, -`Store.total_count(stores, extra)` takes one); each wrapper in a returned -array owns its own reference and should be closed (or left to GC) -individually. Iterators over objects adopt one reference per pull, and a -blocking async call returning an object adopts the pointer inside the -completion callback. - -## Rich (algebraic) enums - -A rich (algebraic) enum is a sum type whose variants carry associated -data. A plain C-style `Enum` crosses as a bare `:int32` discriminant; a -rich enum instead becomes a plain class hierarchy: a base class with a -`tag` reader plus one nested subclass per variant carrying that -variant's fields as attributes, each pinning its `TAG` constant. Rich -enums declare no C symbols; values cross the ABI serialized in value -buffers as an `i32` tag followed by the active variant's fields. From -the `codec` sample: - -```ruby - class Shape - # The active variant's integer tag. - def tag - self.class::TAG - end - - # No payload. - class Empty < Shape - TAG = 0 - - # Structural equality over the variant and its fields. - def ==(other) - return false unless other.is_a?(Empty) - true - end - end - - # One `f64`. - class Circle < Shape - TAG = 1 - - # Radius. - attr_reader :radius - - def initialize(radius:) - @radius = radius - end - - # Structural equality over the variant and its fields. - def ==(other) - return false unless other.is_a?(Circle) - # ... - end - end - end -``` - -Construct variants directly and branch with `case`/`when` on the class: - -```ruby -circle = Codec::Shape::Circle.new(radius: 2.0) - -case circle -when Codec::Shape::Circle - puts circle.radius # 2.0 -when Codec::Shape::Empty - puts 'empty' -end -``` +## Type mapping -Values are plain Ruby data; there's no native handle and nothing to -close. The private `_wv_write_shape`/`_wv_read_shape` codec helpers -write and read the tagged wire format; variant fields of interface type -follow the object token rules above. +| IDL type | Ruby type | C slots | +|----------|-----------|---------| +| `i8` to `i64`, `u8` to `u64` | `Integer` | `:int8` to `:uint64` | +| `f32`, `f64` | `Float` | `:float`, `:double` | +| `bool` | `true` or `false` | `:bool` | +| `string` | `String` (UTF-8) | `:pointer` + `:size_t` | +| `bytes` | `String` (binary) | `:pointer` + `:size_t` | +| C-style enum | `Integer` constants in a module | `:int32` | +| record | plain value class with keyword `initialize` and `==` | value buffer | +| rich enum | base class with one nested class per variant | value buffer | +| `T?` | `T` or `nil` | value buffer | +| `[T]`, `{K: V}` | `Array`, `Hash` | value buffer | +| interface | wrapper class | `:pointer` | +| `Interface?` | wrapper or `nil` | `:pointer` (NULL for `nil`) | +| callback interface | any object with the methods | context + vtable | +| `iter` | lazy `Enumerator` | iterator handle | + +Strings cross as a pointer plus a length, so interior NUL characters +survive. Returned strings are copied, tagged UTF-8, and released with +`{prefix}_free_bytes`. Value buffers use the +[value-buffer format](../reference/value-buffers.md), encoded and decoded +by the runtime's `WvBufferWriter` and `WvBufferReader` and one +`_wv_write_*`/`_wv_read_*` pair per record and rich enum. + +## Objects and lifetime + +Each interface becomes a subclass of the runtime's `WvObject`, holding +one strong reference in an `FFI::AutoPointer`: + +```ruby + # A pocket-sized interface exercising the object surface + class Gadget < WvObject + WV_PTR = GadgetPtr + + # @api private + def self._wv_clone(ptr) + KitchenSink.kitchen_sink_kitchen_Gadget_clone(ptr) + end + + # Create a gadget with the given id + def initialize(id) +``` + +- `close` releases the reference now and is idempotent; otherwise the GC + finalizer releases it. The object itself lives until its last reference + anywhere (another wrapper, a record field, the producer) goes. +- `dup` and `clone` make an independent wrapper with its own reference. +- A constructor named `new` becomes `initialize`; other constructors are + class methods. An interface without one hides `new`. +- Every call pins its receiver and object arguments (`_wv_pin`), so a + `close` from another thread or from a callback during the call takes + effect only when the call returns. +- An object inside a value buffer carries its own reference, minted just + before the call; an object decoded from one is adopted into a new + wrapper. +- Using a closed wrapper raises `Error` ("KitchenSink::Gadget used after close"). + +## Errors + +Every error derives from `{Module}::Error`, which carries `code`. A module +with an error domain gets a domain class (`KvError < Error`) with one +nested class per code (`KvError::KeyNotFound`, carrying `CODE` and any +payload fields as attributes). Functions that declare errors raise the +domain class; runtime failures raise the base classes: + +| Code | Constant | Raised as | +|------|----------|-----------| +| -1 | `GENERIC_ERROR_CODE` | `Error` | +| -2 | `PANIC_ERROR_CODE` | `Error` (the producer panicked) | +| -3 | `MARSHAL_ERROR_CODE` | `Error` (a malformed argument or buffer) | +| -4 | `FOREIGN_ERROR_CODE` | `Error` (a callback implementation raised) | +| -5 | `CANCELLED_ERROR_CODE` | `Cancelled` | + +## Async and cancellation + +An async function blocks the calling thread until the producer's +completion fires, then returns the result or raises. The wait is a +`Queue#pop`, which releases the GVL, so other threads keep running; run +the call in a `Thread` for concurrency. A cancellable function takes a +`cancel:` keyword: + +```ruby + # Cancellable async operation + # Blocks until the call completes on a producer thread. + # @param cancel [CancelToken, nil] cancels the call; it then raises Cancelled + def self.do_cancellable(input, cancel: nil) + input_s = _wv_str(input) + own_token = CancelToken.new if cancel.nil? + token = cancel || own_token + ctx, queue = _wv_async_begin + kitchen_sink_kitchen_do_cancellable(input_s, input_s.bytesize, token._wv_ptr, KITCHEN_SINK_KITCHEN_DO_CANCELLABLE_CALLBACK, ctx) + _wv_async_wait(queue, token) + ensure + own_token&.close + end +``` + +`CancelToken#cancel` may be called from any thread; the call then raises +`Cancelled` unless it already completed. A token stays cancelled, can be +shared by several calls, and is released by `close` or the GC. Without a +token, interrupting the waiting thread (`Thread#raise`, `Timeout`) still +cancels the producer's work through a private token. Each async function +has one completion trampoline constant, so nothing the producer may still +call is ever garbage-collected. ## Callback interfaces -A `callback_interfaces:` entry becomes a Ruby module. Any object that -responds to the methods is accepted (duck typing); including the module -gives you `NotImplementedError` defaults for the methods you don't -override. From the `kvstore` sample: +A callback interface becomes a Ruby module whose methods raise +`NotImplementedError`. Any object that responds to the methods works; +including the module documents intent and supplies the defaults. ```ruby - # Consumer-implemented callback interface. Any object responding to the - # methods below is accepted wherever a EvictionListener parameter is expected; - # include this module to inherit NotImplementedError defaults. The - # producer may call the methods from any thread until it releases the - # implementation. - module EvictionListener - # An entry left the store. Returns whether the listener wants to keep - # receiving notifications; `false` detaches it. - # @return [Object] a bool - def on_evict(entry, reason) - raise NotImplementedError, "#{self.class}#on_evict is not implemented" - end - end -``` +class Recorder + include Events::Subscriber -```ruby -class Auditor - include Kvstore::EvictionListener - - def on_evict(entry, reason) - puts "#{entry.key}: #{reason}" - true + def route(topic) + Events::Delivery::ACCEPT end end - -store.set_eviction_listener(Auditor.new) ``` -Behind the module is one process-wide vtable per callback interface, -whose entries are `FFI::Function` trampolines held in constants so they -live for the process lifetime. Passing an implementation registers it in -a mutex-guarded registry under an integer key; the key (as an -`FFI::Pointer`) crosses as `ctx`, so the producer never holds a Ruby -object: - -```ruby - # Trampoline for EvictionListener#on_evict. - WV_EVICTION_LISTENER_ON_EVICT = FFI::Function.new(:int32, [:pointer, :pointer, :size_t, :int32, :pointer]) do |ctx, entry_ptr, entry_len, reason, out_err| - begin - impl = _wv_cb_lookup(ctx) - entry_r = WvBufferReader.new(entry_ptr.null? ? ''.b : entry_ptr.read_string(entry_len)) - entry_v = _wv_read_entry(entry_r) - entry_r.expect_end! - reason_v = reason - impl.on_evict(entry_v, reason_v) ? 1 : 0 - rescue Exception => e - _wv_cb_fail(out_err, e) - 0 - end - end - - # Releases a EvictionListener implementation when the producer drops its last - # reference. - WV_EVICTION_LISTENER_FREE = FFI::Function.new(:void, [:pointer]) do |ctx| - _wv_cb_free(ctx) - end +The implementation is stored in a handle table and the producer receives +its key, so Ruby object addresses never cross the boundary. The entry is +removed when the producer calls the vtable's `free`. The trampolines +copy borrowed strings and buffers, adopt object arguments, and report any +exception (including `NotImplementedError`) through +`{prefix}_error_set(out_err, -4, message)`; the caller sees +`Error` with code `-4` and the exception's message. - WV_EVICTION_LISTENER_VTABLE = WvEvictionListenerVtable.new - WV_EVICTION_LISTENER_VTABLE[:on_evict] = WV_EVICTION_LISTENER_ON_EVICT - WV_EVICTION_LISTENER_VTABLE[:free] = WV_EVICTION_LISTENER_FREE -``` - -```ruby - def set_eviction_listener(listener) - err = ErrorStruct.new - listener_ctx = Kvstore._wv_cb_register(listener) - Kvstore.weaveffi_kv_Store_set_eviction_listener(handle, listener_ctx, WV_EVICTION_LISTENER_VTABLE.to_ptr, err) - Kvstore.check_error!(err) - end -``` - -- **Lifetime.** The registry entry keeps the implementation alive - exactly as long as the producer may call it; the vtable's `free` - trampoline removes it when the producer drops its last reference. A - producer that retains the implementation (a store's eviction listener) - keeps it alive across calls; one that doesn't (the `events` sample's - `route_once`) frees it before returning. Passing the same object twice - registers two entries. -- **Argument ownership.** Borrowed strings and buffers are copied into - Ruby values before the method runs, so the implementation may keep - them. An object passed to a callback method is owned by the - implementation: the trampoline adopts it into a new wrapper - (`impl.on_attached(EventBus._from_ptr(bus))` in the `events` sample), - and the implementation should `close` it when done (or let GC do it). -- **Return values.** A method's return value is converted back to its C - representation (truthiness to `1`/`0`, an enum constant as its - `:int32`, a record as a value buffer the producer frees). -- **Exceptions.** Any exception escaping a method (including the - `NotImplementedError` default) never unwinds through the C frame. - `_wv_cb_fail` writes `FOREIGN_ERROR_CODE` (-4) with the exception's - message into the producer's error slot, and the trampoline returns a - default; the producer aborts the call in progress, and the original - caller sees `Error` with `code == -4`. For a callable marked `throws`, - the domain mapper falls through to `Error` (so `rescue KvError` - doesn't catch it but `rescue Kvstore::Error` does). The VM is never - taken down. -- **Threads.** The producer may call a method from any thread. The ffi - gem dispatches a callback arriving on a non-Ruby thread onto the VM - by acquiring the GVL first, so the method runs as ordinary Ruby code - on that thread. A blocking producer call that waits for a callback - dispatched on another thread works because the ffi gem releases the - GVL around the C call; a callback that itself blocks waiting on the - calling thread will deadlock. - -## Build instructions - -1. Generate the bindings: - - ```bash - weaveffi generate api.yaml -o generated --target ruby - ``` - -2. Build the Rust shared library: - - ```bash - cargo build --release -p your_library - ``` - -3. Build and install the gem: - - ```bash - cd generated/ruby - gem build weaveffi.gemspec - gem install weaveffi-0.1.0.gem - ``` - -4. Make the cdylib findable at runtime. `WEAVEFFI_LIBRARY` may point at - an exact file; otherwise: - - - macOS: `DYLD_LIBRARY_PATH=$PWD/../../target/release ruby your_script.rb` - - Linux: `LD_LIBRARY_PATH=$PWD/../../target/release ruby your_script.rb` - - Windows: place `weaveffi.dll` next to the script or add its - directory to `PATH`. - -The Ruby module name and gem name can be customised in `weaveffi.toml`: - -```toml -[generators.ruby] -module_name = "MyBindings" -gem_name = "my_bindings" -``` - -## Packaging - -`weaveffi package --target ruby` assembles one precompiled platform gem -tree per supplied desktop binary under `ruby//`, each with -the generated library, the native library at `lib/native/`, and a -gemspec pinned to the RubyGems platform string. The packaged loader -still honours `WEAVEFFI_LIBRARY` first, then opens the bundled library -if it exists, then falls back to the system path. - -| Platform | RubyGems platform | -|----------|-------------------| -| `macos-arm64` | `arm64-darwin` | -| `macos-x64` | `x86_64-darwin` | -| `linux-x64` | `x86_64-linux` | -| `linux-arm64` | `aarch64-linux` | -| `windows-x64` | `x64-mingw-ucrt` | - -Android and `wasm32` binaries have no RubyGems platform and are skipped. -Run `gem build` in each tree to produce the platform gems. See -[Packaging](../guides/packaging.md) for the shared workflow. - -## Memory and ownership - -- **Strings in:** Ruby strings are passed as `:string` parameters and - the FFI gem encodes them to null-terminated C strings. -- **Strings out:** the wrapper reads the returned `:pointer` with - `read_string` (forcing UTF-8), then calls `weaveffi_free_string` to - release the Rust-owned buffer. -- **Bytes:** an `FFI::MemoryPointer` is allocated for inputs; outputs - are copied with `read_string(len)` and the returned buffer is released - with `weaveffi_free_bytes`. -- **Buffered values (structs, rich enums, optionals, arrays, hashes):** - parameters are packed into a binary `String` staged in an - `FFI::MemoryPointer` the producer borrows for the duration of the - call; returns are copied into Ruby memory with `read_string(len)`, - released with `weaveffi_free_bytes`, and decoded with the `_wv_read_*` - helper. Object tokens written into a buffer are fresh strong - references the producer owns; tokens read out are adopted into - wrappers. -- **Interfaces:** one strong reference per wrapper, released by `close` - or by the `FFI::AutoPointer` on GC. -- **Callback implementations:** held in the module's registry until the - producer calls the vtable's `free`. - -## Async support - -Async IDL functions (`async: true`) are exposed as blocking wrapper -methods. The wrapper creates a `Queue`, builds an `FFI::Function` -completion callback that pushes either the converted result or an error -onto it, calls the `_async`-suffixed C launcher, then pops the queue and -raises if the producer reported an error. For a callable marked -`throws: true`, the error goes through the domain mapper -(`kv_error_from` here), so the raised object is the typed class. From -the `kvstore` sample's `Store#compact`: - -```ruby - # 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.force_encoding(Encoding::UTF_8) - payload = err[:payload_ptr].null? ? nil : err[:payload_ptr].read_string(err[:payload_len]) - Kvstore.weaveffi_error_free(err_ptr) - queue << Kvstore.kv_error_from(code, msg, payload) - else - queue << result - end - end - Kvstore.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 -``` - -There is no promise/future type and no `concurrent-ruby` dependency: the -calling thread blocks until the completion callback fires. `Queue#pop` -releases the GVL, and the ffi gem delivers the cross-thread completion -callback safely. Wrap the call in a `Thread` when you need concurrency: - -```ruby -t = Thread.new { store.compact } -reclaimed = t.value # joins; re-raises a Kvstore::Error from the call -``` - -The local `callback` reference keeps the `FFI::Function` alive until -`queue.pop` returns, so the completion callback cannot be collected -mid-flight. - -Result ownership follows the async contract: string, bytes, and buffered -results (records, rich enums, optionals, arrays, and hashes, arriving as -a pointer-plus-length pair) are owned by the consumer, so the callback -copies or decodes them into Ruby values and then releases them with -`weaveffi_free_string` or `weaveffi_free_bytes`. A reported error is -heap-boxed: the callback copies its code, message, and payload, then -releases it with `weaveffi_error_free`. An owned interface result -transfers ownership too: the callback adopts the pointer into a new -wrapper. A non-throwing async callable raises the generic `Error` on a -trap code; a panic inside the spawned future surfaces as -`PANIC_ERROR_CODE` (-2). - -For functions marked `cancellable: true` the C launcher takes an extra -cancel-token parameter. The wrapper always passes `FFI::Pointer::NULL` -for it and doesn't expose the token. Cancellation tokens are currently -surfaced only by the C and C++ targets. +Producer threads may call back at any time: the ffi gem runs the +trampoline on a Ruby thread. In an API with callback interfaces every +call releases the GVL (`blocking: true`), so a producer thread can run a +callback while the call that triggered it is in flight. ## Iterators -Functions returning `iter` return a lazy `Enumerator` that streams -one element per pull: each consumer step issues exactly one call to the -generated `_next` binding, so nothing is drained up front. Call `.to_a` -if you want an eager `Array`. From the `kvstore` sample's `list_keys` -(argument packing elided): - -```ruby - # 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) - # ... pack the optional prefix into prefix_buf ... - Enumerator.new do |y| - err = ErrorStruct.new - iter = Kvstore.weaveffi_kv_Store_list_keys(handle, prefix_buf, prefix_data.bytesize, err) - begin - Kvstore.check_kv_error!(err) - unless iter.null? - loop do - out_item = FFI::MemoryPointer.new(:pointer) - item_err = ErrorStruct.new - has_item = Kvstore.weaveffi_kv_Store_ListKeysIterator_next(iter, out_item, item_err) - Kvstore.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.force_encoding(Encoding::UTF_8) - Kvstore.weaveffi_free_string(item_ptr) - y << item - end - end - end - ensure - Kvstore.weaveffi_kv_Store_ListKeysIterator_destroy(iter) unless iter.null? - end - end - end -``` - -The producer iterator launches on the first pull, so a launch error -raises then, not when the method returns. Each string element is copied -with `read_string` and freed with `weaveffi_free_string`; a buffered -element arrives as a value buffer that's decoded with the `_wv_read_*` -helper and released with `weaveffi_free_bytes`; an object element is -adopted into a new wrapper. The `ensure` block destroys the handle -exactly once, whether iteration exhausts, raises, or is abandoned early -(Ruby runs `ensure` when the enumerator's fiber is torn down, for -example after `break`). Enumerating the same `Enumerator` again -launches a fresh producer iterator. - -The per-step error check follows the function's error strategy: the -throwing `list_keys` checks the launcher and each `next` with -`check_kv_error!`, so a failing step raises the typed `KvError` -subclass; a non-throwing iterator uses the generic `check_error!`, which -raises only on a producer bug. +An `iter` return is a lazy `Enumerator`. The producer iterator starts +on the first pull, each step makes one `next` call, and the handle is +destroyed exactly once, when iteration finishes, raises, or stops early +(`first(2)`, `break`). ## Known limitations -- Async functions block the calling thread; there's no `Fiber`- or - promise-based variant, and `cancellable: true` tokens are not exposed. -- Callback methods run on whatever thread the producer uses, under the - GVL; a callback that blocks waiting for the calling thread deadlocks. -- Callback-interface parameters are duck-typed: a missing method is only - detected when the producer calls it (surfacing as `Error` with - code -4, or `NoMethodError` text if the module wasn't included). -- The plain `generate` output relies on the dynamic loader (or - `WEAVEFFI_LIBRARY`) to find the library; only `weaveffi package` - bundles it. - -## Troubleshooting - -- **`LoadError: Could not open library 'libweaveffi.dylib'`**: the - cdylib is not on the loader path. Set `WEAVEFFI_LIBRARY`, - `DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH`, or copy the library next to - your script. -- **`LoadError: WeaveFFI ABI mismatch`**: the library was built by a - different `weaveffi` release than the bindings. Regenerate the bindings - and rebuild the library together. -- **`FFI::NotFoundError: Function 'weaveffi_*' not found`**: the cdylib - does not export the symbol. Rebuild the Rust crate after regenerating - the IDL. -- **`Error: Store used after close` (code -1)**: a closed wrapper was - used as a receiver, a parameter, or a record field. Keep the wrapper - open for as long as the object is in use; `dup` it if another owner - needs an independent reference. -- **`Error` with `code == -4`**: a callback-interface method you - implemented raised (or wasn't implemented); the message is the - exception's text. -- **Segmentation faults on Ruby exit**: the generated wrappers keep - vtable trampolines in constants and async completion callbacks - referenced until they fire. If you call the `attach_function` bindings - directly, keep your own `FFI::Function` objects alive for the lifetime - of the call; letting them be garbage-collected mid-call corrupts the C - side. -- **Strings come back as binary garbage**: UTF-8 strings should round - trip through `read_string`; for binary data use `read_bytes(length)` - with the `out_len` returned by the C ABI. +- Async calls block the calling thread; there's no Fiber-scheduler or + promise-based surface. +- An `Enumerator` driven externally with `next` and then abandoned + releases its iterator only when the GC collects it. +- Callback interfaces are duck-typed: a missing method is detected only + when the producer calls it. +- A callback that blocks waiting for the thread that made the triggering + call deadlocks. +- Every IDL module shares one Ruby module, so two modules can't both + declare a function or type with the same Ruby name. diff --git a/docs/src/generators/swift.md b/docs/src/generators/swift.md index 9e00c84e..f3e32cfe 100644 --- a/docs/src/generators/swift.md +++ b/docs/src/generators/swift.md @@ -1,193 +1,94 @@ # Swift -## Overview - -The Swift target emits a SwiftPM System Library (`CWeaveFFI`) that -references the generated C header via a `module.modulemap`, plus a thin -Swift module (`WeaveFFI`) that wraps the C ABI in idiomatic Swift: -`throws`-based error handling, plain structs and enums for values, -ARC-managed `final class` wrappers for reference-counted objects, Swift -protocols for callback interfaces, `async` methods for async callables, -and lazy `Sequence`s for iterators. The surface follows ABI revision 2. +The Swift target emits a standalone SwiftPM package that wraps the C ABI in +idiomatic Swift: `throws` for declared errors, plain `Hashable` structs and +enums for values, ARC-managed `final class` wrappers for objects, class-bound +protocols for callback interfaces, `async` functions with task cancellation +for async callables, and lazy `Sequence`s for iterators. ## What gets generated -| File | Purpose | -|------|---------| -| `generated/swift/Package.swift` | SwiftPM manifest declaring `CWeaveFFI` (system library) and `WeaveFFI` (Swift wrapper) | -| `generated/swift/Sources/CWeaveFFI/module.modulemap` | C module map pointing at the generated header | -| `generated/swift/Sources/WeaveFFI/WeaveFFI.swift` | Swift wrapper: enums, structs, object classes, callback protocols, namespaced module functions | - -The module name shown above (`WeaveFFI`) is the default. It is overridden by -`[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 -stays buildable under any name. - -## Type mapping +For a package named `kvstore`: -| IDL type | Swift type | Notes | -|--------------|-----------------------------|----------------------------------| -| `i32` | `Int32` | Direct value | -| `u32` | `UInt32` | Direct value | -| `i64` | `Int64` | Direct value | -| `u64` | `UInt64` | Direct value | -| `i8` | `Int8` | Direct value | -| `i16` | `Int16` | Direct value | -| `u8` | `UInt8` | Direct value | -| `u16` | `UInt16` | Direct value | -| `f32` | `Float` | Direct value | -| `f64` | `Double` | Direct value | -| `bool` | `Bool` | C `bool` at the ABI | -| `string` | `String` | NUL-terminated UTF-8 (`withCString`) | -| `bytes` | `Data` | Pointer + length | -| `StructName` | `StructName` (`struct`) | Plain value type; crosses as a value buffer | -| `InterfaceName` | `InterfaceName` (`final class`) | One strong reference per instance; see [Objects](#objects-interfaces) | -| `InterfaceName?` | `InterfaceName?` | Nullable pointer at the ABI | -| `CallbackName` | `protocol CallbackName` | Implement and pass any conforming value; see [Callback interfaces](#callback-interfaces) | -| `EnumName` (plain) | `EnumName` (`enum`) | Backed by `UInt32` | -| `EnumName` (rich) | `EnumName` (`enum` with associated values) | Crosses as a value buffer | -| `T?` | `T?` | Value buffer | -| `[T]` | `[T]` | Value buffer | -| `{K: V}` | `[K: V]` | Value buffer | -| `iter` | generated `Sequence` class | Lazy; one `_next` call per step | - -64-bit integers are native `Int64`/`UInt64`. `Float` and `Double` cross the -ABI as IEEE values (the value-buffer codec writes `bitPattern`), so NaN, -the infinities, and `-0.0` round-trip unchanged; the `codec` sample's -`Codec.roundtripU64` and `Codec.roundtripF64` exercise this. - -## Example IDL → generated code - -```yaml -version: "0.9.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: name, type: string } - - { name: email, type: "string?" } - - { name: age, type: i32 } - - errors: - name: ContactsError - codes: - - { name: InvalidName, code: 1, message: "name must not be empty" } - - { name: NotFound, code: 2, message: "contact not found" } - - functions: - - name: create_contact - params: - - { name: name, type: string } - - { name: age, type: i32 } - return: Contact - throws: true - - - name: find_contact - params: - - { name: id, type: i32 } - return: "Contact?" - throws: true - - - name: list_contacts - params: [] - return: "[Contact]" - - - name: set_type - params: - - { name: id, type: i32 } - - { name: contact_type, type: ContactType } +```text +swift/ + Package.swift + Sources/ + CKvstore/ + kvstore.h # copy of the C header + module.modulemap # system library: links `kvstore` + Kvstore/ + Kvstore.swift # runtime support and the wrapper API ``` -Enums become Swift enums with lowerCamelCase cases backed by `UInt32`: +The package, product, and Swift module are the package name in PascalCase +(`async-demo` becomes `AsyncDemo`); set `[generators.swift] module_name` to +override it. The C module is always the Swift module's name with a `C` prefix. +Nothing else is configurable: the C symbol prefix and the native library name +come from the package identity. -```swift -public enum ContactType: UInt32 { - case personal = 0 - case work = 1 - case other = 2 -} -``` +## Build and link -Structs are plain Swift structs with typed properties and a public -memberwise initializer. They declare no C symbols; a `Contact` crosses -the ABI serialized in the [value-buffer format](../reference/value-buffers.md) -as a single pointer-plus-length pair, written and read by the generated -`wvWriteContact`/`wvReadContact` codec pair over the private -`WvWriter`/`WvReader` helpers: +The package builds as-is with SwiftPM. Depend on it by path or from a +repository, and tell the linker where `libkvstore` lives: ```swift -public struct Contact { - public var name: String - public var email: String? - public var age: Int32 - - public init(name: String, email: String?, age: Int32) { - self.name = name - self.email = email - self.age = age - } -} +dependencies: [.package(path: "generated/swift")], +targets: [ + .executableTarget(name: "App", dependencies: [.product(name: "Kvstore", package: "swift")]), +] ``` -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 `[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`: - -```swift -public enum Contacts { - public static func createContact(name: String, age: Int32) throws -> Contact { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = name.withCString { name_ptr in - return weaveffi_contacts_create_contact(name_ptr, age, &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 - } -} +```bash +swift build -Xlinker -L/path/to/lib ``` -Call it as `try Contacts.createContact(name: "Grace", age: 46)`. A -function without `throws` keeps a plain non-throwing signature; a failure -reported there traps via `fatalError` (see [Runtime error -codes](#runtime-error-codes)). Nested IDL modules become nested namespace -enums (`Kv.Stats.getStats(store:)` in the `kvstore` sample). +Swift binds the library at link time, so at run time the dynamic loader finds +it the usual way (its install name, an rpath, or `DYLD_LIBRARY_PATH` and +`LD_LIBRARY_PATH`). The `{PREFIX}_LIBRARY` variable other targets honor +doesn't apply. -Optionals, lists, and maps are buffered: the wrapper packs the argument -into a `WvWriter` and passes the resulting bytes as a borrowed -pointer-plus-length pair for the duration of the call. +When a `CKvstore.xcframework` sits next to `Package.swift`, the manifest uses +it as a binary target instead of the system library, so apps link without +flags. `weaveffi package` bundles the desktop libraries under `lib//` +and, when the platforms include an iOS slice, assembles the XCFramework itself +on macOS (see [Packaging](../guides/packaging.md#ios)). -## Objects (interfaces) +Before the first call, the bindings check that the library implements C ABI +revision 3 and that every top-level module's contract checksum matches the +one they were generated from. A mismatch stops the process with a message +naming the module, since a stale binding would otherwise corrupt memory. -An `interfaces:` entry becomes a `final class` holding an `OpaquePointer` -that is one strong reference to a reference-counted producer object. A -constructor named `new` becomes `init`; any other constructor becomes a -static factory. Methods are instance methods and statics are static -methods, all in lowerCamelCase with argument labels. From the `kvstore` -sample's `Store`: +## Type mapping -```swift -public final class Store { +| IDL type | Swift type | At the ABI | +|---|---|---| +| `i8` to `i64`, `u8` to `u64` | `Int8` to `Int64`, `UInt8` to `UInt64` | Direct | +| `f32`, `f64` | `Float`, `Double` | Direct | +| `bool` | `Bool` | C `bool` | +| `string` | `String` | UTF-8 pointer and length | +| `bytes` | `Data` | Pointer and length | +| C-style enum | `enum E: Int32, Sendable` | `int32_t` | +| Record | `struct`, `Sendable` (and `Hashable` when every field is) | Value buffer | +| Rich enum | `enum` with associated values | Value buffer | +| `T?`, `[T]`, `{K: V}` | `T?`, `[T]`, `[K: V]` | Value buffer | +| Interface | `final class`, `@unchecked Sendable` | Object pointer | +| `Interface?` | optional wrapper | Nullable pointer | +| Callback interface | `protocol P: AnyObject, Sendable` | Context and vtable | +| `iter` | generated `Sequence` class | Iterator handle | + +Strings cross as pointer and length in both directions, so interior NULs +survive. Value buffers decode in place from the library's memory before it's +released. Floats round-trip bit for bit, including NaN and `-0.0`. + +## Objects and lifetime + +An interface becomes a `final class` holding one strong reference. `deinit` +releases it, a constructor named `new` becomes `init`, and other constructors +and statics become `static func`s: + +```swift +public final class Gadget: @unchecked Sendable { let ptr: OpaquePointer init(ptr: OpaquePointer) { @@ -195,503 +96,144 @@ public final class Store { } deinit { - weaveffi_kv_Store_destroy(ptr) + kitchen_sink_kitchen_Gadget_destroy(ptr) } /// Returns a new strong reference to the same object, for a position that /// takes ownership of it (an object token inside a value buffer). func clonePtr() -> OpaquePointer { - weaveffi_kv_Store_clone(ptr)! + wvNonNull(kitchen_sink_kitchen_Gadget_clone(ptr)) } - public static func open(path: String) throws -> 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) + /// Create a gadget with the given id + public init(id: Int64) { + wvLoad() + var err = WvError() + let rv = kitchen_sink_kitchen_Gadget_new(id, &err) + wvTrap(&err) + self.ptr = wvNonNull(rv) } - public func delete(key: String) throws -> Bool - public func count() -> Int64 - public func listKeys(prefix: String?) throws -> KvStoreListKeysIterator - public func compact() async throws -> Int64 - public static func defaultCapacity() -> Int64 -} -``` - -```swift -let store = try Store.open(path: "/tmp/cache.kv") -_ = try store.put(key: "alpha", value: Data("1".utf8), kind: .volatile, ttlSeconds: nil) -print(store.count()) -let reclaimed = try await store.compact() -``` - -- **Disposal is ARC.** Each wrapper instance owns exactly one strong - reference and `deinit` releases it with `_destroy`. There is no public - `close()`; drop your last Swift reference and the producer drops the - object when its own last reference goes. Because the wrapper is a class, - assigning it around shares one Swift object (and one native reference); - you never see a use-after-release from Swift code alone. -- **Copies mint new references at the boundary.** The internal - `clonePtr()` calls `_clone` whenever the wrapper must hand the producer a - reference it will own (an object inside a value buffer); a returned - object is adopted into a new instance. `share()` in the sample returns a - second wrapper over the same producer object, and the two are - independently released. -- The `events` sample's `EventBus` declares a constructor named `new`, - which surfaces as a real initializer: `let bus = EventBus()`. - -Deprecated members carry `@available(*, deprecated, message:)`. - -### Objects as parameters, returns, and inside values - -A top-level object parameter is borrowed for the call (the wrapper passes -`ptr`); a returned object is adopted. `Store?` is a nullable pointer both -ways: - -```swift -public func larger(other: Store?) -> Store? { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kv_Store_larger(ptr, other?.ptr, &err) - trap(&err) - return rv.map { Store(ptr: $0) } -} -``` - -An object inside a record, list, map value, optional, or rich-enum payload -is an ordinary property (`public var store: Store; public var mirror: Store?` -in `StoreInfo`). On the wire it's a `u64` token: the codec writes a fresh -`clonePtr()` for each object (so your wrapper keeps its own reference) and -adopts the token on read: - -```swift -func wvWriteStoreInfo(_ value: StoreInfo, into w: inout WvWriter) { - w.writeString(value.label) - w.writeObject(value.store.clonePtr()) - if let v0 = value.mirror { - w.writeOptionFlag(true) - w.writeObject(v0.clonePtr()) - } else { - w.writeOptionFlag(false) + /// Render the gadget as a human-readable string + public func describe() -> String { + var err = WvError() + var outLen = 0 + let rv = kitchen_sink_kitchen_Gadget_describe(ptr, &outLen, &err) + wvTrap(&err) + return wvTakeString(rv, outLen) } - w.writeInt64(value.count) -} - -func wvReadStoreInfo(_ r: inout WvReader) -> StoreInfo { - let v1 = r.readString() - let v2 = Store(ptr: r.readObject()) - // ... } ``` -`Store.openMany(paths:)` returns `[Store]`, one adopted instance per -element, and `Store.totalCount(stores:extra:)` clones each object it -encodes. An async callable returning an object adopts the pointer inside -the completion callback, and an `iter` adopts one per `next()`. +Swift keeps every argument and the receiver alive for the whole call, so a +wrapper can't be freed mid-call. Writing an object into a value buffer clones +its reference, and reading one adopts the reference into a new wrapper, so two +wrappers can share one native object. There's no `close()`: release happens +when the last Swift reference goes away. -## Typed errors +## Errors -A module's error domain becomes a Swift error enum conforming to `Error` -and `LocalizedError`, with one lowerCamelCase case per declared code, each -carrying its message. From the `kvstore` sample's `KvError`: +A module's error domain becomes an `enum` conforming to `Error` and +`LocalizedError`, one case per code, carrying the message (the code's +documented message when the library sends none) and any payload fields. +`errorCode` returns the numeric code: ```swift -/// Typed errors reported by the `kv` module. -public enum KvError: Error, LocalizedError { - case keyNotFound(message: String) - case expired(message: String) - case storeFull(message: String) - case ioError(message: String) - - /// 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 - } - } +public enum KitchenErrorsError: Error, LocalizedError, Sendable { + case notFound(message: String) + case invalidInput(message: String) } ``` -Callables with `throws: true` route failures through a per-domain checker -(`checkKv`) that maps the ABI code to the matching case, falling back to -the generic `WeaveFFIError.error(code:message:)` for codes the domain -doesn't declare: +Throwing wrappers raise that enum for domain codes and +`{Module}RuntimeError` (with `errorCode` and `message`) for runtime failures: +`-1` generic, `-2` panic, `-3` marshalling, and `-4` when a callback +implementation threw. Swift has no unchecked errors, so a function that +doesn't declare `throws` stops the process with `fatalError` when the library +reports a failure, with a message naming the function, code, and message (from +the `events` sample, where a subscriber threw): -```swift -do { - let entry = try store.get(key: "alpha") -} catch KvError.keyNotFound { - print("no such key") -} catch let e as KvError { - print("kv failure \(e.errorCode)") -} catch let e as WeaveFFIError { - print("runtime failure \(e.errorCode): \(e.localizedDescription)") -} +```text +Events.publish(topic:text:tags:) failed with code -4: route rejected boom ``` -An error code that declares payload `fields:` carries them as additional -labeled associated values on its case, alongside `message:`, decoded from -the error's payload buffer before `weaveffi_error_clear` releases it. - -### Runtime error codes +## Async and cancellation -Negative codes are never domain codes, so they always arrive as -`WeaveFFIError.error(code:message:)`: - -| Code | ABI name | When | -|------|----------|------| -| -1 | `GENERIC_ERROR_CODE` | The producer reported a failure with no domain code | -| -2 | `PANIC_ERROR_CODE` | The Rust producer panicked inside an export or a spawned async future | -| -3 | `MARSHAL_ERROR_CODE` | A null object or a malformed value buffer or string was rejected at the boundary | -| -4 | `FOREIGN_ERROR_CODE` | A callback-interface implementation threw | - -Where they surface depends on the callable: - -- A callable with `throws: true` throws `WeaveFFIError` (sync), rejects its - continuation (async), or, for an iterator, ends iteration and stores the - error in the sequence's `error` property. -- A callable without `throws` has a non-throwing Swift signature. The - wrapper still checks the error slot after the call and **traps** with - `fatalError("\(code): \(message)")` when it's nonzero; an async one traps - inside the completion callback. This is the trap idiom: -1 through -3 - there are producer bugs, and -4 means a callback you passed threw on a - path the IDL declared as infallible. -- A malformed value buffer detected on the Swift side (`wvDecodeFailure`) - also traps. - -## Callback interfaces - -A `callback_interfaces:` entry becomes a Swift protocol with one `throws` -requirement per IDL method. Conform any class or struct to it and pass the -value wherever the API expects the interface. From the `events` sample: +An async function becomes an `async` function over a checked continuation; +the library's completion callback resumes it exactly once, from any thread. +A cancellable function also `throws` and runs inside +`withTaskCancellationHandler`: it creates a native cancel token, passes it to +the launch, and cancels it when the calling task is cancelled. A cancelled +call (code `-5`) throws `CancellationError`, even when the task was cancelled +before the call started: ```swift -public protocol Subscriber { - /// Decide how the bus should treat `topic` for this subscriber. - func route(topic: String) throws -> Delivery - /// Receive an accepted message. Returns the subscriber's running count - /// of received messages. - func onMessage(message: Message) throws -> Int64 - /// Receive the bus itself (an object handed through a callback). The - /// consumer adopts the reference and may keep or drop it. - func onAttached(bus: EventBus) throws -} -``` - -```swift -final class LoggingSubscriber: Subscriber { - var attachedBus: EventBus? - - func route(topic: String) throws -> Delivery { - topic == "quiet" ? .skip : .accept - } - func onMessage(message: Message) throws -> Int64 { - print("\(message.topic): \(message.text)") - return 1 - } - func onAttached(bus: EventBus) throws { - attachedBus = bus // keep it, or let it go to release the reference - } -} - -let bus = EventBus() -_ = bus.subscribe(subscriber: LoggingSubscriber()) -``` - -The wrapper boxes your value (`WvSubscriberBox`), retains it with -`Unmanaged.passRetained`, and passes the box as the vtable `ctx` along with -a process-wide static vtable of `@convention(c)` trampolines. The vtable's -`free` entry releases the box when the producer drops its last reference, -which is when your object is deallocated if nothing else holds it: - -```swift -enum WvSubscriberVtable { - static let value = weaveffi_events_Subscriber_vtable( - route: { ctx, topic, out_err in - let wvBox = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() - do { - return weaveffi_events_Delivery(try wvBox.impl.route(topic: String(cString: topic!)).rawValue) - } catch { - wvForeignError(out_err, error) - return weaveffi_events_Delivery(0) +public static func doCancellable(input: String) async throws -> String { + wvLoad() + let token = WvCancelToken() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let context = Unmanaged.passRetained(WvContinuation(continuation)).toOpaque() + wvWithUTF8(input) { input_ptr, input_len in + kitchen_sink_kitchen_do_cancellable(input_ptr, input_len, token.raw, { context, err, resultPtr, resultLen in + let cont = Unmanaged>.fromOpaque(context!).takeRetainedValue().value + if let err = err { + cont.resume(throwing: wvTakeError(err, wvCancelledOrTrap)) + return + } + cont.resume(returning: wvTakeString(resultPtr, resultLen)) + }, context) } - }, - // on_message, on_attached ... - free: { ctx in - Unmanaged.fromOpaque(ctx!).release() } - ) -} -``` - -- **Argument ownership.** Strings and buffered values are copied into - Swift values before your method runs. An object argument (`bus: - EventBus`) is adopted into a fresh wrapper that your implementation - owns: store it to keep the reference, or let it fall out of scope and - `deinit` releases it. -- **Lifetime.** The producer may retain the implementation indefinitely - (`subscribe` holds it until `clearSubscribers()` or the bus is dropped); - a function that only uses it for the call (`Events.routeOnce`) frees it - before returning. -- **Errors.** A thrown Swift error never unwinds through the C frame. The - trampoline reports it with `weaveffi_error_set(out_err, -4, message)`, - using `errorDescription` for a `LocalizedError` and - `String(describing:)` otherwise. The producer aborts the call that - triggered the callback and the original caller sees - `WeaveFFIError.error(code: -4, ...)` if the callable throws; on a - non-throwing callable the wrapper traps. The implementation stays - attached either way. -- **Threads.** Methods run on whichever thread the producer calls from: the - calling thread for a synchronous call, a producer worker for an async one - (`publishLater` in the sample). The wrapper doesn't hop to the main actor - or any queue; synchronize shared state yourself. - -## Rich (algebraic) enums - -An enum whose variants declare `fields` is a *rich* (algebraic) enum, a sum -type with associated data. Plain C-style enums stay Swift `enum`s backed by -`UInt32`; a rich enum instead becomes a native Swift enum with labeled -associated values, one case per variant. From the `shapes` sample: - -```swift -/// An algebraic shape (sum type with associated data) -public enum Shape { - /// The empty shape - case empty - /// A circle with a radius - case circle(radius: Double) - /// An axis-aligned rectangle - case rectangle(width: Float, height: Float) - /// A labeled shape with a small count - case labeled(label: String, count: UInt8) -} -``` - -Build variants directly and pattern-match as with any Swift enum. Module -functions live on the `Shapes` namespace enum and take/return the value: - -```swift -let shape = Shape.circle(radius: 2.0) - -if case let .circle(radius) = shape { - print("radius = \(radius)") -} - -print(Shapes.describe(shape: shape)) -let bigger = Shapes.scale(shape: shape, factor: 3.0) -``` - -There are no C symbols behind a rich enum: on the wire it's a value -buffer holding the `i32` variant tag followed by the active variant's -fields, written and read by the generated `wvWriteShape`/`wvReadShape` -codec pair. Values are plain Swift data; nothing to free. A payload that -holds an object follows the token rules above. - -## Build instructions - -Build the producer cdylib, generate the bindings, and compile your -program against the generated Swift module (the `kvstore` sample shown; -its module resolves to `Kvstore` + `CKvstore` from the package name): - -```bash -cargo build -p kvstore -weaveffi generate samples/kvstore/src/lib.rs -o generated --target c,swift - -swiftc \ - -I generated/swift/Sources/CKvstore \ - -L target/debug -lkvstore \ - -Xlinker -rpath -Xlinker target/debug \ - generated/swift/Sources/Kvstore/Kvstore.swift main.swift -o app - -DYLD_LIBRARY_PATH=target/debug ./app # LD_LIBRARY_PATH on Linux -``` - -The module map points at `../../../c/.h`, so generate the C target -alongside Swift. In a real SwiftPM application, add the generated package -as a path dependency, link the system-library and wrapper targets, and -ship the cdylib as part of an XCFramework or bundled `.dylib`/`.so`. The -`conformance/swift/` consumers show a complete assembly for every sample -(see `conformance/run.sh`). - -## Packaging - -`weaveffi package --target swift` emits a SwiftPM package whose C module is -a `binaryTarget` xcframework, with the prebuilt desktop libraries under -`swift/lib//` and a README describing how to fuse them into -`C.xcframework` with `lipo` and `xcodebuild -create-xcframework` -(the one step that needs Apple tooling). Only desktop slices are bundled -(`darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `windows-x64`); -iOS and Android slices aren't produced. See -[Packaging and Distribution](../guides/packaging.md). - -## Memory and ownership - -- Interface classes own one strong reference; `deinit` releases it. - Structs and rich enums are plain Swift values with nothing to free. -- Returned strings are copied into Swift `String` and the raw pointer is - freed via `weaveffi_free_string` immediately. -- `withUnsafeBufferPointer` keeps input buffers alive only for the - duration of the C call; there's no copy. -- For `bytes` parameters, the wrapper copies the `Data` into a - `[UInt8]` array and passes it via `withUnsafeBufferPointer`; returned - `bytes` are copied into `Data` and the Rust buffer is freed with - `weaveffi_free_bytes`. -- Buffered values (structs, rich enums, optionals, arrays, and - dictionaries) cross as one value buffer: parameters are packed into a - `WvWriter` whose bytes the producer borrows for the call; returns are - copied out, released with `weaveffi_free_bytes`, and decoded with the - matching `wvRead*` routine. Object fields inside are cloned on the way in - and adopted on the way out. -- Callback-interface implementations are retained by the producer's box - until its `free` runs. - -## Async support - -Async IDL functions (`async: true`) are exposed as `async` methods that -bridge the C ABI completion callback into Swift structured concurrency via -a checked continuation. The continuation is boxed in a `ContinuationRef`, -retained with `Unmanaged.passRetained`, and released exactly once by -`takeRetainedValue()` inside the completion callback. From the `kvstore` -sample: - -```swift -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) + } onCancel: { + token.cancel() } } ``` -The completion callback fires exactly once, on an arbitrary producer -thread, and the continuation is resumed exactly once from inside it. -Result buffers passed to the callback are owned by the consumer: the -wrapper copies or decodes them and releases them with -`weaveffi_free_string` or `weaveffi_free_bytes`; a reported error is -heap-boxed and released with `weaveffi_error_free`; an object result is -adopted into a new wrapper whose `deinit` eventually releases it. - -`compact` declares `throws: true`, so the continuation rejects with the -typed `KvError` (via `mapKv`) or the generic `WeaveFFIError` for a runtime -code. An async callable without `throws` (the `events` sample's -`publishLater`) is `async` but not `throws`: it uses a plain -`withCheckedContinuation` whose failure type is `Never`, and a nonzero -code in the callback traps with `fatalError`. - -For callables marked `cancellable: true`, the C ABI takes an extra -`weaveffi_cancel_token*` parameter. The Swift wrapper passes `nil` for -that slot; cancellation isn't surfaced in Swift, and Swift `Task` -cancellation doesn't propagate to the native operation. +The token is released when the wrapper returns; the library holds its own +reference for the call's duration. -## Iterators +## Callback interfaces -`iter` returns are lazy: the wrapper returns a generated -`final class` conforming to `Sequence` and `IteratorProtocol` that -wraps the opaque C iterator handle. Nothing is drained up front; each -`next()` call pulls exactly one element from the producer, copies it -into Swift memory, and frees the element's native allocation. From the -`kvstore` sample (`Store.listKeys` returns `iter`): +A callback interface becomes a protocol whose methods are all `throws`: ```swift -public final class KvStoreListKeysIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - /// The error that ended iteration early, if any. - public private(set) var error: Error? - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - weaveffi_kv_Store_ListKeysIterator_destroy(handle) - self.handle = nil - } - - 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 { - // ... map the code through mapKv and store it in `error` ... - } - destroyHandle() - return nil - } - let element = String(cString: item!) - weaveffi_free_string(item) - return element - } +public protocol ReadyListener: AnyObject, Sendable { + /// Fires when an item is ready + func onReady(code: Int32, msg: String) throws + /// Receives the item itself and says whether to keep listening + func onItem(item: Item, gadget: Gadget) throws -> Bool } ``` -The handle is destroyed exactly once: eagerly when `next()` reports -exhaustion, or from `deinit` when the sequence is abandoned early. Since -the class conforms to `Sequence`, callers just write -`for key in try store.listKeys(prefix: nil) { ... }`; the sequence is -single-pass. A buffered element is decoded from its value buffer and -released with `weaveffi_free_bytes`; an object element is adopted into a -wrapper. - -A throwing iterator function like `listKeys` routes a launch failure -through the typed checker (`checkKv`). Swift's `IteratorProtocol.next()` -can't throw, so a mid-stream error can't surface on the step that failed; -instead it ends iteration and is stored in the sequence's public `error` -property for the caller to inspect after the loop: +Implement it with a class. The library may call it from any thread, which is +why the protocol requires `Sendable`; a class guarding its own state can +declare `@unchecked Sendable`. Passing an implementation retains it until the +library calls the vtable's `free`, and objects passed into a method are +adopted wrappers the implementation may keep. A thrown error is reported to +the library as code `-4` with the error's description; it never unwinds +through C. -```swift -let keys = try store.listKeys(prefix: nil) -for key in keys { print(key) } -if let error = keys.error { throw error } -``` +## Iterators -A non-throwing iterator function (the `events` sample's `messages()`) -traps on a mid-stream code instead. +An `iter` return becomes a class conforming to `Sequence` and +`IteratorProtocol`. Each `next()` pulls exactly one element from the library; +the native iterator is destroyed on exhaustion or from `deinit` when iteration +stops early. `next()` can't throw, so for a throwing function an error +reported mid-stream ends iteration and is stored in the sequence's `error` +property. ## Known limitations -- Cancellation tokens aren't exposed; async wrappers always pass `nil`. -- Mid-stream iterator errors are stored in `error`, not thrown, and only - for throwing callables. -- Non-throwing callables trap (`fatalError`) on any runtime code, - including -4 from a callback you passed; declare `throws: true` on any - callable whose callback implementations may fail. -- The module map references the C header by relative path - (`../../../c/.h`), so the C target must be generated next to the - Swift one. -- Callback methods aren't marshalled to any particular thread or actor. - -## Troubleshooting - -- **`module 'CWeaveFFI' not found`**: Xcode/SwiftPM didn't pick up - the generated `module.modulemap`. Make sure - `Sources/CWeaveFFI/module.modulemap` is on disk and the package - declares `systemLibrary(name: "CWeaveFFI")`. -- **`Library not loaded: libweaveffi.dylib`**: set - `DYLD_LIBRARY_PATH` for development or embed the dylib in your - application bundle for distribution. -- **`Fatal error: -4: ...`**: a callback implementation threw on a - callable without `throws`. Either make the implementation not throw or - mark the callable `throws: true` so the error is catchable. -- **Optional value ends up `nil` even when present**: an optional - crosses inside a value buffer as a presence flag followed by the - value; double-check the Rust implementation actually returns - `Some(_)` for the case you expect. +- The library is linked at build time, so `{PREFIX}_LIBRARY` isn't honored and + library paths come from the linker and loader. +- Contract mismatches, malformed value buffers, and failures of non-throwing + calls stop the process instead of throwing. +- Callback methods return `Void` or a direct value (a number, `Bool`, or + C-style enum); richer returns aren't part of the ABI yet. +- File-scope type names must be unique across modules, since every module's + types share the Swift module's namespace. A type that shares its name with a + module namespace is qualified with the Swift module name where needed. +- Interface wrappers aren't `Hashable`, so records that contain objects are + only `Sendable`. diff --git a/docs/src/generators/wasm.md b/docs/src/generators/wasm.md index a31a7a89..c5af40ba 100644 --- a/docs/src/generators/wasm.md +++ b/docs/src/generators/wasm.md @@ -1,796 +1,143 @@ -# Wasm +# WebAssembly -## Overview - -The Wasm target produces a typed ES module loader for -`wasm32-unknown-unknown` builds of WeaveFFI cdylibs, speaking ABI -revision 2. The loader wraps the raw exports in idiomatic JavaScript: -per-module namespaces, plain JS objects for records and tagged objects -for rich enums, reference-counted object wrappers with `close()` and -`Symbol.dispose`, `Promise`-based async functions, lazy iterators, -callback interfaces implemented as plain JS objects, thrown `Error`s -instead of error slots, and automatic string/bytes staging in linear -memory. TypeScript declarations describe the whole surface. - -C and C++ producers compiled with Emscripten are supported through a -dedicated loader variant; see [Emscripten mode](#emscripten-mode). - -Callback interfaces and async functions deliver **synchronously, on -the calling thread**: a `wasm32-unknown-unknown` module is -single-threaded, so a callback runs only while a call into the module is -on the stack; see [Callback interfaces](#callback-interfaces). +The WebAssembly target generates an npm package for a `wasm32-unknown-unknown` +build of the library: an ES module with TypeScript declarations over glue +that stages values in the module's linear memory. The JavaScript API is the +[Node.js target](node.md)'s, so the sections there on names, types, objects, +errors, callbacks, and iterators apply here unchanged; this page covers what +differs. ## What gets generated -| File | Purpose | -|------|---------| -| `generated/wasm/weaveffi_wasm.js` | ES module: memory helpers, value-buffer codecs, object and iterator runtime, and the async `loadWeaveffiWasm(source)` loader returning typed bindings | -| `generated/wasm/weaveffi_wasm.d.ts` | TypeScript declarations for the loader, every module namespace, object class, and callback interface | -| `generated/wasm/package.json` | npm package manifest (`type: "module"`) | -| `generated/wasm/README.md` | Quickstart and boundary conventions | - -## Configuration - -```toml -[generators.wasm] -module_name = "weaveffi_wasm" # basename of the emitted .js/.d.ts -emscripten = false # see Emscripten mode -allow_unsupported = false # emit throwing stubs instead of failing in Emscripten mode -``` - -`prefix` is also accepted and defaults to `[global] c_prefix`, so the -glue calls the same exported symbols the producer emits. - -## Type mapping - -| IDL type | Wasm boundary | JavaScript surface | -|--------------|---------------|--------------------| -| `i32` / `u32`| `i32` | `number` | -| `i8` / `i16` | `i32` | `number` | -| `u8` / `u16` | `i32` | `number` | -| `i64` | `i64` | `bigint` | -| `u64` | `i64` | `bigint` | -| `f64` | `f64` | `number` | -| `f32` | `f32` | `number` | -| `bool` | `i32` | `boolean` (0/1 at the boundary) | -| `string` | `i32` pointer (NUL-terminated UTF-8) | `string`, staged via `weaveffi_alloc` | -| `bytes` | `i32` pointer + `i32` length | `Uint8Array` copy | -| `InterfaceName` | `i32` pointer into linear memory; one strong reference per wrapper | `class` with `close()` and `Symbol.dispose` | -| `InterfaceName?` | `i32` pointer, 0 = absent | `InterfaceName \| null` | -| `CallbackName` | `i32` context key + `i32` pointer to a static vtable | any object with the interface's methods | -| `StructName` | value buffer (`i32` pointer + `i32` length) | plain JS object | -| `EnumName` (plain, C-style) | `i32` discriminant | `number` (frozen constants object) | -| `EnumName` (rich / algebraic) | value buffer | tagged plain object (`{ tag: "Circle", radius: 2 }`) | -| `T?` (non-object) | value buffer | `T \| null` | -| `[T]` | value buffer | `Array` copy | -| `{K: V}` | value buffer | plain object (`Map` accepted on input) | -| `iter` | iterator handle + `next` out-slot | lazy `IterableIterator` | - -## Example IDL → generated code - -The loader exports a single async entry point that compiles, -instantiates, and wraps a module. It accepts a URL to fetch, the bytes, -or an already compiled `WebAssembly.Module`, and checks the ABI revision -first: - -```javascript -import { loadWeaveffiWasm } from './weaveffi_wasm.js'; - -const api = await loadWeaveffiWasm('/your_library.wasm'); -``` - -```js -const _ABI_VERSION = 2; - -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}`); - } -} -``` - -Functions are grouped by IDL module in lowerCamelCase (nested IDL -modules nest namespaces, e.g. `api.kv.stats.getStats(store)`), object -classes hang off their module namespace (`api.kv.Store`), and error -classes and plain-enum constant objects are top-level named exports: - -```javascript -import { loadWeaveffiWasm, KvError, KeyNotFound, EntryKind } from './weaveffi_wasm.js'; - -const api = await loadWeaveffiWasm('/kvstore.wasm'); -const store = api.kv.Store.open('/tmp/cache.kv'); -store.put('alpha', new Uint8Array([1]), EntryKind.Volatile, null); -for (const key of store.listKeys(null)) console.log(key); -store.close(); -``` - -Records come back as plain JS objects, serialized in a value buffer at -the boundary and packed/unpacked by generated per-record codecs. From -the `kvstore` sample: - -```js -// 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; -} -``` - -The raw exports stay reachable for anything not covered by the typed -surface: - -```javascript -api._raw.weaveffi_alloc(16); -``` - -## Typed errors - -The module exports `WeaveFFIError` (extending `Error` with a numeric -`code`). A module's error domain adds an exported base class named after -the domain plus one exported class per code, each carrying its stable -`CODE` and reachable both flat and via the domain class. From the -`kvstore` sample: - -```js -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, producer - * panics, and callback-interface implementations that raised (code -4). - * 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 {} +For a library named `kvstore` (the snapshot fixtures use `kitchen_sink`): -// key not found -export class KeyNotFound extends KvError { - constructor(message = "key not found") { - super(1001, message); - } -} -KeyNotFound.CODE = 1001; -KvError.KeyNotFound = KeyNotFound; -// Expired, StoreFull, IoError follow the same shape. +```text +wasm/ +├── package.json ES module manifest +├── index.js `init()`, the raw entry points, and the API +├── index.d.ts TypeScript declarations +├── runtime.js the shared runtime (codec, errors, wrappers) +├── linear.js the transport: loading, staging, table functions +└── README.md ``` -A callable with `throws: true` checks the error slot through the -domain's mapper (`_kvErrorFrom`), so a failure arrives as the matching -subclass (`KeyNotFound`), the domain (`KvError`), or, for codes outside -the domain, the generic `WeaveFFIError`. A callable without `throws` -uses the generic checker only; a non-zero code there can only be a -producer panic, a marshalling failure, or a callback implementation that -threw, and it surfaces as `WeaveFFIError`. There is no separate trap -idiom on this target: JavaScript has exceptions, so both paths throw. - -An error code that declares payload `fields:` carries them serialized -in the error's payload buffer; the checker decodes them onto the thrown -error object before clearing the slot. - -### Runtime error codes +The package name defaults to the library identity's name; set +`package_name` under `[generators.wasm]` to override it. `weaveffi package` +adds the prebuilt module as `{library}.wasm`. -| Code | ABI name | When you see it on this target | -|------|----------|--------------------------------| -| `-1` | `GENERIC_ERROR_CODE` | A producer reported a failure without a domain code; a `#[weaveffi::export]` body that returns a bare `Err(String)`. | -| `-2` | `PANIC_ERROR_CODE` | The producer panicked. `wasm32-unknown-unknown` has no unwinding runtime: a panic executes `unreachable`, the engine throws `WebAssembly.RuntimeError`, and the wrapper's `catch` translates it (`_trap`/`_trapError`) into `WeaveFFIError(-2, 'producer panicked: ...')`. The producer frames are not unwound, so a lock the producer held across the call stays locked. | -| `-3` | `MARSHAL_ERROR_CODE` | The consumer passed something the boundary can't carry, most often a closed object wrapper (`_borrow` throws `WeaveFFIError(-3, 'expected a live object wrapper')`), or the producer returned a malformed value buffer (`_BufReader` reports a truncated buffer or invalid UTF-8). | -| `-4` | `FOREIGN_ERROR_CODE` | A callback-interface method you implemented threw. The trampoline records the failure on the producer's `out_err`; the wrapper you called throws `WeaveFFIError` with code `-4` and your exception's message. | +## Build the module -Errors surface wherever the call was made: thrown from synchronous -methods and free functions, as a rejected `Promise` from async -functions, and thrown from `next()` (or the `for...of` loop) for -iterators. +The glue installs async completions and callback-interface methods in the +module's function table, so link the module with an exported, growable +table: -## Objects (interfaces) - -An `interfaces:` entry becomes a class exposed on its module's namespace -(`api.kv.Store`). A constructor named `new` becomes the JS -`constructor`; other constructors are static factories; methods are -camelCased instance methods; statics are static methods. Each wrapper -owns exactly one strong reference to the producer object, and `close()` -releases it. From the `kvstore` sample (trimmed): - -```js -class Store { - static _wrap(handle) { - return _adopt(Object.create(Store.prototype), handle, wasm.weaveffi_kv_Store_destroy); - } - _clone() { - return wasm.weaveffi_kv_Store_clone(_borrow(this)); - } - close() { - _release(this); - } - [_dispose]() { - _release(this); - } - static open(path) { - const [a0_p, a0_s] = _cstr(wasm, path); - const _err = _allocErr(wasm); - let _r; - try { - _r = wasm.weaveffi_kv_Store_open(a0_p, _err); - } catch (e) { - throw _trap(wasm, _err, e); - } finally { - wasm.weaveffi_dealloc(a0_p, a0_s); - } - _checkKvError(wasm, _err); - _freeErr(wasm, _err); - return Store._wrap(_r); - } - share() { /* ... */ return Store._wrap(_r); } - larger(other) { - /* ... */ - _r = wasm.weaveffi_kv_Store_larger(_borrow(this), (other === null || other === undefined ? 0 : _borrow(other)), _err); - /* ... */ - return _r === 0 ? null : Store._wrap(_r); - } -} +```sh +RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table" \ + cargo build --release --target wasm32-unknown-unknown ``` -The shared runtime behind those methods: +## Load -```js -// Bind one strong reference to a wrapper and arm the backstop. -function _adopt(obj, handle, destroy) { - obj._handle = handle; - obj._destroy = destroy; - if (_finalizer !== null) _finalizer.register(obj, [destroy, handle], obj); - return obj; -} - -// Release a wrapper's reference exactly once; later calls are no-ops. -function _release(obj) { - if (!obj._handle) return; - if (_finalizer !== null) _finalizer.unregister(obj); - obj._destroy(obj._handle); - obj._handle = 0; -} - -// The pointer of a live wrapper, lent to the producer for one call (the -// wrapper keeps its own reference). A closed or non-object argument is a -// consumer programming error, reported with the marshalling code. -function _borrow(obj) { - if (obj === null || obj === undefined || !obj._handle) { - throw new WeaveFFIError(-3, 'expected a live object wrapper'); - } - return obj._handle; -} -``` - -- **Disposal.** `close()` calls the producer's `_destroy` symbol and - drops one reference; it is idempotent. The same method is reachable as - `[Symbol.dispose]`, so `using store = api.kv.Store.open(path)` releases - it at block exit on runtimes with explicit resource management (older - runtimes get `Symbol.for('Symbol.dispose')` as a stand-in). A - `FinalizationRegistry` backstop releases wrappers that are garbage - collected without `close()`; on runtimes without - `FinalizationRegistry`, `close()` is the only release path and a - forgotten wrapper keeps its reference until the module instance is - dropped. Don't rely on the backstop for timely release. -- **Use after close.** Every method and every parameter site goes - through `_borrow`, so touching a closed wrapper throws - `WeaveFFIError` with code `-3`, synchronously for methods and as a - rejection for async methods. -- **Passing objects lends, returning objects owns.** A wrapper passed as - a parameter lends its pointer for the duration of the call and keeps - its own reference. A returned pointer is adopted into a fresh wrapper - that owns that reference. `share()` in the sample returns a second - wrapper around the same producer object; both must be closed. -- **Copies mint a new strong reference.** When a wrapper is placed - inside a value buffer (a record field, a list element, an optional) - the codec calls `_clone()`, which invokes the producer's `_clone` - symbol, so the buffer carries its own reference and the producer - adopts it: +`init()` loads the module and checks the contract before any other export +works: the module must implement ABI revision 3, and each top-level module's +checksum must match the bindings. Until the promise resolves, every other +call throws. ```js -// Serialize a `kv.StoreInfo` record into the value-buffer wire format. -function _write_kv_StoreInfo(w, v) { - w.str(v.label); - w.obj(v.store._clone()); - if (v.mirror === null || v.mirror === undefined) { - w.flag(false); - } else { - w.flag(true); - w.obj(v.mirror._clone()); - } - w.i64(v.count); -} +import { init, kv } from 'kvstore'; -// Decode a `kv.StoreInfo` record from the value-buffer wire format. -function _read_kv_StoreInfo(r) { - const v = {}; - v.label = r.str(); - v.store = Store._wrap(r.obj()); - v.mirror = (r.flag() ? Store._wrap(r.obj()) : null); - v.count = r.i64(); - return v; -} +await init(); +const store = kv.Store.open('/tmp/data'); ``` -- **Nullable objects** (`Store?`) are `Store | null`: `null` or - `undefined` becomes a 0 pointer on input, and a 0 return becomes - `null` (`larger` above). -- **Lists of objects** decode into arrays of fresh wrappers, each owning - one reference (`Store.openMany(paths)` returns - `_arr.push(Store._wrap(_rd.obj()))` per element); close each one. -- **Iterators over objects** yield a fresh owning wrapper per `next()`. -- **Async functions returning objects** resolve the `Promise` with a - fresh owning wrapper. - -The TypeScript declarations document the ownership rule at every object -return: +Without an argument, `init()` loads `{library}.wasm` from next to `index.js` +(by `fetch` in a browser, from disk on Node.js), or, on Node.js, the file +named by the `{PREFIX}_LIBRARY` environment variable. It also takes a URL, a +path, bytes, a `Response`, or a compiled `WebAssembly.Module`: -```typescript -/** - * A second reference to this same store (the returned pointer equals - * the receiver's; both must eventually be destroyed). - * @returns A wrapper owning one reference; `close()` it (or bind it with `using`) when done. - * @throws {WeaveFFIError} if the native call fails - */ -share(): Store; -larger(other: Store | null): Store | null; -describe(label: string, mirror: Store | null): StoreInfo; +```ts +export declare function init( + source?: string | URL | BufferSource | WebAssembly.Module | Response, +): Promise; ``` -## Callback interfaces +`init()` may be called more than once; later calls return the same promise, +and a failed load can be retried. -A `callback_interfaces:` entry becomes a TypeScript `interface`; the -consumer implements it with any object that has the methods (a class -instance or an object literal, duck-typed). From the `events` sample's -`weaveffi_wasm.d.ts`: - -```typescript -export interface Subscriber { - /** Decide how the bus should treat `topic` for this subscriber. */ - route(topic: string): Delivery; - /** Receive an accepted message. Returns the subscriber's running count - * of received messages. */ - onMessage(message: Message): bigint; - /** Receive the bus itself (an object handed through a callback). The - * consumer adopts the reference and may keep or drop it. */ - onAttached(bus: EventBus): void; -} -``` - -```js -const received = []; -const sub = { - route(topic) { return topic.startsWith('alerts/') ? Delivery.Accept : Delivery.Skip; }, - onMessage(message) { received.push(message.text); return BigInt(received.length); }, - onAttached(bus) { bus.close(); }, // owned by the implementation -}; -const bus = new api.events.EventBus(); -bus.subscribe(sub); -bus.publish('alerts/disk', 'low space', []); // route and onMessage run before publish returns -``` +## Transport -Under the hood the loader registers the implementation in a `Map` keyed -by an integer context id and passes that id plus a pointer to a static -vtable in linear memory. The vtable is filled once per loaded instance -with function-table indices of `WebAssembly.Function` trampolines, one -per method plus `free`: +Each C symbol gets one entry point in `index.js` that checks its arguments, +copies strings and byte runs into linear memory for the call, and copies +returned strings and buffers out before releasing them: ```js -const _cb_Subscriber_on_attached = _registerTrampoline(_table, ['i32', 'i32', 'i32'], [], (_ctx, a0, _err) => { - try { - const _impl = _callbacks.get(_ctx); - const _p0 = EventBus._wrap(a0); - if (_pendingForeign !== null) { - _reportForeign(wasm, _err, _pendingForeign); - return; - } - _impl.onAttached(_p0); - } catch (e) { - _setForeignError(wasm, _err, e); - } -}); -// `free(ctx)`: the producer dropped its last reference; forget the -// implementation so it can be collected. -const _cb_Subscriber_free = _registerTrampoline(_table, ['i32'], [], (_ctx) => { _callbacks.delete(_ctx); }); -const _vtable_Subscriber = wasm.weaveffi_alloc(16); -{ - const _dv = new DataView(wasm.memory.buffer); - _dv.setUint32(_vtable_Subscriber + 0, _cb_Subscriber_route, true); - _dv.setUint32(_vtable_Subscriber + 4, _cb_Subscriber_on_message, true); - _dv.setUint32(_vtable_Subscriber + 8, _cb_Subscriber_on_attached, true); - _dv.setUint32(_vtable_Subscriber + 12, _cb_Subscriber_free, true); -} -``` - -- **Argument ownership.** Strings, bytes, and buffered values (records, - rich enums, optionals, lists, maps) are borrowed from the producer for - the duration of the dispatch and decoded into fresh JavaScript values - before your method runs, so they're safe to retain. An object argument - (`onAttached(bus)`) is **owned by the implementation**: the trampoline - adopts the reference into a wrapper, and you `close()` it when you're - done (or keep it; the `FinalizationRegistry` backstop applies). -- **Lifetime.** The implementation stays registered until the producer - drops its last reference and calls the vtable's `free`, which removes - the `Map` entry. Replacing a listener (`setEvictionListener`) frees the - previous one. -- **When the implementation throws.** The trampoline catches the - exception, writes `FOREIGN_ERROR_CODE` (`-4`) and the message into the - producer's `out_err`, and returns the method's default value. Because - `wasm32` can't unwind, the producer keeps running to completion; the - message is parked in `_pendingForeign` so any further callback during - that same call is refused with the same error instead of reaching your - implementation again. When the producer call returns, the wrapper you - called throws `WeaveFFIError` with code `-4` carrying your message - (async: the `Promise` rejects). The object stays usable afterwards; the - conformance consumer keeps publishing on the same bus. A method that - returns the wrong type (a non-`bigint`-coercible value from - `onMessage`) surfaces the same way. -- **Thread affinity.** The target is single-threaded, so a callback runs - only while a call into the module is on the stack, before that call - returns. A producer that calls back from a spawned thread cannot run - on `wasm32-unknown-unknown` at all (`std::thread::spawn` fails there). - There is no re-entrancy hazard beyond the ordinary JavaScript one: - calling back into the same object from inside your callback is a - synchronous nested call. - -## Rich (algebraic) enums - -A *rich* (algebraic) enum is a sum type whose variants carry associated -data. A plain C-style enum stays an `i32` discriminant (surfaced as a -`number` plus a frozen constants object), but a rich enum is a plain -object tagged by variant name (`{ tag: "Circle", radius: 2 }`) that -crosses the boundary serialized in a value buffer, exactly like a -record: an `i32` tag followed by the active variant's fields. - -For a `Shape` enum with variants `Empty`, `Circle { radius: f64 }`, -`Rectangle { width: f32, height: f32 }`, and -`Labeled { label: string, count: u8 }`, the generated -`weaveffi_wasm.d.ts` types the value as a discriminated union: - -```typescript -export type Shape = - | { tag: "Empty" } - | { tag: "Circle"; radius: number } - | { tag: "Rectangle"; width: number; height: number } - | { tag: "Labeled"; label: string; count: number }; -``` + kitchen_sink_kitchen_echo_string: (a0) => { + let s0 = null; + try { + s0 = m.str(a0); + const r = x.kitchen_sink_kitchen_echo_string(s0[0], s0[1], m.len, m.err); + m.check(); + return m.takeStr(r, m.outLen()); + } finally { + m.unstage(s0); + } + }, +``` + +One error slot, one `out_len` slot, and one iterator item slot are allocated +at load and reused by every call. Object handles are linear-memory addresses. + +Async completions and callback-interface methods are JavaScript functions +installed in the module's function table. The glue uses +`WebAssembly.Function` where the engine provides it and otherwise compiles a +tiny module that imports the function with the needed signature and +re-exports it, so no experimental flag is needed. -Construct values as object literals and branch on `tag`; TypeScript -narrows the union automatically: - -```js -const circle = { tag: 'Circle', radius: 2.0 }; -console.log(api.shapes.describe(circle)); -const bigger = api.shapes.scale(circle, 3.0); // a fresh Shape object -``` - -Values are plain JavaScript data with nothing to free. A variant field -of interface type follows the object rule: `_clone()` on the way in, a -fresh owning wrapper on the way out. - -## 64-bit integers and floats - -`i64` and `u64` are `bigint` everywhere: as parameters, returns, record -fields, and callback returns (`onMessage(...): bigint`). The value-buffer -codec reads them with `getBigInt64`/`getBigUint64` and writes them with -`BigInt(v)`, so a `number` is accepted on input but the full 64-bit range -only round-trips as `bigint`. From the `codec` sample's declarations: - -```typescript -i64_value: bigint; -u64_value: bigint; -some_i64: bigint | null; -by_name: Record; -``` - -Floats cross as IEEE-754 bit patterns (`setFloat32`/`setFloat64` in -the codec, native `f32`/`f64` params at the boundary), so `NaN`, -`Infinity`, `-Infinity`, and `-0` are preserved; the codec sample's -conformance consumer checks each of them. - -## Async support - -Async IDL functions return real `Promise`s. The loader grows the -module's `__indirect_function_table` and registers one JavaScript -trampoline per completion-callback signature using the -[JS Type Reflection API](https://github.com/WebAssembly/js-types) -(`new WebAssembly.Function(...)`); each call stores its -`resolve`/`reject` pair in a context map keyed by an integer id. From the -`kvstore` sample: - -```js -compact() { - return new Promise((resolve, reject) => { - const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject, mkErr: _kvErrorFrom }); - try { - wasm.weaveffi_kv_Store_compact_async(_borrow(this), 0, _cbPtr_i32_i32_i64, ctxId); - } catch (e) { - _asyncContexts.delete(ctxId); - reject(_trapError(e)); - } - }); -} -``` - -```js -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); - } -} -``` - -When the producer invokes the completion callback, the trampoline looks -up the context, settles the promise, and removes the entry. A callable -with `throws: true` stores the module's typed error mapper in the -context (`mkErr`), so the rejection carries the domain error; a -non-throwing async callable rejects with the generic `WeaveFFIError` -for panics, marshalling failures, and callback failures. A result of -object type resolves to a fresh owning wrapper; a buffered result is -decoded into a plain object. - -Two caveats apply: - -- `WebAssembly.Function` requires a runtime with JS Type Reflection - (recent V8/SpiderMonkey; Chrome, Firefox, Deno, and Node with - `--experimental-wasm-type-reflection`). -- The module is single-threaded. The default `weaveffi-abi` spawner - drives the future inline on `wasm32`, so the completion callback has - already fired by the time the launcher returns and the `Promise` is - settled before you `await` it. A producer that installs a spawner - which defers to another thread cannot run on - `wasm32-unknown-unknown`. - -A `cancellable` function's ABI symbol takes a `weaveffi_cancel_token*` -parameter; the loader passes a null token (the `0` in `compact` above), -so cancellation isn't surfaced on this target. An IDL function that -models cancellation itself is exposed as a plain function in the same -namespace. - -## Iterators - -`iter` returns are lazy: the wrapper launches the producer iterator -and hands back a `_WeaveFFIIterator` implementing the JS iterator -protocol over the iterator handle. Nothing is drained; each `next()` -issues exactly one producer `next` call through a per-element slot -staged in linear memory. From the `kvstore` sample: - -```js -listKeys(prefix) { - /* ... stage the `string?` prefix in a value buffer ... */ - 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))); -} -``` - -The class settles the handle's lifecycle exactly once: `_close()` -destroys the producer iterator, frees the element slot, and zeroes the -handle. It runs eagerly on exhaustion, on a `next` error or trap, or from -`return()` when iteration stops early; a `for...of` loop calls -`return()` automatically on `break` or `throw`. There is no -finalization backstop for iterators (unlike objects), so abandoning one -without exhausting or closing it leaks the producer handle. - -Each decoded element is copied out of linear memory and its producer -allocation released (`_takeCStr` frees strings via -`weaveffi_free_string`); an element of object type is adopted into a -fresh owning wrapper. Errors from the launcher and from each `next` -follow the function's error strategy: a throwing function such as -`Store.listKeys` checks each step with the domain checker and throws the -typed `KvError` subclasses; a non-throwing one throws the generic -`WeaveFFIError` for producer bugs. The TypeScript declaration is -`IterableIterator`. - -## Emscripten mode - -The default loader fetches a bare `.wasm` and calls -`WebAssembly.instantiate` with an empty import object, which only works -for `wasm32-unknown-unknown` builds. A C or C++ library compiled with -Emscripten needs its own JS runtime, its own import object, and exposes -exports as `Module['_name']` rather than `instance.exports.name`. Set -`emscripten` to generate a loader for that layout: - -```toml -# weaveffi.toml -[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: - -```javascript -import Module from './your_library.js'; -import { loadWeaveffiWasm } from './weaveffi_wasm.js'; - -const api = await loadWeaveffiWasm(Module({ locateFile: (p) => 'build/' + p })); -``` - -Internally the loader binds the module's underscore-prefixed exports to -the symbol names the glue calls, once, up front: - -```javascript -const wasm = { - // Emscripten replaces HEAPU8 when linear memory grows, so the - // buffer is re-read on every access instead of captured once. - get memory() { return { buffer: m['HEAPU8'].buffer }; }, - weaveffi_alloc: m['_weaveffi_alloc'], - weaveffi_dealloc: m['_weaveffi_dealloc'], - weaveffi_math_add: m['_weaveffi_math_add'], - // ... -}; -``` - -Everything after that prologue is identical to the standard loader. The -quoted bracket access on the Emscripten module is deliberate: it -survives Closure Compiler's advanced property renaming, while the rest -of the glue keeps consistent dot access on this locally constructed -object, which Closure can rename safely. - -### Building the producer - -The generated header tags every export with `{PREFIX}_API`, which -expands to `__attribute__((used, visibility("default")))` under -Emscripten (the same expansion as `EMSCRIPTEN_KEEPALIVE`), so the -symbols survive dead-code elimination without an `-sEXPORTED_FUNCTIONS` -list. The glue stages arguments through `weaveffi_alloc` / -`weaveffi_dealloc`; the generated `weaveffi.c` provides malloc/free- -backed defaults, so compile it into your library or export your own -implementations. A typical build: - -```bash -emcc your_library.c generated/c/weaveffi.c -Igenerated/c \ - -o your_library.js \ - -sMODULARIZE=1 -sEXPORT_ES6=1 \ - -sEXPORTED_RUNTIME_METHODS=HEAPU8 \ - -sALLOW_MEMORY_GROWTH=1 -``` - -`-sEXPORTED_RUNTIME_METHODS=HEAPU8` is required: the glue reads and -writes linear memory through `Module['HEAPU8']`. - -### Limitations - -Async functions and callback interfaces are not supported in Emscripten -mode. The trampoline registration in the standard loader relies on -`WebAssembly.Function` and a growable `__indirect_function_table`, -neither of which an Emscripten module exposes portably. By default -`weaveffi generate` fails when the IDL uses either feature; with -`allow_unsupported = true` each affected entry point becomes an explicit -stub that throws at call time and is omitted from the TypeScript -declarations. Objects and iterators work normally. +## Type mapping -## Build instructions +The mapping is the Node.js target's: 64-bit integers are `bigint`, `bytes` is +`Uint8Array`, records are plain objects, rich enums are tagged unions, +C-style enums are frozen objects, interfaces are classes with `close()` and +`[Symbol.dispose]()`, optionals are `T | null`, lists are arrays, maps are +`Record`, and `iter` is a lazy `IterableIterator`. -macOS / Linux / Windows (cross-compilation, all hosts): +## Async and cancellation -```bash -rustup target add wasm32-unknown-unknown -cargo build --target wasm32-unknown-unknown --release -p your_library -``` +Async functions return a `Promise`, and cancellable ones take an optional +`{ signal }` last argument, exactly as on Node.js. The module is +single-threaded and the default executor runs a future to completion inside +its launcher, so an async call has usually completed by the time the +`Promise` is returned; only a signal aborted before the call can cancel it. +A cancelled call rejects with `CancelledError` (code -5). -The resulting `.wasm` is in `target/wasm32-unknown-unknown/release/`. -Serve it over HTTP and load it with the generated helper: +## Callbacks and errors -```html - -``` +A callback runs only while a call into the module is on the stack, on the +calling thread. An implementation that throws fails the call with code -4, +as on Node.js. Because `wasm32-unknown-unknown` aborts on a panic, a producer +panic traps the module; the trap surfaces as the package's root error with +code -2, and the module may be unusable afterward. -In Node there is no `file://` fetch; pass the bytes instead: +## Emscripten -```bash -node --experimental-wasm-type-reflection consumer.mjs -``` +With `emscripten = true` under `[generators.wasm]`, `init` instead takes an +initialized Emscripten module (or the promise its `MODULARIZE` factory +returns), binds its underscore-prefixed exports, reads memory through +`HEAPU8`, and installs table functions with `addFunction`. Link the module +with `-sALLOW_TABLE_GROWTH`, `-sWASM_BIGINT`, and +`-sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8`, and export the library's C +symbols, including `{prefix}_alloc` and `{prefix}_dealloc`. The packaged +layout then ships glue only. -```javascript -import { readFile } from 'node:fs/promises'; -const api = await loadWeaveffiWasm(await readFile(new URL('./your_library.wasm', import.meta.url))); +```ts +export declare function init(module: object | Promise): Promise; ``` -## Packaging - -`weaveffi package --target wasm` assembles an npm package under `wasm/` -containing the ES-module loader, its `.d.ts`, a README, and the -`wasm32` binary copied in as `.wasm`. `package.json` lists -exactly those files so `npm publish` ships nothing else. Only the -`wasm32` platform slice is consulted; with no `wasm32/` binary in -`--binaries` the target is skipped with a note, and in Emscripten mode -the package is glue only (the consumer links the module into their own -Emscripten build). Build the slice with -`cargo build --release --target wasm32-unknown-unknown` (the producer -crate needs `crate-type = ["cdylib"]`) and place it under -`/wasm32/`. See the -[packaging guide](../guides/packaging.md) for the full flow. - -## Memory and ownership - -- The wrapper stages strings, bytes, and value buffers into linear - memory with the exported `weaveffi_alloc` / `weaveffi_dealloc` and - releases them after the call (in a `finally`, so a trap can't leak - them); you don't manage buffers for typed calls. -- Producer-owned returns (strings, bytes, and value buffers) are copied - or decoded into JavaScript values and freed via - `weaveffi_free_string` / `weaveffi_free_bytes` inside the wrapper. -- Records, rich enums, optionals, lists, and maps are plain JavaScript - values with nothing to free. Object wrappers own one producer - reference each: `close()` them (or bind with `using`), and let the - `FinalizationRegistry` backstop catch what you miss. -- Callback implementations are pinned in a `Map` until the producer's - `free` runs; object arguments handed to them are owned by the - implementation. -- Error slots are allocated, checked, and cleared internally; failures - surface as thrown `Error`s with the producer's code and message. -- When you bypass the typed surface via `_raw`, the conventions at the - top of `weaveffi_wasm.js` apply and every alloc must be paired with a - dealloc. - ## Known limitations -- **Single-threaded delivery.** Callbacks and async completions fire - only while a call into the module is on the stack. Producers that - spawn threads or install a deferring spawner can't run on - `wasm32-unknown-unknown`. -- **Panics don't unwind.** A producer panic traps; the glue reports - `-2` (or `-4` after a callback failure), but producer frames aren't - unwound, so state the producer was mutating may be left as is. -- **`WebAssembly.Function` is required** for async functions and - callback interfaces (Node needs `--experimental-wasm-type-reflection`). -- **No cancellation.** Cancel tokens are passed as null. -- **Iterators have no finalizer.** Exhaust or `return()` every - iterator; abandoning one leaks the producer handle. -- **Emscripten mode** supports objects and iterators but not async - functions or callback interfaces. - -## Troubleshooting - -- **`WebAssembly.Function is not a constructor`**: the runtime lacks - JS Type Reflection. Use a current Chrome/Firefox/Deno, run Node with - `--experimental-wasm-type-reflection`, or avoid async functions and - callback interfaces for this target. -- **`WeaveFFI ABI mismatch`** at load: the `.wasm` was built against a - different `weaveffi-abi` revision than the glue; regenerate and - rebuild together. -- **`WeaveFFIError: WeaveFFI error -3: expected a live object wrapper`**: - a wrapper was used (as receiver or argument) after `close()`. -- **`LinkError: import object field 'env' is not a Function`**: the - loader instantiates with an empty imports object. If your Rust crate - imports host functions, extend `loadWeaveffiWasm` to pass them in. - If the module was built with Emscripten, use - [Emscripten mode](#emscripten-mode) instead. -- **`WeaveFFI error -2: producer panicked`**: the producer hit a panic - (on `wasm32` often `SystemTime::now()` or another API the target has - no host for); the message carries the engine's trap text. -- **A callback never fires**: delivery is synchronous, so callbacks run - only during one of your calls into the module. There is no background - delivery on this target. -- **Out-of-memory after many `_raw` calls**: every pointer returned - from the module must be deallocated; the typed wrappers do this for - you, raw calls do not. -- **The `.wasm` file fails to instantiate**: the build artifact must - be `wasm32-unknown-unknown`. `wasm32-wasi` modules require WASI - imports and cannot run in the browser without a polyfill. +- The module must export its function table and allow it to grow. +- An async call can't be cancelled once launched, since it completes before + its launcher returns. +- A producer panic traps rather than unwinding, which can leave the module's + state inconsistent. +- Emscripten mode isn't covered by the conformance suite. diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index a24ba3b0..ffa2ce35 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -1,326 +1,287 @@ # Getting Started -This guide walks you through installing WeaveFFI, defining an API as a -language-neutral IDL, generating multi-language bindings from it, implementing -the native library behind the generated C ABI, and calling it from C. +This guide builds a small Rust library, generates bindings for it, and calls +it from Python and from C. A second, shorter path at the end shows how to +start from an IDL instead and implement the generated C header in C. -WeaveFFI works with any native library that exposes a C ABI, so the producer -can be written in Rust, C, C++, Zig, or anything else that can speak C. This -guide implements it in Rust because that's the quickest to set up. If you're -writing a Rust producer, you can also let the `#[weaveffi::module]` macro -generate the C ABI and derive the IDL for you, instead of hand-writing YAML -(see step 2). +## Install the CLI -## Prerequisites - -You need the [Rust toolchain](https://rustup.rs/) (stable channel) to install -the CLI, and for this guide's Rust producer. Verify with: +You need a stable [Rust toolchain](https://rustup.rs/). Install the +`weaveffi` CLI from crates.io (or grab a prebuilt binary with +`cargo binstall weaveffi-cli`): ```bash -rustc --version -cargo --version +cargo install weaveffi-cli +weaveffi --version ``` -The CLI is the only hard requirement. The library you generate bindings for can -be written in any language that exposes a C ABI. - -## 1) Install WeaveFFI +## Write the producer -Install the CLI from crates.io: +Create a library crate, add the `weaveffi` crate, and build it as a C +dynamic library: ```bash -cargo install weaveffi-cli +cargo new --lib mathlib +cd mathlib +cargo add weaveffi ``` -This puts the `weaveffi` binary on your `PATH`. +```toml +# Cargo.toml +[lib] +crate-type = ["cdylib"] +``` -## 2) Define your API as an IDL +Replace `src/lib.rs` with an annotated module. Everything here is safe Rust; +the macro writes the `extern "C"` layer. -Describe the API once in a language-neutral IDL. Create `math.yml` with a -record and a function: +```rust +/// Integer arithmetic and greetings. +#[weaveffi::module] +pub mod math { + /// Errors the math functions report. + #[weaveffi::error] + #[derive(Debug)] + pub enum MathError { + /// Division by zero. + DivisionByZero = 1, + } -```yaml -version: "0.9.0" -modules: - - name: math - structs: - - name: Point - fields: - - { name: x, type: f64 } - - { name: y, type: f64 } - functions: - - name: add - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 -``` + impl std::fmt::Display for MathError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("division by zero") + } + } + + /// Add two integers. + #[weaveffi::export] + pub fn add(a: i32, b: i32) -> i32 { + a + b + } -The IDL describes only the API. It supports primitives (`i32`, `f64`, `bool`, -`string`, `bytes`, and the rest of the fixed-width integers and floats), -optionals (`string?`), lists (`[i32]`), maps (`{string:i64}`), lazy iterators -(`iter`), records and rich enums, interfaces (reference-counted -objects with constructors, methods, and statics), callback interfaces -(methods the consumer implements and the native library calls), async -functions, and typed error domains (opt in per function with `throws: true`). -See the [IDL Schema](reference/idl.md) reference for the full specification -and the [C ABI Contract](reference/abi.md) for how each of them crosses the -boundary. - -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: + /// Divide `a` by `b`, failing when `b` is zero. + #[weaveffi::export] + pub fn div(a: i32, b: i32) -> Result { + a.checked_div(b).ok_or(MathError::DivisionByZero) + } -```toml -[package] -name = "my-math" -version = "0.1.0" + /// Greet someone by name. + #[weaveffi::export] + pub fn greet(name: &str) -> String { + format!("Hello, {name}!") + } +} + +// Export the runtime symbols (errors, memory, cancel tokens) once per library. +weaveffi::export_runtime!(); ``` -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). +A few things to notice: -> **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]` -> and point the generator straight at the source. The macro emits the C ABI and -> derives the IDL from your code, so you write no `unsafe` glue. See -> [The Rust Producer Macro](guides/producer-macro.md). The rest of this guide -> uses the IDL. +- Every C symbol starts with the crate's library name, so this crate exports + `mathlib_math_add`, `mathlib_math_div`, and `mathlib_math_greet`, plus the + runtime (`mathlib_error_clear`, `mathlib_free_bytes`, and so on). +- `Result` makes `div` a throwing function. The enum's + discriminants are the stable error codes, and its `Display` output is the + message consumers see. +- `weaveffi::export_runtime!()` appears exactly once, at the crate root. -## 3) Generate bindings +The [producer macro guide](guides/producer-macro.md) covers records, enums, +objects, callbacks, iterators, and async functions. -Run the generator to produce bindings for all targets: +## Configure and generate + +Run `weaveffi init` in the crate. It writes a `weaveffi.toml` whose +`[project]` table points at `src/lib.rs`, then lists anything the crate still +needs (a `cdylib` crate type, the `weaveffi` dependency, the +`export_runtime!` call): ```bash -weaveffi generate math.yml -o generated +weaveffi init ``` -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 -├── kotlin/ # Kotlin JNI wrapper + Gradle (build.gradle.kts) project -├── 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 +```toml +# weaveffi.toml +[project] +input = "src/lib.rs" +out = "bindings" +targets = ["c", "python"] # omit to generate all eleven ``` -## 4) Examine the generated output - -### C header (`generated/c/weaveffi.h`) +Build the library and generate the bindings. With a `[project]` table, +`weaveffi generate` needs no arguments and works from any directory in the +project: -Records generate no C functions: a `Point` crosses the ABI serialized as -a [value buffer](reference/value-buffers.md), a single -`(const uint8_t*, size_t)` pair, and the header opens with a comment -block spelling out that convention. What remains is one prototype per -module-level function, each taking an `out_err` parameter for error -reporting: - -```c -/* - * Value buffer convention: records, rich enums, lists, maps, and - * optionals cross the ABI serialized in the WeaveFFI value buffer - * format ... - */ - -// Module: math -WEAVEFFI_API int32_t weaveffi_math_add(int32_t a, int32_t b, weaveffi_error* out_err); +```bash +cargo build +weaveffi generate ``` -The header also declares the fixed runtime surface every producer exports -(`weaveffi_abi_version`, the `weaveffi_error` struct and its helpers, -`weaveffi_free_string`/`weaveffi_free_bytes`, and the cancel-token family); -see the [C ABI Contract](reference/abi.md). Had `math.yml` declared an -interface, the header would also carry an opaque typedef plus `_clone` and -`_destroy` symbols for it, and a callback interface would appear as a vtable -typedef. - -### Swift wrapper (`generated/swift/Sources/MyMath/MyMath.swift`) +The output has one directory per target. The C target writes +`bindings/c/mathlib.h`; the Python target writes an installable package +named `mathlib`. Run `weaveffi generate` again after any change: it rewrites +only the files whose contents changed and removes files a previous run wrote +that are no longer produced. -Structs become plain Swift structs with typed properties, packed and -unpacked from value buffers by the wrapper. Module functions are grouped -under a Swift enum namespace. Because `add` doesn't declare -`throws: true`, its Swift wrapper is a plain non-throwing function: +## Call it from Python -```swift -public struct Point { - public var x: Double - public var y: Double - - public init(x: Double, y: Double) { ... } -} +Install the generated package and point it at the library you built. Every +generated loader honors a `{PREFIX}_LIBRARY` environment variable, here +`MATHLIB_LIBRARY`; a packaged release bundles the library instead (see +[Packaging](guides/packaging.md)). -public enum Math { - public static func add(a: Int32, b: Int32) -> Int32 { ... } -} +```bash +pip install ./bindings/python +export MATHLIB_LIBRARY="$PWD/target/debug/libmathlib.dylib" # .so on Linux ``` -### TypeScript types (`generated/node/types.d.ts`) +```python +import mathlib -Structs become interfaces with mapped types. Functions use the IR name -directly (no module prefix): +print(mathlib.add(2, 3)) # 5 +print(mathlib.greet("Python")) # Hello, Python! -```typescript -export interface Point { - x: number; - y: number; -} - -// module math -export function add(a: number, b: number): number +try: + mathlib.div(1, 0) +except mathlib.MathError as e: + print("caught:", e) # caught: division by zero ``` -## 5) Implement the library behind the C ABI +On import the package checks the library's ABI revision and the contract +checksum of the `math` module, so a library built from different source +fails to load with an error naming the module. The +[Python page](generators/python.md) documents the generated surface. -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 and write the one `#[no_mangle] extern "C"` function the header declares -by hand. +## Call it from C -Create a library crate, add the WeaveFFI ABI helpers, and build a `cdylib`: +The header is the contract every other binding is built on. Strings cross as +UTF-8 `(ptr, len)` runs, the caller owns a zeroed `mathlib_error`, and +returned strings are released with `mathlib_free_bytes`: -```bash -cargo new --lib my-math -cd my-math -cargo add weaveffi-abi -``` +```c +#include +#include +#include "mathlib.h" -In `Cargo.toml`: +int main(void) { + if (mathlib_abi_version() != MATHLIB_ABI_VERSION || + mathlib_math_checksum() != MATHLIB_MATH_CHECKSUM) { + fprintf(stderr, "mathlib.h does not match the loaded library\n"); + return 1; + } -```toml -[lib] -crate-type = ["cdylib"] -``` + mathlib_error err = {0}; + printf("2 + 3 = %d\n", mathlib_math_add(2, 3, &err)); -Implementing `add` in `src/lib.rs` looks like this: + mathlib_math_div(1, 0, &err); + if (err.code == mathlib_math_MathError_DivisionByZero) { + printf("error %d: %s\n", err.code, err.message); + mathlib_error_clear(&err); + } -```rust -#![allow(unsafe_code)] -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -use weaveffi_abi::{self as abi, weaveffi_error}; - -#[no_mangle] -pub extern "C" fn weaveffi_math_add( - a: i32, - b: i32, - out_err: *mut weaveffi_error, -) -> i32 { - abi::error_set_ok(out_err); - a + b + const char* name = "C"; + size_t len = 0; + const uint8_t* text = + mathlib_math_greet((const uint8_t*)name, strlen(name), &len, &err); + printf("%.*s\n", (int)len, (const char*)text); + mathlib_free_bytes((uint8_t*)text, len); + return 0; } +``` -// Emit the fixed WeaveFFI C ABI runtime surface (abi_version, error_set, -// error_clear, error_free, free_string, free_bytes, cancel_token_*) in one -// line. Call this exactly once per cdylib. -abi::export_runtime!(); +```bash +cc -I bindings/c main.c -L target/debug -lmathlib -o main +DYLD_LIBRARY_PATH=target/debug ./main # LD_LIBRARY_PATH on Linux ``` -Key points: - -- Every exported function uses `#[no_mangle]` and `extern "C"`. -- `out_err` must always be cleared on success with `abi::error_set_ok`. -- On error, call `abi::error_set(out_err, code, message)` and return a - zero/null value. -- 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 reports ABI revision 2 and - 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 - revision. -- An interface in your IDL adds a `_clone` and `_destroy` pair you implement - with `weaveffi_abi::object_clone` and `object_destroy` over an `Arc`; - the [C ABI Contract](reference/abi.md#objects-interfaces) spells out the - reference-counting rules. The `#[weaveffi::module]` macro writes all of - this for you. - -[export-runtime-doc]: https://docs.rs/weaveffi-abi/latest/weaveffi_abi/macro.export_runtime.html - -> **Tip for Rust producers:** the `#[weaveffi::module]` macro generates these -> `#[no_mangle] extern "C"` thunks for you from safe Rust, so you never fill in -> stubs by hand. See [The Rust Producer Macro](guides/producer-macro.md). - -Build with: +[Errors and Memory](guides/errors-and-memory.md) states every ownership rule +the generated bindings follow for you. + +## Implementing an IDL in C + +A producer doesn't have to be Rust. Start from an IDL, generate the C header, +and implement it in any language that can export C symbols. Outside a Rust +crate, `weaveffi init` writes a starter IDL and a `weaveffi.toml`: ```bash -cargo build +mkdir greeter && cd greeter +weaveffi init # writes greeter.yml and weaveffi.toml ``` -This produces a shared library (`libmy_math.dylib` on macOS, -`libmy_math.so` on Linux, `my_math.dll` on Windows). The exported symbols match -`generated/c/weaveffi.h` by construction. +Edit `greeter.yml` to declare the API (the [IDL reference](reference/idl.md) +has the full schema): -## 6) Build and test with C +```yaml +version: "0.10.0" +modules: + - name: greeter + functions: + - name: add + params: + - { name: a, type: i32 } + - { name: b, type: i32 } + return: i32 +``` -Write a small C program that calls your library: +For an IDL input the identity comes from `[package]` in `weaveffi.toml` +(`name = "greeter"`), so the prefix and the library are both `greeter`. +Generate the header and implement it: -**main.c:** +```bash +weaveffi generate --target c # writes bindings/c/greeter.h +``` ```c -#include -#include "weaveffi.h" +/* greeter.c */ +#include +#include +#include "greeter.h" -int main(void) { - struct weaveffi_error err = {0}; +uint32_t greeter_abi_version(void) { return GREETER_ABI_VERSION; } +uint64_t greeter_greeter_checksum(void) { return GREETER_GREETER_CHECKSUM; } - int32_t sum = weaveffi_math_add(3, 4, &err); - if (err.code) { - printf("error: %s\n", err.message); - weaveffi_error_clear(&err); - return 1; - } - printf("add(3, 4) = %d\n", sum); - - return 0; +int32_t greeter_greeter_add(int32_t a, int32_t b, greeter_error* out_err) { + (void)out_err; /* written only on failure */ + return a + b; } -``` -Compile, link, and run: +void greeter_error_set(greeter_error* err, int32_t code, const char* message) { + greeter_error_clear(err); + err->code = code; + err->message = message ? strdup(message) : NULL; +} -```bash -# macOS -cc -I generated/c main.c -L target/debug -lmy_math -o my_example -DYLD_LIBRARY_PATH=target/debug ./my_example +void greeter_error_clear(greeter_error* err) { + free((void*)err->message); + free((void*)err->payload_ptr); + memset(err, 0, sizeof *err); +} -# Linux -cc -I generated/c main.c -L target/debug -lmy_math -o my_example -LD_LIBRARY_PATH=target/debug ./my_example -``` +void greeter_error_free(greeter_error* err) { + if (err) { greeter_error_clear(err); free(err); } +} -Expected output: +void greeter_free_bytes(uint8_t* ptr, size_t len) { (void)len; free(ptr); } -```text -add(3, 4) = 7 +/* Also required: the four greeter_cancel_token_* functions and + greeter_debug_live (which may return 0). */ ``` +The library must export every runtime symbol the header declares, each +top-level module's checksum function, and the API itself; the +[C ABI contract](reference/abi.md#runtime-surface) lists them, and +[`conformance/c/producer.c`](https://github.com/weavefoundry/weaveffi/blob/main/conformance/c/producer.c) +is a complete hand-written producer to copy from. Build it as +`libgreeter.so` (`libgreeter.dylib`, `greeter.dll`), then run +`weaveffi generate` for the other targets; they load it exactly as they load +a Rust producer. + ## Next steps -- Read the [IDL Schema](reference/idl.md) reference for all supported types - and features, and the [C ABI Contract](reference/abi.md) for how objects, - callback interfaces, value buffers, async functions, and iterators cross - the boundary. -- Writing a Rust producer? See - [The Rust Producer Macro](guides/producer-macro.md) to generate the C ABI - directly from annotated Rust instead of implementing the header by hand. -- Look at the [samples](samples.md): `kvstore` exercises every IDL feature, - and `events` is the smallest example of a reference-counted object plus a - callback interface. -- 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, 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). +- [Project Configuration](guides/config.md): `[package]` metadata, per-target + options, and the generation cache. +- [Samples](samples.md): six complete producers, from `calculator` to the + kitchen-sink `kvstore`. +- [Generators](generators/README.md): what each language gets. +- Gate CI on `weaveffi diff --check` so committed bindings can't drift; see + [Stability and Versioning](stability.md#ci-workflow). diff --git a/docs/src/guides/README.md b/docs/src/guides/README.md deleted file mode 100644 index 46ac3c9e..00000000 --- a/docs/src/guides/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Guides - -Practical guides for working with WeaveFFI bindings across targets. - -- [The Rust Producer Macro](producer-macro.md): write safe Rust, annotate it with `#[weaveffi::module]`, and let the `weaveffi` crate generate the C ABI glue. -- [Memory Ownership](memory.md): allocation rules; freeing strings, bytes, structs, and errors across the FFI boundary. -- [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. -- [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 ae585c4f..a92824b4 100644 --- a/docs/src/guides/async.md +++ b/docs/src/guides/async.md @@ -1,603 +1,196 @@ -# Async Functions +# Async and Cancellation -## Overview +An async function returns immediately and delivers its result later, through +the language's own async idiom: `async`/`await` in Swift, Python, JavaScript, +C#, and Dart, a `suspend fun` in Kotlin, a `std::future` in C++, and a +blocking call that honors a `context.Context` in Go. A cancellable async +function is also cancelled through that idiom: cancelling the awaiting task, +coroutine, or promise cancels the native call. -WeaveFFI exposes asynchronous Rust operations through a single -callback-based C ABI and language-native async wrappers in every target. Mark -a function with `async: true` (and optionally `cancellable: true`) in the -IDL, or write a plain `async fn` under `#[weaveffi::module]`, and the -generators emit the right shape per target: `async` in Swift (`async throws` -when the function also declares `throws: true`), `suspend fun` in Kotlin, -`Promise` in JS, `async def` in Python, `Task` in .NET, and so on. -When an async function declares `throws: true`, the failure that settles the -future is the module's typed domain error (see the -[Error Handling Guide](errors.md)). +## Declaring async functions -The completion contract every wrapper implements is stated once, in -`weaveffi_core::plan::AsyncProtocol`: the callback fires exactly once per -launch, from an arbitrary producer thread, and everything it receives (the -error and the result) is owned by the consumer, which releases it through the -runtime free symbols or adopts it. See -[Result ownership and threading](#result-ownership-and-threading) below. +In Rust, write an `async fn` (a free function, method, or static). Add +`#[weaveffi::cancellable]` and a final `weaveffi::CancelToken` parameter to +make it cancellable: -On the Rust side, something has to drive the future between the launcher -returning and the callback firing. That is the **spawner**: a process-wide -executor hook with a dependency-free default, replaceable once at startup -with `weaveffi::set_spawner` so producers built on Tokio (or any other -runtime) can run their futures where their I/O drivers live. - -## When to use - -Use async functions for: - -- I/O-bound work (network, disk, database). -- Long-running operations that should not block the consumer's event loop - (UI threads, JS event loop, asyncio loop). -- Operations the consumer should be able to cancel (combine with - `cancellable: true`). - -Avoid async for: - -- Short CPU-bound work (math, parsing, validation). The callback overhead is - more expensive than the call itself. -- Functions whose Rust implementation is purely synchronous and finishes in - microseconds. - -## Step-by-step +```rust +#[weaveffi::interface] +pub struct Store { /* ... */ } + +impl Store { + /// Reclaim space; returns the bytes reclaimed. + #[weaveffi::cancellable] + pub async fn compact(&self, cancel: weaveffi::CancelToken) -> Result { + let _ = cancel; + /* ... */ + Ok(0) + } +} +``` -### 1. Declare the function in the IDL +In an IDL, set `async: true` and optionally `cancellable: true` on a +function, method, or static: ```yaml -version: "0.9.0" +version: "0.10.0" modules: - - name: net + - name: kv errors: - name: NetError + name: KvError codes: - - { name: Unreachable, code: 1, message: "host unreachable" } - - { name: Cancelled, code: 2, message: "the operation was cancelled" } - functions: - - name: fetch_data - params: - - { name: url, type: string } - return: string - async: true - throws: true - doc: "Fetches data from the given URL" - + - { name: IoError, code: 1004, message: "I/O failure" } interfaces: - - name: Pool + - name: Store constructors: - - name: new + - name: open params: - - { name: url, type: string } - methods: - - name: connect - params: [] - return: Conn - async: true - cancellable: true + - { name: path, type: string } throws: true - - name: cached - params: [] - return: Conn? - async: true - - - name: Conn methods: - - name: id - params: [] + - name: compact return: i64 + throws: true + async: true + cancellable: true ``` -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `async` | bool | `false` | Mark the function as asynchronous | -| `cancellable` | bool | `false` | Allow the async operation to be cancelled | -| `throws` | bool | `false` | Deliver failures as the module's typed domain error | - -Here `fetch_data` and `connect` fail with a typed `NetError`, while `cached` -is non-throwing: the only failures it can surface are producer bugs. Async -works on free functions, interface methods, and statics alike; interface -constructors are always synchronous. An async function may return any -by-value type, a string, bytes, a buffered type (record, rich enum, list, map, -optional), an object (`Conn`), or a nullable object (`Conn?`); the one shape -the validator rejects is an async `iter` (return a list instead). - -### 2. Implement it in Rust - -With the `#[weaveffi::module]` macro you write plain `async fn`s. The snippet -below implements the IDL above and installs Tokio as the spawner; it compiles -against `weaveffi` plus -`tokio = { version = "1", features = ["rt-multi-thread", "time"] }`: - -```rust -#[weaveffi::module] -pub mod net { - use std::sync::Arc; - - /// The network error domain. - #[weaveffi::error] - #[derive(Debug)] - pub enum NetError { - /// host unreachable - Unreachable = 1, - /// the operation was cancelled - Cancelled = 2, - } - - /// Fetch a URL on the installed spawner. - #[weaveffi::export] - pub async fn fetch_data(url: String) -> Result { - tokio::time::sleep(std::time::Duration::from_millis(1)).await; - Ok(format!("payload from {url}")) - } - - /// Install the Tokio runtime as the spawner. Call once, before the - /// first async export is launched. - #[weaveffi::export] - pub fn init() -> bool { - static RUNTIME: std::sync::OnceLock = std::sync::OnceLock::new(); - let rt = RUNTIME.get_or_init(|| { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("tokio runtime") - }); - let handle = rt.handle().clone(); - weaveffi::set_spawner(move |fut: weaveffi::BoxFuture| { - handle.spawn(fut); - }) - .is_ok() - } - - /// A connection pool. - #[weaveffi::interface] - pub struct Pool { - url: String, - } - - /// One pooled connection. - #[weaveffi::interface] - pub struct Conn { - pool: Arc, - id: i64, - } - - impl Pool { - /// Open a pool for `url`. - pub fn new(url: String) -> Pool { - Pool { url } - } - - /// Lease a connection. The returned object holds a reference to - /// the pool, so the pool outlives every connection. - #[weaveffi::cancellable] - pub async fn connect(self: Arc, cancel: weaveffi::CancelToken) -> Result, NetError> { - for _ in 0..10 { - if cancel.is_cancelled() { - return Err(NetError::Cancelled); - } - tokio::time::sleep(std::time::Duration::from_millis(1)).await; - } - Ok(Arc::new(Conn { pool: self, id: 1 })) - } - - /// Look up a cached connection, if any. - pub async fn cached(&self) -> Option> { - let _ = &self.url; - None - } - } - - impl Conn { - /// The connection id. - pub fn id(&self) -> i64 { - self.id - } - } -} - -weaveffi::export_runtime!(); -``` - -What the macro does with each piece: - -- **Inputs are owned before the spawn.** The foreign caller may free or reuse - its argument buffers as soon as the launcher returns, so strings and bytes - are copied, value buffers are copied raw (and decoded inside the future), - objects are retained with a new strong reference, and callback interfaces - are lifted into their `Arc` on the caller's thread. A `&str` or - `&T` spelling is then satisfied by lending the owned value. -- **The receiver is retained for the life of the call.** An async method - takes one strong reference to its object before spawning, whether you wrote - `&self` or `self: Arc`, so the consumer may drop its own wrapper the - moment the launcher returns and the future still has a live object. Write - `self: Arc` when the future needs to move the object into something - it returns or stores (as `connect` does). -- **Object results transfer one strong reference.** `Arc` and - `Option>` lower exactly like their synchronous counterparts: the - callback's `result` slot is a `{tag}*` the consumer adopts and eventually - releases with `weaveffi_net_Conn_destroy`; `None` is null. -- **`#[weaveffi::cancellable]` adds the token.** The attribute is required; - the macro does not infer it from the parameter type. The producer declares - a trailing `weaveffi::CancelToken` parameter that does not appear in the - IDL signature, and the launcher fills it from its `cancel_token` slot. -- **Panics are caught.** The future runs under `weaveffi::abi::CatchUnwind`, - so a panic (or a consumer callback failure) is delivered through the - callback rather than into the executor. See - [Panic handling](#panic-handling). - -A hand-written producer implements the same pattern directly against -`weaveffi-abi`; the launcher symbol always carries the `_async` suffix -(`weaveffi_net_fetch_data_async`), keeping the plain name free for a -possible synchronous variant: - -```rust -#![allow(unsafe_code)] -#![allow(non_camel_case_types)] -#![allow(clippy::not_unsafe_ptr_arg_deref)] - -use std::ffi::c_void; -use std::os::raw::c_char; -use weaveffi_abi::{self as abi, weaveffi_error}; - -pub type weaveffi_net_fetch_data_callback = - extern "C" fn(context: *mut c_void, err: *mut weaveffi_error, result: *const c_char); - -#[no_mangle] -pub extern "C" fn weaveffi_net_fetch_data_async( - url: *const c_char, - callback: weaveffi_net_fetch_data_callback, - context: *mut c_void, -) { - let url_str = abi::c_ptr_to_string(url).unwrap_or_default(); - let ctx = context as usize; - abi::spawn(async move { - let payload = abi::string_to_c_ptr(&format!("payload from {url_str}")); - // Ownership of the string transfers to the consumer, which - // releases it with `weaveffi_free_string` after copying. - callback(ctx as *mut c_void, std::ptr::null_mut(), payload); - }); -} -``` - -### 3. Choose a spawner - -`weaveffi::abi::spawn` routes every async export's future to the installed -`Spawner`, or to the default when none is installed: - -| | Behaviour | -|-|-----------| -| **Default** (nothing installed) | Each launch spawns a fresh OS thread and drives the future to completion there with `weaveffi::abi::block_on`, a minimal park/unpark executor. No runtime dependency; fine for CPU-bound futures and for futures woken from other threads (a channel, a `JoinHandle`). It has **no reactor**, so a future that awaits Tokio's I/O or timers never wakes. | -| **Custom** (`set_spawner`) | Your closure or `Spawner` implementation receives a `BoxFuture` (`Pin + Send + 'static>>`) and schedules it wherever you like. The future is already wrapped so a panic inside it is caught; a spawner never observes an unwinding future. | -| **`wasm32`** | There are no threads, so the default drives the future inline before the launcher returns; a future that returns `Pending` without an external wake would hang the module. See [WebAssembly](#webassembly). | - -`set_spawner` is first-one-wins: it returns `Err(SpawnerAlreadySet)` on the -second call and never replaces an installed spawner. Install it from an -initialization export (as `init` above does), a library constructor, or the -first constructor of your main interface, before the first async export is -launched. `Spawner` is implemented for every -`Fn(BoxFuture) + Send + Sync + 'static`, so a closure forwarding to a -`tokio::runtime::Handle`, an `async_std::task::spawn`, or a `smol::Executor` -is enough. The trait's one rule: `spawn` must not block, because the launcher -that calls it runs on the consumer's thread. - -### 4. Call it from each target - -Swift: - -```swift -let payload = try await Net.fetchData(url: "https://example.com/data") -let conn = try await pool.connect() -``` - -Kotlin: - -```kotlin -val payload = Net.fetchData("https://example.com/data") -val conn = pool.connect() -``` - -Node.js: - -```typescript -const payload = await fetchData("https://example.com/data"); -const conn = await pool.connect(); -``` - -Python: - -```python -payload = await fetch_data("https://example.com/data") -conn = await pool.connect() -``` - -.NET: - -```csharp -var payload = await Net.FetchData("https://example.com/data"); -var conn = await pool.Connect(); -``` - -Dart: - -```dart -final payload = await fetchData('https://example.com/data'); -final conn = await pool.connect(); -``` - -Because `fetch_data` declares `throws: true`, the error that rejects the -promise (or resumes the continuation, or fails the task) is the typed -`NetError`, so a Swift consumer writes `catch NetError.unreachable` and a -Python consumer writes `except Unreachable`. An object result arrives as a -normal wrapper that owns one reference and releases it through the target's -disposal idiom. +`async` composes with `throws`, with object and buffered results, and with +every parameter type. Constructors can't be async (use an async static that +returns the interface), async functions can't return iterators, and +`cancellable` without `async` is a validation error (`CancellableNotAsync`). -### 5. Cancel a running operation +## The C ABI shape -For `cancellable: true` functions the C launcher gains a -`weaveffi_cancel_token*` slot (before `callback` and `context`), and the -`weaveffi-abi` runtime provides the token lifecycle: +An async function lowers to a launcher named after the function itself and a +completion callback typedef named `{symbol}_callback`: ```c -weaveffi_cancel_token* token = weaveffi_cancel_token_create(); -weaveffi_net_Pool_connect_async(pool, token, on_done, ctx); -/* later, from any thread: */ -weaveffi_cancel_token_cancel(token); -/* after on_done has fired: */ -weaveffi_cancel_token_destroy(token); +typedef void (*kvstore_kv_Store_compact_callback)( + void* context, kvstore_error* err, int64_t result); + +void kvstore_kv_Store_compact( + const kvstore_kv_Store* self, + kvstore_cancel_token* cancel_token, /* cancellable functions only */ + kvstore_kv_Store_compact_callback callback, + void* context); ``` -The Rust worker polls `cancel.is_cancelled()` (the `CancelToken` wraps -`weaveffi_cancel_token_is_cancelled`) at safe points and stops early, but the -callback is **always** invoked exactly once: either with the result or with -whatever error the producer chose to report for cancellation (`connect` above -returns `NetError::Cancelled`; the runtime has no reserved code for it). The -pin/unpin pair (see Reference) runs on the cancellation path identically to -the success path. A null token reads as "never cancelled", which is what the -wrappers that don't expose cancellation pass. +The result slots are nothing (a `void` function), one direct value or object +pointer named `result`, or `const uint8_t* result_ptr, size_t result_len` for +strings, bytes, and value buffers. The launcher has no `out_err`: every +failure, including a marshalling failure of an input, arrives through the +callback. -Today the **C and C++** surfaces expose the token (C++ as a trailing -`cancel_token = nullptr` parameter); the other wrappers pass null. The -operation runs to completion even if the consumer-side future is abandoned. +## The completion contract -## Reference +The runtime guarantees these for every launch: -### C ABI shape +1. **Exactly once.** The callback fires exactly once, on success, on a + domain error, on a marshalling failure (`-3`), on a panic (`-2`), on a + consumer callback failure (`-4`), and on cancellation (`-5`). +2. **From any thread.** It runs on whatever thread completes the future: an + executor thread, a Tokio worker, or (on `wasm32`) the caller's own stack. + Each binding hops back to its own scheduler before touching consumer state. +3. **Owned results.** `err` is `NULL` on success; otherwise it's heap-boxed, + owned by the consumer, and released with `{prefix}_error_free`. A string, + bytes, or buffered result is owned by the consumer and released with + `{prefix}_free_bytes(result_ptr, result_len)` after decoding. An object + result transfers one strong reference. +4. **Dropped futures complete.** If the executor drops a future before it + finishes (a runtime shutting down, say), the drop guard completes the call + with `-5`. -Each async function gets its own callback typedef of the form -`(context, err, )`, and a launcher with the `_async` suffix: +A consumer of the raw C surface follows the same rules in its callback: read +or decode everything, free it, and adopt object pointers. -```c -typedef void (*weaveffi_net_fetch_data_callback)( - void* context, - weaveffi_error* err, - const char* result); - -void weaveffi_net_fetch_data_async( - const char* url, - weaveffi_net_fetch_data_callback callback, - void* context); -``` - -The `err` argument of the callback carries the domain code for a -`throws: true` function; on a non-throwing function a non-zero code only ever -reports a producer bug (see -[Throws versus Trap](errors.md#throws-versus-trap)). +## Cancellation -An async method takes its receiver first, and an object result is a `{tag}*` -the callback adopts: +The runtime surface provides reference-counted cancel tokens: ```c -typedef void (*weaveffi_net_Pool_connect_callback)( - void* context, - weaveffi_error* err, - weaveffi_net_Conn* result); - -void weaveffi_net_Pool_connect_async( - const weaveffi_net_Pool* self, - weaveffi_cancel_token* cancel_token, - weaveffi_net_Pool_connect_callback callback, - void* context); - -weaveffi_cancel_token* weaveffi_cancel_token_create(void); -void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); +kvstore_cancel_token* token = kvstore_cancel_token_create(); /* refcount 1 */ +kvstore_kv_Store_compact(store, token, on_done, ctx); +kvstore_cancel_token_cancel(token); /* any thread, any time; idempotent */ +kvstore_cancel_token_destroy(token); /* drops the consumer's reference */ ``` -### Result ownership and threading - -The completion contract has three clauses, stated once in -`weaveffi_core::plan::AsyncProtocol` and rendered by every wrapper: - -1. **Single completion.** The callback fires exactly once per launch. The - wrapper resolves its native future idiom (a Python `asyncio` future, a JS - `Promise`, a Swift continuation, a C# `TaskCompletionSource`, a Go channel) - exactly once and then releases the registration. This holds on every path: - success, domain error, marshalling failure of an input (there is no - `out_err` on a launcher, so a null string or malformed buffer is reported - through the callback with `-3`), a null receiver, a panic, and - cancellation. -2. **Owned results.** Result buffers passed to the callback (strings, bytes, - and buffered results delivered as a - `(const uint8_t* result_ptr, size_t result_len)` pair holding the - serialized value buffer) are owned by the consumer: the wrapper copies or - decodes them, then releases them through the runtime free symbols - (`weaveffi_free_string` for strings, `weaveffi_free_bytes` for byte and - value buffers). This is what lets runtimes that defer callback bodies past - the native return, such as Dart's `NativeCallable.listener`, decode - safely. Object results (including `Interface?`) transfer one strong - reference the same way: the callback adopts the pointer into the wrapper's - disposal idiom, which eventually calls the type's `_destroy` symbol. -3. **Foreign-thread delivery.** The callback runs on an arbitrary producer - thread (a default-spawner thread, or a Tokio worker), so the wrapper hops - back to its native scheduler before touching consumer state (Python's - `call_soon_threadsafe`, Node's thread-safe function, a dispatched Swift - continuation) rather than resolving inline where the target's runtime - forbids it. - -A non-null error passed to the callback is heap-boxed and also owned by the -consumer: the wrapper copies the code, message, and payload, then releases -the box exactly once with `weaveffi_error_free`. (This differs from the -synchronous `out_err` slot, which is caller-allocated and cleared with -`weaveffi_error_clear`.) - -If you consume the raw C surface directly, the same rules apply to your -callback: copy or decode every buffer and then free it, release a non-null -error with `weaveffi_error_free`, and adopt object pointers. - -### Panic handling - -The generated launcher awaits the producer's future inside -`weaveffi::abi::CatchUnwind`, a future adapter that wraps each `poll` in -`catch_unwind` and resolves to `Err(payload)` instead of unwinding into the -executor. The launcher then reports the payload through the callback's `err` -with `error_set_panic`: - -- an ordinary panic becomes code `-2` (`PANIC_ERROR_CODE`) with the message - `producer panicked: `; -- a `ForeignError` payload (a consumer callback-interface implementation - raised while the future was calling it) becomes code `-4` - (`FOREIGN_ERROR_CODE`) with the consumer's own message. - -Either way the callback fires exactly once and the installed spawner never -sees an unwinding task, so a panicking export can't take down a Tokio worker -or leave a JS `Promise` pending forever. On a `throws: true` function the -consumer sees these as the generic branded error; on a non-throwing function -they trap (see [Throws versus Trap](errors.md#throws-versus-trap)). - -### Per-target async surface - -| Target | Async surface | Cancel token exposure (`cancellable: true`) | -|--------|---------------|---------------------------------------------| -| C | Raw callback + `_async` launcher | `weaveffi_cancel_token*` slot before the callback | -| C++ | `std::future` | trailing `cancel_token = nullptr` parameter | -| Swift | `async` (`async throws` with `throws: true`) | not exposed; wrapper passes `nil` | -| Kotlin | `suspend fun` | not exposed; wrapper passes `0L` | -| Node.js | `Promise` (thread-safe function settling) | not exposed; wrapper passes `NULL` | -| Python | `async def` (asyncio future settled via `call_soon_threadsafe`) | not exposed; wrapper passes `None` | -| .NET | `Task` | not exposed; wrapper passes `IntPtr.Zero` | -| Dart | `Future` (`NativeCallable.listener`) | not exposed; wrapper passes `nullptr` | -| Wasm | `Promise` (table trampolines) | not exposed; wrapper passes `0` | -| Go | blocking bridge (`chan` receive); call from a goroutine | not exposed; wrapper passes `nil` | -| Ruby | blocking bridge (`Queue#pop`); call from a Thread | not exposed; wrapper passes `NULL` | - -A wrapper that does not expose the token still launches and completes the -call correctly; the operation simply runs to completion even if the consumer -abandons the future. Drop to the C surface when you need cooperative -cancellation from one of those targets. - -### WebAssembly - -`wasm32-unknown-unknown` has no threads, so the async story is different on -both sides of the boundary: - -- **Producer.** The default spawner drives the future inline with `block_on` - before the launcher returns, and `set_spawner` has nothing better to offer: - there is no runtime to hand the future to. Futures that resolve after a - bounded amount of computation work; a future that returns `Pending` - waiting for an external wake (a timer, I/O, another thread) parks a thread - that doesn't exist and hangs the module. Keep async exports on wasm to work - that completes synchronously, or gate the awaiting code with - `#[cfg(not(target_arch = "wasm32"))]` as `samples/kvstore` does for its - clock. -- **Consumer.** The generated JavaScript glue installs one long-lived - trampoline per async result shape in the module's function table - (`__indirect_function_table`), registers a per-call `{ resolve, reject }` - context by integer id, and passes that id as `context`. Because the - producer completes inline, the trampoline runs while the launcher call is - still on the stack; the wrapper still returns a real `Promise`, so consumer - code is identical to Node's. Callback interfaces use the same table - mechanism, which is why a producer that calls back from a spawned thread - cannot run on this target at all. -- **Emscripten mode** (`emscripten = true` in `[generators.wasm]`) targets a - pre-initialized Emscripten `Module` that exposes neither - `WebAssembly.Function` nor a portable growable table, so async functions - and callback interfaces are **unsupported** there. `weaveffi generate` - refuses an API that uses them unless `allow_unsupported = true`, in which - case they are emitted as explicit throwing stubs. - -### Pin / unpin matrix - -Every binding pins the user-supplied `void* context` and the callback closure -for the lifetime of the operation, then releases them exactly once on the -callback path. The matrix below is the contract every generator implements; -each row is asserted by that generator's unit tests. - -| Target | Pin (allocate / retain) | Unpin (free / release) on callback | Notes | -|--------|-------------------------|------------------------------------|-------| -| Swift | `Unmanaged.passRetained(ContinuationRef(...))` | `Unmanaged.fromOpaque(ctx).takeRetainedValue()` | The retained `+1` is dropped exactly once when the continuation resumes. | -| .NET | `GCHandle.Alloc(callback, GCHandleType.Normal)` | `GCHandle.FromIntPtr(context).Free()` | The catch path also frees the handle on synchronous failure. | -| Kotlin | JNI `(*env)->NewGlobalRef(env, callback)` | `(*env)->DeleteGlobalRef(env, ctx->callback)` | The JNI shim `malloc`s and `free`s the per-call context exactly once. | -| Node.js | `napi_create_promise(env, &deferred, &promise)` | `napi_resolve_deferred` or `napi_reject_deferred` | The N-API runtime owns the deferred; the per-call context is `malloc`-ed and freed exactly once. | -| Python | `_token = _async_register(_cb)` stores the `ctypes.CFUNCTYPE` trampoline in the module-level `_async_pending` dict | `_async_pending.pop(_token, None)` when the callback fires | The callback settles the `asyncio` future via `loop.call_soon_threadsafe`; no thread blocks waiting. | -| C++ | `new std::promise()` plus the lambda capture | `delete p;` once at the end of the lambda | The lambda owns the heap promise on every exit branch. | -| Dart | `NativeCallable<...>.listener(...)` | `callable.close()` in `finally` and on the catch path | Pointer-typed parameters are kept alive in `whenComplete`. | -| Wasm | one table trampoline per signature plus `_asyncContexts.set(ctxId, ...)` per call | `_asyncContexts.delete(ctxId)` in the trampoline | Per-call resolver closures are removed after resolve/reject. | -| Go | `wvCallbackStore(ch)` registers the channel in a global registry keyed by an integer id | `wvCallbackTake(id)` removes it when the exported trampoline fires | The context crossing C is an integer id, never a Go pointer (cgo rule); the channel is buffered so the producer thread never blocks. | -| Ruby | the `FFI::Function` trampoline is a local kept alive by the enclosing method scope | the blocking `queue.pop` returns only after the callback ran | The wrapper blocks the calling Ruby thread, so the trampoline cannot be collected while the producer can still call it. | - -### Audit invariants - -For every async-capable target: - -1. The `void* context` has exactly one owner at any moment. -2. The callback closure is pinned by an explicit "+1" allocation - (`GCHandle.Alloc`, `Unmanaged.passRetained`, `NewGlobalRef`, - `NativeCallable.listener`, and so on) before the C worker can see it, and - released by the matching "-1" exactly once on the callback path. -3. Synchronous failure of the C call (the callback never fires) is handled in - a `catch` / `try` that frees the pin so it does not leak. -4. The async-demo sample exports `weaveffi_tasks_active_callbacks()` so a - harness can assert the count returns to zero after a burst of concurrent - calls. +The launcher takes its own reference on the token, so the consumer may cancel +and destroy its reference at any point after the launch, even before the +call completes. Cancelling wakes the producer's future; the runtime then +drops the future and completes the call with code `-5` and the message +"cancelled", unless it had already completed. A null token means "never +cancelled". + +Producer code doesn't need to check the token: dropping the future cancels +it at its next suspension point, and RAII guards run as usual. Poll +`cancel.is_cancelled()` only for cooperative cleanup of work the future +doesn't own, such as a thread it spawned. Note that a future that never +suspends can't be interrupted; it runs to completion and its result wins. + +## Per-target surface + +| Target | Async surface | Cancellation | `-5` raises | +|--------|---------------|--------------|-------------| +| [C](../generators/c.md) | launcher plus callback | `{prefix}_cancel_token*` slot | the code in `err` | +| [C++](../generators/cpp.md) | `std::future` | RAII `CancelToken` argument | a `Cancelled` exception | +| [Swift](../generators/swift.md) | `async` (`async throws`) | cancelling the `Task` | `CancellationError` | +| [Kotlin](../generators/kotlin.md) | `suspend fun` | cancelling the coroutine | `CancellationException` | +| [Node.js](../generators/node.md) | `Promise` | `AbortSignal` (`{ signal }`) | `CancelledError` | +| [WebAssembly](../generators/wasm.md) | `Promise` | `AbortSignal` (`{ signal }`) | `CancelledError` | +| [Python](../generators/python.md) | awaitable | cancelling the awaiting task | `asyncio.CancelledError` | +| [.NET](../generators/dotnet.md) | `Task` | `CancellationToken` | `OperationCanceledException` | +| [Dart](../generators/dart.md) | `Future` | a generated cancel-token object | `CancelledException` | +| [Go](../generators/go.md) | blocking call, `context.Context` first | `ctx` done | returns `ctx.Err()` | +| [Ruby](../generators/ruby.md) | blocking call | `cancel:` keyword with a `CancelToken` | a `Cancelled` error | + +Go and Ruby block the calling goroutine or thread until the completion fires +(the native work still runs off-thread), so call them from a goroutine or +`Thread` for concurrency. Exact type names and signatures are on each +language page. + +## Choosing an executor + +Something has to drive each future between launch and completion. The +runtime routes every future to the process-wide spawner: + +- **Default.** Each launch gets a fresh thread that drives the future with a + minimal executor. It needs no runtime and suits CPU-bound work and futures + woken from other threads, but it has no reactor: a future that awaits + Tokio's I/O or timers never wakes. +- **Custom.** Call `weaveffi::set_spawner` once at startup, before the first + async launch. It accepts any `Fn(BoxFuture) + Send + Sync + 'static`: + + ```rust + let handle = tokio::runtime::Handle::current(); + weaveffi::set_spawner(move |fut| { + handle.spawn(fut); + }) + .expect("spawner installed once"); + ``` + + The first call wins; later calls return `Err(SpawnerAlreadySet)`. The + spawner must not block, because the launcher calls it on the consumer's + thread. Every future it receives is already wrapped so a panic becomes a + `-2` completion, and a spawner that drops a future still produces a `-5` + completion. +- **`wasm32`.** There are no threads, so the default drives the future + inline before the launcher returns. Futures that finish after bounded work + are fine; one that waits for an external wake (a timer, I/O) hangs the + module. A call usually completes before an `AbortSignal` can fire, so on + this target cancellation mostly matters for a signal that's already + aborted when the call starts. ## Pitfalls -- **Awaiting Tokio without installing Tokio**: the default spawner has no - reactor, so `tokio::time::sleep` or a `TcpStream` read under it never - wakes and the callback never fires. Call `weaveffi::set_spawner` with a - runtime handle before the first async launch. -- **Installing the spawner twice**: `set_spawner` is first-one-wins and - returns `Err(SpawnerAlreadySet)` afterwards; a second runtime is silently - not used. Install once, from a single well-defined initialization point. -- **Blocking inside `Spawner::spawn`**: the launcher calls it on the - consumer's thread. Hand the future off and return. -- **Forgetting `#[weaveffi::cancellable]`**: a `CancelToken` parameter alone - does not make a function cancellable; without the attribute the macro - reports a missing-argument error at the call site. -- **Async void functions**: the validator emits a warning. They are valid but - almost always indicate a missing return type. -- **Forgetting `cancellable: true`**: without it, the launcher has no - cancel-token slot and the operation cannot be cancelled at all. -- **Expecting a reserved "cancelled" code**: there is none. Return one of - your own domain codes when the token is set, or complete with a partial - result. -- **Using async for CPU-bound work**: the callback overhead exceeds the work - being done; keep it synchronous. -- **Calling Go/Ruby async functions on a latency-sensitive thread**: both - wrappers block the calling thread until the producer completes. Wrap the - call in a goroutine / Ruby `Thread` when you need concurrency; the native - work already runs off-thread. -- **Letting the callback closure get garbage-collected**: every generator - pins it explicitly. Do not strip those pins when editing generated code by - hand. -- **Returning `null` instead of invoking the callback**: the contract is that - the callback fires **exactly once** for every async call, including on - cancellation and on panic. -- **Forgetting to free an owned result**: strings, bytes, and serialized - value buffers passed to the callback belong to the consumer. Copy or decode - them, then release them with `weaveffi_free_string` or - `weaveffi_free_bytes`; a callback that only copies leaks the producer's - allocation. The generated wrappers do this for you; the rule matters when - you consume the raw C surface. -- **Freeing the boxed error with the wrong symbol**: the error passed to an - async callback is heap-boxed, so it is released with `weaveffi_error_free` - (which frees the message, the payload, and the box). Calling only - `weaveffi_error_clear` on it leaks the box; calling `free()` on it leaks - the message and payload. -- **Awaiting an external wake on wasm**: the inline executor has no way to - resume the future, so the module hangs. Keep wasm async exports to work - that completes without waiting. +- **Awaiting Tokio under the default spawner.** The future never wakes and + the callback never fires; install a Tokio spawner first. +- **Expecting the token to stop CPU-bound work.** Cancellation drops the + future at a suspension point. Long synchronous loops should yield or check + `is_cancelled()`. +- **Clearing an async error.** `err` is boxed; release it with + `{prefix}_error_free`, not `error_clear`. +- **Leaking a result buffer.** A raw C callback that only copies the result + must still free it. +- **Async functions with no return value.** Valid, but `weaveffi validate + --warn` flags them (`AsyncVoidFunction`), since they're usually a missing + return type. diff --git a/docs/src/guides/config.md b/docs/src/guides/config.md index 5d0d922e..a7795e2a 100644 --- a/docs/src/guides/config.md +++ b/docs/src/guides/config.md @@ -1,14 +1,17 @@ # Project Configuration -## Overview - -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`. +A project's settings live in one `weaveffi.toml` at its root. The API +definition (annotated Rust or an IDL) describes only the API; this file says +where it is, what the library is called, and how each target is generated. +Every table is optional, and every unknown table or key is an error, so a typo +fails the run instead of being ignored. ```toml +[project] +input = "src/lib.rs" # or an IDL such as "kvstore.yml" +out = "bindings" +targets = ["c", "swift", "kotlin", "python"] + [package] name = "kvstore" version = "1.2.0" @@ -18,242 +21,149 @@ authors = ["Example "] repository = "https://github.com/example/kvstore" [global] -c_prefix = "kv" - -[generators.swift] -module_name = "KVStore" +post_generate = "swiftformat bindings/swift" [generators.kotlin] package = "com.example.kvstore" ``` -The file has three tables, all optional: - -- `[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. +`weaveffi init` writes a starter file: in a Rust crate it points `input` at +`src/lib.rs`; elsewhere it also writes a starter IDL. ## Discovery -`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. +With an explicit input (`weaveffi generate api.yml`), the CLI uses the nearest +`weaveffi.toml` at or above the input's directory. With no input, it looks for +`weaveffi.toml` in the current directory and its parents and reads +`[project] input`, so `weaveffi generate` works from anywhere inside the +project, like `cargo build`. `--config ` names a file explicitly. +Without any file, every setting takes its default. -```bash -weaveffi generate src/lib.rs -o generated # nearest weaveffi.toml -weaveffi generate src/lib.rs -o generated --config ci.toml # a specific file -``` +## `[project]` -Without any config file, every generator runs with its defaults and the -package name falls back as described under [Package identity](#package-identity). +| Key | Default | Meaning | +|-----|---------|---------| +| `input` | none | The API definition: a `.rs` file or a `.yml`, `.yaml`, `.json`, or `.toml` IDL | +| `out` | `generated` | Output directory for `generate` and `diff` | +| `targets` | all eleven | Targets to generate when `--target` isn't given | -Every unknown table and key is rejected, so a typo like `[generator.swift]`, -`c_prefx`, or `modulename` fails the run with the offending name, and so does -a table for a target that no longer exists (such as the pre-0.22 -`[generators.android]`; the target is now `kotlin`). The accepted per-target -keys are listed in the [table below](#generatorstarget). +Paths are relative to the directory holding `weaveffi.toml`. Command-line +arguments (`input`, `-o`, `--target`) override the table. ## `[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.kotlin]` | `package` | string | `"com.weaveffi"` | JVM package for the generated Kotlin wrapper and the `namespace` in `build.gradle.kts` | -| `[generators.kotlin]` | `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` source. Async functions and callback interfaces are unsupported in this mode | -| `[generators.wasm]` | `allow_unsupported` | bool | `false` | In Emscripten mode, generate anyway when the API uses async functions or callback interfaces: the capability failure becomes a warning and those entry points are emitted as explicit throwing stubs. No effect outside Emscripten mode | -| `[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 app with a branded module and symbol prefix -[global] -c_prefix = "myapp" - -[generators.swift] -module_name = "MyAppFFI" -``` - -```toml -# Android or desktop-JVM library -[generators.kotlin] -package = "com.example.myapp.ffi" -``` +`[package]` resolves into the library's [identity](../reference/naming.md): +the package `name` every ecosystem publishes under, the C symbol `prefix`, +and the native `library` base name. Generators derive every other name from +these three. -```toml -# Browser bundle linked into an existing Emscripten build (no async or -# callback interfaces in the API) -[generators.wasm] -module_name = "myapp" -emscripten = true -``` +| Key | Meaning | +|-----|---------| +| `name` | Distribution name (may contain `-` or `.`) | +| `version` | Version stamped into every manifest (default `0.1.0`) | +| `description`, `license`, `authors`, `homepage`, `repository` | Manifest metadata | +| `c_prefix` | C symbol prefix; IDL inputs only | +| `library` | Native library base name (`lib{library}.so`); IDL inputs only | -```toml -# Scoped npm package, module-prefixed names everywhere -[global] -strip_module_prefix = false +The rules depend on the input: -[generators.node] -package_name = "@myorg/myapp-native" -``` +| | Rust producer (`.rs`) | IDL | +|-|-----------------------|-----| +| `name` | `[package] name`, else the crate's Cargo package name | `[package] name`, else the input file stem | +| `prefix` | the crate's library name (`[lib] name`, else the package name with `-` mapped to `_`) | `c_prefix`, else snake-case `name` | +| `library` | same as `prefix` (the cdylib Cargo builds) | `library`, else snake-case `name` | +| metadata | `[package]`, falling back to `Cargo.toml` | `[package]` | -```toml -# Build the producer before generating so `weaveffi package --build` can -# find fresh binaries -[global] -pre_generate = "cargo build --release" -``` +A Rust producer's prefix is what the macro compiled into its symbols, so it +can't be configured: setting `c_prefix` or `library` for a `.rs` input is an +error. Renaming the published package with `name` is fine. -## Wiring it into CI +## `[global]` -`weaveffi diff --check` enforces that the committed bindings still match the -API definition and config. A typical guard job: +| Key | Meaning | +|-----|---------| +| `strip_module_prefix` | Sets `strip_module_prefix` on every target that supports it, overriding their own tables | +| `pre_generate` | Shell command run once before `generate` writes anything | +| `post_generate` | Shell command run once after `generate` finishes writing | -```yaml -# .github/workflows/ci.yml -- name: Verify generated bindings are up to date - run: weaveffi diff src/lib.rs --out generated --check -``` +Targets that place every module's members in one flat namespace strip the +module name from free functions by default (`get_stats`, not +`kv_stats_get_stats`). Set `strip_module_prefix = false` when two modules +declare the same function name; targets that namespace each module +separately don't need it. Each language page says which applies. -Exit codes: +Hooks run through `sh -c` (`cmd /C` on Windows), only when at least one +target actually regenerates; an up-to-date run skips them. `weaveffi diff` +never runs them. Don't put untrusted input in a hook. -| 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. | +## `[generators.]` -`weaveffi validate --format json` emits structured success or failure, and -`--warn` adds the advisory lint warnings to the same document: +One table per target (`c`, `cpp`, `swift`, `kotlin`, `node`, `wasm`, +`python`, `dotnet`, `dart`, `go`, `ruby`) holds that generator's options. +These keys override names derived from the identity: + +| Table | Key | Default | +|-------|-----|---------| +| `[generators.cpp]` | `namespace`, `header_name` | `{prefix}`, `{library}.hpp` | +| `[generators.swift]` | `module_name` | `PascalCase(name)` | +| `[generators.kotlin]` | `package` | `{prefix}` | +| `[generators.node]` | `package_name` | `{name}` | +| `[generators.wasm]` | `package_name` | `{name}` | +| `[generators.python]` | `package_name`, `import_name` | `{name}`, `{prefix}` | +| `[generators.dotnet]` | `namespace` | `PascalCase(name)` | +| `[generators.dart]` | `package_name` | `{prefix}` | +| `[generators.go]` | `module_path` | `{name}` | +| `[generators.ruby]` | `gem_name`, `module_name` | `{name}`, `PascalCase(name)` | + +Every other option (Kotlin's Android or JVM flavor, Wasm's Emscripten mode, +and so on) is documented on the target's page under +[Generators](../generators/README.md). No target has its own C prefix: the +prefix belongs to the library. + +## Generation and caching + +Rendering is pure: each target renders its files in memory, and the +orchestrator does every write. After a target generates, the orchestrator +writes a record to `{out}/.weaveffi-cache/{target}.json` holding a hash of +every input that affects the output (the API, the identity, the target's +config, and the CLI version) and the path and hash of every file written. On +the next run: + +- A target is skipped when its input hash matches and every recorded file is + still on disk unmodified. Deleting or hand-editing a generated file + regenerates it. +- A regenerated target rewrites only files whose contents changed, so file + timestamps (and incremental builds) stay stable. +- Files the previous run wrote that the current run no longer produces (a + renamed type, a removed module) are deleted. Files you added under the + output directory (a `node_modules/`, a build directory) are never touched. + +`--force` ignores the records and regenerates every selected target. +`--dry-run` validates and prints the files that would be written. + +## CI + +`weaveffi diff --check` regenerates in memory and compares with the output +directory without writing anything or running hooks: ```bash -weaveffi --quiet validate src/lib.rs --warn --format json | jq '.ok, .warnings' -``` - -```json -{ "ok": true, "modules": 2, "functions": 8, "structs": 3, "enums": 1, "warnings": [] } +weaveffi diff --check # uses [project] +weaveffi diff src/lib.rs -o bindings --target c,swift --check ``` -```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` 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. +It exits `0` when the directory is up to date, `2` when files would change, +and `3` when files would be added or removed (stale files count as removed). +`weaveffi validate --format json` prints one JSON object with `ok`, counts, +and any errors (each with a `code` matching the +[error catalog](../reference/idl.md#error-catalog)); `--warn` adds advisory +warnings, which never change the exit status. ## Pitfalls -- **The C prefix rewrites every generator**: picking a custom prefix - 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. +- **Config travels with the definition.** Two checkouts with different + `weaveffi.toml` files generate different packages from the same source. + Commit the file next to the definition and let discovery find it. +- **Name overrides don't rename the library.** A target's package or module + override changes what consumers import, not which native library it loads; + that's always `{library}`, overridable at run time with the + `{PREFIX}_LIBRARY` environment variable. diff --git a/docs/src/guides/errors-and-memory.md b/docs/src/guides/errors-and-memory.md new file mode 100644 index 00000000..43425d51 --- /dev/null +++ b/docs/src/guides/errors-and-memory.md @@ -0,0 +1,250 @@ +# Errors and Memory + +This guide states who owns every value that crosses the C ABI and how +failures travel back to the caller. The generated bindings follow these rules +for you; they matter when you consume the C header directly, implement it by +hand, or audit a binding. The [C ABI contract](../reference/abi.md) is the +normative text. Examples use the `kvstore` sample, whose prefix is `kvstore`. + +## Ownership at a glance + +| Family | Parameter | Return, async result, iterator element | Release | +|--------|-----------|----------------------------------------|---------| +| direct (scalars, `bool`, C-style enums) | copied | copied | nothing | +| `string`, `bytes` | borrowed `(ptr, len)` | owned `(ptr, len)` | `{prefix}_free_bytes(ptr, len)` | +| value buffers (records, rich enums, `T?`, `[T]`, `{K:V}`) | borrowed `(ptr, len)` | owned `(ptr, len)` | `{prefix}_free_bytes(ptr, len)` | +| objects (interfaces) | borrowed pointer | one strong reference | `{Type}_destroy` | +| callback interfaces | `ctx` plus vtable, owned by the consumer | never returned | the producer calls `free(ctx)` once | + +*Borrowed* means the callee may read the value only until the call returns +and copies anything it keeps. Everything a producer hands back belongs to the +receiver, who releases it exactly once with the function in the last column. + +## Strings and bytes + +A `string` or `bytes` parameter is a borrowed `const uint8_t* {name}_ptr, +size_t {name}_len` pair. Strings are UTF-8 and never NUL-terminated, so an +interior NUL survives the trip. A null pointer is valid only with length `0` +(the empty value); a null pointer with a non-zero length, or a string that +isn't valid UTF-8, fails the call with the marshalling code `-3`. + +A returned string or byte run is a producer allocation: the function returns +`const uint8_t*` and writes the length through a trailing `size_t* out_len`. +The consumer copies it and frees it with `{prefix}_free_bytes(ptr, len)`, +passing the exact length it received. An empty result may come back as +`NULL` with length `0`. + +```c +/* The calculator sample's `echo(s: string) -> string`. */ +calculator_error err = {0}; +size_t len = 0; +const uint8_t* text = + calculator_calculator_echo((const uint8_t*)"hi", 2, &len, &err); +if (err.code == 0) { + printf("%.*s\n", (int)len, (const char*)text); + calculator_free_bytes((uint8_t*)text, len); +} +``` + +## Value buffers + +Records, rich enums, optionals (except `Interface?`), lists, and maps cross +as one serialized [value buffer](../reference/value-buffers.md), however deep +the nesting. A parameter is a borrowed `(ptr, len)` the caller encodes and +frees itself. A return is one producer allocation the consumer decodes and +then releases once with `{prefix}_free_bytes`; the strings and records it +decodes are copies, so nothing inside is freed separately. The exception is +an object token inside a buffer, which carries one strong reference the +reader adopts. + +## Objects + +Interface objects are reference counted by the producer (an `Arc` in a +Rust producer). Each interface has two lifecycle symbols: + +```c +kvstore_kv_Store* kvstore_kv_Store_clone(const kvstore_kv_Store* self); /* +1 */ +void kvstore_kv_Store_destroy(kvstore_kv_Store* self); /* -1 */ +``` + +A pointer is a strong reference. `_clone` returns another reference to the +same object (the pointer value is unchanged), `_destroy` releases one, and the +object is dropped when the last reference goes. Both accept null as a no-op. + +| Position | Rule | +|----------|------| +| parameter (`Store`, `Store?`) | borrowed for the call; the producer takes its own reference if it keeps the object | +| return, async result, iterator element | one strong reference transfers; the consumer adopts it | +| callback-interface method parameter | one strong reference transfers to the consumer | +| token inside a value buffer (either direction) | carries one strong reference; the reader adopts it | + +Two consequences follow. A consumer that encodes an object into a buffer +must call `_clone` and write the new pointer, never the one its wrapper +holds. And a buffer that contains objects is decoded exactly once, because +each token is one reference. + +Every generated wrapper owns exactly one reference and releases it through +the language's disposal idiom (`close()`, `Dispose()`, `deinit`, a C++ +destructor, `Close()`), with a finalizer or cleaner backstop where the +runtime has one. Each binding also keeps the wrapper alive for the duration +of every native call, so a wrapper that becomes unreachable mid-call can't +free the object under it. An in-flight async call holds its own reference. +The [capability matrix](../generators/README.md) lists each target's idiom. + +## Callback interfaces + +A callback-interface parameter lowers to `void* {name}_ctx` plus +`const {vtable}* {name}_vtable`. The consumer owns `ctx` (a generated binding +uses a key into a table that keeps the implementation alive) and a static +vtable per interface. The producer may call any entry, any number of times, +from any thread, until it calls `free(ctx)` exactly once. + +Arguments to a callback method follow the parameter rules above, seen from +the consumer: strings, bytes, and buffers are borrowed for the duration of +the call and must be copied or decoded before returning; objects transfer one +reference the consumer adopts. + +## Iterators + +An `iter` return is an opaque handle. `{Iter}_next` writes one element +(returning `1`) or reports the end (returning `0`); each element is owned by +the consumer under the return rules above. `{Iter}_destroy` is called exactly +once, either on exhaustion or when the consumer abandons the iteration early. +The generated wrappers do both. + +## Errors + +### The error struct + +Every synchronous symbol except `_clone`, `_destroy`, and the iterator's +`_destroy` takes a trailing `{prefix}_error* out_err`: + +```c +typedef struct kvstore_error { + int32_t code; /* 0 success, >0 domain code, <0 runtime code */ + const char* message; /* NUL-terminated UTF-8, producer-owned */ + const uint8_t* payload_ptr; /* the code's fields as a value buffer, or NULL */ + size_t payload_len; +} kvstore_error; +``` + +The caller owns the struct and zero-initializes it. The producer writes it +only on failure. After a failure the caller reads `code`, `message`, and the +payload, then calls `{prefix}_error_clear(&err)`, which frees the message and +payload and resets `code` to `0`. On failure the function's return value is a +zero sentinel (`0`, `false`, `NULL`) that must not be used or freed. + +```c +kvstore_error err = {0}; +int64_t n = kvstore_kv_Store_count(store, &err); +if (err.code != 0) { + fprintf(stderr, "count failed (%d): %s\n", err.code, err.message); + kvstore_error_clear(&err); +} +``` + +An async completion receives a heap-boxed error instead (or `NULL` on +success), released with `{prefix}_error_free`, which also frees the box. + +### Codes + +| Code | Meaning | +|------|---------| +| `0` | success | +| `> 0` | a declared code of the module's [error domain](../reference/idl.md#error-domains) | +| `-1` | generic: an untyped producer error (`Result`, an error type without a domain code) | +| `-2` | the producer panicked; the message carries the panic text | +| `-3` | marshalling failure: a null or invalid argument, non-UTF-8 text, an out-of-range enum value, a malformed value buffer | +| `-4` | a consumer's callback-interface implementation failed; the message is the consumer's | +| `-5` | cancelled: an async call's cancel token fired, or its executor dropped it (async completions only) | + +Domain codes are validated positive, so the two ranges never collide. The C +header declares each domain as an enum (`kvstore_kv_KvError_KeyNotFound = +1001`). + +### Domain errors and the trap channel + +How a binding surfaces a non-zero code depends on the callable's `throws` +flag, and every target applies the same rule: + +- **`throws: true`.** A positive code is a typed domain error: an exception + subclass, a Swift `Error` case, a Go `error` value, carrying the code, the + producer's message, and the payload fields as properties. A negative code + surfaces as the package's root error type with that code. +- **No `throws`.** The function can't report a domain error, so any non-zero + code is a producer bug or a failed callback. The binding raises it through + the target's programming-error idiom (an unchecked exception, a Go panic, + a Swift `fatalError` that includes the code and message) and never dresses + it up as a domain error. +- **`-5` on an async call** always surfaces as the language's own + cancellation error (Swift's `CancellationError`, Kotlin's + `CancellationException`, .NET's `OperationCanceledException`, Python's + `asyncio.CancelledError`, and so on; see + [Async and Cancellation](async.md#per-target-surface)). + +Error types are named after the package and the IDL, never after WeaveFFI; +each [language page](../generators/README.md) lists its names. + +A Rust producer's domain error message is its `Display` output. An IDL +`message:` (or a Rust variant's doc comment) is the documented default that +generated docs and consumer-side constructors use. + +### Payloads + +An error code may declare fields. When the producer raises it, the fields are +serialized into `payload_ptr`/`payload_len` in the value-buffer format, and +each binding exposes them as properties of the error it raises. A code with +no fields leaves the payload null. `{prefix}_error_clear` and +`{prefix}_error_free` release the payload with the message. + +### When a callback fails + +A consumer's callback implementation can fail in the consumer's language. The +binding catches the exception in its trampoline and reports it through the +vtable entry's `out_err` by calling + +```c +void kvstore_error_set(kvstore_error* err, int32_t code, const char* message); +``` + +with code `-4` and a borrowed message, which the producer copies with its own +allocator. A hand-written C consumer must do the same and never store its own +allocation in `message`. A positive code written there is reported as `-4`, +so a consumer failure can't impersonate a domain error. The producer then +abandons the call it was making (or handles the failure, if its callback +method returns `Result`), and the original caller sees `-4` +with the consumer's message. Because `-4` isn't a domain code, a consumer +that wants to catch its own callback failures should call the producer +through a `throws: true` function. + +### Panics + +A Rust producer's thunks catch panics and report them as `-2`. Destructors +(`_destroy`) have no error slot, so a panic in a `Drop` implementation is +swallowed rather than unwinding into C. Async functions report panics through +the completion callback. + +## Thread safety + +Every `#[weaveffi::interface]` type is `Send + Sync` (the macro asserts it), +so a macro-built producer may be called from any thread, and the bindings add +no thread restriction of their own. Callback methods and async completions may +arrive on any producer thread; each binding hops to its own scheduler where +the language requires it. A hand-written producer with thread-affine state +should document it. + +## Pitfalls + +- **Wrong length to `free_bytes`.** Always pass the exact `out_len` you got. +- **Freeing borrowed inputs.** Parameters are the caller's; never pass them + to `free_bytes` or `_destroy`. +- **Encoding an object without `_clone`.** The token hands away a reference + your wrapper still needs; the later `_destroy` over-releases. +- **Decoding an object-carrying buffer twice.** The second decode adopts + references that no longer exist. +- **Not clearing the error.** The message and payload leak, and a stale + non-zero `code` confuses the next check. Start every slot at `{0}`. +- **`error_clear` on an async error.** It leaks the box; use `error_free`. +- **Holding a lock across a callback call.** With a plain-return callback + method, a consumer failure unwinds through the producer and poisons a + `std::sync::Mutex`; snapshot state, release the lock, then call. diff --git a/docs/src/guides/errors.md b/docs/src/guides/errors.md deleted file mode 100644 index 12861976..00000000 --- a/docs/src/guides/errors.md +++ /dev/null @@ -1,513 +0,0 @@ -# Error Handling - -## Overview - -WeaveFFI's error model is typed and opt-in. A module declares an **error -domain**: a named set of symbolic codes. A function, method, or constructor -opts into that domain by declaring `throws: true`, and every generator then -surfaces its failures through the target's idiomatic error mechanism -(`throws` in Swift, `raise` in Python, `(T, error)` in Go, exceptions -elsewhere) carrying a *typed* error derived from the domain, so consumers -catch and match on the codes you declared. - -A callable **without** `throws` has a plain signature: no `throws` clause, -no error return. It cannot report a domain error; the only failures it can -experience are producer bugs (a panic, a marshalling failure) or a failing -consumer callback, and those trap loudly through the target's -programming-error idiom rather than surfacing as a typed error. The two -interpretations are named once, in `weaveffi_core::plan::ErrorStrategy`, and -every generator renders the same pair; see -[Throws versus Trap](#throws-versus-trap). - -Underneath, every generated symbol still reports through the C-level -out-error parameter (`weaveffi_error*`) with an integer code, an optional -message string, and an optional structured payload; the typed surface is -built on top of it. ABI revision 2 adds one code to the reserved negative -range, `-4` (`FOREIGN_ERROR_CODE`), for failures that originate in a -consumer's callback-interface implementation, and one runtime symbol, -`weaveffi_error_set`, that consumers use to report them. - -## When to use - -Reach for this guide when: - -- You are designing an IDL and want to surface stable, named error codes to - consumers as typed errors. -- You are writing the Rust implementation of a module and need to return - errors over the C ABI. -- You implement a callback interface in a consumer language and want to know - what happens when your implementation throws. -- You are debugging an "unknown error" surface in a generated binding. -- You are reviewing or extending a generator and need to know what the error - contract guarantees. - -## Step-by-step - -### Declare a domain and opt in with `throws` - -```yaml -version: "0.9.0" -modules: - - name: kv - 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 - constructors: - - name: open - params: - - { name: path, type: string } - throws: true - methods: - - name: get - params: - - { name: key, type: string } - return: Entry? - throws: true - - name: count - params: [] - return: i64 -``` - -`open` and `get` are fallible and deliver `KvError` values; `count` has a -plain signature in every target. Code names are PascalCase by convention -(`KeyNotFound`, not `key_not_found`); each generator re-cases them into its -own idiom. - -The domain is in scope for its module and every module nested inside it, so -one domain on a parent module can serve a whole subtree. Free functions, -interface constructors, methods, and statics all opt in with the same -`throws: true` flag. Callback-interface methods can't: they are implemented -by the consumer, and a consumer failure has its own channel (see -[Foreign errors](#foreign-errors)). - -A code may additionally declare structured payload `fields:` (the same shape -as record fields). When a matching error is raised, those fields travel -across the ABI in the error's payload buffer and surface as properties on the -typed error the consumer catches: - -```yaml -codes: - - name: StoreFull - code: 1003 - message: "store has reached capacity" - fields: - - { name: capacity, type: i64 } -``` - -See [Structured error payloads](../reference/idl.md#structured-error-payloads) -for the schema and the -[Value Buffer Protocol](../reference/value-buffers.md#structured-errors) for -the wire format. - -The validator enforces: - -- Codes must be positive. `0` is reserved for success and the whole negative - range belongs to the runtime (`-1` generic error, `-2` producer panic, `-3` - marshalling failure, `-4` foreign error). -- Numeric codes are unique within a domain. -- Code names are unique within a domain **and across every domain in the - API**. Backends with flat namespaces derive one error class or constant per - code, so two domains both declaring `NotFound` would collide; qualify one of - them (for example `OrderNotFound`). -- The domain `name` must not be empty, must not collide with any function - name in the module, and shares the API-wide type namespace with record, - enum, interface, and callback-interface names. -- `throws: true` with no domain in scope (on the module or an ancestor) is an - error, and so is `throws: true` on a callback-interface method. - -### Report errors from the producer - -**With the Rust macro**, declare the domain as a `#[weaveffi::error]` enum -whose discriminants are the ABI codes (doc comments become the default -messages), and return `Result` from fallible functions. This is -the `kvstore` sample's domain: - -```rust -#[weaveffi::module] -pub mod kv { - /// The store's error domain. - #[weaveffi::error] - #[derive(Debug)] - pub enum KvError { - /// key not found - KeyNotFound = 1001, - /// entry expired - Expired = 1002, - /// store has reached capacity - StoreFull = 1003, - /// I/O failure - IoError = 1004, - } - - #[weaveffi::export] - pub fn open_path(path: String) -> Result { - if path.is_empty() { - return Err(KvError::IoError); - } - Ok(path) - } -} - -weaveffi::export_runtime!(); -``` - -The macro generates the `ErrorReport` implementation and the C ABI thunks -that write the matching code and message into `out_err`. `Result` -is also accepted; it reports the generic code `-1` with the string as the -message, which consumers see as the untyped fallback rather than a domain -case. - -A variant may carry named fields, which become the code's structured payload; -the macro serializes them into the error's payload buffer in declaration -order. Field-carrying variants with explicit discriminants require a -primitive repr on the enum: - -```rust -#[weaveffi::error] -#[derive(Debug)] -#[repr(i32)] -pub enum QuotaError { - /// quota exceeded - Exceeded { limit: i64, used: i64 } = 3001, - /// quota service unavailable - Unavailable = 3002, -} -``` - -**If you hand-implement the C ABI** (a non-Rust producer, or Rust without the -macro), report through the `weaveffi-abi` helpers, preferring the codes you -declared in the IDL: - -```rust -use weaveffi_abi::{self as abi, weaveffi_error}; - -#[no_mangle] -pub extern "C" fn weaveffi_kv_open_path( - path: *const std::ffi::c_char, - out_err: *mut weaveffi_error, -) -> *const std::ffi::c_char { - let _ = path; - abi::error_set(out_err, 1004, "I/O failure"); - std::ptr::null() -} -``` - -| Helper | Effect | -|--------|--------| -| `error_set_ok(out_err)` | Sets `code = 0`, frees any prior message and payload | -| `error_set(out_err, code, msg)` | Sets a non-zero code and allocates a message | -| `error_set_with_payload(out_err, code, msg, payload)` | Like `error_set`, plus an owned payload buffer (the code's fields in value-buffer format) | -| `result_to_out_err(result, out_err)` | Maps `Result` through `ErrorReport` (domain code, message, and payload for implementors; generic `-1` for `String` and `&str` errors) | -| `error_set_panic(out_err, payload)` | Reports a caught unwind: `-4` with the consumer's message for a `ForeignError` payload, `-2` for anything else | -| `check_foreign_error(err)` | After a vtable call: no-op on `code == 0`, otherwise unwinds with a `ForeignError` | - -Hand-written `ErrorReport` implementations can override the trait's -`payload(&self) -> Vec` method to serialize a code's declared fields; the -default reports no payload. - -### Handle errors in C - -The C surface is the raw out-error struct: - -```c -weaveffi_error err = {0}; - -weaveffi_kv_Store* store = weaveffi_kv_Store_open("", &err); -if (err.code) { - fprintf(stderr, "error %d: %s\n", err.code, - err.message ? err.message : "unknown"); - weaveffi_error_clear(&err); - return 1; -} - -weaveffi_kv_Store_destroy(store); -``` - -The pattern is always: - -1. Zero-initialise: `weaveffi_error err = {0};`. -2. Call the function with `&err` as the last argument. -3. Check `err.code`; if non-zero, read `err.message` (and `payload_ptr` / - `payload_len` if the code declares fields) and call - `weaveffi_error_clear(&err)`. -4. Reuse the struct for subsequent calls. - -The domain's codes are also emitted as a C enum, so a consumer can match on -names instead of magic numbers: - -```c -typedef enum { - weaveffi_kv_KvError_KeyNotFound = 1001, - weaveffi_kv_KvError_Expired = 1002, - weaveffi_kv_KvError_StoreFull = 1003, - weaveffi_kv_KvError_IoError = 1004 -} weaveffi_kv_KvError; -``` - -Async completion callbacks receive a *heap-boxed* `weaveffi_error*` instead -of writing into a caller-owned slot; release it with `weaveffi_error_free`, -which clears the slot and frees the box. See -[Async Functions](async.md#result-ownership-and-threading). - -### What consumers see - -Every other target wraps that struct into a typed error construct named after -the domain. The table below is taken from the bindings `weaveffi generate` -emits for `samples/kvstore` (domain `KvError`, code `KeyNotFound = 1001`): - -| Target | Domain type | The `KeyNotFound` case | Generic fallback | -|--------|-------------|------------------------|------------------| -| Swift | `enum KvError: Error, LocalizedError` | `.keyNotFound(message:)` | `WeaveFFIError.error(code:message:)` | -| Kotlin | `sealed class KvException : WeaveFFIException` | `KvException.KeyNotFound` | `WeaveFFIException(code, message)` | -| Python | `class KvError(WeaveFFIError)` | `class KeyNotFound(KvError)` | `WeaveFFIError` | -| Node.js | `class KvError extends WeaveFFIError` | `class KeyNotFoundError extends KvError` (`KeyNotFoundError.CODE === 1001`) | `WeaveFFIError` | -| WASM | `class KvError extends WeaveFFIError` | `class KeyNotFound extends KvError` (`KeyNotFound.CODE === 1001`) | `WeaveFFIError` | -| .NET | `class KvException : WeaveFFIException` | `KvException.KeyNotFound` constant, matched on `Code` | `WeaveFFIException` | -| Dart | `class KvException extends WeaveFFIException` | `class KeyNotFoundException extends KvException` | `WeaveFFIException` | -| Ruby | `class KvError < Kvstore::Error` | `KvError::KeyNotFound` | `Kvstore::Error` (the wrapping module is `[generators.ruby] module_name`, `WeaveFFI` by default; the sample sets `Kvstore`) | -| Go | `type KvError struct { Code, Message }` | `KvErrorKeyNotFound` code constant | `*WeaveFFIError` | -| C++ | `class KvError : public WeaveFFIError` | `class KeyNotFoundError : public KvError` | `kvstore::WeaveFFIError` | - -In Swift, throwing wrappers `throw` the domain enum: - -```swift -do { - let store = try Store.open(path: "") -} catch KvError.ioError(let message) { - print("cannot open: \(message)") -} -``` - -In Python, the domain is an exception class (subclassing the generic -`WeaveFFIError`) with one subclass per code carrying its stable `CODE`: - -```python -try: - store = Store.open("") -except IoError as e: - print("cannot open:", e) -``` - -The remaining targets follow the same conceptual shape in their own idiom: -one typed error construct per domain, delivered through the language's native -error channel. Ecosystems that suffix exceptions rename the domain -accordingly (`KvError` becomes `KvException` in Kotlin, .NET, and Dart, and -`IoError` becomes `IoException`). A code the consumer doesn't recognize (from -a newer producer, for example) falls back to the generic branded error rather -than being dropped. - -When a code declares payload `fields:`, each generator decodes the error's -payload buffer and exposes the fields as properties of the raised exception -(or returned error value), keyed by the field names. - -### Foreign errors - -A callback interface is implemented by the consumer, so its methods can fail -in the consumer's language: a Swift `throw`, a Python exception, a Go panic. -A callback method has no `throws` flag and returns only a scalar, so that -failure travels the other way through the ABI: - -1. The generated trampoline catches the failure and reports it into the - vtable entry's `out_err` slot by calling - `weaveffi_error_set(out_err, -4, message)`. The runtime copies the - borrowed message with the producer's allocator, so the trampoline can free - its own copy immediately. -2. On the producer side, the generated trait implementation calls - `check_foreign_error` after every vtable call. A non-zero code unwinds the - producer's current call with a `ForeignError` payload (via - `resume_unwind`, so the panic hook does not fire; this is control flow, not - a bug report). A negative code is kept as written; a positive one is - replaced by `-4`, so a consumer bug can't masquerade as one of the - producer's domain codes. On a `panic = "abort"` build (notably - `wasm32-unknown-unknown`) there's no unwinding: the failure is recorded in - a thread-local slot, the vtable entry's zero return value flows back into - the producer's code, and the thunk picks the recorded failure up with - `take_foreign_error` once the producer returns. -3. The enclosing thunk reports `FOREIGN_ERROR_CODE` (`-4`) with the - consumer's message to the **original caller**, the code that invoked the - producer function that in turn invoked the callback. - -Every generated trampoline already does step 1; the table shows what your -implementation writes and what the original caller reads: - -| Target | How an implementation fails | What the original caller sees | -|--------|-----------------------------|-------------------------------| -| Swift | protocol method is `throws`; throw any `Error` | `WeaveFFIError.error(code: -4, ...)` on a throwing call; `fatalError` on a non-throwing call | -| Kotlin | throw any `Throwable` | `WeaveFFIException` with `code == -4` | -| Python | raise any exception | `WeaveFFIError` with `code == -4` (`WeaveFFIError.FOREIGN_ERROR_CODE`) | -| Node.js / WASM | throw | `WeaveFFIError` with `code === -4` | -| .NET | throw | `WeaveFFIException` with `Code == WeaveFFIException.ForeignErrorCode` | -| Dart | throw | `WeaveFFIException` with `code == WeaveFFIException.foreignCode` | -| Ruby | raise | `Kvstore::Error` with `code == -4` (`Kvstore::FOREIGN_ERROR_CODE`) | -| Go | `panic` inside the method | `*WeaveFFIError` with `Code == -4` on a `(T, error)` call; `panic` on a plain call | -| C++ | throw a `std::exception` (or anything; non-standard exceptions get a fixed message) | `WeaveFFIError` with `code() == -4` | -| C | `weaveffi_error_set(out_err, -4, "message")` and return a default | `err.code == -4` | - -Because `-4` is not a domain code, the typed path never maps it onto a -domain case: a throwing call surfaces it as the generic branded error, and a -non-throwing call traps. If the producer function that called the callback -was declared without `throws`, the consumer's own exception therefore comes -back as a trap (`fatalError` in Swift, `panic` in Go). Declare `throws: true` -on producer functions that invoke callbacks if you want consumers to be able -to catch their own failures. - -A foreign error also unwinds through whatever the producer was holding at the -time. If that was a `std::sync::Mutex` guard, the mutex is poisoned; -producers that call out to consumers should snapshot state under the lock, -release it, and then call. See -[Memory Ownership](memory.md#producer-side-rules). - -### Producer panics - -Generated Rust thunks wrap the producer call in `catch_unwind`. A panic is -reported through `out_err` with the reserved code `-2` -(`weaveffi_abi::PANIC_ERROR_CODE`) and the message -`producer panicked: `, so a consumer can always distinguish "the -producer has a bug" from any declared domain error and from a foreign error. -Panics never surface as typed domain errors: on a throwing callable they -arrive as the generic branded error, and on a non-throwing callable they -surface as the target's unrecoverable idiom (a Swift `fatalError`, a Go -`panic`, a generic exception elsewhere). - -Object destructors (`{tag}_destroy`) and iterator destructors have no -`out_err` slot; a panic inside a `Drop` implementation is swallowed rather -than unwinding into C. Async functions report panics through the completion -callback's error slot; see [Async Functions](async.md#panic-handling). - -## Reference - -### Throws versus Trap - -Every synchronous C ABI entry point carries a trailing `out_err`, and every -async completion callback carries an `err` slot, regardless of `throws`. What -differs is the *meaning* of a non-zero code, and every backend agrees on it -because the two interpretations are stated once as -`weaveffi_core::plan::ErrorStrategy`: - -- **Throws** (`throws: true`): a non-zero code is a typed domain error. The - wrapper maps the code onto the module's error domain (an exception - subclass, a Swift `Error` enum case, a Go `error` value) and surfaces it - through the target's normal error channel so callers can catch and match - on it. Negative codes fall through to the generic branded error. -- **Trap** (no `throws`): the only way `out_err` reports failure is a - producer bug or a failing callback (codes `-2`, `-3`, `-4`). The wrapper - surfaces it through the target's *programming-error* idiom (a Python - `WeaveFFIError`, a Go `panic`, a Swift `fatalError`, a C# exception). A - trapped failure is never silently ignored, and it is never dressed up as a - typed domain error. - -The per-target rendering of both strategies is tabulated -[below](#per-target-surface). - -At the ABI level, `weaveffi_error.code` means: - -| Code | Constant | Meaning | -|------|----------|---------| -| `0` | | Success | -| a declared code | | A typed producer error from the module's domain (always positive) | -| `-1` | `GENERIC_ERROR_CODE` | Untyped producer error (a `Result`, or an error type without a domain code) | -| `-2` | `PANIC_ERROR_CODE` | Producer panic, caught by the thunk's `catch_unwind` | -| `-3` | `MARSHAL_ERROR_CODE` | Marshalling failure: a null or invalid argument, a non-UTF-8 string, an out-of-range enum value, or a malformed value buffer | -| `-4` | `FOREIGN_ERROR_CODE` | A consumer callback-interface implementation failed; the message is the consumer's | - -Domain codes are validated positive-only, so the two ranges can't collide: on -the typed path a wrapper maps a positive code to the matching declared case of -the domain type and falls back to the generic branded error for any code the -domain doesn't declare (including every negative runtime code). Generated -wrappers also raise the generic branded error themselves, without calling the -producer, for contract violations they detect locally (a disposed object -wrapper, a value that isn't an object of the expected class, a malformed -value buffer). - -### Per-target surface - -Per target, the two strategies surface as: - -| Target | Throws (`throws: true`) | Trap (producer bug or foreign error) | -|--------|-------------------------|--------------------------------------| -| C | `weaveffi_error { code, message, payload_ptr, payload_len }` struct | same struct (code `-2`, `-3`, or `-4`) | -| Swift | `throws`, typed domain enum | `fatalError` | -| Python | `raise`, domain exception subclass | `raise WeaveFFIError` | -| Kotlin | `throw`, typed domain exception | `throw WeaveFFIException` | -| C# | `throw`, typed domain exception | `throw WeaveFFIException` | -| Dart | `throw`, typed domain exception | `throw WeaveFFIException` | -| Node.js / WASM | `throw`, typed domain error | `throw WeaveFFIError` | -| Ruby | `raise`, typed domain error | `raise ::Error` (`WeaveFFI::Error` by default) | -| Go | `(T, error)` return, typed domain error | `panic` | -| C++ | `throw`, typed domain error | `throw ::WeaveFFIError` | - -All targets share the canonical `WeaveFFI` brand (never the `heck`-derived -`Weaveffi`) for the generic fallback type. Error type names are derived from -a single naming policy: ecosystems that suffix with `Error` (Swift, C++, -Python, Node, Go) use `WeaveFFIError`; ecosystems that suffix with -`Exception` (Kotlin, .NET, Dart) use `WeaveFFIException`; Ruby nests a plain -`Error` inside the configured wrapper module. Per-code names are PascalCased from the -IDL, and domain type names keep exactly one `Error` (or `Exception`) suffix. - -| Field | Type | Description | -|-------|------|-------------| -| `code` | `int32_t` | `0` = success, non-zero = error | -| `message` | `const char*` | `NULL` on success; Rust-allocated string on error | -| `payload_ptr` | `const uint8_t*` | `NULL` unless the matched code declares `fields:`; the fields serialized in the value-buffer format | -| `payload_len` | `size_t` | Byte length of `payload_ptr`; `0` when null | - -Three runtime symbols operate on the struct: - -| Symbol | Who calls it | Effect | -|--------|--------------|--------| -| `weaveffi_error_clear(err)` | consumer, after reading a sync error | frees message and payload, zeroes the slot; idempotent | -| `weaveffi_error_free(err)` | consumer, after reading an async error | clears the slot and frees the heap box it arrived in | -| `weaveffi_error_set(err, code, message)` | consumer, inside a callback-interface trampoline | copies `message` with the producer's allocator and sets `code` | - -See the [Memory Ownership Guide](memory.md) for the freeing contract on -`err.message` and the payload. - -## Pitfalls - -- **Forgetting to call `weaveffi_error_clear`**: the message and the payload - are Rust-allocated. Skipping the clear leaks them. -- **Reading `err.message` after clearing**: the pointer is invalid as soon as - `weaveffi_error_clear` returns. -- **Using `0` or a negative number as a domain code**: the validator rejects - both; `0` always means success and negative codes are reserved for the - runtime (`-1` generic, `-2` panic, `-3` marshalling failure, `-4` foreign - error). -- **Reusing a code name in two domains**: code names are unique across the - whole API, so the validator rejects a second `NotFound`. Qualify one of - them (`OrderNotFound`). -- **Declaring `throws: true` without a domain in scope**: a throwing callable - needs an `errors:` block on its module or an ancestor. -- **Declaring `throws: true` on a callback-interface method**: rejected. The - consumer reports failures through `weaveffi_error_set` and the producer's - caller sees `-4`. -- **Expecting a typed error from a non-throwing function**: a callable - without `throws` cannot deliver a domain error; a failure there is a - producer bug (or a foreign error) and traps through the target's - programming-error idiom (see [Throws versus Trap](#throws-versus-trap)). -- **Expecting your own callback exception to come back typed**: it surfaces - as the generic branded error with code `-4`, never as a domain case, and - only if the producer function you called was declared `throws: true`. -- **Writing `out_err->message` directly from a hand-written C callback**: - the producer frees that pointer with Rust's allocator. Use - `weaveffi_error_set`, which copies. -- **Not initialising the struct**: always start with `{0}` (or the language - equivalent). Stale `code` values from earlier calls produce confusing - failures. -- **Ignoring the return value when `code != 0`**: Rust does not promise the - return value is meaningful on failure. For pointer returns it is typically - `NULL`; do not free it. diff --git a/docs/src/guides/extract.md b/docs/src/guides/extract.md index 079ebb6f..fbc80fd9 100644 --- a/docs/src/guides/extract.md +++ b/docs/src/guides/extract.md @@ -1,338 +1,84 @@ -# Annotated Rust Extraction - -## Overview - -One way to drive WeaveFFI is to make annotated Rust your source of truth. The -`#[weaveffi::module]` proc-macro reads that annotated source to generate the -producer's C ABI glue (see [The Rust Producer Macro](producer-macro.md)), and -the CLI reads the *same* annotations to derive the IDL and bindings. Both -paths call into one shared extractor (`weaveffi-bridge`), so the IDL the CLI -emits and the symbols the macro produces cannot drift. - -You can point `weaveffi generate` and `weaveffi extract` straight at a `.rs` -file. `generate` lowers the source to the IR in memory and runs the -generators; `extract` writes the derived IDL to disk (handy for review, for -committing a canonical IDL alongside the source, or for piping into another -command). - -## When to use - -Reach for a `.rs` input when: - -- You want the Rust implementation to be the single source of truth, with no - separate IDL to maintain. -- You want the IDL to track signature changes automatically: edit the Rust, - re-run. - -Author an IDL document (YAML/JSON/TOML) directly when: - -- You want to design the API before any Rust exists. -- You need a feature the extractor cannot infer from Rust syntax, such as - record field defaults or a code `doc:` distinct from its `message:`. See - [Pitfalls](#pitfalls). - -## Step-by-step - -### 1. Annotate the Rust source - -Mark an inline module with `#[weaveffi::module]` and tag the items you want -to export. The attributes come from the `weaveffi` crate; the same crate's -macro generates the producer glue when you compile the library. - -```rust -/// Catalog operations. -#[weaveffi::module] -pub mod inventory { - use std::sync::Arc; - - /// A product in the catalog. - #[weaveffi::record] - #[derive(Clone)] - pub struct Product { - /// Stable identifier. - pub id: i32, - pub name: String, - pub price: f64, - pub tags: Vec, - /// The warehouse that stocks it, if known. - pub warehouse: Option>, - } - - /// Product availability. - #[weaveffi::enumeration] - #[repr(i32)] - #[derive(Clone, Copy)] - pub enum Availability { - InStock = 0, - OutOfStock = 1, - Preorder = 2, - } - - /// A stocking location. - #[weaveffi::interface] - pub struct Warehouse { - code: String, - } - - impl Warehouse { - /// Open a warehouse by code. - pub fn new(code: String) -> Warehouse { - Warehouse { code } - } - - /// The warehouse code. - pub fn code(&self) -> String { - self.code.clone() - } - } - - /// Consumer-implemented stock observer. - #[weaveffi::callback_interface] - pub trait StockObserver: Send + Sync { - /// A product's availability changed. - fn on_change(&self, product: &Product, now: Availability); - } - - /// Look up a product by ID. - #[weaveffi::export] - pub fn get_product(id: i32) -> Option { - let _ = id; - None - } - - /// Watch for availability changes; returns the observer count. - #[weaveffi::export] - pub fn watch(observer: Arc) -> i32 { - let _ = observer; - 1 - } - - /// Replaced by `search_v2` in 0.3.0. - #[weaveffi::export] - #[deprecated(since = "0.2.0", note = "use search_v2 instead")] - pub fn search(query: String, limit: i32) -> Vec { - let _ = (query, limit); - Vec::new() - } - - /// A nested namespace. - #[weaveffi::module] - pub mod nested { - /// Lives inside `inventory::nested`. - #[weaveffi::export] - pub fn helper() -> i32 { - 0 - } - } -} -``` - -### 2. Run `weaveffi extract` - -```sh -weaveffi extract src/lib.rs # YAML to stdout -weaveffi extract src/lib.rs -o api.yml # YAML to file -weaveffi extract src/lib.rs -f json -o api.json # JSON to file -weaveffi extract src/lib.rs | weaveffi generate -o generated -``` - -The extracted IDL is validated automatically and **extraction fails loudly** -if the result would not generate, for example an `Arc` whose type the -source never declares, a duplicate name, or a callback-interface method that -returns a string. Pass `--warn` to downgrade those errors to a `warning:` line -on stderr and emit the IDL anyway, which is useful when bootstrapping from -source that references types you have not declared yet: - -```sh -weaveffi extract src/lib.rs --warn # best-effort, errors as warnings -``` - -Type *syntax* WeaveFFI can't express at all (a raw pointer, a `Box`, a -tuple) is a hard error even under `--warn`; those are parse-time rejections, -not validation findings. See [What is rejected](#what-is-rejected). - -### 3. Generate directly, or validate and generate the IDL - -Skip the intermediate file and generate from the source: - -```sh -weaveffi generate src/lib.rs -o generated/ -``` - -Or commit the derived IDL and feed that to the rest of the toolchain: - -```sh -weaveffi extract src/lib.rs -o api.yml -weaveffi validate api.yml -weaveffi generate api.yml -o generated/ -``` - -## Reference - -### CLI command - -``` -weaveffi extract [--output ] [--format ] [--warn] +# Extracting an IDL from Rust + +The CLI reads annotated Rust directly: `weaveffi generate src/lib.rs` lowers +the source to the IR in memory with the same extractor the +`#[weaveffi::module]` macro uses, so the bindings and the compiled symbols +come from one parse. `weaveffi extract` writes that IR out as an IDL +document, which is useful for reviewing the API surface, committing a +canonical IDL next to the source, or handing the API to a non-Rust +implementation. + +```bash +weaveffi extract src/lib.rs # YAML to stdout +weaveffi extract src/lib.rs -o api.yml # YAML to a file +weaveffi extract src/lib.rs -f json -o api.json # JSON (or -f toml) ``` -| Flag | Default | Description | -|------|---------|-------------| -| `` | required | Path to a `.rs` source file | -| `-o`, `--output` | stdout | Write to a file instead of stdout | -| `-f`, `--format` | `yaml` | Output format: `yaml`, `json`, or `toml` | -| `--warn` | off | Downgrade validation errors to warnings and emit the IDL anyway | - -### Attribute reference - -The extractor matches a marker by its final path segment, so both the -namespaced form (`#[weaveffi::record]`) and a bare form (`#[record]`) resolve -identically. Prefer the namespaced form: it is what the `#[weaveffi::module]` -macro consumes, and it reads unambiguously. - -| Attribute | Where it goes | Effect | -|-----------|---------------|--------| -| `#[weaveffi::module]` | inline `mod` | Marks an exported namespace. Required: only modules carrying it are extracted. Modules may nest. | -| `#[weaveffi::export]` | free `fn` | Emits a [`Function`] in the enclosing module. `async fn` sets `async: true`; a `Result` return sets `throws: true` (the IDL return type is `T`). | -| `#[weaveffi::record]` | named-field `struct` | Emits a [`StructDef`]. Fields may be any IDL type, including `Arc` and `Option>` object references. | -| `#[weaveffi::interface]` | `struct` with an inherent `impl` block | Emits an [`InterfaceDef`]. The `impl` block's `pub fn`s become constructors (no receiver, returning `Self`/`Arc`, optionally in a `Result`), methods (`&self` or `self: Arc` receivers), and statics (no receiver, any other return). | -| `#[weaveffi::callback_interface]` | `trait` | Emits a [`CallbackInterfaceDef`]. Every trait method is a consumer-implemented callback taking `&self`. | -| `#[weaveffi::error]` | `enum` with explicit discriminants | Emits the module's error domain. Every variant needs an explicit `= N` discriminant; the first doc line is the code's message. Named-field variants emit the code's payload `fields:` (and require a primitive repr such as `#[repr(i32)]`). | -| `#[weaveffi::enumeration]` + `#[repr(i32)]` | `enum` | Emits an [`EnumDef`]. Every variant must have an explicit `= N` discriminant. | -| `#[weaveffi::cancellable]` | exported `async fn` | Sets `cancellable: true` and strips the trailing `weaveffi::CancelToken` parameter from the IDL signature. | -| `#[deprecated(note = "...")]` | exported `fn`, interface, record, enum, callback interface | Populates `deprecated` with the note. Bare `#[deprecated]` sets `deprecated = "deprecated"`; a `since = "..."` is parsed but has no IDL field (schema 0.9.0 dropped `since`). | - -[`Function`]: https://weaveffi.com/api/rust/weaveffi_ir/struct.Function.html -[`StructDef`]: https://weaveffi.com/api/rust/weaveffi_ir/struct.StructDef.html -[`InterfaceDef`]: https://weaveffi.com/api/rust/weaveffi_ir/struct.InterfaceDef.html -[`CallbackInterfaceDef`]: https://weaveffi.com/api/rust/weaveffi_ir/struct.CallbackInterfaceDef.html -[`EnumDef`]: https://weaveffi.com/api/rust/weaveffi_ir/struct.EnumDef.html - -Doc comments (`///`) on items, fields, methods, and enum variants become the -`doc` field in the IR. - -The module-level `#[weaveffi::callback]` and `#[weaveffi::listener]` markers -from schema 0.8 are gone. A consumer-implemented hook is now a -`#[weaveffi::callback_interface]` trait passed as `Arc`, and a -`handle` is now an interface object passed as `Arc` or `&T`. - -> **Macro versus extraction.** Both the CLI extractor and the -> `#[weaveffi::module]` proc-macro understand the full annotation surface -> above, including interfaces, callback interfaces, error domains, async, -> iterators, rich enums, maps, and objects inside records and collections. -> A hand-authored IDL can additionally carry metadata that source can't -> express (record field defaults and a code `doc:` separate from its -> `message:`), which is why some samples keep a committed YAML IDL for -> generation. See -> [Feature support](producer-macro.md#feature-support) for the macro's -> current matrix. - -### Type mapping - -| Rust type | WeaveFFI TypeRef | IDL string | -|-----------|------------------|------------| -| `i8`, `i16`, `i32`, `i64` | `I8`, `I16`, `I32`, `I64` | `i8`, `i16`, `i32`, `i64` | -| `u8`, `u16`, `u32`, `u64` | `U8`, `U16`, `U32`, `U64` | `u8`, `u16`, `u32`, `u64` | -| `f32`, `f64` | `F32`, `F64` | `f32`, `f64` | -| `bool` | `Bool` | `bool` | -| `String`, `&str` | `StringUtf8` | `string` | -| `Vec`, `&[u8]` | `Bytes` | `bytes` | -| `Vec`, `&[T]` | `List(T)` | `[T]` | -| `Option` | `Optional(T)` | `T?` | -| `HashMap`, `BTreeMap` | `Map(K, V)` | `{K:V}` | -| `weaveffi::Iter` | `Iterator(T)` | `iter` | -| `Arc`, `&T` (`T` a `#[weaveffi::interface]`) | `Named("T")` | `T` | -| `Arc` (`Trait` a `#[weaveffi::callback_interface]`) | `Named("Trait")` | `Trait` | -| `weaveffi::CancelToken` | (removed from the signature) | | -| Any other identifier | `Named(name)` | `name` | - -Compositions work recursively: `Option>` becomes `[i32]?`, -`Vec>` becomes `[string?]`, `Vec>` becomes -`[Gadget]`, and `weaveffi::Iter>` becomes `iter`. - -A reference is a producer-side calling convention, not an IDL distinction: -`&str` and `String` both extract as `string`, `&T` and `Arc` both extract -as the interface `T`. The macro uses the spelling to decide whether the thunk -lends a borrow or takes a new strong reference; the IDL sees the same type -either way. `Arc` in an interface's return position names the interface -itself. A `Named` reference is resolved by the validator to a record, enum, -interface, or callback interface declared somewhere in the API; a name nothing -declares is a validation error (downgradable with `--warn`). - -### What is rejected - -The extractor refuses type syntax that has no ABI representation. These are -`syn` errors with a span, so `weaveffi extract` prints the offending line; the -macro reports the same message as a compile error (each is pinned by a -`trybuild` test under `crates/weaveffi-macros/tests/ui/`): - -| Rust syntax | Diagnostic | -|-------------|------------| -| `*const T`, `*mut T` | `` raw pointers cannot cross the FFI boundary; declare the pointee as a #[weaveffi::interface] and pass it as `&T` or `Arc` `` | -| `Box`, `Box`, `Rc` | `` `Box` cannot cross the FFI boundary; objects and callback interfaces are shared, so spell them as `Arc` / `Arc` `` | -| `&mut T` parameter | `` `&mut` parameters cannot cross the FFI boundary; take the value by `&T` or by value and return the updated result `` | -| `&mut self` or by-value `self` receiver | `` interface methods must take `&self` or `self: Arc`; use interior mutability (Mutex, RwLock, atomics) for mutable state, because the object is shared across the FFI boundary `` | -| `Arc` naming two traits | `` a trait object must name exactly one #[weaveffi::callback_interface] trait (plus optional `Send`/`Sync` bounds) `` | -| tuples, `impl Trait`, function pointers, arrays | `unsupported type syntax` | -| `weaveffi::Handle` | no longer exists; extracts as a `Named("Handle")` reference and fails validation with `unknown type reference: Handle` (the macro fails to compile it) | - -Interface objects must be spelled `Arc` or `&T` and callback interfaces -`Arc` because both are shared across the boundary: the producer -and the consumer each hold references, so a uniquely owned `Box` or a -single-threaded `Rc` cannot describe them. - -### Round-trip integrity - -The `roundtrip_kitchen_sink` integration test in -`crates/weaveffi-cli/tests/extract_roundtrip.rs` proves that the -hand-annotated form of the kitchen-sink IDL -(`crates/weaveffi-cli/tests/fixtures/kitchen_sink_annotated.rs`) round-trips -through `weaveffi extract` and matches the original IR for every supported -feature: modules, nested modules, records (including object and optional -object fields), enums, interfaces with constructors, methods, and statics, -callback interfaces, error domains, per-function `throws`, every primitive -type, borrowed spellings, optional/list/map composites, objects inside lists -and iterators, `Interface?` in both directions, async, cancellable, and -deprecated. - -The gaps the test deliberately tolerates are listed at the top of that -fixture and reproduced in [Pitfalls](#pitfalls). - -## Pitfalls - -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. - -- **An error code's `doc:` separate from its `message:`.** In Rust the first - doc line on a `#[weaveffi::error]` variant is the code's message; the IDL - can carry both, so a distinct `doc:` is dropped on round-trip. The - round-trip test compares codes by name, value, and message only. -- **Record field default values.** The IDL's `default:` field cannot be - derived from Rust syntax (Rust struct fields have no default expressions). -- **Doc comments on parameters.** Rust accepts `///` on `fn` parameters but - most formatters strip them; when present, the extractor preserves them, but - plan for `Param.doc` to be lossy. -- **`#[deprecated(since = "...")]` versions.** Schema 0.9.0 has no `since` - field, so the version is dropped; only the `note` survives as - `deprecated:`. -- **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. -- **Generics, trait `impl` blocks, and macros.** The extractor never resolves - generics or expands macros, and it only reads the inherent `impl` block of - a `#[weaveffi::interface]` type. Items produced by proc-macros and - declarative macros are invisible. -- **Non-`pub` `impl` items.** Only `pub fn`s in an interface's `impl` block - are exported; private helpers stay private. Free functions need the - `#[weaveffi::export]` marker regardless of visibility. -- **External `mod foo;` declarations.** Only inline modules (`mod foo { ... }`) - are processed; declarations that point to other files are skipped. -- **Tuple and unit structs.** Only structs with named fields work with - `#[weaveffi::record]`. -- **Enum discriminants are mandatory.** C-style enums need `#[repr(i32)]` - with explicit `= N` discriminants, and rich (payload-carrying) enum - variants must use named fields; tuple-style variants are rejected. -- **`Result` hides `E`.** The extractor records only `throws: true`; - the error domain it maps to is the module's (or an ancestor's) - `#[weaveffi::error]` enum. A `Result` still extracts as - `throws: true`, which the validator rejects if no domain is in scope. +| Flag | Default | Meaning | +|------|---------|---------| +| `-o`, `--output` | stdout | Write to a file | +| `-f`, `--format` | `yaml` | `yaml`, `json`, or `toml` | +| `--lenient` | off | Emit the IDL even if it doesn't validate | + +The extracted document is validated, and extraction fails with the +validator's diagnostics if the result wouldn't generate (a duplicate name, an +unknown type, a callback method returning a string). `--lenient` prints those +diagnostics as a warning and emits the IDL anyway, which helps when the +source references types declared somewhere the extractor can't see. Rust +syntax with no ABI representation (a raw pointer, a `Box`, a tuple) is a hard +error even with `--lenient`. + +The extracted IDL carries no package identity. When you generate from it, +the identity comes from `[package]` in `weaveffi.toml` (IDL rules), not from +`Cargo.toml`, so set `name`, `c_prefix`, and `library` to match the crate if +the generated bindings must load the Rust library. + +## What the extractor reads + +Only inline modules marked `#[weaveffi::module]` in the input file are read, +recursively. Inside them it reads the items carrying the markers listed in +[the producer macro guide](producer-macro.md#the-attributes), the `pub fn`s of +each interface's inherent `impl` block, doc comments, and +`#[deprecated(note = "...")]`. Markers match by their last path segment, so +`#[weaveffi::record]` and a bare `#[record]` are equivalent. + +## Type mapping + +| Rust | IDL | +|------|-----| +| `i8`..`i64`, `u8`..`u64`, `f32`, `f64`, `bool` | same | +| `String`, `&str` | `string` | +| `Vec`, `&[u8]` | `bytes` | +| `Vec`, `&[T]` | `[T]` | +| `Option` | `T?` | +| `HashMap`, `BTreeMap` | `{K:V}` | +| `weaveffi::Iter` | `iter` | +| `&T`, `Arc` (interface), `Arc` | `T` | +| `Arc` (callback interface) | `Trait` | +| `weaveffi::CancelToken` | removed; sets `cancellable: true` | +| `Result` | return `T`, sets `throws: true` | +| any other name | that name, resolved by the validator | + +Compositions map recursively: `Option>` is `[i32]?` and +`Vec>` is `[Gadget]`. An `async fn` sets `async: true`. A +cross-module reference is emitted with its owning module's path +(`kv.Store`) once the API validates. + +## Limits + +The extractor parses syntax; it doesn't compile or expand anything. + +- **One file.** Every annotated module must be inline (`mod kv { ... }`) in + the input file. `mod kv;` declarations pointing at other files are skipped. +- **No macro expansion or generics.** Items produced by macros are invisible, + and only a `#[weaveffi::interface]` type's inherent `impl` block is read. +- **Error docs.** A `#[weaveffi::error]` variant's doc comment becomes the + code's `message:`, so an IDL code's separate `doc:` has no Rust spelling. +- **Deprecation versions.** `#[deprecated(since = "...")]` keeps only the + note; the IDL has no `since` field. +- **Parameter docs.** Rust accepts `///` on parameters, but formatters tend + to strip them; plan for `Param.doc` to be lossy. + +`crates/weaveffi-cli/tests/cli/extract_roundtrip.rs` checks that an annotated +form of the kitchen-sink fixture extracts to the same IR as the YAML, modulo +the error-doc gap above. diff --git a/docs/src/guides/memory.md b/docs/src/guides/memory.md deleted file mode 100644 index c0ed618f..00000000 --- a/docs/src/guides/memory.md +++ /dev/null @@ -1,465 +0,0 @@ -# Memory Ownership - -## Overview - -WeaveFFI exposes Rust functionality through a stable C ABI (revision 2). -Because Rust and the consumer languages (C, Swift, Kotlin, Python, ...) have -different memory models, every allocation that crosses the boundary follows -strict ownership rules. - -**Golden rule:** whoever allocates owns it, and ownership must be explicitly -transferred back for deallocation. Rust allocates; the consumer frees through -the designated `weaveffi_free_*` functions, releases object references with -the type's `_destroy` symbol, and hands callback-interface implementations -over with a `free` entry the producer calls when it's done. - -The full release contract, exactly which call a wrapper owes after copying a -returned value or a collection element, is stated once, in -`weaveffi_core::plan` (`ReturnFree` for returns, `ElemFree` for array, map, -and iterator elements). Every generated wrapper renders that plan, and this -guide describes the same rules in prose. The normative statement of the ABI is -[C ABI Contract](../reference/abi.md). - -## When to use - -Read this guide when: - -- You are writing a consumer in C or C++ where the compiler won't free - anything for you. -- You are writing a Rust producer and want to know what the - `#[weaveffi::module]` thunks do with the `Arc`s and `Arc`s you - hand them. -- You are debugging a leak, double-free, or use-after-free in a generated - binding. -- You are extending a generator and need to verify the ownership contract for - a new type. - -For higher-level languages (Swift, Kotlin, Python, .NET, Dart, Ruby, Go) the -generated wrappers handle most of this automatically; the rules below explain -what those wrappers are doing under the hood. - -## Step-by-step - -### Strings - -Rust returns NUL-terminated, UTF-8, heap-allocated C strings. The consumer -must free them with `weaveffi_free_string`. String parameters are borrowed -`const char*` views the caller keeps alive for the call. - -```c -weaveffi_error err = {0}; -const char* echoed = weaveffi_calculator_echo("hello", &err); -if (err.code) { - fprintf(stderr, "%s\n", err.message); - weaveffi_error_clear(&err); - return 1; -} - -printf("result: %s\n", echoed); -weaveffi_free_string(echoed); -``` - -Generated wrappers do the same with `defer`, `finally`, or RAII: - -```swift -let raw = weaveffi_calculator_echo(...) -defer { weaveffi_free_string(raw) } -return String(cString: raw!) -``` - -### Byte buffers - -Byte buffers are returned as `const uint8_t*` plus an `out_len`. Free them -with `weaveffi_free_bytes(ptr, len)`; the length must match what the C ABI -returned. `bytes` parameters are borrowed `(ptr, len)` pairs. - -```c -size_t out_len = 0; -const uint8_t* buf = weaveffi_module_get_data(&out_len, &err); -if (err.code) { - weaveffi_error_clear(&err); - return 1; -} - -process_data(buf, out_len); -weaveffi_free_bytes((uint8_t*)buf, out_len); -``` - -### Buffered values: records, rich enums, optionals, lists, and maps - -Records, rich enums, optionals (except `Interface?`), lists, and maps cross -the boundary by value as one serialized -[value buffer](../reference/value-buffers.md) each, no matter how deeply they -nest, so their ownership rules are exactly the byte-buffer rules: - -- **As a parameter**, the caller owns the encoding, keeps it alive for the - duration of the call, and frees (or reuses) it afterward; the callee never - frees it. -- **As a return**, the producer allocates the encoding and hands it back as a - `const uint8_t*` return value plus a `size_t* out_len` out-parameter. The - consumer decodes it, then releases it with a single - `weaveffi_free_bytes(ptr, len)`. There are no per-element frees: the - strings, nested records, and inner collections all live inside the one - buffer. - -```c -size_t out_len = 0; -const uint8_t* buf = weaveffi_contacts_load(42, &out_len, &err); -if (err.code) { - weaveffi_error_clear(&err); - return 1; -} - -/* decode the record's fields out of buf ... */ -weaveffi_free_bytes((uint8_t*)buf, out_len); -``` - -Generated wrappers hide the encoding entirely: they pack and unpack the -buffer into an idiomatic value type (a data class, struct, or sealed class -hierarchy) and free the returned buffer for you. - -One exception to "the buffer is just bytes": a buffer that contains objects -also carries references. See [Objects inside value buffers](#objects-inside-value-buffers). - -### Interface objects - -Interface objects are **reference counted by the producer**. On the Rust side -every `#[weaveffi::interface]` value is an `Arc`; at the ABI a `{tag}*` is -one strong reference to it. Each interface exports two lifecycle symbols: - -```c -weaveffi_kv_Store* weaveffi_kv_Store_clone(const weaveffi_kv_Store* self); -void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self); -``` - -`_clone` returns a new strong reference to the same object (the pointer value -is identical) and `_destroy` releases one. The object is dropped when the last -reference goes, wherever that reference lived: a consumer wrapper, a record -field, a list element, an in-flight async call, or the producer's own state. -Both accept null as a no-op. - -The rule for every position an object can appear in: - -| Position | Direction | Rule | -|----------|-----------|------| -| top-level parameter (`Store`, `Store?`) | consumer to producer | **borrowed** for the call; the consumer keeps its reference and the producer clones if it retains the object | -| return, async result, iterator element | producer to consumer | **one strong reference transfers**; the consumer adopts it and owes one `_destroy` | -| callback-interface method parameter | producer to consumer | **one strong reference transfers** (the slot is `{tag}*`, not `const`); the consumer adopts it | -| inside a value buffer (any direction) | either | the `u64` token **carries one strong reference**; the reader adopts it | - -A consumer-side sequence in C, using the `kvstore` sample: - -```c -weaveffi_error err = {0}; - -weaveffi_kv_Store* store = weaveffi_kv_Store_open("/tmp/db", &err); -if (err.code) { - weaveffi_error_clear(&err); - return 1; -} - -/* `share` returns the receiver with one more reference: same pointer, - two references to release. */ -weaveffi_kv_Store* again = weaveffi_kv_Store_share(store, &err); -assert(again == store); - -int64_t n = weaveffi_kv_Store_count(store, &err); /* borrows */ - -weaveffi_kv_Store_destroy(store); /* releases one */ -n = weaveffi_kv_Store_count(again, &err); /* still alive */ -weaveffi_kv_Store_destroy(again); /* last one; dropped */ -``` - -A method never frees its receiver, and neither does any other function that -takes an object parameter, even a `close`-style one: the only symbol that -releases a reference is `_destroy`. Because the count lives in the producer, -a consumer may hold several wrappers for one object, and each wrapper releases -its own reference independently. - -Every generated wrapper owns exactly one reference and releases it -deterministically, with a garbage-collection backstop where the language has -one: - -| Target | Deterministic release | Backstop | -|--------|-----------------------|----------| -| Swift | `deinit` on the `final class` | (ARC is deterministic) | -| Kotlin | `close()` (`AutoCloseable`) | `java.lang.ref.Cleaner` | -| Python | `close()`, `with` block | `__del__` | -| Node.js | `close()`, `Symbol.dispose` | `FinalizationRegistry` | -| .NET | `Dispose()` (`IDisposable`) | finalizer | -| Dart | `close()` | `NativeFinalizer` | -| Go | `Close()` | `runtime.SetFinalizer` | -| Ruby | `close` | `FFI::AutoPointer` | -| C++ | destructor (RAII) | none needed | - -Using a wrapper after `close()` raises the target's programming-error idiom -(for example a Python `WeaveFFIError` "used after close()" or a C# -`ObjectDisposedException`); it never reaches the producer. - -The generated Swift wrapper, for reference: - -```swift -public final class Store { - let ptr: OpaquePointer - init(ptr: OpaquePointer) { self.ptr = ptr } - deinit { weaveffi_kv_Store_destroy(ptr) } -} -``` - -### Objects inside value buffers - -When an interface appears inside a buffered type (a record field, a list -element, a map value, an optional inside a record), the encoder writes a -`u64` **object token**: the object's pointer, carrying exactly one strong -reference. Whoever decodes the buffer adopts that reference. - -- A **consumer encoding** a buffer that contains an object it holds must call - `_clone` and write the returned pointer, never the pointer its wrapper still - owns. The generated bindings do this (`_clone_ptr()` in Python, - `weaveffi_kv_Store_clone(ptr)` in Swift, and so on). -- A **consumer decoding** a buffer wraps each token in a new object wrapper - that will `_destroy` it. Decoding a returned `[Store]` of three elements - therefore yields three wrappers, each owing one release, and then one - `weaveffi_free_bytes` for the buffer itself. -- A **producer** writes `Arc::into_raw(arc.clone())` and reads with - `Arc::from_raw`; the macro's generated `BufferValue` implementation does - both for you. -- Because each token is one reference, a buffer that contains objects is - **decoded exactly once**. Decode it twice and the second reader adopts - references that no longer exist. Generated bindings encode a fresh buffer per - call and never reuse one. - -The `kvstore` sample exercises every shape: `describe` returns a `StoreInfo` -record whose `store` field is the receiver and whose `mirror` is `Store?`, -`open_many` returns `[Store]`, and `total_count([Store], StoreInfo?)` takes -objects inside buffers as parameters. - -### Callback interfaces - -A callback interface is memory the **consumer** owns and the **producer** -borrows for as long as it likes. A callback-interface parameter lowers to two -slots, `void* ctx` and `const {tag}_vtable* vtable`: - -- The consumer allocates `ctx` (typically a boxed object or a key into a - handle map that keeps the implementing object alive) and points `vtable` at - a process-wide static with one function pointer per method plus the trailing - `free`. -- The producer may call any entry any number of times, from any thread, - until it calls `free(ctx)` **exactly once**, after which it never touches - `ctx` again. On the Rust side the pair lives inside an `Arc`, so - the producer clones it freely and `free` fires when the last clone drops - (the `events` sample's `clear_subscribers` test asserts one `free` per - subscriber). -- Strings, bytes, and buffered method arguments are borrowed for the duration - of the call; the consumer copies or decodes them before returning. Object - arguments transfer one strong reference the consumer adopts. Method returns - are limited to `void`, scalars, `bool`, and C-style enums, so nothing the - consumer allocates crosses back. -- A consumer implementation that fails reports through the method's - `out_err` with `weaveffi_error_set(out_err, -4, message)`; the runtime copies - the borrowed message with the producer's allocator, so the consumer must not - write `message` itself. See [Error Handling](errors.md#foreign-errors). - -### Iterator elements - -An `iter` return hands the consumer an opaque iterator handle, not a -buffer, so there is nothing to free on launch. Ownership flows per step: - -- Each `_next` call writes an element the consumer now owns. After copying or - decoding it, release it per its type: `weaveffi_free_string` for strings, - `weaveffi_free_bytes` for bytes and for buffered elements (records, rich - enums, composites), `_destroy` for object elements (`iter` yields one - strong reference per step), nothing for by-value elements. -- The handle is released with the iterator's own `_destroy` symbol, exactly - once: eagerly on exhaustion, and from the wrapper's disposal idiom (RAII - destructor, finalizer, `close()`, generator cleanup) when iteration is - abandoned early. - -Generated wrappers do both for you; they surface `iter` as the target's -native lazy iteration idiom and pull one element per consumer step. See the -[IDL reference](../reference/idl.md#iterator-types). - -### Sync versus async returns - -Everything above describes **synchronous** returns: the consumer receives an -owned value and owes the matching release call after copying it. - -**Async results follow the same owned-value rule.** The buffers passed to an -async completion callback (strings, bytes, and the serialized value buffers of -buffered results) are owned by the consumer: copy or decode them, then release -them with `weaveffi_free_string` or `weaveffi_free_bytes`. A non-null error is -heap-boxed and released with `weaveffi_error_free` after its code, message, -and payload are copied. Object results (including `Interface?`) transfer one -strong reference the callback adopts, exactly as a synchronous object return -would. An in-flight async method holds its own reference to its receiver, so -destroying the consumer's wrapper while the call is pending is safe. See -[Result ownership and threading](async.md#result-ownership-and-threading). - -### Error struct lifecycle - -Every synchronous C ABI function takes a trailing `weaveffi_error* out_err`. -On failure Rust writes a non-zero `code`, a Rust-allocated `message`, and -(for codes that declare payload fields) a Rust-allocated payload buffer. -Clearing the error frees both: - -```c -weaveffi_error err = {0}; - -int32_t result = weaveffi_calculator_div(10, 0, &err); -if (err.code) { - fprintf(stderr, "error %d: %s\n", err.code, err.message); - weaveffi_error_clear(&err); -} - -result = weaveffi_calculator_add(1, 2, &err); -``` - -Generated wrappers clear the slot for you. On a `throws: true` function they -convert non-zero codes into the module's typed domain error (`throw`, `raise`, -`(T, error)`); on a non-throwing function a non-zero code only ever reports a -producer bug, so the wrapper panics or traps instead. See the -[Error Handling Guide](errors.md). - -`weaveffi_error_clear` is idempotent: it frees the message and payload and -nulls the pointers, so clearing an already-cleared slot is safe. Async -completion callbacks use a different symbol: the error they receive is -heap-boxed and consumer-owned, so it is released exactly once with -`weaveffi_error_free`, which clears the slot and then frees the box itself. -`weaveffi_error_set` is the one runtime symbol a consumer calls to *fill* a -slot, from inside a callback-interface implementation; it copies the borrowed -message so the producer can free it with its own allocator. - -### Thread safety - -Every `#[weaveffi::interface]` type is `Send + Sync` (the macro asserts it), -so a producer built with the macro is safe to call from any thread, and the -generated wrappers place no thread restriction of their own. Callback-interface -methods likewise may be invoked from any producer thread, including the thread -an async future runs on, so consumer implementations must be thread-safe or -must hop to their own scheduler. A hand-written producer that keeps -non-thread-safe state should say so in its documentation; consumers of such a -library synchronise externally: - -```swift -let queue = DispatchQueue(label: "com.app.weaveffi") -queue.sync { - let result = Calculator.add(a: 1, b: 2) -} -``` - -## Producer-side rules - -If you write the producer with `#[weaveffi::module]`, the thunks apply the -rules above for you. What you see in safe Rust: - -- An object parameter typed `&T` is the borrowed reference; typed `Arc` - or `Option>`, the thunk has already cloned it, so you may store it. -- An object you return (`Self`, `Arc`, `Option>`, `Vec>`, - a record with an `Arc` field) transfers one reference per object. A - method that returns `self` on a `self: Arc` receiver hands back the - same pointer with a fresh reference. -- An `Arc` parameter is the consumer's callback; clone and store it - as needed. Its `free` runs when your last clone drops. -- **A callback-interface method may unwind.** A consumer failure surfaces to - the producer as a panic-like unwind that the enclosing thunk catches and - reports as `FOREIGN_ERROR_CODE`. If you hold a `std::sync::Mutex` guard - across the call, the mutex is poisoned when the unwind passes through, and - your next `lock().unwrap()` panics (which the consumer then sees as a - producer panic, `-2`). Either recover from poisoning with - `unwrap_or_else(PoisonError::into_inner)` or, better, snapshot the state you - need under the lock, release the lock, and only then call out. This is the - discipline `samples/events/src/lib.rs` follows in `EventBus::publish`: - -```rust -// Snapshot so no lock is held while the consumer runs. -let subs: Vec> = self - .subscribers - .lock() - .unwrap_or_else(PoisonError::into_inner) - .clone(); -let mut delivered = 0; -for sub in &subs { - match sub.route(message.topic.clone()) { - Delivery::Skip => {} - Delivery::Accept => { - sub.on_message(&message); - delivered += 1; - } - Delivery::AcceptAndStop => { - sub.on_message(&message); - delivered += 1; - break; - } - } -} -``` - -- Never free anything a thunk lends you (`&str`, `&[u8]`, `&T`); the thunk - owns the lifetime of borrowed arguments. - -## Reference - -| Resource | Allocator | Free function | Notes | -|----------|-----------|---------------|-------| -| Returned string | Rust | `weaveffi_free_string` | Every `const char*` return | -| Returned bytes | Rust | `weaveffi_free_bytes` | Pass both pointer and length | -| Returned buffered value (record, rich enum, optional, list, map) | Rust | `weaveffi_free_bytes` | One serialized buffer; decode, then free once with `out_len` | -| Buffered parameter | Caller | none owed to Rust | Borrowed for the call; the caller keeps ownership | -| Object reference (return, async result, iterator element) | Rust | the type's `_destroy` | One strong reference per pointer; `_clone` to take another | -| Object parameter | Consumer | none owed | Borrowed for the call; the producer clones if it retains | -| Object token inside a value buffer | Rust | the type's `_destroy` on the adopted pointer | One reference per token; decode the buffer exactly once | -| Object passed to a callback method | Rust | the type's `_destroy` | The consumer adopts one reference | -| Callback-interface `ctx` | Consumer | the vtable's `free(ctx)`, called by the producer | Exactly once, when the producer's last clone drops | -| Iterator handle | Rust | the iterator's `_destroy` | Exactly once: on exhaustion or abandonment | -| Iterator element | Rust | `weaveffi_free_string` / `weaveffi_free_bytes` / `_destroy` / nothing | Each `_next` yields a consumer-owned element | -| Async result buffer | Rust | `weaveffi_free_string` / `weaveffi_free_bytes` | Consumer-owned: copy or decode, then free | -| Async object result | Rust | the type's `_destroy` | Callback adopts one reference | -| Sync error slot (message and payload) | Rust | `weaveffi_error_clear` | Clears code, frees message and payload; idempotent | -| Async boxed error | Rust | `weaveffi_error_free` | Frees message, payload, and the box; exactly once | -| Cancel token | Rust | `weaveffi_cancel_token_destroy` | Keep alive until the completion callback fires | - -## Pitfalls - -- **Use-after-free**: reading a string after freeing it, or calling a method - through a pointer whose last reference was destroyed. Once the consumer - releases something, the pointer is invalid. -- **Double-free**: freeing the same pointer twice (calling - `weaveffi_free_string` twice, or `_destroy` on a reference the wrapper - already released). Note that `_destroy` on two *different* references to - the same object (one from `open`, one from `share` or `_clone`) is correct, - not a double-free. -- **Forgetting to clone before encoding**: a consumer that writes a wrapper's - own pointer into a value buffer as an object token hands away a reference it - doesn't have; the wrapper's later `_destroy` then over-releases. Always - `_clone` first. -- **Decoding an object-carrying buffer twice**: each token is one reference; - the second decode adopts references that don't exist. -- **Freeing the receiver inside a producer function**: only `_destroy` - releases a reference. A producer that frees a borrowed receiver causes a - double-free as soon as the consumer's wrapper releases its own reference. -- **Holding a lock across a callback-interface call**: the call may unwind - with a foreign error and poison the mutex. Snapshot, release, then call. -- **Wrong length to `weaveffi_free_bytes`**: always free with the exact length - the C ABI returned in `out_len`. -- **Forgetting to clear error structs**: `err.message` and `err.payload_ptr` - are Rust-allocated; failing to call `weaveffi_error_clear` after a non-zero - code leaks them. -- **Writing `out_err.message` yourself in a callback implementation**: the - producer frees that pointer with Rust's allocator. Call `weaveffi_error_set` - instead. -- **Manually freeing pointers passed in as borrowed parameters**: borrowed - inputs (`const char*`, `(ptr, len)` pairs, `const T*`) are owned by the - caller and must not be passed to `weaveffi_free_*` or `_destroy`. -- **Freeing pieces of a returned buffer**: a returned `[string]` or - `[Contact]` is one serialized buffer; the strings and records you decode - out of it are copies. Free the buffer once with - `weaveffi_free_bytes(ptr, out_len)` and nothing else (objects inside it are - the exception: each adopted token owes one `_destroy`). -- **Leaking an async result buffer**: buffers passed to a completion callback - are consumer-owned. Copy or decode the data, then free the buffer with the - matching runtime symbol; a callback that only copies leaks the producer's - allocation. -- **Destroying an iterator handle twice**: destroy it once, on exhaustion or - when abandoning iteration early. Generated wrappers null the handle so their - disposal idiom can't double-destroy; hand-written C consumers must do the - same. diff --git a/docs/src/guides/packaging.md b/docs/src/guides/packaging.md index 97b3d0a2..07cd98c6 100644 --- a/docs/src/guides/packaging.md +++ b/docs/src/guides/packaging.md @@ -1,269 +1,133 @@ -# Packaging and Distribution +# Packaging -## Overview - -`weaveffi generate` emits binding *source*: the consumer still has to compile -it or point it at a native library. `weaveffi package` goes one step further -and assembles ready-to-publish packages that bundle a prebuilt native library -for each target platform, laid out the idiomatic way each ecosystem expects. -The goal is that `dotnet add package`, `pip install`, `gem install`, -`npm install`, and friends "just work" with no local toolchain on a supported -platform. +`weaveffi generate` emits binding source. `weaveffi package` goes one step +further: it assembles publishable packages that bundle a prebuilt native +library for each platform, laid out the way each ecosystem resolves native +code, so `pip install`, `npm install`, `dotnet add package`, and friends work +without a local toolchain. ```bash -weaveffi package api.yml --binaries ./prebuilt --target dotnet,python,ruby -o dist +weaveffi package --binaries prebuilt --target python,node,dotnet -o dist ``` -## Choosing where the native libraries come from - -A package can only bundle libraries you have already built. -`weaveffi package` gets them one of two ways: - -- `--binaries `: a directory of prebuilt libraries laid out as - `//`. This is the path CI uses, building each - platform on its own runner and collecting the results. -- `--build `: cross-compile the given Cargo package as a `cdylib` for - each platform's Rust target triple. Convenient locally, but every target - needs its rustup target and a working cross-linker installed - (`rustup target add aarch64-unknown-linux-gnu`, and so on; the Android - targets additionally need the NDK's linker on `PATH`). +Like `generate`, it reads `[project]` and `[package]` from `weaveffi.toml` +(or takes an explicit input), and every package it writes loads the library +named by the identity's `library` field. -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. +## Where the libraries come from -### The `--binaries` layout +Pass exactly one source: -Each platform gets a subdirectory named for its platform id, holding that -platform's shared library (or, for `wasm32`, the `.wasm` module): +- `--binaries `: prebuilt libraries laid out as + `//`. This is the CI path: build each + platform on its own runner, collect the results, package once. +- `--build `: cross-compile a Cargo package as a `cdylib` for each + requested platform. Each platform needs its rustup target and a working + linker (the Android targets need the NDK's linker); a missing target fails + with the `rustup target add` command to run. ```text prebuilt/ - darwin-arm64/libcontacts.dylib - darwin-x64/libcontacts.dylib - linux-x64/libcontacts.so - linux-arm64/libcontacts.so - windows-x64/contacts.dll - android-arm64/libcontacts.so - android-x64/libcontacts.so - wasm32/contacts.wasm + darwin-arm64/libkvstore.dylib + linux-x64/libkvstore.so + windows-x64/kvstore.dll + android-arm64/libkvstore.so + wasm32/kvstore.wasm ``` -A platform with no subdirectory is skipped with a warning, so a partial -matrix still produces artifacts for what is available. When a platform -directory holds more than one library, name the one to bundle after the -resolved package identity (for example `libcontacts.dylib`) to disambiguate. - -## The platform matrix - -Every platform WeaveFFI can bundle, with the identifier each ecosystem uses -for it. A blank cell means that ecosystem has no slot for the platform, and a -packaging backend **skips** a binary it has no slot for (a wheel has no wasm -tag; a NuGet package has no Android RID) rather than failing. +## Platforms + +`--platforms` takes a comma-separated list of platform ids. With `--build`, +**the default is the host platform only**, so a local run packages what you +just built; CI passes the full list. With `--binaries`, the default is every +platform directory present. + +| Platform id | Rust target | Shipped by | +|-------------|-------------|------------| +| `darwin-arm64` | `aarch64-apple-darwin` | every desktop target | +| `darwin-x64` | `x86_64-apple-darwin` | every desktop target | +| `linux-x64` | `x86_64-unknown-linux-gnu` | every desktop target | +| `linux-arm64` | `aarch64-unknown-linux-gnu` | every desktop target | +| `windows-x64` | `x86_64-pc-windows-msvc` | every desktop target | +| `ios-arm64` | `aarch64-apple-ios` | `swift` (static library) | +| `ios-sim-arm64` | `aarch64-apple-ios-sim` | `swift` (static library) | +| `ios-sim-x64` | `x86_64-apple-ios` | `swift` (static library) | +| `android-arm64` | `aarch64-linux-android` | `kotlin` (and `c`) | +| `android-x64` | `x86_64-linux-android` | `kotlin` (and `c`) | +| `wasm32` | `wasm32-unknown-unknown` | `wasm` (and `c`) | + +A requested platform with no library is skipped with a warning, and a target +skips platforms its ecosystem has no slot for (a wheel has no wasm tag, NuGet +has no Android runtime). The command prints one line per target it packaged +and a note naming any target that produced nothing. + +## What each target produces + +| Target | Package layout | +|--------|----------------| +| `c`, `cpp` | Headers plus `lib//` libraries and a `CMakeLists.txt` that picks the host's library | +| `swift` | A SwiftPM package with the desktop libraries under `lib//`; when an iOS slice is included, `C{Module}.xcframework` (iOS device, iOS simulator, and macOS slices, fused with `lipo`) beside `Package.swift`, which then uses it as a binary target | +| `kotlin` | A Gradle module with Android libraries under `jniLibs//` and desktop libraries as classpath resources | +| `node` | A main npm package plus one `optionalDependencies` package per platform, each gated by `os` and `cpu` | +| `wasm` | An npm package with the ES module, its `.d.ts`, and the `.wasm` binary | +| `python` | One platform-wheel tree per platform with the library inside the import package | +| `dotnet` | A NuGet-ready project with libraries under `runtimes//native/` | +| `dart` | A pub package with desktop libraries under `native//` | +| `go` | A Go module with per-platform libraries and a cgo preamble that links the right one | +| `ruby` | One precompiled platform gem per platform | + +Each language page documents its layout in detail. Bundled libraries are +found before the system loader, and `{PREFIX}_LIBRARY` still overrides them. + +## Not automated yet + +- **Publishing.** There's no `weaveffi publish`; run each ecosystem's own + publish command on the output. +- **Building wheels and gems.** The Python and Ruby trees are ready to build + (`python -m build --wheel`, `gem build`), but the CLI doesn't run those + tools or retag wheels. +- **The Node addon.** The N-API addon is compiled at install time + (`node-gyp`), so consumers need a C compiler; only the producer library is + prebuilt. +- **Flutter mobile.** Flutter's native-assets build isn't generated. + +## iOS + +iOS slices are static libraries, so a Rust producer that ships to iOS +declares `crate-type = ["cdylib", "staticlib"]`. The XCFramework's macOS +slice is the `lib{library}.a` Cargo writes next to the macOS dylib; macOS-only +packages keep linking the dylib. Assembly runs `lipo` and +`xcodebuild -create-xcframework`, so it needs macOS: -| Platform id | OS / arch | Rust target | NuGet RID | Node `os`/`cpu` | Python tag | Ruby platform | Android ABI | -|-------------|-----------|-------------|-----------|-----------------|------------|---------------|-------------| -| `darwin-arm64` | macOS arm64 | `aarch64-apple-darwin` | `osx-arm64` | `darwin`/`arm64` | `macosx_11_0_arm64` | `arm64-darwin` | | -| `darwin-x64` | macOS x64 | `x86_64-apple-darwin` | `osx-x64` | `darwin`/`x64` | `macosx_10_12_x86_64` | `x86_64-darwin` | | -| `linux-x64` | Linux x64 glibc | `x86_64-unknown-linux-gnu` | `linux-x64` | `linux`/`x64` | `manylinux2014_x86_64` | `x86_64-linux` | | -| `linux-arm64` | Linux arm64 glibc | `aarch64-unknown-linux-gnu` | `linux-arm64` | `linux`/`arm64` | `manylinux2014_aarch64` | `aarch64-linux` | | -| `windows-x64` | Windows x64 | `x86_64-pc-windows-msvc` | `win-x64` | `win32`/`x64` | `win_amd64` | `x64-mingw-ucrt` | | -| `android-arm64` | Android arm64 | `aarch64-linux-android` | | | | | `arm64-v8a` | -| `android-x64` | Android x64 (emulator) | `x86_64-linux-android` | | | | | `x86_64` | -| `wasm32` | WebAssembly | `wasm32-unknown-unknown` | | | | | | - -The first five rows are the **desktop** matrix; every dynamic-library -ecosystem (NuGet, npm, wheels, gems, C/C++, Go, Swift, Dart, desktop JVM) -bundles those. The Android rows are consumed only by the `kotlin` target, and -`wasm32` only by the `wasm` target. - -Restrict the build with `--platforms` (a comma-separated list of platform -ids); the default is the full matrix. Restrict the languages with `--target` -exactly as in `weaveffi generate`. The command prints one line per target it -packaged and a `note:` listing any target that produced nothing, either -because it has no binary packaging at all or because no binary was found for a -platform it ships: - -```text -Packaging 'contacts' for platforms: darwin-arm64, linux-x64, wasm32 - dotnet: 3 file(s), 2 bundled binary(ies) - wasm: 4 file(s), 1 bundled binary(ies) -Packaged 2 target(s) into dist +```bash +rustup target add aarch64-apple-ios aarch64-apple-ios-sim +weaveffi package --target swift --build kvstore \ + --platforms ios-arm64,ios-sim-arm64,darwin-arm64 -o dist ``` -The `wasm` target is the one that can come up empty on a desktop-only matrix: -with no `wasm32/` binary it is skipped with a note, and in Emscripten mode it -ships glue only and never bundles a binary. Every other target packages as -long as at least one of its platforms is present. - -## Per-ecosystem layout - -Each target lays the bundled libraries out where its ecosystem resolves -native code automatically. - -### .NET (`dotnet`) - -A single NuGet-ready project with libraries under `runtimes//native/`, -the layout NuGet selects at restore time. The `[DllImport]` library name is -rebound from the WeaveFFI brand to the bundled library's base name, and the -`.csproj` packs the `runtimes/` tree. Just `dotnet add package`. - -### Python (`python`) - -One wheel-ready tree per platform under `python//`, with the library -bundled inside the import package. The loader prefers the bundled library, so -no `WEAVEFFI_LIBRARY` or system install is needed. The generated `setup.py` -forces a non-pure (platform-tagged) wheel; build it with -`python -m build --wheel` and tag it for the target platform before -publishing. - -### Ruby (`ruby`) - -One precompiled platform gem per platform under `ruby//`, with -`s.platform` set and the library bundled under `lib/native/`. The `ffi` loader -prefers the bundled library. - -### Node.js (`node`) +## CI recipe -The idiomatic `optionalDependencies` layout: a main package that depends on -one per-platform package per target (each gated by npm `os`/`cpu`, so only the -matching one installs) under `node/npm/--/`, each bundling its -prebuilt library. Because the Node binding is an N-API addon, the thin addon -is still compiled at install (`node-gyp rebuild`) and links the prebuilt -library from the selected platform package, so no Rust toolchain is needed; a -C compiler and the generated C header (package the `c` target alongside) are. - -### WebAssembly (`wasm`) - -An npm package under `wasm/` containing the ES-module loader -(`.js`), its TypeScript declarations (`.d.ts`), a -README, and the `wasm32` binary copied in as `.wasm`. The -`package.json` lists exactly those files so `npm publish` ships nothing else. -Consumers call the loader's `load…` function with the bundled `.wasm` URL (or -a `BufferSource` or compiled `WebAssembly.Module`) and get the API object back -as a `Promise`. - -Build the binary with `cargo build --release --target wasm32-unknown-unknown` -(the producer crate needs `crate-type = ["cdylib"]`) and place it under -`prebuilt/wasm32/`. In Emscripten mode (`[generators.wasm] emscripten = -true`) the package is glue only: the consumer links the module into their own -Emscripten build, so no binary is bundled and the `wasm32/` directory is not -consulted. - -### Kotlin (`kotlin`) - -A Gradle Android library module under `kotlin/`: `settings.gradle.kts`, -`build.gradle.kts` (`com.android.library` plus the Kotlin Android plugin, -`minSdk = 33` because the wrappers' disposal backstop is -`java.lang.ref.Cleaner`), the Kotlin wrapper under -`src/main/kotlin//WeaveFFI.kt`, and the JNI shim under -`src/main/cpp/` with its own `CMakeLists.txt` and a bundled copy of the C -header under `src/main/cpp/include/`. - -The binaries land in two places depending on the platform: - -- **Android** (`android-arm64`, `android-x64`) libraries are copied to - `src/main/jniLibs//` (`arm64-v8a`, `x86_64`), which the packaged - `build.gradle.kts` registers as a JNI source set. The CMake script links the - shim against the bundled library for the ABI being built, so an app that - depends on the module gets both `.so`s in its APK with no further setup. -- **Desktop JVM** libraries (the five desktop platforms) are copied to - `src/main/resources/natives//`. At runtime the Kotlin loader - picks `natives/-/` from `os.name` and `os.arch`, extracts the - producer library and the shim from the classpath into a temporary - directory, and `System.load`s them, falling back to - `System.loadLibrary("_jni")` on the `java.library.path` when the - resources are absent. When building the shim for a desktop host, pass - `-DWEAVEFFI_PLATFORM_ID=` to CMake so it links the right bundled - library; the generated README has the exact command. - -The `.wasm` binary is ignored by this target. One caveat: the generated JNI -glue always defines `JNI_OnLoad` (it caches the `JavaVM*`, checks the ABI -revision, and resolves the classes callback dispatch and async exception -routing need). Each generated module must therefore link into its own shared -library; compiling the glue for two modules into one `.so` would collide on -the duplicate `JNI_OnLoad` symbol. - -### Swift (`swift`) - -A SwiftPM package that consumes its C ABI through a `binaryTarget` -xcframework. The prebuilt desktop libraries are bundled under -`lib//`; assembling them into the xcframework is the one step that -needs Apple tooling (`lipo` plus `xcodebuild -create-xcframework`, run on -macOS). The generated `README.md` includes the exact recipe. - -### C and C++ (`c`, `cpp`) - -The header (`include/`) plus a prebuilt library under `lib//`, with -a `CMakeLists.txt` that selects the right library for the host and exposes it -as an imported target. `add_subdirectory` and link. The `c` package copies -every binary present (Android and `wasm32` included, since a C consumer may be -an NDK or Emscripten build); the `cpp` package bundles the desktop platforms -only, which are the ones its packaged `CMakeLists.txt` can select among. - -### Go (`go`) - -A Go module that bundles a library per desktop platform under -`lib//`. The cgo preamble adds the matching `${SRCDIR}`-relative -library search path and rpath per GOOS/GOARCH, so `go build` links the right -library with no manual `CGO_LDFLAGS`. The C ABI header is expected at -`../c/include/`, so package the `c` target alongside Go -(`weaveffi package --target c,go`). - -### Dart (`dart`) - -A pub package under `dart/` with the desktop libraries under -`native//`. The packaged `dart:ffi` loader tries the bundled -library for the host OS first, then the bare system name; `WEAVEFFI_LIBRARY` -still overrides. Android libraries are not bundled here: on Flutter they ship -through the app's `jniLibs`, and a `.wasm` module can't be opened with -`DynamicLibrary`. - -## Continuous integration recipe - -In CI the cleanest approach is to build each platform's library on a runner -of that platform (native builds avoid cross-linker setup), collect the -results into the `--binaries` layout, then run `weaveffi package` once. The -matrix below builds a Cargo producer crate (`my-producer`, declaring -`crate-type = ["cdylib"]`) and uploads each library under its platform id, -then a final job assembles the packages. The Android and wasm rows -cross-compile from Linux: the wasm target needs only `rustup target add`, and -the Android targets need the NDK that GitHub's Ubuntu runners ship, exposed -to Cargo through `CARGO_TARGET__LINKER`. +Build each platform natively, upload each library under its platform id, then +package once: ```yaml -name: package -on: - push: - tags: ["v*"] - jobs: build: strategy: matrix: include: - - { platform: darwin-arm64, runner: macos-14, target: aarch64-apple-darwin, lib: libmy_producer.dylib } - - { platform: darwin-x64, runner: macos-13, target: x86_64-apple-darwin, lib: libmy_producer.dylib } - - { platform: linux-x64, runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, lib: libmy_producer.so } - - { platform: linux-arm64, runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, lib: libmy_producer.so } - - { platform: windows-x64, runner: windows-latest, target: x86_64-pc-windows-msvc, lib: my_producer.dll } - - { platform: android-arm64, runner: ubuntu-latest, target: aarch64-linux-android, lib: libmy_producer.so } - - { platform: android-x64, runner: ubuntu-latest, target: x86_64-linux-android, lib: libmy_producer.so } - - { platform: wasm32, runner: ubuntu-latest, target: wasm32-unknown-unknown, lib: my_producer.wasm } + - { platform: darwin-arm64, runner: macos-latest, target: aarch64-apple-darwin, lib: libkvstore.dylib } + - { platform: linux-x64, runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, lib: libkvstore.so } + - { platform: linux-arm64, runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, lib: libkvstore.so } + - { platform: windows-x64, runner: windows-latest, target: x86_64-pc-windows-msvc, lib: kvstore.dll } + - { platform: wasm32, runner: ubuntu-latest, target: wasm32-unknown-unknown, lib: kvstore.wasm } runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - - name: Point Cargo at the NDK linker - if: startsWith(matrix.platform, 'android-') - run: | - bin="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" - case "${{ matrix.target }}" in - aarch64-linux-android) echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$bin/aarch64-linux-android33-clang" >> "$GITHUB_ENV" ;; - x86_64-linux-android) echo "CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=$bin/x86_64-linux-android33-clang" >> "$GITHUB_ENV" ;; - esac - - run: cargo build --release -p my-producer --target ${{ matrix.target }} + - run: cargo build --release --target ${{ matrix.target }} - run: | mkdir -p "prebuilt/${{ matrix.platform }}" cp "target/${{ matrix.target }}/release/${{ matrix.lib }}" "prebuilt/${{ matrix.platform }}/" @@ -284,12 +148,10 @@ jobs: path: prebuilt merge-multiple: true - run: cargo install weaveffi-cli - - run: weaveffi package api.yml --binaries prebuilt --target dotnet,python,node,ruby,kotlin,wasm -o dist - # ... then publish each package with `dotnet nuget push`, `npm publish`, etc. + - run: > + weaveffi package --binaries prebuilt -o dist + --platforms darwin-arm64,linux-x64,linux-arm64,windows-x64,wasm32 ``` -A platform you can't build (no runner, missing target) can simply be dropped -from the matrix; `weaveffi package` warns and produces artifacts for whatever -is present. Each target then bundles the subset of platforms it has a slot -for, so a matrix without the Android rows still yields a `kotlin` package -(desktop JVM only) and one without `wasm32` yields everything except `wasm`. +A platform you can't build can be dropped from the matrix; every target +packages whatever subset is present. diff --git a/docs/src/guides/producer-macro.md b/docs/src/guides/producer-macro.md index 69084af4..1ad5ecb4 100644 --- a/docs/src/guides/producer-macro.md +++ b/docs/src/guides/producer-macro.md @@ -1,153 +1,135 @@ # The Rust Producer Macro -If your producer is written in Rust, the most ergonomic workflow is to write a -normal, safe Rust library, annotate it with the `#[weaveffi::module]` family of -attributes, and let the `weaveffi` crate generate the `#[no_mangle] extern "C"` -thunks that back the stable C ABI. The same annotated source is what -`weaveffi generate src/lib.rs` reads to emit the IDL, the C header, and every -language binding, so the producer you compile and the bindings you ship can't -drift: they are two views of one parse. - -This is the "Rust as the source of truth" model. You never hand-write `unsafe` -FFI glue, and there is no separate IDL file to keep in sync. +A Rust producer is an ordinary library crate. You annotate the modules you +want to export with `#[weaveffi::module]`, tag the items inside, and call +`weaveffi::export_runtime!()` once. The macro emits the `extern "C"` thunks, +marshalling every argument through the audited `weaveffi::abi` runtime, so +the crate contains no `unsafe` glue. The CLI reads the same source +(`weaveffi generate src/lib.rs`) through the same extractor, so the compiled +symbols and the generated bindings can't drift. ## Setup -Add the single `weaveffi` facade crate and build a `cdylib` (plus an `rlib` if -you also want to unit-test the safe functions in-crate): - ```toml -[package] -name = "my-lib" -version = "0.1.0" -edition = "2021" - [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] # add "staticlib" for iOS, "rlib" for in-crate tests [dependencies] -weaveffi = "0.22" +weaveffi = "0.23" ``` -## A complete example +**The prefix is the crate name.** Every C symbol starts with the crate's +library name (`[lib] name`, else the package name with `-` mapped to `_`), +which the macro reads from `CARGO_CRATE_NAME` at expansion time. The CLI +derives the same prefix from `Cargo.toml`, and it isn't configurable: setting +`[package] c_prefix` or `library` in `weaveffi.toml` for a `.rs` input is an +error. A crate named `kvstore` with a module `kv` exports +`kvstore_kv_Store_open`, `kvstore_error_clear`, `kvstore_kv_checksum`, and so +on, and builds `libkvstore.so`. + +**`export_runtime!()` exactly once,** at the crate root. It takes no +arguments and exports the [runtime surface](../reference/abi.md#runtime-surface) +under the crate's prefix: `abi_version`, the error helpers, `free_bytes`, the +cancel-token functions, `debug_live`, and on `wasm32` `alloc` and `dealloc`. +Each top-level `#[weaveffi::module]` additionally exports its contract +checksum, `{prefix}_{module}_checksum()`. -```rust -//! src/lib.rs +## The attributes -/// Arithmetic over 32-bit integers. +| Attribute | On | Effect | +|-----------|----|--------| +| `#[weaveffi::module]` | inline `mod name { ... }` | An exported namespace and the driver of the expansion. Nested `#[weaveffi::module]` modules become IDL submodules. | +| `#[weaveffi::export]` | `fn`, `async fn` | Exports a free function. `Result` makes it throwing; `async fn` makes it async. | +| `#[weaveffi::record]` | struct with named fields | A by-value record, serialized as a value buffer. | +| `#[weaveffi::enumeration]` | `#[repr(i32)]` enum, or enum with named-field variants | A C-style enum (explicit `= N` on every variant) or a rich enum. | +| `#[weaveffi::interface]` | struct plus its inherent `impl` | A reference-counted object type; the `impl`'s `pub fn`s become constructors, methods, and statics. | +| `#[weaveffi::callback_interface]` | `trait Name: Send + Sync` | Methods the consumer implements; accepted as `Arc`. | +| `#[weaveffi::error]` | enum with explicit discriminants | The module's error domain. Must implement `Display`. | +| `#[weaveffi::cancellable]` | exported `async fn` or async method | Takes a `weaveffi::CancelToken` as its last parameter. | + +Only tagged items are exported; private helpers, `use` items, and state are +left alone. Doc comments flow into the IDL and every binding, and +`#[deprecated(note = "...")]` becomes the IDL's `deprecated:` text. + +## Types + +| Rust | IDL | Crosses the ABI as | +|------|-----|--------------------| +| `i8`..`i64`, `u8`..`u64`, `f32`, `f64`, `bool` | same | one value | +| `String`, `&str` | `string` | UTF-8 `(ptr, len)` | +| `Vec`, `&[u8]` | `bytes` | `(ptr, len)` | +| `#[weaveffi::record]` struct, rich enum | the type | value buffer | +| `#[repr(i32)]` enum | the enum | `int32_t` | +| `Option`, `Vec`, `HashMap`, `BTreeMap` | `T?`, `[T]`, `{K:V}` | value buffer | +| `&T`, `Arc`, `Option>` (interface `T`) | `T`, `T?` | object pointer | +| `Arc` (callback interface) | `Trait` | `ctx` plus vtable (parameters only) | +| `weaveffi::Iter` | `iter` | iterator handle (returns only) | +| `weaveffi::CancelToken` | none | the launcher's cancel-token slot | + +A reference (`&str`, `&[u8]`, `&Contact`, `&Store`) is a calling convention, +not an IDL distinction: the thunk lends the lifted argument for the call. +Objects compose with every buffered shape, so `Vec>`, +`Option>`, an `Arc` record field, and `weaveffi::Iter>` all +work. [Extracting an IDL from Rust](extract.md#type-mapping) has the full +mapping. + +## Functions and errors + +```rust #[weaveffi::module] pub mod calculator { - /// The calculator's error domain: the codes its throwing functions report. + /// The calculator's error domain. #[weaveffi::error] #[derive(Debug)] pub enum CalcError { - /// division by zero + /// Division by zero. DivisionByZero = 1, } - /// Add two integers. - #[weaveffi::export] - pub fn add(a: i32, b: i32) -> i32 { - a + b + impl std::fmt::Display for CalcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("division by zero") + } } /// Divide two integers, failing on a zero divisor. #[weaveffi::export] pub fn div(a: i32, b: i32) -> Result { - if b == 0 { - return Err(CalcError::DivisionByZero); - } - Ok(a / b) + a.checked_div(b).ok_or(CalcError::DivisionByZero) } } - -// Emit the fixed runtime surface (memory, error, and cancel-token helpers) -// exactly once per cdylib. -weaveffi::export_runtime!(); ``` -That is the whole producer. Building it yields a shared library exporting -`weaveffi_calculator_add` and `weaveffi_calculator_div` with the exact -signatures the generated C header declares. A `Result` return marks the -function `throws: true` in the IDL: the return type is `T`, and `Err` is -reported through the trailing `out_err` parameter with the code the -`#[weaveffi::error]` enum declares, so every binding surfaces it as a typed -domain error (see [Error Handling](errors.md)). A throwing function needs an -error domain in scope on its module or an ancestor. +A `Result` return is `throws: true` in the IDL with return type `T`; +`()` and `Result<(), E>` return nothing. The `#[weaveffi::error]` enum's +discriminants are the codes (positive and unique), and its runtime message +is the enum's `Display` output; a variant's doc comment is the documented +default message that appears in the IDL and in generated docs. Without a +`Display` impl the macro fails with a trait-bound error naming the enum. -Generate the bindings straight from the same file: +A variant may carry named fields, which travel as the error's structured +payload (this needs a primitive repr): -```bash -weaveffi generate src/lib.rs -o generated --target c,swift,python +```rust +#[weaveffi::error] +#[derive(Debug)] +#[repr(i32)] +pub enum QuotaError { + /// Quota exceeded. + Exceeded { limit: i64, used: i64 } = 3001, + /// Quota service unavailable. + Unavailable = 3002, +} ``` -## The attributes +A module declares at most one domain; it's in scope for that module and every +module nested in it, and a `Result` with no domain in scope is a compile +error. `Result` compiles too (it reports the generic code `-1`), +and any type implementing `weaveffi::ErrorReport` can be an error type. A +panic in producer code is caught at the boundary and reported as code `-2`. +See [Errors and Memory](errors-and-memory.md). -| Attribute | Where it goes | Effect | -|-----------|---------------|--------| -| `#[weaveffi::module]` | inline `mod foo { ... }` | Marks an exported namespace and drives the codegen. Modules may nest. | -| `#[weaveffi::export]` | `fn` or `async fn` | Exports a function. A `Result` return is `throws: true`; `()` (and `Result<(), E>`) is a `void` return; `async fn` is `async: true`. | -| `#[weaveffi::record]` | named-field `struct` | A by-value record. Generates a `BufferValue` implementation (encode and decode in the value-buffer format); no per-record C symbols. | -| `#[weaveffi::interface]` | `struct` with an inherent `impl` block | A reference-counted object type (see [Interfaces](#interfaces)). The `impl` block's `pub fn`s become constructors, methods, and statics; `_clone` and `_destroy` symbols are implicit. | -| `#[weaveffi::callback_interface]` | `trait Name: Send + Sync` | A set of methods the consumer implements and the producer calls (see [Callback interfaces](#callback-interfaces)). Producers accept one as `Arc`. | -| `#[weaveffi::error]` | `enum` with explicit discriminants | Declares the module's error domain. Every variant needs an explicit `= N` discriminant; the doc comment is the code's default message. A variant may carry named fields, which become the code's structured payload (this requires a primitive repr such as `#[repr(i32)]`). | -| `#[weaveffi::enumeration]` | `#[repr(i32)]` `enum`, or an `enum` with named-field variants | A C-style enum (every variant needs an explicit `= N` discriminant) or a rich enum whose variants carry named fields and cross as a value buffer. | -| `#[weaveffi::cancellable]` | exported `async fn` | Marks the function as accepting a cancel token; its final parameter must be a `weaveffi::CancelToken`. | - -Only items carrying a marker are exported. Private helpers, `use` items, the -module's in-memory state, and free functions without `#[weaveffi::export]` are -left untouched, so a module can freely mix its exported surface with its -implementation. Doc comments (`///`) on items, fields, and variants flow into -the generated IDL and every binding. `#[deprecated(note = "...")]` on an -exported function or method marks it `deprecated:` in the IDL. - -Call `weaveffi::export_runtime!()` exactly once in the crate (not per module). -It emits the fixed C ABI runtime symbols (`weaveffi_abi_version`, -`weaveffi_error_set`, `weaveffi_error_clear`, `weaveffi_error_free`, -`weaveffi_free_string`, `weaveffi_free_bytes`, the cancel-token helpers, and -on `wasm32` the `weaveffi_alloc`/`weaveffi_dealloc` pair) that every binding -links against. See [C ABI Contract](../reference/abi.md#runtime-surface). - -## How values cross the boundary - -The macro marshals each argument and result through the audited -[`weaveffi::abi`](https://docs.rs/weaveffi-abi) runtime, so every `unsafe` -pointer operation lives in one reviewed place rather than in generated glue. -You write ordinary Rust types; the macro picks the matching ABI shape: - -| Rust type | IDL type | C ABI shape | -|-----------|----------|-------------| -| `i8`..`i64`, `u8`..`u64`, `f32`, `f64`, `bool` | same | the scalar | -| `String` (or `&str` as a parameter) | `string` | `const char*` | -| `Vec` (or `&[u8]` as a parameter) | `bytes` | `const uint8_t* ptr, size_t len` | -| a `#[weaveffi::record]` struct | the record | serialized value buffer (`ptr` + `len`) | -| a `#[repr(i32)]` `#[weaveffi::enumeration]` enum | the enum | `int`-sized discriminant | -| a data-carrying `#[weaveffi::enumeration]` enum | the rich enum | serialized value buffer | -| `Option` | `T?` | serialized value buffer (nullable pointer for `Option>`) | -| `Vec` | `[T]` | serialized value buffer | -| `HashMap`, `BTreeMap` | `{K:V}` | serialized value buffer | -| `Arc` or `&T` where `T` is a `#[weaveffi::interface]` | the interface | `const {tag}*` parameter (borrowed); `{tag}*` return (one strong reference) | -| `Arc` where `Trait` is a `#[weaveffi::callback_interface]` | the callback interface | `void* ctx, const {tag}_vtable* vtable` (parameters only) | -| `weaveffi::Iter` (returns only) | `iter` | opaque iterator with `_next`/`_destroy` | -| `weaveffi::CancelToken` (final parameter of a `#[weaveffi::cancellable]` `async fn`) | not in the IDL | the launcher's `weaveffi_cancel_token*` slot | - -A reference such as `&str`, `&[u8]`, `&Contact`, or `&Shelf` is a -producer-side calling convention, not an IDL distinction: the thunk lifts the -argument and lends it to your function. Interfaces compose with every buffered -shape, so `Option>`, `Vec>`, `{string: Arc}`, a record field -of type `Arc`, and `weaveffi::Iter>` all work. See -[Annotated Rust Extraction](extract.md#type-mapping) for the exhaustive table. - -## Records - -A `#[weaveffi::record]` struct crosses the boundary by value as a serialized -[value buffer](../reference/value-buffers.md). The macro generates a -`weaveffi::abi::BufferValue` implementation (an `encode` into a -`BufferWriter` and a `decode` from a `BufferReader`, one field at a time in -declaration order); the surrounding marshalling calls it to decode buffered -parameters and encode buffered returns. No per-record C symbols are -generated: consumers pack and unpack the bytes with their own generated -routines. +## Records and enums ```rust #[weaveffi::record] @@ -155,619 +137,240 @@ routines. pub struct Contact { /// Stable identifier. pub id: i64, - /// Given name. - pub first_name: String, - /// Optional email address. + pub name: String, pub email: Option, - /// Kind of contact. pub kind: ContactType, } -``` -A record field may hold an object (`Arc`, `Option>`, -`Vec>`). Inside the buffer the object is a `u64` token carrying -one strong reference; the macro's generated `encode` clones the `Arc` into the -token and `decode` adopts it, so a record that carries objects is always -encoded fresh and decoded exactly once. +#[weaveffi::enumeration] +#[repr(i32)] +#[derive(Clone, Copy, Debug)] +pub enum ContactType { + Personal = 0, + Work = 1, +} -## Interfaces +#[weaveffi::enumeration] +#[derive(Clone, Debug)] +pub enum Shape { + Empty, + Circle { radius: f64 }, + Rect { width: f32, height: f32 }, +} +``` -A `#[weaveffi::interface]` struct is a first-class, reference-counted object -type: the consumer holds a strong reference to a live `Arc` rather than a -copied value. The `impl` block defines the surface; the struct's own fields -(its state) never cross the boundary, so they need no annotations. +Records and rich enums get a generated `weaveffi::abi::BufferValue` +implementation; no per-type C symbols exist. Rich-enum variants use named +fields (tuple variants are rejected), and tags follow declaration order unless +the variant declares a discriminant. -Because the object is shared across the FFI boundary (several consumer -wrappers, records, collections, and in-flight async calls may all hold a -reference), the macro asserts that the type is `Send + Sync`, and the -receivers are restricted: +## Interfaces -- Constructors are `pub fn`s without a receiver that return `Self` or - `Arc` (spelled either way or by the type's own name), optionally - inside a `Result`. -- Methods take `&self` or `self: Arc`. Use interior mutability - (`Mutex`, `RwLock`, atomics) for mutable state; `&mut self` and `self` by - value are rejected. -- Statics are `pub fn`s without a receiver that return anything else. +An interface is shared across the boundary (by consumer wrappers, records, +collections, and in-flight async calls), so the type must be `Send + Sync` +and keeps mutable state behind interior mutability. ```rust -#[weaveffi::module] -pub mod library { - use std::sync::{Arc, Mutex, PoisonError}; +#[weaveffi::interface] +pub struct Shelf { + titles: std::sync::Mutex>, +} - /// The library's error domain. - #[weaveffi::error] - #[derive(Debug)] - pub enum LibraryError { - /// no such shelf - NoSuchShelf = 1, +impl Shelf { + /// Constructor: no receiver, returns `Self` or `Arc`. + pub fn new() -> Self { + Self { titles: Default::default() } } - /// A shelf snapshot that carries the shelf itself. - #[weaveffi::record] - #[derive(Clone)] - pub struct ShelfInfo { - /// A caller-chosen label. - pub label: String, - /// The described shelf (one strong reference). - pub shelf: Arc, - /// A neighbouring shelf, if any. - pub neighbour: Option>, - } + /// Fallible constructor. + pub fn open(name: String) -> Result, LibraryError> { /* ... */ } - /// A shelf of titles. Shared across the FFI boundary, so its state - /// lives behind a `Mutex`. - #[weaveffi::interface] - pub struct Shelf { - titles: Mutex>, - } + /// Method on `&self`. + pub fn count(&self) -> i64 { /* ... */ } - impl Shelf { - /// Create an empty shelf. // constructor -> Self - pub fn new() -> Self { - Self { - titles: Mutex::new(Vec::new()), - } - } + /// Method on `self: Arc`, returning another reference to itself. + pub fn share(self: Arc) -> Arc { self } - /// Open a named shelf. // fallible constructor -> Arc - pub fn open(name: String) -> Result, LibraryError> { - if name.is_empty() { - return Err(LibraryError::NoSuchShelf); - } - Ok(Arc::new(Self::new())) - } + /// Iterator return. + pub fn titles(&self) -> weaveffi::Iter { /* ... */ } - /// Add a title. // method on &self - pub fn add(&self, title: String) { - self.titles - .lock() - .unwrap_or_else(PoisonError::into_inner) - .push(title); - } - - /// Number of titles. - pub fn count(&self) -> i64 { - self.titles - .lock() - .unwrap_or_else(PoisonError::into_inner) - .len() as i64 - } - - /// A second reference to this same shelf. // method on self: Arc - pub fn share(self: Arc) -> Arc { - self - } - - /// Whichever shelf holds more titles. // Option> in and out - pub fn larger(self: Arc, other: Option>) -> Option> { - match other { - Some(o) if o.count() > self.count() => Some(o), - Some(_) => Some(self), - None => None, - } - } - - /// Snapshot into a record that carries the shelf. - pub fn describe(self: Arc, label: String) -> ShelfInfo { - ShelfInfo { - label, - shelf: self, - neighbour: None, - } - } - - /// Split into `n` empty shelves. // Vec> return - pub fn split(&self, n: i32) -> Vec> { - (0..n).map(|_| Arc::new(Shelf::new())).collect() - } - - /// Stream every title lazily. // iter - pub fn titles(&self) -> weaveffi::Iter { - let snapshot: Vec = self - .titles - .lock() - .unwrap_or_else(PoisonError::into_inner) - .clone(); - weaveffi::Iter::new(snapshot) - } - - /// Copy the shelf on a producer thread. // async returning an object - pub async fn duplicate(self: Arc) -> Arc { - let copy = Shelf::new(); - for t in self.titles.lock().unwrap_or_else(PoisonError::into_inner).iter() { - copy.add(t.clone()); - } - Arc::new(copy) - } - - /// The largest shelf a library will hold. // static (no receiver) - pub fn capacity() -> i64 { - 10_000 - } - } + /// Async method. + pub async fn duplicate(self: Arc) -> Arc { /* ... */ } - /// Total titles across shelves and an optional snapshot. - #[weaveffi::export] - pub fn total(shelves: Vec>, extra: Option) -> i64 { - let base: i64 = shelves.iter().map(|s| s.count()).sum(); - base + extra.map_or(0, |info| info.shelf.count()) - } - - /// Stream shelves lazily. // iter - #[weaveffi::export] - pub fn stream_shelves(n: i32) -> weaveffi::Iter> { - weaveffi::Iter::new((0..n).map(|_| Arc::new(Shelf::new()))) - } + /// Static: no receiver, returns something other than `Self`. + pub fn capacity() -> i64 { 10_000 } } ``` -The macro emits `weaveffi_library_Shelf_clone` and -`weaveffi_library_Shelf_destroy` alongside the constructors, methods, and -statics. `_clone` returns a new strong reference to the same object and -`_destroy` releases one; the `Shelf` is dropped when the last reference goes. -An object parameter (`&Shelf`, `Arc`, `Option>`) is -borrowed for the call: when your function takes `Arc` the thunk bumps -the count for you, so retaining it past the call is safe. An object return -transfers one strong reference to the consumer; a `self: Arc` method -that returns `self` hands back the same pointer the consumer passed in, now -with one more reference, which the consumer must eventually release too. - -Every binding wraps the reference in an idiomatic class (a Swift `final class` -with `deinit`, a Kotlin `AutoCloseable` backed by a `Cleaner`, a Python class -with `close()` and `__del__`, C# `IDisposable`, Go `Close()` with a -finalizer, Dart `NativeFinalizer`, Ruby `FFI::AutoPointer`, C++ RAII) that -releases its reference exactly once. See `samples/kvstore` and -`samples/events` for complete producers and -[Memory Ownership](memory.md#interface-objects) for the full contract. +- Constructors return `Self`, `Arc`, or the type by name, optionally in + a `Result`. They can't be `async`; use an async static that returns + `Arc`. +- Methods take `&self` or `self: Arc`. `&mut self` and `self` by value + are rejected. +- An object parameter `&T` is borrowed for the call; `Arc` (or + `Option>`) is a new strong reference the thunk took for you, so you + may store it. +- An object you return transfers one strong reference to the consumer. + +The macro also emits `{prefix}_{module}_{Type}_clone` and `_destroy`. Every +binding wraps the reference in a class that releases it exactly once. ## Callback interfaces -A `#[weaveffi::callback_interface]` trait is the inverse of an interface: the -**consumer** implements it and the **producer** calls it. Declare it as a -`Send + Sync` trait whose methods take `&self`, and accept it in exported -functions, constructors, statics, or methods as `Arc`. The macro -implements the trait on a foreign wrapper around the consumer's `(ctx, vtable)` -pair, so your code calls the methods like any other trait object and can clone -and retain the `Arc` for as long as it likes. When the last `Arc` drops the -consumer's `free(ctx)` entry runs exactly once. +A callback interface is the inverse of an interface: the consumer implements +it and the producer calls it, from any thread, for as long as it holds the +`Arc`. When the last clone drops, the consumer's release hook runs exactly +once. ```rust -#[weaveffi::module] -pub mod events { - use std::sync::{Arc, Mutex, PoisonError}; - - /// How a subscriber wants to be told about a message. - #[weaveffi::enumeration] - #[repr(i32)] - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Delivery { - /// Deliver the message. - Accept = 0, - /// Skip this subscriber for this message. - Skip = 1, - } - - /// A published message as subscribers see it. - #[weaveffi::record] - #[derive(Clone, Debug, PartialEq)] - pub struct Message { - /// Monotonic sequence number, starting at 1. - pub seq: i64, - /// Message text. - pub text: String, - } - - /// A consumer-implemented subscriber. - #[weaveffi::callback_interface] - pub trait Subscriber: Send + Sync { - /// Decide how the bus should treat `topic` for this subscriber. - fn route(&self, topic: String) -> Delivery; - /// Receive an accepted message. Returns the running count. - fn on_message(&self, message: &Message) -> i64; - /// Receive the bus itself; the consumer adopts the reference. - fn on_attached(&self, bus: Arc); - } - - /// A bus that retains its subscribers. - #[weaveffi::interface] - pub struct EventBus { - subscribers: Mutex>>, - seq: Mutex, - } - - impl EventBus { - /// Create an empty bus. - pub fn new() -> Arc { - Arc::new(Self { - subscribers: Mutex::new(Vec::new()), - seq: Mutex::new(0), - }) - } - - /// Retain `subscriber` and tell it which bus it joined. - pub fn subscribe(self: Arc, subscriber: Arc) -> i64 { - subscriber.on_attached(Arc::clone(&self)); - let mut subs = self - .subscribers - .lock() - .unwrap_or_else(PoisonError::into_inner); - subs.push(subscriber); - subs.len() as i64 - } - - /// Publish `text` under `topic`, returning how many subscribers - /// accepted it. A subscriber failure aborts the call. - pub fn publish(&self, topic: String, text: String) -> i64 { - let message = { - let mut seq = self.seq.lock().unwrap_or_else(PoisonError::into_inner); - *seq += 1; - Message { seq: *seq, text } - }; - // Snapshot so no lock is held while the consumer runs. - let subs: Vec> = self - .subscribers - .lock() - .unwrap_or_else(PoisonError::into_inner) - .clone(); - let mut delivered = 0; - for sub in &subs { - if sub.route(topic.clone()) == Delivery::Accept { - sub.on_message(&message); - delivered += 1; - } - } - delivered - } - } - - /// Ask `subscriber` how it would route `topic` without a bus. - #[weaveffi::export] - pub fn route_once(subscriber: Arc, topic: String) -> Delivery { - subscriber.route(topic) - } +#[weaveffi::callback_interface] +pub trait Subscriber: Send + Sync { + /// A consumer failure comes back as an `Err`. + fn route(&self, topic: &str) -> Result; + /// A consumer failure aborts the enclosing call. + fn on_message(&self, message: &Message) -> i64; + /// The consumer adopts the reference it receives. + fn on_attached(&self, bus: Arc); } -``` - -The allowed method shapes are deliberately narrow so that nothing the consumer -allocates has to cross back into the producer: - -- The receiver is `&self`. `&mut self`, `self`, and associated functions - without a receiver are rejected. -- Parameters may be any IDL type except another callback interface or an - iterator: scalars, strings, bytes, records, enums, optionals, lists, maps, - and objects (`Arc` or `Option>`). Strings, bytes, and buffered - values are borrowed by the consumer for the duration of the call; an object - parameter transfers one strong reference to the consumer, which adopts it. -- The return type is `()`, a scalar, `bool`, or a C-style enum. Strings, - bytes, records, and objects can't be returned. Methods can't be `async` and - can't return `Result`. -- A callback interface may only appear as a parameter of a function, - constructor, static, or method. It can't be returned, stored in a record, - wrapped in `Option`, put in a list, or passed to another callback method. - -The generated C header declares one vtable per trait, with an entry per method -plus the trailing `free`. This is the `Subscriber` vtable from the example -above: - -```c -typedef struct weaveffi_events_Subscriber_vtable { - /** Decide how the bus should treat `topic` for this subscriber. */ - weaveffi_events_Delivery (*route)(void* ctx, const char* topic, weaveffi_error* out_err); - /** - * Receive an accepted message. Returns the running count. - */ - int64_t (*on_message)(void* ctx, const uint8_t* message_ptr, size_t message_len, weaveffi_error* out_err); - /** Receive the bus itself; the consumer adopts the reference. */ - void (*on_attached)(void* ctx, weaveffi_events_EventBus* bus, weaveffi_error* out_err); - void (*free)(void* ctx); -} weaveffi_events_Subscriber_vtable; -``` - -### When the consumer fails - -Every vtable entry carries a trailing `out_err`. A consumer implementation -that raises (a Python exception, a Swift `throw`, a Go panic, and so on) is -reported there by the generated binding with `FOREIGN_ERROR_CODE` (`-4`) and -the foreign error's text. Because callback methods never return `Result`, the -generated trait implementation can't hand you that error as a value. Instead it -aborts the producer call by unwinding: `weaveffi::abi::check_foreign_error` -raises a `ForeignError` payload with `std::panic::resume_unwind`, the unwind -travels through your frames to the nearest exported thunk, and the thunk's -`catch_unwind` reports `-4` with the consumer's message to the original caller -(the same channel a producer panic uses, with a different code). - -Treat every callback method call as potentially panicking, exactly as you'd -treat a call into an arbitrary closure: - -- Don't hold a `std::sync::Mutex` guard across the call unless you recover - from poisoning (`unwrap_or_else(PoisonError::into_inner)`, as the example - does). Otherwise the next lock attempt after a foreign failure panics too, - and that panic *is* reported as a producer bug (`-2`). -- Prefer the snapshot pattern: clone the list of subscribers (or the state you - need) under the lock, release the lock, then call out. -- Make state changes before the call or make them idempotent, because the - code after a failing callback doesn't run. -- Tolerate a callback method returning its type's zero value (`0`, `false`, - an empty string) after a failure. On a `panic = "abort"` build such as - `wasm32-unknown-unknown` there's no unwinding, so the runtime records the - failure in a thread-local slot instead, your code keeps running on the - zero return, and the thunk reports the recorded failure once you return. - -`samples/events/src/lib.rs` follows these rules and has a test -(`foreign_error_aborts_publish`) showing the bus remains usable after a -subscriber failure. - -## Errors - -Declare a domain as a `#[weaveffi::error]` enum whose discriminants are the -ABI codes (positive, unique) and whose doc comments are the default messages. -Return `Result` from anything that can fail; the macro generates -the `ErrorReport` implementation that writes the code, message, and payload -into `out_err`. A variant may carry named fields, which travel as a structured -payload and surface as properties on the typed error the consumer catches; -field-carrying variants with explicit discriminants require a primitive repr: -```rust -#[weaveffi::error] -#[derive(Debug)] -#[repr(i32)] -pub enum QuotaError { - /// quota exceeded - Exceeded { limit: i64, used: i64 } = 3001, - /// quota service unavailable - Unavailable = 3002, +#[weaveffi::export] +pub fn route_once(subscriber: Arc, topic: &str) -> Delivery { + subscriber.route(topic).unwrap_or_else(|e| { + weaveffi::abi::raise_foreign_error(e); + Delivery::Skip + }) } ``` -A module may declare at most one error domain, and it's in scope for the -module and every nested module. `Result` also compiles (it reports -the generic code `-1`), but only a `#[weaveffi::error]` enum gives consumers -named codes to match on. See [Error Handling](errors.md). - -## Iterators and cancel tokens - -Return `weaveffi::Iter` when the consumer should pull elements lazily -instead of receiving one materialized `[T]` buffer. Build it from any -`Send + 'static` iterator with `Iter::new`; the macro emits the -`{IterTag}*` launcher plus `_next` and `_destroy` symbols, and every binding -surfaces it as the language's native lazy iteration idiom. `T` may be any -IDL type including an object (`weaveffi::Iter>`). Iterators are -returns only; an `Iter` parameter isn't supported. - -Mark an exported `async fn` `#[weaveffi::cancellable]` and accept a -`weaveffi::CancelToken` as its final parameter. The token is part of the async -calling convention rather than the IDL signature: the launcher gains a -`weaveffi_cancel_token*` slot and the macro lifts it for you. Poll -`is_cancelled()` at safe points and return early: +The rules: + +- The receiver is `&self`. Methods are synchronous. +- Parameters may be any IDL type except a callback interface or an iterator. + Strings, bytes, and buffered values are lent to the consumer for the call; + an object parameter transfers one strong reference to the consumer. +- The return is `()`, a scalar, `bool`, a C-style enum, or + `Result` of one of those. Strings, buffers, and + objects can't be returned yet (see the [roadmap](../roadmap.md)). +- A callback interface may appear only as a parameter, never in a return, + record, `Option`, or collection. + +**When the consumer fails.** The binding reports a consumer exception +through the vtable entry's error slot with code `-4`. A method declared with +`Result` receives it as `Err(ForeignError { code, message })` +and nothing unwinds; propagate it with `weaveffi::abi::raise_foreign_error` +or handle it. A method with a plain return can't hand the error back, so the +failure aborts the producer call: on an unwinding build it unwinds to the +enclosing thunk, which reports `-4` with the consumer's message, and on a +`panic = "abort"` build (notably `wasm32-unknown-unknown`) the method returns +its type's zero value, your code keeps running, and the thunk reports the +failure when it returns. Either way the original caller sees the consumer's +message. A failure on a thread with no WeaveFFI call active (a thread you +spawned) is written to stderr and dropped, never attached to a later call. + +Prefer the `Result` form. With plain returns, don't hold a `Mutex` guard +across a callback call: snapshot the state you need, release the lock, then +call out, as `samples/events` does. + +## Iterators + +Return `weaveffi::Iter` (built with `Iter::new` from any `Send + 'static` +iterator) when the consumer should pull elements lazily instead of receiving +a materialized list. It may be wrapped in a `Result`, and `T` may be any IDL +type, including an object. Iterators are returns of synchronous callables +only; an `Iter` parameter, a nested `Iter`, or an async function returning one +is rejected. + +## Async functions and cancellation + +An `async fn` (free function, method, or static) lowers to a launcher that +returns immediately and a completion callback that fires exactly once. +`#[weaveffi::cancellable]` adds a `weaveffi::CancelToken` parameter: ```rust -/// Sum bytes on a producer thread, stopping early when cancelled. #[weaveffi::export] #[weaveffi::cancellable] -pub async fn checksum(data: Vec, cancel: weaveffi::CancelToken) -> Result { - let mut total = 0i64; - for chunk in data.chunks(4096) { - if cancel.is_cancelled() { - return Err(ContactsError::InvalidName); - } - total += chunk.iter().map(|b| i64::from(*b)).sum::(); - } - Ok(total) +pub async fn wait(timeout_ms: i64, cancel: weaveffi::CancelToken) -> i64 { + let _ = cancel; // the runtime drops this future when the token fires + Delay::new(timeout_ms as u64).await; + timeout_ms } ``` -Async functions and methods run on the spawner installed with -`weaveffi::set_spawner` (a detached thread per future by default); see -[Async Functions](async.md). - -## Cross-module references - -Modules can reference each other's records, enums, interfaces, and callback -interfaces. Import the type with a normal `use` and pass it by value or by -reference: - -```rust -#[weaveffi::module] -pub mod products { - /// A product in the catalog. - #[weaveffi::record] - #[derive(Clone)] - pub struct Product { - /// Stable identifier. - pub id: i64, - /// Unit price. - pub price: f64, - } -} - -#[weaveffi::module] -pub mod orders { - use super::products::Product; - - /// Takes a `products::Product` across the module boundary. - #[weaveffi::export] - pub fn add_product(order_id: u64, product: Product) -> bool { - let _ = (order_id, product); - true - } -} -``` - -Each module is expanded on its own, so the macro emits a thunk named for its -own module while the CLI (which sees the whole crate) resolves the reference -to `products.Product` in the IDL and header. Both spellings are the same -serialized value buffer at the ABI level, so the producer and the generated -bindings agree. See `samples/inventory` and the `kv.stats` submodule of -`samples/kvstore` for complete examples. - -## What the macro rejects - -When the macro can't express a producer it fails at compile time with a -spanned diagnostic rather than emitting glue that disagrees with the header. -Every rejection below is pinned by a `trybuild` test in -`crates/weaveffi-macros/tests/ui/`; the messages are quoted from those tests' -`.stderr` files. - -**Raw pointers.** There is no `handle` type in ABI 2; declare the pointee -as an interface. - -```text -error: weaveffi: raw pointers cannot cross the FFI boundary; declare the pointee as a #[weaveffi::interface] and pass it as `&T` or `Arc` - --> tests/ui/fail_raw_pointer.rs:6:22 - | -6 | pub fn open() -> *mut Token { - | ^ -``` - -**`Box` and `Rc`.** Objects are shared, so only `Arc` is accepted. +When the consumer cancels, the runtime drops your future at its next +suspension point and completes the call with code `-5`; you don't need to +poll the token. Poll `cancel.is_cancelled()` only for cooperative cleanup, +such as work running on another thread. Futures run on the executor +installed with `weaveffi::set_spawner` (a thread per future by default), and +a future the executor drops without finishing also completes with `-5`. See +[Async and Cancellation](async.md). + +## Modules and cross-module references + +Each `#[weaveffi::module]` at the top level of the crate is a root; nested +`#[weaveffi::module]` modules are its submodules, and their symbols carry the +joined path (`contacts_contacts_groups_count_of_type`). Inside one root tree, +any module may use any declaration in the tree: records, enums, interfaces, +callback interfaces, and the error domains of its ancestors. + +The macro expands each root on its own and can't see a sibling root's +declarations. A named type it can't resolve is assumed to be a value buffer, +and the macro asserts at compile time that the type implements +`weaveffi::abi::ByValue`. Records and rich enums do, so sharing a record +between roots works. A C-style enum, an interface, or a callback interface +from another root does not, and the build fails: ```text -error: weaveffi: `Box` cannot cross the FFI boundary; objects and callback interfaces are shared, so spell them as `Arc` / `Arc` - --> tests/ui/fail_box_param.rs:7:20 - | -7 | pub fn take(w: Box) { - | ^^^ +error[E0277]: `Color` is declared in a different `#[weaveffi::module]` tree and isn't a record or rich enum + = note: nest the modules under one `#[weaveffi::module]` root (as inner `mod`s) so the macro can see the declaration ``` -**`&mut` parameters.** Nothing mutates a caller's value in place across the -boundary. +The fix is in the message: put the modules under one root. The `contacts` +sample shows both shapes: a nested `contacts::groups` that uses the parent's +interface and enum, and a sibling `directory` root that shares only the +`Contact` record. -```text -error: weaveffi: `&mut` parameters cannot cross the FFI boundary; take the value by `&T` or by value and return the updated result - --> tests/ui/fail_mut_ref_param.rs:4:23 - | -4 | pub fn fill(text: &mut String) { - | ^ -``` +## Leak checks -**Interface methods taking `self` by value or `&mut self`.** +The `leak-check` cargo feature (on `weaveffi`) counts live objects, foreign +callbacks, iterators, cancel tokens, and returned allocations. +`{prefix}_debug_live(kind)` reports them (`0` objects, `1` callbacks, +`2` iterators, `3` tokens, `4` allocations); without the feature the symbol +still exists and returns `0`. Every sample enables it, and every conformance +consumer asserts all five counts are zero at exit. -```text -error: weaveffi: interface methods must take `&self` or `self: Arc`; use interior mutability (Mutex, RwLock, atomics) for mutable state, because the object is shared across the FFI boundary - --> tests/ui/fail_interface_self_by_value.rs:7:24 - | -7 | pub fn consume(self) {} - | ^^^^ -``` - -**Interfaces that aren't `Send + Sync`.** The assertion is a plain trait -bound, so the compiler's own diagnostic names the offending field: - -```text -error[E0277]: `Cell` cannot be shared between threads safely - --> tests/ui/fail_interface_not_sync.rs:1:1 - | -1 | #[weaveffi::module] - | ^^^^^^^^^^^^^^^^^^^ `Cell` cannot be shared between threads safely - | - = help: within `Counter`, the trait `Sync` is not implemented for `Cell` - = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicI32` instead -note: required because it appears within the type `Counter` - --> tests/ui/fail_interface_not_sync.rs:6:16 - | -6 | pub struct Counter { - | ^^^^^^^ -note: required by a bound in `__wv_assert_send_sync` -``` - -**Callback methods with the wrong receiver or a non-direct return.** - -```text -error: weaveffi: callback interface methods must take `&self` - --> tests/ui/fail_callback_mut_self.rs:5:21 - | -5 | fn on_event(&mut self, n: i32); - | ^ -``` - -```text -error: weaveffi: unsupported non-direct return type for `callback return` (not yet implemented by #[weaveffi::module]) - --> tests/ui/fail_callback_string_return.rs:1:1 - | -1 | #[weaveffi::module] - | ^^^^^^^^^^^^^^^^^^^ -``` - -**A `Result` with no error domain in scope.** - -```text -error: weaveffi: `risky` returns a Result but no error domain is in scope; declare a #[weaveffi::error] enum in this module (or a parent module) - --> tests/ui/fail_result_without_error_domain.rs:2:5 - | -2 | mod bad { - | ^^^ +```toml +[dependencies] +weaveffi = { version = "0.23", features = ["leak-check"] } ``` -**A C-style enum without `#[repr(i32)]`.** - -```text -error: enum `Mode` must have #[repr(i32)] to be a #[weaveffi::enumeration] - --> tests/ui/fail_enum_without_repr.rs:3:5 - | -3 | #[weaveffi::enumeration] - | ^ -``` +## What the macro rejects -Two removals from earlier releases have no dedicated diagnostic because the -spelling simply no longer exists: `weaveffi::Handle` (the untyped `handle` -type) is gone from the crate, so referring to it is an ordinary unresolved -type error, and the IDL's borrowed `&str`/`&[u8]` types are gone. Rust `&str` -and `&[u8]` parameters are still accepted; they are the borrowed spellings of -`string` and `bytes`. Also rejected, with a spanned message: tuple-style -rich-enum variants (use named fields), error variants without an explicit -discriminant, a second `#[weaveffi::error]` in one module, an `Arc` -naming more than one trait, and a callback interface not spelled -`Arc`. - -## Feature support - -The proc-macro generates cdylib glue for the full IDL feature set. Every -feature below is understood by the IDL, the validator, and every generator, and -the macro emits the matching producer glue, so an annotated module compiles -straight to a `weaveffi_*` cdylib with no hand-written `extern "C"` layer. - -| Feature | Macro codegen | Reference sample | -|---------|---------------|------------------| -| Modules, nested modules | Supported | `inventory`, `kvstore` | -| Sync functions, `Result` errors | Supported | `calculator`, `contacts` | -| Error domains (`#[weaveffi::error]`), structured payloads | Supported | `calculator`, `contacts`, `weaveffi` crate's runtime tests | -| Reference-counted interfaces (constructors, methods, statics, `_clone`/`_destroy`) | Supported | `contacts`, `kvstore`, `events` | -| Objects in optionals, lists, maps, record fields, iterators, and async returns | Supported | `kvstore` (`share`, `fork`, `larger`, `describe`, `open_many`, `total_count`), `codec` | -| Callback interfaces (`Arc`) | Supported | `events`, `kvstore` (`EvictionListener`) | -| Records (value-buffer encode / decode) | Supported | `contacts`, `codec` | -| C-style enums | Supported | `contacts`, `shapes` | -| Rich (data-carrying) enums | Supported | `shapes` | -| Scalars, `string`, `bytes` | Supported | `calculator`, `kvstore` | -| Optionals, lists, maps (nested composites included) | Supported | `inventory`, `kvstore`, `codec` | -| Async functions and methods, pluggable spawner | Supported | `async-demo`, `kvstore`, `events` | -| Cancellable async (`weaveffi::CancelToken`) | Supported | `kvstore` (`compact`) | -| Iterator returns | Supported | `events`, `kvstore` | +Each rejection is a spanned compile error, pinned by a `trybuild` test in +`crates/weaveffi-macros/tests/ui/`: + +| Source | Why | +|--------|-----| +| `*const T`, `*mut T` | Declare the pointee as an interface and pass `&T` or `Arc`. | +| `Box`, `Rc`, `Box` | Objects and callbacks are shared: use `Arc`, `Arc`. | +| `&mut T` parameter | Take `&T` or a value and return the result. | +| interface method on `self` or `&mut self` | Use `&self` or `self: Arc` with interior mutability. | +| interface that isn't `Send + Sync` | The compiler names the offending field. | +| callback method on `&mut self`, or returning a string, buffer, or object | Callback returns are direct values only. | +| `Result` with no error domain in scope | Declare a `#[weaveffi::error]` enum in the module or an ancestor. | +| `#[weaveffi::error]` enum without `Display` | `Display` supplies the runtime message. | +| C-style enum without `#[repr(i32)]` | The discriminant crosses as `int32_t`. | +| `Iter` parameter or nested `Iter` | Iterators are outermost returns only. | +| non-value type from another root | See [cross-module references](#modules-and-cross-module-references). | + +Also rejected: tuple-style rich-enum variants, error variants without an +explicit discriminant, two `#[weaveffi::error]` enums in one module, and an +`Arc` naming more than one trait. Whole-API rules (duplicate +names, [C symbol collisions](../reference/idl.md#validation)) are checked by +the CLI, not the macro, so run `weaveffi validate` or `weaveffi generate` in +CI. ## See also -- [Getting Started](../getting-started.md): the end-to-end IDL-first walkthrough; this guide is the Rust-macro alternative to its step 2. -- [Annotated Rust Extraction](extract.md): the `weaveffi extract`/`generate ` CLI and the full attribute and type reference. -- [C ABI Contract](../reference/abi.md): the normative description of the symbols the macro emits. -- [Memory Ownership](memory.md), [Error Handling](errors.md), and [Async Functions](async.md): the contracts the macro upholds for you. -- [Rust API](../api/rust.md): the public items of the `weaveffi` and `weaveffi-abi` crates. +- [Samples](../samples.md): `kvstore` uses every feature on this page. +- [C ABI Contract](../reference/abi.md): what the thunks implement. +- [Rust API Map](../api/rust.md): the `weaveffi` and `weaveffi-abi` items. diff --git a/docs/src/intro.md b/docs/src/intro.md index fcb4c178..f094e84e 100644 --- a/docs/src/intro.md +++ b/docs/src/intro.md @@ -1,68 +1,60 @@ # Introduction -**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 another -language: no hand-written JNI, no duplicate implementations, no unsafe -boilerplate.** - -Define your API once as an IDL in YAML, JSON, or TOML, and WeaveFFI generates -idiomatic packages for C, C++, Swift, Kotlin, Node.js, WebAssembly, Python, -.NET, Dart, Go, and Ruby, all talking to the same stable C ABI. Any backend -that implements the symbols declared in the generated C header can be the -producer. - -Writing your producer in Rust? Annotate a normal module with -`#[weaveffi::module]` and the macro generates the C ABI and derives the IDL for -you, so you write no `unsafe` glue and keep no separate IDL in sync. The macro -is one ergonomic path onto the same engine the IDL uses, so whichever you pick, -the producer you build and the bindings you ship cannot drift. - -## Why WeaveFFI? - -- **One definition, eleven languages.** Write the API once (safe Rust or an - IDL) and ship packages to npm, SwiftPM, Maven, PyPI, NuGet, pub.dev, - RubyGems, and Go modules. -- **Safe Rust in, C ABI out.** The `#[weaveffi::module]` macro emits the - `extern "C"` thunks, marshalling every argument through an audited runtime, - so a Rust producer writes no `unsafe` glue and the IDL is derived from the - code rather than maintained beside it. -- **Stable C ABI underneath.** Every target speaks to the same `extern "C"` - contract ([ABI revision 2](reference/abi.md)), so adding a new platform - later is a code-gen change, not a rewrite. -- **Objects and callbacks that behave the same everywhere.** Interfaces are - reference-counted objects (`Arc` in Rust) that consumers can share, - nest inside records and collections, and release deterministically, with a - garbage-collector backstop where the language has one. Callback interfaces - are method sets the consumer implements as a protocol, interface, or - abstract class and the native library calls from any thread. -- **Idiomatic per-target output.** No lowest-common-denominator surface - area. Swift gets `async/await` and `throws`, Kotlin gets `suspend` and - JNI glue, Python gets typed `.pyi` stubs, TypeScript gets `Promise`s and - `BigInt`, Dart gets `dart:ffi` with `NativeFinalizer`, all from the same - definition. Async producers keep their own executor: plug Tokio in with - `weaveffi::set_spawner`, or let the default thread-per-future spawner do - the work. - -## Design principle: standalone generated packages - -Generated packages are fully self-contained and publishable to their -native ecosystem (npm, SwiftPM, Maven Central, PyPI, NuGet, pub.dev, -RubyGems, and so on) without requiring consumers to install WeaveFFI tooling -or runtime dependencies. WeaveFFI is a build-time tool for library -authors; consumers should never need to know it exists. Helper code -(error types, the value-buffer codec, object wrappers, callback trampolines) -is generated inline into each package rather than pulled from a shared -runtime dependency. +**WeaveFFI generates idiomatic bindings for 11 languages from one API +definition, over a stable C ABI.** Write the API as safe Rust annotated with +`#[weaveffi::module]`, or as a YAML, JSON, or TOML IDL implemented by any +library that can export C symbols (C, C++, Zig, and so on). WeaveFFI emits +packages for C, C++, Swift, Kotlin, Node.js, WebAssembly, Python, .NET, Dart, +Go, and Ruby that all call the same C functions. + +## The design in one page + +**One definition, two front doors.** A Rust producer annotates ordinary +modules; the `#[weaveffi::module]` macro emits the `extern "C"` thunks, and +the CLI reads the same source to emit the bindings. Both run one shared +extractor, so the library you compile and the bindings you ship are two views +of one parse. A non-Rust producer writes an [IDL](reference/idl.md) instead +and implements the C header WeaveFFI generates from it. + +**Identity drives every name.** A library has one resolved +[identity](reference/naming.md): a package `name`, a C symbol `prefix`, and a +native `library` base name. For a Rust producer all three come from the crate +(`kvstore` gives `kvstore_kv_Store_open`, `libkvstore.so`, and an npm, PyPI, +or NuGet package named `kvstore`). Nothing a generator emits is named after +WeaveFFI, so any number of WeaveFFI-built libraries can live in one process. + +**A small, explicit C ABI.** [Revision 3](reference/abi.md) has five value +families: direct scalars, UTF-8 strings and bytes as `(ptr, len)` runs, +serialized [value buffers](reference/value-buffers.md) for records, enums, +optionals, lists, and maps, reference-counted objects, and +consumer-implemented callback vtables. Every fallible call reports through a +`{prefix}_error` struct. Async functions complete through a callback that +fires exactly once, and cancellation is a first-class runtime code. Every +consumer checks the ABI revision and a per-module contract checksum when it +loads the library, so a stale binding fails loudly instead of misreading +memory. + +**Idiomatic generators.** Each target maps the model onto its own idioms: +objects become classes with deterministic disposal and a garbage-collector +backstop, async functions become `async`/`await`, coroutines, promises, or +tasks with native cancellation, iterators stay lazy, and error domains +become typed exceptions or error values. The +[capability matrix](generators/README.md) summarizes all eleven. + +**Standalone output.** Generated packages are self-contained: helper code +(the value-buffer codec, error types, object wrappers, loaders) is generated +into each package, so consumers never install WeaveFFI. `weaveffi package` +bundles prebuilt native libraries into publishable packages per ecosystem. ## Where to next -- [Getting Started](getting-started.md): install, define an IDL, generate, and call from C. -- [The Rust Producer Macro](guides/producer-macro.md): the `#[weaveffi::module]` attribute family and the supported feature set. -- [C ABI Contract](reference/abi.md): the normative description of objects, callback interfaces, value buffers, async functions, and iterators at the boundary. -- [Comparison](comparison.md): feature matrix vs UniFFI, Diplomat, cbindgen, swift-bridge, napi-rs, wasm-bindgen, SWIG, autocxx, and an honest "when to choose WeaveFFI" guide. -- [FAQ](faq.md): object ownership, callback interfaces, the async spawner, runtime cost, customization, Windows support, distribution, licensing. -- [Samples](samples.md): the kitchen-sink `kvstore` reference, the `events` callback-interface bus, the `codec` round-trip oracle, and the smaller walkthroughs. -- [Stability and Versioning](stability.md): what schema `0.9.0` and ABI 2 changed and how to migrate. -- [Roadmap](roadmap.md): what's planned after ABI 2. -- [Generators](generators/README.md): per-target reference for each of the eleven languages. -- [Guides](guides/README.md): memory ownership, error handling, async, configuration, packaging. +- [Getting Started](getting-started.md): a Rust producer called from Python + and C in a few minutes, plus the IDL path for a C implementation. +- [The Rust Producer Macro](guides/producer-macro.md): every supported + signature and what the macro rejects. +- [C ABI Contract](reference/abi.md): the normative description of the + boundary. +- [Comparison](comparison.md) and [FAQ](faq.md): how WeaveFFI relates to + UniFFI, Diplomat, cbindgen, and the single-language bridges. +- [Stability and Versioning](stability.md): what changed in schema 0.10 and + ABI 3, and how to migrate. diff --git a/docs/src/performance.md b/docs/src/performance.md deleted file mode 100644 index 9368884c..00000000 --- a/docs/src/performance.md +++ /dev/null @@ -1,167 +0,0 @@ -# Performance - -WeaveFFI is designed to disappear in the build. Code generation should -finish in under a second on every project from the calculator sample to a -fully featured kitchen-sink API, leaving a budget for the surrounding -build steps. - -This page lists the explicit performance targets the project commits to, -the methodology used to measure them, the latest measurements taken on -commodity hardware, and the locations of the workflow artifacts that the -CI system uploads on every push to `main`. - -## Targets - -The values below are hard targets enforced via the criterion benchmarks -in [`crates/weaveffi-core/benches/codegen_bench.rs`][core-bench] and -[`crates/weaveffi-cli/benches/generate_bench.rs`][cli-bench]. The first -two benchmarks measure single-purpose pipeline stages; the latter two -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/kitchen_sink.yml` | -| `hash_kitchen_sink` | < 1 ms | Same fixture, post-validation | -| `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 -release blocker; the CI workflow uploads benchmark output as an artifact -on every push to `main` so reviewers can spot drift before it ships. - -## Methodology - -The benchmarks use [criterion.rs] in its default sampling mode (100 -samples, ~3 s measurement, statistical analysis). Each benchmark builds -a fresh temporary directory per iteration so I/O is included in the -measurement; this matches what users observe at the command line. - -```bash -cargo bench --workspace -- --noplot -``` - -Profile a generator end-to-end with a flame graph: - -```bash -cargo flamegraph -p weaveffi-cli --bench generate_bench -``` - -On macOS, the equivalent invocation uses `cargo-instruments`: - -```bash -cargo instruments -t Time -p weaveffi-cli --bench generate_bench -``` - -Reference hardware for the numbers below: Apple M-series laptop, release -build (`--release`, `lto = false`), no other heavy processes running. - -## Latest measurements - -These numbers were captured on the baseline run that followed the hot-path -optimizations described below, before the ABI revision 2 generators landed. -Each row is the criterion median; the last column shows the headroom -relative to the documented target. ABI 2 added `_clone` symbols, callback -vtables, and object-token codec paths to every generator's output, so expect -the absolute medians to move slightly on the next baseline; the targets are -unchanged and CI's `bench-results` artifact (below) has the current figures. - -| Benchmark | Median | Headroom vs target | -| -------------------------------- | -------- | ------------------ | -| `validate_kitchen_sink` | 7.45 µs | ~670× under | -| `hash_kitchen_sink` | 37.5 µs | ~27× under | -| `full_codegen_calculator` | 6.92 ms | ~72× under | -| `full_codegen_kitchen_sink` | 7.27 ms | ~275× under | -| `generate_c_large_api` | 904 µs | n/a | -| `generate_swift_large_api` | 1.93 ms | n/a | -| `generate_all_large_api` | 24.1 ms | n/a | -| `generate_all_kitchen_sink` | 7.27 ms | n/a | - -The `*_large_api` benchmarks operate on a synthetic 10-module × 50-function -API (500 functions total) that does not have a documented ceiling; they -exist as a regression signal for the per-function cost of each generator. - -## Optimized hot paths - -Profiling revealed three meaningful hot paths in the code-generation -pipeline. Each one was tightened in this iteration; the optimizations -delivered the cumulative ~7-10 % wall-clock improvement visible in the -table above. - -1. **Pre-allocate output buffers.** Both `render_c_header` and - `render_swift_wrapper` started from `String::new()` and let the - buffer grow by doubling, copying the entire string on each - re-allocation. They now estimate the final output size from the - number of modules, functions, records, interfaces, and callback - interfaces in the API and pre-allocate accordingly via - `String::with_capacity`. - -2. **`write!` instead of `push_str(&format!(...))`** in the per-function - hot loop of `render_module_header` (C generator) and the function - wrappers in the Swift generator. Each replacement eliminates the - intermediate `String` that `format!` allocates before the result is - appended to the output buffer. - -3. **Drop the `Vec` + `join(", ")` pattern** when emitting - parameter signatures. The Swift generator now writes the - comma-separated parameter list directly into the output buffer via - the `write_swift_params_sig` helper; the C generator routes through a - `write_params_into` helper that takes string slices, eliminating the - per-parameter allocation loop and the joined intermediate. - -These three categories are the ones explicitly called out as candidates -in the original performance plan, in order of impact. - -### Things explicitly not optimized - -- **`serde_yaml` parsing** is the dominant cost of the - `weaveffi generate` happy path on disk because parsing happens before - the benchmarks above run. The kitchen-sink fixture takes ~50 µs to - parse on reference hardware, well below the validate/hash targets, - and `serde_yaml` does not expose a streaming API that is materially - faster for our schemas. We accept it as the dominant CLI startup - cost and document it here. - -## Runtime cost of the ABI 2 object model - -The benchmarks above measure code generation. At run time, the constructs -ABI revision 2 introduced are deliberately cheap: - -- **Object references.** `{tag}_clone` is one atomic increment on the - producer's `Arc` and `{tag}_destroy` is one atomic decrement (plus the - drop when it reaches zero). Passing an object as a parameter is a single - pointer with no count traffic; the producer clones only if it retains the - object. -- **Objects inside value buffers.** An object token is eight bytes in the - buffer and one `_clone` at encode time; decoding adopts the reference with - no further call into the producer. -- **Callback interfaces.** A method call is one indirect call through a - static vtable plus the usual marshalling of its arguments. Registering an - implementation allocates one context per registration on the consumer side - and one `Arc` on the producer side; there is no per-call allocation for - scalar arguments. -- **Async spawner.** The default spawner costs one thread per in-flight - future. Producers with many concurrent async calls should install a pooled - runtime with `weaveffi::set_spawner`. - -## CI artifacts - -The [`bench.yml`][bench-workflow] workflow runs `cargo bench` on every -push to `main` and uploads the captured `criterion` output as a -`bench-results` artifact (retained for 90 days). To inspect the most -recent run: - -1. Open the [bench workflow runs][bench-runs] on GitHub. -2. Pick the latest run that succeeded. -3. Download the `bench-results` artifact and extract `bench.txt`; it - contains the full criterion output (medians, ranges, outlier counts) - for the entire workspace. - -The workflow does not gate merges on absolute thresholds today; instead -it serves as the authoritative trail when a PR claims to improve or -preserve benchmark numbers. - -[core-bench]: https://github.com/weavefoundry/weaveffi/blob/main/crates/weaveffi-core/benches/codegen_bench.rs -[cli-bench]: https://github.com/weavefoundry/weaveffi/blob/main/crates/weaveffi-cli/benches/generate_bench.rs -[bench-workflow]: https://github.com/weavefoundry/weaveffi/blob/main/.github/workflows/bench.yml -[bench-runs]: https://github.com/weavefoundry/weaveffi/actions/workflows/bench.yml -[criterion.rs]: https://github.com/bheisler/criterion.rs diff --git a/docs/src/reference/README.md b/docs/src/reference/README.md deleted file mode 100644 index 46369624..00000000 --- a/docs/src/reference/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Reference - -The reference pages state the contracts the rest of the documentation -assumes. Two of them are normative: the [C ABI Contract](abi.md) and the -[Value Buffer Protocol](value-buffers.md) define what a producer exports and -what a consumer may rely on, and every generator, sample, and conformance -lane is checked against them. The others summarize those contracts from a -particular angle and point back to the normative text rather than -restating it. - -| Page | Read it when you need | -|------|-----------------------| -| [IDL Reference](idl.md) | The complete schema `0.9.0` grammar: every field, every type position, every `ValidationError` code with its message and suggestion, and validated examples of each construct | -| [C ABI Contract](abi.md) | The normative ABI revision 2: symbol shapes, ownership by position, object reference counting, callback vtables, error codes, cancellation, and the revision check | -| [Value Buffer Protocol](value-buffers.md) | The byte-level encoding of records, rich enums, optionals, lists, maps, object tokens, and structured error payloads | -| [Memory and Error Model](memory-error.md) | A working summary of who frees what: strings, bytes, buffers, `_clone`/`_destroy`, callback `free`, `weaveffi_error_set`/`_clear`/`_free`, and cancel tokens | -| [Naming and Package Conventions](naming.md) | How the project names itself and how the generators spell C symbols, wrapper types, error classes, and the Kotlin package | - -For the pipeline that turns an IDL into these outputs, see -[Architecture](../architecture.md); for per-language usage, see the -generator pages. diff --git a/docs/src/reference/abi.md b/docs/src/reference/abi.md index 7168e3b1..1f6439b8 100644 --- a/docs/src/reference/abi.md +++ b/docs/src/reference/abi.md @@ -1,247 +1,282 @@ # C ABI Contract -This page is the normative description of the WeaveFFI C ABI, revision 2. It -is the contract the `#[weaveffi::module]` macro implements on the producer -side and that every language generator consumes. Where another page and this -one disagree, this one wins. - -The ABI revision is exported by every producer as `{prefix}_abi_version()` -and embedded in every generated consumer as `{PREFIX}_ABI_VERSION`. A -consumer that can do so cheaply compares the two at load time and refuses to -run against a producer built for another revision. +This page is the normative description of the WeaveFFI C ABI, **revision 3**. +The `#[weaveffi::module]` macro implements it on the producer side, the C +generator renders it as a header, and every other generator consumes it. +Where another page disagrees with this one, this one wins. ## Vocabulary -- **Producer**: the native library that implements the symbols (a Rust cdylib +- **Producer**: the native library implementing the symbols (a Rust cdylib built with the macro, or a C, C++, or Zig library implementing the header). -- **Consumer**: the generated bindings in one of the eleven target languages. -- **Prefix**: the configurable symbol prefix, `weaveffi` by default. Runtime - symbols (`weaveffi_error`, `weaveffi_free_string`, and so on) always keep - the `weaveffi_` spelling; a non-default prefix is aliased onto them in the - generated C header. +- **Consumer**: generated bindings, or C code calling the header directly. +- **`{p}`**: the library's C symbol prefix from its + [identity](naming.md), a lower-snake identifier (`kvstore`). **`{P}`** is + the same prefix uppercased, used for macros (`KVSTORE`). +- **`{path}`**: a module path joined with `_` (`kv`, `kv_stats`). - **Slot**: one C parameter. An IDL parameter lowers to one or more slots. -## Type families - -Every resolved IDL type belongs to exactly one family, which decides how it -crosses a call boundary. +Every identifier the ABI defines starts with `{p}_` (functions, types, +constants) or `{P}_` (macros). There are no fixed `weaveffi_*` symbols, so any +number of WeaveFFI libraries can link into one process, statically or +dynamically. + +## Load-time checks + +The revision is the constant `{P}_ABI_VERSION` in the header and the result +of `{p}_abi_version()`. It changes only when the runtime surface, the +value-buffer encoding, or the object, callback, async, or iterator +conventions change incompatibly; it's independent of the crate version and +the IDL schema version. + +Every top-level module `m` also has a **contract checksum**: the producer +exports `uint64_t {p}_{m}_checksum(void)`, and the header defines +`#define {P}_{M}_CHECKSUM 0x...ull` (`M` is the module name uppercased). The +value is a 64-bit FNV-1a hash over a canonical walk of the module and its +submodules: every declaration name, every type reference, parameter and +field order, declaration order, the `throws`, `async`, and `cancellable` +flags, enum values, and error-code values. Documentation, deprecation +messages, and error messages are excluded, so prose edits never break a +deployed binding. The macro and the CLI compute it with the same function +(`weaveffi_model::checksum::module_checksum`) over the same IR. + +A generated consumer checks `{p}_abi_version()` and every top-level +module's checksum once, when it loads the library, and raises the language's +load or import error naming the mismatched module. A C or C++ producer that +implements an IDL by hand returns the header's constant: -| Family | IDL types | Parameter slots | Return | -|------------|---------------------------------------------------------------|---------------------------------------------------------|-----------------------------------------------| -| Direct | `i8`..`u64`, `f32`, `f64`, `bool`, C-style enums | one slot by value | by value | -| String | `string` | `const char* {name}` (NUL-terminated, borrowed) | `const char*`, freed with `free_string` | -| Bytes | `bytes` | `const uint8_t* {name}_ptr, size_t {name}_len` (borrowed) | `const uint8_t*` + `size_t* out_len`, freed with `free_bytes` | -| Buffer | records, rich enums, `T?` (except `Interface?`), `[T]`, `{K:V}` | `const uint8_t* {name}_ptr, size_t {name}_len` (borrowed) | `const uint8_t*` + `size_t* out_len`, freed with `free_bytes` | -| Object | interfaces, `Interface?` | `const {tag}* {name}` (borrowed; null is "none" for `?`) | `{tag}*` (owned reference; null is "none" for `?`) | -| Callback | callback interfaces | `void* {name}_ctx, const {tag}_vtable* {name}_vtable` | not allowed | -| Iterator | `iter` | not allowed | `{IterTag}*` plus `_next`/`_destroy` | - -C-style enums are `typedef enum` with `int` storage and cross as `int32_t`. -Every synchronous symbol carries a trailing `{prefix}_error* out_err` slot -after all inputs and out-parameters. +```c +uint64_t kv_kv_checksum(void) { return KV_KV_CHECKSUM; } +``` ## Runtime surface -Every producer exports these symbols (the Rust macro's `export_runtime!` -emits them): +Every producer exports these symbols (`weaveffi::export_runtime!()` emits +them for a Rust producer): ```c -uint32_t weaveffi_abi_version(void); -void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -void weaveffi_error_clear(weaveffi_error* err); -void weaveffi_error_free(weaveffi_error* err); -void weaveffi_free_string(const char* ptr); -void weaveffi_free_bytes(uint8_t* ptr, size_t len); -uint8_t* weaveffi_alloc(uint32_t size); /* wasm32 producers only */ -void weaveffi_dealloc(uint8_t* ptr, uint32_t size); /* wasm32 producers only */ -weaveffi_cancel_token* weaveffi_cancel_token_create(void); -void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); -``` - -### Errors +#define {P}_ABI_VERSION 3u +uint32_t {p}_abi_version(void); -```c -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; +typedef struct {p}_error { + int32_t code; /* 0 ok; >0 domain code; <0 runtime code */ + const char* message; /* NUL-terminated UTF-8, producer-owned */ + const uint8_t* payload_ptr; /* the code's fields as a value buffer, or NULL */ size_t payload_len; -} weaveffi_error; -``` - -`code == 0` is success. Positive codes are the module's declared domain codes. -Negative codes are reserved for the runtime: - -| Code | Constant | Meaning | -|------|-------------------------|------------------------------------------------------------------| -| `-1` | `GENERIC_ERROR_CODE` | an untyped producer error (`Result`) | -| `-2` | `PANIC_ERROR_CODE` | the producer panicked; the message carries the panic text | -| `-3` | `MARSHAL_ERROR_CODE` | an argument could not be lifted (null, non-UTF-8, bad enum, malformed buffer) | -| `-4` | `FOREIGN_ERROR_CODE` | a consumer callback-interface implementation raised; the message carries the foreign error text | +} {p}_error; +void {p}_error_set({p}_error* err, int32_t code, const char* message); /* copies message */ +void {p}_error_clear({p}_error* err); /* frees message and payload; code = 0 */ +void {p}_error_free({p}_error* err); /* clear, then free a heap-boxed error */ -On a `throws: true` function a non-zero code is a typed domain error the -wrapper surfaces through the language's error channel; on a non-throwing -function any non-zero code is a programming error the wrapper traps on. See -[Memory and Error Model](memory-error.md). +void {p}_free_bytes(uint8_t* ptr, size_t len); /* strings, bytes, buffers */ -## Objects (interfaces) +typedef struct {p}_cancel_token {p}_cancel_token; +{p}_cancel_token* {p}_cancel_token_create(void); /* refcount 1 */ +void {p}_cancel_token_cancel({p}_cancel_token* token); /* idempotent */ +bool {p}_cancel_token_is_cancelled(const {p}_cancel_token* token); +void {p}_cancel_token_destroy({p}_cancel_token* token); /* drops one reference */ -An interface `Name` declared in module path `p` has the opaque tag -`{prefix}_{p}_{Name}` and these lifecycle symbols: +uint64_t {p}_debug_live(int32_t kind); /* leak counters; 0 when not counted */ -```c -typedef struct {tag} {tag}; -{tag}* {tag}_clone(const {tag}* self); -void {tag}_destroy({tag}* self); +/* wasm32 producers only */ +uint8_t* {p}_alloc(uint32_t size); +void {p}_dealloc(uint8_t* ptr, uint32_t size); ``` -Objects are **reference counted by the producer**. A `{tag}*` is a strong -reference. `_clone` returns a new strong reference to the same object (the -pointer value is identical); `_destroy` releases one strong reference. The -object is dropped when the last reference is released. Both accept null as a -no-op. Because the count lives in the producer, an object may be shared by any -number of consumer wrappers, stored inside records and collections, and -outlive the call that produced it. An in-flight async call holds its own -reference, so destroying a wrapper while a call is pending is safe. - -Ownership rules for every position an object can appear in: - -| Position | Direction | Rule | -|----------------------------------------------|--------------------|-------------------------------------------------------------| -| top-level parameter (`Store`, `Store?`) | consumer -> producer | borrowed for the call; the producer clones if it retains it | -| return, async result, iterator element | producer -> consumer | one strong reference transfers; the consumer adopts it | -| callback-interface method parameter | producer -> consumer | one strong reference transfers (the slot is `{tag}*`, not `const`); the consumer adopts it | -| inside a value buffer (any direction) | either | the token carries one strong reference; the reader adopts it | +Every function accepts null where a pointer is optional (`error_clear`, +`error_free`, `free_bytes`, the token functions, `_clone`, `_destroy`) and +treats it as a no-op; a null token reads as never cancelled. + +`{p}_debug_live(kind)` reports live resources for leak checks: `0` objects, +`1` foreign callbacks, `2` iterators, `3` cancel tokens, `4` returned byte +allocations. A Rust producer counts them only with the `leak-check` feature; +otherwise, and in a producer that doesn't count, it returns `0`. + +## Errors + +Every synchronous symbol except `_clone`, `_destroy`, and an iterator's +`_destroy` takes a trailing `{p}_error* out_err` after all inputs and +out-parameters. The caller owns the struct and zero-initializes it; the +producer writes it only on failure, and then returns a zero sentinel (`0`, +`false`, `NULL`) the caller must not use. The caller releases a failure's +message and payload with `{p}_error_clear`. + +| Code | Meaning | +|------|---------| +| `0` | success; every pointer is null | +| `> 0` | a declared code of the callable's error domain; `payload_ptr`/`payload_len` hold the code's fields, if any | +| `-1` | generic: an untyped producer error | +| `-2` | the producer panicked | +| `-3` | marshalling failure: an argument couldn't be lifted (null with a non-zero length, invalid UTF-8, an out-of-range enum value, a malformed buffer) | +| `-4` | a consumer's callback-interface implementation failed | +| `-5` | cancelled (async completions only) | + +A non-zero code on a callable without `throws` is a producer bug or a failed +callback; consumers trap on it rather than report a domain error. + +## Families and slots + +Every resolved type belongs to one family, which decides how it crosses: + +| Family | IDL types | Parameter slots | Return | +|--------|-----------|-----------------|--------| +| Direct | integers, `f32`, `f64`, `bool`, C-style enums | one by value (`bool` is C `bool`; enums are `int32_t`) | by value | +| String | `string` | `const uint8_t* {n}_ptr, size_t {n}_len`: UTF-8, not NUL-terminated, borrowed | `const uint8_t*` plus trailing `size_t* out_len` | +| Bytes | `bytes` | same as String | same as String | +| Buffer | structs, rich enums, `T?` (except `Interface?`), `[T]`, `{K:V}` | `const uint8_t* {n}_ptr, size_t {n}_len` holding a [value buffer](value-buffers.md), borrowed | same as String | +| Object | interfaces, `Interface?` | `const {p}_{path}_{I}* {n}`, borrowed; null means none | `{p}_{path}_{I}*`, one strong reference; null means none | +| Callback | callback interfaces | `void* {n}_ctx, const {p}_{path}_{Cb}_vtable* {n}_vtable` | not allowed | +| Iterator | `iter` | not allowed | `{Iter}*` (see [Iterators](#iterators)) | + +A parameter `ptr` may be null when its `len` is `0`. A String, Bytes, or +Buffer return is a producer allocation the consumer releases with +`{p}_free_bytes(ptr, len)` using the exact length written to `out_len`; null +with length `0` is the empty value. Out-parameters for a return precede +`out_err`. Methods take the receiver as a leading `const {p}_{path}_{I}* self`. + +## Symbol names + +All names are computed once by the binding model and recorded in one symbol +table; validation fails with `SymbolCollision` if two declarations would +produce the same identifier. + +| Declaration | C identifier | +|-------------|--------------| +| function `f` | `{p}_{path}_f` | +| interface `I` | type `{p}_{path}_I`; members `{p}_{path}_I_{member}`; `{p}_{path}_I_clone`, `{p}_{path}_I_destroy` | +| async function `f` | launcher `{p}_{path}_f` (no suffix); completion type `{p}_{path}_f_callback` | +| iterator returned by `f` | type `{p}_{path}_{Owner_}{PascalF}Iterator`, plus `{Iter}_next` and `{Iter}_destroy` | +| C-style enum `E`, variant `V` | type `{p}_{path}_E`, constant `{p}_{path}_E_V` | +| error domain `D`, code `C` | type `{p}_{path}_D`, constant `{p}_{path}_D_C` | +| callback interface `Cb` | vtable type `{p}_{path}_Cb_vtable` | +| top-level module `m` | `{p}_{m}_checksum`, `{P}_{M}_CHECKSUM` | +| runtime | the names in [Runtime surface](#runtime-surface) (reserved) | + +`{Owner_}` is empty for a free function and the interface name plus `_` for +a member, and the function name is converted to PascalCase: `stream_items` +in `kitchen` is `kitchen_sink_kitchen_StreamItemsIterator`, and +`Store.list_keys` in `kv` is `kvstore_kv_Store_ListKeysIterator`. Records and +rich enums produce no C identifiers (they're buffers), but their names are +still reserved in the module's type namespace. IDL spellings are never +re-cased in C. + +## Objects + +Objects are **reference counted by the producer**. A `{p}_{path}_{I}*` is one +strong reference. `_clone` returns another strong reference to the same +object (the pointer value is unchanged); `_destroy` releases one, and the +object is dropped with the last reference. A consumer may hold any number of +references and use them from any thread. + +| Position | Rule | +|----------|------| +| parameter | borrowed for the call; the producer takes its own reference if it retains the object | +| return, async result, iterator element | one strong reference transfers to the consumer | +| callback method parameter | one strong reference transfers to the consumer (the slot is `{p}_{path}_{I}*`, not `const`) | +| object token inside a value buffer | carries one strong reference; the reader adopts it | + +A consumer encoding an object into a buffer writes a fresh reference from +`_clone`. An in-flight async call holds its own reference, so releasing a +wrapper while a call is pending is safe. -A consumer that encodes an object into a value buffer must therefore call -`_clone` and write the returned pointer, never the pointer it still holds. -See [Value Buffer Protocol](value-buffers.md#objects). +## Callback interfaces -Methods take the receiver as the leading `const {tag}* self` slot. -Constructors are statics returning `{tag}*`. +```c +typedef struct {p}_{path}_{Cb}_vtable { + (*{method})(void* ctx, , {p}_error* out_err); /* declaration order */ + void (*free)(void* ctx); /* always last */ +} {p}_{path}_{Cb}_vtable; +``` -## Callback interfaces +A callback interface has no exported symbols; it lowers to a vtable type. +The consumer passes a `ctx` it owns and a pointer to a vtable that outlives +the producer's use of it (in practice, one static vtable per interface). The +producer may call any entry, any number of times, from any thread, until it +calls `free(ctx)` exactly once, after which it never touches `ctx` again. + +Method parameters are borrowed for the duration of the call (strings, bytes, +and buffers as `ptr`/`len`), except objects, which transfer one strong +reference. Returns are `void` or Direct, so no consumer allocation crosses +back; methods are never async or throwing. + +A consumer implementation that fails reports it by calling +`{p}_error_set(out_err, -4, message)` with a borrowed message, which the +producer copies with its own allocator; it never writes `message` itself. The +producer treats any non-zero code as a failure and reports a positive one as +`-4`, so a consumer bug can't masquerade as a domain error. It abandons the +operation in progress, and the original caller observes `-4` with the +consumer's message (through `out_err` or the async completion). + +A Rust producer sees the failure according to the trait method's return +type. A method returning `Result` gets +`Err(ForeignError { code, message })` and nothing unwinds. A method returning +a plain `T` unwinds to the enclosing thunk on a `panic = "unwind"` build; on +a `panic = "abort"` build (notably `wasm32-unknown-unknown`) it returns the +type's zero value, the producer continues, and the thunk reports the recorded +failure in place of its result. Deferral applies only while a WeaveFFI thunk +is running on the current thread; a failure on any other thread is written to +stderr and dropped, never attached to a later call. -A callback interface `Name` in module path `p` is a set of methods the -**consumer** implements and the **producer** calls. It lowers to a vtable -type and no exported symbols: +## Async functions ```c -typedef struct {prefix}_{p}_{Name}_vtable { - /* one entry per method, in declaration order */ - (*{method})(void* ctx, , weaveffi_error* out_err); - /* always last */ - void (*free)(void* ctx); -} {prefix}_{p}_{Name}_vtable; +typedef void (*{p}_{path}_{f}_callback)(void* context, {p}_error* err, ); +void {p}_{path}_{f}(, , [{p}_cancel_token* cancel_token,] + {p}_{path}_{f}_callback callback, void* context); ``` -A callback-interface parameter `listener` lowers to two slots: -`void* listener_ctx, const {tag}_vtable* listener_vtable`. The consumer owns -`ctx` (typically a key into a handle map that keeps the implementing object -alive) and a process-wide static vtable per callback interface. The producer -may call any method any number of times, from any thread, until it calls -`free(ctx)` exactly once, after which it never touches `ctx` again. The -producer holds the pair behind a reference count of its own, so it may clone -the callback freely; `free` fires when its last clone drops. - -Method parameters follow the usual families with these ownership rules: -strings, bytes, and buffers are borrowed for the duration of the call (the -consumer copies what it needs); objects transfer one strong reference. Method -returns are restricted to `void` and the Direct family, so no allocation -crosses back from the consumer. Methods can't be `async` or `throws`. - -`out_err` is the consumer's channel for an implementation that raised. The -consumer must not write `message` with its own allocator (the producer frees -it); instead it calls the runtime helper +Result slots are: none for a `void` function; `result` for a Direct value or +an object (adopted); or `const uint8_t* result_ptr, size_t result_len` for a +String, Bytes, or Buffer result, released with `{p}_free_bytes`. `err` is +`NULL` on success; otherwise it's heap-boxed, owned by the consumer, and +released with `{p}_error_free`. The launcher has no `out_err`; every failure, +including an input marshalling failure, is delivered through the callback. + +The runtime guarantees: + +- The callback fires **exactly once**, from any thread, possibly before the + launcher returns. +- If the executor drops the future before it completes, the completion fires + with `-5` from the drop guard. +- For a `cancellable` function, the producer takes its own reference on the + token at launch; the consumer may cancel and destroy its reference at any + time afterward. Cancelling wakes the future; the runtime then drops it and + completes with `-5` and the message `cancelled`, unless it already + completed. Producer code may still poll the token for cooperative cleanup. + +## Iterators ```c -void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); +{Iter}* launcher(, , {p}_error* out_err); +int32_t {Iter}_next({Iter}* iter, , {p}_error* out_err); /* 1 item, 0 done */ +void {Iter}_destroy({Iter}* iter); ``` -with `FOREIGN_ERROR_CODE` (`-4`) and a borrowed message, which the producer -copies. A positive code written here is normalized to `-4` (a consumer failure -must never masquerade as one of the producer's domain errors); the other -reserved negative codes pass through. The producer abandons the current call -when a callback method reports a non-zero code, and the original caller -observes the code with the foreign message, exactly as it would observe a -producer panic. - -On the Rust side, on any build with unwinding (`panic = "unwind"`, the default -for every native target), the abandonment is literally a panic: the generated -trait implementation unwinds from the callback method, through the producer's -frames, to the nearest exported thunk, which catches it. Producer code should -therefore treat every callback-interface method call as potentially panicking, -exactly as it would treat a call into an arbitrary closure. In particular, don't -hold a `std::sync::Mutex` guard across a callback call unless the lock is -tolerant of poisoning (or snapshot the state and release the lock first). - -### Builds without unwinding - -On a `panic = "abort"` build (notably `wasm32-unknown-unknown`) nothing can -unwind, so the runtime takes a second route with the same observable result -for the caller: the failure is recorded in a thread-local slot, the callback -method returns the value the vtable entry produced (the consumer's default: -zero, `false`, or the enum's first variant), and the producer's code runs to -completion. Every exported thunk drains that slot after the producer returns -and reports the recorded failure through `out_err` (or the async completion -callback) in place of the result, which is discarded. The first failure recorded -during a call wins. - -Two consequences follow for producers that must work on such builds. The -producer's code observes the default return value once and may act on it (a -"return `false` to detach" protocol detaches), so producers should prefer -protocols where the zero value is harmless. And any side effect the producer -performs after the failed callback still happens, though a consumer wrapper is -expected to refuse further callback invocations during the same producer call -with the same error, so the implementation itself is not consulted again. -Destroy entry points discard a failure recorded while dropping, since they have -no `out_err` to report it through. - -Callback interfaces can't appear in returns, inside buffers, as iterator -elements, or as callback-method parameters. +Item out slots are `T* out_item` for a Direct or object element and +`const uint8_t** out_item, size_t* out_len` for a String, Bytes, or Buffer +element. Each element follows the return rules of its family. `_next` is +internally synchronized, so concurrent calls are safe (though pointless). +The consumer calls `_destroy` exactly once, after exhaustion or to abandon +the iteration. ## Value buffers -Records, rich enums, optionals, lists, and maps cross as one packed -little-endian encoding; see [Value Buffer Protocol](value-buffers.md). Revision -2 adds the object token (`u64` pointer, one strong reference) so interfaces -compose with every buffered shape. +Buffers have one encoding, specified in [Value Buffers](value-buffers.md): +little-endian, packed, `u32` lengths and counts, object tokens as `u64`. +Error payloads use the same encoding. -## Async functions - -An `async` function `f` lowers to a launcher and a completion callback -typedef: - -```c -typedef void (*{sym}_callback)(void* context, weaveffi_error* err, ); -void {sym}_async(, [weaveffi_cancel_token* cancel_token,] {sym}_callback callback, void* context); -``` +## Producers in other languages -The launcher returns immediately; the callback fires exactly once, from an -arbitrary producer thread, with everything it delivers owned by the consumer: -a non-null `err` is heap-boxed and released with `weaveffi_error_free`; -string results with `free_string`; bytes and buffered results with -`free_bytes`; object results are adopted. The producer runs the future on -its configured spawner (`weaveffi::set_spawner`); the default spawner drives -each future on a dedicated thread. +A producer implementing a generated header exports: -## Iterators +1. every function the header declares for the API; +2. one `{p}_{m}_checksum` per top-level module, returning the header's + `{P}_{M}_CHECKSUM`; +3. the runtime surface above (`alloc` and `dealloc` only for `wasm32`), with + `{p}_abi_version` returning `{P}_ABI_VERSION`. -An `iter` return lowers to a launcher returning `{IterTag}*`, an -`int32_t {IterTag}_next({IterTag}* iter, , weaveffi_error* out_err)` -that writes one element (returning `1`) or reports exhaustion (returning `0`), -and `void {IterTag}_destroy({IterTag}* iter)`, called exactly once. Elements -follow the return rules of their family: strings are freed with -`free_string`, bytes and buffers with `free_bytes`, objects are adopted. - -## What revision 2 removed - -Revision 1 had untyped `handle` tokens and typed `handle` pointers, -borrowed `&str`/`&[u8]` parameter spellings, `mutable` parameters, -module-level `callbacks:` and `listeners:` (`register_*`/`unregister_*` -returning a subscription id), and a batch-free `weaveffi_arena_*` runtime. -All of them are gone: interfaces with reference counting replace handles, -`string`/`bytes` parameters are always borrowed views at the ABI, and callback -interfaces replace callbacks and listeners. +The producer owns every allocation it hands out, so `error_clear`, +`error_free`, and `free_bytes` must free with the same allocator that +produced the message, payload, or run. When building the library, define +`{P}_BUILD` so the header's `{P}_API` macro exports symbols on Windows. +`conformance/c/producer.c` is a complete example. diff --git a/docs/src/reference/idl.md b/docs/src/reference/idl.md index a3e1e2e3..a2c732ac 100644 --- a/docs/src/reference/idl.md +++ b/docs/src/reference/idl.md @@ -1,38 +1,30 @@ -# IDL Reference +# IDL Schema -WeaveFFI consumes a declarative IDL (interface definition language) that -describes modules, types, and functions. YAML, JSON, and TOML are all -supported; this reference uses YAML throughout. This page documents schema -version `0.9.0`, the only version the current tools accept. +An IDL document describes an API: its modules, types, and functions. YAML, +JSON, and TOML are all accepted; this page uses YAML. It documents schema +version `0.10.0`, the only version the current tools accept. A Rust producer +never writes one by hand (the CLI extracts it from the source), but the +schema is the same either way. Every YAML block on this page is a complete document that passes -`weaveffi validate`, except the blocks whose first line is `# rejected:`, -which show exactly what the validator refuses and which error code it -reports. +`weaveffi validate`, except blocks whose first line is `# rejected:`, which +show what the validator refuses and the error it reports. -## Editor autocomplete (JSON Schema) +## Editor support -WeaveFFI ships a JSON Schema for the IDL. To get autocomplete and validation -in editors that support the [YAML Language Server](https://github.com/redhat-developer/yaml-language-server) -(VS Code, Neovim, Helix, and others), add this header comment to the top of -your YAML file: +The CLI prints a JSON Schema for the document with +`weaveffi schema --format json-schema`, and a copy is checked in as +[`weaveffi.schema.json`](https://github.com/weavefoundry/weaveffi/blob/main/weaveffi.schema.json). +Editors using the YAML Language Server pick it up from a header comment: ```yaml # yaml-language-server: $schema=./weaveffi.schema.json ``` -The schema is generated by `weaveffi schema --format json-schema`, and a copy -is checked in at [`weaveffi.schema.json`](https://github.com/weavefoundry/weaveffi/blob/main/weaveffi.schema.json) -in the repository root. - -## Top-level structure - -The shape of an IDL document, with placeholder ellipses for nested arrays -and objects: +## Document structure ```text -# yaml-language-server: $schema=./weaveffi.schema.json -version: "0.9.0" +version: "0.10.0" modules: - name: my_module doc: "..." @@ -45,356 +37,189 @@ modules: modules: [...] ``` -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). Every object in the schema -rejects unknown keys, so a stray `package:` block, a leftover -`callbacks:` list, or a misspelled field fails to parse instead of being -silently ignored. - -A complete, validating example lives at the bottom of this page in the -[Complete example](#complete-example) section. +The document describes only the API. Package identity and generator options +live in [`weaveffi.toml`](../guides/config.md). Every object rejects unknown +keys, so a misspelled field fails to parse instead of being ignored. -| Field | Type | Required | Description | -|-----------|-----------------|----------|----------------------------------------------------------------------| -| `version` | string | yes | Schema version; only the current version (`"0.9.0"`) is accepted | -| `modules` | array of Module | yes | One or more modules | +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `version` | string | yes | Must be `"0.10.0"` | +| `modules` | array of Module | yes | Top-level modules | ### Module -| Field | Type | Required | Description | -|-----------------------|-----------------------------|----------|---------------------------------------------------------------------------------| -| `name` | string | yes | Identifier used as a namespace segment and C symbol component (e.g. `kv`) | -| `doc` | string | no | Documentation for the module as a whole | -| `functions` | array of Function | no | Free functions exported by this module | -| `interfaces` | array of Interface | no | Reference-counted object types (see [Interfaces](#interfaces)) | -| `callback_interfaces` | array of CallbackInterface | no | Method sets the consumer implements (see [Callback interfaces](#callback-interfaces)) | -| `structs` | array of Struct | no | Record (value) types | -| `enums` | array of Enum | no | C-style or algebraic enums | -| `errors` | ErrorDomain | no | The module's error domain (see [Error domain](#error-domain)) | -| `modules` | array of Module | no | Nested submodules (see [Nested modules](#nested-modules)) | +| Field | Type | Meaning | +|-------|------|---------| +| `name` | string (required) | Namespace segment and C symbol component (`kv`) | +| `doc` | string | Module documentation | +| `functions` | array of Function | Free functions | +| `structs` | array of Struct | Records (value types) | +| `enums` | array of Enum | C-style or rich enums | +| `interfaces` | array of Interface | Reference-counted object types | +| `callback_interfaces` | array of CallbackInterface | Method sets the consumer implements | +| `errors` | ErrorDomain | The module's error domain | +| `modules` | array of Module | Nested submodules | ### Function -| Field | Type | Required | Description | -|---------------|----------------|----------|--------------------------------------------------------------------------------------------------------------------| -| `name` | string | yes | Function identifier | -| `params` | array of Param | no | Ordered input parameters; defaults to none (`params: []` and omitting the key are equivalent) | -| `return` | TypeRef | no | Return type; omit for a function that returns nothing | -| `doc` | string | no | Documentation string | -| `throws` | bool | no | Report typed domain errors (default `false`); requires an error domain in scope (see [Error domain](#error-domain)) | -| `async` | bool | no | Lower to an asynchronous launcher plus completion callback (default `false`) | -| `cancellable` | bool | no | Accept a cancel token (only meaningful with `async: true`) | -| `deprecated` | string | no | Deprecation message propagated to every generated binding | - -The same shape describes an interface's constructors, methods, and statics, -and a callback interface's methods, with the restrictions noted in their -sections. - -### Param - -| Field | Type | Required | Description | -|--------|---------|----------|----------------------------------------------------------------------| -| `name` | string | yes | Parameter name | -| `type` | TypeRef | yes | Parameter type | -| `doc` | string | no | Documentation string (see [Documentation comments](#documentation-comments)) | - -### What schema 0.9.0 removed - -Schema 0.9.0 accompanies ABI revision 2 and drops the pre-1.0 spellings -below. There is no compatibility shim; the parser rejects each with a -message pointing at its replacement. - -| Removed | Replacement | -|-------------------------------------|--------------------------------------------------------------------------------------------------------------| -| `handle`, `handle` types | Declare an [interface](#interfaces) and reference it by name; objects are reference counted by the producer | -| `&str`, `&[u8]` types | `string` and `bytes`; parameters are always borrowed views at the ABI, so the borrowed spellings said nothing | -| `Param.mutable` | Removed; no type crosses the boundary by mutable reference | -| `Function.since` | Use `deprecated:` text when a function is on its way out | -| module-level `callbacks:` | A [callback interface](#callback-interfaces) with one method | -| module-level `listeners:` | A callback interface passed to a `subscribe`-style method on an interface | - ---- - -## Package metadata - -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.kts`, `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. - ---- - -## Type syntax - -A `TypeRef` is written as a string. The grammar is small: - -| Syntax | Meaning | -|------------|---------------------------------------------------------------| -| `i32`, `string`, ... | A [primitive](#primitive-types) | -| `Name` | A user type: struct, enum, interface, or callback interface, by bare or dot-qualified name | -| `T?` | [Optional](#optional-types) | -| `[T]` | [List](#list-types) | -| `{K:V}` | [Map](#map-types) | -| `iter` | [Lazy iterator](#iterator-types), returns only | - -The parser reads outside in: `[Contact?]` is a list of optional contacts, -`[Contact]?` an optional list. Quote anything that YAML would otherwise -interpret (`"string?"`, `"[i32]"`, `"{string:i32}"`). - ---- - -## Primitive types - -Every primitive is valid in parameters, returns, fields, and every composite -position. - -| Type | Description | Example value | -|----------|---------------------------------|---------------| -| `i8` | Signed 8-bit integer | `-12` | -| `i16` | Signed 16-bit integer | `-1000` | -| `i32` | Signed 32-bit integer | `-42` | -| `i64` | Signed 64-bit integer | `9000000000` | -| `u8` | Unsigned 8-bit integer | `200` | -| `u16` | Unsigned 16-bit integer | `60000` | -| `u32` | Unsigned 32-bit integer | `300` | -| `u64` | Unsigned 64-bit integer | `18000000000` | -| `f32` | 32-bit floating point | `1.5` | -| `f64` | 64-bit floating point | `3.14` | -| `bool` | Boolean | `true` | -| `string` | UTF-8 string | `"hello"` | -| `bytes` | Byte buffer | binary data | - -`string` and `bytes` are borrowed for the duration of a call when passed in -(the producer copies what it keeps) and owned by the receiver when returned -(the consumer frees them with `weaveffi_free_string` or -`weaveffi_free_bytes`). See [Memory and Error Model](memory-error.md). - -`[u8]` is accepted and canonicalizes to `bytes` at parse time: the two -encode identically inside a value buffer, and `bytes` is what a Rust -producer's `Vec` maps to. - -> **Note on JavaScript and Wasm:** 64-bit integers (`i64`, `u64`) surface as -> `BigInt` in the Node and WebAssembly backends; every narrower integer and -> the floats surface as `number`. - -### Primitive examples +The same shape describes free functions, interface constructors, methods, +and statics, and callback-interface methods, with the restrictions noted in +their sections. + +| Field | Type | Meaning | +|-------|------|---------| +| `name` | string (required) | Function name | +| `params` | array of Param | Ordered parameters (default none) | +| `return` | TypeRef | Return type; omit for none | +| `doc` | string | Documentation | +| `throws` | bool | Report typed domain errors (default `false`) | +| `async` | bool | Complete asynchronously (default `false`) | +| `cancellable` | bool | Accept a cancel token; requires `async: true` | +| `deprecated` | string | Deprecation message propagated to every binding | + +A Param has a required `name` and `type` and an optional `doc`. + +## Types + +A `TypeRef` is a string: + +| Syntax | Meaning | +|--------|---------| +| `i8` `i16` `i32` `i64` `u8` `u16` `u32` `u64` | Fixed-width integers | +| `f32` `f64` | IEEE 754 floats | +| `bool` | Boolean | +| `string` | UTF-8 text | +| `bytes` | Byte string (`[u8]` is accepted and becomes `bytes`) | +| `Name`, `a.b.Name` | A struct, enum, interface, or callback interface | +| `T?` | Optional | +| `[T]` | List | +| `{K:V}` | Map | +| `iter` | Lazy iterator (returns only) | + +The parser reads outside in: `[Contact?]` is a list of optional contacts and +`[Contact]?` an optional list. Composites nest to any depth. Quote anything +YAML would otherwise interpret (`"string?"`, `"[i32]"`, `"{string:i32}"`). +The Node and Wasm targets surface `i64` and `u64` as `BigInt`. + +**Map keys** must be an integer, `bool`, `string`, or a C-style enum, so +every target can use its native dictionary with exact equality. Floats, +`bytes`, composites, and objects are rejected: ```yaml -version: "0.9.0" +# rejected: InvalidMapKey +version: "0.10.0" modules: - - name: primitives + - name: maps 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 + - name: by_weight params: - - { name: limit, type: u32 } - return: u32 - - - name: timestamp - return: i64 - - - name: is_valid - params: - - { name: token, type: string } - return: bool + - { name: index, type: "{f64:i32}" } +``` - - name: echo - params: - - { name: message, type: string } - return: string +**Iterators** are lazy: every binding pulls one element per step of its +native iteration idiom and destroys the handle exactly once. `iter` is +valid only as the outermost return of a synchronous callable. It's rejected +in a parameter, a field, or nested in another type (`IteratorInInvalidPosition`), +and as the return of an async function (`AsyncIteratorReturn`): - - name: compress +```yaml +# rejected: IteratorInInvalidPosition +version: "0.10.0" +modules: + - name: streaming + functions: + - name: consume params: - - { name: data, type: bytes } - return: bytes + - { name: items, type: "iter" } ``` ---- - -## Struct definitions +### Type positions -Structs define composite value types with named, typed fields. Define them -under the `structs` key of a module, then reference them by name in function -signatures and other type positions. +"Buffered field" means a struct field, rich-enum variant field, or error +payload field. -### Struct schema +| Type | Param | Return | Buffered field | Inside `T?` `[T]` `{K:V}` | Map key | Callback param | Callback return | +|------|-------|--------|----------------|---------------------------|---------|----------------|-----------------| +| integers, floats, `bool` | yes | yes | yes | yes | integers, `bool` | yes | yes | +| `string` | yes | yes | yes | yes | yes | yes | no | +| `bytes` | yes | yes | yes | yes | no | yes | no | +| C-style enum | yes | yes | yes | yes | yes | yes | yes | +| struct, rich enum | yes | yes | yes | yes | no | yes | no | +| interface | yes | yes | yes | yes | no | yes | no | +| callback interface | yes | no | no | no | no | no | no | +| `T?`, `[T]`, `{K:V}` | yes | yes | yes | yes | no | yes | no | +| `iter` | no | yes (sync) | no | no | no | no | no | -| Field | Type | Required | Description | -|--------------|----------------|----------|------------------------------------------------------| -| `name` | string | yes | Struct name (e.g. `Contact`) | -| `doc` | string | no | Documentation string | -| `deprecated` | string | no | Deprecation message applied to the generated type | -| `fields` | array of Field | yes | Must have at least one field | +How each family crosses the boundary is in the [C ABI contract](abi.md#families-and-slots). -Each field: +## Structs -| Field | Type | Required | Description | -|--------|---------|----------|----------------------| -| `name` | string | yes | Field name | -| `type` | TypeRef | yes | Field type | -| `doc` | string | no | Documentation string | +A struct is a record: a value type with named fields, crossing the ABI by +value as a [value buffer](value-buffers.md). Bindings map it to a data +class, struct, or record type; no per-struct C functions exist. -### Struct example +| Field | Type | Meaning | +|-------|------|---------| +| `name` | string (required) | Type name | +| `doc` | string | Documentation | +| `deprecated` | string | Deprecation message | +| `fields` | array of Field (required, non-empty) | Each with `name`, `type`, optional `doc` | ```yaml -version: "0.9.0" +version: "0.10.0" modules: - name: geometry structs: - name: Point - doc: "A 2D point in space" + doc: A point in the plane. fields: - - name: x - type: f64 - doc: "X coordinate" - - name: "y" - type: f64 - doc: "Y coordinate" - - - name: Rect + - { name: x, type: f64 } + - { name: "y", type: f64 } + - name: Polygon fields: - - name: origin - type: Point - - name: width - type: f64 - - name: height - type: f64 - - - name: Config - fields: - - name: timeout - type: i32 - - name: retries - type: i32 - - name: label - type: "string?" - + - { name: points, type: "[Point]" } + - { name: label, type: "string?" } functions: - - name: distance + - name: area params: - - { name: a, type: Point } - - { name: b, type: Point } + - { name: shape, type: Polygon } return: f64 - - - name: bounding_box - params: - - { name: points, type: "[Point]" } - return: Rect ``` -Struct fields may reference other structs, enums, interfaces, optionals, -lists, or maps, nested to any depth. Iterators and callback interfaces are -not valid field types (see [Type positions](#type-positions)). - -A struct crosses the C ABI **by value** as a serialized value buffer: one -`(const uint8_t*, size_t)` pair holding the fields packed in declaration -order. Generators map it to an idiomatic value type (a data class, struct, -or record) with a generated pack and unpack routine; no per-struct C -functions exist. An interface-typed field is encoded as an object token that -carries one strong reference; see [Objects in value buffers](value-buffers.md#objects). - ---- - -## Enum definitions - -Enums define a fixed set of named integer variants. Each variant has an -explicit `value` (`i32`). Define enums under the `enums` key. +## Enums -### Enum schema +An enum is a fixed set of variants, each with an explicit `i32` `value`. -| Field | Type | Required | Description | -|--------------|------------------|----------|---------------------------------------------------| -| `name` | string | yes | Enum name (e.g. `Color`) | -| `doc` | string | no | Documentation string | -| `deprecated` | string | no | Deprecation message applied to the generated type | -| `variants` | array of Variant | yes | Must have at least one variant | +| Field | Type | Meaning | +|-------|------|---------| +| `name` | string (required) | Type name | +| `doc`, `deprecated` | string | Documentation, deprecation message | +| `variants` | array of Variant (required, non-empty) | Each with `name`, `value`, optional `doc` and `fields` | -Each variant: - -| Field | Type | Required | Description | -|----------|----------------|----------|----------------------------------------------| -| `name` | string | yes | Variant name (e.g. `Red`) | -| `value` | i32 | yes | Integer discriminant | -| `doc` | string | no | Documentation string | -| `fields` | array of Field | no | Associated data; makes the enum a *sum type* | - -### Enum example +When no variant has `fields`, the enum is **C-style**: it crosses as an +`int32_t`, may be a map key, and may be returned from a callback method. +When any variant has `fields`, the enum is **rich** (a sum type): it crosses +as a value buffer holding the variant's `value` as a tag followed by its +fields, and bindings map it to a Swift enum with associated values, a sealed +class hierarchy, a tagged union, and so on. Unit and data variants may mix. ```yaml -version: "0.9.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 -``` - -Variant values must be unique within an enum, and variant names must be -unique within an enum. - -### Rich (algebraic) enums: sum types - -When one or more variants declare `fields`, the enum becomes a *rich* enum: -an algebraic sum type whose variants carry associated data (like a Rust -`enum` or a Swift `enum` with associated values). A *unit* variant (no -`fields`) and a *data* variant may coexist in the same enum. - -```yaml -version: "0.9.0" +version: "0.10.0" modules: - name: shapes enums: + - name: Unit + variants: + - { name: Metric, value: 0 } + - { name: Imperial, value: 1 } - name: Shape - doc: "An algebraic shape" variants: - - name: Empty - value: 0 + - { name: Empty, value: 0 } - name: Circle value: 1 fields: - { name: radius, type: f64 } - - name: Rectangle + - name: Rect value: 2 fields: - { name: width, type: f32 } @@ -403,300 +228,141 @@ modules: - name: area params: - { name: shape, type: Shape } + - { name: unit, type: Unit } return: f64 ``` -A plain C-style enum crosses the ABI by value as an `int32_t`. A rich enum -also crosses by value, but as a serialized value buffer: an `i32` tag (the -variant's declared `value`) followed by the active variant's fields in -declaration order, in one `(const uint8_t*, size_t)` pair. No per-enum C -functions are generated; each backend maps the enum to its idiomatic sum -type (a Rust-style enum, a Swift enum with associated values, a sealed class -hierarchy, a tagged union class) with a generated pack and unpack routine. -See the [Value Buffer Protocol](value-buffers.md) for the encoding. - -A variant `fields` entry uses the same shape as a struct field (`name`, -`type`, `doc`) and obeys the positional rules of a buffered slot: no -iterators and no callback interfaces. Field names must be unique within a -variant. - -Only a C-style enum (no variant carries fields) is a member of the *direct* -family: it may be a map key and may be returned from a callback-interface -method. A rich enum is a buffered value like a struct. - ---- +Variant names and values are unique within an enum, and a variant's field +names are unique within the variant. ## Interfaces -An interface is a first-class object type: a stateful resource with identity -and behavior, declared under the `interfaces` key of a module. Where a struct -models a plain data record with fields, an interface models something you -*hold and operate on*: a store, a session, a connection. The object lives -behind the FFI boundary and crosses it as an opaque, reference-counted -pointer; consumers see a real class (or the target's closest analogue) whose -constructors, methods, and statics call into the producer. - -### Interface schema - -| Field | Type | Required | Description | -|----------------|----------------------|----------|-----------------------------------------------------| -| `name` | string | yes | Interface type name (e.g. `Store`) | -| `doc` | string | no | Documentation string | -| `deprecated` | string | no | Deprecation message applied to the generated class | -| `constructors` | array of Constructor | no | Static functions returning a new instance | -| `methods` | array of Function | no | Instance methods | -| `statics` | array of Function | no | Static functions namespaced under the interface | - -An interface must declare at least one member, and constructor, method, and -static names share one namespace per interface (no duplicates across the -three lists). - -Each constructor: - -| Field | Type | Required | Description | -|----------|----------------|----------|---------------------------------------------------------------| -| `name` | string | yes | Constructor identifier (e.g. `open`) | -| `params` | array of Param | no | Input parameters | -| `doc` | string | no | Documentation string | -| `throws` | bool | no | Report a typed domain error on failure (default `false`) | - -A constructor implicitly returns a new instance of its interface, so it -declares no `return` field, and it may not be `async` (expose an async -static factory returning the interface instead). A constructor named `new` -becomes the canonical constructor where the target language has one (Swift -`init`, Python `__init__`); every other constructor becomes a static factory -method. - -Methods and statics use the full [Function schema](#function), including -`throws`, `async`, `cancellable`, and `deprecated`. A method receives the -instance implicitly; it does not declare a `self` parameter. A static takes -no instance at all. - -### Reference counting and lifecycle - -Objects are **reference counted by the producer**. Every interface receives -two implicit lifecycle symbols, `{tag}_clone` and `{tag}_destroy`; you never -declare either in the IDL. A pointer to an object is a strong reference: -`_clone` returns a second strong reference to the same object (the pointer -value is identical), `_destroy` releases one, and the object is dropped when -the last reference goes. - -Every generated wrapper owns exactly one strong reference and releases it -deterministically through the language's natural disposal hook (C++ -destructor, Swift `deinit`, Kotlin `close()` and `AutoCloseable`, Python -`close()` and context managers, .NET `IDisposable`, Ruby `AutoPointer`, Go -`Close()`, Dart `close()`, JavaScript `close()` and `Symbol.dispose`), with a -finalizer or `Cleaner` backstop where the runtime has one. Because the count -lives in the producer, the same object can be held by any number of -wrappers, stored inside records and collections, retained by the producer, -and outlive the call that produced it. - -Ownership follows the reference direction: - -| Position | Direction | Rule | -|---------------------------------------------|----------------------|-------------------------------------------------------------------------| -| parameter (`Store`, `Store?`) | consumer to producer | borrowed for the call; the producer clones if it retains the object | -| return, async result, iterator element | producer to consumer | one strong reference transfers; the wrapper adopts it | -| callback-interface method parameter | producer to consumer | one strong reference transfers; the consumer adopts it | -| inside a value buffer (record field, list element, map value, optional) | either | the object token carries one strong reference; the reader adopts it | - -The normative statement of these rules is in the -[C ABI Contract](abi.md#objects-interfaces). - -### Where interface types may appear - -An interface name is a valid `TypeRef` in **every** position except a map -key: parameters, returns, optionals (`Store?`), lists (`[Store]`), map -values (`{string:Store}`), struct fields, rich-enum variant fields, error -payload fields, iterator elements (`iter`), async returns, and -callback-interface method parameters. `Store?` at the top level of a -parameter or return is a nullable pointer (null means none); inside a value -buffer it uses the ordinary optional flag byte followed by the object token. - -The one rejection is the map key: no target can hash an object by identity -in a way that survives the ABI. The validator reports it as -`InterfaceInInvalidPosition` (alongside `InvalidMapKey`): - -```yaml -# rejected: InterfaceInInvalidPosition -version: "0.9.0" -modules: - - name: kv - interfaces: - - name: Store - methods: - - { name: count, return: i64 } - functions: - - name: index_by_store - params: - - { name: stores, type: "{Store:i32}" } -``` - -### Interface example - -A trimmed version of the `kvstore` sample's `Store` interface, showing an -object in each of the positions above: +An interface is an object type: state with identity and behavior that lives +in the producer and crosses the ABI as an opaque, reference-counted pointer. +Consumers see a class whose constructors, methods, and statics call into the +producer. + +| Field | Type | Meaning | +|-------|------|---------| +| `name` | string (required) | Type name | +| `doc`, `deprecated` | string | Documentation, deprecation message | +| `constructors` | array of Function | Return a new instance; no `return`, never `async` | +| `methods` | array of Function | Take the instance implicitly | +| `statics` | array of Function | No instance | + +An interface needs at least one member, and constructor, method, and static +names share one namespace. A constructor named `new` becomes the canonical +constructor where the target has one (Swift `init`, Python `__init__`); +others become static factories. For an async factory, declare an `async` +static that returns the interface. + +An interface may appear in every position except a map key: parameters, +returns, optionals, lists, map values, record and variant fields, error +payloads, iterator elements, async results, and callback parameters. `Store?` +at the top level of a parameter or return is a nullable pointer. Every +position transfers or borrows references by the rules in +[Errors and Memory](../guides/errors-and-memory.md#objects). ```yaml -version: "0.9.0" +version: "0.10.0" modules: - name: kv errors: name: KvError codes: - { name: KeyNotFound, code: 1001, message: "key not found" } - - { name: IoError, code: 1004, message: "I/O failure" } structs: - name: StoreInfo - doc: A named view of a store; `store` carries a strong reference fields: - { name: label, type: string } - { name: store, type: Store } - - { name: mirror, type: "Store?" } - - { name: count, type: i64 } 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: get - doc: Look up a value by key params: - { name: key, type: string } return: bytes throws: true - - name: count - doc: Return the number of live entries in the store - return: i64 - name: share - doc: A second reference to this same store return: Store - name: larger - doc: Whichever of `self` and `other` holds more entries params: - { name: other, type: "Store?" } return: "Store?" - name: describe - doc: Snapshot this store into a record that carries the object itself params: - { name: label, type: string } - - { name: mirror, type: "Store?" } return: StoreInfo statics: - name: open_many - doc: Open one store per path params: - { name: paths, type: "[string]" } return: "[Store]" throws: true - - name: total_count - doc: Total live entries across `stores` - params: - - { name: stores, type: "[Store]" } - - { name: extra, type: "StoreInfo?" } - return: i64 ``` -### C ABI lowering +```yaml +# rejected: InterfaceInInvalidPosition +version: "0.10.0" +modules: + - name: kv + interfaces: + - name: Store + methods: + - { name: count, return: i64 } + functions: + - name: index + params: + - { name: stores, type: "{Store:i32}" } +``` -An interface lowers to an opaque struct tag, one C symbol per member, and -the two lifecycle symbols. Constructors return an owned reference; methods -take a leading `const {tag}* self` slot before their declared parameters; -statics take no `self`; a top-level object parameter is a borrowed -`const {tag}*`; an object return is an owned `{tag}*`; and a list or record -that carries objects is an ordinary value buffer whose object tokens each -carry one reference. The header the C generator emits for the example above: +An interface lowers to an opaque type, one symbol per member (methods take a +leading `self`), and implicit `_clone` and `_destroy` symbols you never +declare. From the `kitchen_sink` test fixture (prefix `kitchen_sink`, module +`kitchen`): ```c -typedef struct weaveffi_kv_Store weaveffi_kv_Store; - -/* Constructor: returns one strong reference. */ -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_open(const char* path, weaveffi_error* out_err); - -/* Statics: no self slot. `[Store]` in and out is a value buffer of object tokens. */ -WEAVEFFI_API const uint8_t* weaveffi_kv_Store_open_many(const uint8_t* paths_ptr, size_t paths_len, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API int64_t weaveffi_kv_Store_total_count(const uint8_t* stores_ptr, size_t stores_len, const uint8_t* extra_ptr, size_t extra_len, weaveffi_error* out_err); - -/* Methods: an implicit leading self slot. */ -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); -WEAVEFFI_API int64_t weaveffi_kv_Store_count(const weaveffi_kv_Store* self, weaveffi_error* out_err); -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_share(const weaveffi_kv_Store* self, weaveffi_error* out_err); -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_larger(const weaveffi_kv_Store* self, const weaveffi_kv_Store* other, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_kv_Store_describe(const weaveffi_kv_Store* self, const char* label, const weaveffi_kv_Store* mirror, size_t* out_len, weaveffi_error* out_err); - -/* Implicit lifecycle symbols. */ -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_clone(const weaveffi_kv_Store* self); -WEAVEFFI_API void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self); +typedef struct kitchen_sink_kitchen_Gadget kitchen_sink_kitchen_Gadget; +kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_new(int64_t id, kitchen_sink_error* out_err); +const uint8_t* kitchen_sink_kitchen_Gadget_describe(const kitchen_sink_kitchen_Gadget* self, size_t* out_len, kitchen_sink_error* out_err); +kitchen_sink_kitchen_Gadget* kitchen_sink_kitchen_Gadget_clone(const kitchen_sink_kitchen_Gadget* self); +void kitchen_sink_kitchen_Gadget_destroy(kitchen_sink_kitchen_Gadget* self); ``` -Async and iterator-returning members follow the same shapes as free -functions (the async launcher and the iterator handle simply carry the -`self` slot). Free functions and interface members share the module's C -symbol namespace, so a free function named `Store_get` would collide with a -`get` method on an interface `Store`, and a free function named -`Store_clone` would collide with the implicit lifecycle symbol; the -validator rejects both as `AbiSymbolCollision`. +## Callback interfaces -On the Rust side an interface is a `#[weaveffi::interface]` struct that -must be `Send + Sync`; constructors return `Self` or `Arc`, methods take -`&self` or `self: Arc`, and an object in any other position is an -`Arc`. See [The Rust Producer Macro](../guides/producer-macro.md). +A callback interface is the inverse of an interface: methods the consumer +implements and the producer calls, any number of times and from any thread, +for as long as it holds the implementation. ---- +| Field | Type | Meaning | +|-------|------|---------| +| `name` | string (required) | Type name | +| `doc`, `deprecated` | string | Documentation, deprecation message | +| `methods` | array of Function (required, non-empty) | In vtable order | -## Callback interfaces +Methods are restricted, each enforced as `InvalidCallbackMethod`: + +- synchronous: no `async`, no `cancellable`; +- no `throws`: a consumer failure is reported as the runtime code `-4`; +- return nothing or a direct value (an integer, float, `bool`, or C-style + enum), because no consumer allocation can flow back to the producer; +- parameters may be any type except a callback interface or an iterator; + an object parameter transfers one reference to the consumer. -A callback interface is the inverse of an interface: a set of methods the -**consumer** implements and the **producer** calls. Declare one under the -`callback_interfaces` key of a module, then take it as a parameter wherever -the producer needs to call back into the host language: an event -subscriber, an eviction observer, a progress reporter. The consumer passes -an implementation (a class instance, a closure record, a trait object); the -producer keeps it alive as long as it needs to and may call its methods any -number of times, from any thread. - -### Callback interface schema - -| Field | Type | Required | Description | -|--------------|-------------------|----------|----------------------------------------------------------------------| -| `name` | string | yes | Callback interface type name (e.g. `EvictionListener`) | -| `doc` | string | no | Documentation string | -| `deprecated` | string | no | Deprecation message applied to the generated type | -| `methods` | array of Function | yes | Methods the consumer implements, in vtable order; must be non-empty | - -Each method uses the [Function schema](#function) with these restrictions, -each enforced by the validator as `InvalidCallbackMethod`: - -- **Synchronous.** `async: true` and `cancellable: true` are rejected. -- **Never `throws`.** A method has no typed error channel. A consumer - implementation that raises is reported to the original caller as the - runtime's foreign error (`-4`); see below. -- **Direct returns only.** A method returns nothing, a scalar (`i8`..`u64`, - `f32`, `f64`), `bool`, or a C-style enum. Strings, bytes, records, rich - enums, optionals, lists, maps, and objects can't be returned, because no - producer-owned allocation may flow back from the consumer. -- **Parameters may be anything except another callback interface or an - iterator.** Strings, bytes, records, enums, optionals, lists, maps, and - interface objects are all valid; objects transfer a strong reference to - the consumer. - -### Where callback interfaces may appear - -A callback interface is valid **only** as a bare top-level parameter of a -function, constructor, method, or static. It can't be returned, wrapped in an -optional or collection (`Listener?`, `[Listener]`), used as a struct or -variant field, or passed to another callback-interface method. The -validator reports every other position as +A callback interface is valid only as a top-level parameter of a function, +constructor, method, or static. Anything else is `CallbackInterfaceInInvalidPosition`: ```yaml # rejected: CallbackInterfaceInInvalidPosition -version: "0.9.0" +version: "0.10.0" modules: - name: events callback_interfaces: @@ -704,56 +370,34 @@ modules: methods: - { name: on_event, params: [{ name: id, type: i64 }] } functions: - - name: maybe_listen - params: - - { name: listener, type: "Listener?" } - name: current_listener return: Listener ``` -### Callback interface example - -A trimmed version of the `events` sample: a `Subscriber` the consumer -implements, an `EventBus` object that retains subscribers, and an object -(`EventBus`) handed back to the consumer through a callback method: - ```yaml -version: "0.9.0" +version: "0.10.0" modules: - name: events - doc: A publish/subscribe event bus driven by a consumer-implemented subscriber. enums: - name: Delivery - doc: How a subscriber wants to be told about a message. variants: - { name: Accept, value: 0 } - { name: Skip, value: 1 } - - { name: AcceptAndStop, value: 2 } structs: - name: Message fields: - - { name: seq, type: i64 } - { name: topic, type: string } - { name: text, type: string } - - { name: tags, type: "[string]" } callback_interfaces: - name: Subscriber - doc: A consumer-implemented subscriber. methods: - name: route - doc: Decide how the bus should treat `topic` for this subscriber. params: - { name: topic, type: string } return: Delivery - name: on_message - doc: Receive an accepted message; returns the running count. params: - { name: message, type: Message } - return: i64 - - name: on_attached - doc: Receive the bus itself; the consumer adopts the reference. - params: - - { name: bus, type: EventBus } interfaces: - name: EventBus constructors: @@ -763,448 +407,139 @@ modules: params: - { name: subscriber, type: Subscriber } return: i64 - - name: publish - params: - - { name: topic, type: string } - - { name: text, type: string } - - { name: tags, type: "[string]" } - return: i64 - functions: - - name: route_once - doc: Ask `subscriber` how it would route `topic` without a bus. - params: - - { name: subscriber, type: Subscriber } - - { name: topic, type: string } - return: Delivery ``` -### C ABI lowering - -A callback interface lowers to a vtable type and no exported symbols. The -vtable has one function-pointer entry per method, in declaration order, and -a trailing `free`. Every entry takes the consumer's `void* ctx` first and a -`weaveffi_error* out_err` last. A parameter of the type lowers to two slots, -`void* {name}_ctx` and `const {tag}_vtable* {name}_vtable`. The header for -the example above: +A callback interface lowers to a vtable type with one entry per method and a +trailing `free`; a parameter lowers to a `ctx` pointer plus the vtable. From +the `kitchen_sink` fixture: ```c -typedef struct weaveffi_events_Subscriber_vtable { - weaveffi_events_Delivery (*route)(void* ctx, const char* topic, weaveffi_error* out_err); - int64_t (*on_message)(void* ctx, const uint8_t* message_ptr, size_t message_len, weaveffi_error* out_err); - void (*on_attached)(void* ctx, weaveffi_events_EventBus* bus, weaveffi_error* out_err); +typedef struct kitchen_sink_kitchen_ReadyListener_vtable { + void (*on_ready)(void* ctx, int32_t code, const uint8_t* msg_ptr, size_t msg_len, kitchen_sink_error* out_err); + bool (*on_item)(void* ctx, const uint8_t* item_ptr, size_t item_len, kitchen_sink_kitchen_Gadget* gadget, kitchen_sink_error* out_err); void (*free)(void* ctx); -} weaveffi_events_Subscriber_vtable; - -WEAVEFFI_API int64_t weaveffi_events_EventBus_subscribe(const weaveffi_events_EventBus* self, void* subscriber_ctx, const weaveffi_events_Subscriber_vtable* subscriber_vtable, weaveffi_error* out_err); -WEAVEFFI_API weaveffi_events_Delivery weaveffi_events_route_once(void* subscriber_ctx, const weaveffi_events_Subscriber_vtable* subscriber_vtable, const char* topic, weaveffi_error* out_err); -``` - -Note the `on_attached` slot: an object parameter of a callback method is a -mutable `{tag}*`, not the borrowed `const {tag}*` of an ordinary parameter, -because the producer transfers one strong reference that the consumer adopts. -Strings, bytes, and buffers arriving in a callback method are borrowed for -the duration of the call; the consumer copies or decodes them before -returning. - -The consumer owns `ctx` (generated wrappers use a handle-table key that -keeps the implementing object alive) and one process-wide static vtable per -callback interface. The producer may call any entry from any thread until it -calls `free(ctx)` exactly once, after which it never touches `ctx` again. A -consumer implementation that raises calls -`weaveffi_error_set(out_err, -4, message)` instead of letting the exception -unwind through C; the producer aborts the call it was making and the -original caller observes `FOREIGN_ERROR_CODE` with the consumer's message. -The full contract, including the Rust-side unwinding behavior producers -must tolerate, is in the [C ABI Contract](abi.md#callback-interfaces). - -On the Rust side a callback interface is a `#[weaveffi::callback_interface]` -trait with `Send + Sync` supertraits and `&self` methods; producers accept -one as `Arc`. - ---- - -## Optional types - -Append `?` to any type to make it optional (nullable). - -| Syntax | Meaning | -|------------|---------------------------------------------------------------------------| -| `string?` | Optional string | -| `i32?` | Optional i32 | -| `Contact?` | Optional struct | -| `Color?` | Optional enum | -| `Store?` | Optional interface (a nullable pointer at the top level, an optional token inside a buffer) | - -### Optional example - -```yaml -version: "0.9.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?" } -``` - -> **YAML note:** Quote optional types like `"string?"` and `"Contact?"` to -> prevent the YAML parser from treating `?` as special syntax. - -Every optional except `Interface?` crosses the ABI as a value buffer (a flag -byte, then the value when present). `Interface?` stays a nullable object -pointer so the common "maybe an object" shape needs no encoding step. - ---- - -## List types - -Wrap a type in `[T]` brackets to declare a list (variable-length sequence). - -| Syntax | Meaning | -|-------------|-------------------------------------------------| -| `[i32]` | List of i32 | -| `[string]` | List of strings | -| `[Contact]` | List of structs | -| `[Color]` | List of enums | -| `[Store]` | List of objects (one strong reference per element) | - -### List example - -```yaml -version: "0.9.0" -modules: - - name: lists - structs: - - name: Contact - fields: - - { name: id, type: i64 } - interfaces: - - name: Store - methods: - - { name: count, return: i64 } - functions: - - name: sum - params: - - { name: values, type: "[i32]" } - return: i32 - - - name: list_contacts - return: "[Contact]" +} kitchen_sink_kitchen_ReadyListener_vtable; - - name: batch_delete - params: - - { name: ids, type: "[i64]" } - return: i32 - - - name: open_all - params: - - { name: paths, type: "[string]" } - return: "[Store]" +int32_t kitchen_sink_kitchen_subscribe(void* listener_ctx, const kitchen_sink_kitchen_ReadyListener_vtable* listener_vtable, kitchen_sink_error* out_err); ``` -> **YAML note:** Quote list types like `"[i32]"` and `"[Contact]"` because -> YAML interprets bare `[...]` as an inline array. - ---- - -## Map types +## Error domains -Wrap a key-value pair in `{K:V}` braces to declare a map (dictionary, -associative array). Keys must be scalar, `bool`, `string`, or C-style enum -types; values may be any valid `TypeRef` other than an iterator or a -callback interface. +A module may declare one error domain: named, stable codes its throwing +callables report. Each binding generates a typed error (an exception +hierarchy, a Swift `Error` enum, a Go error type) so consumers match on the +names you declared. -| Syntax | Meaning | -|--------------------|----------------------------------| -| `{string:i32}` | Map from string to i32 | -| `{string:Contact}` | Map from string to struct | -| `{i32:string}` | Map from i32 to string | -| `{string:[i32]}` | Map from string to list of i32 | -| `{string:Store}` | Map from string to object | +| Field | Type | Meaning | +|-------|------|---------| +| `name` | string (required) | Domain type name (`KvError`) | +| `codes` | array of Code (required) | The codes | -### Map example +Each code has a `name` (PascalCase by convention), a positive `code`, a +default `message`, an optional `doc`, and optional `fields`: a structured +payload with the same shape as struct fields, delivered as properties of the +raised error. ```yaml -version: "0.9.0" +version: "0.10.0" modules: - - name: maps - structs: - - name: Contact - fields: - - { name: id, type: i64 } - - { name: name, type: string } - - { name: email, type: "string?" } + - name: kv + errors: + name: KvError + codes: + - name: KeyNotFound + code: 1001 + message: key not found + fields: + - { name: key, type: string } + - { name: StoreFull, code: 1003, message: store has reached capacity } functions: - - name: update_scores - params: - - { name: scores, type: "{string:i32}" } - return: bool - doc: "Update player scores by name" - - - name: get_contacts - return: "{string:Contact}" - doc: "Returns a map of name to Contact" - - - name: merge_tags + - name: lookup params: - - { name: current, type: "{string:string}" } - - { name: additions, type: "{string:string}" } - return: "{string:string}" -``` - -> **YAML note:** Quote map types like `"{string:i32}"` because YAML -> interprets bare `{...}` as an inline mapping. - -### C ABI convention - -Maps cross the FFI boundary **by value** as a serialized value buffer: a -`u32` entry count followed by alternating keys and values, all in one -`(const uint8_t*, size_t)` pair. A map parameter `{K:V}` named `m` expands -to two C parameters: - -```c -const uint8_t* m_ptr, size_t m_len -``` - -A map return comes back as a `const uint8_t*` return value plus a -`size_t* out_len` out-parameter, freed by the consumer with -`weaveffi_free_bytes`. For example, `update_scores(scores: {string:i32})` -generates: - -```c -WEAVEFFI_API bool weaveffi_maps_update_scores(const uint8_t* scores_ptr, size_t scores_len, weaveffi_error* out_err); + - { name: key, type: string } + return: bytes + throws: true + - name: count + return: i64 ``` -See the [Value Buffer Protocol](value-buffers.md) for the encoding and -ownership rules. - -### Key type restrictions - -Only the integers, `f32`, `f64`, `bool`, `string`, and C-style enum types -are valid map keys, so every target language can use the key in its native -dictionary idiom. The validator rejects structs, rich enums, interfaces, -lists, maps, optionals, and `bytes` as key types with `InvalidMapKey`: +Declaring a domain reserves the codes; a function, constructor, method, or +static opts in with `throws: true`. A callable without `throws` (`count` +above) has a plain signature and can't report a domain error; a failure there +is a producer bug and traps (see +[Errors and Memory](../guides/errors-and-memory.md#domain-errors-and-the-trap-channel)). +A domain is in scope for its module and every module nested in it, and +`throws: true` with no domain in scope is rejected: ```yaml -# rejected: InvalidMapKey -version: "0.9.0" +# rejected: ThrowsWithoutErrorDomain +version: "0.10.0" modules: - - name: maps + - name: contacts functions: - - name: by_blob + - name: get_contact params: - - { name: index, type: "{bytes:i32}" } -``` - ---- - -## Nested types - -Optional, list, and map modifiers compose freely and nest to any depth, -because every composite serializes into one value buffer: - -| Syntax | Meaning | -|------------------|----------------------------------------------------| -| `[Contact?]` | List of optional contacts (items may be null) | -| `[i32]?` | Optional list of i32 (the entire list may be null) | -| `[string?]` | List of optional strings | -| `{string:[i32]}` | Map from string to list of i32 | -| `{string:i32}?` | Optional map (the entire map may be null) | -| `{string:[Store]}` | Map from string to list of objects | - -### Nested type example - -```yaml -version: "0.9.0" -modules: - - name: nested - structs: - - name: Contact - fields: - { name: id, type: i64 } - functions: - - name: search - params: - - { name: query, type: string } - return: "[Contact?]" - doc: "Returns a list where some entries may be null (redacted)" - - - name: get_scores - params: - - { name: user_id, type: i64 } - return: "[i32]?" - doc: "Returns null if user has no scores, otherwise a list" - - - name: bulk_update - params: - - { name: emails, type: "[string?]" } - return: i32 + return: string + throws: true ``` -The parser evaluates type syntax outside-in: `[Contact?]` is parsed as -`List(Optional(Contact))`, while `[Contact]?` is parsed as -`Optional(List(Contact))`. Nesting deeper than three levels passes -validation but emits the `DeepNesting` warning under `--warn`. - ---- - -## Iterator types +Codes must be positive (`0` is success and negative codes belong to the +runtime) and unique within their domain. Code names must be unique across +every domain in the API, because several targets flatten them into one +namespace; qualify one (`OrderNotFound`) when two domains need the same idea. -Wrap a type in `iter` to declare a lazy iterator over values of type `T`. -Unlike `[T]` (which materializes the full list), iterators yield elements -one at a time and are suitable for large or streaming result sets. +## Async, cancellable, and deprecated -| Syntax | Meaning | -|-----------------|-------------------------------------------| -| `iter` | Iterator over i32 values | -| `iter` | Iterator over strings | -| `iter` | Iterator over structs | -| `iter` | Iterator over objects (one reference each) | - -### Iterator example - -```yaml -version: "0.9.0" -modules: - - name: streaming - structs: - - name: Contact - fields: - - { name: id, type: i64 } - functions: - - name: scan_entries - params: - - { name: prefix, type: string } - return: "iter" - doc: "Lazily iterates over matching contacts" -``` - -Laziness is part of the binding contract, not just the C ABI shape. Every -generated wrapper surfaces `iter` as its language's native lazy iteration -idiom: an `iter.Seq` in Go (`iter.Seq2` when the function throws), a -`Sequence` in Swift, an input-iterator range in C++, an iterator in Python, -an `Enumerator` in Ruby, an `IEnumerable` in C#, an `Iterable` in -Dart, an iterable in JavaScript, and an `Iterator` with `close()` in -Kotlin. The wrapper issues one producer `next` call per consumer step; no -target drains the sequence into a hidden list. The underlying iterator -handle is destroyed exactly once, eagerly on exhaustion or from the -wrapper's disposal idiom when iteration is abandoned early, and errors from -the launch and from each `next` follow the function's error strategy (see -the [Error Handling guide](../guides/errors.md#throws-versus-trap)). The -full pull contract is stated in `weaveffi_core::plan::IteratorProtocol`; -element ownership is covered in the -[Memory and Error Model](memory-error.md#iterators). - -Iterators are only valid as the **outermost** return type of a synchronous -callable. The validator rejects an iterator in a parameter, a struct or -variant field, an error payload field, or a callback-method parameter, and a -return type that nests one (`[iter]`, `iter?`, `iter>`), as -`IteratorInInvalidPosition`, and an iterator returned from an `async` -function as `AsyncIteratorReturn`: +`async: true` makes a function, method, or static complete through a +callback, surfaced as each language's async idiom; `cancellable: true` adds a +cancel token. Both are covered in [Async and Cancellation](../guides/async.md). +Constructors can't be async, and `cancellable` requires `async`: ```yaml -# rejected: IteratorInInvalidPosition -version: "0.9.0" +# rejected: CancellableNotAsync +version: "0.10.0" modules: - - name: streaming + - name: net functions: - - name: consume - params: - - { name: items, type: "iter" } + - { name: ping, return: bool, cancellable: true } ``` ---- - -## Nested modules - -Modules can contain submodules, enabling hierarchical organization of large -APIs. Nested modules share the same validation rules as top-level modules, -and an error domain declared on a parent module is in scope for every -module nested inside it. - -### Nested module example - -```yaml -version: "0.9.0" -modules: - - name: app - functions: - - name: init - - modules: - - name: auth - interfaces: - - name: Session - constructors: - - name: login - params: - - { name: username, type: string } - - { name: password, type: string } - methods: - - name: user_id - return: i64 - - - name: data - structs: - - name: Record - fields: - - { name: id, type: i64 } - - { name: value, type: string } +`deprecated:` on a function, method, static, struct, enum, interface, or +callback interface propagates its message to every binding's native +deprecation marker. - functions: - - name: get_record - params: - - { name: id, type: i64 } - return: Record -``` +## Modules and type references -C ABI symbols for nested modules use underscores to join the path: -`weaveffi_app_auth_Session_login`, `weaveffi_app_data_get_record`. +Modules nest to any depth. A nested module shares the validation rules of a +top-level one, and an error domain declared on a parent serves the subtree. +C symbols join the path with underscores: a function `get_record` in module +`app.data` is `{prefix}_app_data_get_record`. -### Cross-module type references +Every resolved type is identified by its **absolute path**: the declaring +module's path plus the name (`kv.Store`, `app.data.Record`). A reference +spells it one of two ways: -Struct, enum, interface, callback interface, and error domain names are -unique across the whole API (the validator rejects two types sharing a bare -name, wherever they are declared, as `DuplicateTypeName`). A bare type name -therefore resolves unambiguously from any module: reference the type by name -and the toolchain qualifies the reference to its owning module internally. -A dot-qualified spelling (`kv.Store`) is also accepted. +- **Bare** (`Store`): resolves through the API-wide type namespace. Struct, + enum, interface, callback interface, and error domain names are unique + across the whole API (`DuplicateTypeName`), so a bare name is never + ambiguous. +- **Qualified** (`kv.Store`): the qualifier must match the declaring module's + path exactly; `stats.Store` for a `Store` declared in `kv` is an + `UnknownTypeRef`. -For example, a nested `stats` module can take the parent `kv` module's -`Store` interface as a parameter: +A reference to a name nothing declares is always an `UnknownTypeRef`. ```yaml -version: "0.9.0" +version: "0.10.0" modules: - name: kv - errors: - name: KvError - codes: - - { name: IoError, code: 1004, message: "I/O failure" } interfaces: - name: Store constructors: - name: open params: - { name: path, type: string } - throws: true - methods: - - name: count - return: i64 modules: - name: stats structs: @@ -1213,585 +548,161 @@ modules: - { name: total_entries, type: i64 } functions: - name: get_stats - doc: Snapshot the current store statistics params: - - { name: store, type: Store } + - { name: store, type: kv.Store } return: Stats - throws: true -``` - -The generated bindings spell a cross-module reference with the owning -module's qualification where the target needs one; the C symbol is -`weaveffi_kv_stats_get_stats(const weaveffi_kv_Store* store, ...)`. - ---- - -## Async and lifecycle annotations - -### Async functions - -Functions, methods, and statics can be marked as asynchronous. See the -[Async Functions guide](../guides/async.md) for detailed per-target -behavior. - -```yaml -version: "0.9.0" -modules: - - name: net - errors: - name: NetError - codes: - - { name: Unreachable, code: 1, message: "host unreachable" } - functions: - - name: fetch_data - params: - - { name: url, type: string } - return: string - async: true - throws: true - - - name: upload_file - params: - - { name: path, type: string } - return: bool - async: true - cancellable: true -``` - -An async function lowers to a launcher `{sym}_async` that returns -immediately and a completion callback typedef `{sym}_callback` that fires -exactly once, from an arbitrary producer thread. A `cancellable` function -adds a `weaveffi_cancel_token*` slot to the launcher before the callback: - -```c -typedef void (*weaveffi_net_upload_file_callback)(void* context, weaveffi_error* err, bool result); -WEAVEFFI_API void weaveffi_net_upload_file_async(const char* path, weaveffi_cancel_token* cancel_token, weaveffi_net_upload_file_callback callback, void* context); ``` -`async` composes with `throws`: an async function that also declares -`throws: true` delivers its failure as the module's typed domain error -through the target's async idiom (an `async throws` function in Swift, a -rejected promise carrying the typed error in JavaScript, and so on). An -async function without `throws` gets a plain async shape, and a failure can -only be a producer bug. An async function may return an interface object -(the callback adopts one strong reference) or any buffered type; it cannot -return an iterator. - -Async void functions (no return type) emit the `AsyncVoidFunction` -validator warning since they are unusual. Constructors can't be `async` -(`AsyncConstructor`); expose an async static factory returning the interface -instead. - -The producer runs each future on its configured spawner -(`weaveffi::set_spawner`); the default drives every future on a dedicated -thread. See [Async Functions](../guides/async.md). - -### Deprecated declarations - -Mark a function, method, static, struct, enum, interface, or callback -interface as deprecated with a migration message: - -```yaml -version: "0.9.0" -modules: - - name: legacy - functions: - - name: add_old - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 - deprecated: "Use add_v2 instead" -``` - -Generators propagate the deprecation message to the target language (a -`WEAVEFFI_DEPRECATED("...")` attribute in C, `@available(*, deprecated)` in -Swift, `@Deprecated` in Kotlin, `warn` in Ruby, and so on). A deprecated -free function also emits the `DeprecatedFunction` warning under `--warn`, -which is informational. - ---- - -## Generator options - -Per-generator options (Swift module name, Kotlin 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: +## Documentation comments -```toml -[generators.swift] -module_name = "MyMathLib" +`doc:` is accepted on modules, functions, parameters, structs, fields, enums, +variants, interfaces and their members, callback interfaces and their +methods, and error codes. Every generator emits it in the target's native +doc-comment syntax (`///`, `/** */`, docstrings, XML docs); multi-line YAML +block strings (`|`) are preserved. An absent `doc:` emits nothing. -[generators.kotlin] -package = "com.example.math" +## Validation -[generators.go] -module_path = "github.com/myorg/mathlib" -``` +`weaveffi validate` (and every generating command) runs every rule and +reports every violation in one pass, each with a message, a suggestion, and a +source span when one can be located. `--format json` emits the same +diagnostics with a `code` field naming the variant; `--warn` adds advisory +warnings. + +The rules in brief: + +- Names are identifiers (a letter or `_`, then ASCII letters, digits, and + `_`) and not one of `if`, `else`, `for`, `while`, `loop`, `match`, `type`, + `return`, `async`, `await`, `break`, `continue`, `fn`, `struct`, `enum`, + `mod`, `use`. Generators escape other languages' keywords themselves. +- Names are unique in their scope; type names and error code names are + unique across the whole API. +- Structs, enums, interfaces, and callback interfaces aren't empty. +- Types resolve, and appear only in the positions in the table above. +- `throws` needs a domain in scope; `cancellable` needs `async`. +- Codes are positive and unique; a domain's name doesn't collide with a + function in the same module. +- **No two declarations lower to the same C symbol.** Every symbol is the + prefix plus the underscore-joined module path plus the declaration name, + so `m.x_y`, `m.x.y`, and `m_x.y` collide; so do a free function + `Store_get` and a `get` method on interface `Store`, a function named + `Store_clone` and `Store`'s implicit `_clone`, and a function named + `foo_callback` and the completion type of an async `foo`. The full symbol + table is in the [C ABI contract](abi.md#symbol-names). -See the [Project Configuration guide](../guides/config.md) for the full list -of options. +A document with an unsupported `version` reports only +`UnsupportedSchemaVersion`. ---- +### Error catalog -## Type positions +| Code | Reported when | +|------|---------------| +| `UnsupportedSchemaVersion` | `version` isn't `0.10.0` | +| `NoModuleName` | a module has no name | +| `InvalidModuleName` | a module name isn't an identifier or is reserved | +| `DuplicateModuleName` | two sibling modules share a name | +| `InvalidIdentifier` | a name isn't a valid identifier | +| `ReservedKeyword` | a name is a reserved word | +| `DuplicateFunctionName` | two functions in a module share a name | +| `DuplicateParamName` | two parameters of a callable share a name | +| `NameCollisionWithErrorDomain` | a function and the module's error domain share a name | +| `DuplicateStructName` | two structs in a module share a name | +| `DuplicateStructField` | two fields of a struct share a name | +| `EmptyStruct` | a struct has no fields | +| `DuplicateEnumName` | two enums in a module share a name | +| `EmptyEnum` | an enum has no variants | +| `DuplicateEnumVariant` | two variants of an enum share a name | +| `DuplicateEnumValue` | two variants of an enum share a value | +| `DuplicateEnumVariantField` | two fields of a rich-enum variant share a name | +| `DuplicateInterfaceName` | two interfaces in a module share a name | +| `DuplicateInterfaceMember` | two members of an interface share a name | +| `EmptyInterface` | an interface has no members | +| `ConstructorHasReturn` | a constructor declares `return` | +| `AsyncConstructor` | a constructor is `async` | +| `InterfaceInInvalidPosition` | an interface is used as a map key | +| `DuplicateCallbackInterfaceName` | two callback interfaces in a module share a name | +| `EmptyCallbackInterface` | a callback interface has no methods | +| `DuplicateCallbackMethod` | two methods of a callback interface share a name | +| `InvalidCallbackMethod` | a callback method is async, cancellable, throwing, or returns a non-direct type | +| `CallbackInterfaceInInvalidPosition` | a callback interface appears other than as a top-level parameter | +| `DuplicateTypeName` | two types anywhere in the API share a bare name | +| `UnknownTypeRef` | a reference names no declaration, or its qualifier doesn't match the declaring module | +| `InvalidMapKey` | a map key isn't an integer, `bool`, `string`, or C-style enum | +| `IteratorInInvalidPosition` | `iter` appears other than as an outermost return | +| `AsyncIteratorReturn` | an async function returns `iter` | +| `CancellableNotAsync` | a synchronous callable is `cancellable` | +| `ThrowsWithoutErrorDomain` | `throws: true` with no error domain in scope | +| `ErrorDomainMissingName` | an error domain has no name | +| `DuplicateErrorName` | two codes in a domain share a name | +| `DuplicateErrorCode` | two codes in a domain share a value | +| `DuplicateErrorCodeName` | two domains declare a code with the same name | +| `InvalidErrorCode` | a code is zero or negative | +| `SymbolCollision` | two declarations lower to the same C identifier | -The validator admits each type in the positions below. "Buffered field" -covers struct fields, rich-enum variant fields, and error payload fields; -"callback param" and "callback return" are the parameters and return of a -callback-interface method. +### Warnings -| Type | Param | Return | Buffered field | Element of `T?`, `[T]`, `{K:V}` value | Map key | Callback param | Callback return | Notes | -|-------------------------|-------|--------|----------------|----------------------------------------|---------|----------------|-----------------|-------| -| integers, `f32`, `f64`, `bool` | yes | yes | yes | yes | yes | yes | yes | the *direct* family | -| `string` | yes | yes | yes | yes | yes | yes | no | | -| `bytes` | yes | yes | yes | yes | no | yes | no | | -| C-style enum | yes | yes | yes | yes | yes | yes | yes | direct family | -| rich enum | yes | yes | yes | yes | no | yes | no | buffered | -| struct | yes | yes | yes | yes | no | yes | no | buffered | -| interface | yes | yes | yes | yes | no | yes | no | reference counted everywhere | -| callback interface | yes | no | no | no | no | no | no | bare top-level parameter only | -| `T?`, `[T]`, `{K:V}` | yes | yes | yes | yes | no | yes | no | buffered; obey the inner type's rules | -| `iter` | no | yes | no | no | no | no | no | outermost return of a sync callable only | +Warnings never fail validation. ---- +| Code | Fires when | +|------|------------| +| `LargeEnumVariantCount` | an enum has more than 100 variants | +| `DeepNesting` | a type nests composites more than three levels deep | +| `EmptyModuleDoc` | a module with functions has no `doc:` on itself or any function | +| `AsyncVoidFunction` | an async free function returns nothing | +| `DeprecatedFunction` | a free function is deprecated (informational) | ## Complete example -A full IDL combining structs, enums, optionals, lists, an interface, a -callback interface, and a typed error domain (a trimmed version of the -`contacts` sample): +A trimmed version of the `contacts` sample, combining an enum, a record, an +error domain, an interface, and a nested module that uses its parent's types: ```yaml -version: "0.9.0" +version: "0.10.0" modules: - name: contacts - doc: "An in-memory address book" + doc: An in-memory address book. + errors: + name: ContactsError + codes: + - { name: InvalidName, code: 1, message: name must not be empty } + - { name: NotFound, code: 2, message: contact not found } 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: 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" } - - callback_interfaces: - - name: ChangeListener - doc: "Notified whenever a contact is added or removed" - methods: - - name: on_change - params: - - { name: contact, type: Contact } - - { name: removed, type: bool } - interfaces: - name: ContactBook - doc: "An address book owning its contacts" constructors: - name: new 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: 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 return: "[Contact]" - - - name: remove - params: - - { name: id, type: i64 } - return: bool - - - name: watch - doc: "Attach a listener; the book retains it until it is dropped" + modules: + - name: groups + functions: + - name: count_of_type params: - - { name: listener, type: ChangeListener } - - - name: count + - { name: book, type: ContactBook } + - { name: contact_type, type: ContactType } return: i32 ``` - -## Validation - -`weaveffi validate ` (and every generating command) runs the complete -rule set below and reports **every** violation it finds in one pass, each -with a message, a suggestion, and, when the offending name can be located, -an underlined span in the source. `weaveffi validate --format json` emits -the same diagnostics as machine-readable objects whose `code` field is the -variant name listed in the catalog; `--warn` adds the advisory warnings. - -### Rules - -Names and uniqueness: - -- Module, function, parameter, struct, field, enum, variant, variant field, - interface, callback interface, callback method, error domain, and error - code names must be valid - identifiers (start with a letter or `_`, contain only ASCII alphanumerics - and `_`) and must not be one of the reserved words `if`, `else`, `for`, - `while`, `loop`, `match`, `type`, `return`, `async`, `await`, `break`, - `continue`, `fn`, `struct`, `enum`, `mod`, `use`. -- Names must be unique within their scope: module names among siblings, - function names within a module, parameter names within a callable, field - names within a struct or variant, variant names and values within an enum, - and struct, enum, interface, and callback interface names within a - module. -- Struct, enum, interface, callback interface, and error domain names must - be unique across the **whole API**, not just their module: generators emit - flat per-language type names, and bare-name references resolve API-wide. -- Free functions and interface members share the module's C symbol - namespace. A free function claims `{name}`; an interface claims - `{Interface}_{member}` for every member plus `{Interface}_clone` and - `{Interface}_destroy`. Two declarations lowering to the same symbol are - rejected. - -Shapes: - -- Structs must have at least one field. Enums must have at least one - variant. Interfaces must have at least one constructor, method, or static. - Callback interfaces must have at least one method. -- Interface members share one namespace per interface. A constructor may - not declare a `return` type and may not be `async`. -- Callback-interface methods are synchronous (no `async`, no - `cancellable`), never `throws`, and return nothing or a direct value (a - scalar, `bool`, or C-style enum). - -Types and positions: - -- Every user type reference must resolve to a struct, enum, interface, or - callback interface declared somewhere in the API. -- Map keys must be scalars, `bool`, `string`, or a C-style enum. -- An interface may appear anywhere except as a map key. -- A callback interface may appear only as a bare top-level parameter of a - function, constructor, static, or method. -- An iterator may appear only as the outermost return type of a - synchronous callable. - -Errors: - -- `throws: true` requires an error domain in scope: on the same module or - an ancestor module. -- Error codes must be positive (`0` means success and negative codes are - reserved for the runtime) and unique within their domain. Code names must - be unique within their domain and across every domain in the API. -- The error domain's name must not collide with a function name in the same - module. - -### Error catalog - -Every `ValidationError` the validator can report. The **Code** column is -the `code` field of `weaveffi validate --format json` and the identifier to -match on in CI scripts; the other two columns are the message and the -suggestion the CLI prints. Braces mark the names the message interpolates. - -| Code | Message | Suggestion | -|------|---------|------------| -| `UnsupportedSchemaVersion` | unsupported schema version '{version}'; supported versions: {supported} | set the version field to the current schema version and update the document to match the current schema | -| `NoModuleName` | module has no name | every module must have a non-empty 'name' field | -| `DuplicateModuleName` | duplicate module name: {name} | module names must be unique within an API definition; rename or merge the duplicate | -| `InvalidModuleName` | invalid module name '{name}': {reason} | choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word | -| `InvalidIdentifier` | invalid identifier '{name}': {reason} | identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters | -| `ReservedKeyword` | reserved keyword used: {name} | choose a different name that is not a language reserved word | -| `DuplicateFunctionName` | duplicate function name in module '{module}': {function} | function names must be unique within a module; rename the duplicate | -| `DuplicateParamName` | duplicate param name in function '{function}' of module '{module}': {param} | parameter names must be unique within a function; rename the duplicate | -| `DuplicateStructName` | duplicate struct name in module '{module}': {name} | struct names must be unique within a module; rename the duplicate | -| `DuplicateStructField` | duplicate field name in struct '{struct_name}': {field} | field names must be unique within a struct; rename the duplicate | -| `EmptyStruct` | empty struct in module '{module}': {name} | structs must have at least one field; add a field or remove the struct | -| `DuplicateEnumName` | duplicate enum name in module '{module}': {name} | enum names must be unique within a module; rename the duplicate | -| `EmptyEnum` | empty enum in module '{module}': {name} | enums must have at least one variant; add a variant or remove the enum | -| `DuplicateEnumVariant` | duplicate enum variant in enum '{enum_name}': {variant} | variant names must be unique within an enum; rename the duplicate | -| `DuplicateEnumValue` | duplicate enum value in enum '{enum_name}': {value} | variant numeric values must be unique within an enum; assign a different value | -| `DuplicateEnumVariantField` | duplicate field '{field}' in variant '{variant}' of enum '{enum_name}' | associated field names must be unique within an enum variant; rename the duplicate | -| `DuplicateInterfaceName` | duplicate interface name in module '{module}': {name} | interface names must be unique within a module; rename the duplicate | -| `DuplicateInterfaceMember` | duplicate member name in interface '{interface}': {name} | constructor, method, and static names share one namespace per interface; rename the duplicate | -| `EmptyInterface` | empty interface in module '{module}': {name} | interfaces must declare at least one constructor, method, or static; add a member or remove the interface | -| `ConstructorHasReturn` | constructor '{constructor}' of interface '{interface}' declares a return type | a constructor implicitly returns a new instance of its interface; remove the `return` field | -| `AsyncConstructor` | constructor '{constructor}' of interface '{interface}' cannot be async | constructors are synchronous; expose an async static factory returning the interface instead | -| `InterfaceInInvalidPosition` | interface type '{name}' is not valid in {location} | interface objects may appear anywhere except as map keys; key a map by a scalar, string, or C-style enum instead | -| `DuplicateCallbackInterfaceName` | duplicate callback interface name in module '{module}': {name} | callback interface names must be unique within a module; rename the duplicate | -| `EmptyCallbackInterface` | empty callback interface in module '{module}': {name} | callback interfaces must declare at least one method; add a method or remove the callback interface | -| `DuplicateCallbackMethod` | duplicate method name in callback interface '{interface}': {name} | method names must be unique within a callback interface; rename the duplicate | -| `InvalidCallbackMethod` | method '{method}' of callback interface '{interface}' {reason} | callback-interface methods are synchronous, never throw, and return either nothing or a direct value (a scalar, bool, or C-style enum): the consumer implements them, so no producer-owned allocation or error payload can flow back | -| `CallbackInterfaceInInvalidPosition` | callback interface '{name}' is not valid in {location} | a callback interface is a consumer-implemented vtable: it may be passed as a parameter of a function, constructor, static, or method, but it can't be returned, nested inside another type, or passed to another callback-interface method | -| `DuplicateTypeName` | duplicate type name '{name}' (declared in '{first}' and '{second}') | struct, enum, interface, callback interface, and error domain names must be unique across the whole API; rename one of the declarations | -| `UnknownTypeRef` | unknown type reference: {name} | define a struct, enum, interface, or callback interface with this name, or check for typos | -| `InvalidMapKey` | invalid map key type: {key_type}; only scalars, strings, and C-style enums are allowed as map keys | map keys must be integers (i8 through u64), f32, f64, bool, string, or a C-style enum; structs, rich enums, interfaces, optionals, lists, and maps cannot be keys | -| `IteratorInInvalidPosition` | iterator type is only valid as a function return type, found in {location} | iterator types can only be used as function return types, not as parameters or struct fields | -| `AsyncIteratorReturn` | async function '{module}::{function}' cannot return an iterator | the callback-completed async ABI has no streaming protocol; return a list (`[T]`) from the async function, or make the function synchronous and return `iter` | -| `AbiSymbolCollision` | C symbol collision in module '{module}': two declarations lower to '..._{symbol}' | free functions and interface members share the module's C symbol namespace (interface members are prefixed with the interface name); rename one of the colliding declarations | -| `ThrowsWithoutErrorDomain` | function '{module}::{function}' declares throws but no error domain is in scope | a throwing function reports codes from its module's error domain; declare an `errors:` block on this module (or an ancestor module), or remove `throws: true` | -| `ErrorDomainMissingName` | error domain missing name in module '{module}' | add a non-empty 'name' field to the error domain | -| `DuplicateErrorName` | duplicate error code name in module '{module}': {name} | error code names must be unique within a module; rename the duplicate | -| `DuplicateErrorCode` | duplicate error numeric code in module '{module}': {code} | numeric error codes must be unique within a module; assign a different value | -| `DuplicateErrorCodeName` | duplicate error code name '{name}' (declared in '{first}' and '{second}') | error code names must be unique across the whole API; qualify one of them (e.g. 'OrderNotFound') | -| `InvalidErrorCode` | invalid error code in module '{module}' for '{name}': must be a positive integer | 0 means success and negative codes are reserved for the runtime (-1 generic, -2 panic, -3 marshalling failure, -4 foreign callback error); use a positive integer | -| `NameCollisionWithErrorDomain` | function name collides with error domain name in module '{module}': {name} | function and error domain names share a namespace; rename one to avoid the collision | - -A document with an unsupported `version` reports only -`UnsupportedSchemaVersion`; the remaining rules assume the current schema's -shape and are not run. Removed type spellings (`handle`, `&str`) and -removed keys (`callbacks:`, `since:`, `mutable:`) are rejected earlier, by -the parser, with a message naming the replacement. - -### Warnings - -Warnings never fail validation. `weaveffi validate --warn` and -`weaveffi generate --warn` print them; the JSON output lists them under -`warnings` with the same `code` names. - -| Code | Fires when | -|------|------------| -| `LargeEnumVariantCount` | an enum declares more than 100 variants | -| `DeepNesting` | a struct field, parameter, or return type nests composites deeper than 3 levels (`[[[[i32]]]]`) | -| `EmptyModuleDoc` | a module declares functions but has no `doc:` on itself or on any of them | -| `AsyncVoidFunction` | a free function is `async: true` with no return type | -| `DeprecatedFunction` | a free function carries `deprecated:` (informational) | - -## ABI mapping - -A one-screen summary; the [C ABI Contract](abi.md) is normative. - -- Scalars, `bool`, and C-style enums pass by value in one slot; C-style - enums cross as `int32_t`. -- `string` is one `const char*` slot; `bytes` is a - `const uint8_t* {name}_ptr, size_t {name}_len` pair. Both are borrowed for - the call as parameters. -- Structs, rich enums, optionals (except `Interface?`), lists, and maps are - *buffered*: they cross by value as one serialized - `(const uint8_t* {name}_ptr, size_t {name}_len)` pair, borrowed for the - duration of the call (see the [Value Buffer Protocol](value-buffers.md)). -- Interfaces cross as `const {tag}*` parameters (borrowed) and `{tag}*` - returns (one strong reference); `Interface?` is the same slot with null - meaning none. Every interface has `{tag}_clone` and `{tag}_destroy`. -- Callback interfaces lower to `void* {name}_ctx, const {tag}_vtable* {name}_vtable`. -- Return values are direct scalars except: `string` returns `const char*` - (freed with `weaveffi_free_string`); `bytes` and every buffered type - return `const uint8_t*` plus a `size_t* out_len` out-parameter (freed with - `weaveffi_free_bytes`); `iter` returns an iterator handle with `_next` - and `_destroy` symbols. -- Every synchronous symbol takes a trailing `weaveffi_error* out_err` - after all inputs and out-parameters. `_clone` and `_destroy` take none. - -## Error domain - -A module can declare one error domain: a named set of symbolic error codes -its fallible callables report. The domain generates a *typed* error -construct in every target language (an error enum in Swift, an exception -class hierarchy in Python, sealed exception subclasses in Kotlin, and so -on), so consumers catch and match on the codes you declared rather than on -raw integers. - -### Error domain schema - -| Field | Type | Required | Description | -|---------|---------------|----------|-----------------------------------------------------------------------------| -| `name` | string | yes | Domain name, used to name the generated error type (e.g. `KvError`) | -| `codes` | array of Code | yes | The named codes belonging to this domain | - -Each code: - -| Field | Type | Required | Description | -|-----------|----------------|----------|----------------------------------------------------------------------------------------------------| -| `name` | string | yes | Code name, lowered to a case or subclass on the generated error type (e.g. `KeyNotFound`) | -| `code` | i32 | yes | Stable numeric value carried across the C ABI; must be positive | -| `message` | string | yes | Default human-readable message | -| `doc` | string | no | Documentation string | -| `fields` | array of Field | no | Structured payload fields this error carries (see [Structured error payloads](#structured-error-payloads)) | - -PascalCase code names (`KeyNotFound`, `StoreFull`) are the convention: each -generator re-cases them into its own idiom (see -[Naming and Package Conventions](naming.md#error-domains-and-codes)). - -### Structured error payloads - -A code may declare `fields:` using the same shape as struct fields (`name`, -`type`, `doc`). When a matching error is raised, those fields are serialized -into the `weaveffi_error` struct's `payload_ptr`/`payload_len` slots in the -value-buffer format, and each generator decodes them into properties of the -raised exception or returned error value, keyed by the field names: - -```yaml -version: "0.9.0" -modules: - - name: kv - errors: - name: KvError - codes: - - name: KeyNotFound - code: 1001 - message: "key not found" - fields: - - { name: key, type: string } - - { name: attempts, type: i32 } - functions: - - name: lookup - params: - - { name: key, type: string } - return: bytes - throws: true -``` - -Payload fields obey the buffered positional rules: no iterators and no -callback interfaces (an interface object is allowed and carries a -reference like any other object token). A code without `fields` leaves the -payload slots null. See the -[Value Buffer Protocol](value-buffers.md#structured-errors) for the wire -format and freeing rules. - -### Opting in with `throws` - -Declaring a domain reserves the codes; a function, method, static, or -constructor joins the typed error path by declaring `throws: true`: - -```yaml -version: "0.9.0" -modules: - - name: contacts - errors: - name: ContactsError - codes: - - { name: InvalidName, code: 1, message: "name must not be empty" } - - { name: NotFound, code: 2, message: "contact not found" } - functions: - - name: get_contact - params: - - { name: id, type: i64 } - return: string - throws: true - - - name: count_contacts - return: i32 -``` - -A throwing callable surfaces in each target's error idiom (`throws` in -Swift, `raise` in Python, `(T, error)` in Go, exceptions elsewhere), and the -error it delivers is the domain type: the numeric code maps to the matching -declared case. A callable **without** `throws` (like `count_contacts` -above) has a plain signature at the idiomatic surface; it cannot report a -domain error, and a failure there can only be a producer bug, which -surfaces as a generic branded error or a trap rather than a typed error. -See the [Error Handling guide](../guides/errors.md) for the full model, -including panic behavior. - -The domain is in scope for its module **and every module nested inside -it**, so a domain declared on a parent module serves the whole subtree. -Declaring `throws: true` with no domain in scope is a validation error -(`ThrowsWithoutErrorDomain`): - -```yaml -# rejected: ThrowsWithoutErrorDomain -version: "0.9.0" -modules: - - name: contacts - functions: - - name: get_contact - params: - - { name: id, type: i64 } - return: string - throws: true -``` - -### Validation - -- Codes must be positive: `0` means success, and the negative range is - reserved for the runtime (`-1` generic error, `-2` producer panic, `-3` - marshalling failure, `-4` foreign callback error). -- Numeric codes must be unique within a domain. -- Code names must be unique within a domain **and across every domain in - the API**: backends with flat namespaces derive one error class or - constant per code, so two domains both declaring `NotFound` would - collide. Qualify one of them (e.g. `OrderNotFound`). -- The domain name must not collide with a function name in the same module, - and it shares the API-wide type namespace with struct, enum, interface, - and callback interface names. - -## Documentation comments - -Every IR element accepts an optional `doc:` field. WeaveFFI propagates that -text into the generated bindings using each language's native doc-comment -syntax. Multi-line strings (use YAML's `|` block form) are preserved across -the boundary; single-line strings collapse to a one-liner where the target -syntax allows. - -Supported sites: - -- `Module.doc` -- `Function.doc`, `Param.doc` -- `InterfaceDef.doc`, plus each of its constructors, methods, and statics - (interface members use the function schema, so they carry `Function.doc` - and `Param.doc`) -- `CallbackInterfaceDef.doc`, plus each of its methods and their parameters -- `StructDef.doc`, `StructField.doc` -- `EnumDef.doc`, `EnumVariant.doc` -- `ErrorCode.doc` - -Per-target syntax: - -| Target | Comment syntax | Param docs | -|-------------------|---------------------------------------------------------|-----------------------------------------------------------| -| C / C++ | `/** ... */` directly above the declaration | not emitted | -| Swift | `/// ...` per line | `/// - Parameter name: ...` | -| Kotlin | `/** ... */` KDoc block | `@param name ...` inside the KDoc block | -| TypeScript (Node) | `/** ... */` JSDoc | `@param name ...` | -| TypeScript (Wasm) | `/** ... */` JSDoc | `@param name ...` | -| Python | `"""..."""` first statement; `# ...` above C ABI binds | NumPy-style `Parameters` section in the wrapper docstring | -| .NET (C#) | `/// ...` XML doc | `/// ...` | -| Dart | `/// ...` | not emitted | -| Go | `// SymbolName ...` per Go's convention | trailing `// Parameters:` block | -| Ruby | `# ...` lines above the `def` | `# @param name [Object] ...` | - -Example IDL: - -```yaml -version: "0.9.0" -modules: - - name: docs - doc: Document tracking - structs: - - name: Document - doc: | - Represents a single document tracked by the system. - - Documents are persisted to disk and exposed via the public API. - fields: - - { name: id, type: i64, doc: Stable opaque identifier } - - { name: title, type: string, doc: Human-readable title } - functions: - - name: create_document - doc: Create a brand new document - params: - - { name: title, type: string, doc: Human-readable title } - return: Document -``` - -All eleven generators emit doc comments before every documented -declaration; absent or empty `doc:` fields produce no extra output, so the -feature is fully opt-in. diff --git a/docs/src/reference/memory-error.md b/docs/src/reference/memory-error.md deleted file mode 100644 index b367f40b..00000000 --- a/docs/src/reference/memory-error.md +++ /dev/null @@ -1,320 +0,0 @@ -# Memory and Error Model - -This page is the working summary of who owns what across the WeaveFFI C -ABI (revision 2) and how failures travel back to the caller. It is written -for someone consuming the generated C header directly or auditing a -generated wrapper. The normative text is the [C ABI Contract](abi.md); where -the two disagree, that page wins, and this one points at the relevant -section rather than restating it. - -The model has five kinds of values, each with one ownership rule: - -| Kind | Crosses as | Parameter | Return, async result, iterator element | Release | -|-------------------------------|----------------------------------------------|------------------------------------|---------------------------------------------|-------------------------------------------------| -| direct (scalars, `bool`, C-style enums) | one slot by value | copied | copied | nothing | -| `string` | `const char*` | borrowed for the call | owned by the consumer | `weaveffi_free_string` | -| `bytes` and value buffers | `const uint8_t*` + `size_t` | borrowed for the call | owned by the consumer | `weaveffi_free_bytes(ptr, len)` | -| objects (interfaces) | `{tag}*` | borrowed for the call | one strong reference, adopted | `{tag}_destroy` | -| callback interfaces | `void* ctx` + `const {tag}_vtable*` | owned by the consumer until `free` | never returned | the producer calls `free(ctx)` exactly once | - -"Borrowed for the call" means the producer may read the value only until -the symbol returns and must copy anything it keeps. Everything a producer -hands back is owned by the receiver, who releases it with the runtime -function in the last column exactly once. - -## Errors - -Every synchronous symbol takes a trailing `weaveffi_error* out_err` slot -after all inputs and out-parameters; `_clone` and `_destroy` are the only -exported symbols without one. The caller owns the struct (stack or heap), -zero-initializes it, and checks `code` after the call: - -```c -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -void weaveffi_error_set(weaveffi_error* err, int32_t code, const char* message); -void weaveffi_error_clear(weaveffi_error* err); -void weaveffi_error_free(weaveffi_error* err); -``` - -- `code == 0` is success; `message` and `payload_ptr` are null. -- A non-zero `code` means failure. `message` is a producer-allocated, - NUL-terminated UTF-8 string, and `payload_ptr`/`payload_len` hold the - matched error code's `fields:` serialized in the - [value-buffer format](value-buffers.md#structured-errors), or null and - zero for a code without fields. Both belong to the caller until it calls - `weaveffi_error_clear`, which frees the message and the payload, nulls - both pointers, and resets `code` to `0`. Clearing an already-clear struct - is a no-op. -- Interior NUL bytes are stripped from messages on the producer side so - `message` is always a well-formed C string. - -Typical C usage against the `kvstore` sample: - -```c -weaveffi_error err = {0}; -int64_t n = weaveffi_kv_Store_count(store, &err); -if (err.code != 0) { - fprintf(stderr, "count failed (%d): %s\n", err.code, err.message ? err.message : ""); - weaveffi_error_clear(&err); -} -``` - -### Domain codes and runtime codes - -Positive codes are the module's declared [error domain](idl.md#error-domain); -the C header emits one enum constant per code -(`weaveffi_kv_KvError_KeyNotFound = 1001`). Negative codes belong to the -runtime and are the same in every module: - -| Code | Constant | Meaning | -|------|----------------------|------------------------------------------------------------------------------------------------| -| `-1` | `GENERIC_ERROR_CODE` | an untyped producer error (a Rust `Result` or any error type without a domain code) | -| `-2` | `PANIC_ERROR_CODE` | the producer panicked; `message` carries the panic text | -| `-3` | `MARSHAL_ERROR_CODE` | an argument couldn't be lifted: null where a value was required, non-UTF-8 text, an out-of-range enum, a malformed value buffer | -| `-4` | `FOREIGN_ERROR_CODE` | a consumer callback-interface implementation raised; `message` carries the foreign error text | - -How a wrapper interprets a non-zero code depends on the callable's -`throws` flag, and every generator follows the same rule -(`weaveffi_core::plan::ErrorStrategy`). On a `throws: true` callable a -positive code is a typed domain error surfaced through the language's -normal error channel (a Swift `throw`, a Python exception subclass, a Go -`error`). On a callable without `throws`, and for every negative code on -any callable, the failure is a programming error: the wrapper traps (a -branded `WeaveFFIError`/`WeaveFFIException`, a Go `panic`, a Swift -`fatalError`) and never dresses it up as a domain error. See the -[Error Handling guide](../guides/errors.md#throws-versus-trap). - -### `weaveffi_error_set`: the consumer's write channel - -Only one situation asks a consumer to *write* an error: a callback-interface -method that fails. The vtable entry receives an `out_err` the producer -owns; the consumer must not store a pointer from its own allocator there, -because the producer will free `message` with the Rust allocator. Instead it -calls `weaveffi_error_set(out_err, -4, "what went wrong")` with a borrowed -message, which the producer copies. See -[Callback interfaces](#callback-interfaces) below. - -### `weaveffi_error_free`: heap-boxed async errors - -An async completion callback receives `weaveffi_error* err` rather than -filling a caller-owned slot: the producer heap-allocates the struct so the -consumer can read it after the callback returns. A non-null `err` is owned -by the consumer, who copies `code`, `message`, and the payload, then calls -`weaveffi_error_free(err)` exactly once; it releases the message, the -payload, and the box. Never call `weaveffi_error_clear` alone on an async -error (it leaks the box) and never call `weaveffi_error_free` on a -stack-allocated `out_err`. - -## Strings and bytes - -A `string` parameter is a borrowed `const char*` and a `bytes` parameter a -borrowed `(const uint8_t* {name}_ptr, size_t {name}_len)` pair; the caller -keeps them alive until the symbol returns and frees them however it -allocated them. A returned `string` is a producer-allocated `const char*` -released with `weaveffi_free_string`; returned `bytes` come back as -`const uint8_t*` plus a `size_t* out_len` out-parameter and are released -with `weaveffi_free_bytes(ptr, len)` using that exact length: - -```c -void weaveffi_free_string(const char* ptr); -void weaveffi_free_bytes(uint8_t* ptr, size_t len); -``` - -```c -size_t len = 0; -const uint8_t* value = weaveffi_kv_Store_get(store, "user:1", &len, &err); -if (err.code == 0) { - /* decode or copy value[0..len) before freeing it */ - weaveffi_free_bytes((uint8_t*)value, len); -} -``` - -Both free functions accept null as a no-op. Passing a pointer to -`weaveffi_free_string` that did not come from the producer (or freeing a -returned buffer with the consumer's own allocator) is undefined behavior. - -## Value buffers - -Records, rich enums, optionals (except `Interface?`), lists, and maps cross -the boundary by value as serialized [value buffers](value-buffers.md). -Ownership is exactly the `bytes` rule: a buffered parameter is a borrowed -`(ptr, len)` pair the caller encodes, keeps alive for the call, and frees -itself; a buffered return is a producer-allocated `(ptr, out_len)` pair the -consumer decodes and then releases with `weaveffi_free_bytes`. - -Two extra rules apply when a buffer contains **objects**. Each object token -inside the encoding carries one strong reference, so a consumer that -encodes an object it holds must write a freshly cloned pointer -(`{tag}_clone(obj)`), never the pointer its own wrapper still owns; and a -buffer that carries objects is consumed exactly once, because decoding it -adopts those references. Generated wrappers encode a fresh buffer per call -and never reuse one. If decoding fails partway, the reader releases every -token it already adopted. See -[Objects in value buffers](value-buffers.md#objects). - -## Objects - -An interface `Name` in module path `p` is an opaque `{prefix}_{p}_{Name}` -tag with two lifecycle symbols the IDL never declares: - -```c -weaveffi_kv_Store* weaveffi_kv_Store_clone(const weaveffi_kv_Store* self); -void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self); -``` - -Objects are **reference counted by the producer**; a `{tag}*` is one strong -reference. `_clone` returns a second strong reference to the same object -(the pointer value is unchanged), `_destroy` releases one, and the object -is dropped when the last reference goes. Both accept null as a no-op, and -`_destroy` never unwinds into C even if the object's destructor panics. - -The invariant every generated wrapper maintains is **one strong reference -per wrapper**: a wrapper object (a Swift class instance, a Python object, a -Kotlin `AutoCloseable`, a C++ RAII class, and so on) owns exactly one -reference, which it acquired either by adopting a returned pointer or by -calling `_clone`, and releases exactly once through the language's disposal -idiom, with a finalizer or `Cleaner` as a backstop where the runtime has -one. Two wrappers may point at the same object; each still owes its own -`_destroy`. - -Ownership by position (the normative table is in -[C ABI Contract: Objects](abi.md#objects-interfaces)): - -- **Parameter** (`Store`, `Store?`): borrowed. Pass the pointer you hold; - the producer clones internally if it retains the object. Null is "none" - for `Store?`. -- **Return, async result, iterator element**: one strong reference - transfers to you. Adopt it (wrap it) and eventually `_destroy` it. A - method such as `share` that returns the receiver itself hands back the - same pointer value with the count bumped; both references must be - destroyed. -- **Callback-interface method parameter**: one strong reference transfers - to you (the slot is `{tag}*`, not `const`). Adopt it or `_destroy` it - before returning; dropping it on the floor leaks. -- **Inside a value buffer**: the token carries one reference; the reader - adopts it. Writers clone before encoding. - -Because the count lives in the producer, an in-flight async call and any -producer-side retention hold their own references, so destroying a wrapper -while a call is pending or while the producer still uses the object is -safe. - -```c -weaveffi_kv_Store* a = weaveffi_kv_Store_open("/tmp/db", &err); -weaveffi_kv_Store* b = weaveffi_kv_Store_share(a, &err); /* same object, count 2 */ -weaveffi_kv_Store* c = weaveffi_kv_Store_clone(a); /* count 3, no out_err */ -weaveffi_kv_Store_destroy(c); -weaveffi_kv_Store_destroy(b); -weaveffi_kv_Store_destroy(a); /* dropped here */ -``` - -## Callback interfaces - -A callback interface is the one value the **consumer** owns and the -**producer** borrows. A parameter of callback-interface type lowers to -`void* {name}_ctx, const {tag}_vtable* {name}_vtable`. The consumer supplies -`ctx` (generated wrappers use a handle-table key that keeps the -implementing object alive, never a raw GC pointer) and a process-wide static -vtable with one entry per method plus a trailing `void (*free)(void* ctx)`. - -The producer may call any entry any number of times, from any thread, until -it calls `free(ctx)` exactly once; after that it never touches `ctx` again. -The producer holds the pair behind a reference count of its own, so it may -clone the callback freely (an `Arc` in Rust); `free` fires when -the last clone drops, which may be long after the call that passed it or -never, if the producer keeps it until process exit. - -Inside a method call the usual families apply with these directions: -strings, bytes, and buffers arriving as arguments are borrowed for the -duration of the dispatch (the consumer copies or decodes them before -returning and frees nothing); object arguments transfer one strong -reference the consumer adopts; the return is a direct value written -straight into the C return, so nothing allocated by the consumer ever -crosses back. - -### Foreign errors - -Callback methods can't `throws`, but a consumer implementation can still -fail. The vtable entry's trailing `weaveffi_error* out_err` is the channel: -the trampoline catches the exception, calls -`weaveffi_error_set(out_err, -4, message)` with a borrowed message (the -producer copies it), and returns a default value. It must never let an -exception unwind through the C frame. The producer then aborts the call it -was making, and the original caller observes `FOREIGN_ERROR_CODE` with the -consumer's message in `out_err`, exactly as it would observe a producer -panic. On the Rust side the abort is an unwind through the producer's -frames to the exported thunk, so producer code treats every callback call -as potentially panicking; see -[C ABI Contract: Callback interfaces](abi.md#callback-interfaces). - -## Iterators - -An `iter` return yields an opaque iterator handle `{IterTag}*`. Each -`int32_t {IterTag}_next(iter, , out_err)` writes one element the -caller now owns and returns `1`, or returns `0` on exhaustion. Elements -follow the return rules of their family: free a string element with -`weaveffi_free_string`, a bytes or buffered element with -`weaveffi_free_bytes` after decoding, adopt an object element, and do -nothing for a direct element. Call `{IterTag}_destroy(iter)` exactly once, -whether iteration ran to exhaustion or was abandoned early. Each `next` -carries `out_err` and follows the owning function's error strategy. - -## Async completions - -An `async` symbol lowers to a `{sym}_async` launcher and a `{sym}_callback` -typedef. The launcher returns immediately; the callback fires exactly once, -from an arbitrary producer thread. Everything it receives is owned by the -consumer: a string result is released with `weaveffi_free_string`, a bytes -or buffered result with `weaveffi_free_bytes`, an object result is adopted, -and a non-null `err` is heap-boxed and released with `weaveffi_error_free`. -The `void* context` you passed to the launcher comes back untouched. -Wrappers hop back to their own scheduler before touching consumer state. - -Panics inside the producer's future are caught and reported through the -callback as `PANIC_ERROR_CODE`, so the callback fires even when the -producer is buggy. The producer runs futures on its configured spawner -(`weaveffi::set_spawner`); the default drives each future on a dedicated -thread. - -## Cancel tokens - -A `cancellable` async launcher takes a `weaveffi_cancel_token*` before the -callback and context. The consumer creates it, may cancel it from any -thread, and destroys it after the completion callback has fired (the -producer only reads the atomic flag, never frees the token): - -```c -weaveffi_cancel_token* weaveffi_cancel_token_create(void); -void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); -``` - -Passing null as the token means "never cancelled". Cancellation is -cooperative: the producer polls the token at safe points and returns early -(typically with a domain error) when it observes the flag. Every function -above treats null as a no-op. - -## ABI revision check - -Every producer exports `uint32_t weaveffi_abi_version(void)` (`2` for this -revision) and every generated consumer embeds the revision it was built for. -Consumers that can do so cheaply compare the two at load time and refuse to -run against a producer built for another revision (Python raises -`ImportError`, Ruby `LoadError`, Go panics in `init`, and so on), so a -layout mismatch is never a garbled buffer later. A hand-written C consumer -should do the same before calling anything else. - -## Non-default prefixes - -Runtime symbols always keep the `weaveffi_` spelling in the producer. When -a project configures another `c_prefix`, the generated header `#define`s -each prefixed runtime name (`{prefix}_error`, `{prefix}_free_string`, -`{prefix}_error_set`, and so on) onto its canonical symbol, so consumer -code may use either spelling. diff --git a/docs/src/reference/naming.md b/docs/src/reference/naming.md index 49bc4e97..92a8c37b 100644 --- a/docs/src/reference/naming.md +++ b/docs/src/reference/naming.md @@ -1,357 +1,130 @@ -# Naming and Package Conventions - -This page has two halves. The first is the naming policy for the WeaveFFI -project itself: brand names, repository slugs, package names across -registries, and the names of the generator targets. The second is the -naming policy the **generators** apply to -the identifiers they emit from your IDL: C symbols, wrapper classes, -functions, error types, and the Kotlin package. Every rule in the second -half was checked against the output of `weaveffi generate` for the -`kvstore` and `events` samples. - -## Project names - -### Human-facing brand names (prose) - -Use the condensed names in sentences and documentation: WeaveFFI, -WeaveHeap. The brand stem is spelled `WeaveFFI` with an uppercase `FFI` -everywhere, including in generated code (`WeaveFFIError`, -`WeaveFFIException`), never the case-converter's `Weaveffi`. - -### Repository and package slugs (URLs and registries) - -- Use condensed lowercase slugs for top-level repositories: GitHub - `weavefoundry/weaveffi`, `weavefoundry/weaveheap`. -- Use hyphenated slugs for subpackages and components, prefixed with the - top-level slug: `weaveffi-core`, `weaveffi-ir`, `weaveffi-gen-kotlin`. -- Planned package names (not yet published): crates.io `weaveffi`, - `weaveffi-core`, `weaveffi-ir`, and so on; npm `@weavefoundry/weaveffi`; - PyPI `weaveffi`; SPM repo slug `weaveffi`. - -Rationale: condensed top-level slugs unify handles across registries and -are ergonomic to type; hyphenated subpackages remain idiomatic and map -cleanly to ecosystems that normalize to underscores or CamelCase. - -### Target names - -A generator's target name is the one identifier that appears everywhere -the generator does: the `--target` flag, the `[generators.]` -table in `weaveffi.toml`, the output directory under `generated/`, the -`weaveffi-gen-` crate, and the page under -[Generators](../generators/README.md). The eleven targets are `c`, `cpp`, -`swift`, `kotlin`, `node`, `wasm`, `python`, `dotnet`, `dart`, `go`, and -`ruby`. - -The rule is to name the target after the thing a consumer holds and -identifies with, which is sometimes a language and sometimes a runtime: - -- When the emitted surface is idiomatic to one language, the target is - that language (`swift`, `python`, `dart`, `go`, `ruby`, `kotlin`). -- When one runtime serves several languages equally, the target is the - runtime (`dotnet`, `node`, `wasm`). Two targets that emit the same - language for different runtimes are named by runtime so they can be - told apart (`node` and `wasm` both emit JavaScript). -- A deployment platform is never a target name. Where a target runs on - more than one platform, the platform is a setting under its - `[generators.]` table, so adding a platform never renames the - target. - -The two cases that look inconsistent are both applications of the same -rule: - -- `dotnet`, not `csharp`. The generator emits C#, but the P/Invoke surface - and the NuGet package are consumed equally from C#, F#, and Visual Basic, - so the runtime is what a consumer identifies with. -- `kotlin`, not `android` or `jvm`. The generator was called `android` - through schema `0.8.0`. It was renamed because the same output runs on - Android and on a desktop JVM (the conformance harness drives it with - `kotlinc` and `java`, with no Android SDK), so a platform name described - only one of its runtimes. It isn't `jvm` because the emitted API depends - on Kotlin-only features (`suspend fun` for async, sealed classes for rich - enums, data classes for records), and a Java caller would not find it - idiomatic; naming it after the runtime would promise a language-neutral - surface the generator doesn't provide. - -A new generator should pick its name by the same test: if a second -language could consume the output as naturally as the first, name the -target after the runtime; otherwise name it after the language. Don't -name it after the platform it happens to ship on first. - -### Code identifiers by ecosystem - -- Rust: hyphenated subcrates on crates.io (`weaveffi-core`), imported with - underscores (`weaveffi_core`); the facade crate is `weaveffi`. Modules - and paths are snake_case; types, traits, and enums are CamelCase. -- Swift: package products and modules are UpperCamelCase (`WeaveFFI`); - the repo slug stays condensed and the SPM product provides the CamelCase - surface. -- Java and Kotlin: group ID and package base are reverse-DNS lowercase - (`com.weavefoundry.weaveffi`); artifact IDs are condensed at the top level - (`weaveffi`) and hyphenated for sub-artifacts (`weaveffi-android`); class - names are UpperCamelCase (`WeaveFFI`). -- JavaScript and TypeScript: scope plus condensed name for the top-level - package (`@weavefoundry/weaveffi`), hyphenated for subpackages - (`@weavefoundry/weaveffi-core`). -- Python: PyPI name condensed at the top level (`weaveffi`), hyphenated for - subpackages (`weaveffi-core`); import as `weaveffi` and `weaveffi_core`. -- C and CMake: target and library names are snake_case (`weaveffi`, - `weaveffi_core`); includes are directory based (`#include `). - -### Writing guidelines - -- In prose, prefer the condensed brand names. -- In code snippets, follow the host language conventions above. -- For cross-language docs, show both the repo or package slug and the - language-appropriate identifier on first mention: "Install `weaveffi` - (import as `weaveffi`, Swift module `WeaveFFI`)." -- New crates and packages follow the condensed-top-level plus - hyphenated-subpackage pattern (`weaveffi-*`, `weaveheap-*`). Avoid - hyphenated top-level slugs such as `weave-ffi`. - -## Generated identifiers - -The generators derive every emitted name from three inputs: the IDL name, -the module path, and the target's casing idiom. The rules below are what -the shared model in `weaveffi-core` decides once for all targets -(`weaveffi_core::utils`, `weaveffi_core::errors`, and the ABI slot -assignment in `weaveffi_core::model`), followed by each target's casing. -IDL names are expected to be `snake_case` for modules, functions, -parameters, and fields and `PascalCase` for types, variants, and error -codes; the validator only enforces that they are identifiers, so a name -written in another style is re-cased on a best-effort basis. - -### C ABI symbols - -The C names are normative in the [C ABI Contract](abi.md); this is the -summary. `{prefix}` is `weaveffi` unless a project sets `c_prefix`, and -`{path}` is the module path joined with underscores (`kv`, `kv_stats`). - -| Declaration | C spelling | `kvstore` example | -|--------------------------------------|----------------------------------------------|---------------------------------------------------| -| free function `f` in module `{path}` | `{prefix}_{path}_f` | `weaveffi_kv_stats_get_stats` | -| interface `T` (opaque tag) | `{prefix}_{path}_T` | `weaveffi_kv_Store` | -| interface member `m` (ctor, method, static) | `{tag}_m` | `weaveffi_kv_Store_open`, `weaveffi_kv_Store_get` | -| implicit lifecycle | `{tag}_clone`, `{tag}_destroy` | `weaveffi_kv_Store_clone`, `weaveffi_kv_Store_destroy` | -| callback interface `L` (vtable type) | `{prefix}_{path}_L_vtable` | `weaveffi_kv_EvictionListener_vtable` | -| callback parameter `p` | `void* p_ctx, const {vtable}* p_vtable` | `listener_ctx`, `listener_vtable` | -| C-style enum `E` and variant `V` | `{prefix}_{path}_E`, `{prefix}_{path}_E_V` | `weaveffi_kv_EntryKind_Volatile` | -| error domain `D` and code `C` | `{prefix}_{path}_D` enum, `{prefix}_{path}_D_C` | `weaveffi_kv_KvError_KeyNotFound` | -| iterator returned by `f` | `{owner}_{PascalF}Iterator` tag with `_next` and `_destroy` | `weaveffi_kv_Store_ListKeysIterator` | -| async function `f` | `{sym}_async` launcher, `{sym}_callback` typedef | `weaveffi_kv_Store_compact_async` | -| `bytes` or buffered parameter `p` | `p_ptr`, `p_len` | `value_ptr`, `value_len` | -| out-length of a returned buffer | `out_len` | | -| error out-parameter | `out_err` | | - -The iterator `{owner}` is `{prefix}_{path}` for a free function and -`{prefix}_{path}_T` for a method or static of interface `T`; the function -name is re-cased to PascalCase (`list_keys` becomes `ListKeys`). Types are -never re-cased: the IDL spelling of `Store`, `EntryKind`, and `KeyNotFound` -appears verbatim in every C symbol. A cross-module type reference resolves -to its owner's path, so a `stats` function taking the parent module's -`Store` still spells the parameter `const weaveffi_kv_Store*`. - -Runtime symbols (`weaveffi_error`, `weaveffi_error_set`, -`weaveffi_error_clear`, `weaveffi_error_free`, `weaveffi_free_string`, -`weaveffi_free_bytes`, `weaveffi_abi_version`, and the four -`weaveffi_cancel_token_*` functions) always keep the `weaveffi_` spelling -in the producer. Under a non-default prefix the generated header adds -`#define {prefix}_{name} weaveffi_{name}` aliases for each of them, so -consumer code may use either spelling. - -Because free functions and interface members share `{prefix}_{path}_`, a -free function named `Store_get` next to a `get` method on `Store` is a -validation error (`AbiSymbolCollision`), as is a free function named -`Store_clone` or `Store_destroy`. - -### Functions, methods, and parameters - -Free functions, methods, statics, constructors, and parameters are re-cased -to the target's idiom: - -| Target | Callables and parameters | Example (`get_stats`, `open_many`) | -|---------------|--------------------------|------------------------------------------------| -| C, C++, Python, Ruby | `snake_case` (unchanged) | `get_stats`, `open_many` | -| Swift, Kotlin, Dart, Node, Wasm | `camelCase` | `getStats`, `openMany` | -| Go, .NET | `PascalCase` | `GetStats`, `OpenMany` | - -A free function keeps its bare name by default. Every target except `c`, -`cpp`, and `wasm` has a `strip_module_prefix` option (see -[Project Configuration](../guides/config.md)); setting it to `false` -prepends `{module}_` before re-casing, giving `kv_stats_get_stats`, -`kvStatsGetStats`, or `KvStatsGetStats`. - -Where a free function lives depends on the target's notion of a -namespace. Python, Ruby, C++, Dart, Node, and Wasm emit module-level -functions (Ruby's live on the package module, `Kvstore.get_stats`). Swift -nests them in one `enum` per module path segment (`Kv.Stats.getStats`). -.NET puts them on a static class per module whose name is the PascalCase -module path (`Kv.GetStats`, `KvStats.GetStats`). Kotlin puts every free -function on the companion object of a single `WeaveFFI` holder class -(`WeaveFFI.getStats(...)`). Go exports them at package level. - -Constructors follow the interface: a constructor named `new` becomes the -canonical constructor where the target has one (Swift `init`, Python -`__init__`, Ruby `initialize`, a C# or C++ constructor, a Dart `factory -EventBus()`, JavaScript `new EventBus()`, and in Kotlin a companion -`operator fun invoke()` so `EventBus()` reads like a constructor); every -other constructor becomes a static factory in the target's casing -(`Store.open`, `Store.openMany`, `Store.Open`). Go has no constructors, so -each becomes a package-level function named `{PascalCtor}{Type}` -(`NewEventBus`, `OpenStore`); Dart uses a named constructor -(`Store.open`). - -### Records and enums - -Struct and rich-enum names are kept verbatim (`Entry`, `StoreInfo`, -`Shape`) in every target; each is an idiomatic value type (a Kotlin `data -class`, a Swift `struct`, a Go `struct`, a .NET `sealed class`, a -TypeScript `interface`, a Python class with annotated attributes). -Fields are re-cased like parameters (`created_at` becomes `createdAt` in -Swift, Kotlin, Dart, and JavaScript and `CreatedAt` in Go and .NET). - -C-style enum names are kept verbatim. Variant casing follows each target's -enum idiom: - -| Target | Variant of `EntryKind { Volatile }` | -|-----------------------------------------|-------------------------------------| -| C, C++, Kotlin, Python, .NET, Node, Wasm, Dart | `Volatile` | -| Swift | `.volatile` (lowerCamelCase case) | -| Go | `EntryKindVolatile` (type-prefixed constant) | -| Ruby | `EntryKind::VOLATILE` (module constant) | - -### Interfaces - -An interface `Store` is a class named `Store` in every target (a Swift -`final class`, a Kotlin `class ... : AutoCloseable`, a Python class, a Ruby -class over an `FFI::AutoPointer`, a Go `struct` used through `*Store`, a -.NET `class Store : IDisposable`, a Dart `class Store implements -Finalizable`, a C++ RAII class with copy semantics that call `_clone`, a -TypeScript class). Methods and statics are re-cased like free functions -and attached to the class; statics use the target's static idiom -(`static func`, `companion object`, `@staticmethod`, `def self.`, C# -`static`, and package-level functions in Go). - -The wrapper's disposal method is named by the target's convention, never -by the IDL: `close()` in Kotlin, Python, Dart, Node, and Wasm (also -`Symbol.dispose` in JavaScript and `AutoCloseable` in Kotlin), `Close()` -in Go, `Dispose()` in .NET, and the destructor or `deinit` in C++ and -Swift. Ruby releases through the `AutoPointer` and exposes `close`. No -target exposes `clone`; copying a wrapper (C++ copy constructor, or simply -holding two references) is the only way to get a second reference from the -consumer side. - -### Callback interfaces - -A callback interface `EvictionListener` becomes the target's "abstract -method set" type, named after the IDL with at most one idiomatic prefix: - -| Target | Declaration | -|----------|----------------------------------------------------------| -| C | `weaveffi_kv_EvictionListener_vtable` struct | -| C++ | `class EvictionListener` with pure virtual methods | -| Swift | `protocol EvictionListener` | -| Kotlin | `interface EvictionListener` | -| Python | `class EvictionListener(abc.ABC)` | -| Ruby | `module EvictionListener` (mixin with `NotImplementedError` stubs) | -| Go | `type EvictionListener interface` | -| .NET | `interface IEvictionListener` (C#'s `I` prefix) | -| Dart | `abstract class EvictionListener` | -| Node, Wasm | `interface EvictionListener` in the `.d.ts` | - -Methods are re-cased like other callables (`on_evict` becomes `onEvict`, -`OnEvict`, or stays `on_evict`). No lifecycle name is exposed to the -consumer: the vtable's trailing `free` entry and the `ctx` handle are -generated glue, and the consumer only ever hands over an implementation. - -### Error domains and codes - -The base error type is branded, and every target uses one of two brands -from `weaveffi_core::errors`: `WeaveFFIError` where the ecosystem's errors -end in `Error` (Swift, Python, TypeScript, C++, Go) and -`WeaveFFIException` where they end in `Exception` (Kotlin, .NET, Dart). -Ruby is the one exception: its base class is `Error` nested in the package -module (`Kvstore::Error`), because the module already provides the brand. - -A domain `KvError` with codes `KeyNotFound` and `IoError` yields: - -| Target | Domain type | Per-code name | -|---------|----------------------------------|--------------------------------------------------| -| C, C++ header enum | `weaveffi_kv_KvError` | `weaveffi_kv_KvError_KeyNotFound` constants | -| C++ | `class KvError : WeaveFFIError` | `class KeyNotFoundError : KvError`, `class IoError` | -| Swift | `enum KvError: Error` | `case keyNotFound(message:)`, `case ioError(message:)` | -| Kotlin | `sealed class KvException : WeaveFFIException` | nested `KvException.KeyNotFound`, `KvException.IoError` | -| Python | `class KvError(WeaveFFIError)` | `class KeyNotFound(KvError)`, also reachable as `KvError.KeyNotFound` | -| Ruby | `class KvError < Kvstore::Error` | nested `KvError::KeyNotFound`, `KvError::IoError` | -| Go | `type KvError struct` | `KvErrorKeyNotFound` and `KvErrorIoError` code constants | -| .NET | `class KvException : WeaveFFIException` | `KvException.KeyNotFound` code constants | -| Dart | `class KvException extends WeaveFFIException` | `class KeyNotFoundException`, `class IoException` | -| Node, Wasm | `class KvError extends WeaveFFIError` | `class KeyNotFoundError`, `class IoError` | - -The rules behind the table: - -- `type_name(raw, suffix)` re-cases a name to PascalCase and appends the - suffix exactly once: `KEY_NOT_FOUND` with `Error` is `KeyNotFoundError`, - and `AlreadyError` stays `AlreadyError`. C++, Node, and Wasm use it with - `Error`; Dart uses it with `Exception`. -- `exception_type_name(raw)` swaps a trailing `Error` stem for `Exception` - instead of stacking them: `KvError` becomes `KvException` (Kotlin, .NET, - Dart), `Failure` becomes `FailureException`, and a domain named just - `Error` falls back to the brand. -- `pascal(raw)` is the suffix-free PascalCase form used where codes are - nested cases or constants (Kotlin, Ruby, Python, .NET, Go). -- Swift lowerCamelCases the code into an enum case (`keyNotFound`). - -The upshot for IDL authors: write PascalCase code names and let the -generator add or replace suffixes. Writing `KeyNotFoundError` as the code -name produces `KeyNotFoundError` in C++ and `KeyNotFoundException` in Dart -(the stem is normalized), but `KeyNotFoundError` as the Kotlin nested class -and `keyNotFoundError` as the Swift case, which is why the samples leave -the suffix off. Code names are unique across the whole API for exactly this -reason: every target flattens them into one namespace. - -### Package, module, and file names - -Ecosystem package names come from `[package] name` in `weaveffi.toml` -(`kvstore` in the samples) and are re-cased per ecosystem; see -[Project Configuration](../guides/config.md#package). The names that -appear in code: - -| Target | Namespace derived from the package name | -|---------|----------------------------------------------------------------------| -| Swift | module `Kvstore` (PascalCase); free functions under per-module enums | -| Ruby | `module Kvstore` wrapping everything | -| .NET | `namespace Kvstore` | -| C++ | `namespace kvstore` (lowercase) | -| Go | `package kvstore`; the import path comes from `[generators.go] module_path` | -| Python | package directory `kvstore/` containing `weaveffi.py` and `weaveffi.pyi` | -| Kotlin | Gradle `rootProject.name = "kvstore"`; the JVM package comes from config | - -### Kotlin package and class names - -The `kotlin` target is configured under `[generators.kotlin]`: - -```toml -[generators.kotlin] -package = "com.example.kv" # default "com.weaveffi" -strip_module_prefix = true # default -``` - -- **Package.** Every generated Kotlin file starts with `package {package}` - and is written under `src/main/kotlin/{package with dots as slashes}/`, - so the default lands at `src/main/kotlin/com/weaveffi/WeaveFFI.kt`. The - JNI glue is `src/main/cpp/weaveffi_jni.c`, and its exported JNI symbols - are derived from the package (`Java_com_weaveffi_WeaveFFI_...`), so - changing the package regenerates both files consistently. -- **Holder class.** Free functions live on the companion object of - `class WeaveFFI`, together with `setCallbackExceptionHandler`. The - holder's name is fixed; only the package is configurable. -- **Types.** Interfaces, records, enums, callback interfaces, and errors - are top-level declarations in the same package, named as in the tables - above (`Store`, `Entry`, `EntryKind`, `EvictionListener`, `KvException`). - An iterator returned by `list_keys` on `Store` is - `KvStoreListKeysIterator` (`{PascalModule}{Interface}{PascalFn}Iterator`), - implementing `Iterator` and `AutoCloseable`. -- **Runtime glue.** Internal helpers are prefixed `Weave` or `weave` - (`WeaveNativeLibrary`, `WeaveBufferReader`, `weaveCleaner`) and marked - `internal`, so they never collide with an IDL type whose name starts - with `WeaveFFI`. +# Naming + +Every name WeaveFFI emits derives from two things: the library's +**identity** (what the library is called) and the **IDL names** (what the +API calls things). This page states both sets of rules. + +## Identity + +The CLI resolves one identity per library from `weaveffi.toml` and, for a +Rust producer, `Cargo.toml` (the full rules are in +[Project Configuration](../guides/config.md#package)): + +| Field | Meaning | Rust producer | IDL | +|-------|---------|---------------|-----| +| `name` | package name as published | `[package] name`, else the crate's package name | `[package] name`, else the input file stem | +| `prefix` | C symbol prefix | the crate's library name | `[package] c_prefix`, else snake(`name`) | +| `library` | native library base name | same as `prefix` | `[package] library`, else snake(`name`) | +| `version`, `description`, `license`, `authors`, `homepage`, `repository` | metadata | `[package]`, falling back to `Cargo.toml` | `[package]` | + +Two conversions turn an arbitrary name into identifiers: + +- **snake**: lowercase ASCII letters and digits, with every run of other + characters collapsed to one `_` and trailing `_` trimmed; a leading digit + gets a `lib_` prefix. `my-kv.store` becomes `my_kv_store`, `3d-engine` + becomes `lib_3d_engine`. +- **PascalCase**: each run of letters and digits starts a word whose first + letter is uppercased, and existing capitals are kept; a leading digit gets + an `N` prefix. `my-kv.store` becomes `MyKvStore`. + +`{PREFIX}` is the prefix uppercased. + +## Per-target defaults + +| Target | Default names (each overridable in `[generators.]`) | +|--------|---------------------------------------------------------------| +| C | header `{library}.h`; include guard `{PREFIX}_H`; export macro `{PREFIX}_API` | +| C++ | header `{library}.hpp`, which includes `{library}.h` (shipped alongside); namespace `{prefix}` | +| Swift | SwiftPM package, product, and module `PascalCase(name)`; C module `C{PascalCase(name)}` | +| Kotlin | package `{prefix}`; one Kotlin `object` per IDL module; JNI library `{library}_jni` | +| Node.js | npm package `{name}`; addon `{library}_node.node`; one namespace export per module | +| WebAssembly | npm package `{name}`; ES module; one namespace export per module | +| Python | distribution `{name}`; import package `{prefix}` | +| .NET | namespace and assembly `PascalCase(name)` | +| Dart | package `{prefix}` | +| Go | module path `{name}` unless `module_path` is set; package `{prefix}` | +| Ruby | gem `{name}`; module `PascalCase(name)`; require path `{prefix}` | + +For the `kvstore` sample (crate `kvstore`) that's `kvstore.h`, Swift module +`Kvstore`, `import kvstore` in Python, and `require "kvstore"` in Ruby. + +**Library loading.** Every target loads the native library by its +`library` name using the platform's file naming (`lib{library}.dylib`, +`lib{library}.so`, `{library}.dll`). Every loader first honors the +environment variable `{PREFIX}_LIBRARY` (for example `KVSTORE_LIBRARY`), an +explicit path that wins over bundled and system copies. + +Nothing a generator emits is named after WeaveFFI; the only mentions are the +generated-file header comment and runtime-version comments. Two libraries +built with WeaveFFI therefore never collide in symbols, packages, types, or +environment variables. + +## C identifiers + +C identifiers are the prefix, the underscore-joined module path, and the IDL +name, verbatim: `kvstore_kv_Store_open`, `kvstore_kv_KvError_KeyNotFound`. +IDL names are never re-cased in C, except that an iterator type's function +part is PascalCase (`kvstore_kv_Store_ListKeysIterator`). The full table is +in the [C ABI contract](abi.md#symbol-names); validation rejects any API in +which two declarations produce the same identifier. + +## Identifiers in generated code + +IDL names are expected in `snake_case` for modules, functions, parameters, +and fields, and in `PascalCase` for types, variants, and error codes. The +validator only requires identifiers, so other styles are re-cased on a +best-effort basis. + +**Functions, methods, parameters, and fields** follow each language's +convention: + +| Target | Style | `get_stats`, `created_at` | +|--------|-------|---------------------------| +| C, C++, Python, Ruby | `snake_case` | `get_stats`, `created_at` | +| Swift, Kotlin, Node.js, WebAssembly, Dart | `camelCase` | `getStats`, `createdAt` | +| Go, .NET | `PascalCase` | `GetStats`, `CreatedAt` | + +**Types** (records, enums, interfaces, callback interfaces, error domains) +keep their IDL names in every target, except where a language idiom adds a +prefix or suffix (.NET prefixes interfaces it generates for callback +interfaces with `I`, for example). **C-style enum variants** follow the +target's enum idiom: `Volatile` in most targets, `.volatile` in Swift, +`EntryKindVolatile` in Go, `EntryKind::VOLATILE` in Ruby. + +**Constructors.** A constructor named `new` becomes the language's +constructor (`init`, `__init__`, `initialize`, a C++ or C# constructor); +other constructors become static factories (`Store.open`). Go has no +constructors, so they become package functions (`NewEventBus`, `OpenStore`). + +**Modules.** How module members are grouped depends on the target's notion +of a namespace. Kotlin emits one `object` per IDL module, and Node.js and +WebAssembly export one namespace object per module (nested modules nest), +so two modules can both declare `get`. Targets that place members in one +flat namespace drop the module name from free functions by default and +offer `strip_module_prefix = false` to keep it (`kv_stats_get_stats`); see +[Project Configuration](../guides/config.md#global). Each language page +documents its layout. + +**Errors.** Each target has a root error type named after the package and +one type (or case) per error domain and code, named from the IDL with at +most one idiomatic suffix (`Error` or `Exception`). Write code names without +a suffix (`KeyNotFound`, not `KeyNotFoundError`) and let the generator add +it. Code names are unique across the API because several targets flatten +them into one namespace. + +**Reserved words.** A name that's a keyword in the target language gains a +trailing `_` (`type` becomes `type_`), a rule that's stable under repetition. +A user type whose name would shadow a standard type in the target (a Kotlin +type named `Result`, say) is referenced in a way that avoids the clash; the +language pages describe how. + +## Target names + +A target is named after what its consumer identifies with: the language when +the output is idiomatic to one language (`swift`, `python`, `kotlin`), and +the runtime when several languages consume it equally or two targets share a +language (`dotnet` serves C#, F#, and Visual Basic; `node` and `wasm` both +emit JavaScript). A deployment platform is never a target name: `kotlin` +covers Android and the desktop JVM, selected by a setting. The same token +names the `--target` value, the `[generators.]` table, the output +directory, and the page under [Generators](../generators/README.md). diff --git a/docs/src/reference/value-buffers.md b/docs/src/reference/value-buffers.md index fcec0900..02bafaf1 100644 --- a/docs/src/reference/value-buffers.md +++ b/docs/src/reference/value-buffers.md @@ -1,162 +1,118 @@ -# Value Buffer Protocol +# Value Buffers Records, rich enums, optionals, lists, maps, and error payloads cross the C -ABI *by value*, serialized in a compact binary format called the WeaveFFI -value buffer. A buffered value occupies exactly one `(const uint8_t*, size_t)` -slot pair at the ABI, no matter how deeply it nests. This page is the -normative wire-format and ownership specification every generator implements. +ABI *by value*, serialized in one compact binary format. A buffered value +occupies one `(const uint8_t*, size_t)` slot pair however deeply it nests. +This page is the normative wire format every generator and the +`weaveffi-abi` runtime (`weaveffi_abi::buffer`) implement. ## Which types are buffered -A type is *buffered* when it is one of: +- a struct (record) +- a rich enum (any variant has fields) +- `[T]`, `{K:V}` +- `T?`, except `Interface?`, which stays a nullable object pointer at the top + level of a parameter or return -- a record (struct) -- a rich (algebraic) enum -- `[T]` (list) -- `{K: V}` (map) -- `T?` (optional), except `Interface?` - -Everything else keeps its scalar or pointer ABI: integers, floats, and `bool` -pass by value; strings pass as C strings; `bytes` as a raw pointer plus -length; interfaces and iterators as opaque object pointers; `Interface?` as a -nullable object pointer; C-style enums as `int32_t`. - -Interfaces *may* appear inside a buffered type (a record field, a list -element, a map value, an optional) and encode as an object token; see -[Objects](#objects). Iterators and callback interfaces never appear inside a -buffered type; validation rejects them in buffered positions. +Everything else keeps its own slot shape at the top level (see the +[C ABI contract](abi.md#families-and-slots)): direct values by value, strings +and bytes as `(ptr, len)`, objects as pointers. Any of them may appear +*inside* a buffer. Iterators and callback interfaces never do; validation +rejects them in buffered positions. ## Encoding -All multi-byte values are little-endian. There is no padding and no -alignment; values are packed back to back. Lengths and element counts are -`u32`, capping any single string, byte buffer, or collection at -232 − 1 entries. - -| IDL type | Encoding | -|-----------------------|------------------------------------------------------| -| `bool` | 1 byte: `0` or `1` | -| `i8` / `u8` | 1 byte | -| `i16` / `u16` | 2 bytes | -| `i32` / `u32` | 4 bytes | -| `i64` / `u64` | 8 bytes | -| `f32` | 4 bytes (IEEE 754 bits) | -| `f64` | 8 bytes (IEEE 754 bits) | -| enum (C-style) | `i32` discriminant | -| interface | `u64` object token (one strong reference) | -| `string` | `u32` byte length, then UTF-8 bytes (no terminator) | -| `bytes` | `u32` length, then raw bytes | -| `T?` | 1 flag byte (`0` absent, `1` present), then the value when present | -| `[T]` | `u32` count, then each element | -| `{K: V}` | `u32` count, then alternating key, value | -| record | each field in declaration order | -| rich enum | `i32` tag, then the active variant's fields in order | -| error payload | the matched code's fields in declaration order | - -Because the format is compositional, arbitrary nesting (`{string: [T?]}`, -records containing records, lists of rich enums, and so on) works with no -per-shape special cases. - -Note that `[u8]` canonicalizes to `bytes` at parse time: the two encode -identically inside a buffer (a `u32` count followed by the raw bytes), and -canonicalizing keeps the top-level ABI consistent with Rust producers, where -`Vec` maps to `bytes`. - -A decoder must reject a buffer that is exhausted mid-value, holds an invalid -`bool` or optional flag byte, holds invalid UTF-8 in a string, declares a -length prefix larger than the bytes remaining, or leaves trailing bytes after -the complete value. A malformed buffer is a producer/consumer contract -violation (both sides are generated from one IDL), so consumers surface it -through the same channel as a producer panic, not as a typed domain error. +All multi-byte values are little-endian, packed back to back with no padding +or alignment. Lengths and counts are `u32`, so a single string, byte run, or +collection holds at most 232 − 1 entries; an +encoder refuses to write more rather than truncate. + +| IDL type | Encoding | +|----------|----------| +| `bool` | 1 byte: `0` or `1` | +| `i8`, `u8` | 1 byte | +| `i16`, `u16` | 2 bytes | +| `i32`, `u32` | 4 bytes | +| `i64`, `u64` | 8 bytes | +| `f32`, `f64` | 4 or 8 bytes of IEEE 754 bits (NaN payloads, infinities, and `-0` round-trip) | +| C-style enum | `i32` value | +| interface | `u64` object token | +| `string` | `u32` byte length, then UTF-8 bytes (no terminator) | +| `bytes` | `u32` length, then raw bytes | +| `T?` | 1 flag byte (`0` absent, `1` present), then the value if present | +| `[T]` | `u32` count, then each element | +| `{K:V}` | `u32` count, then alternating key and value | +| struct | each field, in declaration order | +| rich enum | `i32` tag (the variant's `value`), then the variant's fields in order | +| error payload | the code's fields, in declaration order | + +The format is compositional, so `{string:[T?]}`, records inside records, and +lists of rich enums need no special cases. `[u8]` is canonicalized to `bytes` +at parse time; the two encode identically. + +Example: a `Point { x: f64, y: f64 }` with `x = 1.5`, `y = -2.0` is 16 +bytes, `00 00 00 00 00 00 F8 3F 00 00 00 00 00 00 00 C0`; a `[string]` of +`["hi"]` is `01 00 00 00 02 00 00 00 68 69`. + +## Decoding + +A decoder rejects a buffer that: + +- ends in the middle of a value; +- holds a `bool` or optional flag byte other than `0` or `1`; +- holds invalid UTF-8 in a string; +- declares a string or byte length larger than the bytes remaining; +- holds an enum value or rich-enum tag that isn't declared; +- has bytes left over after the complete value. + +A collection count is not checked against the bytes remaining, because an +element's encoding can be empty; a decoder caps how much it preallocates +from a count instead, so a hostile count fails when the elements run out. +A malformed buffer is a contract violation between two sides generated from +one definition, so it's reported as the marshalling code `-3`, never as a +domain error. ## Objects -An interface value inside a buffer is an **object token**: the object's +An interface value inside a buffer is an **object token**: the object pointer widened to `u64` (zero-extended on 32-bit targets). A token carries exactly one strong reference, in either direction. -- **Writing.** The side that encodes a token must own the reference it - writes. A consumer that holds an object wrapper calls `{tag}_clone` and - writes the returned pointer, keeping its own reference for the wrapper; a - producer writes `Arc::into_raw(arc.clone())`. -- **Reading.** The side that decodes a token adopts the reference: a consumer - wraps the pointer in a new object wrapper whose finalizer will call - `{tag}_destroy`; a producer calls `Arc::from_raw`. -- **Failure.** If decoding fails partway through a buffer, the reader is - responsible for releasing every token it already adopted. The writer keeps - no responsibility once the buffer is handed over. -- **Single use.** Because each token is one reference, an encoding that - contains objects is consumed exactly once: decode it twice and the second - reader adopts references that no longer exist. Generated bindings encode a - fresh buffer per call and never reuse one. +- **Writing.** The encoder owns the reference it writes. A consumer holding a + wrapper calls `_clone` and writes the returned pointer, keeping its own + reference; a Rust producer writes a cloned `Arc`. +- **Reading.** The decoder adopts the reference: a consumer wraps it in a new + object wrapper, a Rust producer turns it back into an `Arc`. +- **Failure.** If decoding fails partway, the reader releases every token it + already adopted; the writer has no further responsibility once the buffer + is handed over. +- **Single use.** Because each token is one reference, a buffer holding + objects is decoded exactly once. Bindings encode a fresh buffer per call. A zero token is invalid in a non-optional position; `Interface?` inside a -buffer uses the ordinary optional flag byte followed by the token when -present. - -## ABI slots and ownership - -**Parameters.** A buffered parameter named `v` lowers to two slots: - -```c -const uint8_t* v_ptr, size_t v_len -``` - -The caller owns the encoding and keeps it alive for the duration of the call; -the callee decodes and never frees it. - -**Returns.** A buffered return lowers exactly like a `bytes` return: the -producer allocates the encoding and hands it back as the return value plus a -trailing out-parameter: - -```c -const uint8_t* f(..., size_t* out_len, weaveffi_error* out_err); -``` - -The consumer decodes the buffer and then releases it with -`weaveffi_free_bytes(ptr, len)`. - -**Iterator elements.** `_next` writes a producer-allocated buffer through -`const uint8_t** out_item` plus `size_t* out_len`; the consumer decodes and -frees it with `weaveffi_free_bytes` per element. - -**Async results.** The completion callback receives an *owned* -`(const uint8_t* result_ptr, size_t result_len)` pair: the consumer decodes -(or copies) the encoding and then releases it with `weaveffi_free_bytes`. -Owned interface results transfer ownership the same way: the callback adopts -the object pointer. - -**Callback-interface method arguments.** Buffered arguments to a -callback-interface method are borrowed `(ptr, len)` pairs valid only for the -duration of the dispatch; the consumer decodes before returning. Object tokens -inside such a buffer still transfer one strong reference to the consumer. - -## Structured errors +buffer uses the optional flag byte followed by the token. -`weaveffi_error` carries a payload alongside the `(code, message)` pair: +## Slots and ownership -```c -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; -``` +| Position | Slots | Owner | +|----------|-------|-------| +| parameter `v` | `const uint8_t* v_ptr, size_t v_len` | the caller; borrowed for the call | +| return | `const uint8_t*` return plus `size_t* out_len` | the consumer; free with `{p}_free_bytes(ptr, len)` | +| iterator element | `const uint8_t** out_item, size_t* out_len` | the consumer, per element | +| async result | `const uint8_t* result_ptr, size_t result_len` | the consumer | +| callback method argument | `const uint8_t* v_ptr, size_t v_len` | the producer; borrowed for the call | +| error payload | `payload_ptr`, `payload_len` in `{p}_error` | freed by `{p}_error_clear` or `{p}_error_free` | -When an error domain code declares fields, a matching error's -`payload_ptr`/`payload_len` hold those fields serialized in the value-buffer -format, in declaration order; `payload_ptr` is null when the code declares no -fields. `weaveffi_error_clear` releases both the message and the payload. -Generators decode the payload into properties of the raised exception (or -returned error value) keyed by the field names. +Object tokens inside any of these transfer their reference to whoever +decodes them, including the consumer decoding a callback argument. ## What generators emit -Each language binding ships a small private runtime with a buffer writer and -reader implementing the table above, plus one pack and one unpack routine per -record and rich enum (generated from the IDL, so field order is fixed at -generation time). Optionals, lists, and maps are handled generically by the -writer/reader, recursing through element types. Records and rich enums map to -idiomatic value types in the target language (data classes, structs, sealed -class hierarchies); no handle wrapping, no destructors, no builders. +Each binding ships a small private codec (a writer and a reader for the +table above) plus one encode and one decode routine per record and rich enum, +generated from the definition so field order is fixed at generation time. +Optionals, lists, and maps are handled generically. Lists of bytes and of +fixed-width numbers (every integer and float type, but not `bool`, whose +bytes must be validated) encode and decode with a single copy where the +platform is little-endian. The `codec` sample's conformance lane checks every +binding's codec against the Rust runtime's, in both directions. diff --git a/docs/src/roadmap.md b/docs/src/roadmap.md index 97ac1814..047a9700 100644 --- a/docs/src/roadmap.md +++ b/docs/src/roadmap.md @@ -1,165 +1,138 @@ # Roadmap -WeaveFFI is in active `0.x` development. Schema `0.9.0` and ABI revision 2 -landed the object model the project had been building toward: reference-counted -interfaces that compose with every other type, callback interfaces the consumer -implements, and a pluggable async executor. This page lists what comes next. -Items are marked **planned** (the design is settled and the work is scheduled) -or **exploring** (we want it, but the design has open questions). Nothing here -carries a date; the [CHANGELOG][changelog] is the source of truth for what has -shipped, and [Stability and Versioning](stability.md) explains how releases and -schema versions work. +WeaveFFI is in active `0.x` development. Schema 0.10 and ABI revision 3 made +every name derive from the library's identity, moved strings to `(ptr, len)` +runs, added contract checksums, and made cancellation work idiomatically on +every target. This page lists what comes next. Items are **planned** (the +design is settled) or **exploring** (wanted, with open design questions). +Nothing carries a date; the +[changelog](https://github.com/weavefoundry/weaveffi/blob/main/CHANGELOG.md) +records what shipped. ## Callback interfaces -### Callback methods returning strings, buffers, and objects (planned) - -Today a callback-interface method returns nothing, a scalar, `bool`, or a -C-style enum, because the ABI has no way for a consumer allocation to cross -back into the producer safely: the producer frees with its own allocator, and -eleven languages have eleven allocators. The planned fix is an explicit -allocator contract for callback returns. The consumer will write the result -into producer-owned storage obtained through the runtime (in the spirit of -`weaveffi_alloc`, which the Wasm target already uses), or return a consumer -allocation together with a release function the producer calls once it has -copied the bytes. Once that contract exists, `string`, `bytes`, records, rich -enums, optionals, lists, maps, and objects become valid callback returns on -every target, and `throws` on callback methods (a typed domain error raised by -the consumer, rather than the catch-all `FOREIGN_ERROR_CODE`) follows -naturally. +### Rich callback returns (planned) + +Callback methods return nothing or a direct value, because a consumer +allocation can't safely cross back to a producer with a different allocator. +The plan is an allocator contract for callback returns: the consumer writes +the result into producer-owned storage obtained through the runtime (as the +Wasm glue already does with `{prefix}_alloc`), or returns its own allocation +with a release function the producer calls after copying. With that, +strings, bytes, records, rich enums, optionals, lists, maps, and objects +become valid callback returns, and typed `throws` on callback methods follows. ### Async callback methods (planned) -A callback method that returns a future on the consumer side (`async` in -Swift, Python, Kotlin, and JavaScript; a `Task` in .NET; a `Future` in Dart) -needs a completion callback flowing the other way, plus a cancellation story -when the producer drops the future. The vtable shape is straightforward (an -extra completion function and context per async method); the hard part is -making the same producer code work whether the consumer runtime is an event -loop, a thread pool, or the single-threaded Wasm host. This lands after the -allocator contract above, because an async result needs to carry the same -return types a synchronous one does. +A callback method that returns a future on the consumer side needs a +completion flowing the other way and a cancellation story when the producer +drops the future. The vtable shape is simple (a completion function and +context per async method); the hard part is one producer working the same way +whether the consumer runtime is an event loop, a thread pool, or the +single-threaded Wasm host. This follows the allocator contract above. + +### Vtable versioning (exploring) + +A vtable is a fixed struct, so adding a method to a callback interface is a +breaking change even when no consumer needs it. A size or version field at +the head of each vtable would let a newer producer detect an older consumer's +shorter table and fall back, which matters once 1.0 promises additive +changes are compatible. + +### Callback thread affinity (exploring) + +Dart can't run a value-returning callback method synchronously on a thread +other than its isolate's, so a producer that calls one from a worker thread +aborts the process today (void methods are forwarded safely). A per-vtable +thread-affinity hint, or a runtime helper that lets a consumer refuse an +off-thread call with `-4`, would turn that abort into an error. + +## Definitions + +### Multi-file IDL (planned) + +An IDL API is one document. Large APIs want to split by module, and a +monorepo wants to reference another package's types. The plan is an +`imports:` list resolved at parse time, with bare type names still unique +across the merged API, and `diff`, `validate`, the cache, and checksums +tracking every imported file. + +### Extraction from the compiled library (exploring) -## Type system +The CLI reads a Rust producer's API by parsing source, so it sees only +inline `#[weaveffi::module]`s in one file and can't expand macros. The macro +already lowers each module to the IR at compile time; embedding that IR in +the built library (a custom section or an exported symbol) would let +`weaveffi generate` read the exact API from the artifact, removing the +one-file limit and any chance of the parser and the compiler disagreeing. ### Generic and trait-object interfaces (exploring) -WeaveFFI deliberately ships a fixed set of generic shapes (`T?`, `[T]`, -`{K:V}`, `iter`) rather than user-defined generics. Two extensions are under -discussion. The first is *trait-object interfaces*: an interface declared as a -set of methods with more than one producer-side implementation, so a Rust -`Arc` can be returned and each consumer sees one class. The second -is *parameterized interfaces* such as `Cache`, monomorphized per -instantiation in the IDL. The first is likely; the second only if it can be -done without every generator growing type-erasure machinery. +WeaveFFI has a fixed set of generic shapes (`T?`, `[T]`, `{K:V}`, `iter`). +Under discussion are trait-object interfaces (one declared method set with +several producer implementations behind `Arc`) and, less likely, +parameterized interfaces monomorphized per instantiation. ### Duration and timestamp primitives (exploring) -Most real APIs carry time. Today producers pass `i64` seconds or milliseconds -and document the unit. A `duration` primitive (nanoseconds as `i64`, mapped to -`Duration`, `TimeInterval`, `TimeSpan`, `timedelta`, `kotlin.time.Duration`, -and so on) and a `timestamp` primitive (a UTC instant) would remove the -ambiguity. The open question is representation: one width for everything, or a -`u64` seconds plus `u32` nanoseconds pair as UniFFI uses. +Producers pass time as `i64` with a documented unit. `duration` and +`timestamp` primitives mapped to each language's types would remove the +ambiguity; the open question is the representation. -## IDL +## Targets and runtime -### Multi-file IDL imports (planned) +### Kotlin Multiplatform (exploring) -An API is one document today. Large APIs want to split by module and share -types across files, and a monorepo wants to reference an interface declared by -another package's IDL. The plan is an `imports:` list that resolves relative -paths at parse time, with bare type names remaining unique across the merged -API (as they are today) and `weaveffi diff`, `validate`, and the output cache -tracking every imported file. Annotated Rust already handles this shape -naturally (one crate, many `#[weaveffi::module]` blocks), so the IDL path is -the one catching up. +The Kotlin target reaches Android and the JVM through JNI. A Multiplatform +flavor using Kotlin/Native `cinterop` for iOS and desktop is the natural next +step. -## Generators and runtime +### Wasm threads (exploring) + +On `wasm32-unknown-unknown` futures run inline and callbacks fire only while +a call is on the stack. A spawner that schedules on the JS event loop, or +shared-memory builds with Web Workers, would lift both limits and let +Emscripten mode support async functions and callback interfaces. ### Per-language support packages (exploring) -Every generated package inlines its own helper code: the value-buffer codec, -the error hierarchy, the object wrapper base, the callback trampolines. That -keeps consumers free of any WeaveFFI dependency, which is a design principle we -intend to keep. It also means a codec fix ships as a regeneration of every -package. We are exploring extracting the stable runtime portions into optional -per-language support packages (an npm package, a Swift target, a Python wheel, -and so on) that generated code can depend on *if the library author opts in*, -with inlining remaining the default. The conformance harness and the `codec` -sample exist so that this refactor, if it happens, is mechanical. - -### Wasm threads and spawner (exploring) - -On `wasm32-unknown-unknown` the default spawner drives each future inline, -callback-interface methods fire only while a call into the module is on the -stack, and a producer that calls back from a spawned thread can't run at all. -Two paths are being evaluated: a spawner that schedules futures on the JS -microtask queue through `wasm-bindgen-futures`-style glue, and support for -`wasm32` builds with shared memory and Web Workers (`atomics` plus -`bulk-memory`) where a real thread-per-future spawner works. Either would also -let Emscripten mode regain async functions and callback interfaces, which it -currently rejects at generation time. - -### Library naming in bare `generate` output (planned) - -`weaveffi package` names the native library after the package identity on -every target. The bare `weaveffi generate` trees are not yet consistent: Dart -loads `lib.`, while Python, Node (`binding.gyp`), Swift -(`module.modulemap`), and Kotlin (`CMakeLists.txt`) still assume a library -called `weaveffi`, and the bare Kotlin `CMakeLists.txt` doesn't link the JNI -shim against the producer at all (the packaged module does). The plan is to -derive one library base name from the package identity in the binding model -and have every generator use it, so `WEAVEFFI_LIBRARY` overrides stop being -necessary in the tutorials. - -### Cancel tokens on wasm (planned) - -The wasm glue passes a null cancel token for `#[weaveffi::cancellable]` -functions, so JS can't cancel an in-flight async call the way every native -target can (`AbortSignal` is the natural shape). This follows the spawner -work above, since inline completion leaves nothing to cancel today. - -### Kotlin Multiplatform and other targets (exploring) - -The Kotlin generator targets Android and the desktop JVM through JNI. A Kotlin -Multiplatform flavor (Kotlin/Native `cinterop` for iOS and desktop) is the -natural next step. Further targets (Java without Kotlin, PHP, Lua) are -possible because a generator is a self-contained crate implementing -`LanguageBackend`, but none is scheduled. +Every package inlines its helpers (codec, error types, object base), which +keeps consumers free of dependencies but means a codec fix ships as a +regeneration. Opt-in shared support packages per ecosystem are under +consideration; inlining would stay the default. + +## Testing + +### Windows conformance lanes (planned) + +The workspace's tests run on Windows, Linux, and macOS, but the conformance +harness runs only on Linux and macOS. Adding Windows lanes means making +`conformance/run.sh` and the per-language scripts portable (or adding +PowerShell equivalents) and installing each toolchain on Windows runners. + +### Shared codec vectors (planned) + +Each language's `codec` conformance consumer asserts the same round-trip +values by hand. Moving those values into one data file that every consumer +reads (or into producer-side golden values) would shrink the consumers and +keep the eleven lanes from drifting apart. ## Toward 1.0 -1.0 means the surfaces listed in [What semver covers](stability.md#what-semver-covers-post-10) -stop changing without a major release. We will cut it when all of the following -hold: - -- **ABI revision 2 has been stable for several minor releases** with no - incompatible change, and the callback-return allocator contract has shipped - (so that 1.0 doesn't immediately need revision 3). -- **Every target passes the full conformance matrix**, including the `codec` - round-trip lane, the async lanes, and the callback-interface lanes, on - Linux, macOS, and Windows in CI. -- **Multi-file imports and the deprecation policy are live**, so a post-1.0 - API can grow and shed surface the way the - [deprecation policy](stability.md#post-10-deprecation-policy) describes. -- **The schema has a migration tool** (`weaveffi migrate`) and - `SUPPORTED_VERSIONS` accepts more than one version. -- **The public Rust API of every published crate has been reviewed** for - items that should be `#[doc(hidden)]` or private before they become a - contract. - -Until then, expect pre-1.0 churn to be batched (one schema bump per minor -release) and documented, and pin the CLI version in CI as -[Stability and Versioning](stability.md) recommends. - -## Contributing to the roadmap - -Open a discussion or issue on -[GitHub](https://github.com/weavefoundry/weaveffi/issues) if one of the -exploring items matters to you, or if something you need isn't listed. Items -move from exploring to planned when a design has been written up and reviewed; -see [CONTRIBUTING.md](https://github.com/weavefoundry/weaveffi/blob/main/CONTRIBUTING.md) -for how proposals work. - -[changelog]: https://github.com/weavefoundry/weaveffi/blob/main/CHANGELOG.md +1.0 means the surfaces in [What 1.0 will cover](stability.md#what-10-will-cover) +stop changing without a major release. It needs: + +- ABI revision 3 stable for several releases, with the callback-return + allocator contract and vtable versioning settled so 1.0 doesn't need + revision 4; +- every target passing the full conformance matrix on Linux, macOS, and + Windows; +- multi-file IDL and the deprecation policy in place; +- a schema migration tool and more than one accepted schema version; +- a review of every published crate's public API. + +## Contributing + +Open an issue or discussion on +[GitHub](https://github.com/weavefoundry/weaveffi/issues) if an item here +matters to you or something you need is missing. diff --git a/docs/src/samples.md b/docs/src/samples.md index b406df4e..6b2c1f4a 100644 --- a/docs/src/samples.md +++ b/docs/src/samples.md @@ -1,340 +1,86 @@ # Samples -This repo includes eight 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)), -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. - -Each sample builds as a `cdylib`, ships Rust tests that drive its own exported -C symbols the way a foreign consumer would, and is regenerated in CI with -`weaveffi diff --check` so the committed source and the generators can't drift. - -| Sample | Path | Showcases | -|---------------|-----------------------|--------------------------------------------------------------------------------| -| `calculator` | `samples/calculator` | The smallest module: scalars, strings, one error domain | -| `contacts` | `samples/contacts` | A record, a C-style enum, an interface with CRUD methods, list returns | -| `inventory` | `samples/inventory` | Two modules, cross-module records, an interface owning a collection | -| `shapes` | `samples/shapes` | Rich (algebraic) enums and the full numeric primitive set | -| `async-demo` | `samples/async-demo` | Async functions returning records and lists | -| `events` | `samples/events` | A callback interface, a shared reference-counted object, an iterator | -| `kvstore` | `samples/kvstore` | Every IDL feature, including an object graph and an eviction listener | -| `codec` | `samples/codec` | A round-trip oracle for every value-buffer wire shape, object tokens included | - -## Kvstore (kitchen-sink reference) - -Path: `samples/kvstore` - -A production-quality, in-memory key/value store that exercises **every IDL -feature WeaveFFI supports** in a single sample. Use this as the canonical -reference when learning the IDL surface or when you need to copy a real-world -pattern for a new generator. - -**What it demonstrates:** - -- A reference-counted interface (`Store`) with a throwing constructor - (`open`), instance methods, statics (`default_capacity`, `open_many`, - `total_count`), and the implicit `_clone`/`_destroy` pair -- An **object graph**: `share` returns a second reference to the same store - (`self: Arc` in, `Arc` out), `fork` returns a new store, - `larger(other: Store?) -> Store?` takes and returns an optional object, - `describe -> StoreInfo` returns a record whose `store` field carries the - object itself (and an optional `mirror`), `open_many -> [Store]` returns a - list of objects, and `total_count([Store], StoreInfo?)` takes objects inside - a list and inside an optional record -- A **callback interface** (`EvictionListener`) attached with - `set_eviction_listener` and detached with `clear_eviction_listener`; the - store retains the consumer's implementation, notifies it outside every - lock, and drops it when `on_evict` returns `false` -- A record (`Entry`) with every primitive: `i64`, `string`, `bytes`, an - optional field (`expires_at: i64?`), a list field (`tags: [string]`), and a - map field (`metadata: {string:string}`), plus per-field doc strings -- Documented C-style enums (`EntryKind`, `EvictionReason`) -- A documented error domain (`KvError` with `KeyNotFound`, `Expired`, - `StoreFull`, `IoError`) and opt-in `throws: true` on the fallible methods -- A streaming iterator return (`list_keys -> iter`) with a prefix - filter -- A cancellable async method (`compact`, `#[weaveffi::cancellable]`) that - checks its `CancelToken` before reclaiming expired entries -- A deprecated method (`legacy_put`, via `#[deprecated(note = ...)]`) -- A nested sub-module (`kv.stats`) with its own record (`Stats`) and a - function that takes the parent module's `Store` by reference -- 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:** +The `samples/` directory holds six Rust producers written with +`#[weaveffi::module]`. Each is a workspace crate built as a `cdylib` with the +`leak-check` feature on, has unit tests that call its exported symbols +directly, and is consumed by the [conformance harness](generators/README.md#conformance-lanes) +in all eleven languages. ```bash cargo build -p kvstore -cargo test -p kvstore weaveffi generate samples/kvstore/src/lib.rs -o generated ``` -The `conformance/` harness ships a kvstore consumer for every language that -opens a `Store`, round-trips entries, shares and forks stores, attaches an -eviction listener, drives the async `compact`, and asserts the typed `KvError` -surface; see `conformance/run.sh`. - -## Events (callback interface + shared object) - -Path: `samples/events` - -A publish/subscribe bus and the reference sample for **callback interfaces** -and **object sharing**. The consumer implements `Subscriber`; the bus retains -any number of subscribers, asks each how to `route` a message, delivers -accepted messages, and hands the bus itself back through a callback. - -**What it demonstrates:** - -- A callback interface (`Subscriber`) with three method shapes: `route(topic: - string) -> Delivery` returns a C-style enum, `on_message(message: Message) - -> i64` takes a record and returns a scalar, and `on_attached(bus: - EventBus)` receives an object the consumer adopts -- A reference-counted interface (`EventBus`) whose constructor returns - `Arc` and whose `subscribe` method takes `self: Arc` so it can - hand a strong reference to the subscriber -- Producer-side discipline for calling into consumer code: the bus snapshots - its subscriber list and never holds a lock across a callback, because a - failing subscriber aborts the publishing call with `FOREIGN_ERROR_CODE` -- Consumer `free` semantics: `clear_subscribers` (or destroying the bus) - releases the producer's references and each consumer implementation's - `free` entry runs exactly once -- An async method (`publish_later`), an iterator return (`messages -> - iter`), an optional record return (`last_message -> Message?`), and - a free function taking a callback interface (`route_once`) - -**Build and run tests:** - -```bash -cargo build -p events -cargo test -p events -weaveffi generate samples/events/src/lib.rs -o generated -``` - -The Rust tests build a `Subscriber` vtable by hand, exactly as a generated -binding does, and assert reference counts, `free` calls, and the foreign-error -path. The `conformance/` harness runs an `events` consumer in all eleven -languages. - -## Codec (value-buffer round-trip oracle) - -Path: `samples/codec` - -Every generated binding ships its own encoder and decoder for the -[value-buffer protocol](reference/value-buffers.md). This sample gives the -conformance harness one producer that exercises **every wire shape in both -directions**, so a codec bug in any language shows up as a concrete mismatch -rather than a subtle corruption. - -**What it demonstrates:** - -- `Scalars`: a record with every fixed-width scalar (`i8` through `u64`, - `f32`, `f64`, `bool`) and a C-style enum, using edge values such as - `u64::MAX`, `i64::MIN`, and a non-integer `f64` -- `Composite`: a record with strings (including non-ASCII), bytes, present - and absent optionals, lists, lists of lists, an empty list, string-keyed and - integer-keyed maps, a nested record, a rich enum, a list of rich enums, an - optional rich enum, an optional list, a list of optionals, and a list of - enums -- `Shape`: a rich enum with unit, scalar, mixed, string, and nested-record - variants -- `Holder`: **objects inside buffers** (a required `Token`, an optional one, - and a list of them), with `make_holder`, `sum_holder`, `primary_of`, and - `same_primary` proving that each token carries one strong reference, that a - buffer is decoded exactly once, and that identity survives the round trip -- Three function families per shape: `sample_*` (producer encodes, consumer - decodes), `verify_*` (consumer encodes, producer decodes and fails with - `CodecError::Mismatch` on any difference), and `roundtrip_*` (echo), plus - `describe_*` helpers that render what the producer actually saw - -**Build and run tests:** - -```bash -cargo build -p codec -cargo test -p codec -weaveffi generate samples/codec/src/lib.rs -o generated -``` - -The `conformance/` harness runs the `codec` consumer in all eleven languages; -it is the lane to watch when touching any generator's buffer code. - -## Shapes (rich enums + numerics) - -Path: `samples/shapes` - -The reference sample for **rich (algebraic) enums** (sum types whose variants -carry associated data) and the **expanded numeric primitives**. Use it when -learning how a tagged union crosses the C ABI serialized in a value buffer and -how each backend surfaces it as an idiomatic sum type. - -**What it demonstrates:** - -- A rich enum (`Shape`) with a data-less variant (`Empty`) and three payload - variants (`Circle { radius: f64 }`, `Rectangle { width: f32, height: f32 }`, - and `Labeled { label: string, count: u8 }`), serialized on the wire as an - `i32` tag followed by the active variant's fields -- A plain C-style enum (`Channel`) alongside the rich enum, showing both enum - flavors in one module -- The numeric primitives `f32`, `u8`, and `u64` as variant fields, parameters, - and return types -- Functions that take and return a rich enum (`describe`, `scale`) and a - list-of-bytes reduction (`sum_bytes(values: [u8]) -> u64`) - -**Build, generate bindings, and run the C ABI tests:** - -```bash -cargo build -p shapes -cargo test -p shapes -weaveffi generate samples/shapes/src/lib.rs -o generated -``` - -The `conformance/` harness ships a `shapes` consumer for every language that -constructs each variant, reads the tag and fields back, and round-trips through -`describe`/`scale`. - -## Calculator - -Path: `samples/calculator` - -The simplest sample: a single `#[weaveffi::module]` with four functions that -exercise primitive types (`i32`) and string passing. Good starting point for -understanding the basic C ABI contract and the macro workflow. - -**What it demonstrates:** - -- Scalar parameters and return values (`i32`) -- String parameters and return values (borrowed in, producer-owned out, freed - with `weaveffi_free_string`) -- The smallest possible typed error surface: a `#[weaveffi::error]` enum - (`CalcError`) and one throwing function (`div` returns - `Result`) -- A producer written entirely as safe Rust (no hand-written FFI glue) - -**Build and generate bindings (from the annotated source):** - -```bash -cargo build -p calculator -weaveffi generate samples/calculator/src/lib.rs -o generated -``` - -This produces target-specific output under `generated/` for all eleven -languages. The [Calculator tutorial](tutorials/calculator.md) walks through -running C, Node, and Swift consumers against it. - -## Contacts - -Path: `samples/contacts` - -A CRUD-style sample with a single module. It exercises richer type-system -features than the calculator while writing no `unsafe` glue. - -**What it demonstrates:** - -- A `#[weaveffi::enumeration]` (`ContactType` with `Personal`, `Work`, `Other`) -- A `#[weaveffi::record]` (`Contact`) with a generated `BufferValue` - encode/decode impl -- Optional fields (`Option` for the email) -- A `#[weaveffi::interface]` (`ContactBook`) with a `new` constructor, - `&self` methods guarding a `Mutex`, and the implicit `_clone`/`_destroy` - pair -- List return types (`Vec` from `ContactBook::list`) -- A `#[weaveffi::error]` domain (`ContactsError`) surfaced by the throwing - methods via `Result` - -**Build and generate bindings (from the annotated source):** - -```bash -cargo build -p contacts -weaveffi generate samples/contacts/src/lib.rs -o generated -``` - -The `conformance/` harness runs a `contacts` consumer in all eleven languages. - -## Inventory - -Path: `samples/inventory` - -A richer, multi-module sample with `products` and `orders` modules, written as -safe Rust with two `#[weaveffi::module]` blocks. It exercises cross-module -references and record lists. - -**What it demonstrates:** - -- Two annotated modules in one crate, each with its own error domain - (`ProductsError`, `OrdersError`) -- A `#[weaveffi::interface]` (`Catalog`) owning its product list, alongside - free functions in the `orders` module -- A `#[weaveffi::enumeration]` (`Category`) and `#[weaveffi::record]`s - (`Product`, `Order`, `OrderItem`) -- Optional and list fields (`Option`, `Vec` tags) -- A record-list return (`Catalog::search -> Vec`) and a record-list - parameter (`create_order(items: Vec)`) -- A cross-module record parameter (`orders::add_product_to_order` takes a - `products::Product`) - -**Build and generate bindings (from the annotated source):** - -```bash -cargo build -p inventory -weaveffi generate samples/inventory/src/lib.rs -o generated -``` - -The `conformance/` harness runs `inventory` consumers in C and Python. - -## Async Demo - -Path: `samples/async-demo` - -Demonstrates the async function pattern. An `async fn` export lowers to an -`_async` launcher at the C ABI that accepts a completion callback plus a -context pointer instead of returning directly, and each target wraps it in its -native awaitable. - -**What it demonstrates:** - -- Async exports (`pub async fn` under `#[weaveffi::export]`) returning a - record (`run_task -> TaskResult`), a list of records (`run_batch`), and a - scalar (`run_n_tasks`) -- A throwing async function (`run_task` returns - `Result`), delivered as a heap-boxed error the - consumer frees with `weaveffi_error_free` -- Synchronous functions in the same module (`cancel_task`, - `active_callbacks`) -- The default spawner (one thread per future) in action; see the - [Async Functions guide](guides/async.md) for plugging in Tokio with - `weaveffi::set_spawner` - -**Build and run tests:** - -```bash -cargo build -p async-demo -cargo test -p async-demo -``` - -The `conformance/` harness runs an `async-demo` consumer in all eleven -languages, awaiting the results through each target's native idiom. - -## End-to-end testing - -The `conformance/` directory is the end-to-end regression oracle for the code -generators. Every consumer under `conformance//` binds through the -*generated* wrappers (not the raw C ABI) and asserts concrete results against -the samples: `contacts`, `events`, `kvstore`, `shapes`, `codec`, and -`async-demo` in all eleven languages, plus `inventory` in C and Python. The -`conformance/run.sh` harness builds each producer cdylib, runs -`weaveffi generate` for it, then compiles and runs every per-(language, -sample) consumer: - -```bash -bash conformance/run.sh -``` - -It prints `[OK] {target}` for each consumer that succeeds and reports a -pass/fail summary at the end. Use `ONLY=c-contacts,cpp-contacts` to run a -subset, or `SKIP=go-contacts` to omit individual targets. Missing toolchains -cause the affected target to fail; skip those explicitly. See the comment -block at the top of `conformance/run.sh` for the per-target prerequisites. +Each sample's crate name is its C prefix: `kvstore` exports +`kvstore_kv_Store_open`, and its bindings load `libkvstore` (or +`KVSTORE_LIBRARY`). The `weaveffi.toml` beside each crate supplies package +metadata. + +## calculator + +The smallest producer: a `calculator` module with `add`, `mul`, `echo` +(a string round trip), and `div`, which throws the one-code `CalcError` +domain. Start here to see the shape of a producer and its tests. The +`c-producer-exports` lane also implements the calculator's generated header +by hand in C (`conformance/c/producer.c`) and checks that every required +runtime and checksum symbol is exported. + +## contacts + +An address book built around a `ContactBook` interface whose methods guard +their state with a `Mutex` and throw `ContactsError` codes. It shows records +(`Contact`, with an optional email), a C-style enum (`ContactType`), lists of +records, and both ways modules relate: + +- `contacts::groups` is nested under the `contacts` root, so it uses the + parent's `ContactBook` interface and `ContactType` enum, returns new + `ContactBook` objects, and inherits the parent's error domain. +- `directory` is a sibling root with its own `DirectoryError`. Across roots + only value types can be shared, so it takes the `Contact` record. + +## events + +A publish/subscribe bus: an `EventBus` interface, a consumer-implemented +`Subscriber` callback interface, a `Message` record, and a `Delivery` enum. +It shows both callback styles: `route` returns +`Result` and receives consumer failures as values, +while `on_message` and `on_attached` (which hands the bus itself to the +consumer) return plain values. It also has an iterator over published +messages (`messages`), an async method (`publish_later`), an optional record +return (`last_message`), and a free function taking a callback +(`route_once`). The bus snapshots its subscribers before calling out and +never holds a lock across a callback. + +## kvstore + +The kitchen-sink reference: an in-memory key-value store that uses every +feature WeaveFFI supports. The `kv` module has a `Store` interface with a +throwing constructor, methods, and statics; a `KvError` domain; records with +optional, list, map, and bytes fields; `StoreInfo`, a record carrying `Store` +objects; methods that pass objects in every position (`share`, `fork`, +`larger` with `Store?` in and out, `describe`, `open_many` returning +`[Store]`, `total_count`); an `EvictionListener` callback interface; a +throwing iterator (`list_keys`); a cancellable async method (`compact`); a +deprecated method (`legacy_put`); and a nested `kv.stats` module that takes +the parent's `Store`. + +## async-demo + +Async functions in the `tasks` module: `run_task` (an async throwing function +returning a record), `run_batch` (an async list), `run_n_tasks` (for stress +tests of concurrent launches), and `wait`, a cancellable timer. Cancelling +`wait` completes the call with `-5` immediately and drops the pending timer; +`active_callbacks` returns to zero once every task body has finished or been +cancelled, which the lanes assert. The futures need no async runtime, so the +default spawner drives them. + +## codec + +A round-trip oracle for the [value-buffer format](reference/value-buffers.md). +`sample_*` functions return canonical values the consumer checks field by +field (producer encodes, consumer decodes); `verify_*` functions take the +same values back and fail with `CodecError::Mismatch` unless they decode +exactly (consumer encodes, producer decodes); `roundtrip_*` functions echo +scalars, 64-bit edge values, strings, bytes, optionals, maps, rich enums, and +records; and `describe_*` functions render what the producer saw. A `Token` +interface and a `Holder` record cover object tokens inside buffers. diff --git a/docs/src/stability.md b/docs/src/stability.md index 805c3d35..6a78d1ad 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -1,306 +1,154 @@ # Stability and Versioning -WeaveFFI follows [Semantic Versioning](https://semver.org/) once it reaches -1.0.0. Until then it is in active pre-1.0 development and **any** surface area -may change between minor versions. This page documents exactly what is and -isn't covered, what the deprecation policy will look like post-1.0, and how -to bind your CI to a stable WeaveFFI workflow today. - -## What semver covers (post-1.0) - -After the 1.0.0 release, the following surfaces will be governed by SemVer: - -- **CLI flags and subcommands.** Every documented `weaveffi `, - every flag, every exit code, and every documented stdout/stderr format - (`--format json` payloads in particular). Adding a new optional flag is a - minor bump; removing or renaming one is a breaking change. -- **IDL schema.** The set of accepted top-level keys, type-reference syntax - (`iter`, `[T]`, `{K:V}`, `T?`, primitives, and user-defined struct, enum, - interface, and callback-interface names), `version` semantics, and the - JSON Schema exported by `weaveffi schema --format json-schema`. -- **Generated code shape.** The exported symbol names, function signatures, - type names, package layouts, and ABI conventions of every generator's - output. A patch release will not change the bytes of the generated output; - a minor release may add new symbols but will not remove or rename existing - ones; a major release may break. -- **Public Rust API of every published crate.** That is `weaveffi-ir`, - `weaveffi-abi`, `weaveffi-core`, `weaveffi-gen-c`, `weaveffi-gen-cpp`, - `weaveffi-gen-swift`, `weaveffi-gen-kotlin`, `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 `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, the object and callback-interface conventions, - and the `weaveffi_*` symbol set); the current revision is 2, and - [C ABI Contract](reference/abi.md) is its normative description. 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. Post-1.0, the revision - only changes with a major release. - -## What is NOT covered pre-1.0 - -While the workspace is at `0.x`, **everything** above may change without -warning. In practice we try to keep breaking changes batched (one batch per -minor release, with a schema-version bump), but the contract is "no -contract." Things that have already changed during 0.x: - -- IR type-reference syntax (`callback` was removed in `0.3.0`). -- The IR `TypeRef::Struct` variant was split into `Named` (the - unresolved parsed form), `Record`, and `RichEnum`, and a shared - marshalling-plan module (`weaveffi_core::plan`) now states the - calling contracts every generator renders. Generated wrapper - surfaces changed with it: `iter` returns became each target's - native lazy iteration idiom (Go `[]T` became an `iter.Seq`, C++ - `std::vector` became a range type), and async result buffers - became producer-freed after the completion callback returns. -- Schema `0.5.0` introduced first-class interfaces (`interfaces:`, objects - with constructors, methods, and statics) and per-function typed errors - (`throws:`), and made bare type names unique across the whole API. The - samples' handle-based resource surfaces were rewritten as interfaces. -- Schema `0.7.0` moved records, rich enums, optionals, lists, and maps to - by-value crossing as serialized value buffers (one - `(const uint8_t*, size_t)` pair per value; see the - [Value Buffer Protocol](reference/value-buffers.md)), replacing opaque - record pointers, per-type create/destroy/getter symbols, and parallel-array - maps. Nested composites became fully supported. The struct `builder:` flag - and the `#[weaveffi::builder]` attribute were removed. Error codes gained - structured payload `fields:`, and `weaveffi_error` gained - `payload_ptr`/`payload_len` slots. -- The `Generator` trait gained `generate_with_config` in `0.3.0`, then - was reworked in `0.5.0` into an associated `Config` type (with an - object-safe `DynGenerator` view) that replaced the - `*_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, 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. -- Schema `0.9.0` and **ABI revision 2** made interface objects reference - counted (every interface gained `_clone` beside `_destroy`, and objects may - now appear inside records, optionals, lists, maps, iterators, and async - results as `u64` tokens carrying one strong reference), replaced - module-level `callbacks:`/`listeners:` with `callback_interfaces:` (a - consumer-implemented vtable of methods), made the async executor pluggable - (`weaveffi::set_spawner`), and added `weaveffi_error_set` plus the - `FOREIGN_ERROR_CODE` (`-4`) reserved code. It removed the `handle`, - `handle`, `&str`, and `&[u8]` type spellings, `Param.mutable`, - `Function.since`, the `weaveffi_arena_*` runtime, and `weaveffi::Handle`, - and renamed the `android` target to `kotlin`. See the - [migration guide](#migrating-from-schema-080--abi-1-to-090--abi-2) below. - -Pin the WeaveFFI version in CI (`cargo install weaveffi-cli --version -=0.22.0`) and vendor the generated output in your repository so that -upgrades are an explicit, reviewable event. - -## Migrating from schema 0.8.0 / ABI 1 to 0.9.0 / ABI 2 - -This release removed constructs rather than deprecating them, so a `0.8.0` -document is rejected until it's rewritten. Change `version: "0.8.0"` to -`version: "0.9.0"`, then work through the tables below. `weaveffi validate` -names every remaining offender with a hint; a Rust producer gets the same -guidance as compile errors from the macros. - -### Removed IDL constructs - -| Removed in `0.8.0` | Replacement in `0.9.0` | -|-------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| -| `type: handle` (untyped opaque token) | Declare an interface for the resource and use its name as the type. Every interface is reference counted and may appear anywhere a type can. | -| `type: handle` | `type: T`, where `T` is an interface declared under `interfaces:`. | -| `type: "&str"` / `type: "&[u8]"` | `type: string` / `type: bytes`. Parameters of these families are always borrowed views at the ABI, so the explicit borrowed spelling no longer means anything. | -| `mutable: true` on a parameter | Drop it. Interface methods take a shared `&self`; guard interior state with a `Mutex` (or another `Sync` primitive) in the producer. | -| `since:` on a function | Drop it. Use `deprecated: ""` to mark surface that's on its way out; generators emit the target's deprecation marker. | -| module-level `callbacks:` | A `callback_interfaces:` entry whose `methods:` list the calls the producer makes. Functions that took a callback type take the callback interface by name instead. | -| module-level `listeners:` (`register_*` / `unregister_*` returning a subscription id) | A `callback_interfaces:` entry plus an ordinary interface method (or function) that accepts it, such as `subscribe(listener: MyListener)`. The producer retains the implementation for as long as it likes; the consumer's `free` fires when the last reference drops, so there's no unregister token to manage. | - -A callback-interface method is synchronous, can't declare `throws`, `async`, -or `cancellable`, and returns nothing, a scalar, `bool`, or a C-style enum. -Its parameters may use any type except another callback interface or an -iterator. Callback interfaces may appear only as parameters of functions, -constructors, statics, and methods (not as returns, not inside records or -collections, and not as `T?`). - -### Renamed things - -| `0.8.0` / ABI 1 | `0.9.0` / ABI 2 | -|----------------------------------------------|------------------------------------------------------------------------| -| `--target android`, output dir `android/` | `--target kotlin`, output dir `kotlin/` (with `build.gradle.kts`) | -| `[generators.android]` in `weaveffi.toml` | `[generators.kotlin]` | -| crate `weaveffi-gen-android` | crate `weaveffi-gen-kotlin` | -| `#[weaveffi::callback]` / `#[weaveffi::listener]` | `#[weaveffi::callback_interface]` on a `trait Name: Send + Sync` | -| `weaveffi::Handle` | `Arc` of a `#[weaveffi::interface]` type | -| `weaveffi_arena_*` runtime symbols | Removed. Buffered values are individually owned and freed with `weaveffi_free_bytes`. | -| `errors::ResolvedError`, `ConstPos::East` (Rust API) | Removed. | -| `ABI_VERSION == 1` | `ABI_VERSION == 2` | - -The reserved error codes are now `GENERIC_ERROR_CODE = -1`, -`PANIC_ERROR_CODE = -2`, `MARSHAL_ERROR_CODE = -3`, and the new -`FOREIGN_ERROR_CODE = -4` (a consumer callback-interface implementation -raised). Consumers report the last one through the new runtime symbol -`weaveffi_error_set(err, code, message)` so no foreign allocator touches -`weaveffi_error.message`. - -### Rust producer changes - -- Every `#[weaveffi::interface]` type must be `Send + Sync`; the macro - asserts it. Constructors return `Self`, `Arc`, or a `Result` of - either; methods take `&self` or `self: Arc`. `&mut self` methods no - longer compile. -- Interface types can be used as `Arc` (or `&T` for a borrowed parameter) - in any position: `Option>`, `Vec>`, `BTreeMap>`, - record fields, iterator items, and async returns. -- Replace `#[weaveffi::callback]`/`#[weaveffi::listener]` with a - `#[weaveffi::callback_interface] trait Name: Send + Sync { ... }` and accept - it as `Arc`. Treat every call into it as potentially panicking - (a consumer failure unwinds to the exported thunk, which reports - `FOREIGN_ERROR_CODE`): snapshot state and release locks before calling out. -- Async exports run on the installed spawner. Nothing changes if the default - thread-per-future executor is fine; call `weaveffi::set_spawner(...)` once - at startup to run futures on Tokio or another runtime instead. - -### Consumer-facing ownership changes - -- **Every wrapper owns one strong reference.** Wrappers release it through - the language's disposal hook (`close()`, `Dispose()`, `deinit`, RAII - destructor, `dispose()`, `Close()`) with a garbage-collector backstop where - one exists. Two wrappers may refer to the same native object; releasing one - never invalidates the other. -- **Objects returned from calls, iterators, async results, and callback-method - parameters are adopted.** Consumers no longer need to think about whether a - returned pointer is borrowed: it's always one reference they own. -- **Objects inside value buffers are tokens carrying one reference.** A - wrapper that encodes an object into a record or list calls `_clone` first; - a buffer that contains objects is decoded exactly once. -- **Callback implementations are freed by the producer.** The producer calls - the vtable's `free(ctx)` exactly once when its last reference drops; there - is no unregister call. A consumer implementation that raises is reported to - the original caller as `FOREIGN_ERROR_CODE`. -- **Kotlin consumers move from `android/` to `kotlin/`** and from a Groovy - `build.gradle` to `build.gradle.kts`. The generated code still targets - Android (JNI plus Gradle) and also runs on the desktop JVM. -- **Node.js consumers receive `i64`/`u64` values as `BigInt`**, and Dart - wrappers use `NativeFinalizer` as their backstop. - -## Post-1.0 deprecation policy - -Once we reach 1.0.0, breaking changes will follow this path: - -1. The feature is marked deprecated in a minor release. The CLI prints a - `--warn`-style diagnostic (`weaveffi: warning: is deprecated; - `) on every invocation that touches it. The - generators emit a native deprecation marker where the target language - supports one (`#[deprecated]` in Rust, `@Deprecated` in Kotlin/Java, - `@available(*, deprecated:)` in Swift, `[Obsolete]` in .NET, JSDoc - `@deprecated` in TypeScript, and so on, driven by the existing IDL - `deprecated:` field). -2. The deprecated feature continues to work for **at least one full minor - version**. -3. Removal lands in the next major release with a migration note in - `CHANGELOG.md`. - -In short: nothing disappears in a patch release, nothing disappears without -at least one minor release of warnings, and every removal ships with a -documented replacement. - -## IR schema version policy - -The IR schema version is independent of the workspace version, but it is -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.9.0`. - -Pre-1.0, **only the current schema version is accepted** -(`SUPPORTED_VERSIONS` contains exactly `CURRENT_SCHEMA_VERSION`), so a -document declaring any earlier revision is rejected with an actionable -error. When a schema bump lands, update the `version` field in your IDL and -adjust the document to the new schema by hand; the changes are documented -in `CHANGELOG.md` and, for the `0.9.0` bump, in the -[migration guide](#migrating-from-schema-080--abi-1-to-090--abi-2) above. -Post-1.0, schema bumps will ship with an automated migration tool and a -widened `SUPPORTED_VERSIONS` window. - -The C ABI revision is independent of both: it changes only when the runtime -contract changes incompatibly. Schema `0.9.0` happened to ship with ABI -revision 2 because reference-counted objects and callback-interface vtables -needed new symbols; a future schema bump that only adds IDL surface would -leave the ABI revision alone. - -## Generated-code stability (determinism) - -> **Regenerating with the same WeaveFFI version on the same IDL produces -> byte-identical output.** - -This is enforced by the determinism tests: every generator's output is -hashed and re-hashed on the kitchen-sink fixture, and any deviation fails -CI. Internally, every -`HashMap` iteration that contributes to generated output has been replaced -by `BTreeMap` or an explicit sort. The `serde_json`-backed cache key uses a -canonical key ordering. - -Practical consequences: - -- Vendoring the generated `bindings/` directory in your repository is - safe. A reviewer will only see a diff when the IDL or the generator - itself changes. -- `weaveffi diff --check` (see below) is a reliable CI gate. -- Cross-platform regeneration (Linux vs macOS vs Windows) produces the - same bytes for the same WeaveFFI version. - -If you ever observe non-determinism, please file an issue with the IDL -that triggers it. It's a bug, not a quirk. - -## The `weaveffi diff --check` workflow for downstream CI - -The single recommended way to guard a downstream repository against -"forgot to regenerate" mistakes is `weaveffi diff --check`: - -```bash -weaveffi diff path/to/api.yml --out generated/ --check -``` - -`diff --check` regenerates into a temporary directory, compares against -`--out`, and exits: - -- **0** when the on-disk output matches what regeneration would produce, -- **2** when at least one file differs (modified content), -- **3** when files are missing or extra (a target was added/removed). - -It prints only the summary `+ N added, - M removed, ~ K modified`, -suitable for CI logs without flooding the output. - -A typical GitHub Actions step: +WeaveFFI will follow [Semantic Versioning](https://semver.org/) from 1.0.0. +Until then it's in active pre-1.0 development and any surface may change +between minor releases. This page says what the version numbers mean, what +1.0 will promise, how to keep a downstream project steady today, and how to +migrate to the current release. + +## Three version numbers + +- **The crate version** (`0.23.x`) is shared by every published crate + (`weaveffi`, `weaveffi-abi`, `weaveffi-macros`, `weaveffi-model`, + `weaveffi-gen`, `weaveffi-cli`) and by the CLI. Releases are cut from + Conventional Commits by release-plz. +- **The IDL schema version** (`0.10.0`) is the `version:` an IDL document + declares. Pre-1.0, only the current version is accepted; a document + declaring any other is rejected with `UnsupportedSchemaVersion`. +- **The C ABI revision** (`3`) is what `{prefix}_abi_version()` returns. It + changes only when the runtime surface or a calling convention changes + incompatibly, independently of the other two. + +## What 1.0 will cover + +After 1.0.0, these surfaces change incompatibly only in a major release: + +- **The CLI**: subcommands, flags, exit codes, and documented output + formats (`validate --format json` in particular). +- **The IDL schema**: accepted keys, type syntax, and the exported JSON + Schema. +- **Generated code**: exported symbols, signatures, type names, and package + layouts. A patch release won't change generated bytes; a minor release may + add symbols but won't remove or rename any. +- **The C ABI revision** and the [contract](reference/abi.md) it names. +- **The public Rust API** of the published crates. + +Deprecation will precede removal: a deprecated feature warns for at least +one minor release (the CLI prints a diagnostic, and generators emit each +language's deprecation marker from the IDL's `deprecated:` field) and is +removed in the next major release with a migration note. Post-1.0 schema +bumps will ship with a migration tool and accept more than one version. + +## Determinism + +Regenerating with the same WeaveFFI version from the same definition and +`weaveffi.toml` produces byte-identical output on every platform. Generators +never iterate hash maps into output, and a test regenerates the kitchen-sink +fixture twice and compares. Non-determinism is a bug; please report it with +the definition that triggers it. + +## CI workflow + +Pin the CLI version and commit the generated output, so an upgrade is an +explicit, reviewable change. Then gate CI on `weaveffi diff --check`, which +regenerates in memory and compares with the committed directory without +writing anything or running hooks: ```yaml -- name: Verify generated bindings are up to date +- name: Bindings are up to date run: | - cargo install weaveffi-cli --locked --version =0.22.0 - weaveffi diff idl/api.yml --out generated/ --check + cargo install weaveffi-cli --locked --version =0.23.0 + weaveffi validate --warn + weaveffi diff --check ``` -Combine it with `weaveffi validate --warn idl/api.yml` (schema correctness -plus advisory lints) for a complete CI guard. - -## See also - -- [IDL Schema](reference/idl.md): the type system the schema version - governs. -- [C ABI Contract](reference/abi.md): the normative description of ABI - revision 2. -- [Roadmap](roadmap.md): what's planned after ABI 2 and the criteria for 1.0. -- [Getting Started](getting-started.md): installation and the basic - workflow `diff --check` plugs into. +`diff --check` exits `0` when the output matches, `2` when files would +change, and `3` when files would be added or removed. + +## Migrating to schema 0.10 and ABI 3 + +This release renames every C symbol after the library and changes how +strings cross the boundary, so every consumer must be regenerated and every +hand-written producer or consumer updated. There are no compatibility shims. + +### IDL + +- Set `version: "0.10.0"`. +- Map keys can't be floats (`f32`, `f64`); key by an integer, `bool`, + `string`, or C-style enum. +- A qualified type reference (`a.b.T`) must spell the declaring module's + path exactly, and an unknown type name is always an error. +- `cancellable: true` on a synchronous callable is an error + (`CancellableNotAsync`). +- Every C identifier is checked across the whole API, including module path + flattening (`m.x_y` versus `m.x.y`) and async completion types + (`foo_callback`). `SymbolCollision` replaces `AbiSymbolCollision`. + +### C ABI + +- `{prefix}_abi_version()` returns `3`. +- Every symbol, type, constant, and macro carries the library's prefix: + `weaveffi_error` is `{prefix}_error`, `weaveffi_free_bytes` is + `{prefix}_free_bytes`, `WEAVEFFI_API` is `{PREFIX}_API`, and + `weaveffi_kv_Store_open` is `{prefix}_kv_Store_open`. The header is + `{library}.h` instead of `weaveffi.h`, and no `weaveffi_*` aliases remain. +- Strings cross as UTF-8 `(ptr, len)` everywhere: parameters, returns, + callback arguments, async results, and iterator elements. Returned strings + are freed with `{prefix}_free_bytes`; `free_string` is gone. +- Async launchers are named after the function, without the `_async` + suffix. +- New runtime code `-5` (cancelled). Cancel tokens are reference counted: + the launcher takes its own reference, cancelling completes the call with + `-5`, and the consumer may destroy its token at any time. +- Each top-level module exports `{prefix}_{module}_checksum()` and the + header defines `{PREFIX}_{MODULE}_CHECKSUM`. Generated consumers check + both the revision and every checksum at load time. +- New runtime symbol `{prefix}_debug_live`, which a hand-written producer + may implement as returning `0`. + +### Configuration and CLI + +- The C prefix and library name come from the library's identity. + `[global] c_prefix` and every per-target `prefix`, `c_prefix`, and + `input_basename` key are removed. For an IDL, set `[package] c_prefix` or + `[package] library`; for a Rust producer they're the crate's library name + and can't be set. +- Generated packages, modules, namespaces, and error types are named from + the identity; nothing is named after WeaveFFI. Loaders look for + `{library}` and honor `{PREFIX}_LIBRARY` instead of `WEAVEFFI_LIBRARY`. +- New `[project]` table (`input`, `out`, `targets`), so `weaveffi generate` + runs without arguments, and new `weaveffi init`. +- Cache records moved to `{out}/.weaveffi-cache/{target}.json`; generation + rewrites only changed files and deletes files it previously wrote but no + longer produces. +- `weaveffi diff` accepts `--target` and never runs hooks. +- `weaveffi extract --warn` is now `--lenient`. +- `weaveffi package` packages for the host platform unless `--platforms` is + given (it used to default to every platform). + +### Rust producers + +- Symbols start with the crate's library name instead of `weaveffi`; a crate + `kvstore` exports `kvstore_kv_Store_open`. +- The runtime is exported with the `weaveffi::export_runtime!()` proc macro; + `weaveffi_abi::export_runtime!` is gone. +- A `#[weaveffi::error]` enum must implement `Display`, whose output is the + runtime error message. Doc comments remain the documented default. +- A callback-interface method may return `Result` + to receive consumer failures as values. A plain-return method's failure + is deferred only while a WeaveFFI call is active on the thread. +- A reference to a C-style enum, interface, or callback interface declared + in a different `#[weaveffi::module]` root is a compile error; nest the + modules under one root. +- A cancellable function no longer has to notice cancellation: the runtime + drops its future and completes with `-5`. +- The model and generator crates were consolidated: `weaveffi-ir`, + `weaveffi-core`, and `weaveffi-bridge` are now `weaveffi-model`, and the + eleven `weaveffi-gen-*` crates are now `weaveffi-gen`. The runtime's + Rust error type is `weaveffi_abi::FfiError`, and its string helpers work + on `(ptr, len)` runs (`lift_str`, `lower_string`). + +### Samples + +The `inventory` and `shapes` samples were removed; `contacts` covers nested +and sibling modules, and the `shapes` test fixture covers rich enums. diff --git a/docs/src/tutorials/README.md b/docs/src/tutorials/README.md deleted file mode 100644 index a289ccb1..00000000 --- a/docs/src/tutorials/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Tutorials - -Each tutorial follows the same shape: **Goal**, **Prerequisites**, -**Step-by-step**, **Verification**, **Cleanup**, **Next steps**. Pick -the target you're shipping to and follow it end-to-end. - -- [Calculator](calculator.md): fastest path to generate every target, - build the cdylib from the in-tree sample, extract and validate its IDL, - and run small C/Node/Swift consumers against it. -- [Swift iOS](swift.md): Rust → SwiftPM → Xcode iOS app, with a macOS - smoke test first. -- [Kotlin](kotlin.md): Rust → `weaveffi package` → Gradle library module - with the cdylib bundled per Android ABI → Android Studio app, with a - desktop JVM smoke test first. -- [Python](python.md): Rust → ctypes package → `pip install` and - `python demo.py`. -- [Node.js](node.md): Rust → N-API addon → `npm publish` shape. - -The four greeter tutorials share one IDL and one `#[weaveffi::module]` -crate, so you can generate every target from a single source and follow -them in any order. diff --git a/docs/src/tutorials/calculator.md b/docs/src/tutorials/calculator.md deleted file mode 100644 index c740926d..00000000 --- a/docs/src/tutorials/calculator.md +++ /dev/null @@ -1,326 +0,0 @@ -# Calculator end-to-end - -## Goal - -Take the in-tree `samples/calculator` producer (safe Rust annotated with -`#[weaveffi::module]`), generate bindings for every target, build the cdylib, -and run the calculator from a real consumer (C, Node.js, Swift, then optionally -Kotlin and Wasm). By the end you will have produced bindings, executed them on -at least one host, and seen the same Rust `add(a, b)` answer come back through -three different runtimes, plus the typed `CalcError` surface when you divide -by zero. Along the way you'll extract the sample's IDL to YAML and validate -it, which is the same path a hand-written IDL takes. - -## Prerequisites - -- Rust toolchain (stable channel) with `cargo` on `PATH`. -- The WeaveFFI CLI (`cargo install weaveffi-cli` or - `cargo run -p weaveffi-cli --` if you are working in the repo). -- macOS or Linux for the C/Node/Swift steps; Windows works for C and - Node but the Swift step requires macOS. -- For the optional Kotlin and Wasm steps: - - Android Studio with the NDK installed. - - `rustup target add wasm32-unknown-unknown`. - -## Step-by-step - -### 1. Look at the producer - -The whole sample is one annotated module. The `#[weaveffi::error]` enum is -the module's error domain, `div` returns `Result` and so -becomes a `throws` function, and `echo` shows a `String` crossing in both -directions: - -```rust -#[weaveffi::module] -pub mod calculator { - /// The calculator's error domain: the codes its throwing functions report. - #[weaveffi::error] - #[derive(Debug)] - pub enum CalcError { - /// division by zero - DivisionByZero = 1, - } - - /// Add two integers. - #[weaveffi::export] - pub fn add(a: i32, b: i32) -> i32 { - a + b - } - - /// Multiply two integers. - #[weaveffi::export] - pub fn mul(a: i32, b: i32) -> i32 { - a * b - } - - /// Divide two integers, failing on a zero divisor. - #[weaveffi::export] - pub fn div(a: i32, b: i32) -> Result { - if b == 0 { - return Err(CalcError::DivisionByZero); - } - Ok(a / b) - } - - /// Echo a string back to the caller. - #[weaveffi::export] - pub fn echo(s: String) -> String { - s - } -} - -weaveffi::export_runtime!(); -``` - -The `Cargo.toml` builds it as a `cdylib` (plus an `rlib` so the unit tests -can call the thunks) and depends on the `weaveffi` facade crate. The -[Producer Macro](../guides/producer-macro.md) guide covers every attribute. - -### 2. Extract and validate the IDL - -Every generator consumes the IDL, whether it came from annotated Rust or a -YAML file you wrote by hand. Extract the sample's IDL so you can see what the -generators see, then validate it: - -```bash -weaveffi extract samples/calculator/src/lib.rs -o calculator.yml -weaveffi validate calculator.yml -``` - -The second command prints `Validation passed` and a one-line summary -(`1 modules, 4 functions, 0 structs, 0 enums`). The extracted file is the -complete IDL for this module: - -```yaml -version: 0.9.0 -modules: -- name: calculator - functions: - - name: add - params: - - name: a - type: i32 - - name: b - type: i32 - return: i32 - doc: Add two integers. - - name: mul - params: - - name: a - type: i32 - - name: b - type: i32 - return: i32 - doc: Multiply two integers. - - name: div - params: - - name: a - type: i32 - - name: b - type: i32 - return: i32 - doc: Divide two integers, failing on a zero divisor. - throws: true - - name: echo - params: - - name: s - type: string - return: string - doc: Echo a string back to the caller. - errors: - name: CalcError - codes: - - name: DivisionByZero - code: 1 - message: division by zero - doc: division by zero -``` - -If you'd rather author the IDL first and write the Rust afterwards, start -from this file: `weaveffi generate calculator.yml -o generated` produces the -same bindings as the next step, provided the same `weaveffi.toml` sits beside -the YAML (otherwise the package name falls back to the file stem, `calc` for -`calc.yml`, and the loaders look for `libcalc.dylib`). The -[IDL reference](../reference/idl.md) documents every field, and the -[Configuration](../guides/config.md) guide covers `weaveffi.toml`. - -### 3. Generate every target - -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 -``` - -The command finishes with -`Generated artifacts in generated (config: samples/calculator/weaveffi.toml)`. -The output appears under `generated/`, one directory per target. The three -this tutorial exercises: - -- `generated/c`: C header (`weaveffi.h`) and convenience C file -- `generated/swift`: SwiftPM System Library (`CCalculator`) and Swift wrapper - (`Calculator`) -- `generated/node`: N-API addon source, JS loader, `binding.gyp`, and - `types.d.ts` - -The rest (`cpp`, `kotlin`, `wasm`, `python`, `dotnet`, `dart`, `go`, `ruby`) -follow the same pattern; the [generator pages](../generators/README.md) cover -each one. - -### 4. Build the Rust sample - -```bash -cargo build -p calculator -``` - -The cdylib lands in `target/debug/`: - -- macOS: `libcalculator.dylib` -- Linux: `libcalculator.so` -- Windows: `calculator.dll` - -### 5. Run a C consumer - -Write a minimal `main.c` that calls through the generated header. `add` is -non-throwing, so its error slot only trips on a panic or a marshalling -failure; `div` is declared `throws`, so a zero divisor fills `out_err` with -the typed `CalcError` code: - -```c -#include -#include "weaveffi.h" - -int main(void) { - weaveffi_error err = {0}; - printf("2 + 3 = %d\n", weaveffi_calculator_add(2, 3, &err)); - - weaveffi_calculator_div(1, 0, &err); - if (err.code == weaveffi_calculator_CalcError_DivisionByZero) { - printf("div(1, 0) failed: %s\n", err.message); - weaveffi_error_clear(&err); - } - return 0; -} -``` - -Compile and run it from the repo root (on Linux, replace -`DYLD_LIBRARY_PATH` with `LD_LIBRARY_PATH`): - -```bash -cc -I generated/c main.c -L target/debug -lcalculator -o calc_c -DYLD_LIBRARY_PATH=target/debug ./calc_c -``` - -You should see `2 + 3 = 5` followed by `div(1, 0) failed: division by zero`. - -### 6. Run a Node consumer - -The generated `binding.gyp` links against `libweaveffi`, so give the sample -cdylib that name with a symlink, then build the addon in place (`npm install` -runs `node-gyp rebuild`; `LIBRARY_PATH` tells the linker where to find the -alias): - -```bash -ln -sf libcalculator.dylib target/debug/libweaveffi.dylib # .so on Linux -cd generated/node -LIBRARY_PATH="$PWD/../../target/debug" npm install -``` - -Then call the wrapper. Names are camelCase with the module prefix stripped, -and the throwing `div` raises a typed error class: - -```bash -DYLD_LIBRARY_PATH=../../target/debug node -e " -const calc = require('./index.js'); -console.log('2 + 3 =', calc.add(2, 3)); -try { calc.div(1, 0); } catch (e) { console.log(e.name + ':', e.message); } -" -``` - -This prints `2 + 3 = 5` and `DivisionByZeroError: (1) division by zero`. - -### 7. Run a Swift consumer (macOS / Linux) - -Write a `main.swift` at the repo root. The wrapper exposes the module as -a `Calculator` enum namespace, and `div` is `throws`: - -```swift -print("2 + 3 = \(Calculator.add(a: 2, b: 3))") -do { - _ = try Calculator.div(a: 1, b: 0) -} catch { - print("div(1, 0) failed: \(error.localizedDescription)") -} -``` - -Compile the generated wrapper together with your `main.swift` (the module map -also links `libweaveffi`, so this reuses the symlink from step 6): - -```bash -swiftc \ - -I generated/swift/Sources/CCalculator \ - -L target/debug \ - -Xlinker -rpath -Xlinker target/debug \ - generated/swift/Sources/Calculator/Calculator.swift main.swift -o calc_swift -./calc_swift -``` - -On Linux, export `LD_LIBRARY_PATH=target/debug` before running so the loader -resolves the `libweaveffi.so` alias. - -### 8. Optional: Kotlin and Wasm - -- Open `generated/kotlin` in Android Studio and build the `calculator` - library module (that's the `rootProject.name` in the generated - `settings.gradle.kts`). The [Kotlin tutorial](kotlin.md) walks through - cross-compiling the cdylib for Android and wiring the AAR into an app. -- For Wasm, run - `cargo build -p calculator --target wasm32-unknown-unknown --release` and - load `target/wasm32-unknown-unknown/release/calculator.wasm` with - `generated/wasm/weaveffi_wasm.js`; the generated `README.md` beside it has - a loading example. - -## Verification - -You should see the same calculator output from each consumer. Concretely: - -- `weaveffi validate` reports `Validation passed` for the extracted IDL. -- The C consumer prints `2 + 3 = 5` and the typed division error. -- The Node one-liner prints the sum, then `DivisionByZeroError` - from the thrown error class. -- The Swift binary prints the same arithmetic, catches the thrown - `CalcError`, and exits cleanly. - -For fuller consumers that exercise interfaces, callback interfaces, and async -functions, see the `conformance/` directory: each `conformance//` -file is a runnable program against the richer samples (contacts, events, -kvstore, shapes), and `conformance/run.sh` builds and runs them all. - -If the host cannot find the cdylib, you will see -`dyld: Library not loaded` (macOS) or `error while loading shared -libraries` (Linux). Re-export `DYLD_LIBRARY_PATH` / -`LD_LIBRARY_PATH` and rerun. - -## Cleanup - -```bash -rm -rf generated/ calculator.yml main.c main.swift calc_c calc_swift -rm -f target/debug/libweaveffi.dylib # the link alias from step 6 -cargo clean -p calculator -``` - -The `generated/` directory is safe to delete and recreate; nothing -else in the repository depends on its contents. - -## Next steps - -- Walk through the per-target details in - [Generators](../generators/README.md). -- Read the [Memory Ownership](../guides/memory.md) and - [Error Handling](../guides/errors.md) guides for the contracts - every consumer must follow. -- Try a target-specific tutorial: - [Swift iOS](swift.md), [Kotlin](kotlin.md), - [Python](python.md), or [Node.js](node.md). diff --git a/docs/src/tutorials/kotlin.md b/docs/src/tutorials/kotlin.md deleted file mode 100644 index 33637749..00000000 --- a/docs/src/tutorials/kotlin.md +++ /dev/null @@ -1,405 +0,0 @@ -# Kotlin (Android and desktop JVM) - -## Goal - -Build a small Rust greeter library with `#[weaveffi::module]`, generate the -Kotlin/JNI bindings with WeaveFFI, package them as a Gradle library module -with the cross-compiled cdylib bundled per Android ABI, and call them from an -Android Studio app running on an emulator or a physical device. The same -module also runs on a desktop JVM, which is how you'll smoke-test it before -opening Android Studio. - -## Prerequisites - -- [Rust toolchain](https://rustup.rs/) (stable channel). -- Android Studio with the NDK installed (via SDK Manager). -- WeaveFFI CLI (`cargo install weaveffi-cli`). -- The Android Rust targets WeaveFFI packages (`android-arm64` and - `android-x64`; there's no 32-bit platform in the matrix): - - ```bash - rustup target add aarch64-linux-android x86_64-linux-android - ``` - -- For the desktop smoke test: a JDK (17 or newer), CMake 3.22+, and - `kotlinc`. - -## Step-by-step - -### 1. Author the IDL - -Save as `greeter.yml`: - -```yaml -version: "0.9.0" -modules: - - name: greeter - errors: - name: GreeterError - codes: - - { name: UnknownLang, code: 1, message: "unknown language" } - structs: - - name: Greeting - fields: - - { name: message, type: string } - - { name: lang, type: string } - functions: - - name: hello - params: - - { name: name, type: string } - return: string - - name: greeting - throws: true - params: - - { name: name, type: string } - - { name: lang, type: string } - return: Greeting -``` - -`hello` can't fail, so it stays non-throwing. `greeting` declares -`throws: true` and reports codes from the module's `GreeterError` -domain when the language is unknown. Check it: - -```bash -weaveffi validate greeter.yml -``` - -You should see `Validation passed` and `1 modules, 2 functions, 1 structs, -0 enums`. - -Put a `weaveffi.toml` beside it so the package and the native library get a -stable name (otherwise both default to the IDL's file stem, `greeter`): - -```toml -[package] -name = "mygreeter" -version = "0.1.0" -``` - -### 2. Generate bindings - -```bash -weaveffi generate greeter.yml -o generated -``` - -You should see, among other targets: - -```text -generated/ -├── c/ -│ ├── weaveffi.c -│ └── weaveffi.h -└── kotlin/ - ├── build.gradle.kts - ├── settings.gradle.kts - └── src/main/ - ├── kotlin/com/weaveffi/WeaveFFI.kt - └── cpp/ - ├── weaveffi_jni.c - └── CMakeLists.txt -``` - -`WeaveFFI.kt` holds the `WeaveFFI` class (a companion object with one -`@JvmStatic` function per IDL function), the `Greeting` data class, the -`GreeterException` sealed hierarchy, and the value-buffer codec. This bare -output is what you'd read to learn the API; step 6 produces the module you -actually build, with the native libraries bundled in. - -### 3. Implement the Rust library - -```bash -cargo init --lib mygreeter -``` - -`mygreeter/Cargo.toml`: - -```toml -[package] -name = "mygreeter" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -weaveffi = "0.22" -``` - -`mygreeter/src/lib.rs` is plain safe Rust. The macro reads the annotated -items and emits the `extern "C"` thunks that the JNI shim calls, so the -module needs no `unsafe` and no hand-written signatures: - -```rust -#[weaveffi::module] -pub mod greeter { - /// Codes the greeter reports from its throwing functions. - #[weaveffi::error] - #[derive(Debug)] - pub enum GreeterError { - /// unknown language - UnknownLang = 1, - } - - /// A greeting and the language it was rendered in. - #[weaveffi::record] - pub struct Greeting { - pub message: String, - pub lang: String, - } - - /// Greet someone in English. - #[weaveffi::export] - pub fn hello(name: String) -> String { - format!("Hello, {name}!") - } - - /// Greet someone in the given language, failing on an unknown one. - #[weaveffi::export] - pub fn greeting(name: String, lang: String) -> Result { - let message = match lang.as_str() { - "en" => format!("Hello, {name}!"), - "es" => format!("Hola, {name}!"), - "fr" => format!("Bonjour, {name}!"), - _ => return Err(GreeterError::UnknownLang), - }; - Ok(Greeting { message, lang }) - } -} - -weaveffi::export_runtime!(); -``` - -`weaveffi extract mygreeter/src/lib.rs` produces the same IDL as -`greeter.yml` (plus the doc comments), so the two stay in step; you can -generate from either. The [Producer Macro](../guides/producer-macro.md) -guide covers every attribute. - -### 4. Configure the NDK toolchain - -```bash -export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/$(ls $HOME/Library/Android/sdk/ndk | sort -V | tail -1)" -export PATH="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH" -``` - -Replace `darwin-x86_64` with `linux-x86_64` on Linux. Add the matching -`linker = ...` entries to `mygreeter/.cargo/config.toml`. The generated -module sets `minSdk = 33` (its wrappers use `java.lang.ref.Cleaner`), so -pick the API 33 clang wrappers: - -```toml -[target.aarch64-linux-android] -linker = "aarch64-linux-android33-clang" - -[target.x86_64-linux-android] -linker = "x86_64-linux-android33-clang" -``` - -### 5. Cross-compile for every ABI - -```bash -cd mygreeter -cargo build --target aarch64-linux-android --release -cargo build --target x86_64-linux-android --release -``` - -You should now have: - -```text -target/aarch64-linux-android/release/libmygreeter.so -target/x86_64-linux-android/release/libmygreeter.so -``` - -### 6. Package the Gradle module - -`weaveffi package` assembles the Kotlin module with the cdylib bundled under -`src/main/jniLibs//` and a `CMakeLists.txt` that links the JNI shim -against it per ABI. From inside `mygreeter/`, let the CLI run the cross -builds itself: - -```bash -weaveffi package ../greeter.yml --build mygreeter \ - --platforms android-arm64,android-x64 -t kotlin -o ../dist -``` - -It prints one `Building mygreeter for Android arm64 (aarch64-linux-android)...` -line per platform, then: - -```text -Packaging 'mygreeter' for platforms: android-arm64, android-x64 - kotlin: 7 file(s), 2 bundled binary(ies) -Packaged 1 target(s) into ../dist -``` - -If you'd rather build in CI and package afterwards, lay the binaries out as -`prebuilt//libmygreeter.so` and pass `--binaries prebuilt` -instead of `--build`; the [Packaging](../guides/packaging.md) guide has the -full recipe. Either way you get: - -```text -dist/kotlin/ -├── README.md -├── build.gradle.kts -├── settings.gradle.kts -└── src/main/ - ├── kotlin/com/weaveffi/WeaveFFI.kt - ├── cpp/ - │ ├── CMakeLists.txt - │ ├── include/weaveffi.h - │ └── weaveffi_jni.c - └── jniLibs/ - ├── arm64-v8a/libmygreeter.so - └── x86_64/libmygreeter.so -``` - -### 7. Optional: smoke-test on the desktop JVM - -The same module runs on your workstation, which is the quickest way to prove -the Rust and the bindings agree before involving an emulator. Add the host -platform to the package (`darwin-arm64`, `darwin-x64`, or `linux-x64`), then -build the shim against your JDK and run a `main`: - -```bash -weaveffi package ../greeter.yml --build mygreeter \ - --platforms android-arm64,android-x64,darwin-arm64 -t kotlin -o ../dist -cd ../dist/kotlin -cmake -S src/main/cpp -B build -DWEAVEFFI_PLATFORM_ID=darwin-arm64 -cmake --build build -``` - -CMake writes `libmygreeter_jni.dylib` next to the bundled producer library -under `src/main/resources/natives/darwin-arm64/`; at runtime the wrapper -extracts both from the classpath. Save this as `Main.kt`: - -```kotlin -import com.weaveffi.WeaveFFI -import com.weaveffi.GreeterException - -fun main() { - println(WeaveFFI.hello("Android")) - val g = WeaveFFI.greeting("Ada", "es") - println("${g.message} (${g.lang})") - try { - WeaveFFI.greeting("Ada", "tlh") - } catch (e: GreeterException.UnknownLang) { - println("greeting failed: ${e.message} (code ${e.code})") - } -} -``` - -Compile it together with the generated wrapper and run with the resources -directory on the classpath: - -```bash -kotlinc src/main/kotlin/com/weaveffi/WeaveFFI.kt Main.kt -include-runtime -d app.jar -java -cp app.jar:src/main/resources MainKt -``` - -Expected output: - -```text -Hello, Android! -Hola, Ada! (es) -greeting failed: unknown language (code 1) -``` - -### 8. Wire it into Android Studio - -1. Create a new Android project (Empty Activity, Kotlin, `minSdk` 33+; - the generated module won't go lower). -2. Include the packaged module in the root `settings.gradle.kts`: - - ```kotlin - include(":mygreeter") - project(":mygreeter").projectDir = file("../dist/kotlin") - ``` - -3. Add it as a dependency in your app's `build.gradle.kts`: - - ```kotlin - dependencies { - implementation(project(":mygreeter")) - } - ``` - -The module's `build.gradle.kts` applies `com.android.library`, points -`externalNativeBuild` at `src/main/cpp/CMakeLists.txt`, and depends on -`kotlinx-coroutines-core`. The NDK's CMake builds `libmygreeter_jni.so` -for each ABI against the bundled `libmygreeter.so`, so there's nothing to -copy into your app's own `jniLibs/`. - -### 9. Call from Kotlin - -```kotlin -import com.weaveffi.WeaveFFI -import com.weaveffi.GreeterException - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - findViewById(R.id.textView).text = WeaveFFI.hello("Android") - - try { - val g = WeaveFFI.greeting("Hi", "en") - println("${g.message} (${g.lang})") - } catch (e: GreeterException) { - println("greeting failed: ${e.message}") - } - } -} -``` - -The `WeaveFFI` companion object loads `libmygreeter_jni` on first use -(`System.loadLibrary("mygreeter_jni")` on Android) and exposes: - -- `WeaveFFI.hello(name: String): String`: non-throwing. -- `WeaveFFI.greeting(name: String, lang: String): Greeting`: declared - `throws` in the IDL, so the JNI shim raises `GreeterException.UnknownLang` - (a sealed class extending `WeaveFFIException`, one nested class per error - code) on failure. Catch the nested class, the sealed domain, or the - generic base. - -`Greeting` is a plain Kotlin data class decoded from the value buffer the -C ABI returns; there's nothing to close. - -## Verification - -- Sync Gradle in Android Studio. -- Pick an emulator or a connected device and press **Run** (Shift+F10). -- The text view should display `Hello, Android!` and Logcat should - show `Hi (en)` from the `Greeting` block. -- Common error mappings: - - | Symptom | Likely cause | - |----------------------------------------------------|-----------------------------------------------------------------------------| - | `UnsatisfiedLinkError: dlopen failed` | The ABI you're running on isn't in `jniLibs/`; add its platform to `--platforms` and repackage. | - | `WeaveFFIException` from JNI | A WeaveFFI error was raised; inspect the code and message. | - | Linker errors during `cargo build` | `ANDROID_NDK_HOME` is not set or the NDK toolchain is missing from `PATH`. | - | `No implementation found for native method` | JNI symbol names do not match the Kotlin package; re-run `weaveffi package`. | - | `minSdkVersion 33 cannot be smaller than version` | Your app's `minSdk` is below 33; raise it to match the module. | - -## Cleanup - -```bash -rm -rf generated/ dist/ -cargo clean -p mygreeter -``` - -Drop the `include(":mygreeter")` lines from `settings.gradle.kts` and -remove the dependency from your app module if you do not want to keep the -packaged bindings around. - -## Next steps - -- See the [Kotlin generator reference](../generators/kotlin.md) for - the full type mapping and JNI conventions. -- Read [Error Handling](../guides/errors.md): JNI shims convert C - errors to `WeaveFFIException` (or a typed domain exception) - automatically. -- Read [Packaging](../guides/packaging.md) for the full platform matrix - and a CI recipe that builds every platform before packaging. -- Try the [Calculator tutorial](calculator.md) for a simpler - end-to-end walkthrough or [Swift iOS](swift.md) for a sibling - mobile target. diff --git a/docs/src/tutorials/node.md b/docs/src/tutorials/node.md deleted file mode 100644 index 3fdd5c53..00000000 --- a/docs/src/tutorials/node.md +++ /dev/null @@ -1,337 +0,0 @@ -# Node.js npm Package - -## Goal - -Build a small Rust greeter library, generate Node.js bindings with -WeaveFFI, build the N-API addon, and call the bindings from a -JavaScript script. By the end you will have an `npm`-installable -package shape ready to publish. - -## Prerequisites - -- [Rust toolchain](https://rustup.rs/) (stable channel). -- Node.js 16 or later and `npm`. -- WeaveFFI CLI (`cargo install weaveffi-cli`). -- A C compiler in the `PATH` (Xcode CLT on macOS, `build-essential` on - Linux, MSVC build tools on Windows) for the N-API addon build. - -## Step-by-step - -### 1. Author the IDL - -Save as `greeter.yml`: - -```yaml -version: "0.9.0" -modules: - - name: greeter - errors: - name: GreeterError - codes: - - { name: UnknownLang, code: 1, message: "unknown language" } - structs: - - name: Greeting - fields: - - { name: message, type: string } - - { name: lang, type: string } - functions: - - name: hello - params: - - { name: name, type: string } - return: string - - name: greeting - throws: true - params: - - { name: name, type: string } - - { name: lang, type: string } - return: Greeting -``` - -`hello` can't fail, so it stays non-throwing. `greeting` declares -`throws: true` and reports codes from the module's `GreeterError` -domain when the language is unknown. Check it with -`weaveffi validate greeter.yml`; you should see `Validation passed`. - -Put a `weaveffi.toml` beside it so the npm package gets a stable name -(otherwise it defaults to the IDL's file stem, `greeter`): - -```toml -[package] -name = "mygreeter" -version = "0.1.0" -``` - -### 2. Generate bindings - -```bash -weaveffi generate greeter.yml -o generated -``` - -Among other targets you should see: - -```text -generated/ -├── c/ -│ ├── weaveffi.c -│ └── weaveffi.h -└── 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 -runtime to the C ABI, and `binding.gyp` builds it with `node-gyp`; you -don't write any addon code yourself. `package.json` carries the name and -version from `weaveffi.toml` plus an `install` script that runs -`node-gyp rebuild`. - -### 3. Implement the Rust library - -```bash -cargo init --lib mygreeter -``` - -`mygreeter/Cargo.toml`: - -```toml -[package] -name = "mygreeter" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -weaveffi = "0.22" -``` - -`mygreeter/src/lib.rs` is plain safe Rust. The macro reads the annotated -items and emits the `extern "C"` thunks the addon calls, so the module -needs no `unsafe` and no hand-written signatures: - -```rust -#[weaveffi::module] -pub mod greeter { - /// Codes the greeter reports from its throwing functions. - #[weaveffi::error] - #[derive(Debug)] - pub enum GreeterError { - /// unknown language - UnknownLang = 1, - } - - /// A greeting and the language it was rendered in. - #[weaveffi::record] - pub struct Greeting { - pub message: String, - pub lang: String, - } - - /// Greet someone in English. - #[weaveffi::export] - pub fn hello(name: String) -> String { - format!("Hello, {name}!") - } - - /// Greet someone in the given language, failing on an unknown one. - #[weaveffi::export] - pub fn greeting(name: String, lang: String) -> Result { - let message = match lang.as_str() { - "en" => format!("Hello, {name}!"), - "es" => format!("Hola, {name}!"), - "fr" => format!("Bonjour, {name}!"), - _ => return Err(GreeterError::UnknownLang), - }; - Ok(Greeting { message, lang }) - } -} - -weaveffi::export_runtime!(); -``` - -`weaveffi extract mygreeter/src/lib.rs` produces the same IDL as -`greeter.yml` (plus the doc comments), so the two stay in step; you can -generate from either. The [Producer Macro](../guides/producer-macro.md) -guide covers every attribute. - -### 4. Build the cdylib and the N-API addon - -```bash -cargo build -p mygreeter --release -``` - -The generated `binding.gyp` links against `libweaveffi`, so give the -cdylib that name with a symlink, then build the addon in place -(`npm install` runs `node-gyp rebuild`; `LIBRARY_PATH` tells the -linker where to find the alias): - -```bash -ln -sf libmygreeter.dylib target/release/libweaveffi.dylib # .so on Linux -cd generated/node -LIBRARY_PATH="$PWD/../../target/release" npm install -``` - -The compiled addon lands at `build/Release/weaveffi.node`, which is -the first place the generated `index.js` looks. You can also point -the loader at any built addon with the `WEAVEFFI_ADDON` environment -variable, or ship a prebuilt binary as `index.node` next to -`index.js` (the fallback location). - -### 5. Run the bindings locally - -Save as `generated/node/demo.js`. Function names are camelCase with -the module prefix stripped, and the throwing `greeting` raises typed -error classes (`GreeterError` extends `WeaveFFIError`, with an -`UnknownLangError` subclass per code). Every error carries `code`, the -bare producer message in `errorMessage`, and `(code) message` in the -standard `message`: - -```javascript -const greeter = require("./index"); - -console.log(greeter.hello("Node")); - -try { - const g = greeter.greeting("Node", "en"); - console.log(`${g.message} (${g.lang})`); - greeter.greeting("Node", "tlh"); -} catch (e) { - if (e instanceof greeter.GreeterError) { - console.log(`${e.name}: ${e.errorMessage}`); - } else { - throw e; - } -} -``` - -Run it (the cdylib must be on the loader path so the addon's -`libweaveffi` reference resolves): - -macOS: - -```bash -cd generated/node -DYLD_LIBRARY_PATH=../../target/release node demo.js -``` - -Linux: - -```bash -cd generated/node -LD_LIBRARY_PATH=../../target/release node demo.js -``` - -Expected output: - -```text -Hello, Node! -Hello, Node! (en) -UnknownLangError: unknown language -``` - -For TypeScript consumers, the generated `types.d.ts` is enough: - -```typescript -import * as greeter from "./index"; - -const msg: string = greeter.hello("TypeScript"); -const g: greeter.Greeting = greeter.greeting("TS", "en"); -console.log(`${g.message} (${g.lang})`); -``` - -### 6. Prepare for publishing - -Copy the built addon to the fallback location the loader checks, so -consumers don't need node-gyp: - -```bash -cp build/Release/weaveffi.node index.node -``` - -Then edit `generated/node/package.json`. The generated file sets -`"gypfile": true` and an `install` script that rebuilds the addon; drop -both so consumers use the prebuilt `index.node`, and add a `files` list: - -```json -{ - "name": "@myorg/greeter", - "version": "0.1.0", - "main": "index.js", - "types": "types.d.ts", - "files": [ - "index.js", - "index.node", - "types.d.ts" - ], - "os": ["darwin", "linux"], - "cpu": ["x64", "arm64"] -} -``` - -`files` must include `index.node`. For multi-platform packages, publish -per-platform optional dependencies instead. `weaveffi package` automates -that shape: pass it prebuilt cdylibs per platform and it emits the main -package plus one `npm/mygreeter--/` package per platform, each -gated by npm `os`/`cpu` so only the matching one installs; see -[Packaging](../guides/packaging.md). - -### 7. Publish - -```bash -cd generated/node -npm pack -npm publish -``` - -For scoped packages, append `--access public`. Consumers then run: - -```bash -npm install @myorg/greeter -``` - -```javascript -const { hello } = require("@myorg/greeter"); -console.log(hello("npm")); -``` - -## Verification - -- `node demo.js` prints the three lines above and exits with code `0`. -- `npm pack` produces a `.tgz` containing `index.node`, - `types.d.ts`, and `index.js`. -- TypeScript consumers see the `Greeting` interface and `hello` - signature without manual type declarations. -- Common error mappings: - - | Symptom | Likely cause | - |----------------------------------------------------------|-------------------------------------------------------------------------------| - | `Error: Cannot find module './index.node'` | The addon isn't built; run `npm install` or set `WEAVEFFI_ADDON`. | - | `Error: dlopen ... not found` | Cdylib not on the loader path; set `DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH`. | - | `TypeError: greeter.hello is not a function` | The addon is stale; rerun `npm install` after IDL edits. | - | `WeaveFFIError` with a negative code | A producer panic (-2) or marshalling failure (-3); see [Error Handling](../guides/errors.md). | - | Crashes on `require()` | Addon built for the wrong Node.js version or architecture; rebuild. | - -## Cleanup - -```bash -rm -rf generated/ -rm -f target/release/libweaveffi.dylib # the link alias from step 4 -cargo clean -p mygreeter -``` - -If you published a test version, mark it as deprecated with -`npm deprecate @myorg/greeter@0.1.0 "test publish"`. - -## Next steps - -- See the [Node generator reference](../generators/node.md) for the - full type mapping and `types.d.ts` layout. -- Read [Memory Ownership](../guides/memory.md) for buffered value and - interface lifetime semantics. -- Try the [Calculator tutorial](calculator.md) for a simpler - end-to-end walkthrough or [Python](python.md) for a sibling - scripting target. diff --git a/docs/src/tutorials/python.md b/docs/src/tutorials/python.md deleted file mode 100644 index 319bd512..00000000 --- a/docs/src/tutorials/python.md +++ /dev/null @@ -1,280 +0,0 @@ -# Python Package - -## Goal - -Build a small Rust greeter library, generate Python ctypes bindings -with WeaveFFI, install the package locally, and call it from a Python -script. - -## Prerequisites - -- [Rust toolchain](https://rustup.rs/) (stable channel). -- Python 3.8 or later (`python3 --version`). -- WeaveFFI CLI (`cargo install weaveffi-cli`). -- `pip` (ships with Python). - -## Step-by-step - -### 1. Author the IDL - -Save as `greeter.yml`: - -```yaml -version: "0.9.0" -modules: - - name: greeter - errors: - name: GreeterError - codes: - - { name: UnknownLang, code: 1, message: "unknown language" } - structs: - - name: Greeting - fields: - - { name: message, type: string } - - { name: lang, type: string } - functions: - - name: hello - params: - - { name: name, type: string } - return: string - - name: greeting - throws: true - params: - - { name: name, type: string } - - { name: lang, type: string } - return: Greeting -``` - -`hello` can't fail, so it stays non-throwing. `greeting` declares -`throws: true` and reports codes from the module's `GreeterError` -domain when the language is unknown. Check it with -`weaveffi validate greeter.yml`; you should see `Validation passed`. - -Put a `weaveffi.toml` beside it so the Python distribution and import -package get a stable name (otherwise both default to the IDL's file stem, -`greeter`): - -```toml -[package] -name = "mygreeter" -version = "0.1.0" -``` - -### 2. Generate bindings - -```bash -weaveffi generate greeter.yml -o generated -``` - -Among other targets, you should see: - -```text -generated/ -├── c/ -│ ├── weaveffi.c -│ └── weaveffi.h -└── python/ - ├── pyproject.toml - ├── setup.py - ├── README.md - └── mygreeter/ - ├── __init__.py - ├── weaveffi.py - └── weaveffi.pyi -``` - -The package directory and distribution name follow the `[package]` name -from `weaveffi.toml`. The Python target uses ctypes: no native extension to -compile on the Python side. - -### 3. Implement the Rust library - -```bash -cargo init --lib mygreeter -``` - -`mygreeter/Cargo.toml`: - -```toml -[package] -name = "mygreeter" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -weaveffi = "0.22" -``` - -`mygreeter/src/lib.rs` is plain safe Rust. The macro reads the annotated -items and emits the `extern "C"` thunks the ctypes loader binds to, so the -module needs no `unsafe` and no hand-written signatures: - -```rust -#[weaveffi::module] -pub mod greeter { - /// Codes the greeter reports from its throwing functions. - #[weaveffi::error] - #[derive(Debug)] - pub enum GreeterError { - /// unknown language - UnknownLang = 1, - } - - /// A greeting and the language it was rendered in. - #[weaveffi::record] - pub struct Greeting { - pub message: String, - pub lang: String, - } - - /// Greet someone in English. - #[weaveffi::export] - pub fn hello(name: String) -> String { - format!("Hello, {name}!") - } - - /// Greet someone in the given language, failing on an unknown one. - #[weaveffi::export] - pub fn greeting(name: String, lang: String) -> Result { - let message = match lang.as_str() { - "en" => format!("Hello, {name}!"), - "es" => format!("Hola, {name}!"), - "fr" => format!("Bonjour, {name}!"), - _ => return Err(GreeterError::UnknownLang), - }; - Ok(Greeting { message, lang }) - } -} - -weaveffi::export_runtime!(); -``` - -`weaveffi extract mygreeter/src/lib.rs` produces the same IDL as -`greeter.yml` (plus the doc comments), so the two stay in step; you can -generate from either. The [Producer Macro](../guides/producer-macro.md) -guide covers every attribute. - -### 4. Build the cdylib - -```bash -cargo build -p mygreeter --release -``` - -Produces: - -| Platform | Output | -|----------|-----------------------------------------| -| macOS | `target/release/libmygreeter.dylib` | -| Linux | `target/release/libmygreeter.so` | -| Windows | `target/release/mygreeter.dll` | - -### 5. Install the Python package - -```bash -cd generated/python -pip install . -``` - -Use `pip install -e .` for an editable install during development. - -### 6. Make the cdylib findable - -The simplest option on any platform is the `WEAVEFFI_LIBRARY` -environment variable, which the generated loader checks first and -treats as an explicit path: - -```bash -WEAVEFFI_LIBRARY=target/release/libmygreeter.dylib python demo.py -``` - -Without the override, the loader looks for `libweaveffi.dylib` (macOS), -`libweaveffi.so` (Linux), or `weaveffi.dll` (Windows) on the system -loader path. Symlink or copy your cdylib to the expected name and set -the loader path. - -macOS: - -```bash -ln -sf libmygreeter.dylib target/release/libweaveffi.dylib -DYLD_LIBRARY_PATH=target/release python demo.py -``` - -Linux: - -```bash -ln -sf libmygreeter.so target/release/libweaveffi.so -LD_LIBRARY_PATH=target/release python demo.py -``` - -Windows: place `weaveffi.dll` next to your script or add its -directory to `PATH`. - -### 7. Use the bindings - -Save as `demo.py`. Function names are snake_case with the module -prefix stripped, and the throwing `greeting` raises the typed -exception hierarchy (`GreeterError` extends `WeaveFFIError`, with an -`UnknownLang` subclass per code, also reachable as -`GreeterError.UnknownLang`): - -```python -from mygreeter import hello, greeting, GreeterError - -print(hello("Python")) - -try: - g = greeting("Python", "en") - print(f"{g.message} ({g.lang})") - greeting("Python", "tlh") -except GreeterError as e: - print(f"Error {e.code}: {e.message}") -``` - -`Greeting` is a plain Python dataclass decoded from the value buffer -the C ABI returns; there's no native allocation to manage. - -## Verification - -- `pip show mygreeter` lists the package. -- Running `demo.py` prints: - - ```text - Hello, Python! - Hello, Python! (en) - Error 1: unknown language - ``` - -- `mypy demo.py` reports no errors thanks to the generated `weaveffi.pyi` - stub, which the package ships alongside a PEP 561 `py.typed` marker so the - installed wheel is typed too. -- Common error mappings: - - | Symptom | Likely cause | - |-----------------------------------------------------------|-------------------------------------------------------------------------------| - | `OSError: dlopen ... not found` | Cdylib not on the loader path; set `WEAVEFFI_LIBRARY` or the loader path. | - | `mygreeter.weaveffi.UnknownLang: (1) unknown language` | Rust reported a domain error code; inspect `e.code` and `e.message`. | - | `WeaveFFIError` with a negative code | A producer panic (-2) or marshalling failure (-3); see [Error Handling](../guides/errors.md). | - | `ModuleNotFoundError: No module named 'mygreeter'` | Package not installed; rerun `pip install .` from `generated/python/`. | - | mypy says the module is untyped | Reinstall from a fresh `generated/python/` so `py.typed` and `weaveffi.pyi` ship. | - -## Cleanup - -```bash -pip uninstall mygreeter -rm -rf generated/ -rm -f target/release/libweaveffi.dylib # the link alias from step 6 -cargo clean -p mygreeter -``` - -## Next steps - -- See the [Python generator reference](../generators/python.md) for - the full type mapping and memory contract. -- Read [Error Handling](../guides/errors.md) for the cross-target - error model. -- Try the [Calculator tutorial](calculator.md) for a simpler - end-to-end walkthrough or [Node.js](node.md) for a sibling - scripting target. diff --git a/docs/src/tutorials/swift.md b/docs/src/tutorials/swift.md deleted file mode 100644 index bc7e3aa4..00000000 --- a/docs/src/tutorials/swift.md +++ /dev/null @@ -1,328 +0,0 @@ -# Swift iOS App - -## Goal - -Build a small Rust greeter library, generate Swift bindings with -WeaveFFI, and call them from a SwiftUI iOS app running in the -simulator. A macOS command-line smoke test along the way proves the -Rust and the bindings agree before Xcode enters the picture. - -## Prerequisites - -- [Rust toolchain](https://rustup.rs/) (stable channel). -- Xcode 15 or later with the iOS SDK installed. -- WeaveFFI CLI (`cargo install weaveffi-cli`). -- iOS Rust targets: - - ```bash - rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios - ``` - -## Step-by-step - -### 1. Author the IDL - -Save as `greeter.yml`: - -```yaml -version: "0.9.0" -modules: - - name: greeter - errors: - name: GreeterError - codes: - - { name: UnknownLang, code: 1, message: "unknown language" } - structs: - - name: Greeting - fields: - - { name: message, type: string } - - { name: lang, type: string } - functions: - - name: hello - params: - - { name: name, type: string } - return: string - - name: greeting - throws: true - params: - - { name: name, type: string } - - { name: lang, type: string } - return: Greeting -``` - -`hello` can't fail, so it stays non-throwing. `greeting` declares -`throws: true` and reports codes from the module's `GreeterError` -domain when the language is unknown. Check it with -`weaveffi validate greeter.yml`; you should see `Validation passed`. - -Put a `weaveffi.toml` beside it so the Swift package gets a stable name -(otherwise it defaults to the IDL's file stem, `greeter`): - -```toml -[package] -name = "mygreeter" -version = "0.1.0" -``` - -### 2. Generate bindings - -```bash -weaveffi generate greeter.yml -o generated -``` - -You should see, among other targets: - -```text -generated/ -├── c/ -│ ├── weaveffi.c -│ └── weaveffi.h -└── swift/ - ├── Package.swift - └── Sources/ - ├── CMygreeter/ - │ └── module.modulemap - └── Mygreeter/ - └── Mygreeter.swift -``` - -The package name and both target names are the capitalized `[package]` -name: `CMygreeter` is the SwiftPM system library that wraps -`generated/c/weaveffi.h`, and `Mygreeter` is the Swift wrapper you -`import`. `Package.swift` declares iOS 13, macOS 10.15, tvOS 13, and -watchOS 6 as the minimum platforms. - -### 3. Implement the Rust library - -```bash -cargo init --lib mygreeter -``` - -`mygreeter/Cargo.toml`: - -```toml -[package] -name = "mygreeter" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["staticlib", "cdylib"] - -[dependencies] -weaveffi = "0.22" -``` - -The `staticlib` is what the iOS app links; the `cdylib` is for the macOS -smoke test in step 5. `mygreeter/src/lib.rs` is plain safe Rust. The macro -reads the annotated items and emits the `extern "C"` thunks the Swift -wrapper calls, so the module needs no `unsafe` and no hand-written -signatures: - -```rust -#[weaveffi::module] -pub mod greeter { - /// Codes the greeter reports from its throwing functions. - #[weaveffi::error] - #[derive(Debug)] - pub enum GreeterError { - /// unknown language - UnknownLang = 1, - } - - /// A greeting and the language it was rendered in. - #[weaveffi::record] - pub struct Greeting { - pub message: String, - pub lang: String, - } - - /// Greet someone in English. - #[weaveffi::export] - pub fn hello(name: String) -> String { - format!("Hello, {name}!") - } - - /// Greet someone in the given language, failing on an unknown one. - #[weaveffi::export] - pub fn greeting(name: String, lang: String) -> Result { - let message = match lang.as_str() { - "en" => format!("Hello, {name}!"), - "es" => format!("Hola, {name}!"), - "fr" => format!("Bonjour, {name}!"), - _ => return Err(GreeterError::UnknownLang), - }; - Ok(Greeting { message, lang }) - } -} - -weaveffi::export_runtime!(); -``` - -`weaveffi extract mygreeter/src/lib.rs` produces the same IDL as -`greeter.yml` (plus the doc comments), so the two stay in step; you can -generate from either. The [Producer Macro](../guides/producer-macro.md) -guide covers every attribute. - -### 4. Build for iOS targets - -```bash -cargo build -p mygreeter --target aarch64-apple-ios --release -cargo build -p mygreeter --target aarch64-apple-ios-sim --release -cargo build -p mygreeter --target x86_64-apple-ios --release -``` - -The generated module map autolinks a library called `weaveffi` -(`link "weaveffi"`), so name the archives you hand to Xcode -`libweaveffi.a`. Combine the simulator architectures with `lipo` and -bundle everything in an `XCFramework` so Xcode can pick the right slice -automatically: - -```bash -mkdir -p target/universal-ios-sim/release -lipo -create \ - target/aarch64-apple-ios-sim/release/libmygreeter.a \ - target/x86_64-apple-ios/release/libmygreeter.a \ - -output target/universal-ios-sim/release/libweaveffi.a -cp target/aarch64-apple-ios/release/libmygreeter.a \ - target/aarch64-apple-ios/release/libweaveffi.a - -xcodebuild -create-xcframework \ - -library target/aarch64-apple-ios/release/libweaveffi.a \ - -headers generated/c/ \ - -library target/universal-ios-sim/release/libweaveffi.a \ - -headers generated/c/ \ - -output MyGreeter.xcframework -``` - -### 5. Optional: smoke-test on macOS - -Before opening Xcode, compile the generated wrapper together with a -`main.swift` against the host cdylib. Build it, give it the `libweaveffi` -name the module map links, and run: - -```bash -cargo build -p mygreeter --release -ln -sf libmygreeter.dylib target/release/libweaveffi.dylib -``` - -`main.swift`: - -```swift -print(Greeter.hello(name: "Swift")) -do { - let g = try Greeter.greeting(name: "Ada", lang: "fr") - print("\(g.message) (\(g.lang))") - _ = try Greeter.greeting(name: "Ada", lang: "tlh") -} catch let e as GreeterError { - print("greeting failed: \(e.localizedDescription) (code \(e.errorCode))") -} -``` - -```bash -swiftc \ - -I generated/swift/Sources/CMygreeter \ - -L target/release \ - -Xlinker -rpath -Xlinker target/release \ - generated/swift/Sources/Mygreeter/Mygreeter.swift main.swift -o greet -./greet -``` - -Expected output: - -```text -Hello, Swift! -Bonjour, Ada! (fr) -greeting failed: unknown language (code 1) -``` - -### 6. Wire it into Xcode - -1. Create a new iOS App in Xcode (SwiftUI or UIKit). -2. Drag `MyGreeter.xcframework` into the project navigator. Confirm it - appears under **Build Phases > Link Binary With Libraries**. -3. **File > Add Package Dependencies > Add Local…** and pick - `generated/swift/`. The package contributes the `CMygreeter` and - `Mygreeter` targets; add the `Mygreeter` library product to your app. -4. **Build Settings > Header Search Paths**: add the path to - `generated/c/` (e.g. `$(SRCROOT)/../generated/c`), which the module - map's relative `header` path resolves against. -5. **Build Phases > Dependencies**: ensure `Mygreeter` is listed. - -### 7. Call from Swift - -```swift -import SwiftUI -import Mygreeter - -struct ContentView: View { - @State private var greeting = "" - - var body: some View { - VStack { - Text(greeting) - Button("Greet") { - greeting = Greeter.hello(name: "Swift") - } - Button("Greet in French") { - do { - greeting = try Greeter.greeting(name: "Swift", lang: "fr").message - } catch { - greeting = error.localizedDescription - } - } - } - .padding() - } -} -``` - -The generated `Mygreeter` module exposes: - -- `Greeter.hello(name:)`: non-throwing, returns `String`. -- `Greeter.greeting(name:lang:)`: declared `throws` in the IDL, so - the Swift wrapper is `throws` and surfaces `GreeterError`; returns - a `Greeting` value with `.message` and `.lang` properties. -- `GreeterError`: the module's error domain as a Swift `enum` - conforming to `Error` and `LocalizedError`, one case per code - (`.unknownLang(message:)`) plus an `errorCode` accessor. Runtime traps - (a producer panic or a marshalling failure) arrive as the generic - `WeaveFFIError.error(code:message:)` instead. -- `Greeting`: a plain Swift struct decoded from the value buffer the - C ABI returns, with a public memberwise initializer. - -## Verification - -- Select an iOS Simulator target and press **Cmd+R**. -- Tap **Greet** in the running app; the label changes to - `Hello, Swift!`. **Greet in French** changes it to `Bonjour, Swift!`. -- Re-run on a physical device after building for `aarch64-apple-ios` - to confirm the device path also works. -- Common error mappings: - - | Symptom | Likely cause | - |---------------------------------------------------|------------------------------------------------------------------------------| - | `Undefined symbols for architecture arm64` | Static library not linked, or it isn't named `libweaveffi.a`. | - | `Module 'CMygreeter' not found` | Header search path does not point at `generated/c/`. | - | `No such module 'Mygreeter'` | Local Swift package not added under **Add Package Dependencies > Add Local…**.| - | Crash when running on Intel simulator | Build for `x86_64-apple-ios` and combine with `lipo`. | - -## Cleanup - -```bash -rm -rf generated/ MyGreeter.xcframework main.swift greet -rm -f target/release/libweaveffi.dylib # the link alias from step 5 -cargo clean -p mygreeter -``` - -Remove the `MyGreeter.xcframework` reference from the Xcode project -and undo the **Header Search Paths** edit. - -## Next steps - -- See the [Swift generator reference](../generators/swift.md) for the - full type mapping. -- Read the [Memory Ownership](../guides/memory.md) guide to understand - buffered value and interface lifetime rules. -- Try the [Calculator tutorial](calculator.md) for a simpler - end-to-end walkthrough or [Kotlin](kotlin.md) for a JVM target. diff --git a/justfile b/justfile index 297c593d..9655f9ff 100644 --- a/justfile +++ b/justfile @@ -2,33 +2,32 @@ build: cargo build --release -p weaveffi-cli -# Generate bindings from the calculator sample -generate: build - cargo run -p weaveffi-cli -- generate samples/calculator/src/lib.rs -o generated - -# Run all tests +# Run every test once (fails on snapshot drift) test: - cargo test --workspace + cargo insta test --workspace --check + +# Accept intentional snapshot changes after reviewing them +snapshots: + cargo insta test --workspace --accept -# Check formatting and lints (clippy -D warnings also enforces the doc lints: -# missing_docs, missing_errors_doc, missing_panics_doc, missing_safety_doc, -# and doc_markdown) +# Formatting, clippy (which also enforces the doc lints), and rustdoc check: cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings + RUSTDOCFLAGS="-D warnings -D rustdoc::all -D rustdoc::missing_crate_level_docs" cargo doc --workspace --no-deps + +# Run the end-to-end conformance harness (ONLY=python just conformance) +conformance: + bash conformance/run.sh -# Build the Rust API docs (rustdoc) and the mdBook site. Uses the same -# RUSTDOCFLAGS as the CI rustdoc job so broken intra-doc links and missing -# crate-level docs fail locally too. -doc: - RUSTDOCFLAGS="-D rustdoc::all -D rustdoc::missing_crate_level_docs" cargo doc --workspace --no-deps +# Compile-check every snapshot fixture with each language's toolchain +fixtures *targets: + bash scripts/check-fixtures.sh {{targets}} + +# Build the mdBook site +docs: mdbook build docs # Format all code fmt: cargo fmt --all - -# Clean generated output and build artifacts -clean: - rm -rf generated/ - cargo clean diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 318d92ba..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6344 +0,0 @@ -{ - "name": "weaveffi", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "devDependencies": { - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/exec": "^7.0.3", - "@semantic-release/git": "^10.0.1", - "conventional-changelog-conventionalcommits": "^8.0.0", - "semantic-release": "^24.2.1" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/core": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", - "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.3", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/endpoint": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", - "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/graphql": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", - "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", - "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.2.1.tgz", - "integrity": "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^15.0.1" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", - "integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.2.tgz", - "integrity": "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^26.0.0" - } - }, - "node_modules/@octokit/plugin-retry": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.0.tgz", - "integrity": "sha512-O1FZgXeiGb2sowEr/hYTr6YunGdSAFWnr2fyW39Ah85H8O33ELASQxcvOFF5LE6Tjekcyu2ms4qAzJVhSaJxTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=7" - } - }, - "node_modules/@octokit/plugin-throttling": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.3.tgz", - "integrity": "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": "^7.0.0" - } - }, - "node_modules/@octokit/request": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", - "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^11.0.3", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "fast-content-type-parse": "^3.0.0", - "json-with-bigint": "^3.5.3", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", - "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/types": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", - "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^27.0.0" - } - }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "4.2.10" - }, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true, - "license": "ISC" - }, - "node_modules/@pnpm/npm-conf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", - "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@semantic-release/changelog": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", - "integrity": "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@semantic-release/error": "^3.0.0", - "aggregate-error": "^3.0.0", - "fs-extra": "^11.0.0", - "lodash": "^4.17.4" - }, - "engines": { - "node": ">=14.17" - }, - "peerDependencies": { - "semantic-release": ">=18.0.0" - } - }, - "node_modules/@semantic-release/commit-analyzer": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-13.0.1.tgz", - "integrity": "sha512-wdnBPHKkr9HhNhXOhZD5a2LNl91+hs8CC2vsAVYxtZH3y0dV3wKn+uZSN61rdJQZ8EGxzWB3inWocBHV9+u/CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "conventional-changelog-angular": "^8.0.0", - "conventional-changelog-writer": "^8.0.0", - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.0.0", - "debug": "^4.0.0", - "import-from-esm": "^2.0.0", - "lodash-es": "^4.17.21", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=20.1.0" - } - }, - "node_modules/@semantic-release/error": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", - "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.17" - } - }, - "node_modules/@semantic-release/exec": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@semantic-release/exec/-/exec-7.1.0.tgz", - "integrity": "sha512-4ycZ2atgEUutspPZ2hxO6z8JoQt4+y/kkHvfZ1cZxgl9WKJId1xPj+UadwInj+gMn2Gsv+fLnbrZ4s+6tK2TFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@semantic-release/error": "^4.0.0", - "aggregate-error": "^3.0.0", - "debug": "^4.0.0", - "execa": "^9.0.0", - "lodash-es": "^4.17.21", - "parse-json": "^8.0.0" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=24.1.0" - } - }, - "node_modules/@semantic-release/exec/node_modules/@semantic-release/error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", - "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@semantic-release/git": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", - "integrity": "sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@semantic-release/error": "^3.0.0", - "aggregate-error": "^3.0.0", - "debug": "^4.0.0", - "dir-glob": "^3.0.0", - "execa": "^5.0.0", - "lodash": "^4.17.4", - "micromatch": "^4.0.0", - "p-reduce": "^2.0.0" - }, - "engines": { - "node": ">=14.17" - }, - "peerDependencies": { - "semantic-release": ">=18.0.0" - } - }, - "node_modules/@semantic-release/git/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/@semantic-release/git/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/git/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@semantic-release/git/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/git/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@semantic-release/git/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@semantic-release/git/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@semantic-release/github": { - "version": "11.0.6", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-11.0.6.tgz", - "integrity": "sha512-ctDzdSMrT3H+pwKBPdyCPty6Y47X8dSrjd3aPZ5KKIKKWTwZBE9De8GtsH3TyAlw3Uyo2stegMx6rJMXKpJwJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/core": "^7.0.0", - "@octokit/plugin-paginate-rest": "^13.0.0", - "@octokit/plugin-retry": "^8.0.0", - "@octokit/plugin-throttling": "^11.0.0", - "@semantic-release/error": "^4.0.0", - "aggregate-error": "^5.0.0", - "debug": "^4.3.4", - "dir-glob": "^3.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "issue-parser": "^7.0.0", - "lodash-es": "^4.17.21", - "mime": "^4.0.0", - "p-filter": "^4.0.0", - "tinyglobby": "^0.2.14", - "url-join": "^5.0.0" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=24.1.0" - } - }, - "node_modules/@semantic-release/github/node_modules/@semantic-release/error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", - "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@semantic-release/github/node_modules/aggregate-error": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", - "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^5.2.0", - "indent-string": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/github/node_modules/clean-stack": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", - "integrity": "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/github/node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-12.0.2.tgz", - "integrity": "sha512-+M9/Lb35IgnlUO6OSJ40Ie+hUsZLuph2fqXC/qrKn0fMvUU/jiCjpoL6zEm69vzcmaZJ8yNKtMBEKHWN49WBbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@semantic-release/error": "^4.0.0", - "aggregate-error": "^5.0.0", - "execa": "^9.0.0", - "fs-extra": "^11.0.0", - "lodash-es": "^4.17.21", - "nerf-dart": "^1.0.0", - "normalize-url": "^8.0.0", - "npm": "^10.9.3", - "rc": "^1.2.8", - "read-pkg": "^9.0.0", - "registry-auth-token": "^5.0.0", - "semver": "^7.1.2", - "tempy": "^3.0.0" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=20.1.0" - } - }, - "node_modules/@semantic-release/npm/node_modules/@semantic-release/error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", - "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@semantic-release/npm/node_modules/aggregate-error": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", - "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^5.2.0", - "indent-string": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/clean-stack": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", - "integrity": "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/release-notes-generator": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.1.0.tgz", - "integrity": "sha512-CcyDRk7xq+ON/20YNR+1I/jP7BYKICr1uKd1HHpROSnnTdGqOTburi4jcRiTYz0cpfhxSloQO3cGhnoot7IEkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "conventional-changelog-angular": "^8.0.0", - "conventional-changelog-writer": "^8.0.0", - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.0.0", - "debug": "^4.0.0", - "get-stream": "^7.0.0", - "import-from-esm": "^2.0.0", - "into-stream": "^7.0.0", - "lodash-es": "^4.17.21", - "read-package-up": "^11.0.0" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=20.1.0" - } - }, - "node_modules/@semantic-release/release-notes-generator/node_modules/get-stream": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", - "integrity": "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://ko-fi.com/dangreen" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/argv-formatter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz", - "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", - "dev": true, - "license": "MIT" - }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true, - "license": "MIT" - }, - "node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bottleneck": { - "version": "2.19.5", - "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "dev": true, - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cli-highlight/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cli-highlight/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/conventional-changelog-angular": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.0.tgz", - "integrity": "sha512-DOuBwYSqWzfwuRByY9O4oOIvDlkUCTDzfbOgcSbkY+imXXj+4tmrEFao3K+FxemClYfYnZzsvudbwrhje9VHDA==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-8.0.0.tgz", - "integrity": "sha512-eOvlTO6OcySPyyyk8pKz2dP4jjElYunj9hn9/s0OB+gapTO8zwS9UQWrZ1pmF2hFs3vw1xhonOLGcGjy/zgsuA==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-writer": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.4.0.tgz", - "integrity": "sha512-HHBFkk1EECxxmCi4CTu091iuDpQv5/OavuCUAuZmrkWpmYfyD816nom1CvtfXJ/uYfAAjavgHvXHX291tSLK8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-libs/stream-utils": "^1.2.0", - "conventional-commits-filter": "^5.0.0", - "handlebars": "^4.7.7", - "meow": "^13.0.0", - "semver": "^7.5.2" - }, - "bin": { - "conventional-changelog-writer": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-commits-filter": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz", - "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-commits-parser": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.3.0.tgz", - "integrity": "sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-libs/stream-utils": "^1.2.0", - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/convert-hrtime": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", - "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/emojilib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/env-ci": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.2.0.tgz", - "integrity": "sha512-D5kWfzkmaOQDioPmiviWAVtKmpPT4/iJmMVQxWxMPJTFyTkdc5JQUfc5iXEeWxcOdsYTKSAiA/Age4NUOqKsRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^8.0.0", - "java-properties": "^1.0.2" - }, - "engines": { - "node": "^18.17 || >=20.6.1" - } - }, - "node_modules/env-ci/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/env-ci/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/env-ci/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", - "figures": "^6.1.0", - "get-stream": "^9.0.0", - "human-signals": "^8.0.1", - "is-plain-obj": "^4.1.0", - "is-stream": "^4.0.1", - "npm-run-path": "^6.0.0", - "pretty-ms": "^9.2.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": "^18.19.0 || >=20.5.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", - "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/find-up-simple": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-versions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz", - "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver-regex": "^4.0.5", - "super-regex": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/function-timeout": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", - "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/git-log-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.1.tgz", - "integrity": "sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "argv-formatter": "~1.0.0", - "spawn-error-forwarder": "~1.0.0", - "split2": "~1.0.0", - "stream-combiner2": "~1.1.1", - "through2": "~2.0.0", - "traverse": "0.6.8" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/hook-std": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-4.0.0.tgz", - "integrity": "sha512-IHI4bEVOt3vRUDJ+bFA9VUJlo7SzvFARPNLw75pqSmAOP2HmTWfFJtPvLBrDrlgjEYXY9zs7SFdHPQaJShkSCQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-from-esm": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-2.0.0.tgz", - "integrity": "sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "import-meta-resolve": "^4.0.0" - }, - "engines": { - "node": ">=18.20" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/index-to-position": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/into-stream": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", - "integrity": "sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/issue-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", - "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.capitalize": "^4.2.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.uniqby": "^4.7.0" - }, - "engines": { - "node": "^18.17 || >=20.6.1" - } - }, - "node_modules/java-properties": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", - "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-with-bigint": { - "version": "3.5.8", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", - "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.capitalize": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", - "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniqby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/make-asynchronous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", - "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-event": "^6.0.0", - "type-fest": "^4.6.0", - "web-worker": "^1.5.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "dev": true, - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/marked-terminal": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", - "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "ansi-regex": "^6.1.0", - "chalk": "^5.4.1", - "cli-highlight": "^2.1.11", - "cli-table3": "^0.6.5", - "node-emoji": "^2.2.0", - "supports-hyperlinks": "^3.1.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "marked": ">=1 <16" - } - }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", - "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa" - ], - "license": "MIT", - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nerf-dart": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", - "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-emoji": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-url": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", - "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm": { - "version": "10.9.7", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.7.tgz", - "integrity": "sha512-17u9+Ssv6as3iua2l6abTv1H4TtQDhle/Qn+XJ4TjKR4SzIjk1Ox3SZXRVBUW48KojLttHNQUk/U00m7sh1OGw==", - "bundleDependencies": [ - "@isaacs/string-locale-compare", - "@npmcli/arborist", - "@npmcli/config", - "@npmcli/fs", - "@npmcli/map-workspaces", - "@npmcli/package-json", - "@npmcli/promise-spawn", - "@npmcli/redact", - "@npmcli/run-script", - "@sigstore/tuf", - "abbrev", - "archy", - "cacache", - "chalk", - "ci-info", - "cli-columns", - "fastest-levenshtein", - "fs-minipass", - "glob", - "graceful-fs", - "hosted-git-info", - "ini", - "init-package-json", - "is-cidr", - "json-parse-even-better-errors", - "libnpmaccess", - "libnpmdiff", - "libnpmexec", - "libnpmfund", - "libnpmhook", - "libnpmorg", - "libnpmpack", - "libnpmpublish", - "libnpmsearch", - "libnpmteam", - "libnpmversion", - "make-fetch-happen", - "minimatch", - "minipass", - "minipass-pipeline", - "ms", - "node-gyp", - "nopt", - "normalize-package-data", - "npm-audit-report", - "npm-install-checks", - "npm-package-arg", - "npm-pick-manifest", - "npm-profile", - "npm-registry-fetch", - "npm-user-validate", - "p-map", - "pacote", - "parse-conflict-json", - "proc-log", - "qrcode-terminal", - "read", - "semver", - "spdx-expression-parse", - "ssri", - "supports-color", - "tar", - "text-table", - "tiny-relative-date", - "treeverse", - "validate-npm-package-name", - "which", - "write-file-atomic" - ], - "dev": true, - "license": "Artistic-2.0", - "workspaces": [ - "docs", - "smoke-tests", - "mock-globals", - "mock-registry", - "workspaces/*" - ], - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^8.0.4", - "@npmcli/config": "^9.0.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^4.0.2", - "@npmcli/package-json": "^6.2.0", - "@npmcli/promise-spawn": "^8.0.3", - "@npmcli/redact": "^3.2.2", - "@npmcli/run-script": "^9.1.0", - "@sigstore/tuf": "^3.1.1", - "abbrev": "^3.0.1", - "archy": "~1.0.0", - "cacache": "^19.0.1", - "chalk": "^5.6.2", - "ci-info": "^4.4.0", - "cli-columns": "^4.0.0", - "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.3", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "hosted-git-info": "^8.1.0", - "ini": "^5.0.0", - "init-package-json": "^7.0.2", - "is-cidr": "^5.1.1", - "json-parse-even-better-errors": "^4.0.0", - "libnpmaccess": "^9.0.0", - "libnpmdiff": "^7.0.4", - "libnpmexec": "^9.0.4", - "libnpmfund": "^6.0.4", - "libnpmhook": "^11.0.0", - "libnpmorg": "^7.0.0", - "libnpmpack": "^8.0.4", - "libnpmpublish": "^10.0.2", - "libnpmsearch": "^8.0.0", - "libnpmteam": "^7.0.0", - "libnpmversion": "^7.0.0", - "make-fetch-happen": "^14.0.3", - "minimatch": "^9.0.9", - "minipass": "^7.1.3", - "minipass-pipeline": "^1.2.4", - "ms": "^2.1.2", - "node-gyp": "^11.5.0", - "nopt": "^8.1.0", - "normalize-package-data": "^7.0.1", - "npm-audit-report": "^6.0.0", - "npm-install-checks": "^7.1.2", - "npm-package-arg": "^12.0.2", - "npm-pick-manifest": "^10.0.0", - "npm-profile": "^11.0.1", - "npm-registry-fetch": "^18.0.2", - "npm-user-validate": "^3.0.0", - "p-map": "^7.0.4", - "pacote": "^19.0.1", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "qrcode-terminal": "^0.12.0", - "read": "^4.1.0", - "semver": "^7.7.4", - "spdx-expression-parse": "^4.0.0", - "ssri": "^12.0.0", - "supports-color": "^9.4.0", - "tar": "^7.5.11", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "treeverse": "^3.0.0", - "validate-npm-package-name": "^6.0.2", - "which": "^5.0.0", - "write-file-atomic": "^6.0.0" - }, - "bin": { - "npm": "bin/npm-cli.js", - "npx": "bin/npx-cli.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/npm/node_modules/@isaacs/string-locale-compare": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/agent": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "8.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/metavuln-calculator": "^8.0.0", - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.1", - "@npmcli/query": "^4.0.0", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", - "bin-links": "^5.0.0", - "cacache": "^19.0.1", - "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "json-stringify-nice": "^1.1.4", - "lru-cache": "^10.2.2", - "minimatch": "^9.0.4", - "nopt": "^8.0.0", - "npm-install-checks": "^7.1.0", - "npm-package-arg": "^12.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.1", - "pacote": "^19.0.0", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "proggy": "^3.0.0", - "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^3.0.1", - "promise-retry": "^2.0.1", - "read-package-json-fast": "^4.0.0", - "semver": "^7.3.7", - "ssri": "^12.0.0", - "treeverse": "^3.0.0", - "walk-up-path": "^3.0.1" - }, - "bin": { - "arborist": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/config": { - "version": "9.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", - "ci-info": "^4.0.0", - "ini": "^5.0.0", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "walk-up-path": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/fs": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/git": { - "version": "6.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^8.0.0", - "ini": "^5.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "4.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "glob": "^10.2.2", - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "8.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "cacache": "^19.0.0", - "json-parse-even-better-errors": "^4.0.0", - "pacote": "^20.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": { - "version": "20.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^9.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", - "ssri": "^12.0.0", - "tar": "^7.5.10" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "6.2.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", - "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "4.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/redact": { - "version": "3.2.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^11.0.0", - "proc-log": "^5.0.0", - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.4.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/core": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.4.3", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "make-fetch-happen": "^14.0.2", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.4.1", - "tuf-js": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/verify": { - "version": "2.1.1", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/abbrev": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/agent-base": { - "version": "7.1.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-styles": { - "version": "6.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/aproba": { - "version": "2.1.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/archy": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/bin-links": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^7.0.0", - "npm-normalize-package-bin": "^4.0.0", - "proc-log": "^5.0.0", - "read-cmd-shim": "^5.0.0", - "write-file-atomic": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/binary-extensions": { - "version": "2.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/cacache": { - "version": "19.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/chalk": { - "version": "5.6.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm/node_modules/chownr": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/ci-info": { - "version": "4.4.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/cidr-regex": { - "version": "4.1.3", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "^5.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/cmd-shim": { - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/npm/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cross-spawn": { - "version": "7.0.6", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/debug": { - "version": "4.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/diff": { - "version": "5.2.2", - "dev": true, - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/npm/node_modules/eastasianwidth": { - "version": "0.2.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/npm/node_modules/env-paths": { - "version": "2.2.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/exponential-backoff": { - "version": "3.1.3", - "dev": true, - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/npm/node_modules/fastest-levenshtein": { - "version": "1.0.16", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/npm/node_modules/fdir": { - "version": "6.5.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/foreground-child": { - "version": "3.3.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/glob": { - "version": "10.5.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/graceful-fs": { - "version": "4.2.11", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/hosted-git-info": { - "version": "8.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.2.0", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/http-proxy-agent": { - "version": "7.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/https-proxy-agent": { - "version": "7.0.6", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/ignore-walk": { - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/npm/node_modules/ini": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/init-package-json": { - "version": "7.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/package-json": "^6.0.0", - "npm-package-arg": "^12.0.0", - "promzard": "^2.0.0", - "read": "^4.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/ip-address": { - "version": "10.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/npm/node_modules/ip-regex": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/is-cidr": { - "version": "5.1.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "cidr-regex": "^4.1.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/jackspeak": { - "version": "3.4.3", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/json-stringify-nice": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/jsonparse": { - "version": "1.3.1", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff-apply": { - "version": "5.5.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/libnpmaccess": { - "version": "9.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff": { - "version": "7.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^8.0.4", - "@npmcli/installed-package-contents": "^3.0.0", - "binary-extensions": "^2.3.0", - "diff": "^5.1.0", - "minimatch": "^9.0.4", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0", - "tar": "^7.5.11" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmexec": { - "version": "9.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^8.0.4", - "@npmcli/run-script": "^9.0.1", - "ci-info": "^4.0.0", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0", - "proc-log": "^5.0.0", - "read": "^4.0.0", - "read-package-json-fast": "^4.0.0", - "semver": "^7.3.7", - "walk-up-path": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmfund": { - "version": "6.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^8.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmhook": { - "version": "11.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmorg": { - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmpack": { - "version": "8.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^8.0.4", - "@npmcli/run-script": "^9.0.1", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmpublish": { - "version": "10.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "ci-info": "^4.0.0", - "normalize-package-data": "^7.0.0", - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1", - "proc-log": "^5.0.0", - "semver": "^7.3.7", - "sigstore": "^3.0.0", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmsearch": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmteam": { - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmversion": { - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.1", - "@npmcli/run-script": "^9.0.1", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/lru-cache": { - "version": "10.4.3", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/make-fetch-happen": { - "version": "14.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/minimatch": { - "version": "9.0.9", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/minipass": { - "version": "7.1.3", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-collect": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-fetch": { - "version": "4.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/minipass-pipeline": { - "version": "1.2.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/minizlib": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/mute-stream": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/negotiator": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/node-gyp": { - "version": "11.5.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "tinyglobby": "^0.2.12", - "which": "^5.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/nopt": { - "version": "8.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/normalize-package-data": { - "version": "7.0.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^8.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-audit-report": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-bundled": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.2", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-package-arg": { - "version": "12.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-packlist": { - "version": "9.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^7.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "10.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^7.1.0", - "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-profile": { - "version": "11.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "18.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/redact": "^3.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-user-validate": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/p-map": { - "version": "7.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/package-json-from-dist": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/npm/node_modules/pacote": { - "version": "19.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^9.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", - "ssri": "^12.0.0", - "tar": "^7.5.10" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/parse-conflict-json": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "just-diff": "^6.0.0", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/path-scurry": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/picomatch": { - "version": "4.0.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/proc-log": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/proggy": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/promise-all-reject-late": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-call-limit": { - "version": "3.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/promzard": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "read": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/qrcode-terminal": { - "version": "0.12.0", - "dev": true, - "inBundle": true, - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/npm/node_modules/read": { - "version": "4.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "mute-stream": "^2.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-cmd-shim": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-package-json-fast": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm/node_modules/safer-buffer": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true - }, - "node_modules/npm/node_modules/semver": { - "version": "7.7.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/signal-exit": { - "version": "4.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/sigstore": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "@sigstore/sign": "^3.1.0", - "@sigstore/tuf": "^3.1.0", - "@sigstore/verify": "^2.1.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/smart-buffer": { - "version": "4.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks": { - "version": "2.8.7", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.2.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.5.0", - "dev": true, - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.23", - "dev": true, - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/npm/node_modules/ssri": { - "version": "12.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/supports-color": { - "version": "9.4.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/npm/node_modules/tar": { - "version": "7.5.11", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tiny-relative-date": { - "version": "1.3.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.15", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/npm/node_modules/treeverse": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "3.0.1", - "debug": "^4.4.1", - "make-fetch-happen": "^14.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-filename": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/walk-up-path": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/which": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/which/node_modules/isexe": { - "version": "3.1.5", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/wrap-ansi": { - "version": "8.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/write-file-atomic": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/yallist": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-each-series": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", - "integrity": "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-event": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", - "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-timeout": "^6.1.2" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-filter": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", - "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-map": "^7.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-reduce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", - "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-timeout": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", - "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "dev": true, - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", - "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^2.0.0", - "load-json-file": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true, - "license": "ISC" - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/registry-auth-token": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", - "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pnpm/npm-conf": "^3.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/semantic-release": { - "version": "24.2.9", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.9.tgz", - "integrity": "sha512-phCkJ6pjDi9ANdhuF5ElS10GGdAKY6R1Pvt9lT3SFhOwM4T7QZE7MLpBDbNruUx/Q3gFD92/UOFringGipRqZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@semantic-release/commit-analyzer": "^13.0.0-beta.1", - "@semantic-release/error": "^4.0.0", - "@semantic-release/github": "^11.0.0", - "@semantic-release/npm": "^12.0.2", - "@semantic-release/release-notes-generator": "^14.0.0-beta.1", - "aggregate-error": "^5.0.0", - "cosmiconfig": "^9.0.0", - "debug": "^4.0.0", - "env-ci": "^11.0.0", - "execa": "^9.0.0", - "figures": "^6.0.0", - "find-versions": "^6.0.0", - "get-stream": "^6.0.0", - "git-log-parser": "^1.2.0", - "hook-std": "^4.0.0", - "hosted-git-info": "^8.0.0", - "import-from-esm": "^2.0.0", - "lodash-es": "^4.17.21", - "marked": "^15.0.0", - "marked-terminal": "^7.3.0", - "micromatch": "^4.0.2", - "p-each-series": "^3.0.0", - "p-reduce": "^3.0.0", - "read-package-up": "^11.0.0", - "resolve-from": "^5.0.0", - "semver": "^7.3.2", - "semver-diff": "^5.0.0", - "signale": "^1.2.1", - "yargs": "^17.5.1" - }, - "bin": { - "semantic-release": "bin/semantic-release.js" - }, - "engines": { - "node": ">=20.8.1" - } - }, - "node_modules/semantic-release/node_modules/@semantic-release/error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", - "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/semantic-release/node_modules/aggregate-error": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", - "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^5.2.0", - "indent-string": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/clean-stack": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", - "integrity": "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/p-reduce": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", - "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-5.0.0.tgz", - "integrity": "sha512-0HbGtOm+S7T6NGQ/pxJSJipJvc4DK3FcRVMRkhsIwJDJ4Jcz5DQC1cPPzB5GhzyHjwttW878HaWQq46CkL3cqg==", - "deprecated": "Deprecated as the semver package now supports this built-in.", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver-regex": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", - "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/signale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", - "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^2.3.2", - "figures": "^2.0.0", - "pkg-conf": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/signale/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/signale/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/signale/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/signale/node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/skin-tone": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", - "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-emoji-modifier-base": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spawn-error-forwarder": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", - "integrity": "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==", - "dev": true, - "license": "MIT" - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/split2": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz", - "integrity": "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==", - "dev": true, - "license": "ISC", - "dependencies": { - "through2": "~2.0.0" - } - }, - "node_modules/stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/super-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz", - "integrity": "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-timeout": "^1.0.1", - "make-asynchronous": "^1.0.1", - "time-span": "^5.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", - "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=14.18" - }, - "funding": { - "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" - } - }, - "node_modules/temp-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/tempy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.2.0.tgz", - "integrity": "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^3.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/time-span": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", - "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "convert-hrtime": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/traverse": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", - "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unicode-emoji-modifier-base": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", - "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", - "dev": true, - "license": "ISC" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/url-join": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", - "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/web-worker": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", - "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index ffbacd73..00000000 --- a/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "private": true, - "devDependencies": { - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/exec": "^7.0.3", - "@semantic-release/git": "^10.0.1", - "conventional-changelog-conventionalcommits": "^8.0.0", - "semantic-release": "^24.2.1" - } -} diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 00000000..dd1d543b --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,54 @@ +# Releases: release-plz opens (and keeps updating) a release PR from the +# Conventional Commits on `main`. CI runs on that PR like any other; merging +# it publishes every crate at the new shared version, tags `v{version}`, and +# creates the GitHub release, which triggers the binary build in +# .github/workflows/release-binaries.yml. +[workspace] +# Every published crate shares one version. +changelog_update = false +git_tag_enable = false +git_release_enable = false +semver_check = false +dependencies_update = false +pr_labels = ["release"] +release_always = false + +[changelog] +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^revert", group = "Reverts" }, + { message = "^(chore|ci|build|docs|style|test|refactor)", skip = true }, +] + +[[package]] +name = "weaveffi-model" +version_group = "weaveffi" + +[[package]] +name = "weaveffi-abi" +version_group = "weaveffi" + +[[package]] +name = "weaveffi-macros" +version_group = "weaveffi" + +[[package]] +name = "weaveffi" +version_group = "weaveffi" + +[[package]] +name = "weaveffi-gen" +version_group = "weaveffi" + +[[package]] +name = "weaveffi-cli" +version_group = "weaveffi" +changelog_update = true +changelog_path = "CHANGELOG.md" +changelog_include = ["weaveffi-model", "weaveffi-abi", "weaveffi-macros", "weaveffi", "weaveffi-gen"] +git_tag_enable = true +git_tag_name = "v{{ version }}" +git_release_enable = true +git_release_name = "v{{ version }}" diff --git a/samples/async-demo/Cargo.toml b/samples/async-demo/Cargo.toml index 0250e71a..06d72635 100644 --- a/samples/async-demo/Cargo.toml +++ b/samples/async-demo/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -weaveffi = { path = "../../crates/weaveffi" } +weaveffi = { path = "../../crates/weaveffi", features = ["leak-check"] } [lints] workspace = true diff --git a/samples/async-demo/src/lib.rs b/samples/async-demo/src/lib.rs index 1e00ed12..73a3fcc6 100644 --- a/samples/async-demo/src/lib.rs +++ b/samples/async-demo/src/lib.rs @@ -2,15 +2,23 @@ //! code generation across all targets. //! //! The producer writes plain `async fn`s; the `#[weaveffi::module]` expansion -//! emits the `_async` launcher for each (running the future to completion on a -//! worker thread, then firing the host completion callback). A small RAII -//! `ActiveGuard` keeps `active_callbacks` honest: it counts task bodies that -//! are in flight and returns to zero once every spawned body has completed. +//! emits a launcher for each (running the future to completion on a worker +//! thread, then firing the host completion callback exactly once). A small +//! RAII `ActiveGuard` keeps `active_callbacks` honest: it counts task bodies +//! that are in flight and returns to zero once every spawned body has +//! completed or been cancelled. +//! +//! `wait` is cancellable for real: it waits on a timer until its timeout, +//! and cancelling its token completes the call with the cancelled code (`-5`) +//! right away, dropping the pending timer. /// Async/await and cancellation demo across WeaveFFI's async-capable targets. #[weaveffi::module] pub mod tasks { + use std::future::Future; + use std::pin::Pin; use std::sync::atomic::{AtomicI64, Ordering}; + use std::task::{Context, Poll}; /// The task module's error domain. #[weaveffi::error] @@ -20,6 +28,12 @@ pub mod tasks { InvalidName = 1, } + impl std::fmt::Display for TaskError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("task name must not be empty") + } + } + static NEXT_TASK_ID: AtomicI64 = AtomicI64::new(1); static ACTIVE_CALLBACKS: AtomicI64 = AtomicI64::new(0); @@ -88,12 +102,75 @@ pub mod tasks { .collect() } - /// Best-effort cancel of a task by id. This demo has no long-running work - /// to interrupt, so it always reports "not cancelled". + /// A timer future that needs no async runtime: the first poll starts a + /// thread that sleeps until the deadline and then wakes the task. + struct Delay { + #[cfg(not(target_arch = "wasm32"))] + deadline: std::time::Instant, + waker: Option>>, + } + + impl Delay { + fn new(ms: u64) -> Self { + // `wasm32-unknown-unknown` has no clock (`Instant::now` traps), so + // there the delay completes on its first poll. + #[cfg(target_arch = "wasm32")] + let _ = ms; + Self { + #[cfg(not(target_arch = "wasm32"))] + deadline: std::time::Instant::now() + std::time::Duration::from_millis(ms), + waker: None, + } + } + } + + impl Future for Delay { + type Output = (); + + #[cfg(target_arch = "wasm32")] + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> { + // `wasm32-unknown-unknown` has neither threads nor a clock to + // sleep on, so the timeout elapses at once there. + let _ = &self.waker; + Poll::Ready(()) + } + + #[cfg(not(target_arch = "wasm32"))] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + if std::time::Instant::now() >= self.deadline { + return Poll::Ready(()); + } + match &self.waker { + // Keep the waker current in case the executor moved the task. + Some(slot) => cx.waker().clone_into(&mut slot.lock().unwrap()), + None => { + let slot = std::sync::Arc::new(std::sync::Mutex::new(cx.waker().clone())); + let deadline = self.deadline; + let wake = std::sync::Arc::clone(&slot); + std::thread::spawn(move || { + std::thread::sleep( + deadline.saturating_duration_since(std::time::Instant::now()), + ); + wake.lock().unwrap().wake_by_ref(); + }); + self.waker = Some(slot); + } + } + Poll::Pending + } + } + + /// Wait `timeout_ms` milliseconds, then complete with the milliseconds + /// waited. Cancelling the token first completes the call with the + /// cancelled code instead: the runtime drops this future, timer and all. #[weaveffi::export] - pub fn cancel_task(id: i64) -> bool { - let _ = id; - false + #[weaveffi::cancellable] + pub async fn wait(timeout_ms: i64, cancel: weaveffi::CancelToken) -> i64 { + let _guard = ActiveGuard::new(); + let _ = cancel; + let ms = u64::try_from(timeout_ms).unwrap_or(0); + Delay::new(ms).await; + timeout_ms.max(0) } /// Complete immediately with `n`. Drives the async stress examples, which @@ -119,90 +196,95 @@ weaveffi::export_runtime!(); #[allow(unsafe_code)] mod tests { use crate::tasks::*; - use std::ffi::CString; use std::os::raw::c_void; use std::sync::mpsc; - use std::time::Duration; - use weaveffi::abi::{self, weaveffi_error}; + use std::time::{Duration, Instant}; + use weaveffi::abi::{self, FfiError}; type TaskCbMsg = (i32, Option); type BatchCbMsg = (bool, Vec); - /// A buffered async result arrives as a borrowed `(ptr, len)` value - /// buffer the launcher frees after the callback returns, so the callback - /// decodes it before sending. + /// The error code of a consumer-owned async error, which is released. + fn take_code(err: *mut FfiError) -> i32 { + if err.is_null() { + return 0; + } + let code = unsafe { (*err).code }; + unsafe { crate::async_demo_error_free(err) }; + code + } + + /// A buffered async result is an owned `(ptr, len)` value buffer: decode + /// it, then release it with `{prefix}_free_bytes`. + fn take_buffer(ptr: *const u8, len: usize) -> Option { + if ptr.is_null() { + return None; + } + let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; + let value = abi::decode_value::(bytes).expect("well-formed value buffer"); + unsafe { crate::async_demo_free_bytes(ptr.cast_mut(), len) }; + Some(value) + } + extern "C" fn task_callback( context: *mut c_void, - err: *mut weaveffi_error, + err: *mut FfiError, result_ptr: *const u8, result_len: usize, ) { let tx = unsafe { &*(context as *const mpsc::Sender) }; - let code = if err.is_null() { - 0 - } else { - unsafe { (*err).code } - }; - let result = if result_ptr.is_null() { - None - } else { - let bytes = unsafe { std::slice::from_raw_parts(result_ptr, result_len) }; - Some(abi::decode_value::(bytes).expect("well-formed TaskResult buffer")) - }; - let _ = tx.send((code, result)); + let _ = tx.send((take_code(err), take_buffer(result_ptr, result_len))); } extern "C" fn batch_callback( context: *mut c_void, - err: *mut weaveffi_error, + err: *mut FfiError, results_ptr: *const u8, results_len: usize, ) { let tx = unsafe { &*(context as *const mpsc::Sender) }; - let had_error = !err.is_null() && unsafe { (*err).code } != 0; - // Decode the borrowed value buffer before returning; the launcher - // frees it once the callback completes. - let results = if results_ptr.is_null() { - Vec::new() - } else { - let bytes = unsafe { std::slice::from_raw_parts(results_ptr, results_len) }; - abi::decode_value::>(bytes).expect("well-formed batch buffer") - }; + let had_error = take_code(err) != 0; + let results = take_buffer(results_ptr, results_len).unwrap_or_default(); let _ = tx.send((had_error, results)); } - extern "C" fn n_tasks_callback(context: *mut c_void, err: *mut weaveffi_error, result: i32) { - let tx = unsafe { &*(context as *const mpsc::Sender<(bool, i32)>) }; - let had_error = !err.is_null() && unsafe { (*err).code } != 0; - let _ = tx.send((had_error, result)); + extern "C" fn i32_callback(context: *mut c_void, err: *mut FfiError, result: i32) { + let tx = unsafe { &*(context as *const mpsc::Sender<(i32, i32)>) }; + let _ = tx.send((take_code(err), result)); + } + + extern "C" fn i64_callback(context: *mut c_void, err: *mut FfiError, result: i64) { + let tx = unsafe { &*(context as *const mpsc::Sender<(i32, i64)>) }; + let _ = tx.send((take_code(err), result)); } /// Intentionally leak a callback-context box. /// - /// The `#[weaveffi::module]` async launchers invoke the completion callback - /// on a detached worker thread, so a worker may still be inside the - /// callback's `send` when the test's `recv` returns (the receiver unblocks - /// as soon as the message is queued, before `send` finishes). Reclaiming the - /// box here would free the `Sender` out from under that in-flight `send`, a - /// use-after-free. The test deliberately leaks the context instead, which - /// keeps the channel alive for the brief remaining life of any in-flight - /// callback; the OS reclaims the memory at process exit. + /// The completion callback runs on a worker thread, so a worker may still + /// be inside the callback's `send` when the test's `recv` returns (the + /// receiver unblocks as soon as the message is queued, before `send` + /// finishes). Reclaiming the box here would free the `Sender` out from + /// under that in-flight `send`, so the test leaks the context instead; + /// the OS reclaims the memory at process exit. fn leak_ctx(ptr: *mut T) { std::mem::forget(unsafe { Box::from_raw(ptr) }); } - #[test] - fn run_task_calls_callback() { + fn run_task(name: &str) -> TaskCbMsg { let (tx, rx) = mpsc::channel::(); let tx_ptr = Box::into_raw(Box::new(tx)); - let name = CString::new("test-task").unwrap(); - - weaveffi_tasks_run_task_async(name.as_ptr(), task_callback, tx_ptr as *mut c_void); - - let (code, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); + unsafe { + async_demo_tasks_run_task(name.as_ptr(), name.len(), task_callback, tx_ptr.cast()) + }; + let out = rx.recv_timeout(Duration::from_secs(5)).unwrap(); leak_ctx(tx_ptr); - assert_eq!(code, 0); + out + } + #[test] + fn run_task_calls_callback() { + let (code, result) = run_task("test-task"); + assert_eq!(code, 0); let r = result.expect("success path passes a result buffer"); assert!(r.id > 0); assert!(r.success); @@ -211,142 +293,145 @@ mod tests { #[test] fn run_task_empty_name_reports_invalid_name() { - let (tx, rx) = mpsc::channel::(); - let tx_ptr = Box::into_raw(Box::new(tx)); - - let empty = CString::new("").unwrap(); - weaveffi_tasks_run_task_async(empty.as_ptr(), task_callback, tx_ptr as *mut c_void); - - let (code, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - leak_ctx(tx_ptr); + let (code, result) = run_task(""); assert_eq!(code, 1, "TaskError::InvalidName's declared code"); assert!(result.is_none(), "error path passes a null result buffer"); } #[test] - fn run_task_null_name_reports_marshal_error_through_the_callback() { + fn run_task_invalid_name_reports_marshal_error_through_the_callback() { let (tx, rx) = mpsc::channel::(); let tx_ptr = Box::into_raw(Box::new(tx)); - // The launcher has no out_err slot, so an argument that fails to lift - // is reported through the completion callback with the reserved - // marshalling code, exactly like the sync path would report it. - weaveffi_tasks_run_task_async(std::ptr::null(), task_callback, tx_ptr as *mut c_void); - + // (a null pointer with a length) is reported through the completion + // callback with the reserved marshalling code. + unsafe { async_demo_tasks_run_task(std::ptr::null(), 3, task_callback, tx_ptr.cast()) }; let (code, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); leak_ctx(tx_ptr); assert_eq!(code, abi::MARSHAL_ERROR_CODE); assert!(result.is_none()); } - #[test] - fn run_batch_processes_sequentially() { + fn run_batch(names: &[&str]) -> BatchCbMsg { let (tx, rx) = mpsc::channel::(); let tx_ptr = Box::into_raw(Box::new(tx)); - // The list-of-strings parameter is buffered: encode `Vec` - // and pass the (ptr, len) pair. The launcher copies the bytes before - // returning, so the local buffer only needs to outlive the call. - let names = abi::encode_value(&vec![ - "task-a".to_string(), - "task-b".to_string(), - "task-c".to_string(), - ]); - - weaveffi_tasks_run_batch_async( - names.as_ptr(), - names.len(), - batch_callback, - tx_ptr as *mut c_void, - ); - - let (had_error, results) = rx.recv_timeout(Duration::from_secs(10)).unwrap(); + // The list-of-strings parameter is buffered: the launcher copies the + // bytes before returning, so the local buffer only needs to outlive + // the call. + let names = abi::encode_value(&names.iter().map(|n| n.to_string()).collect::>()); + unsafe { + async_demo_tasks_run_batch(names.as_ptr(), names.len(), batch_callback, tx_ptr.cast()) + }; + let out = rx.recv_timeout(Duration::from_secs(10)).unwrap(); leak_ctx(tx_ptr); + out + } + + #[test] + fn run_batch_processes_sequentially() { + let (had_error, results) = run_batch(&["task-a", "task-b", "task-c"]); assert!(!had_error); assert_eq!(results.len(), 3); - - for r in &results { - assert!(r.id > 0); - assert!(r.success); - } - + assert!(results.iter().all(|r| r.id > 0 && r.success)); assert!(results[0].value.contains("task-a")); assert!(results[2].value.contains("task-c")); } #[test] fn run_batch_empty_names() { - let (tx, rx) = mpsc::channel::(); - let tx_ptr = Box::into_raw(Box::new(tx)); - - let names = abi::encode_value(&Vec::::new()); - weaveffi_tasks_run_batch_async( - names.as_ptr(), - names.len(), - batch_callback, - tx_ptr as *mut c_void, - ); - - let (had_error, results) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - leak_ctx(tx_ptr); + let (had_error, results) = run_batch(&[]); assert!(!had_error); assert!(results.is_empty()); } #[test] - fn cancel_task_returns_false() { - let mut err = weaveffi_error::default(); - let cancelled = weaveffi_tasks_cancel_task(42, &mut err); - assert_eq!(err.code, 0); - assert!(!cancelled); + fn task_result_buffer_round_trip() { + for result in [ + TaskResult { + id: 42, + value: "hello".to_string(), + success: true, + }, + TaskResult { + id: 1, + value: "fail".to_string(), + success: false, + }, + ] { + let back = abi::decode_value::(&abi::encode_value(&result)).unwrap(); + assert_eq!(back.id, result.id); + assert_eq!(back.value, result.value); + assert_eq!(back.success, result.success); + } } #[test] - fn task_result_buffer_round_trip() { - // `TaskResult` crosses the ABI as a value buffer; the macro - // implements `BufferValue`, so every field round-trips. - let result = TaskResult { - id: 42, - value: "hello".to_string(), - success: true, - }; - let bytes = abi::encode_value(&result); - let back = abi::decode_value::(&bytes).unwrap(); - assert_eq!(back.id, 42); - assert_eq!(back.value, "hello"); - assert!(back.success); + fn run_n_tasks_invokes_callback_with_n() { + let (tx, rx) = mpsc::channel::<(i32, i32)>(); + let tx_ptr = Box::into_raw(Box::new(tx)); + unsafe { async_demo_tasks_run_n_tasks(7, i32_callback, tx_ptr.cast()) }; + assert_eq!(rx.recv_timeout(Duration::from_secs(5)).unwrap(), (0, 7)); + leak_ctx(tx_ptr); + } + + fn launch_wait(timeout_ms: i64, token: *mut abi::FfiCancelToken) -> mpsc::Receiver<(i32, i64)> { + let (tx, rx) = mpsc::channel::<(i32, i64)>(); + let tx_ptr = Box::into_raw(Box::new(tx)); + unsafe { async_demo_tasks_wait(timeout_ms, token, i64_callback, tx_ptr.cast()) }; + leak_ctx(tx_ptr); + rx } #[test] - fn task_result_round_trips_success_false() { - let result = TaskResult { - id: 1, - value: "fail".to_string(), - success: false, - }; - let bytes = abi::encode_value(&result); - let back = abi::decode_value::(&bytes).unwrap(); - assert!(!back.success); + fn wait_completes_after_its_timeout() { + let token = crate::async_demo_cancel_token_create(); + let rx = launch_wait(20, token); + assert_eq!(rx.recv_timeout(Duration::from_secs(5)).unwrap(), (0, 20)); + unsafe { crate::async_demo_cancel_token_destroy(token) }; } #[test] - fn run_n_tasks_invokes_callback_with_n() { - let (tx, rx) = mpsc::channel::<(bool, i32)>(); - let tx_ptr = Box::into_raw(Box::new(tx)); - weaveffi_tasks_run_n_tasks_async(7, n_tasks_callback, tx_ptr as *mut c_void); - let (had_error, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - leak_ctx(tx_ptr); - assert!(!had_error); - assert_eq!(result, 7); + fn cancelling_wait_completes_with_the_cancelled_code() { + let token = crate::async_demo_cancel_token_create(); + let start = Instant::now(); + let rx = launch_wait(60_000, token); + std::thread::sleep(Duration::from_millis(20)); + unsafe { + crate::async_demo_cancel_token_cancel(token); + crate::async_demo_cancel_token_destroy(token); + } + let (code, result) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); + assert_eq!(code, abi::CANCELLED_ERROR_CODE); + assert_eq!(result, 0); + assert!( + start.elapsed() < Duration::from_secs(5), + "didn't wait for the timeout" + ); + assert!( + rx.recv_timeout(Duration::from_millis(100)).is_err(), + "completes once" + ); + } + + #[test] + fn a_token_cancelled_before_launch_never_runs_the_body() { + let token = crate::async_demo_cancel_token_create(); + unsafe { crate::async_demo_cancel_token_cancel(token) }; + let rx = launch_wait(60_000, token); + unsafe { crate::async_demo_cancel_token_destroy(token) }; + assert_eq!( + rx.recv_timeout(Duration::from_secs(5)).unwrap(), + (abi::CANCELLED_ERROR_CODE, 0) + ); } #[test] fn active_callbacks_returns_to_zero() { - let mut err = weaveffi_error::default(); - let (tx, rx) = mpsc::channel::<(bool, i32)>(); + let mut err = FfiError::default(); + let (tx, rx) = mpsc::channel::<(i32, i32)>(); let tx_ptr = Box::into_raw(Box::new(tx)); - for i in 0..16 { - weaveffi_tasks_run_n_tasks_async(i, n_tasks_callback, tx_ptr as *mut c_void); + unsafe { async_demo_tasks_run_n_tasks(i, i32_callback, tx_ptr.cast()) }; } for _ in 0..16 { rx.recv_timeout(Duration::from_secs(5)).unwrap(); @@ -354,11 +439,11 @@ mod tests { leak_ctx(tx_ptr); for _ in 0..50 { - if weaveffi_tasks_active_callbacks(&mut err) == 0 { + if unsafe { async_demo_tasks_active_callbacks(&mut err) } == 0 { break; } std::thread::sleep(Duration::from_millis(20)); } - assert_eq!(weaveffi_tasks_active_callbacks(&mut err), 0); + assert_eq!(unsafe { async_demo_tasks_active_callbacks(&mut err) }, 0); } } diff --git a/samples/calculator/Cargo.toml b/samples/calculator/Cargo.toml index aca354fb..da5a5840 100644 --- a/samples/calculator/Cargo.toml +++ b/samples/calculator/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -weaveffi = { path = "../../crates/weaveffi" } +weaveffi = { path = "../../crates/weaveffi", features = ["leak-check"] } [lints] workspace = true diff --git a/samples/calculator/src/lib.rs b/samples/calculator/src/lib.rs index f518822f..3e287444 100644 --- a/samples/calculator/src/lib.rs +++ b/samples/calculator/src/lib.rs @@ -17,6 +17,14 @@ pub mod calculator { DivisionByZero = 1, } + impl std::fmt::Display for CalcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DivisionByZero => f.write_str("division by zero"), + } + } + } + /// Add two integers. #[weaveffi::export] pub fn add(a: i32, b: i32) -> i32 { @@ -48,33 +56,54 @@ pub mod calculator { weaveffi::export_runtime!(); #[cfg(test)] +#[allow(unsafe_code)] mod tests { use super::calculator::*; - use weaveffi::abi::{self, c_ptr_to_string, weaveffi_error}; + use weaveffi::abi::{self, FfiError}; + + fn message(err: &FfiError) -> &str { + unsafe { err.message_str() }.unwrap_or_default() + } #[test] fn add_and_mul() { - let mut err = weaveffi_error::default(); - assert_eq!(weaveffi_calculator_add(2, 40, &mut err), 42); + let mut err = FfiError::default(); + assert_eq!(unsafe { calculator_calculator_add(2, 40, &mut err) }, 42); assert_eq!(err.code, 0); - assert_eq!(weaveffi_calculator_mul(6, 7, &mut err), 42); + assert_eq!(unsafe { calculator_calculator_mul(6, 7, &mut err) }, 42); assert_eq!(err.code, 0); } #[test] fn div_ok_path() { - let mut err = weaveffi_error::default(); - assert_eq!(weaveffi_calculator_div(10, 2, &mut err), 5); + let mut err = FfiError::default(); + assert_eq!(unsafe { calculator_calculator_div(10, 2, &mut err) }, 5); assert_eq!(err.code, 0); } #[test] fn div_by_zero_reports_domain_code() { - let mut err = weaveffi_error::default(); - let r = weaveffi_calculator_div(1, 0, &mut err); + let mut err = FfiError::default(); + let r = unsafe { calculator_calculator_div(1, 0, &mut err) }; assert_eq!(r, 0, "error path returns the zero sentinel"); assert_eq!(err.code, 1, "CalcError::DivisionByZero's declared code"); - assert_eq!(c_ptr_to_string(err.message).unwrap(), "division by zero"); - abi::error_clear(&mut err); + assert_eq!(message(&err), "division by zero"); + } + + #[test] + fn echo_round_trips_utf8_with_interior_nul() { + let text = "h\u{e9}llo\0world"; + let mut err = FfiError::default(); + let mut len = 0usize; + let ptr = + unsafe { calculator_calculator_echo(text.as_ptr(), text.len(), &mut len, &mut err) }; + assert_eq!(err.code, 0); + assert_eq!(unsafe { abi::lift_str(ptr, len) }, Some(text)); + unsafe { abi::free_bytes(ptr.cast_mut(), len) }; + } + + #[test] + fn checksum_is_exported() { + assert_ne!(calculator_calculator_checksum(), 0); } } diff --git a/samples/codec/Cargo.toml b/samples/codec/Cargo.toml index c304bdbe..fe2c1522 100644 --- a/samples/codec/Cargo.toml +++ b/samples/codec/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -weaveffi = { path = "../../crates/weaveffi" } +weaveffi = { path = "../../crates/weaveffi", features = ["leak-check"] } [lints] workspace = true diff --git a/samples/codec/src/lib.rs b/samples/codec/src/lib.rs index de7a27ba..5254c6c9 100644 --- a/samples/codec/src/lib.rs +++ b/samples/codec/src/lib.rs @@ -33,6 +33,12 @@ pub mod codec { Mismatch = 1, } + impl std::fmt::Display for CodecError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("value does not match the canonical fixture") + } + } + /// A C-style enum (crosses by value, and as an `i32` inside buffers). #[weaveffi::enumeration] #[repr(i32)] @@ -421,68 +427,90 @@ weaveffi::export_runtime!(); mod tests { use crate::codec::*; use std::sync::Arc; - use weaveffi::abi::{self, weaveffi_error}; + use weaveffi::abi::{self, FfiError}; fn decode_and_free(ptr: *const u8, len: usize) -> T { assert!(!ptr.is_null()); let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; let value = abi::decode_value::(bytes).expect("well-formed value buffer"); - abi::free_bytes(ptr as *mut u8, len); + unsafe { abi::free_bytes(ptr.cast_mut(), len) }; value } #[test] fn composite_round_trips_through_the_abi() { - let mut err = weaveffi_error::default(); + let mut err = FfiError::default(); let mut len = 0usize; - let ptr = weaveffi_codec_sample_composite(&mut len, &mut err); + let ptr = unsafe { codec_codec_sample_composite(&mut len, &mut err) }; let sample = decode_and_free::(ptr, len); assert_eq!(sample.names.len(), 3); assert_eq!(sample.by_id.len(), 2); let bytes = abi::encode_value(&sample); - assert!(weaveffi_codec_verify_composite( - bytes.as_ptr(), - bytes.len(), - &mut err - )); + assert!(unsafe { codec_codec_verify_composite(bytes.as_ptr(), bytes.len(), &mut err) }); assert_eq!(err.code, 0); let mut changed = sample.clone(); changed.sparse[1] = Some(true); let bytes = abi::encode_value(&changed); - assert!(!weaveffi_codec_verify_composite( - bytes.as_ptr(), - bytes.len(), - &mut err - )); + assert!(!unsafe { codec_codec_verify_composite(bytes.as_ptr(), bytes.len(), &mut err) }); assert_eq!(err.code, 1); - abi::error_clear(&mut err); + assert_eq!( + unsafe { err.message_str() }, + Some("value does not match the canonical fixture") + ); } #[test] fn scalars_round_trip() { - let mut err = weaveffi_error::default(); + let mut err = FfiError::default(); let mut len = 0usize; - let ptr = weaveffi_codec_sample_scalars(&mut len, &mut err); + let ptr = unsafe { codec_codec_sample_scalars(&mut len, &mut err) }; let sample = decode_and_free::(ptr, len); assert_eq!(sample.u64_value, u64::MAX); let bytes = abi::encode_value(&sample); - let out = weaveffi_codec_roundtrip_scalars(bytes.as_ptr(), bytes.len(), &mut len, &mut err); + let out = unsafe { + codec_codec_roundtrip_scalars(bytes.as_ptr(), bytes.len(), &mut len, &mut err) + }; assert_eq!(decode_and_free::(out, len), sample); - assert_eq!(weaveffi_codec_roundtrip_i64(i64::MIN, &mut err), i64::MIN); - assert_eq!(weaveffi_codec_roundtrip_u64(u64::MAX, &mut err), u64::MAX); - assert_eq!( - weaveffi_codec_roundtrip_color(Color::Blue as i32, &mut err), - Color::Blue as i32 - ); + unsafe { + assert_eq!(codec_codec_roundtrip_i64(i64::MIN, &mut err), i64::MIN); + assert_eq!(codec_codec_roundtrip_u64(u64::MAX, &mut err), u64::MAX); + assert_eq!( + codec_codec_roundtrip_color(Color::Blue as i32, &mut err), + Color::Blue as i32 + ); + } + } + + #[test] + fn strings_and_bytes_round_trip_verbatim() { + let mut err = FfiError::default(); + let mut len = 0usize; + let text = "nul\0inside \u{1F980}"; + let out = + unsafe { codec_codec_roundtrip_string(text.as_ptr(), text.len(), &mut len, &mut err) }; + assert_eq!(unsafe { abi::lift_str(out, len) }, Some(text)); + unsafe { abi::free_bytes(out.cast_mut(), len) }; + + let data = [0u8, 255, 0, 7]; + let out = + unsafe { codec_codec_roundtrip_bytes(data.as_ptr(), data.len(), &mut len, &mut err) }; + assert_eq!(unsafe { abi::lift_bytes(out, len) }, Some(data.to_vec())); + unsafe { abi::free_bytes(out.cast_mut(), len) }; + + // The empty string is `(NULL, 0)` in both directions. + let out = unsafe { codec_codec_roundtrip_string(std::ptr::null(), 0, &mut len, &mut err) }; + assert!(out.is_null()); + assert_eq!(len, 0); + assert_eq!(err.code, 0); } #[test] fn holders_carry_object_references() { - let mut err = weaveffi_error::default(); + let mut err = FfiError::default(); let mut len = 0usize; - let ptr = weaveffi_codec_make_holder(10, true, &mut len, &mut err); + let ptr = unsafe { codec_codec_make_holder(10, true, &mut len, &mut err) }; let holder = decode_and_free::(ptr, len); assert_eq!(holder.primary.value(), 10); assert_eq!(holder.spare.as_ref().unwrap().value(), 11); @@ -492,30 +520,26 @@ mod tests { // decode adopts it, so an encoded buffer is consumed exactly once. let bytes = abi::encode_value(&holder); assert_eq!( - weaveffi_codec_sum_holder(bytes.as_ptr(), bytes.len(), &mut err), + unsafe { codec_codec_sum_holder(bytes.as_ptr(), bytes.len(), &mut err) }, 10 + 11 + 12 + 13 + 14 ); assert_eq!(Arc::strong_count(&holder.primary), 1); let bytes = abi::encode_value(&holder); - let primary = weaveffi_codec_primary_of(bytes.as_ptr(), bytes.len(), &mut err); + let primary = unsafe { codec_codec_primary_of(bytes.as_ptr(), bytes.len(), &mut err) }; assert_eq!(primary as *const Token, Arc::as_ptr(&holder.primary)); assert_eq!(Arc::strong_count(&holder.primary), 2); - weaveffi_codec_Token_destroy(primary); + unsafe { codec_codec_Token_destroy(primary) }; assert_eq!(Arc::strong_count(&holder.primary), 1); let a = abi::encode_value(&holder); let b = abi::encode_value(&holder); - assert!(weaveffi_codec_same_primary( - a.as_ptr(), - a.len(), - b.as_ptr(), - b.len(), - &mut err - )); + assert!(unsafe { + codec_codec_same_primary(a.as_ptr(), a.len(), b.as_ptr(), b.len(), &mut err) + }); assert_eq!(Arc::strong_count(&holder.primary), 1); - let ptr = weaveffi_codec_make_holder(0, false, &mut len, &mut err); + let ptr = unsafe { codec_codec_make_holder(0, false, &mut len, &mut err) }; let without = decode_and_free::(ptr, len); assert!(without.spare.is_none()); } diff --git a/samples/contacts/Cargo.toml b/samples/contacts/Cargo.toml index 5c1c740d..9805f46e 100644 --- a/samples/contacts/Cargo.toml +++ b/samples/contacts/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -weaveffi = { path = "../../crates/weaveffi" } +weaveffi = { path = "../../crates/weaveffi", features = ["leak-check"] } [lints] workspace = true diff --git a/samples/contacts/src/lib.rs b/samples/contacts/src/lib.rs index 0990c415..9a18732c 100644 --- a/samples/contacts/src/lib.rs +++ b/samples/contacts/src/lib.rs @@ -1,12 +1,21 @@ //! Contacts sample: a small CRUD address book written as plain, safe Rust. //! //! `#[weaveffi::module]` reads the annotated items and generates the -//! `#[no_mangle] extern "C"` thunks that back the stable C ABI (and every -//! generated language binding). The address book is exported as an interface: -//! each `ContactBook` object owns its contacts directly (methods take `&self` -//! and guard the state with a `Mutex`, because the object is shared across the +//! `extern "C"` thunks that back the stable C ABI (and every generated +//! language binding). The address book is exported as an interface: each +//! `ContactBook` object owns its contacts directly (methods take `&self` and +//! guard the state with a `Mutex`, because the object is shared across the //! FFI boundary), and fallible methods report typed `ContactsError` codes //! through the ABI's error channel. The producer writes no `unsafe` glue. +//! +//! The sample also shows the two ways modules relate: +//! +//! * `contacts::groups` is nested under the `contacts` root, so it may use +//! any of the parent's types, including the `ContactType` C-style enum and +//! the `ContactBook` interface, and it inherits the parent's error domain. +//! * `directory` is a sibling root. A module tree only sees its own +//! declarations, so across roots only records and rich enums (value +//! types) may be shared; it uses the `Contact` record. #[weaveffi::module] pub mod contacts { @@ -23,6 +32,15 @@ pub mod contacts { NotFound = 2, } + impl std::fmt::Display for ContactsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::InvalidName => "name must not be empty", + Self::NotFound => "contact not found", + }) + } + } + /// How a contact is classified. #[weaveffi::enumeration] #[repr(i32)] @@ -131,6 +149,118 @@ pub mod contacts { self.contacts.lock().unwrap().len() as i32 } } + + /// Group queries over a book. Nested under the `contacts` root, so its + /// functions take and return the parent's `ContactType` enum and + /// `ContactBook` interface, and report the parent's `ContactsError`. + #[weaveffi::module] + pub mod groups { + use super::{Contact, ContactBook, ContactType, ContactsError}; + + /// Count the contacts of one type. + #[weaveffi::export] + pub fn count_of_type(book: &ContactBook, contact_type: ContactType) -> i32 { + book.list() + .iter() + .filter(|c| c.contact_type == contact_type) + .count() as i32 + } + + /// The most common contact type in a book (`Personal` when empty or + /// tied). + #[weaveffi::export] + pub fn dominant_type(book: &ContactBook) -> ContactType { + let count = |t| count_of_type(book, t); + [ContactType::Work, ContactType::Other] + .into_iter() + .filter(|t| count(*t) > count(ContactType::Personal)) + .max_by_key(|t| count(*t)) + .unwrap_or(ContactType::Personal) + } + + /// Copy every contact of one type into a new book. + #[weaveffi::export] + pub fn split_by_type(book: &ContactBook, contact_type: ContactType) -> ContactBook { + let out = ContactBook::new(); + for c in book + .list() + .into_iter() + .filter(|c| c.contact_type == contact_type) + { + // The names were validated when the contact was first added. + let _ = out.add(c.first_name, c.last_name, c.email, c.contact_type); + } + out + } + + /// The first contact of one type, failing with the parent domain's + /// [`ContactsError::NotFound`] when there is none. + #[weaveffi::export] + pub fn first_of_type( + book: &ContactBook, + contact_type: ContactType, + ) -> Result { + book.list() + .into_iter() + .find(|c| c.contact_type == contact_type) + .ok_or(ContactsError::NotFound) + } + } +} + +/// Directory formatting. A sibling root of `contacts`, so it shares only the +/// `Contact` record (a value type) with it. +#[weaveffi::module] +pub mod directory { + use super::contacts::Contact; + + /// The directory's error domain. + #[weaveffi::error] + #[derive(Debug)] + pub enum DirectoryError { + /// no contacts to list + Empty = 1, + } + + impl std::fmt::Display for DirectoryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("no contacts to list") + } + } + + /// A contact formatted for display. + #[weaveffi::record] + #[derive(Clone, Debug, PartialEq)] + pub struct Card { + /// `Last, First`. + pub display_name: String, + /// The first letters of the first and last names. + pub initials: String, + /// Whether the contact has an email address. + pub has_email: bool, + } + + /// Format one contact as a card. + #[weaveffi::export] + pub fn card(contact: &Contact) -> Card { + let initial = |s: &str| s.chars().next().map(String::from).unwrap_or_default(); + Card { + display_name: format!("{}, {}", contact.last_name, contact.first_name), + initials: initial(&contact.first_name) + &initial(&contact.last_name), + has_email: contact.email.is_some(), + } + } + + /// Sort contacts by last name, then first name, failing with + /// [`DirectoryError::Empty`] for an empty list. + #[weaveffi::export] + pub fn sorted(mut contacts: Vec) -> Result, DirectoryError> { + if contacts.is_empty() { + return Err(DirectoryError::Empty); + } + contacts.sort_by(|a, b| (&a.last_name, &a.first_name).cmp(&(&b.last_name, &b.first_name))); + Ok(contacts) + } } weaveffi::export_runtime!(); @@ -213,92 +343,200 @@ mod tests { } // A direct exercise of the generated C ABI thunks: construct a book - // through the interface constructor (interfaces still cross as opaque - // pointers with `_new`/`_destroy`), drive its methods (including the - // typed error path), and decode the buffered `Contact` returns. The - // optional email parameter and the record results cross as value buffers. - #[test] - fn ffi_surface_smoke() { - use super::contacts::{ - weaveffi_contacts_ContactBook_add, weaveffi_contacts_ContactBook_count, - weaveffi_contacts_ContactBook_destroy, weaveffi_contacts_ContactBook_get, - weaveffi_contacts_ContactBook_new, weaveffi_contacts_ContactBook_remove, Contact, - ContactType, - }; - use std::ffi::CString; - use weaveffi::abi::{self, weaveffi_error}; - - fn decode_and_free(ptr: *const u8, len: usize) -> Contact { + // through the interface constructor, drive its methods (including the + // typed error path), and decode the buffered `Contact` returns. Strings + // cross as `(ptr, len)`; the optional email and the records cross as + // value buffers. + mod ffi { + use super::super::contacts::groups::*; + use super::super::contacts::*; + use super::super::directory::*; + use weaveffi::abi::{self, FfiError}; + + fn decode(ptr: *const u8, len: usize) -> T { assert!(!ptr.is_null()); let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; - let contact = abi::decode_value::(bytes).expect("well-formed Contact buffer"); - abi::free_bytes(ptr as *mut u8, len); - contact + let value = abi::decode_value::(bytes).expect("well-formed value buffer"); + unsafe { abi::free_bytes(ptr.cast_mut(), len) }; + value } - let mut err = weaveffi_error::default(); - let book = weaveffi_contacts_ContactBook_new(&mut err); - assert_eq!(err.code, 0); - assert!(!book.is_null()); - - let first = CString::new("Zoe").unwrap(); - let last = CString::new("Quinn").unwrap(); - let email = abi::encode_value(&Some("zoe@example.com".to_string())); - let mut added_len: usize = 0; - let added_ptr = weaveffi_contacts_ContactBook_add( - book, - first.as_ptr(), - last.as_ptr(), - email.as_ptr(), - email.len(), - ContactType::Work as i32, - &mut added_len, - &mut err, - ); - assert_eq!(err.code, 0); - let added = decode_and_free(added_ptr, added_len); - assert!(added.id > 0); - assert_eq!(added.first_name, "Zoe"); - assert_eq!(added.email.as_deref(), Some("zoe@example.com")); - - // An empty first name reports the InvalidName domain code. - let empty = CString::new("").unwrap(); - let no_email = abi::encode_value(&None::); - let mut rejected_len: usize = 0; - let rejected = weaveffi_contacts_ContactBook_add( - book, - empty.as_ptr(), - last.as_ptr(), - no_email.as_ptr(), - no_email.len(), - ContactType::Work as i32, - &mut rejected_len, - &mut err, - ); - assert!(rejected.is_null()); - assert_eq!(err.code, 1); - abi::error_clear(&mut err); - - let mut fetched_len: usize = 0; - let fetched_ptr = - weaveffi_contacts_ContactBook_get(book, added.id, &mut fetched_len, &mut err); - assert_eq!(err.code, 0); - let fetched = decode_and_free(fetched_ptr, fetched_len); - assert_eq!(fetched.first_name, "Zoe"); - assert_eq!(fetched.contact_type, ContactType::Work); - - assert_eq!(weaveffi_contacts_ContactBook_count(book, &mut err), 1); - assert!(weaveffi_contacts_ContactBook_remove( - book, added.id, &mut err - )); + fn add(book: *mut ContactBook, first: &str, last: &str, t: ContactType) -> Contact { + let mut err = FfiError::default(); + let email = abi::encode_value(&None::); + let mut len = 0usize; + let ptr = unsafe { + contacts_contacts_ContactBook_add( + book, + first.as_ptr(), + first.len(), + last.as_ptr(), + last.len(), + email.as_ptr(), + email.len(), + t as i32, + &mut len, + &mut err, + ) + }; + assert_eq!(err.code, 0); + decode(ptr, len) + } + + #[test] + fn book_surface() { + let mut err = FfiError::default(); + let book = unsafe { contacts_contacts_ContactBook_new(&mut err) }; + assert_eq!(err.code, 0); + assert!(!book.is_null()); + + let (first, last) = ("Zoe", "Quinn"); + let email = abi::encode_value(&Some("zoe@example.com".to_string())); + let mut len = 0usize; + let ptr = unsafe { + contacts_contacts_ContactBook_add( + book, + first.as_ptr(), + first.len(), + last.as_ptr(), + last.len(), + email.as_ptr(), + email.len(), + ContactType::Work as i32, + &mut len, + &mut err, + ) + }; + assert_eq!(err.code, 0); + let added: Contact = decode(ptr, len); + assert!(added.id > 0); + assert_eq!(added.first_name, "Zoe"); + assert_eq!(added.email.as_deref(), Some("zoe@example.com")); + + // An empty first name reports the InvalidName domain code. + let rejected = unsafe { + contacts_contacts_ContactBook_add( + book, + std::ptr::null(), + 0, + last.as_ptr(), + last.len(), + email.as_ptr(), + email.len(), + ContactType::Work as i32, + &mut len, + &mut err, + ) + }; + assert!(rejected.is_null()); + assert_eq!(err.code, 1); + assert_eq!(unsafe { err.message_str() }, Some("name must not be empty")); + + let ptr = + unsafe { contacts_contacts_ContactBook_get(book, added.id, &mut len, &mut err) }; + assert_eq!(err.code, 0); + let fetched: Contact = decode(ptr, len); + assert_eq!(fetched.contact_type, ContactType::Work); - // A missing id reports the NotFound domain code. - let mut missing_len: usize = 0; - let missing = weaveffi_contacts_ContactBook_get(book, added.id, &mut missing_len, &mut err); - assert!(missing.is_null()); - assert_eq!(err.code, 2); - abi::error_clear(&mut err); + assert_eq!( + unsafe { contacts_contacts_ContactBook_count(book, &mut err) }, + 1 + ); + assert!(unsafe { contacts_contacts_ContactBook_remove(book, added.id, &mut err) }); - weaveffi_contacts_ContactBook_destroy(book); + // A missing id reports the NotFound domain code. + let missing = + unsafe { contacts_contacts_ContactBook_get(book, added.id, &mut len, &mut err) }; + assert!(missing.is_null()); + assert_eq!(err.code, 2); + + unsafe { contacts_contacts_ContactBook_destroy(book) }; + } + + #[test] + fn nested_module_uses_the_parent_enum_interface_and_errors() { + let mut err = FfiError::default(); + let book = unsafe { contacts_contacts_ContactBook_new(&mut err) }; + add(book, "Ann", "Lee", ContactType::Work); + add(book, "Bo", "Kim", ContactType::Work); + add(book, "Cy", "Ng", ContactType::Personal); + + let work = ContactType::Work as i32; + assert_eq!( + unsafe { contacts_contacts_groups_count_of_type(book, work, &mut err) }, + 2 + ); + assert_eq!( + unsafe { contacts_contacts_groups_dominant_type(book, &mut err) }, + work + ); + + let split = unsafe { contacts_contacts_groups_split_by_type(book, work, &mut err) }; + assert_eq!(err.code, 0); + assert_eq!( + unsafe { contacts_contacts_ContactBook_count(split, &mut err) }, + 2 + ); + unsafe { contacts_contacts_ContactBook_destroy(split) }; + + let mut len = 0usize; + let ptr = + unsafe { contacts_contacts_groups_first_of_type(book, work, &mut len, &mut err) }; + assert_eq!(decode::(ptr, len).first_name, "Ann"); + let other = ContactType::Other as i32; + let none = + unsafe { contacts_contacts_groups_first_of_type(book, other, &mut len, &mut err) }; + assert!(none.is_null()); + assert_eq!(err.code, 2, "the parent domain's NotFound code"); + + // An out-of-range discriminant is a marshalling failure. + assert_eq!( + unsafe { contacts_contacts_groups_count_of_type(book, 9, &mut err) }, + 0 + ); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + unsafe { contacts_contacts_ContactBook_destroy(book) }; + } + + #[test] + fn sibling_root_shares_the_contact_record() { + let mut err = FfiError::default(); + let book = unsafe { contacts_contacts_ContactBook_new(&mut err) }; + let zoe = add(book, "Zoe", "Quinn", ContactType::Personal); + let ada = add(book, "Ada", "Byron", ContactType::Work); + unsafe { contacts_contacts_ContactBook_destroy(book) }; + + let bytes = abi::encode_value(&zoe); + let mut len = 0usize; + let ptr = + unsafe { contacts_directory_card(bytes.as_ptr(), bytes.len(), &mut len, &mut err) }; + assert_eq!( + decode::(ptr, len), + Card { + display_name: "Quinn, Zoe".into(), + initials: "ZQ".into(), + has_email: false, + } + ); + + let list = abi::encode_value(&vec![zoe, ada]); + let ptr = + unsafe { contacts_directory_sorted(list.as_ptr(), list.len(), &mut len, &mut err) }; + let sorted: Vec = decode(ptr, len); + assert_eq!(sorted[0].last_name, "Byron"); + + let empty = abi::encode_value(&Vec::::new()); + let none = unsafe { + contacts_directory_sorted(empty.as_ptr(), empty.len(), &mut len, &mut err) + }; + assert!(none.is_null()); + assert_eq!(err.code, 1); + assert_eq!(unsafe { err.message_str() }, Some("no contacts to list")); + } + + #[test] + fn each_root_exports_a_checksum() { + assert_ne!(contacts_contacts_checksum(), contacts_directory_checksum()); + } } } diff --git a/samples/events/Cargo.toml b/samples/events/Cargo.toml index e3ca4742..c2cd3ac4 100644 --- a/samples/events/Cargo.toml +++ b/samples/events/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -weaveffi = { path = "../../crates/weaveffi" } +weaveffi = { path = "../../crates/weaveffi", features = ["leak-check"] } [lints] workspace = true diff --git a/samples/events/src/lib.rs b/samples/events/src/lib.rs index b57ecaf0..48cbf785 100644 --- a/samples/events/src/lib.rs +++ b/samples/events/src/lib.rs @@ -2,7 +2,7 @@ //! callback interface, a reference-counted object, and an iterator. //! //! The `#[weaveffi::module]` expansion emits exactly the ABI the WeaveFFI -//! generators bind to (see the generated `weaveffi.h`): a `Subscriber` vtable +//! generators bind to (see the generated `events.h`): a `Subscriber` vtable //! the consumer implements, an `EventBus` object with `_clone`/`_destroy` //! reference counting, and an opaque iterator with an //! `int32_t next(iter, out_item, out_err)` contract. The conformance harness @@ -11,8 +11,13 @@ //! //! The producer writes only safe Rust. The consumer's subscriber arrives as an //! `Arc`; the bus retains it for as long as it likes and the -//! consumer's `free` entry fires when the last reference drops. A subscriber -//! that fails aborts the publishing call with `FOREIGN_ERROR_CODE`, so the bus +//! consumer's `free` entry fires when the last reference drops. +//! +//! The two callback styles are both on show. `route` returns +//! `Result`, so a consumer failure comes back as a +//! value; the bus chooses to propagate it, aborting the call with +//! `FOREIGN_ERROR_CODE`. `on_message` and `on_attached` return plain values, +//! so a consumer failure there aborts the call directly. Either way the bus //! snapshots its subscriber list before calling out and never holds a lock //! across a callback. @@ -52,8 +57,9 @@ pub mod events { /// deliver each message and then calls `on_message` for accepted ones. #[weaveffi::callback_interface] pub trait Subscriber: Send + Sync { - /// Decide how the bus should treat `topic` for this subscriber. - fn route(&self, topic: String) -> Delivery; + /// Decide how the bus should treat `topic` for this subscriber. A + /// consumer failure is returned as an `Err`. + fn route(&self, topic: &str) -> Result; /// Receive an accepted message. Returns the subscriber's running count /// of received messages. fn on_message(&self, message: &Message) -> i64; @@ -112,7 +118,16 @@ pub mod events { .clone(); let mut delivered = 0; for sub in &subs { - match sub.route(message.topic.clone()) { + let delivery = match sub.route(&message.topic) { + Ok(d) => d, + Err(e) => { + // Propagate the subscriber's failure to our caller, + // exactly as a plain-return method would have. + weaveffi::abi::raise_foreign_error(e); + return delivered; + } + }; + match delivery { Delivery::Skip => {} Delivery::Accept => { sub.on_message(&message); @@ -172,10 +187,14 @@ pub mod events { } } - /// Ask `subscriber` how it would route `topic` without a bus. + /// Ask `subscriber` how it would route `topic` without a bus. A + /// subscriber failure fails the call. #[weaveffi::export] - pub fn route_once(subscriber: Arc, topic: String) -> Delivery { - subscriber.route(topic) + pub fn route_once(subscriber: Arc, topic: &str) -> Delivery { + subscriber.route(topic).unwrap_or_else(|e| { + weaveffi::abi::raise_foreign_error(e); + Delivery::Skip + }) } } @@ -185,15 +204,10 @@ weaveffi::export_runtime!(); #[allow(unsafe_code)] mod tests { use crate::events::*; - use std::ffi::CString; - use std::os::raw::{c_char, c_void}; + use std::os::raw::c_void; use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; use std::sync::Arc; - use weaveffi::abi::{self, weaveffi_error}; - - fn new_err() -> weaveffi_error { - weaveffi_error::default() - } + use weaveffi::abi::{self, FfiError}; /// A consumer-side subscriber, exactly as a generated binding builds one: /// a heap-allocated context and a process-wide vtable. @@ -207,17 +221,16 @@ mod tests { unsafe extern "C" fn route( ctx: *mut c_void, - topic: *const c_char, - out_err: *mut weaveffi_error, + topic_ptr: *const u8, + topic_len: usize, + out_err: *mut FfiError, ) -> i32 { - let state = &*(ctx as *const SubState); - let topic = abi::c_ptr_to_string(topic).unwrap(); + let state = unsafe { &*(ctx as *const SubState) }; + // String arguments are borrowed `(ptr, len)` runs for the call. + let topic = unsafe { abi::lift_str(topic_ptr, topic_len) }.unwrap(); if topic == state.fail_topic { - abi::error_set( - out_err, - abi::FOREIGN_ERROR_CODE, - "subscriber rejected topic", - ); + let msg = c"subscriber rejected topic"; + unsafe { crate::events_error_set(out_err, abi::FOREIGN_ERROR_CODE, msg.as_ptr()) }; return 0; } if topic == state.skip_topic { @@ -233,32 +246,29 @@ mod tests { ctx: *mut c_void, message_ptr: *const u8, message_len: usize, - _out_err: *mut weaveffi_error, + _out_err: *mut FfiError, ) -> i64 { - let state = &*(ctx as *const SubState); + let state = unsafe { &*(ctx as *const SubState) }; let message: Message = - abi::decode_value(std::slice::from_raw_parts(message_ptr, message_len)).unwrap(); + abi::decode_value(unsafe { std::slice::from_raw_parts(message_ptr, message_len) }) + .unwrap(); assert!(message.seq >= 1); state.received.fetch_add(1, Ordering::SeqCst) + 1 } - unsafe extern "C" fn on_attached( - ctx: *mut c_void, - bus: *mut EventBus, - _out_err: *mut weaveffi_error, - ) { - let state = &*(ctx as *const SubState); + unsafe extern "C" fn on_attached(ctx: *mut c_void, bus: *mut EventBus, _err: *mut FfiError) { + let state = unsafe { &*(ctx as *const SubState) }; state.attached.fetch_add(1, Ordering::SeqCst); // The reference is ours: dropping it must not free the live bus. - weaveffi_events_EventBus_destroy(bus); + unsafe { events_events_EventBus_destroy(bus) }; } unsafe extern "C" fn free(ctx: *mut c_void) { - let state = Box::from_raw(ctx as *mut SubState); + let state = unsafe { Box::from_raw(ctx as *mut SubState) }; state.freed.fetch_add(1, Ordering::SeqCst); } - static VTABLE: weaveffi_events_Subscriber_vtable = weaveffi_events_Subscriber_vtable { + static VTABLE: events_events_Subscriber_vtable = events_events_Subscriber_vtable { route, on_message, on_attached, @@ -275,37 +285,44 @@ mod tests { })) as *mut c_void } - fn publish(bus: *mut EventBus, topic: &str, text: &str, err: &mut weaveffi_error) -> i64 { - let topic = CString::new(topic).unwrap(); - let text = CString::new(text).unwrap(); + fn new_bus() -> *mut EventBus { + let mut err = FfiError::default(); + let bus = unsafe { events_events_EventBus_new(&mut err) }; + assert!(!bus.is_null()); + bus + } + + fn subscribe(bus: *mut EventBus, sub: *mut c_void) -> i64 { + let mut err = FfiError::default(); + unsafe { events_events_EventBus_subscribe(bus, sub, &VTABLE, &mut err) } + } + + fn publish(bus: *mut EventBus, topic: &str, text: &str, err: &mut FfiError) -> i64 { let tags = abi::encode_value(&vec!["a".to_string()]); - weaveffi_events_EventBus_publish( - bus, - topic.as_ptr(), - text.as_ptr(), - tags.as_ptr(), - tags.len(), - err, - ) + unsafe { + events_events_EventBus_publish( + bus, + topic.as_ptr(), + topic.len(), + text.as_ptr(), + text.len(), + tags.as_ptr(), + tags.len(), + err, + ) + } } #[test] fn subscribe_publish_and_iterate() { - let mut err = new_err(); + let mut err = FfiError::default(); let freed = Arc::new(AtomicUsize::new(0)); - let bus = weaveffi_events_EventBus_new(&mut err); - assert!(!bus.is_null()); + let bus = new_bus(); let a = new_sub("quiet", "", &freed); let b = new_sub("", "", &freed); - assert_eq!( - weaveffi_events_EventBus_subscribe(bus, a, &VTABLE, &mut err), - 1 - ); - assert_eq!( - weaveffi_events_EventBus_subscribe(bus, b, &VTABLE, &mut err), - 2 - ); + assert_eq!(subscribe(bus, a), 1); + assert_eq!(subscribe(bus, b), 2); let a_state = unsafe { &*(a as *const SubState) }; assert_eq!(a_state.attached.load(Ordering::SeqCst), 1); @@ -314,53 +331,56 @@ mod tests { assert_eq!(publish(bus, "stop", "last", &mut err), 1); assert_eq!(err.code, 0); - let iter = weaveffi_events_EventBus_messages(bus, &mut err); + let iter = unsafe { events_events_EventBus_messages(bus, &mut err) }; let mut got = Vec::new(); loop { - let mut item: *const c_char = std::ptr::null(); - if weaveffi_events_EventBus_MessagesIterator_next(iter, &mut item, &mut err) == 0 { + let mut item: *const u8 = std::ptr::null(); + let mut len = 0usize; + let has = unsafe { + events_events_EventBus_MessagesIterator_next(iter, &mut item, &mut len, &mut err) + }; + if has == 0 { break; } - got.push(abi::c_ptr_to_string(item).unwrap()); - abi::free_string(item); + got.push(unsafe { abi::lift_string(item, len) }.unwrap()); + unsafe { crate::events_free_bytes(item.cast_mut(), len) }; } - weaveffi_events_EventBus_MessagesIterator_destroy(iter); + unsafe { events_events_EventBus_MessagesIterator_destroy(iter) }; assert_eq!(got, vec!["hello", "psst", "last"]); let mut len = 0usize; - let ptr = weaveffi_events_EventBus_last_message(bus, &mut len, &mut err); + let ptr = unsafe { events_events_EventBus_last_message(bus, &mut len, &mut err) }; let last: Option = abi::decode_value(unsafe { std::slice::from_raw_parts(ptr, len) }).unwrap(); - abi::free_bytes(ptr as *mut u8, len); + unsafe { crate::events_free_bytes(ptr.cast_mut(), len) }; assert_eq!(last.unwrap().text, "last"); - weaveffi_events_EventBus_clear_subscribers(bus, &mut err); + unsafe { events_events_EventBus_clear_subscribers(bus, &mut err) }; assert_eq!( freed.load(Ordering::SeqCst), 2, "free ran once per subscriber" ); - weaveffi_events_EventBus_destroy(bus); + unsafe { events_events_EventBus_destroy(bus) }; } #[test] - fn foreign_error_aborts_publish() { - let mut err = new_err(); + fn a_failed_route_aborts_publish() { + let mut err = FfiError::default(); let freed = Arc::new(AtomicUsize::new(0)); - let bus = weaveffi_events_EventBus_new(&mut err); - let a = new_sub("", "boom", &freed); - weaveffi_events_EventBus_subscribe(bus, a, &VTABLE, &mut err); + let bus = new_bus(); + subscribe(bus, new_sub("", "boom", &freed)); publish(bus, "boom", "x", &mut err); assert_eq!(err.code, abi::FOREIGN_ERROR_CODE); - assert!(abi::c_ptr_to_string(err.message) - .unwrap() - .contains("rejected topic")); - abi::error_clear(&mut err); + assert_eq!( + unsafe { err.message_str() }, + Some("subscriber rejected topic") + ); // The bus is still usable afterward. assert_eq!(publish(bus, "ok", "y", &mut err), 1); assert_eq!(err.code, 0); - weaveffi_events_EventBus_destroy(bus); + unsafe { events_events_EventBus_destroy(bus) }; assert_eq!( freed.load(Ordering::SeqCst), 1, @@ -370,27 +390,45 @@ mod tests { #[test] fn route_once_does_not_retain() { - let mut err = new_err(); + let mut err = FfiError::default(); let freed = Arc::new(AtomicUsize::new(0)); - let a = new_sub("quiet", "", &freed); - let topic = CString::new("quiet").unwrap(); - let d = weaveffi_events_route_once(a, &VTABLE, topic.as_ptr(), &mut err); + let topic = "quiet"; + let d = unsafe { + events_events_route_once( + new_sub("quiet", "", &freed), + &VTABLE, + topic.as_ptr(), + topic.len(), + &mut err, + ) + }; assert_eq!(d, Delivery::Skip as i32); assert_eq!(freed.load(Ordering::SeqCst), 1); + + let d = unsafe { + events_events_route_once( + new_sub("", "quiet", &freed), + &VTABLE, + topic.as_ptr(), + topic.len(), + &mut err, + ) + }; + assert_eq!(d, 0); + assert_eq!(err.code, abi::FOREIGN_ERROR_CODE); } #[test] fn reference_counting() { - let mut err = new_err(); - let bus = weaveffi_events_EventBus_new(&mut err); - let again = weaveffi_events_EventBus_clone(bus); - assert_eq!(bus, again); - weaveffi_events_EventBus_destroy(bus); - assert_eq!( - weaveffi_events_EventBus_subscriber_count(again, &mut err), - 0 - ); - weaveffi_events_EventBus_destroy(again); - weaveffi_events_EventBus_destroy(std::ptr::null_mut()); + let mut err = FfiError::default(); + let bus = new_bus(); + unsafe { + let again = events_events_EventBus_clone(bus); + assert_eq!(bus, again); + events_events_EventBus_destroy(bus); + assert_eq!(events_events_EventBus_subscriber_count(again, &mut err), 0); + events_events_EventBus_destroy(again); + events_events_EventBus_destroy(std::ptr::null_mut()); + } } } diff --git a/samples/inventory/Cargo.toml b/samples/inventory/Cargo.toml deleted file mode 100644 index 9cccb695..00000000 --- a/samples/inventory/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "inventory" -version = "0.1.0" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -weaveffi = { path = "../../crates/weaveffi" } - -[lints] -workspace = true diff --git a/samples/inventory/src/lib.rs b/samples/inventory/src/lib.rs deleted file mode 100644 index c8ecb651..00000000 --- a/samples/inventory/src/lib.rs +++ /dev/null @@ -1,553 +0,0 @@ -//! Inventory sample: a two-module product and order catalog in safe Rust. -//! -//! This sample shows how `#[weaveffi::module]` scales past a single namespace. -//! The `products` module owns the `Product` record, its `Category` enum, and -//! the `Catalog` interface: each catalog object holds its products directly -//! (methods take `&self` and guard the state with a `Mutex`). The `orders` -//! module keeps free functions and an in-memory store behind a `Mutex`, and -//! references `products::Product` across the module boundary. Both modules -//! declare typed error domains, and the macro generates every `extern "C"` -//! thunk and the value-buffer marshalling that carries records and lists -//! across the boundary. No `unsafe` glue is written by hand. - -/// Product catalog: the `Product` record, its `Category` enum, and the -/// `Catalog` interface. -#[weaveffi::module] -pub mod products { - use std::sync::atomic::{AtomicI64, Ordering}; - use std::sync::Mutex; - - /// The product catalog's error domain. - #[weaveffi::error] - #[derive(Debug)] - pub enum ProductsError { - /// price must be positive - InvalidPrice = 1, - /// product not found - ProductNotFound = 2, - } - - /// The shelf a product belongs to. - #[weaveffi::enumeration] - #[repr(i32)] - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub enum Category { - /// Phones, laptops, and other devices. - Electronics = 0, - /// Apparel and accessories. - Clothing = 1, - /// Anything edible. - Food = 2, - /// Printed and digital books. - Books = 3, - } - - /// A catalog item. - #[weaveffi::record] - #[derive(Clone, Debug)] - pub struct Product { - /// Stable identifier assigned on creation. - pub id: i64, - /// Display name. - pub name: String, - /// Optional long-form description. - pub description: Option, - /// Price in the catalog's base currency. - pub price: f64, - /// Which shelf the product sits on. - pub category: Category, - /// Free-form search tags. - pub tags: Vec, - } - - /// A product catalog exported as an interface. Each catalog owns its - /// products and id counter directly; destroying the object (via the - /// generated destroy symbol) releases that state. - #[weaveffi::interface] - pub struct Catalog { - products: Mutex>, - next_id: AtomicI64, - } - - impl Default for Catalog { - fn default() -> Self { - Self::new() - } - } - - impl Catalog { - /// Create an empty catalog. - pub fn new() -> Self { - Catalog { - products: Mutex::new(Vec::new()), - next_id: AtomicI64::new(1), - } - } - - /// Add a product, returning the stored record with its assigned id. - /// A non-positive price is rejected with - /// [`ProductsError::InvalidPrice`]. - pub fn add_product( - &self, - name: String, - price: f64, - category: Category, - ) -> Result { - if price <= 0.0 { - return Err(ProductsError::InvalidPrice); - } - let id = self.next_id.fetch_add(1, Ordering::Relaxed); - let product = Product { - id, - name, - description: None, - price, - category, - tags: Vec::new(), - }; - self.products.lock().unwrap().push(product.clone()); - Ok(product) - } - - /// Look up a product by id, failing with [`ProductsError::ProductNotFound`] - /// when none exists. - pub fn get_product(&self, id: i64) -> Result { - self.products - .lock() - .unwrap() - .iter() - .find(|p| p.id == id) - .cloned() - .ok_or(ProductsError::ProductNotFound) - } - - /// Return every product on the given shelf. - pub fn search(&self, category: Category) -> Vec { - self.products - .lock() - .unwrap() - .iter() - .filter(|p| p.category == category) - .cloned() - .collect() - } - - /// Update a product's price, returning whether the product existed. - /// A non-positive price is rejected with - /// [`ProductsError::InvalidPrice`]. - pub fn update_price(&self, id: i64, price: f64) -> Result { - if price <= 0.0 { - return Err(ProductsError::InvalidPrice); - } - let mut products = self.products.lock().unwrap(); - match products.iter_mut().find(|p| p.id == id) { - Some(p) => { - p.price = price; - Ok(true) - } - None => Ok(false), - } - } - - /// Remove a product by id, returning whether it existed. - pub fn remove(&self, id: i64) -> bool { - let mut products = self.products.lock().unwrap(); - let before = products.len(); - products.retain(|p| p.id != id); - products.len() < before - } - } -} - -/// Order management: the `Order`/`OrderItem` records and order operations, -/// including one that takes a `products::Product` across the module boundary. -#[weaveffi::module] -pub mod orders { - use super::products::Product; - use std::sync::atomic::{AtomicI64, Ordering}; - use std::sync::Mutex; - - /// The order module's error domain. - #[weaveffi::error] - #[derive(Debug)] - pub enum OrdersError { - /// order not found - OrderNotFound = 1, - /// order must contain at least one item - EmptyOrder = 2, - } - - /// A single line in an order. - #[weaveffi::record] - #[derive(Clone, Debug)] - pub struct OrderItem { - /// Id of the ordered product. - pub product_id: i64, - /// How many units were ordered. - pub quantity: i32, - /// Price charged per unit at order time. - pub unit_price: f64, - } - - /// A customer order. - #[weaveffi::record] - #[derive(Clone, Debug)] - pub struct Order { - /// Stable identifier assigned on creation. - pub id: i64, - /// The ordered line items. - pub items: Vec, - /// Sum of `unit_price * quantity` across the items. - pub total: f64, - /// Lifecycle status (`pending`, `cancelled`, ...). - pub status: String, - } - - static STORE: Mutex> = Mutex::new(Vec::new()); - static NEXT_ID: AtomicI64 = AtomicI64::new(1); - - /// Create an order from a list of items, returning its id. An empty item - /// list is rejected with [`OrdersError::EmptyOrder`]. - #[weaveffi::export] - pub fn create_order(items: Vec) -> Result { - if items.is_empty() { - return Err(OrdersError::EmptyOrder); - } - let total = items - .iter() - .map(|it| it.unit_price * it.quantity as f64) - .sum(); - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - STORE.lock().unwrap().push(Order { - id, - items, - total, - status: "pending".to_string(), - }); - Ok(id) - } - - /// Look up an order by id, failing with [`OrdersError::OrderNotFound`] when - /// none exists. - #[weaveffi::export] - pub fn get_order(id: i64) -> Result { - STORE - .lock() - .unwrap() - .iter() - .find(|o| o.id == id) - .cloned() - .ok_or(OrdersError::OrderNotFound) - } - - /// Cancel an order, returning whether this call changed its status (a - /// missing or already-cancelled order yields `false`). - #[weaveffi::export] - pub fn cancel_order(id: i64) -> bool { - let mut store = STORE.lock().unwrap(); - match store.iter_mut().find(|o| o.id == id) { - Some(o) if o.status != "cancelled" => { - o.status = "cancelled".to_string(); - true - } - _ => false, - } - } - - /// Append a single unit of `product` to an existing order, returning - /// whether the order existed. Demonstrates a cross-module record - /// parameter. - #[weaveffi::export] - pub fn add_product_to_order(order_id: i64, product: Product) -> bool { - let mut store = STORE.lock().unwrap(); - match store.iter_mut().find(|o| o.id == order_id) { - Some(order) => { - let item = OrderItem { - product_id: product.id, - quantity: 1, - unit_price: product.price, - }; - order.total += item.unit_price; - order.items.push(item); - true - } - None => false, - } - } - - /// Reset the in-memory store. Test-only helper (not part of the ABI). - #[cfg(test)] - pub(crate) fn reset() { - STORE.lock().unwrap().clear(); - NEXT_ID.store(1, Ordering::Relaxed); - } -} - -weaveffi::export_runtime!(); - -#[cfg(test)] -#[allow(unsafe_code)] -mod tests { - use super::orders::{ - add_product_to_order, cancel_order, create_order, get_order, OrderItem, OrdersError, - }; - use super::products::{Catalog, Category, ProductsError}; - use std::sync::Mutex; - - // The orders store is process-global, so serialize the tests that touch it. - static TEST_LOCK: Mutex<()> = Mutex::new(()); - - fn orders_guard() -> std::sync::MutexGuard<'static, ()> { - let g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - super::orders::reset(); - g - } - - fn item(product_id: i64, quantity: i32, unit_price: f64) -> OrderItem { - OrderItem { - product_id, - quantity, - unit_price, - } - } - - // ── Products ──────────────────────────────────────────── - - #[test] - fn add_and_get_product() { - let catalog = Catalog::new(); - let added = catalog - .add_product("Widget".into(), 9.99, Category::Electronics) - .expect("valid product"); - assert!(added.id > 0); - let p = catalog.get_product(added.id).expect("product exists"); - assert_eq!(p.name, "Widget"); - assert_eq!(p.price, 9.99); - assert_eq!(p.category, Category::Electronics); - assert_eq!(p.description, None); - assert!(p.tags.is_empty()); - } - - #[test] - fn add_product_rejects_non_positive_price() { - let catalog = Catalog::new(); - assert!(matches!( - catalog.add_product("Free".into(), 0.0, Category::Food), - Err(ProductsError::InvalidPrice) - )); - } - - #[test] - fn get_missing_product_is_not_found() { - let catalog = Catalog::new(); - assert!(matches!( - catalog.get_product(999), - Err(ProductsError::ProductNotFound) - )); - } - - #[test] - fn search_products_by_category() { - let catalog = Catalog::new(); - catalog - .add_product("Laptop".into(), 999.99, Category::Electronics) - .unwrap(); - catalog - .add_product("Shirt".into(), 29.99, Category::Clothing) - .unwrap(); - catalog - .add_product("Phone".into(), 499.99, Category::Electronics) - .unwrap(); - - assert_eq!(catalog.search(Category::Electronics).len(), 2); - assert!(catalog.search(Category::Books).is_empty()); - } - - #[test] - fn update_and_remove_product() { - let catalog = Catalog::new(); - let added = catalog - .add_product("Item".into(), 10.0, Category::Food) - .unwrap(); - assert!(catalog.update_price(added.id, 20.0).unwrap()); - assert_eq!(catalog.get_product(added.id).unwrap().price, 20.0); - assert!(!catalog.update_price(999, 1.0).unwrap()); - assert!(matches!( - catalog.update_price(added.id, -5.0), - Err(ProductsError::InvalidPrice) - )); - - assert!(catalog.remove(added.id)); - assert!(!catalog.remove(added.id)); - } - - // ── Orders ────────────────────────────────────────────── - - #[test] - fn create_and_get_order() { - let _g = orders_guard(); - let id = create_order(vec![item(1, 2, 10.0), item(2, 1, 25.0)]).expect("non-empty order"); - assert!(id > 0); - let o = get_order(id).expect("order exists"); - assert_eq!(o.items.len(), 2); - assert_eq!(o.total, 45.0); - assert_eq!(o.status, "pending"); - } - - #[test] - fn create_empty_order_is_rejected() { - let _g = orders_guard(); - assert!(matches!( - create_order(Vec::new()), - Err(OrdersError::EmptyOrder) - )); - } - - #[test] - fn get_missing_order_is_not_found() { - let _g = orders_guard(); - assert!(matches!(get_order(999), Err(OrdersError::OrderNotFound))); - } - - #[test] - fn cancel_order_transitions() { - let _g = orders_guard(); - let id = create_order(vec![item(1, 1, 5.0)]).unwrap(); - assert!(cancel_order(id)); - assert!(!cancel_order(id)); - assert_eq!(get_order(id).unwrap().status, "cancelled"); - assert!(!cancel_order(999)); - } - - // ── Cross-module ──────────────────────────────────────── - - #[test] - fn add_product_to_order_across_modules() { - let _g = orders_guard(); - let catalog = Catalog::new(); - let product = catalog - .add_product("Gadget".into(), 49.99, Category::Electronics) - .unwrap(); - let order_id = create_order(vec![item(0, 1, 1.0)]).unwrap(); - - assert!(add_product_to_order(order_id, product.clone())); - - let o = get_order(order_id).unwrap(); - assert_eq!(o.items.len(), 2); - assert_eq!(o.items[1].product_id, product.id); - assert_eq!(o.items[1].unit_price, 49.99); - assert_eq!(o.total, 50.99); - - assert!(!add_product_to_order(999, product)); - } - - // A direct exercise of the generated C ABI thunks. This drives the - // list-of-record parameter (`create_order`) through the `extern "C"` - // boundary as one encoded value buffer, and decodes the buffered `Order` - // return. - #[test] - fn ffi_order_surface_smoke() { - use super::orders::{weaveffi_orders_create_order, weaveffi_orders_get_order, Order}; - use weaveffi::abi::{self, weaveffi_error}; - - let _g = orders_guard(); - let mut err = weaveffi_error::default(); - - // Encode the item list into the (ptr, len) slot pair the thunk - // expects; the buffer is borrowed for the duration of the call. - let items = vec![item(1, 2, 10.0), item(2, 1, 25.0)]; - let items_buf = abi::encode_value(&items); - let id = weaveffi_orders_create_order(items_buf.as_ptr(), items_buf.len(), &mut err); - assert_eq!(err.code, 0); - assert!(id > 0); - - let mut order_len: usize = 0; - let order_ptr = weaveffi_orders_get_order(id, &mut order_len, &mut err); - assert_eq!(err.code, 0); - assert!(!order_ptr.is_null()); - let order = - abi::decode_value::(unsafe { std::slice::from_raw_parts(order_ptr, order_len) }) - .expect("well-formed Order buffer"); - abi::free_bytes(order_ptr as *mut u8, order_len); - assert_eq!(order.total, 45.0); - assert_eq!(order.items.len(), 2); - assert_eq!(order.status, "pending"); - - // An empty item list reports the EmptyOrder domain code. - let empty_buf = abi::encode_value(&Vec::::new()); - let rejected = weaveffi_orders_create_order(empty_buf.as_ptr(), empty_buf.len(), &mut err); - assert_eq!(rejected, 0); - assert_eq!(err.code, 2); - abi::error_clear(&mut err); - } - - // The interface thunks: construct a catalog, add a product (success and - // typed-error paths), and read it back, all through the C ABI. The - // catalog itself still crosses as an opaque pointer; the `Product` - // results are decoded from value buffers. - #[test] - fn ffi_catalog_surface_smoke() { - use super::products::{ - weaveffi_products_Catalog_add_product, weaveffi_products_Catalog_destroy, - weaveffi_products_Catalog_get_product, weaveffi_products_Catalog_new, - weaveffi_products_Catalog_remove, Product, - }; - use std::ffi::CString; - use weaveffi::abi::{self, weaveffi_error}; - - fn decode_and_free(ptr: *const u8, len: usize) -> Product { - assert!(!ptr.is_null()); - let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; - let product = abi::decode_value::(bytes).expect("well-formed Product buffer"); - abi::free_bytes(ptr as *mut u8, len); - product - } - - let mut err = weaveffi_error::default(); - let catalog = weaveffi_products_Catalog_new(&mut err); - assert_eq!(err.code, 0); - assert!(!catalog.is_null()); - - let name = CString::new("Widget").unwrap(); - let mut added_len: usize = 0; - let added_ptr = weaveffi_products_Catalog_add_product( - catalog, - name.as_ptr(), - 9.99, - Category::Electronics as i32, - &mut added_len, - &mut err, - ); - assert_eq!(err.code, 0); - let added = decode_and_free(added_ptr, added_len); - assert!(added.id > 0); - assert_eq!(added.name, "Widget"); - - // A non-positive price reports the InvalidPrice domain code. - let mut rejected_len: usize = 0; - let rejected = weaveffi_products_Catalog_add_product( - catalog, - name.as_ptr(), - 0.0, - Category::Electronics as i32, - &mut rejected_len, - &mut err, - ); - assert!(rejected.is_null()); - assert_eq!(err.code, 1); - abi::error_clear(&mut err); - - let mut fetched_len: usize = 0; - let fetched_ptr = - weaveffi_products_Catalog_get_product(catalog, added.id, &mut fetched_len, &mut err); - assert_eq!(err.code, 0); - let fetched = decode_and_free(fetched_ptr, fetched_len); - assert_eq!(fetched.price, 9.99); - - assert!(weaveffi_products_Catalog_remove( - catalog, added.id, &mut err - )); - - weaveffi_products_Catalog_destroy(catalog); - } -} diff --git a/samples/inventory/weaveffi.toml b/samples/inventory/weaveffi.toml deleted file mode 100644 index ffc3b349..00000000 --- a/samples/inventory/weaveffi.toml +++ /dev/null @@ -1,8 +0,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" diff --git a/samples/kvstore/Cargo.toml b/samples/kvstore/Cargo.toml index 6cb6cec7..78317ed8 100644 --- a/samples/kvstore/Cargo.toml +++ b/samples/kvstore/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -weaveffi = { path = "../../crates/weaveffi" } +weaveffi = { path = "../../crates/weaveffi", features = ["leak-check"] } [lints] workspace = true diff --git a/samples/kvstore/src/lib.rs b/samples/kvstore/src/lib.rs index 7749a211..7c5cef4e 100644 --- a/samples/kvstore/src/lib.rs +++ b/samples/kvstore/src/lib.rs @@ -13,11 +13,9 @@ //! `Store` is exported as an interface, so each object owns its rich state //! (its entries and the monotonic entry-id counter) directly. Methods take //! `&self` and guard that state with a `Mutex` because the object is shared -//! across the FFI boundary; the last `weaveffi_kv_Store_destroy` releases the +//! across the FFI boundary; the last `kvstore_kv_Store_destroy` releases the //! state with it. -#![allow(unsafe_code)] - /// An embedded key-value store API with TTLs, iteration, and async compaction. #[weaveffi::module] pub mod kv { @@ -28,8 +26,9 @@ pub mod kv { use std::time::{SystemTime, UNIX_EPOCH}; /// The store's error domain. Each variant's discriminant is the stable - /// ABI code a throwing method reports through `out_err`, and its doc - /// comment is the default message. + /// ABI code a throwing method reports through `out_err`, its `Display` + /// output is the runtime message, and its doc comment is the documented + /// default message. #[weaveffi::error] #[derive(Debug)] pub enum KvError { @@ -43,6 +42,17 @@ pub mod kv { IoError = 1004, } + impl std::fmt::Display for KvError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::KeyNotFound => "key not found", + Self::Expired => "entry expired", + Self::StoreFull => "store has reached capacity", + Self::IoError => "I/O failure", + }) + } + } + /// The largest number of live entries one store will hold before `put` /// rejects a new key with [`KvError::StoreFull`]. const STORE_CAPACITY: usize = 1_000_000; @@ -365,9 +375,9 @@ pub mod kv { } /// Reclaim space asynchronously; returns the number of bytes - /// reclaimed. Honors the caller's cancellation token: a token already - /// cancelled when the future runs fails with [`KvError::IoError`] - /// instead of compacting. + /// reclaimed. Cancelling the call's token completes it with the + /// cancelled code (the runtime drops the work); the check here only + /// covers a cancellation that lands as compaction starts. #[weaveffi::cancellable] pub async fn compact(&self, cancel: weaveffi::CancelToken) -> Result { if cancel.is_cancelled() { @@ -444,22 +454,32 @@ mod tests { use crate::kv::stats::*; use crate::kv::*; use std::collections::BTreeMap; - use std::ffi::{c_void, CString}; - use std::os::raw::c_char; + use std::ffi::c_void; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{mpsc, Arc, Mutex}; use std::time::Duration; - use weaveffi::abi::{self, weaveffi_error}; + use weaveffi::abi::{self, FfiError}; /// Decode a buffered return and release the producer-owned bytes. fn decode_and_free(ptr: *const u8, len: usize) -> T { assert!(!ptr.is_null()); let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }; let value = abi::decode_value::(bytes).expect("well-formed value buffer"); - abi::free_bytes(ptr as *mut u8, len); + unsafe { abi::free_bytes(ptr.cast_mut(), len) }; value } + /// Copy a returned string and release it. + fn take_string(ptr: *const u8, len: usize) -> String { + let s = unsafe { abi::lift_string(ptr, len) }.expect("UTF-8"); + unsafe { abi::free_bytes(ptr.cast_mut(), len) }; + s + } + + fn message(err: &FfiError) -> &str { + unsafe { err.message_str() }.unwrap_or_default() + } + // Stores are independent objects, but the tests share process-wide // vtables and counters, so they run serialized for readable failures. static TEST_MUTEX: Mutex<()> = Mutex::new(()); @@ -470,90 +490,146 @@ mod tests { .unwrap_or_else(std::sync::PoisonError::into_inner) } - fn new_err() -> weaveffi_error { - weaveffi_error::default() - } - fn open() -> *mut Store { - let mut err = new_err(); - let path = CString::new("/tmp/kvstore-test").unwrap(); - let s = weaveffi_kv_Store_open(path.as_ptr(), &mut err); + let mut err = FfiError::default(); + let path = "/tmp/kvstore-test"; + let s = unsafe { kvstore_kv_Store_open(path.as_ptr(), path.len(), &mut err) }; assert_eq!(err.code, 0); assert!(!s.is_null()); s } + fn destroy(s: *mut Store) { + unsafe { kvstore_kv_Store_destroy(s) }; + } + + fn count(s: *const Store) -> i64 { + let mut err = FfiError::default(); + unsafe { kvstore_kv_Store_count(s, &mut err) } + } + + /// `put` through the thunk; the optional TTL is buffered, so it's + /// encoded as `Option` and passed as a borrowed (ptr, len) pair. + fn put( + s: *mut Store, + key: &str, + value: &[u8], + kind: i32, + ttl: Option, + err: &mut FfiError, + ) -> bool { + let ttl = abi::encode_value(&ttl); + unsafe { + kvstore_kv_Store_put( + s, + key.as_ptr(), + key.len(), + value.as_ptr(), + value.len(), + kind, + ttl.as_ptr(), + ttl.len(), + err, + ) + } + } + fn put_simple(s: *mut Store, k: &str, v: &[u8]) { - let mut err = new_err(); - let key = CString::new(k).unwrap(); - // The optional TTL is buffered: encode `Option` and pass the - // borrowed (ptr, len) pair. - let ttl = abi::encode_value(&None::); - let ok = weaveffi_kv_Store_put( - s, - key.as_ptr(), - v.as_ptr(), - v.len(), - EntryKind::Persistent as i32, - ttl.as_ptr(), - ttl.len(), - &mut err, - ); + let mut err = FfiError::default(); + assert!(put(s, k, v, EntryKind::Persistent as i32, None, &mut err)); assert_eq!(err.code, 0); - assert!(ok); + } + + fn get(s: *mut Store, key: &str, err: &mut FfiError) -> Option { + let mut out_len = 0usize; + let ptr = unsafe { kvstore_kv_Store_get(s, key.as_ptr(), key.len(), &mut out_len, err) }; + if ptr.is_null() { + assert_ne!(err.code, 0); + return None; + } + decode_and_free::>(ptr, out_len) + } + + fn delete(s: *mut Store, key: &str, err: &mut FfiError) -> bool { + unsafe { kvstore_kv_Store_delete(s, key.as_ptr(), key.len(), err) } + } + + fn keys(s: *mut Store, prefix: Option<&str>) -> Vec { + let mut err = FfiError::default(); + let prefix = abi::encode_value(&prefix.map(str::to_string)); + let iter = + unsafe { kvstore_kv_Store_list_keys(s, prefix.as_ptr(), prefix.len(), &mut err) }; + assert_eq!(err.code, 0); + assert!(!iter.is_null()); + let mut got = Vec::new(); + loop { + let mut item: *const u8 = std::ptr::null(); + let mut len = 0usize; + let has = unsafe { + kvstore_kv_Store_ListKeysIterator_next(iter, &mut item, &mut len, &mut err) + }; + assert_eq!(err.code, 0); + if has == 0 { + assert!(item.is_null()); + break; + } + got.push(take_string(item, len)); + } + unsafe { kvstore_kv_Store_ListKeysIterator_destroy(iter) }; + got } #[test] fn open_destroy_lifecycle() { let _g = setup(); - let s = open(); - weaveffi_kv_Store_destroy(s); + destroy(open()); } #[test] fn open_empty_path_reports_io_error() { let _g = setup(); - let mut err = new_err(); - // The fallible constructor rejects an empty path with the IoError - // domain code and returns null. - let path = CString::new("").unwrap(); - let s = weaveffi_kv_Store_open(path.as_ptr(), &mut err); + let mut err = FfiError::default(); + // The fallible constructor rejects an empty path (here the canonical + // `(NULL, 0)` empty string) with the IoError domain code. + let s = unsafe { kvstore_kv_Store_open(std::ptr::null(), 0, &mut err) }; assert!(s.is_null()); assert_eq!(err.code, 1004, "KvError::IoError's declared code"); - assert_eq!(abi::c_ptr_to_string(err.message).unwrap(), "I/O failure"); - abi::error_clear(&mut err); + assert_eq!(message(&err), "I/O failure"); } #[test] - fn open_null_path_errors() { + fn open_invalid_path_is_a_marshalling_error() { let _g = setup(); - let mut err = new_err(); - // A `string` parameter rejects a null pointer with the reserved - // marshalling code, before `open` ever runs. - let s = weaveffi_kv_Store_open(std::ptr::null(), &mut err); + let mut err = FfiError::default(); + // A null pointer with a length, or bytes that aren't UTF-8, are + // rejected with the reserved marshalling code before `open` runs. + let s = unsafe { kvstore_kv_Store_open(std::ptr::null(), 4, &mut err) }; + assert!(s.is_null()); + assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); + let bad = [0xC0u8, 0x00]; + let s = unsafe { kvstore_kv_Store_open(bad.as_ptr(), bad.len(), &mut err) }; assert!(s.is_null()); assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); - abi::error_clear(&mut err); } #[test] fn default_capacity_static() { let _g = setup(); - let mut err = new_err(); - assert_eq!(weaveffi_kv_Store_default_capacity(&mut err), 1_000_000); + let mut err = FfiError::default(); + assert_eq!( + unsafe { kvstore_kv_Store_default_capacity(&mut err) }, + 1_000_000 + ); assert_eq!(err.code, 0); } #[test] fn null_self_method_call_reports_error() { let _g = setup(); - let mut err = new_err(); - // A method thunk rejects a null object pointer with the reserved - // marshalling code before touching the producer. - let n = weaveffi_kv_Store_count(std::ptr::null(), &mut err); + let mut err = FfiError::default(); + let n = unsafe { kvstore_kv_Store_count(std::ptr::null(), &mut err) }; assert_eq!(n, 0); assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); - abi::error_clear(&mut err); } #[test] @@ -561,86 +637,54 @@ mod tests { let _g = setup(); let s = open(); put_simple(s, "alpha", b"hello"); - - let mut err = new_err(); - let key = CString::new("alpha").unwrap(); - let mut out_len: usize = 0; - let ptr = weaveffi_kv_Store_get(s, key.as_ptr(), &mut out_len, &mut err); + let mut err = FfiError::default(); + let e = get(s, "alpha", &mut err).expect("entry present"); assert_eq!(err.code, 0); - - let e = decode_and_free::>(ptr, out_len).expect("entry present"); assert_eq!(e.key, "alpha"); assert_eq!(e.value, b"hello"); assert!(e.id > 0); - - weaveffi_kv_Store_destroy(s); + destroy(s); } #[test] fn put_invalid_kind_errors() { let _g = setup(); let s = open(); - let mut err = new_err(); - let key = CString::new("k").unwrap(); + let mut err = FfiError::default(); // An out-of-range `EntryKind` discriminant is rejected by the macro's // enum lift with the reserved marshalling code. - let ttl = abi::encode_value(&None::); - let ok = weaveffi_kv_Store_put( - s, - key.as_ptr(), - std::ptr::null(), - 0, - 999, - ttl.as_ptr(), - ttl.len(), - &mut err, - ); - assert!(!ok); + assert!(!put(s, "k", b"", 999, None, &mut err)); assert_eq!(err.code, abi::MARSHAL_ERROR_CODE); - abi::error_clear(&mut err); - weaveffi_kv_Store_destroy(s); + destroy(s); } #[test] fn get_missing_key_returns_not_found() { let _g = setup(); let s = open(); - let mut err = new_err(); - let k = CString::new("nope").unwrap(); - let mut out_len: usize = 0; - let p = weaveffi_kv_Store_get(s, k.as_ptr(), &mut out_len, &mut err); - assert!(p.is_null()); + let mut err = FfiError::default(); + assert!(get(s, "nope", &mut err).is_none()); assert_eq!(err.code, 1001, "KvError::KeyNotFound's declared code"); - assert_eq!(abi::c_ptr_to_string(err.message).unwrap(), "key not found"); - abi::error_clear(&mut err); - weaveffi_kv_Store_destroy(s); + assert_eq!(message(&err), "key not found"); + destroy(s); } #[test] fn put_with_ttl_expires() { let _g = setup(); let s = open(); - let mut err = new_err(); - let key = CString::new("ttl").unwrap(); - let ttl = abi::encode_value(&Some(-1i64)); - let ok = weaveffi_kv_Store_put( + let mut err = FfiError::default(); + assert!(put( s, - key.as_ptr(), - b"x".as_ptr(), - 1, + "ttl", + b"x", EntryKind::Volatile as i32, - ttl.as_ptr(), - ttl.len(), - &mut err, - ); - assert!(ok); - - let mut out_len: usize = 0; - let entry = weaveffi_kv_Store_get(s, key.as_ptr(), &mut out_len, &mut err); - assert!(entry.is_null()); + Some(-1), + &mut err + )); + assert!(get(s, "ttl", &mut err).is_none()); assert_eq!(err.code, 1002, "KvError::Expired's declared code"); - abi::error_clear(&mut err); - weaveffi_kv_Store_destroy(s); + destroy(s); } #[test] @@ -648,12 +692,11 @@ mod tests { let _g = setup(); let s = open(); put_simple(s, "k", b"v"); - let mut err = new_err(); - let key = CString::new("k").unwrap(); - assert!(weaveffi_kv_Store_delete(s, key.as_ptr(), &mut err)); + let mut err = FfiError::default(); + assert!(delete(s, "k", &mut err)); assert_eq!(err.code, 0); - assert!(!weaveffi_kv_Store_delete(s, key.as_ptr(), &mut err)); - weaveffi_kv_Store_destroy(s); + assert!(!delete(s, "k", &mut err)); + destroy(s); } #[test] @@ -663,29 +706,8 @@ mod tests { put_simple(s, "alpha", b"1"); put_simple(s, "beta", b"2"); put_simple(s, "gamma", b"3"); - - let mut err = new_err(); - let prefix = abi::encode_value(&None::); - let iter = weaveffi_kv_Store_list_keys(s, prefix.as_ptr(), prefix.len(), &mut err); - assert_eq!(err.code, 0); - assert!(!iter.is_null()); - - let mut got = Vec::new(); - loop { - let mut item: *const c_char = std::ptr::null(); - let r = weaveffi_kv_Store_ListKeysIterator_next(iter, &mut item, &mut err); - if r == 0 { - assert!(item.is_null()); - break; - } - assert!(!item.is_null()); - got.push(abi::c_ptr_to_string(item).unwrap()); - abi::free_string(item); - } - weaveffi_kv_Store_ListKeysIterator_destroy(iter); - assert_eq!(got, vec!["alpha", "beta", "gamma"]); - - weaveffi_kv_Store_destroy(s); + assert_eq!(keys(s, None), vec!["alpha", "beta", "gamma"]); + destroy(s); } #[test] @@ -695,132 +717,104 @@ mod tests { put_simple(s, "user.alice", b"1"); put_simple(s, "user.bob", b"2"); put_simple(s, "system.x", b"3"); - - let mut err = new_err(); - let prefix = abi::encode_value(&Some("user.".to_string())); - let iter = weaveffi_kv_Store_list_keys(s, prefix.as_ptr(), prefix.len(), &mut err); - let mut got = Vec::new(); - loop { - let mut item: *const c_char = std::ptr::null(); - if weaveffi_kv_Store_ListKeysIterator_next(iter, &mut item, &mut err) == 0 { - break; - } - got.push(abi::c_ptr_to_string(item).unwrap()); - abi::free_string(item); - } - weaveffi_kv_Store_ListKeysIterator_destroy(iter); - assert_eq!(got, vec!["user.alice", "user.bob"]); - - weaveffi_kv_Store_destroy(s); + assert_eq!(keys(s, Some("user.")), vec!["user.alice", "user.bob"]); + destroy(s); } #[test] fn count_and_clear() { let _g = setup(); let s = open(); - let mut err = new_err(); - assert_eq!(weaveffi_kv_Store_count(s, &mut err), 0); + let mut err = FfiError::default(); + assert_eq!(count(s), 0); put_simple(s, "a", b"1"); put_simple(s, "b", b"2"); - assert_eq!(weaveffi_kv_Store_count(s, &mut err), 2); - weaveffi_kv_Store_clear(s, &mut err); + assert_eq!(count(s), 2); + unsafe { kvstore_kv_Store_clear(s, &mut err) }; assert_eq!(err.code, 0); - assert_eq!(weaveffi_kv_Store_count(s, &mut err), 0); - weaveffi_kv_Store_destroy(s); + assert_eq!(count(s), 0); + destroy(s); } #[test] fn legacy_put_inserts_volatile() { let _g = setup(); let s = open(); - let mut err = new_err(); - let k = CString::new("legacy").unwrap(); + let mut err = FfiError::default(); + let (k, v) = ("legacy", b"v"); // The generated thunk carries its own `#[allow(deprecated)]`, so // calling it needs no opt-in here. - let ok = weaveffi_kv_Store_legacy_put(s, k.as_ptr(), b"v".as_ptr(), 1, &mut err); + let ok = unsafe { + kvstore_kv_Store_legacy_put(s, k.as_ptr(), k.len(), v.as_ptr(), v.len(), &mut err) + }; assert!(ok); - assert_eq!(weaveffi_kv_Store_count(s, &mut err), 1); - weaveffi_kv_Store_destroy(s); + assert_eq!(count(s), 1); + destroy(s); + } + + type Done = mpsc::Sender<(i32, i64)>; + + extern "C" fn on_compacted(context: *mut c_void, err: *mut FfiError, result: i64) { + let tx = unsafe { &*(context as *const Done) }; + let code = if err.is_null() { + 0 + } else { + let code = unsafe { (*err).code }; + unsafe { crate::kvstore_error_free(err) }; + code + }; + tx.send((code, result)).unwrap(); + } + + fn compact(s: *mut Store, cancelled: bool) -> (i32, i64) { + let (tx, rx) = mpsc::channel::<(i32, i64)>(); + let tx_ptr = Box::into_raw(Box::new(tx)); + let token = crate::kvstore_cancel_token_create(); + unsafe { + if cancelled { + crate::kvstore_cancel_token_cancel(token); + } + kvstore_kv_Store_compact(s, token, on_compacted, tx_ptr.cast()); + // The launcher took its own reference, so the consumer may + // release the token right away. + crate::kvstore_cancel_token_destroy(token); + } + let out = rx.recv_timeout(Duration::from_secs(5)).unwrap(); + drop(unsafe { Box::from_raw(tx_ptr) }); + out } #[test] fn compact_reclaims_expired_bytes() { let _g = setup(); let s = open(); - let mut err = new_err(); - let k = CString::new("dead").unwrap(); - let expired_ttl = abi::encode_value(&Some(-1i64)); - weaveffi_kv_Store_put( + let mut err = FfiError::default(); + put( s, - k.as_ptr(), - b"hello".as_ptr(), - 5, + "dead", + b"hello", EntryKind::Volatile as i32, - expired_ttl.as_ptr(), - expired_ttl.len(), + Some(-1), &mut err, ); - let k2 = CString::new("alive").unwrap(); - let no_ttl = abi::encode_value(&None::); - weaveffi_kv_Store_put( + put( s, - k2.as_ptr(), - b"x".as_ptr(), - 1, + "alive", + b"x", EntryKind::Persistent as i32, - no_ttl.as_ptr(), - no_ttl.len(), + None, &mut err, ); - - let (tx, rx) = mpsc::channel::<(i32, i64)>(); - let tx_ptr = Box::into_raw(Box::new(tx)); - extern "C" fn cb(context: *mut c_void, err: *mut weaveffi_error, result: i64) { - let tx = unsafe { &*(context as *const mpsc::Sender<(i32, i64)>) }; - let code = if err.is_null() { - 0 - } else { - unsafe { (*err).code } - }; - tx.send((code, result)).unwrap(); - } - let token = abi::cancel_token_create(); - weaveffi_kv_Store_compact_async(s, token, cb, tx_ptr as *mut c_void); - - let (code, reclaimed) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - unsafe { drop(Box::from_raw(tx_ptr)) }; - assert_eq!(code, 0); - assert_eq!(reclaimed, 5); - abi::cancel_token_destroy(token); - weaveffi_kv_Store_destroy(s); + assert_eq!(compact(s, false), (0, 5)); + destroy(s); } #[test] fn compact_honors_cancel_token() { let _g = setup(); let s = open(); - - let (tx, rx) = mpsc::channel::<(i32, i64)>(); - let tx_ptr = Box::into_raw(Box::new(tx)); - extern "C" fn cb(context: *mut c_void, err: *mut weaveffi_error, result: i64) { - let tx = unsafe { &*(context as *const mpsc::Sender<(i32, i64)>) }; - let code = if err.is_null() { - 0 - } else { - unsafe { (*err).code } - }; - tx.send((code, result)).unwrap(); - } - let token = abi::cancel_token_create(); - abi::cancel_token_cancel(token); - weaveffi_kv_Store_compact_async(s, token, cb, tx_ptr as *mut c_void); - - let (code, reclaimed) = rx.recv_timeout(Duration::from_secs(5)).unwrap(); - unsafe { drop(Box::from_raw(tx_ptr)) }; - assert_eq!(code, 1004, "a cancelled compact reports KvError::IoError"); - assert_eq!(reclaimed, 0); - abi::cancel_token_destroy(token); - weaveffi_kv_Store_destroy(s); + assert_eq!(compact(s, true), (abi::CANCELLED_ERROR_CODE, 0)); + destroy(s); } #[test] @@ -829,15 +823,15 @@ mod tests { let s = open(); put_simple(s, "a", b"hi"); put_simple(s, "b", b"bye"); - let mut err = new_err(); + let mut err = FfiError::default(); let mut out_len: usize = 0; - let ptr = weaveffi_kv_stats_get_stats(s, &mut out_len, &mut err); + let ptr = unsafe { kvstore_kv_stats_get_stats(s, &mut out_len, &mut err) }; assert_eq!(err.code, 0); let stats = decode_and_free::(ptr, out_len); assert_eq!(stats.total_entries, 2); assert_eq!(stats.total_bytes, 5); assert_eq!(stats.expired_entries, 0); - weaveffi_kv_Store_destroy(s); + destroy(s); } #[test] @@ -917,33 +911,34 @@ mod tests { entry_ptr: *const u8, entry_len: usize, reason: i32, - _out_err: *mut weaveffi_error, + _out_err: *mut FfiError, ) -> bool { - let state = &*(ctx as *const ListenerState); + let state = unsafe { &*(ctx as *const ListenerState) }; let entry: Entry = - abi::decode_value(std::slice::from_raw_parts(entry_ptr, entry_len)).unwrap(); + abi::decode_value(unsafe { std::slice::from_raw_parts(entry_ptr, entry_len) }).unwrap(); let mut seen = state.evictions.lock().unwrap(); seen.push((entry.key, reason)); seen.len() < state.keep_after } unsafe extern "C" fn free_listener(ctx: *mut c_void) { - let state = Box::from_raw(ctx as *mut ListenerState); + let state = unsafe { Box::from_raw(ctx as *mut ListenerState) }; state.freed.fetch_add(1, Ordering::SeqCst); } - static LISTENER_VTABLE: weaveffi_kv_EvictionListener_vtable = - weaveffi_kv_EvictionListener_vtable { + static LISTENER_VTABLE: kvstore_kv_EvictionListener_vtable = + kvstore_kv_EvictionListener_vtable { on_evict, free: free_listener, }; - fn new_listener(keep_after: usize, freed: &Arc) -> *mut ListenerState { + fn new_listener(keep_after: usize, freed: &Arc) -> *mut c_void { Box::into_raw(Box::new(ListenerState { evictions: Mutex::new(Vec::new()), keep_after, freed: Arc::clone(freed), })) + .cast() } #[test] @@ -951,37 +946,30 @@ mod tests { let _g = setup(); let s = open(); let freed = Arc::new(AtomicUsize::new(0)); - let listener = new_listener(2, &freed); - let mut err = new_err(); - weaveffi_kv_Store_set_eviction_listener( - s, - listener as *mut c_void, - &LISTENER_VTABLE, - &mut err, - ); + let mut err = FfiError::default(); + unsafe { + kvstore_kv_Store_set_eviction_listener( + s, + new_listener(2, &freed), + &LISTENER_VTABLE, + &mut err, + ) + }; assert_eq!(err.code, 0); put_simple(s, "evict-me", b"v"); - let key = CString::new("evict-me").unwrap(); - assert!(weaveffi_kv_Store_delete(s, key.as_ptr(), &mut err)); + assert!(delete(s, "evict-me", &mut err)); - let expiring = CString::new("expiring").unwrap(); - let ttl = abi::encode_value(&Some(-1i64)); - weaveffi_kv_Store_put( + put( s, - expiring.as_ptr(), - b"x".as_ptr(), - 1, + "expiring", + b"x", EntryKind::Volatile as i32, - ttl.as_ptr(), - ttl.len(), + Some(-1), &mut err, ); - let mut out_len = 0usize; - let p = weaveffi_kv_Store_get(s, expiring.as_ptr(), &mut out_len, &mut err); - assert!(p.is_null()); + assert!(get(s, "expiring", &mut err).is_none()); assert_eq!(err.code, 1002); - abi::error_clear(&mut err); // The second eviction returned `false`, so the store detached (and // freed) the listener; a third eviction is not observed. @@ -991,9 +979,8 @@ mod tests { "detached listener is freed" ); put_simple(s, "again", b"x"); - let again = CString::new("again").unwrap(); - weaveffi_kv_Store_delete(s, again.as_ptr(), &mut err); - weaveffi_kv_Store_destroy(s); + delete(s, "again", &mut err); + destroy(s); } #[test] @@ -1001,30 +988,30 @@ mod tests { let _g = setup(); let s = open(); let freed = Arc::new(AtomicUsize::new(0)); - let mut err = new_err(); - let first = new_listener(usize::MAX, &freed); - weaveffi_kv_Store_set_eviction_listener( - s, - first as *mut c_void, - &LISTENER_VTABLE, - &mut err, - ); - assert_eq!(freed.load(Ordering::SeqCst), 0); - let second = new_listener(usize::MAX, &freed); - weaveffi_kv_Store_set_eviction_listener( - s, - second as *mut c_void, - &LISTENER_VTABLE, - &mut err, - ); - assert_eq!( - freed.load(Ordering::SeqCst), - 1, - "replaced listener is freed" - ); - weaveffi_kv_Store_clear_eviction_listener(s, &mut err); + let mut err = FfiError::default(); + unsafe { + kvstore_kv_Store_set_eviction_listener( + s, + new_listener(usize::MAX, &freed), + &LISTENER_VTABLE, + &mut err, + ); + assert_eq!(freed.load(Ordering::SeqCst), 0); + kvstore_kv_Store_set_eviction_listener( + s, + new_listener(usize::MAX, &freed), + &LISTENER_VTABLE, + &mut err, + ); + assert_eq!( + freed.load(Ordering::SeqCst), + 1, + "replaced listener is freed" + ); + kvstore_kv_Store_clear_eviction_listener(s, &mut err); + } assert_eq!(freed.load(Ordering::SeqCst), 2); - weaveffi_kv_Store_destroy(s); + destroy(s); } #[test] @@ -1032,24 +1019,24 @@ mod tests { let _g = setup(); let s = open(); put_simple(s, "a", b"1"); - let mut err = new_err(); + let mut err = FfiError::default(); - let shared = weaveffi_kv_Store_share(s, &mut err); + let shared = unsafe { kvstore_kv_Store_share(s, &mut err) }; assert_eq!(shared, s, "share returns the same object"); - weaveffi_kv_Store_destroy(s); - assert_eq!(weaveffi_kv_Store_count(shared, &mut err), 1, "still alive"); + destroy(s); + assert_eq!(count(shared), 1, "still alive"); - let forked = weaveffi_kv_Store_fork(shared, &mut err); + let forked = unsafe { kvstore_kv_Store_fork(shared, &mut err) }; assert_ne!(forked, shared); put_simple(forked, "b", b"2"); - assert_eq!(weaveffi_kv_Store_count(forked, &mut err), 2); - assert_eq!(weaveffi_kv_Store_count(shared, &mut err), 1); - - let cloned = weaveffi_kv_Store_clone(forked); - weaveffi_kv_Store_destroy(forked); - assert_eq!(weaveffi_kv_Store_count(cloned, &mut err), 2); - weaveffi_kv_Store_destroy(cloned); - weaveffi_kv_Store_destroy(shared); + assert_eq!(count(forked), 2); + assert_eq!(count(shared), 1); + + let cloned = unsafe { kvstore_kv_Store_clone(forked) }; + destroy(forked); + assert_eq!(count(cloned), 2); + destroy(cloned); + destroy(shared); } #[test] @@ -1058,18 +1045,18 @@ mod tests { let a = open(); let b = open(); put_simple(b, "x", b"1"); - let mut err = new_err(); - - assert!(weaveffi_kv_Store_larger(a, std::ptr::null(), &mut err).is_null()); - let bigger = weaveffi_kv_Store_larger(a, b, &mut err); - assert_eq!(bigger, b); - weaveffi_kv_Store_destroy(bigger); - let own = weaveffi_kv_Store_larger(b, std::ptr::null(), &mut err); - assert_eq!(own, b); - weaveffi_kv_Store_destroy(own); - - weaveffi_kv_Store_destroy(a); - weaveffi_kv_Store_destroy(b); + let mut err = FfiError::default(); + unsafe { + assert!(kvstore_kv_Store_larger(a, std::ptr::null(), &mut err).is_null()); + let bigger = kvstore_kv_Store_larger(a, b, &mut err); + assert_eq!(bigger, b); + destroy(bigger); + let own = kvstore_kv_Store_larger(b, std::ptr::null(), &mut err); + assert_eq!(own, b); + destroy(own); + } + destroy(a); + destroy(b); } #[test] @@ -1077,12 +1064,20 @@ mod tests { let _g = setup(); let s = open(); put_simple(s, "k", b"v"); - let mut err = new_err(); + let mut err = FfiError::default(); - let label = CString::new("primary").unwrap(); + let label = "primary"; let mut out_len = 0usize; - let ptr = - weaveffi_kv_Store_describe(s, label.as_ptr(), std::ptr::null(), &mut out_len, &mut err); + let ptr = unsafe { + kvstore_kv_Store_describe( + s, + label.as_ptr(), + label.len(), + std::ptr::null(), + &mut out_len, + &mut err, + ) + }; assert_eq!(err.code, 0); let info = decode_and_free::(ptr, out_len); assert_eq!(info.label, "primary"); @@ -1094,36 +1089,43 @@ mod tests { let paths = abi::encode_value(&vec!["/a".to_string(), "/b".to_string()]); let mut many_len = 0usize; - let many_ptr = - weaveffi_kv_Store_open_many(paths.as_ptr(), paths.len(), &mut many_len, &mut err); + let many_ptr = unsafe { + kvstore_kv_Store_open_many(paths.as_ptr(), paths.len(), &mut many_len, &mut err) + }; assert_eq!(err.code, 0); let many = decode_and_free::>>(many_ptr, many_len); assert_eq!(many.len(), 2); - put_simple(Arc::as_ptr(&many[0]) as *mut Store, "m", b"1"); + put_simple(Arc::as_ptr(&many[0]).cast_mut(), "m", b"1"); // Objects written into a parameter buffer carry one reference each. let stores = abi::encode_value(&many); let extra = abi::encode_value(&Some(info.clone())); - let total = weaveffi_kv_Store_total_count( - stores.as_ptr(), - stores.len(), - extra.as_ptr(), - extra.len(), - &mut err, - ); + let total = unsafe { + kvstore_kv_Store_total_count( + stores.as_ptr(), + stores.len(), + extra.as_ptr(), + extra.len(), + &mut err, + ) + }; assert_eq!(total, 2); drop(info); drop(many); - weaveffi_kv_Store_destroy(s); + destroy(s); } #[test] - fn cancel_token_helpers_are_reexported() { - let t = crate::weaveffi_cancel_token_create(); + fn runtime_symbols_carry_the_crate_prefix() { + assert_eq!(crate::kvstore_abi_version(), weaveffi::abi::ABI_VERSION); + assert_ne!(crate::kv::kvstore_kv_checksum(), 0); + let t = crate::kvstore_cancel_token_create(); assert!(!t.is_null()); - assert!(!crate::weaveffi_cancel_token_is_cancelled(t)); - crate::weaveffi_cancel_token_cancel(t); - assert!(crate::weaveffi_cancel_token_is_cancelled(t)); - crate::weaveffi_cancel_token_destroy(t); + unsafe { + assert!(!crate::kvstore_cancel_token_is_cancelled(t)); + crate::kvstore_cancel_token_cancel(t); + assert!(crate::kvstore_cancel_token_is_cancelled(t)); + crate::kvstore_cancel_token_destroy(t); + } } } diff --git a/samples/kvstore/weaveffi.toml b/samples/kvstore/weaveffi.toml index b0bc541f..cdfbff76 100644 --- a/samples/kvstore/weaveffi.toml +++ b/samples/kvstore/weaveffi.toml @@ -1,5 +1,11 @@ +# `weaveffi generate` (no arguments) from anywhere in this crate uses this. +[project] +input = "src/lib.rs" +out = "bindings" + +# The package name, prefix, and library come from Cargo.toml (`kvstore`); +# this table adds the published metadata. [package] -name = "kvstore" version = "1.0.0" description = "An embedded key-value store API with TTLs, iteration, and async compaction." license = "MIT" @@ -7,20 +13,5 @@ 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/Cargo.toml b/samples/shapes/Cargo.toml deleted file mode 100644 index 8f572fc6..00000000 --- a/samples/shapes/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "shapes" -version = "0.1.0" -edition.workspace = true -publish = false - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -weaveffi = { path = "../../crates/weaveffi" } - -[lints] -workspace = true diff --git a/samples/shapes/src/lib.rs b/samples/shapes/src/lib.rs deleted file mode 100644 index f3512013..00000000 --- a/samples/shapes/src/lib.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Shapes sample cdylib: exercises WeaveFFI's rich (algebraic) enums and the -//! expanded numeric type set over the stable C ABI. -//! -//! `Shape` is a sum type whose variants carry associated data, so the -//! `#[weaveffi::module]` expansion crosses it as a value buffer: an `i32` -//! tag followed by the active variant's fields, moved through one -//! `(const uint8_t*, size_t)` slot pair. The macro implements `BufferValue` -//! 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 src/lib.rs -//! --target c`). - -/// Rich-enum + numerics smoke test -#[weaveffi::module] -pub mod shapes { - /// An algebraic shape (sum type with associated data) - #[weaveffi::enumeration] - #[derive(Debug, Clone, PartialEq)] - pub enum Shape { - /// The empty shape - Empty, - /// A circle with a radius - Circle { - /// Radius in points - radius: f64, - }, - /// An axis-aligned rectangle - Rectangle { width: f32, height: f32 }, - /// A labeled shape with a small count - Labeled { label: String, count: u8 }, - } - - /// A plain C-style enum (no payloads) - #[weaveffi::enumeration] - #[repr(i32)] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Channel { - Red = 0, - Green = 1, - Blue = 2, - } - - /// Render a shape to a string - #[weaveffi::export] - pub fn describe(shape: &Shape) -> String { - match shape { - Shape::Empty => "empty".to_string(), - Shape::Circle { radius } => format!("circle(r={radius})"), - Shape::Rectangle { width, height } => format!("rectangle({width}x{height})"), - Shape::Labeled { label, count } => format!("labeled({label} x{count})"), - } - } - - /// Scale a shape by a factor, returning a new shape - #[weaveffi::export] - pub fn scale(shape: &Shape, factor: f64) -> Shape { - match shape { - Shape::Empty => Shape::Empty, - Shape::Circle { radius } => Shape::Circle { - radius: radius * factor, - }, - Shape::Rectangle { width, height } => Shape::Rectangle { - width: (f64::from(*width) * factor) as f32, - height: (f64::from(*height) * factor) as f32, - }, - Shape::Labeled { label, count } => Shape::Labeled { - label: label.clone(), - count: *count, - }, - } - } - - /// Sum a list of bytes into a wide integer (numerics smoke) - #[weaveffi::export] - pub fn sum_bytes(values: Vec) -> u64 { - values.iter().map(|b| u64::from(*b)).sum() - } -} - -weaveffi::export_runtime!(); - -#[cfg(test)] -#[allow(unsafe_code)] -mod tests { - use crate::shapes::*; - use weaveffi::abi::{self, weaveffi_error}; - - fn new_err() -> weaveffi_error { - weaveffi_error::default() - } - - /// Encode a shape into the borrowed value buffer a thunk parameter takes. - fn encode_shape(shape: &Shape) -> Vec { - abi::encode_value(shape) - } - - #[test] - fn circle_round_trips_with_tag() { - // A rich enum crosses as a value buffer: an i32 tag (declaration - // order, so Circle is 1) followed by the variant's fields. - let shape = Shape::Circle { radius: 2.5 }; - let bytes = encode_shape(&shape); - assert_eq!(bytes[..4], 1i32.to_le_bytes()); - assert_eq!(abi::decode_value::(&bytes).unwrap(), shape); - } - - #[test] - fn rectangle_round_trips_f32_fields() { - let shape = Shape::Rectangle { - width: 3.0, - height: 4.0, - }; - let bytes = encode_shape(&shape); - assert_eq!(bytes[..4], 2i32.to_le_bytes()); - assert_eq!(abi::decode_value::(&bytes).unwrap(), shape); - } - - #[test] - fn labeled_round_trips_string_and_u8() { - let shape = Shape::Labeled { - label: "hex".to_string(), - count: 6, - }; - let bytes = encode_shape(&shape); - assert_eq!(bytes[..4], 3i32.to_le_bytes()); - assert_eq!(abi::decode_value::(&bytes).unwrap(), shape); - } - - #[test] - fn empty_has_tag_zero() { - let bytes = encode_shape(&Shape::Empty); - assert_eq!(bytes, 0i32.to_le_bytes()); - assert_eq!(abi::decode_value::(&bytes).unwrap(), Shape::Empty); - } - - #[test] - fn describe_and_scale() { - let mut err = new_err(); - let circle = encode_shape(&Shape::Circle { radius: 2.0 }); - let d = weaveffi_shapes_describe(circle.as_ptr(), circle.len(), &mut err); - assert_eq!(err.code, 0); - assert_eq!(abi::c_ptr_to_string(d).unwrap(), "circle(r=2)"); - abi::free_string(d); - - // A buffered return: the thunk returns producer-owned bytes plus an - // out-length; the consumer decodes and frees them. - let mut out_len: usize = 0; - let scaled_ptr = - weaveffi_shapes_scale(circle.as_ptr(), circle.len(), 3.0, &mut out_len, &mut err); - assert_eq!(err.code, 0); - assert!(!scaled_ptr.is_null()); - let scaled_bytes = unsafe { std::slice::from_raw_parts(scaled_ptr, out_len) }; - let scaled = abi::decode_value::(scaled_bytes).unwrap(); - abi::free_bytes(scaled_ptr as *mut u8, out_len); - assert_eq!(scaled, Shape::Circle { radius: 6.0 }); - } - - #[test] - fn sum_bytes_widens_to_u64() { - let mut err = new_err(); - let data: [u8; 4] = [250, 250, 250, 250]; - let total = weaveffi_shapes_sum_bytes(data.as_ptr(), data.len(), &mut err); - assert_eq!(total, 1000); - assert_eq!(err.code, 0); - } -} diff --git a/samples/shapes/weaveffi.toml b/samples/shapes/weaveffi.toml deleted file mode 100644 index 27e6e9c0..00000000 --- a/samples/shapes/weaveffi.toml +++ /dev/null @@ -1,21 +0,0 @@ -[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/scripts/check-fixtures.sh b/scripts/check-fixtures.sh new file mode 100755 index 00000000..7bfd7e70 --- /dev/null +++ b/scripts/check-fixtures.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Compile-check the generated bindings for every snapshot fixture with each +# target language's own toolchain. +# +# Snapshot tests pin the generated text; this catches text that is not valid +# code (a reserved word left unescaped, a shadowed standard-library name, a +# missing import) in fixtures no conformance sample exercises. +# +# Usage: scripts/check-fixtures.sh [target ...] (default: every target) +# +# For each target it generates crates/weaveffi-cli/tests/fixtures/*.yml into a +# scratch directory and runs scripts/fixtures/.sh , +# which must exit non-zero on any compiler or type-checker error. +set -uo pipefail +ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$ROOT" +TARGETS=${*:-c cpp swift kotlin node wasm python dotnet dart go ruby} +cargo build -q -p weaveffi-cli || exit 1 +TARGET_DIR=$(cargo metadata --format-version 1 --no-deps 2>/dev/null \ + | tr ',' '\n' | grep '"target_directory"' | head -1 \ + | sed 's/.*"target_directory":"//; s/"$//') +WEAVEFFI="${TARGET_DIR:-$ROOT/target}/debug/weaveffi" +SCRATCH=$(mktemp -d) +trap 'rm -rf "$SCRATCH"' EXIT +FAILED="" +for fixture in crates/weaveffi-cli/tests/fixtures/*.yml; do + name=$(basename "$fixture" .yml) + for t in $TARGETS; do + out="$SCRATCH/$name" + "$WEAVEFFI" generate "$fixture" -o "$out" --target "c,$t" --force >/dev/null || { + FAILED="$FAILED $t:$name(generate)" + continue + } + echo "==> $t: $name" + if ! bash "scripts/fixtures/$t.sh" "$out"; then + FAILED="$FAILED $t:$name" + fi + done +done +if [ -n "$FAILED" ]; then + echo "fixture check failed:$FAILED" >&2 + exit 1 +fi +echo "fixture check passed" diff --git a/scripts/check-summary.sh b/scripts/check-summary.sh deleted file mode 100755 index 7b3310f7..00000000 --- a/scripts/check-summary.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# Verify every `[label](path.md)` link in `docs/src/SUMMARY.md` resolves to -# an existing file under `docs/src/`. Mirrors the contract `mdbook` enforces -# at build time, but runs in <100 ms without a toolchain so the release -# pipeline can guard against typos in seconds. -# -# Usage: -# scripts/check-summary.sh -# -# Exits non-zero with one line per missing path on failure. -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -SUMMARY="$REPO_ROOT/docs/src/SUMMARY.md" -SRC_ROOT="$REPO_ROOT/docs/src" - -if [ ! -f "$SUMMARY" ]; then - echo "ERROR: $SUMMARY not found." >&2 - exit 1 -fi - -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$SRC_ROOT/$path" ]; then - echo "MISSING: docs/src/$path" - missing=$((missing + 1)) - fi -done < <(grep -oE '\]\([^)]+\.md\)' "$SUMMARY" | sed -E 's/^\]\(([^)]+)\)$/\1/') - -if [ "$missing" -gt 0 ]; then - echo "ERROR: $missing path(s) referenced by SUMMARY.md are missing." >&2 - exit 1 -fi - -echo "All SUMMARY.md paths exist." diff --git a/scripts/fixtures/c.sh b/scripts/fixtures/c.sh new file mode 100755 index 00000000..19f8cc18 --- /dev/null +++ b/scripts/fixtures/c.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Compile every generated C header (the ABI header and, when present, the +# value-buffer helper header) as C11 and C++17 with warnings as errors. +set -euo pipefail +dir=$1 +for header in "$dir"/c/*.h; do + name=$(basename "$header" .h) + src="$dir/c_check_$name" + printf '#include "%s.h"\nint main(void) { return 0; }\n' "$name" > "$src.c" + cc -std=c11 -Wall -Wextra -Wpedantic -Werror -fsyntax-only -I "$dir/c" "$src.c" + cp "$src.c" "$src.cpp" + c++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -fsyntax-only -I "$dir/c" "$src.cpp" +done diff --git a/scripts/fixtures/cpp.sh b/scripts/fixtures/cpp.sh new file mode 100644 index 00000000..3eaf181f --- /dev/null +++ b/scripts/fixtures/cpp.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Compile the generated C++ wrapper header, together with the C header it +# ships, as C++17 with warnings as errors. The translation unit includes the +# wrapper and then the C header directly, so including both must compile. +set -euo pipefail +dir=$1 +hpp=$(ls "$dir"/cpp/*.hpp | head -1) +h=$(ls "$dir"/cpp/*.h | head -1) +printf '#include "%s"\n#include "%s"\nint main() { return 0; }\n' "$hpp" "$h" > "$dir/cpp_check.cpp" +"${CXX:-clang++}" -std=c++17 -Wall -Wextra -Wpedantic -Werror -fsyntax-only "$dir/cpp_check.cpp" diff --git a/scripts/fixtures/dart.sh b/scripts/fixtures/dart.sh new file mode 100644 index 00000000..07724587 --- /dev/null +++ b/scripts/fixtures/dart.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Type-check the generated Dart package with the analyzer, infos and +# warnings fatal. Resolving the `ffi` dependency uses the pub cache when it +# can and the network otherwise. +set -euo pipefail +dir=$1 +cd "$dir/dart" +dart pub get --offline >/dev/null 2>&1 || dart pub get >/dev/null +dart analyze --fatal-infos --fatal-warnings . diff --git a/scripts/fixtures/dotnet.sh b/scripts/fixtures/dotnet.sh new file mode 100644 index 00000000..99aa9b35 --- /dev/null +++ b/scripts/fixtures/dotnet.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Build the generated .NET project with warnings as errors. The project +# targets net8.0; restore needs that reference pack (cached by any earlier +# net8.0 restore, or downloaded from NuGet). +set -euo pipefail +dir=$1 +csproj=$(ls "$dir"/dotnet/*.csproj | head -1) +dotnet build "$csproj" -warnaserror -nologo -v quiet \ + -p:BaseOutputPath="$dir/dotnet-bin/" -p:BaseIntermediateOutputPath="$dir/dotnet-obj/" diff --git a/scripts/fixtures/go.sh b/scripts/fixtures/go.sh new file mode 100755 index 00000000..f1c2f067 --- /dev/null +++ b/scripts/fixtures/go.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Check the generated Go module: gofmt-clean, `go vet`, and `go build` with +# C warnings as errors. No producer exists for a fixture, so the module links +# against an empty stand-in library with unresolved symbols allowed. +set -euo pipefail +dir=$1 +mod="$dir/go" +header=$(ls "$mod"/*.h | head -1) +library=$(basename "$header" .h) + +stub="$dir/go-stub" +mkdir -p "$stub" +echo 'void go_fixture_stub(void) {}' > "$stub/stub.c" +case "$(uname -s)" in + Darwin) + cc -dynamiclib -o "$stub/lib$library.dylib" "$stub/stub.c" + undefined="-Wl,-undefined,dynamic_lookup" + ;; + *) + cc -shared -fPIC -o "$stub/lib$library.so" "$stub/stub.c" + undefined="-Wl,--unresolved-symbols=ignore-all" + ;; +esac + +unformatted=$(gofmt -l "$mod") +if [ -n "$unformatted" ]; then + echo "not gofmt-clean: $unformatted" >&2 + exit 1 +fi + +cd "$mod" +export GOFLAGS=-mod=mod GOPROXY=off GOSUMDB=off +export CGO_CFLAGS="-Wall -Werror" +export CGO_LDFLAGS="-L$stub $undefined" +go vet ./... +go build ./... diff --git a/scripts/fixtures/js.sh b/scripts/fixtures/js.sh new file mode 100644 index 00000000..162e05bb --- /dev/null +++ b/scripts/fixtures/js.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Shared by node.sh and wasm.sh: `js_check ...` runs +# `tsc --noEmit --strict` on /index.d.ts and `node --check` on each +# module. Without a `tsc` on PATH, TypeScript is installed once into +# ${WEAVEFFI_TSC_DIR:-$TMPDIR/weaveffi-typescript} (this needs the network). + +tsc_bin() { + if command -v tsc >/dev/null 2>&1; then + command -v tsc + return + fi + local cache=${WEAVEFFI_TSC_DIR:-${TMPDIR:-/tmp}/weaveffi-typescript} + if [ ! -x "$cache/node_modules/.bin/tsc" ]; then + mkdir -p "$cache" + npm install --prefix "$cache" --no-audit --no-fund --loglevel=error typescript >&2 + fi + echo "$cache/node_modules/.bin/tsc" +} + +js_check() { + local dir=$1 + shift + local tsc + tsc=$(tsc_bin) + "$tsc" --noEmit --strict --target es2022 --module es2022 \ + --lib es2022,dom,esnext.disposable --types "" "$dir/index.d.ts" + for f in "$@"; do + node --check "$dir/$f" + done +} diff --git a/scripts/fixtures/kotlin.sh b/scripts/fixtures/kotlin.sh new file mode 100755 index 00000000..7755c989 --- /dev/null +++ b/scripts/fixtures/kotlin.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Compile the generated Kotlin sources with kotlinc (warnings as errors) and +# syntax-check the generated JNI shim against the JDK's JNI headers with +# warnings as errors. +set -euo pipefail +dir=$1 +kt="$dir/kotlin" + +java_home=${JAVA_HOME:-$(/usr/libexec/java_home 2>/dev/null || true)} +if [ -z "$java_home" ] || [ ! -f "$java_home/include/jni.h" ]; then + javabin=$(readlink -f "$(command -v java)" 2>/dev/null || true) + [ -n "$javabin" ] && java_home=$(dirname "$(dirname "$javabin")") +fi +[ -f "$java_home/include/jni.h" ] || { echo "jni.h not found (set JAVA_HOME)" >&2; exit 1; } +case "$(uname -s)" in + Darwin) jni_os="$java_home/include/darwin" ;; + *) jni_os="$java_home/include/linux" ;; +esac +for c in "$kt"/src/main/cpp/*.c; do + cc -std=c11 -Wall -Wextra -Werror -fsyntax-only -I"$java_home/include" -I"$jni_os" "$c" +done + +# The coroutines runtime ships inside the kotlinc distribution. +kotlinc_bin=$(readlink -f "$(command -v kotlinc)" 2>/dev/null || command -v kotlinc) +coroutines=$(ls "$(dirname "$kotlinc_bin")"/../libexec/lib/kotlinx-coroutines-core-jvm.jar \ + "$(dirname "$kotlinc_bin")"/../lib/kotlinx-coroutines-core-jvm.jar 2>/dev/null | head -1 || true) +[ -n "$coroutines" ] || { echo "kotlinx-coroutines-core-jvm.jar not found" >&2; exit 1; } +rm -rf "$dir/kotlin_classes" +find "$kt/src/main/kotlin" -name '*.kt' > "$dir/kotlin_sources.txt" +kotlinc -Werror -cp "$coroutines" -d "$dir/kotlin_classes" @"$dir/kotlin_sources.txt" diff --git a/scripts/fixtures/node.sh b/scripts/fixtures/node.sh new file mode 100644 index 00000000..38e68aae --- /dev/null +++ b/scripts/fixtures/node.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Check the generated Node.js package: type-check index.d.ts with +# `tsc --strict`, parse every ES module with `node --check`, and compile the +# N-API addon with warnings as errors against the cached node-gyp headers +# (skipped, with a note, when no headers are cached). +set -euo pipefail +dir=$1/node +here=$(cd "$(dirname "$0")" && pwd) +. "$here/js.sh" + +js_check "$dir" index.js runtime.js + +headers="" +for base in "$HOME/Library/Caches/node-gyp" "$HOME/.cache/node-gyp" "${LOCALAPPDATA:-/nonexistent}/node-gyp/Cache"; do + candidate="$base/$(node -p 'process.versions.node')/include/node" + if [ -f "$candidate/node_api.h" ]; then + headers=$candidate + break + fi +done +if [ -z "$headers" ]; then + echo "note: no node-gyp headers cached; skipping the addon compile check" >&2 + exit 0 +fi +for c in "$dir"/*_node.c; do + cc -std=c11 -Wall -Wextra -Werror -fsyntax-only -I "$headers" -I "$dir" "$c" +done diff --git a/scripts/fixtures/python.sh b/scripts/fixtures/python.sh new file mode 100755 index 00000000..c49ba30f --- /dev/null +++ b/scripts/fixtures/python.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Byte-compile every generated Python file, then type-check the package with +# mypy when it is available: the `.pyi` stub under --strict and the +# implementation module itself (which catches undefined names and bad calls +# into the runtime). Without mypy, only the byte-compile runs. +set -euo pipefail +dir=$1 +find "$dir/python" -name '*.py' -print0 | xargs -0 python3 -m py_compile +if command -v mypy >/dev/null 2>&1; then + mypy=(mypy) +elif python3 -c "import mypy" >/dev/null 2>&1; then + mypy=(python3 -m mypy) +else + echo "note: mypy not found; type checks skipped (pip install mypy)" + exit 0 +fi +for pkg in "$dir"/python/*/; do + name=$(basename "$pkg") + "${mypy[@]}" --strict --python-version 3.9 --no-incremental "$pkg$name.pyi" + "${mypy[@]}" --python-version 3.9 --no-incremental "$pkg$name.py" +done diff --git a/scripts/fixtures/ruby.sh b/scripts/fixtures/ruby.sh new file mode 100644 index 00000000..2a417f0e --- /dev/null +++ b/scripts/fixtures/ruby.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Syntax-check every generated Ruby file with warnings enabled (any warning +# fails), then build the gem from its generated gemspec. +set -euo pipefail +dir=$(cd "$1" && pwd) +status=0 +while IFS= read -r file; do + if ! out=$(ruby -wc "$file" 2>&1 >/dev/null) || [ -n "$out" ]; then + echo "$file:" >&2 + echo "$out" >&2 + status=1 + fi +done < <(find "$dir/ruby" -name '*.rb' -o -name '*.gemspec') +spec=$(cd "$dir/ruby" && ls ./*.gemspec) +(cd "$dir/ruby" && gem build --silent "$spec" -o "$dir/ruby-fixture.gem") || status=1 +exit $status diff --git a/scripts/fixtures/swift.sh b/scripts/fixtures/swift.sh new file mode 100644 index 00000000..d56e653e --- /dev/null +++ b/scripts/fixtures/swift.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Type-check the generated Swift package: the manifest must load, and the +# wrapper sources must compile against the bundled C module map with +# warnings as errors, in both the Swift 5 and Swift 6 language modes. +set -euo pipefail +dir=$1 +pkg="$dir/swift" +cdir=$(ls -d "$pkg"/Sources/C*/ | head -1) +cmod=$(basename "$cdir") +mod=${cmod#C} +swift package dump-package --package-path "$pkg" >/dev/null +for version in 5 6; do + swiftc -typecheck -warnings-as-errors -swift-version "$version" \ + -module-name "$mod" \ + -Xcc -fmodule-map-file="$cdir/module.modulemap" \ + "$pkg/Sources/$mod"/*.swift +done diff --git a/scripts/fixtures/wasm.sh b/scripts/fixtures/wasm.sh new file mode 100644 index 00000000..0ef74e78 --- /dev/null +++ b/scripts/fixtures/wasm.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Check the generated WebAssembly package: type-check index.d.ts with +# `tsc --strict` and parse every ES module with `node --check`. +set -euo pipefail +dir=$1/wasm +here=$(cd "$(dirname "$0")" && pwd) +. "$here/js.sh" + +js_check "$dir" index.js runtime.js linear.js diff --git a/scripts/publish-crates.sh b/scripts/publish-crates.sh deleted file mode 100755 index 1276924f..00000000 --- a/scripts/publish-crates.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Publish order matters: dependencies must be available on crates.io -# before dependents can be published. -CRATES=( - weaveffi-ir - weaveffi-abi - weaveffi-core - # Producer-macro crates. weaveffi-bridge must precede weaveffi-macros and - # weaveffi-cli, which both depend on it; the weaveffi facade re-exports - # weaveffi-macros, so it follows them. - weaveffi-bridge - weaveffi-macros - weaveffi - weaveffi-gen-c - weaveffi-gen-swift - weaveffi-gen-kotlin - weaveffi-gen-node - weaveffi-gen-wasm - weaveffi-gen-python - weaveffi-gen-dotnet - weaveffi-gen-cpp - weaveffi-gen-dart - weaveffi-gen-go - weaveffi-gen-ruby - weaveffi-cli -) - -MAX_RETRIES=5 -RETRY_WAIT=180 -# Transient network/CDN errors recover quickly, so back off briefly rather -# than the multi-minute wait used for rate limits and index propagation. -NET_RETRY_WAIT=20 -# Sparse-index polling: crates.io can lag minutes behind a successful -# upload (cargo's own post-publish wait has been observed to time out -# under backlog), and a dependent crate cannot be published until its -# dependency is resolvable in the index. -INDEX_POLL_INTERVAL=10 -INDEX_POLL_ATTEMPTS=90 # 90 x 10s = 15 minutes - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -cd "$REPO_ROOT" - -# Harden cargo's network layer against the transient crates.io CDN failures -# that have aborted releases mid-publish (e.g. "[16] Error in the HTTP2 framing -# layer" while fetching a dependency during `cargo publish`'s verify step): -# forcing HTTP/1.1 sidesteps the intermittent HTTP/2 framing bug, and -# CARGO_NET_RETRY lets cargo retry dropped downloads before this script does. -export CARGO_NET_RETRY="${CARGO_NET_RETRY:-10}" -export CARGO_HTTP_MULTIPLEXING="${CARGO_HTTP_MULTIPLEXING:-false}" - -# Path of a crate in the sparse index (https://index.crates.io), per -# https://doc.rust-lang.org/cargo/reference/registry-index.html#index-files -index_path() { - local name="$1" - local len=${#name} - if [ "$len" -eq 1 ]; then - echo "1/$name" - elif [ "$len" -eq 2 ]; then - echo "2/$name" - elif [ "$len" -eq 3 ]; then - echo "3/${name:0:1}/$name" - else - echo "${name:0:2}/${name:2:2}/$name" - fi -} - -# Block until $1@$2 is resolvable in the sparse index (or time out). -wait_for_index() { - local crate="$1" version="$2" - local url="https://index.crates.io/$(index_path "$crate")" - local attempt=1 - while [ "$attempt" -le "$INDEX_POLL_ATTEMPTS" ]; do - if curl -fsSL "$url" 2>/dev/null | grep -q "\"vers\":\"$version\""; then - echo "$crate v$version is available in the index." - return 0 - fi - echo "Waiting for $crate v$version to appear in the index (attempt $attempt/$INDEX_POLL_ATTEMPTS)..." - sleep "$INDEX_POLL_INTERVAL" - attempt=$((attempt + 1)) - done - echo "ERROR: $crate v$version did not appear in the index within $((INDEX_POLL_INTERVAL * INDEX_POLL_ATTEMPTS))s." - return 1 -} - -# Version of a workspace crate (versions are lockstep, but read per-crate -# to stay correct if that ever changes). -crate_version() { - cargo pkgid -p "$1" | sed -E 's/.*[#@]//' -} - -for crate in "${CRATES[@]}"; do - version="$(crate_version "$crate")" - echo "Publishing $crate v$version..." - - attempt=0 - while true; do - output=$(cargo publish -p "$crate" 2>&1) && { - echo "$output" - echo "$crate published successfully." - break - } - - if echo "$output" | grep -q "already uploaded\|already exists"; then - echo "$crate already published at this version, skipping." - break - fi - - attempt=$((attempt + 1)) - if echo "$output" | grep -q "429\|Too Many Requests\|rate limit"; then - if [ "$attempt" -ge "$MAX_RETRIES" ]; then - echo "ERROR: $crate hit rate limit after $MAX_RETRIES retries." - echo "$output" - exit 1 - fi - echo "$crate hit rate limit, waiting ${RETRY_WAIT}s (attempt $attempt/$MAX_RETRIES)..." - sleep "$RETRY_WAIT" - continue - fi - - # A dependency uploaded moments ago may not be resolvable yet - # (index propagation lag); retry instead of aborting the release. - if echo "$output" | grep -q "failed to select a version for the requirement"; then - if [ "$attempt" -ge "$MAX_RETRIES" ]; then - echo "ERROR: $crate still cannot resolve its dependencies after $MAX_RETRIES retries." - echo "$output" - exit 1 - fi - echo "$crate cannot resolve a just-published dependency yet, waiting ${RETRY_WAIT}s (attempt $attempt/$MAX_RETRIES)..." - sleep "$RETRY_WAIT" - continue - fi - - # Transient crates.io / CDN / registry errors (HTTP/2 framing glitches, - # dropped downloads, registry-update hiccups). These are non-deterministic - # and almost always succeed on retry, so back off and retry rather than - # aborting a release that has already published earlier crates. - if echo "$output" | grep -qiE "error in the http2 framing layer|curl failed|spurious network error|unable to update registry|download of .+ failed|failed to get .+ as a dependency|(operation )?timed out|connection (reset|closed|refused)|\b(502|503|504)\b"; then - if [ "$attempt" -ge "$MAX_RETRIES" ]; then - echo "ERROR: $crate failed to publish after $MAX_RETRIES retries (transient network/registry error)." - echo "$output" - exit 1 - fi - echo "$crate hit a transient network/registry error, waiting ${NET_RETRY_WAIT}s (attempt $attempt/$MAX_RETRIES)..." - sleep "$NET_RETRY_WAIT" - continue - fi - - echo "ERROR: failed to publish $crate" - echo "$output" - exit 1 - done - - # Whether freshly published or already uploaded, do not move on to a - # dependent crate until this one is actually resolvable. - wait_for_index "$crate" "$version" -done - -echo "All crates published successfully." diff --git a/scripts/update-cargo-versions.sh b/scripts/update-cargo-versions.sh deleted file mode 100755 index ca431217..00000000 --- a/scripts/update-cargo-versions.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -VERSION="$1" -if [ -z "$VERSION" ]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" - -for toml in "$REPO_ROOT"/crates/*/Cargo.toml; do - crate="$(basename "$(dirname "$toml")")" - # Update package version - sed "s/^version = \"[^\"]*\"/version = \"$VERSION\"/" "$toml" > "$toml.tmp" && mv "$toml.tmp" "$toml" - # Update inter-crate dependency versions (lines containing path = ".*weaveffi") - sed '/path = ".*weaveffi/s/version = "[^"]*"/version = "'"$VERSION"'"/' "$toml" > "$toml.tmp" && mv "$toml.tmp" "$toml" - echo " $crate -> $VERSION" -done - -cd "$REPO_ROOT" -cargo generate-lockfile 2>/dev/null -echo "Updated Cargo.lock" -echo "All publishable crates set to v$VERSION" diff --git a/weaveffi.schema.json b/weaveffi.schema.json index dca5f81d..b389c4f3 100644 --- a/weaveffi.schema.json +++ b/weaveffi.schema.json @@ -9,7 +9,7 @@ ], "properties": { "version": { - "description": "IR schema version this document targets (for example `0.9.0`). Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].", + "description": "IR schema version this document targets (for example `0.10.0`). Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].", "type": "string" }, "modules": {